blob: dd1002a8f9317ee68c10fbc137f9c7a015c56463 [file]
// Copyright 2024 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
#include "iree/compiler/Dialect/HAL/IR/HALDialect.h"
#include "iree/compiler/Dialect/HAL/IR/HALOps.h"
#include "iree/compiler/Dialect/HAL/IR/HALTypes.h"
#include "iree/compiler/Utils/StringUtils.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/MemoryBuffer.h"
#include "llvm/Support/Path.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Parser/Parser.h"
// clang-format off: must be included after all LLVM/MLIR headers.
#define GET_ATTRDEF_CLASSES
#include "iree/compiler/Dialect/HAL/IR/HALAttrs.cpp.inc" // IWYU pragma: keep
#include "iree/compiler/Dialect/HAL/IR/HALEnums.cpp.inc" // IWYU pragma: keep
// clang-format on
namespace mlir::iree_compiler::IREE::HAL {
//===----------------------------------------------------------------------===//
// Utilities
//===----------------------------------------------------------------------===//
template <typename AttrType>
static LogicalResult parseEnumAttr(AsmParser &parser, StringRef attrName,
AttrType &attr) {
Attribute genericAttr;
auto loc = parser.getCurrentLocation();
if (failed(parser.parseAttribute(genericAttr,
parser.getBuilder().getNoneType()))) {
return parser.emitError(loc)
<< "failed to parse '" << attrName << "' enum string value";
}
auto stringAttr = llvm::dyn_cast<StringAttr>(genericAttr);
if (!stringAttr) {
return parser.emitError(loc)
<< "expected " << attrName << " attribute specified as string";
}
auto symbolized =
symbolizeEnum<typename AttrType::ValueType>(stringAttr.getValue());
if (!symbolized.hasValue()) {
return parser.emitError(loc)
<< "failed to parse '" << attrName << "' enum value";
}
attr = AttrType::get(parser.getBuilder().getContext(), symbolized.getValue());
return success();
}
template <typename AttrType>
static LogicalResult parseOptionalEnumAttr(AsmParser &parser,
StringRef attrName, AttrType &attr) {
if (succeeded(parser.parseOptionalQuestion())) {
// Special case `?` to indicate any/none/undefined/etc.
attr = AttrType::get(parser.getBuilder().getContext(), 0);
return success();
}
return parseEnumAttr<AttrType>(parser, attrName, attr);
}
//===----------------------------------------------------------------------===//
// #hal.collective<*>
//===----------------------------------------------------------------------===//
// See the iree/hal/command_buffer.h iree_hal_collective_op_t for details.
uint32_t CollectiveAttr::getEncodedValue() const {
union {
uint32_t packed; // packed value
struct {
uint8_t kind;
uint8_t reduction;
uint8_t elementType;
uint8_t reserved;
};
} value = {0};
value.kind = static_cast<uint8_t>(getKind());
value.reduction = static_cast<uint8_t>(
getReduction().value_or(CollectiveReductionOp::None));
value.elementType = static_cast<uint8_t>(getElementType());
return value.packed;
}
//===----------------------------------------------------------------------===//
// #hal.pipeline.layout<*>
//===----------------------------------------------------------------------===//
PipelineBindingAttr PipelineLayoutAttr::getBinding(int64_t ordinal) const {
assert(ordinal >= 0 && ordinal < getBindings().size() &&
"binding ordinal out of bounds");
return getBindings()[ordinal];
}
//===----------------------------------------------------------------------===//
// #hal.executable.target<*>
//===----------------------------------------------------------------------===//
// static
ExecutableTargetAttr ExecutableTargetAttr::get(MLIRContext *context,
StringRef backend,
StringRef format) {
return get(context, StringAttr::get(context, backend),
StringAttr::get(context, format), DictionaryAttr::get(context));
}
// static
Attribute ExecutableTargetAttr::parse(AsmParser &p, Type type) {
StringAttr backendAttr;
StringAttr formatAttr;
DictionaryAttr configurationAttr;
// `<"backend", "format"`
if (failed(p.parseLess()) || failed(p.parseAttribute(backendAttr)) ||
failed(p.parseComma()) || failed(p.parseAttribute(formatAttr))) {
return {};
}
// `, {config}`
if (succeeded(p.parseOptionalComma()) &&
failed(p.parseAttribute(configurationAttr))) {
return {};
}
// `>`
if (failed(p.parseGreater())) {
return {};
}
return get(p.getContext(), backendAttr, formatAttr, configurationAttr);
}
void ExecutableTargetAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<";
p.printAttribute(getBackend());
os << ", ";
p.printAttribute(getFormat());
auto config = getConfiguration();
if (config && !config.empty()) {
os << ", ";
p.printAttribute(config);
}
os << ">";
}
std::string ExecutableTargetAttr::getSymbolNameFragment() const {
return sanitizeSymbolName(getFormat().getValue().lower());
}
bool ExecutableTargetAttr::hasConfigurationAttr(StringRef name) {
auto configAttr = getConfiguration();
return configAttr && configAttr.get(name);
}
// For now this is very simple: if there are any specified fields that are
// present in this attribute they must match. We could allow target backends
// to customize this via attribute interfaces in the future if we needed.
bool ExecutableTargetAttr::isGenericOf(
IREE::HAL::ExecutableTargetAttr specificAttr) {
if (getBackend() != specificAttr.getBackend() ||
getFormat() != specificAttr.getFormat()) {
// Totally different backends and binary formats.
// There may be cases where we want to share things - such as when targeting
// both DLLs and dylibs or something - but today almost all of these are
// unique situations.
return false;
}
// If the config is empty on either we can quickly match.
// This is the most common case for users manually specifying targets.
auto genericConfigAttr = getConfiguration();
auto specificConfigAttr = specificAttr.getConfiguration();
if (!genericConfigAttr || !specificConfigAttr)
return true;
// Ensure all fields in specificConfigAttr either don't exist or match.
for (auto expectedAttr : specificConfigAttr.getValue()) {
auto actualValue = genericConfigAttr.getNamed(expectedAttr.getName());
if (!actualValue) {
continue; // ignore, not present in generic
}
if (actualValue->getValue() != expectedAttr.getValue()) {
return false; // mismatch, both have values but they differ
}
}
// Ensure all fields in genericConfigAttr exist in the specific one.
// If missing then the generic is _more_ specific and can't match.
for (auto actualAttr : genericConfigAttr.getValue()) {
if (!specificConfigAttr.getNamed(actualAttr.getName())) {
return false; // mismatch, present in generic but not specific
}
}
// All fields match or are omitted in the generic version.
return true;
}
// static
ExecutableTargetAttr ExecutableTargetAttr::lookup(Operation *op) {
auto *context = op->getContext();
auto attrId = StringAttr::get(context, "hal.executable.target");
while (op) {
// Take directly from the enclosing variant.
if (auto variantOp = llvm::dyn_cast<IREE::HAL::ExecutableVariantOp>(op)) {
return variantOp.getTarget();
}
// Use an override if specified.
auto attr = op->getAttrOfType<IREE::HAL::ExecutableTargetAttr>(attrId);
if (attr)
return attr;
// Continue walk.
op = op->getParentOp();
}
// No target found during walk. No default to provide so fail and let the
// caller decide what to do (assert/fallback/etc).
return nullptr;
}
//===----------------------------------------------------------------------===//
// #hal.executable.object<*>
//===----------------------------------------------------------------------===//
// static
Attribute ExecutableObjectAttr::parse(AsmParser &p, Type type) {
NamedAttrList dict;
// `<{` dict `}>`
if (failed(p.parseLess()) || failed(p.parseOptionalAttrDict(dict)) ||
failed(p.parseGreater())) {
return {};
}
auto pathAttr = llvm::dyn_cast_if_present<StringAttr>(dict.get("path"));
auto dataAttr =
llvm::dyn_cast_if_present<IREE::Util::SerializableAttrInterface>(
dict.get("data"));
return get(p.getContext(), pathAttr, dataAttr);
}
void ExecutableObjectAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<{";
if (auto pathAttr = getPath()) {
os << "path = ";
p.printAttribute(getPath());
}
if (auto dataAttr = getData()) {
os << ", data = ";
p.printAttribute(getData());
}
os << "}>";
}
// static
void ExecutableObjectAttr::filterObjects(
ArrayAttr objectAttrs, ArrayRef<StringRef> extensions,
SmallVectorImpl<IREE::HAL::ExecutableObjectAttr> &filteredAttrs) {
if (!objectAttrs)
return;
for (auto objectAttr :
objectAttrs.getAsRange<IREE::HAL::ExecutableObjectAttr>()) {
auto path = objectAttr.getPath();
auto ext = llvm::sys::path::extension(path);
if (llvm::is_contained(extensions, ext)) {
filteredAttrs.push_back(objectAttr);
}
}
}
// Tries to find |filePath| on disk either at its absolute path or joined with
// any of the specified |searchPaths| in order.
// Returns the absolute file path when found or a failure if there are no hits.
static FailureOr<std::string>
findFileInPaths(StringRef filePath, ArrayRef<std::string> searchPaths) {
// First try to see if it's an absolute path - we don't want to perform any
// additional processing on top of that.
if (llvm::sys::path::is_absolute(filePath)) {
if (llvm::sys::fs::exists(filePath))
return filePath.str();
return failure();
}
// Try a relative lookup from the current working directory.
if (llvm::sys::fs::exists(filePath))
return filePath.str();
// Search each path in turn for a file that exists.
// It doesn't mean we can open it but we'll get a better error out of the
// actual open attempt than what we could produce here.
for (auto searchPath : searchPaths) {
SmallVector<char> tryPath{searchPath.begin(), searchPath.end()};
llvm::sys::path::append(tryPath, filePath);
if (llvm::sys::fs::exists(Twine(tryPath)))
return Twine(tryPath).str();
}
// Not found in either the user-specified absolute path, cwd, or the search
// paths.
return failure();
}
static llvm::cl::list<std::string> clExecutableObjectSearchPath(
"iree-hal-executable-object-search-path",
llvm::cl::desc("Additional search paths for resolving "
"#hal.executable.object file references."),
llvm::cl::ZeroOrMore);
FailureOr<std::string> ExecutableObjectAttr::getAbsolutePath() {
auto pathAttr = getPath();
if (!pathAttr)
return failure(); // not a file reference
return findFileInPaths(pathAttr.getValue(), clExecutableObjectSearchPath);
}
std::optional<std::string> ExecutableObjectAttr::loadData() {
if (auto dataAttr = getData()) {
std::string buffer;
buffer.resize(dataAttr.getStorageSize());
if (failed(dataAttr.serializeToBuffer(
UnknownLoc::get(dataAttr.getContext()), llvm::endianness::native,
ArrayRef(buffer.data(), buffer.size())))) {
return std::nullopt;
}
return buffer;
} else if (auto pathAttr = getPath()) {
// Search for file and try to load it if found.
auto filePath =
findFileInPaths(pathAttr.getValue(), clExecutableObjectSearchPath);
if (failed(filePath)) {
llvm::errs()
<< "ERROR: referenced object file not found on any path; use "
"--iree-hal-executable-object-search-path= to add search paths: "
<< *this << "\n";
return std::nullopt;
}
auto file = llvm::MemoryBuffer::getFile(*filePath);
if (!file)
return std::nullopt;
return std::string((*file)->getBuffer());
}
return std::nullopt;
}
//===----------------------------------------------------------------------===//
// #hal.executable.objects<*>
//===----------------------------------------------------------------------===//
// static
LogicalResult ExecutableObjectsAttr::verify(
function_ref<mlir::InFlightDiagnostic()> emitError, ArrayAttr targetsAttr,
ArrayAttr targetObjectsAttr) {
if (targetsAttr.size() != targetObjectsAttr.size()) {
return emitError() << "targets and objects must be 1:1";
}
for (auto targetAttr : targetsAttr) {
if (!llvm::isa<IREE::HAL::ExecutableTargetAttr>(targetAttr)) {
return emitError()
<< "target keys must be #hal.executable.target attributes";
}
}
for (auto objectsAttr : targetObjectsAttr) {
auto objectsArrayAttr = llvm::dyn_cast<ArrayAttr>(objectsAttr);
if (!objectsArrayAttr) {
return emitError() << "target objects must be an array of "
"#hal.executable.object attributes";
}
}
return success();
}
// static
Attribute ExecutableObjectsAttr::parse(AsmParser &p, Type type) {
// `<{` target = [objects, ...], ... `}>`
SmallVector<Attribute> targetAttrs;
SmallVector<Attribute> objectsAttrs;
if (failed(p.parseLess()))
return {};
if (succeeded(p.parseLBrace()) && !succeeded(p.parseOptionalRBrace())) {
do {
Attribute targetAttr;
ArrayAttr objectsAttr;
if (failed(p.parseAttribute(targetAttr)) || failed(p.parseEqual()) ||
failed(p.parseAttribute(objectsAttr))) {
return {};
}
targetAttrs.push_back(targetAttr);
objectsAttrs.push_back(objectsAttr);
} while (succeeded(p.parseOptionalComma()));
if (failed(p.parseRBrace()))
return {};
}
if (failed(p.parseGreater()))
return {};
return get(p.getContext(), ArrayAttr::get(p.getContext(), targetAttrs),
ArrayAttr::get(p.getContext(), objectsAttrs));
}
void ExecutableObjectsAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<{";
llvm::interleaveComma(llvm::zip_equal(getTargets(), getTargetObjects()), os,
[&](std::tuple<Attribute, Attribute> keyValue) {
p.printAttribute(std::get<0>(keyValue));
os << " = ";
p.printAttributeWithoutType(std::get<1>(keyValue));
});
os << "}>";
}
std::optional<ArrayAttr> ExecutableObjectsAttr::getApplicableObjects(
IREE::HAL::ExecutableTargetAttr specificTargetAttr) {
SmallVector<Attribute> allObjectAttrs;
for (auto [targetAttr, objectsAttr] :
llvm::zip_equal(getTargets(), getTargetObjects())) {
auto genericTargetAttr =
llvm::cast<IREE::HAL::ExecutableTargetAttr>(targetAttr);
if (genericTargetAttr.isGenericOf(specificTargetAttr)) {
auto objectsArrayAttr = llvm::cast<ArrayAttr>(objectsAttr);
allObjectAttrs.append(objectsArrayAttr.begin(), objectsArrayAttr.end());
}
}
if (allObjectAttrs.empty())
return std::nullopt;
return ArrayAttr::get(specificTargetAttr.getContext(), allObjectAttrs);
}
//===----------------------------------------------------------------------===//
// #hal.device.alias<*>
//===----------------------------------------------------------------------===//
// static
DeviceAliasAttr DeviceAliasAttr::get(MLIRContext *context, StringRef deviceID) {
return get(context, IREE::HAL::DeviceType::get(context),
StringAttr::get(context, deviceID), std::nullopt,
DictionaryAttr::get(context));
}
//===----------------------------------------------------------------------===//
// #hal.device.target<*>
//===----------------------------------------------------------------------===//
// static
DeviceTargetAttr DeviceTargetAttr::get(MLIRContext *context,
StringRef deviceID) {
// TODO(benvanik): query default configuration from the target backend.
return get(context, StringAttr::get(context, deviceID),
DictionaryAttr::get(context), {});
}
// static
Attribute DeviceTargetAttr::parse(AsmParser &p, Type type) {
StringAttr deviceIDAttr;
DictionaryAttr configAttr;
SmallVector<IREE::HAL::ExecutableTargetAttr> executableTargetAttrs;
// `<"device-id"`
if (failed(p.parseLess()) || failed(p.parseAttribute(deviceIDAttr))) {
return {};
}
// `, `
if (succeeded(p.parseOptionalComma())) {
if (succeeded(p.parseOptionalLSquare())) {
// `[targets, ...]` (optional)
do {
IREE::HAL::ExecutableTargetAttr executableTargetAttr;
if (failed(p.parseAttribute(executableTargetAttr))) {
return {};
}
executableTargetAttrs.push_back(executableTargetAttr);
} while (succeeded(p.parseOptionalComma()));
if (failed(p.parseRSquare())) {
return {};
}
} else {
// `{config dict}` (optional)
if (failed(p.parseAttribute(configAttr))) {
return {};
}
// `, [targets, ...]` (optional)
if (succeeded(p.parseOptionalComma())) {
if (failed(p.parseLSquare())) {
return {};
}
do {
IREE::HAL::ExecutableTargetAttr executableTargetAttr;
if (failed(p.parseAttribute(executableTargetAttr))) {
return {};
}
executableTargetAttrs.push_back(executableTargetAttr);
} while (succeeded(p.parseOptionalComma()));
if (failed(p.parseRSquare())) {
return {};
}
}
}
}
// `>`
if (failed(p.parseGreater())) {
return {};
}
return get(p.getContext(), deviceIDAttr, configAttr, executableTargetAttrs);
}
void DeviceTargetAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<";
p.printAttribute(getDeviceID());
auto configAttr = getConfiguration();
if (configAttr && !configAttr.empty()) {
os << ", ";
p.printAttribute(configAttr);
}
auto executableTargetAttrs = getExecutableTargets();
if (!executableTargetAttrs.empty()) {
os << ", [";
llvm::interleaveComma(executableTargetAttrs, os,
[&](auto executableTargetAttr) {
p.printAttribute(executableTargetAttr);
});
os << "]";
}
os << ">";
}
std::string DeviceTargetAttr::getSymbolNameFragment() {
std::string name = getDeviceID().getValue().lower();
if (auto ordinalAttr =
dyn_cast_if_present<IntegerAttr>(getConfigurationAttr("ordinal"))) {
name += "_";
name += std::to_string(ordinalAttr.getInt());
name += "_"; // can't have trailing numbers
}
return sanitizeSymbolName(name);
}
bool DeviceTargetAttr::hasConfigurationAttr(StringRef name) {
auto configAttr = getConfiguration();
return configAttr && configAttr.get(name);
}
Attribute DeviceTargetAttr::getConfigurationAttr(StringRef name) {
if (auto configAttr = getConfiguration()) {
return configAttr.get(name);
}
return {};
}
void DeviceTargetAttr::getExecutableTargets(
SetVector<IREE::HAL::ExecutableTargetAttr> &resultAttrs) {
for (auto attr : getExecutableTargets()) {
resultAttrs.insert(attr);
}
}
void IREE::HAL::DeviceTargetAttr::printStatusDescription(
llvm::raw_ostream &os) const {
mlir::cast<Attribute>(this)->print(os, /*elideType=*/true);
}
// Produces a while-loop that enumerates each device available and tries to
// match it against the target information. SCF is... not very wieldy, but this
// is effectively:
// ```
// %device_count = hal.devices.count : index
// %result:3 = scf.while(%i = 0, %match_ordinal = 0, %device = null) {
// %is_null = util.cmp.eq %device, null : !hal.device
// %in_bounds = arith.cmpi slt %i, %device_count : index
// %continue_while = arith.andi %is_null, %in_bounds : i1
// scf.condition(%continue_while) %i, %match_ordinal %device
// : index, index, !hal.device
// } do {
// %device_i = hal.devices.get %i : !hal.device
// %device_match = <<buildDeviceMatch>>(%device_i)
// %ordinal_match = arith.cmpi eq %match_ordinal, %device_ordinal : index
// %is_match = arith.andi %device_match, %ordinal_match : i1
// %try_device = arith.select %is_match, %device_i, null : !hal.device
// %next_i = arith.addi %i, %c1 : index
// %match_adv = arith.select %device_match, %c1, %c0 : index
// %next_match_ordinal = arith.addi %match_ordinal, %match_adv : index
// scf.yield %next_i, %next_match_ordinal, %try_device
// : index, index !hal.device
// }
// ```
// Upon completion %result#1 contains the device (or null).
// If the target had an ordinal specified we skip matches until a match with the
// specified ordinal is reached.
Value IREE::HAL::DeviceTargetAttr::buildDeviceEnumeration(
Location loc, IREE::HAL::BuildDeviceTargetMatchFn buildDeviceTargetMatch,
OpBuilder &builder) const {
// Device configuration can control selection beyond just the match
// expression.
auto configAttr = getConfiguration();
IntegerAttr deviceOrdinalAttr =
configAttr ? configAttr.getAs<IntegerAttr>("ordinal") : IntegerAttr{};
// Defers to the target backend to build the device match or does a simple
// fallback for unregistered backends (usually for testing, but may be used
// as a way to bypass validation for out-of-tree experiments).
auto buildDeviceMatch = [&](Location loc, Value device,
OpBuilder &builder) -> Value {
// Ask the target backend to build the match expression. It may opt to
// let the default handling take care of things.
Value match = buildDeviceTargetMatch(loc, device, *this, builder);
if (match)
return match;
return IREE::HAL::DeviceTargetAttr::buildDeviceIDAndExecutableFormatsMatch(
loc, device, getDeviceID(), getExecutableTargets(), builder);
};
// Enumerate all devices and match the first one found (if any).
Type indexType = builder.getIndexType();
Type deviceType = builder.getType<IREE::HAL::DeviceType>();
Value c0 = builder.create<arith::ConstantIndexOp>(loc, 0);
Value c1 = builder.create<arith::ConstantIndexOp>(loc, 1);
Value nullDevice = builder.create<IREE::Util::NullOp>(loc, deviceType);
Value deviceOrdinal = deviceOrdinalAttr
? builder.create<arith::ConstantIndexOp>(
loc, deviceOrdinalAttr.getInt())
: c0;
Value deviceCount = builder.create<IREE::HAL::DevicesCountOp>(loc, indexType);
auto whileOp = builder.create<scf::WhileOp>(
loc,
TypeRange{
/*i=*/indexType,
/*match_ordinal=*/indexType,
/*device=*/deviceType,
},
ValueRange{
/*i=*/c0,
/*match_ordinal=*/c0,
/*device=*/nullDevice,
},
[&](OpBuilder &beforeBuilder, Location loc, ValueRange operands) {
Value isNull = beforeBuilder.create<IREE::Util::CmpEQOp>(
loc, operands[/*device=*/2], nullDevice);
Value inBounds = beforeBuilder.create<arith::CmpIOp>(
loc, arith::CmpIPredicate::slt, operands[/*i=*/0], deviceCount);
Value continueWhile =
beforeBuilder.create<arith::AndIOp>(loc, isNull, inBounds);
beforeBuilder.create<scf::ConditionOp>(loc, continueWhile, operands);
},
[&](OpBuilder &afterBuilder, Location loc, ValueRange operands) {
// Check whether the device is a match.
Value device = afterBuilder.create<IREE::HAL::DevicesGetOp>(
loc, deviceType, operands[/*i=*/0]);
Value isDeviceMatch = buildDeviceMatch(loc, device, afterBuilder);
// Check whether whether this matching device ordinal is the requested
// ordinal out of all matching devices.
Value isOrdinalMatch = afterBuilder.create<arith::CmpIOp>(
loc, arith::CmpIPredicate::eq, operands[/*match_ordinal=*/1],
deviceOrdinal);
Value nextMatchOrdinal = afterBuilder.create<arith::AddIOp>(
loc, operands[/*match_ordinal=*/1],
afterBuilder.create<arith::SelectOp>(loc, isDeviceMatch, c1, c0));
// Break if the device and ordinal match, otherwise continue with null.
Value isMatch = afterBuilder.create<arith::AndIOp>(loc, isDeviceMatch,
isOrdinalMatch);
Value tryDevice = afterBuilder.create<arith::SelectOp>(
loc, isMatch, device, nullDevice);
Value nextI =
afterBuilder.create<arith::AddIOp>(loc, operands[/*i=*/0], c1);
afterBuilder.create<scf::YieldOp>(
loc, ValueRange{
/*i=*/nextI,
/*match_ordinal=*/nextMatchOrdinal,
/*device=*/tryDevice,
});
});
return whileOp.getResult(/*device=*/2);
}
// static
Value DeviceTargetAttr::buildDeviceIDAndExecutableFormatsMatch(
Location loc, Value device, StringRef deviceIDPattern,
ArrayRef<IREE::HAL::ExecutableTargetAttr> executableTargetAttrs,
OpBuilder &builder) {
// Match first on the device ID, as that's the top-level filter.
Value idMatch = IREE::HAL::DeviceQueryOp::createI1(
loc, device, "hal.device.id", deviceIDPattern, builder);
// If there are executable formats defined we should check at least one of
// them is supported.
if (executableTargetAttrs.empty()) {
return idMatch; // just device ID
} else {
auto ifOp = builder.create<scf::IfOp>(loc, builder.getI1Type(), idMatch,
true, true);
auto thenBuilder = ifOp.getThenBodyBuilder();
Value anyFormatMatch = buildExecutableFormatMatch(
loc, device, executableTargetAttrs, thenBuilder);
thenBuilder.create<scf::YieldOp>(loc, anyFormatMatch);
auto elseBuilder = ifOp.getElseBodyBuilder();
Value falseValue = elseBuilder.create<arith::ConstantIntOp>(loc, 0, 1);
elseBuilder.create<scf::YieldOp>(loc, falseValue);
return ifOp.getResult(0);
}
}
// static
Value DeviceTargetAttr::buildExecutableFormatMatch(
Location loc, Value device,
ArrayRef<IREE::HAL::ExecutableTargetAttr> executableTargetAttrs,
OpBuilder &builder) {
if (executableTargetAttrs.empty())
return builder.create<arith::ConstantIntOp>(loc, 1, 1);
Value anyFormatMatch;
for (auto executableTargetAttr : executableTargetAttrs) {
Value formatMatch = IREE::HAL::DeviceQueryOp::createI1(
loc, device, "hal.executable.format",
executableTargetAttr.getFormat().getValue(), builder);
if (!anyFormatMatch) {
anyFormatMatch = formatMatch;
} else {
anyFormatMatch =
builder.create<arith::OrIOp>(loc, anyFormatMatch, formatMatch);
}
}
return anyFormatMatch;
}
//===----------------------------------------------------------------------===//
// #hal.device.ordinal<*>
//===----------------------------------------------------------------------===//
void IREE::HAL::DeviceOrdinalAttr::printStatusDescription(
llvm::raw_ostream &os) const {
mlir::cast<Attribute>(this)->print(os, /*elideType=*/true);
}
Value IREE::HAL::DeviceOrdinalAttr::buildDeviceEnumeration(
Location loc, IREE::HAL::BuildDeviceTargetMatchFn buildDeviceTargetMatch,
OpBuilder &builder) const {
return builder.create<IREE::HAL::DevicesGetOp>(
loc, getType(),
builder.create<arith::ConstantIndexOp>(loc, getOrdinal()));
}
//===----------------------------------------------------------------------===//
// #hal.device.fallback<*>
//===----------------------------------------------------------------------===//
void IREE::HAL::DeviceFallbackAttr::printStatusDescription(
llvm::raw_ostream &os) const {
mlir::cast<Attribute>(this)->print(os, /*elideType=*/true);
}
Value IREE::HAL::DeviceFallbackAttr::buildDeviceEnumeration(
Location loc, IREE::HAL::BuildDeviceTargetMatchFn buildDeviceTargetMatch,
OpBuilder &builder) const {
// TODO(benvanik): hal.device.cast if needed - may need to look up the global
// to do it as we don't encode what the device is here in a way that is
// guaranteed to be consistent.
return builder.create<IREE::Util::GlobalLoadOp>(loc, getType(),
getName().getValue());
}
//===----------------------------------------------------------------------===//
// #hal.device.select<*>
//===----------------------------------------------------------------------===//
// static
DeviceSelectAttr DeviceSelectAttr::get(MLIRContext *context,
ArrayRef<Attribute> values) {
return DeviceSelectAttr::get(context, IREE::HAL::DeviceType::get(context),
ArrayAttr::get(context, values));
}
// static
LogicalResult
DeviceSelectAttr::verify(function_ref<mlir::InFlightDiagnostic()> emitError,
Type type, ArrayAttr devicesAttr) {
if (devicesAttr.empty())
return emitError() << "must have at least one device to select";
for (auto deviceAttr : devicesAttr) {
if (!mlir::isa<IREE::HAL::DeviceAliasAttr>(deviceAttr) &&
!mlir::isa<IREE::HAL::DeviceInitializationAttrInterface>(deviceAttr)) {
return emitError() << "can only select between #hal.device.alias, "
"#hal.device.target, #hal.device.ordinal, "
"#hal.device.fallback, or other device "
"initialization attributes";
}
}
// TODO(benvanik): when !hal.device is parameterized we should check that the
// type is compatible with the entries.
return success();
}
void IREE::HAL::DeviceSelectAttr::printStatusDescription(
llvm::raw_ostream &os) const {
// TODO(benvanik): print something easier to read (newline per device, etc).
mlir::cast<Attribute>(this)->print(os, /*elideType=*/true);
}
// Builds a recursive nest of try-else blocks for each device specified.
Value IREE::HAL::DeviceSelectAttr::buildDeviceEnumeration(
Location loc, IREE::HAL::BuildDeviceTargetMatchFn buildDeviceTargetMatch,
OpBuilder &builder) const {
Type deviceType = builder.getType<IREE::HAL::DeviceType>();
Value nullDevice = builder.create<IREE::Util::NullOp>(loc, deviceType);
std::function<Value(ArrayRef<IREE::HAL::DeviceInitializationAttrInterface>,
OpBuilder &)>
buildTry;
buildTry =
[&](ArrayRef<IREE::HAL::DeviceInitializationAttrInterface> deviceAttrs,
OpBuilder &tryBuilder) -> Value {
auto deviceAttr = deviceAttrs.front();
Value tryDevice = deviceAttr.buildDeviceEnumeration(
loc, buildDeviceTargetMatch, tryBuilder);
if (deviceAttrs.size() == 1)
return tryDevice; // termination case
Value isNull =
tryBuilder.create<IREE::Util::CmpEQOp>(loc, tryDevice, nullDevice);
auto ifOp =
tryBuilder.create<scf::IfOp>(loc, deviceType, isNull, true, true);
auto thenBuilder = ifOp.getThenBodyBuilder();
Value tryChainDevice = buildTry(deviceAttrs.drop_front(1), thenBuilder);
thenBuilder.create<scf::YieldOp>(loc, tryChainDevice);
auto elseBuilder = ifOp.getElseBodyBuilder();
elseBuilder.create<scf::YieldOp>(loc, tryDevice);
return ifOp.getResult(0);
};
SmallVector<IREE::HAL::DeviceInitializationAttrInterface> deviceAttrs(
getDevices().getAsRange<IREE::HAL::DeviceInitializationAttrInterface>());
return buildTry(deviceAttrs, builder);
}
//===----------------------------------------------------------------------===//
// #hal.device.affinity<*>
//===----------------------------------------------------------------------===//
// static
Attribute DeviceAffinityAttr::parse(AsmParser &p, Type type) {
// `<@device`
StringAttr deviceName;
int64_t queueMask = -1;
if (failed(p.parseLess()) || failed(p.parseSymbolName(deviceName)))
return {};
if (succeeded(p.parseOptionalComma())) {
// `[`queue_bit[, ...] `]`
queueMask = 0;
if (failed(p.parseCommaSeparatedList(AsmParser::Delimiter::Square, [&]() {
int64_t i = 0;
if (failed(p.parseInteger(i)))
return failure();
queueMask |= 1ll << i;
return success();
}))) {
return {};
}
}
// `>`
if (failed(p.parseGreater()))
return {};
return get(p.getContext(), FlatSymbolRefAttr::get(deviceName), queueMask);
}
void DeviceAffinityAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<";
os << getDevice();
int64_t queueMask = getQueueMask();
if (queueMask != -1) {
os << ", [";
for (int i = 0, j = 0; i < sizeof(queueMask) * 8; ++i) {
if (queueMask & (1ll << i)) {
if (j++ > 0)
os << ", ";
os << i;
}
}
os << "]";
}
os << ">";
}
bool DeviceAffinityAttr::isExecutableWith(
IREE::Stream::AffinityAttr other) const {
if (!other)
return true;
// Only compatible with the same exact devices today. We could support a
// peering model to allow operations to move across devices in a peered set
// but that may be best done at higher levels and avoided once we get to the
// "are these the same device" stage.
auto otherAffinityAttr = llvm::dyn_cast_if_present<DeviceAffinityAttr>(other);
if (!otherAffinityAttr || getDevice() != otherAffinityAttr.getDevice())
return false;
// If this affinity is a subset of the target affinity then it can execute
// with it.
if ((getQueueMask() & otherAffinityAttr.getQueueMask()) == getQueueMask())
return true;
// Otherwise not compatible.
return false;
}
IREE::Stream::AffinityAttr
DeviceAffinityAttr::joinOR(IREE::Stream::AffinityAttr other) const {
if (!other)
return *this;
if (!IREE::Stream::AffinityAttr::canExecuteTogether(*this, other)) {
return nullptr;
}
auto otherAffinityAttr = llvm::dyn_cast_if_present<DeviceAffinityAttr>(other);
return DeviceAffinityAttr::get(getContext(), getDevice(),
getQueueMask() |
otherAffinityAttr.getQueueMask());
}
IREE::Stream::AffinityAttr
DeviceAffinityAttr::joinAND(IREE::Stream::AffinityAttr other) const {
if (!other)
return *this;
if (!IREE::Stream::AffinityAttr::canExecuteTogether(*this, other)) {
return nullptr;
}
auto otherAffinityAttr = llvm::dyn_cast_if_present<DeviceAffinityAttr>(other);
return DeviceAffinityAttr::get(getContext(), getDevice(),
getQueueMask() &
otherAffinityAttr.getQueueMask());
}
bool DeviceAffinityAttr::isLegalToInline(Operation *inlineSite,
Operation *inlinable) const {
// Look up the affinity of the inlining target site and only allow inlining if
// it matches exactly. We could make a decision as to whether we allow
// inlining when queues are subsets (so if the target site allows any queue
// and the inlinable allows queue 2 then allow, etc). In the future we may
// want to allow util.scope restrictions within the inline target to keep
// queue specification tighter but today most queue masks are wildcarded
// anyway.
auto targetAffinityAttr = IREE::Stream::AffinityAttr::lookup(inlineSite);
return *this == targetAffinityAttr;
}
//===----------------------------------------------------------------------===//
// #hal.device.promise<*>
//===----------------------------------------------------------------------===//
// static
Attribute DevicePromiseAttr::parse(AsmParser &p, Type type) {
// `<@device`
StringAttr deviceName;
int64_t queueMask = -1;
if (failed(p.parseLess()) || failed(p.parseSymbolName(deviceName)))
return {};
if (succeeded(p.parseOptionalComma())) {
// `[`queue_bit[, ...] `]`
queueMask = 0;
if (failed(p.parseCommaSeparatedList(AsmParser::Delimiter::Square, [&]() {
int64_t i = 0;
if (failed(p.parseInteger(i)))
return failure();
queueMask |= 1ll << i;
return success();
}))) {
return {};
}
}
// `>`
if (failed(p.parseGreater()))
return {};
return get(p.getContext(), deviceName, queueMask);
}
void DevicePromiseAttr::print(AsmPrinter &p) const {
auto &os = p.getStream();
os << "<@";
os << getDevice().getValue();
int64_t queueMask = getQueueMask();
if (queueMask != -1) {
os << ", [";
for (int i = 0, j = 0; i < sizeof(queueMask) * 8; ++i) {
if (queueMask & (1ll << i)) {
if (j++ > 0)
os << ", ";
os << i;
}
}
os << "]";
}
os << ">";
}
bool DevicePromiseAttr::isExecutableWith(
IREE::Stream::AffinityAttr other) const {
if (!other)
return true;
// Only compatible with the same exact devices today. We could support a
// peering model to allow operations to move across devices in a peered set
// but that may be best done at higher levels and avoided once we get to the
// "are these the same device" stage.
auto otherPromiseAttr = llvm::dyn_cast_if_present<DevicePromiseAttr>(other);
if (!otherPromiseAttr || getDevice() != otherPromiseAttr.getDevice())
return false;
// If this affinity is a subset of the target affinity then it can execute
// with it.
if ((getQueueMask() & otherPromiseAttr.getQueueMask()) == getQueueMask())
return true;
// Otherwise not compatible.
return false;
}
IREE::Stream::AffinityAttr
DevicePromiseAttr::joinOR(IREE::Stream::AffinityAttr other) const {
if (!other)
return *this;
if (!IREE::Stream::AffinityAttr::canExecuteTogether(*this, other)) {
return nullptr;
}
auto otherPromiseAttr = llvm::dyn_cast_if_present<DevicePromiseAttr>(other);
return DevicePromiseAttr::get(getContext(), getDevice(),
getQueueMask() |
otherPromiseAttr.getQueueMask());
}
IREE::Stream::AffinityAttr
DevicePromiseAttr::joinAND(IREE::Stream::AffinityAttr other) const {
if (!other)
return *this;
if (!IREE::Stream::AffinityAttr::canExecuteTogether(*this, other)) {
return nullptr;
}
auto otherPromiseAttr = llvm::dyn_cast_if_present<DevicePromiseAttr>(other);
return DevicePromiseAttr::get(getContext(), getDevice(),
getQueueMask() &
otherPromiseAttr.getQueueMask());
}
bool DevicePromiseAttr::isLegalToInline(Operation *inlineSite,
Operation *inlinable) const {
// Look up the affinity of the inlining target site and only allow inlining if
// it matches exactly. We could make a decision as to whether we allow
// inlining when queues are subsets (so if the target site allows any queue
// and the inlinable allows queue 2 then allow, etc). In the future we may
// want to allow util.scope restrictions within the inline target to keep
// queue specification tighter but today most queue masks are wildcarded
// anyway.
auto targetAffinityAttr = IREE::Stream::AffinityAttr::lookup(inlineSite);
return *this == targetAffinityAttr;
}
//===----------------------------------------------------------------------===//
// IREE::HAL::HALDialect
//===----------------------------------------------------------------------===//
// At the end so it can use functions above:
#include "iree/compiler/Dialect/HAL/IR/HALAttrInterfaces.cpp.inc"
void HALDialect::registerAttributes() {
// Register command line flags:
(void)clExecutableObjectSearchPath;
addAttributes<
#define GET_ATTRDEF_LIST
#include "iree/compiler/Dialect/HAL/IR/HALAttrs.cpp.inc" // IWYU pragma: keep
>();
}
Attribute HALDialect::parseAttribute(DialectAsmParser &parser,
Type type) const {
StringRef mnemonic;
Attribute genAttr;
OptionalParseResult parseResult =
generatedAttributeParser(parser, &mnemonic, type, genAttr);
if (parseResult.has_value())
return genAttr;
parser.emitError(parser.getNameLoc())
<< "unknown HAL attribute: " << mnemonic;
return {};
}
void HALDialect::printAttribute(Attribute attr, DialectAsmPrinter &p) const {
TypeSwitch<Attribute>(attr).Default([&](Attribute) {
if (failed(generatedAttributePrinter(attr, p))) {
assert(false && "unhandled HAL attribute kind");
}
});
}
} // namespace mlir::iree_compiler::IREE::HAL