| // 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 <memory> |
| #include <utility> |
| |
| #include "iree/compiler/Dialect/HAL/IR/HALDialect.h" |
| #include "iree/compiler/Dialect/HAL/IR/HALOps.h" |
| #include "iree/compiler/Dialect/HAL/Target/TargetBackend.h" |
| #include "iree/compiler/Dialect/HAL/Target/TargetRegistry.h" |
| #include "iree/compiler/Dialect/HAL/Transforms/Passes.h" |
| #include "llvm/ADT/STLExtras.h" |
| #include "llvm/Support/raw_ostream.h" |
| #include "mlir/AsmParser/AsmParser.h" |
| #include "mlir/IR/Attributes.h" |
| #include "mlir/IR/Builders.h" |
| #include "mlir/IR/BuiltinTypes.h" |
| #include "mlir/IR/Diagnostics.h" |
| #include "mlir/Pass/Pass.h" |
| |
| namespace mlir::iree_compiler::IREE::HAL { |
| |
| #define GEN_PASS_DEF_ASSIGNTARGETDEVICESPASS |
| #include "iree/compiler/Dialect/HAL/Transforms/Passes.h.inc" |
| |
| namespace { |
| |
| //===----------------------------------------------------------------------===// |
| // --iree-hal-assign-target-devices |
| //===----------------------------------------------------------------------===// |
| |
| // Strips leading and trailing whitespace from |value|. |
| static StringRef stripWhitespace(StringRef value) { |
| while (!value.empty() && llvm::isSpace(value.front())) { |
| value = value.drop_front(1); |
| } |
| while (!value.empty() && llvm::isSpace(value.back())) { |
| value = value.drop_back(1); |
| } |
| return value; |
| } |
| |
| // Strips leading and trailing double quotes from |value| if both exist. |
| static StringRef stripQuotes(StringRef value) { |
| value = stripWhitespace(value); |
| StringRef unquoted = value; |
| if (unquoted.consume_front("\"") && unquoted.consume_back("\"")) { |
| return stripWhitespace(unquoted); |
| } |
| return value; |
| } |
| |
| // Consumes a leading `name=` literal. |
| // Returns the `name` and leaves remaining characters after `=` in |value|. |
| // Returns an empty string if no name literal is present. |
| static StringRef consumeNameLiteral(StringRef &value) { |
| value = stripWhitespace(value); |
| const size_t splitIdx = value.find('='); |
| if (splitIdx == std::string::npos) { |
| return ""; |
| } |
| for (size_t i = 0; i < splitIdx; ++i) { |
| const char c = value[i]; |
| if (!llvm::isAlnum(c) && c != '_') { |
| return value; |
| } |
| } |
| const StringRef name = value.substr(0, splitIdx); |
| value = stripWhitespace(value.substr(splitIdx + 1)); |
| return stripWhitespace(name); |
| } |
| |
| // Consumes the first portion of |value| corresponding to a device alias. |
| // Expects: `abc` or `abc[123]` (and allows `"abc"[123]`). |
| // Only valid literals will be parsed (a-z0-9_). |
| // Returns the device ID and optional ordinal. All other unconsumed characters |
| // will remain in |value| upon return. |
| static std::pair<StringRef, std::optional<int64_t>> |
| consumeAliasLiteral(StringRef &value) { |
| value = stripWhitespace(value); |
| const size_t splitIdx = value.find(','); |
| StringRef part = |
| splitIdx == std::string::npos ? value : value.substr(0, splitIdx); |
| |
| StringRef deviceID = part; |
| std::optional<int64_t> ordinal; |
| |
| const size_t ordinalIdx = part.find('['); |
| if (ordinalIdx != std::string::npos) { |
| deviceID = part.substr(0, ordinalIdx); |
| StringRef ordinalStr = part.substr(ordinalIdx + 1); |
| APInt ordinalInt; |
| if (!ordinalStr.consumeInteger(10, ordinalInt)) { |
| ordinal = ordinalInt.getSExtValue(); |
| } |
| } |
| |
| value = stripWhitespace(value.substr(part.size())); |
| return std::make_pair(stripQuotes(deviceID), ordinal); |
| } |
| |
| struct TargetSpec { |
| StringAttr name; |
| TypedAttr attr; |
| }; |
| |
| // Parses the user-provided string into a target spec. |
| // |
| // Supports attributes: |
| // #hal.device.alias<...> |
| // #hal.device.target<...> |
| // #hal.device.select<...> |
| // #hal.device.fallback<...> |
| // Supports convenience shorthand: |
| // ...,... -> #hal.device.select<[...,...]> |
| // target -> #hal.device.alias<"target"> |
| // target[0] -> #hal.device.alias<"target"[0]> |
| // "target"[0] -> #hal.device.alias<"target"[0]> |
| // Supports name= prefixes: |
| // name=... -> ... |
| static FailureOr<TargetSpec> parseTargetSpec(Location loc, |
| StringRef targetSpecStr) { |
| auto *context = loc.getContext(); |
| targetSpecStr = stripQuotes(targetSpecStr); |
| |
| // Check for a name prefix and strip it from the spec. |
| StringRef name = consumeNameLiteral(targetSpecStr); |
| StringAttr nameAttr = |
| name.empty() ? StringAttr{} : StringAttr::get(context, name); |
| |
| // Parse the spec attributes. |
| SmallVector<Attribute> attrs; |
| while (!targetSpecStr.empty()) { |
| TypedAttr typedAttr; |
| if (targetSpecStr.starts_with('#')) { |
| // MLIR attribute. |
| size_t numRead = 0; |
| auto parsedAttr = mlir::parseAttribute(targetSpecStr, context, |
| /*type=*/nullptr, &numRead); |
| if (!parsedAttr) { |
| return mlir::emitError(loc) << "failed to parse target spec prefix `" |
| << targetSpecStr << "`"; |
| } |
| typedAttr = dyn_cast<TypedAttr>(parsedAttr); |
| if (!typedAttr) { |
| return mlir::emitError(loc) << "unexpected target attribute type: " |
| "expected a `!hal.device` but got `" |
| << parsedAttr << "`"; |
| } |
| targetSpecStr = stripWhitespace(targetSpecStr.substr(numRead)); |
| } else { |
| // Alias string. |
| auto [deviceID, ordinal] = consumeAliasLiteral(targetSpecStr); |
| typedAttr = IREE::HAL::DeviceAliasAttr::get( |
| context, IREE::HAL::DeviceType::get(context), |
| StringAttr::get(context, deviceID), ordinal, DictionaryAttr{}); |
| } |
| |
| if (!typedAttr || !isa<IREE::HAL::DeviceType>(typedAttr.getType())) { |
| return mlir::emitError(loc) << "unexpected target attribute type: " |
| "expected a `!hal.device` but got `" |
| << typedAttr.getType() << "`"; |
| } |
| attrs.push_back(typedAttr); |
| |
| if (targetSpecStr.empty()) { |
| break; // done |
| } else if (!targetSpecStr.starts_with(',')) { |
| return mlir::emitError(loc) |
| << "unexpected additional characters after parsing an element: `" |
| << targetSpecStr << "`"; |
| } |
| targetSpecStr = targetSpecStr.substr(1); // strip , |
| } |
| |
| if (attrs.empty()) { |
| return mlir::emitError(loc) << "expected one or more target attributes"; |
| } else if (attrs.size() == 1) { |
| return TargetSpec{nameAttr, cast<TypedAttr>(attrs.front())}; |
| } else { |
| return TargetSpec{nameAttr, |
| IREE::HAL::DeviceSelectAttr::get(context, attrs)}; |
| } |
| } |
| |
| struct AssignTargetDevicesPass |
| : public IREE::HAL::impl::AssignTargetDevicesPassBase< |
| AssignTargetDevicesPass> { |
| using IREE::HAL::impl::AssignTargetDevicesPassBase< |
| AssignTargetDevicesPass>::AssignTargetDevicesPassBase; |
| |
| void runOnOperation() override { |
| auto moduleOp = getOperation(); |
| |
| // If no targets are specified we can't do anything - another pass earlier |
| // in the pipeline will have had to add the targets. |
| if (targetDevices.empty()) { |
| return; |
| } |
| |
| // Check to see if targets are already specified and if so then no-op the |
| // pass so that we don't mess with whatever the user intended. |
| if (moduleOp->hasAttr("hal.device.targets")) { |
| return; |
| } |
| |
| // If there are any device globals declared then bail as it means the user |
| // has already materialized the devices they want. |
| for (auto globalOp : moduleOp.getOps<IREE::Util::GlobalOpInterface>()) { |
| if (isa<IREE::HAL::DeviceType>(globalOp.getGlobalType())) { |
| return; |
| } |
| } |
| |
| // Parse each spec and validate correctness. |
| bool hasAnyNamed = false; |
| bool hasAnyUnnamed = false; |
| SmallVector<TargetSpec> targetSpecs; |
| for (auto &targetDevice : targetDevices) { |
| auto targetSpecOr = parseTargetSpec(moduleOp.getLoc(), targetDevice); |
| if (failed(targetSpecOr)) { |
| return signalPassFailure(); |
| } |
| if (targetSpecOr->name) { |
| hasAnyNamed = true; |
| } else { |
| hasAnyUnnamed = true; |
| } |
| targetSpecs.push_back(*targetSpecOr); |
| } |
| |
| // If any spec has a name assigned then all must have names assigned. |
| if (hasAnyNamed && hasAnyUnnamed) { |
| emitError(moduleOp.getLoc()) |
| << "if any target device spec has a name then all must be named"; |
| return signalPassFailure(); |
| } |
| |
| if (hasAnyNamed) { |
| // NOTE: we allow duplicate names to override assignment. |
| llvm::MapVector<StringAttr, Attribute> deviceAttrMap; |
| for (auto targetSpec : targetSpecs) { |
| assert(targetSpec.name && "all devices must be named"); |
| deviceAttrMap[targetSpec.name] = targetSpec.attr; |
| } |
| SmallVector<NamedAttribute> deviceAttrs; |
| for (auto [name, value] : deviceAttrMap) { |
| deviceAttrs.push_back(NamedAttribute(name, value)); |
| } |
| moduleOp->setAttr( |
| "hal.device.targets", |
| DictionaryAttr::get(moduleOp.getContext(), deviceAttrs)); |
| } else { |
| SmallVector<Attribute> deviceAttrs; |
| for (auto [name, value] : targetSpecs) { |
| assert(!name && "no devices may have names"); |
| deviceAttrs.push_back(value); |
| } |
| moduleOp->setAttr("hal.device.targets", |
| ArrayAttr::get(moduleOp.getContext(), deviceAttrs)); |
| } |
| } |
| }; |
| |
| } // namespace |
| |
| } // namespace mlir::iree_compiler::IREE::HAL |