| // 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/Analysis/DeviceAnalysis.h" |
| |
| #include "iree/compiler/Dialect/HAL/Analysis/Attributes/DeviceGlobalPVS.h" |
| #include "iree/compiler/Dialect/HAL/Analysis/Attributes/DeviceTargetPVS.h" |
| #include "iree/compiler/Dialect/HAL/IR/HALOps.h" |
| #include "iree/compiler/Dialect/Util/Analysis/DFX/Element.h" |
| #include "iree/compiler/Dialect/Util/Analysis/DFX/State.h" |
| #include "mlir/Dialect/SCF/IR/SCF.h" |
| #include "mlir/Interfaces/FunctionInterfaces.h" |
| |
| namespace mlir::iree_compiler::IREE::HAL { |
| |
| //===----------------------------------------------------------------------===// |
| // DeviceAnalysis |
| //===----------------------------------------------------------------------===// |
| |
| DeviceAnalysis::DeviceAnalysis(Operation *rootOp) |
| : explorer(rootOp, TraversalAction::SHALLOW), solver(explorer, allocator) { |
| explorer.setOpInterfaceAction<mlir::FunctionOpInterface>( |
| TraversalAction::RECURSE); |
| explorer.setOpAction<mlir::scf::ForOp>(TraversalAction::RECURSE); |
| explorer.setOpAction<mlir::scf::IfOp>(TraversalAction::RECURSE); |
| explorer.setOpAction<mlir::scf::WhileOp>(TraversalAction::RECURSE); |
| // Ignore the contents of executables (linalg goo, etc). |
| explorer.setOpAction<IREE::HAL::ExecutableOp>(TraversalAction::IGNORE); |
| explorer.initialize(); |
| } |
| |
| DeviceAnalysis::~DeviceAnalysis() = default; |
| |
| LogicalResult DeviceAnalysis::run() { |
| // TODO(multi-device): remove this fallback path when device globals are fully |
| // plumbed through. Today we still have inputs with the hal.device.targets |
| // attribute. |
| if (auto targetsAttr = explorer.getRootOp()->getAttrOfType<ArrayAttr>( |
| "hal.device.targets")) { |
| if (!targetsAttr.empty()) { |
| defaultDeviceSet = DeviceSet(targetsAttr); |
| } |
| } |
| |
| // Initialize device globals (in declaration order). |
| for (auto globalOp : explorer.getRootOp() |
| ->getRegion(0) |
| .getOps<IREE::Util::GlobalOpInterface>()) { |
| auto globalType = globalOp.getGlobalType(); |
| if (isa<IREE::HAL::DeviceType>(globalType)) { |
| solver.getOrCreateElementFor<DeviceTargetGlobalPVS>( |
| Position::forOperation(globalOp)); |
| deviceGlobals.push_back(globalOp); |
| } |
| } |
| |
| // Initialize all SSA values so we can do just with trivial search. |
| explorer.walkValuesOfType<IREE::HAL::DeviceType>([&](Value value) { |
| solver.getOrCreateElementFor<DeviceGlobalValuePVS>( |
| Position::forValue(value)); |
| solver.getOrCreateElementFor<DeviceTargetValuePVS>( |
| Position::forValue(value)); |
| return WalkResult::advance(); |
| }); |
| |
| return solver.run(); |
| } |
| |
| // Sorts |globalOps| in the natural affinity sort order. |
| // We unfortunately have to do this as the PVS elements we source from are |
| // unsorted. |
| static void |
| sortGlobalOps(SmallVectorImpl<IREE::Util::GlobalOpInterface> &globalOps) { |
| // HACK: this should probably do a type id ordering followed by a |
| // type-specific ordering (interface compare method?). We just need this to be |
| // stable as the globals come from multiple DenseSets that have run-to-run |
| // ordering variance. This is very inefficient but is only used when there are |
| // multiple possible devices and we try to avoid that anyway. |
| if (globalOps.size() <= 1) { |
| return; |
| } |
| llvm::stable_sort(globalOps, [](IREE::Util::GlobalOpInterface lhs, |
| IREE::Util::GlobalOpInterface rhs) { |
| std::string lhsStr; |
| llvm::raw_string_ostream lhsStream(lhsStr); |
| lhs.print(lhsStream); |
| std::string rhsStr; |
| llvm::raw_string_ostream rhsStream(rhsStr); |
| rhs.print(rhsStream); |
| return lhsStr < rhsStr; |
| }); |
| } |
| |
| std::optional<SmallVector<IREE::Util::GlobalOpInterface>> |
| DeviceAnalysis::lookupDeviceGlobals(Value deviceValue) { |
| auto globalPVS = solver.lookupElementFor<DeviceGlobalValuePVS>( |
| Position::forValue(deviceValue)); |
| if (!globalPVS || !globalPVS->isValidState() || |
| globalPVS->isUndefContained()) { |
| return std::nullopt; |
| } |
| SmallVector<IREE::Util::GlobalOpInterface> globalOps; |
| for (auto globalOp : globalPVS->getAssumedSet()) { |
| globalOps.push_back(globalOp); |
| } |
| sortGlobalOps(globalOps); |
| return globalOps; |
| } |
| |
| std::optional<DeviceSet> DeviceAnalysis::lookupDeviceTargets( |
| IREE::Util::GlobalOpInterface deviceGlobalOp) { |
| return lookupDeviceTargets(FlatSymbolRefAttr::get(deviceGlobalOp)); |
| } |
| |
| std::optional<DeviceSet> |
| DeviceAnalysis::lookupDeviceTargets(SymbolRefAttr deviceGlobalAttr) { |
| SetVector<IREE::HAL::DeviceTargetAttr> resultSet; |
| gatherDeviceTargets(deviceGlobalAttr, explorer.getRootOp(), resultSet); |
| return DeviceSet(resultSet.getArrayRef()); |
| } |
| |
| std::optional<DeviceSet> |
| DeviceAnalysis::lookupDeviceTargets(Value deviceValue) { |
| auto valuePVS = solver.lookupElementFor<DeviceTargetValuePVS>( |
| Position::forValue(deviceValue)); |
| if (!valuePVS || !valuePVS->isValidState() || valuePVS->isUndefContained()) { |
| return defaultDeviceSet; |
| } |
| return DeviceSet(valuePVS->getAssumedSet()); |
| } |
| |
| // Returns a set of target devices that may be active for the given |
| // operation. This will recursively walk parent operations until one with |
| // the `hal.device.targets` attribute is found. |
| // |
| // This is a legacy mechanism for performing the search. Newer code should use |
| // affinities or !hal.device analysis instead. |
| static void gatherLegacyDeviceTargetAttrs( |
| Operation *op, SetVector<IREE::HAL::DeviceTargetAttr> &resultSet) { |
| auto attrId = StringAttr::get(op->getContext(), "hal.device.targets"); |
| while (op) { |
| auto targetsAttr = op->getAttrOfType<ArrayAttr>(attrId); |
| if (targetsAttr) { |
| for (auto elementAttr : targetsAttr) { |
| if (auto targetAttr = |
| dyn_cast<IREE::HAL::DeviceTargetAttr>(elementAttr)) { |
| resultSet.insert(targetAttr); |
| } else { |
| // HACK: this legacy approach is deprecated and only preserved for |
| // existing behavior. It's ok to get angry here as users should not be |
| // trying to use this pass prior to device materialization. |
| assert(false && |
| "legacy hal.device.targets only support hal.device.targets"); |
| } |
| } |
| return; |
| } |
| op = op->getParentOp(); |
| } |
| // No devices found; let caller decide what to do. |
| } |
| |
| // Recursively resolves the referenced device into targets. |
| void DeviceAnalysis::gatherDeviceTargets( |
| Attribute rootAttr, Operation *fromOp, |
| SetVector<IREE::HAL::DeviceTargetAttr> &resultSet) { |
| SetVector<Attribute> worklist; |
| worklist.insert(rootAttr); |
| do { |
| auto attr = worklist.pop_back_val(); |
| if (!TypeSwitch<Attribute, bool>(attr) |
| .Case<SymbolRefAttr>([&](auto symRefAttr) { |
| auto globalOp = |
| explorer.getSymbolTables() |
| .lookupNearestSymbolFrom<IREE::Util::GlobalOpInterface>( |
| fromOp, symRefAttr); |
| assert(globalOp && "global reference must be valid"); |
| if (auto initialValueAttr = globalOp.getGlobalInitialValue()) { |
| // Global with a device initialization value we can analyze. |
| worklist.insert(initialValueAttr); |
| return true; |
| } else { |
| return false; |
| } |
| }) |
| .Case<IREE::HAL::DeviceTargetAttr>([&](auto targetAttr) { |
| resultSet.insert(targetAttr); |
| return true; |
| }) |
| .Case<IREE::HAL::DeviceFallbackAttr>([&](auto fallbackAttr) { |
| worklist.insert(fallbackAttr.getName()); |
| return true; |
| }) |
| .Case<IREE::HAL::DeviceSelectAttr>([&](auto selectAttr) { |
| worklist.insert(selectAttr.getDevices().begin(), |
| selectAttr.getDevices().end()); |
| return true; |
| }) |
| .Default([](auto attr) { return false; })) { |
| // No initial value means fall back to defaults. We do that by |
| // inserting all knowable targets. |
| gatherLegacyDeviceTargetAttrs(fromOp, resultSet); |
| return; |
| } |
| } while (!worklist.empty()); |
| } |
| |
| void DeviceAnalysis::gatherAllDeviceTargets( |
| SetVector<IREE::HAL::DeviceTargetAttr> &resultSet) { |
| for (auto globalOp : deviceGlobals) { |
| gatherDeviceTargets(FlatSymbolRefAttr::get(globalOp), explorer.getRootOp(), |
| resultSet); |
| } |
| } |
| |
| void DeviceAnalysis::gatherDeviceAffinityTargets( |
| IREE::Stream::AffinityAttr affinityAttr, Operation *fromOp, |
| SetVector<IREE::HAL::DeviceTargetAttr> &resultSet) { |
| // We currently only know how to handle HAL device affinities. |
| // We could support other ones via an interface but instead we just fall back |
| // to default logic if no affinity or an unknown one is found. |
| auto deviceAffinityAttr = |
| dyn_cast_if_present<IREE::HAL::DeviceAffinityAttr>(affinityAttr); |
| if (!deviceAffinityAttr) { |
| gatherLegacyDeviceTargetAttrs(fromOp, resultSet); |
| return; |
| } |
| |
| // Recursively resolve the referenced device into targets. |
| gatherDeviceTargets(deviceAffinityAttr.getDevice(), fromOp, resultSet); |
| } |
| |
| void DeviceAnalysis::gatherAllExecutableTargets( |
| SetVector<IREE::HAL::ExecutableTargetAttr> &resultSet) { |
| SetVector<IREE::HAL::DeviceTargetAttr> deviceTargetSet; |
| gatherAllDeviceTargets(deviceTargetSet); |
| for (auto deviceTargetAttr : deviceTargetSet) { |
| deviceTargetAttr.getExecutableTargets(resultSet); |
| } |
| } |
| |
| void DeviceAnalysis::gatherRequiredExecutableTargets( |
| Operation *forOp, SetVector<IREE::HAL::ExecutableTargetAttr> &resultSet) { |
| // Get the affinity from the op or an ancestor. Note that there may be no |
| // affinity specified at all. |
| auto affinityAttr = IREE::Stream::AffinityAttr::lookupOrDefault(forOp); |
| |
| // Gather the device targets that are referenced by the affinity. |
| SetVector<IREE::HAL::DeviceTargetAttr> deviceTargetSet; |
| gatherDeviceAffinityTargets(affinityAttr, forOp, deviceTargetSet); |
| |
| // Add all executable targets on the device targets. |
| for (auto deviceTargetAttr : deviceTargetSet) { |
| resultSet.insert(deviceTargetAttr.getExecutableTargets().begin(), |
| deviceTargetAttr.getExecutableTargets().end()); |
| } |
| } |
| |
| void DeviceAnalysis::gatherRequiredExecutableTargets( |
| IREE::Stream::AffinityAttr affinityAttr, Operation *fromOp, |
| SetVector<IREE::HAL::ExecutableTargetAttr> &resultSet) { |
| SetVector<IREE::HAL::DeviceTargetAttr> deviceTargetAttrs; |
| gatherDeviceAffinityTargets(affinityAttr, fromOp, deviceTargetAttrs); |
| for (auto deviceTargetAttr : deviceTargetAttrs) { |
| deviceTargetAttr.getExecutableTargets(resultSet); |
| } |
| } |
| |
| } // namespace mlir::iree_compiler::IREE::HAL |