blob: 1af7cce39c28d804bd9de3d984693bd27ee53922 [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/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