blob: 6ac9cc8061522703345f916c18155bc35b3c0618 [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 <memory>
#include <utility>
#include "iree/compiler/Dialect/HAL/Analysis/DeviceAnalysis.h"
#include "iree/compiler/Dialect/HAL/IR/HALDialect.h"
#include "iree/compiler/Dialect/HAL/IR/HALOps.h"
#include "iree/compiler/Dialect/HAL/Target/TargetRegistry.h"
#include "iree/compiler/Dialect/HAL/Transforms/Passes.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_VERIFYDEVICESPASS
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h.inc"
namespace {
//===----------------------------------------------------------------------===//
// --iree-hal-verify-devices
//===----------------------------------------------------------------------===//
static void printAvailable(InFlightDiagnostic &diagnostic,
const TargetRegistry &targetRegistry) {
diagnostic << "available devices: [";
llvm::interleaveComma(targetRegistry.getRegisteredTargetDevices(),
diagnostic);
diagnostic << "], available backends = [";
llvm::interleaveComma(targetRegistry.getRegisteredTargetBackends(),
diagnostic);
diagnostic << "]";
}
static LogicalResult
verifyDeviceTargetAttr(Operation *deviceOp,
IREE::HAL::DeviceTargetAttr deviceTargetAttr,
const TargetRegistry &targetRegistry) {
auto targetDevice =
targetRegistry.getTargetDevice(deviceTargetAttr.getDeviceID().getValue());
if (!targetDevice) {
auto diagnostic = deviceOp->emitError();
diagnostic << "unregistered target device "
<< deviceTargetAttr.getDeviceID()
<< "; ensure it is linked into the compiler (available = [ ";
for (const auto &targetName : targetRegistry.getRegisteredTargetDevices()) {
diagnostic << "'" << targetName << "' ";
}
diagnostic << "])";
return diagnostic;
}
for (auto executableTargetAttr : deviceTargetAttr.getExecutableTargets()) {
auto targetBackend = targetRegistry.getTargetBackend(
executableTargetAttr.getBackend().getValue());
if (!targetBackend) {
auto diagnostic = deviceOp->emitError();
diagnostic << "unregistered target backend "
<< executableTargetAttr.getBackend()
<< "; ensure it is linked into the compiler (available = [ ";
for (const auto &targetName :
targetRegistry.getRegisteredTargetBackends()) {
diagnostic << "'" << targetName << "' ";
}
diagnostic << "])";
return diagnostic;
}
}
return success();
}
static LogicalResult verifyAttr(Operation *deviceOp, Attribute attr,
const TargetRegistry &targetRegistry) {
return TypeSwitch<Attribute, LogicalResult>(attr)
.Case<IREE::HAL::DeviceTargetAttr>([&](auto deviceTargetAttr) {
return verifyDeviceTargetAttr(deviceOp, deviceTargetAttr,
targetRegistry);
})
.Case<IREE::HAL::DeviceSelectAttr>([&](auto deviceSelectAttr) {
for (auto attr : deviceSelectAttr.getDevices().getValue()) {
if (failed(verifyAttr(deviceOp, attr, targetRegistry))) {
return failure();
}
}
return success();
})
.Default([&](auto attr) {
return success(); // probably fallback/ordinal/etc - can't verify
});
}
struct VerifyDevicesPass
: public IREE::HAL::impl::VerifyDevicesPassBase<VerifyDevicesPass> {
using IREE::HAL::impl::VerifyDevicesPassBase<
VerifyDevicesPass>::VerifyDevicesPassBase;
void runOnOperation() override {
auto moduleOp = getOperation();
// Devices are required if we need to convert host code or executables.
// If we only have hal.executables as input then we can bypass this.
// We could extend this check to be a bit smarter at the risk of false
// negatives - today this is just handling the standalone hal.executable
// compilation workflow.
bool anyNonExecutableOps = false;
for (auto &op : moduleOp.getOps()) {
if (!isa<IREE::HAL::ExecutableOp>(op)) {
anyNonExecutableOps = true;
break;
}
}
if (!anyNonExecutableOps) {
return;
}
// Analyze the module to find all devices.
DeviceAnalysis deviceAnalysis(moduleOp);
if (failed(deviceAnalysis.run())) {
return signalPassFailure();
}
// Devices are only required if we have dialects we may lower into device
// code. For now checking for tensor types is probably sufficient though we
// may want a pluggable way to decide this (e.g. dialect/type/op
// interfaces).
auto isTensor = [](Type type) { return isa<TensorType>(type); };
bool anyTensors = false;
for (auto &op : moduleOp.getOps()) {
if (op.hasTrait<OpTrait::IREE::Util::ObjectLike>()) {
continue; // ignore executables
}
op.walk([&](Operation *childOp) {
if (llvm::any_of(childOp->getOperandTypes(), isTensor) ||
llvm::any_of(childOp->getResultTypes(), isTensor)) {
anyTensors = true;
return WalkResult::interrupt();
}
return WalkResult::advance();
});
}
// TODO(multi-device): the logic above is insufficient; we only need devices
// if the program will end up requiring them but we don't know that here.
// We have to wait until we've lowered to the point where we do require a
// device _and_ we actually want one (aren't compiling a non-HAL program).
// We could probably have an op interface, better output from the pass that
// requires the devices, etc. For now we error out in HAL conversion when we
// try to resolve devices.
if (false && anyTensors && deviceAnalysis.getDeviceGlobals().empty()) {
auto diagnostic = moduleOp.emitError();
diagnostic
<< "no HAL devices defined in the module; use the module-level "
"hal.device.targets attribute, the --iree-hal-target-device= "
"flag, or provide inputs with global !hal.devices defined; ";
printAvailable(diagnostic, *targetRegistry.value);
return signalPassFailure();
}
// Walk all devices and verify them.
for (auto deviceOp : deviceAnalysis.getDeviceGlobals()) {
if (auto initialValue = deviceOp.getGlobalInitialValue()) {
if (failed(verifyAttr(deviceOp, initialValue, *targetRegistry.value))) {
return signalPassFailure();
}
}
}
}
};
} // namespace
} // namespace mlir::iree_compiler::IREE::HAL