blob: 9d7cc63165b977d1b124bb7207193955bb073f46 [file]
// Copyright 2022 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/Transforms/Passes.h"
#include "iree/compiler/Dialect/Stream/IR/StreamOps.h"
#include "iree/compiler/Dialect/Util/IR/UtilDialect.h"
#include "iree/compiler/Utils/IntegerSet.h"
#include "llvm/Support/FileSystem.h"
#include "llvm/Support/Path.h"
#include "llvm/Support/ToolOutputFile.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Support/FileUtilities.h"
#include "mlir/Transforms/Passes.h"
// NOTE: redundant bindings will result in unique buffer locations during the
// benchmark and will impact caching behavior.
#define DEBUG_TYPE "iree-dump-executable-benchmarks"
namespace mlir::iree_compiler::IREE::HAL {
#define GEN_PASS_DEF_DUMPEXECUTABLEBENCHMARKSPASS
#include "iree/compiler/Dialect/HAL/Transforms/Passes.h.inc"
namespace {
// We could use the resource constraints in the module when we have them.
static const int64_t kBufferAlignment = 256;
using Vec3 = std::tuple<unsigned, unsigned, unsigned>;
struct Binding {
unsigned binding = 0;
int64_t size = 0;
};
// Combined data for all dispatches of a particular static workload size.
struct DispatchParams {
// All locations that dispatch with these parameters.
SmallVector<Location> locs;
// All affinities that dispatch with these parameters.
// Empty if no affinities were specified.
SetVector<IREE::Stream::AffinityAttr> affinities;
// Workload used as input to the workgroup count calculation function.
SmallVector<unsigned> workload;
// Analyzed minimum binding sizes.
SmallVector<Binding> bindings;
// Push constant operands that are known constant. May be null if dynamic.
SmallVector<TypedAttr> uniformOperands;
};
using DispatchParamsMap =
llvm::DenseMap<SymbolRefAttr, std::vector<DispatchParams>>;
// Walk |moduleOp| and gather all of the dispatches to each executable.
// Dispatch parameters are deduplicated by workload so that there's only ever
// one entry for all dispatches with a given workgroup count.
// Dispatches will be ignored if they have a dynamic workload or any dynamically
// sized resources.
static DispatchParamsMap gatherDispatchParams(mlir::ModuleOp moduleOp,
SymbolTable &symbolTable) {
DispatchParamsMap map;
for (auto funcOp : moduleOp.getOps<mlir::FunctionOpInterface>()) {
funcOp.walk([&](IREE::Stream::CmdDispatchOp dispatchOp) {
auto affinityAttr = dyn_cast_if_present<IREE::HAL::DeviceAffinityAttr>(
IREE::Stream::AffinityAttr::lookup(dispatchOp));
if (!affinityAttr) {
LLVM_DEBUG(
llvm::dbgs()
<< "skipping dispatch because it has no affinity specified\n");
return;
}
auto workloadValues = dispatchOp.getWorkload();
SmallVector<unsigned> workload;
workload.reserve(workloadValues.size());
for (auto workloadValue : workloadValues) {
APInt workloadConstValue;
if (!matchPattern(workloadValue, m_ConstantInt(&workloadConstValue))) {
LLVM_DEBUG({
auto firstEntryPoint = *dispatchOp.getEntryPointRefs().begin();
llvm::dbgs() << "skipping dispatch of entry point `"
<< firstEntryPoint << "` (non-constant workload)\n";
});
return;
}
workload.push_back(workloadConstValue.getSExtValue());
}
SmallVector<TypedAttr> uniformOperands;
for (auto operand : dispatchOp.getUniformOperands()) {
TypedAttr uniformOperand;
if (!matchPattern(operand, m_Constant(&uniformOperand))) {
// TODO(benvanik): extract information from the executable annotations
// or allow the dynamic value to be passed in as an additional arg.
LLVM_DEBUG({
auto firstEntryPoint = *dispatchOp.getEntryPointRefs().begin();
llvm::dbgs() << "Skipping dispatch of entry point `"
<< firstEntryPoint
<< "` (non-constant uniform operand)\n";
});
return;
}
uniformOperands.push_back(uniformOperand);
}
// Work around needing a mutable key for the set; C++ was a mistake.
dispatchOp.forEachEntryPointAttr([&](SymbolRefAttr entryPointAttr) {
SmallVector<Binding> bindings;
for (auto [i, resourceLength] :
llvm::enumerate(dispatchOp.getResourceLengths())) {
APInt resourceLengthInt;
if (!matchPattern(resourceLength,
m_ConstantInt(&resourceLengthInt))) {
LLVM_DEBUG(llvm::dbgs() << "skipping dispatch of entry point `"
<< entryPointAttr
<< "` (non-constant resource length)\n";);
return;
}
bindings.push_back({(unsigned)i, resourceLengthInt.getSExtValue()});
}
auto &dispatchParamsSet = map[entryPointAttr];
DispatchParams *dispatchParams = nullptr;
for (auto &it : dispatchParamsSet) {
if (it.workload == workload) {
dispatchParams = &it;
break;
}
}
if (!dispatchParams) {
dispatchParamsSet.push_back({});
dispatchParams = &dispatchParamsSet.back();
}
dispatchParams->locs.push_back(dispatchOp.getLoc());
dispatchParams->affinities.insert(affinityAttr);
dispatchParams->workload = workload;
dispatchParams->bindings = std::move(bindings);
dispatchParams->uniformOperands = std::move(uniformOperands);
});
});
}
return map;
}
static std::pair<Value, Value>
getDeviceAndQueueAffinity(Location loc, IREE::Stream::AffinityAttr affinityAttr,
OpBuilder &builder) {
if (auto deviceAffinityAttr =
dyn_cast_if_present<IREE::HAL::DeviceAffinityAttr>(affinityAttr)) {
auto resolveOp = builder.create<IREE::HAL::DeviceResolveOp>(
loc,
TypeRange{
builder.getType<IREE::HAL::DeviceType>(),
builder.getI64Type(),
},
deviceAffinityAttr);
return std::make_pair(resolveOp.getResult(0), resolveOp.getResult(1));
}
auto device = IREE::HAL::DeviceType::resolveAny(loc, builder);
auto queueAffinity = builder.create<arith::ConstantIntOp>(loc, -1, 64);
return std::make_pair(device, queueAffinity);
}
// Appends a global hal.buffer initialized to the size required for all
// of the bindings in |dispatchParams| (plus alignment).
static IREE::Util::GlobalOp appendGlobalBuffer(
Location loc, StringRef baseName, const DispatchParams &dispatchParams,
IREE::Stream::AffinityAttr affinityAttr, OpBuilder &moduleBuilder) {
// Create a global to hold the HAL buffer.
auto globalOp = moduleBuilder.create<IREE::Util::GlobalOp>(
loc, (baseName + "_buffer").str(),
/*isMutable=*/true,
IREE::HAL::BufferType::get(moduleBuilder.getContext()));
globalOp.setPrivate();
// Compute the total size of the buffer based on all binding sizes when
// aligned.
int64_t totalLength = 0;
for (auto binding : dispatchParams.bindings) {
totalLength =
IREE::Util::align(totalLength + binding.size, kBufferAlignment);
}
// Build an initializer to allocate the buffer.
auto initOp = moduleBuilder.create<IREE::Util::InitializerOp>(loc);
auto initBuilder = OpBuilder::atBlockBegin(initOp.addEntryBlock());
IndexSet indexSet(loc, initBuilder);
// Resolve allocator for the benchmark device.
auto [device, queueAffinity] =
getDeviceAndQueueAffinity(loc, affinityAttr, initBuilder);
auto allocator =
initBuilder.create<IREE::HAL::DeviceAllocatorOp>(loc, device).getResult();
auto memoryTypes = IREE::HAL::MemoryTypeBitfield::DeviceLocal;
auto bufferUsage = IREE::HAL::BufferUsageBitfield::Transfer |
IREE::HAL::BufferUsageBitfield::DispatchStorage;
auto allocateOp = initBuilder.create<IREE::HAL::AllocatorAllocateOp>(
loc, globalOp.getType(), allocator, queueAffinity, memoryTypes,
bufferUsage, indexSet.get(totalLength));
globalOp.createStoreOp(loc, allocateOp.getResult(), initBuilder);
initBuilder.create<IREE::Util::ReturnOp>(loc);
return globalOp;
}
// Appends a function calling the given |exportOp| with |dispatchParams|.
// This will add a global value for the resources required.
//
// Expects the runner to pass an i32 value indicating the number of dispatches
// to be made in one submission.
static void appendDispatchBenchmark(IREE::Stream::AffinityAttr affinityAttr,
IREE::HAL::ExecutableOp executableOp,
IREE::HAL::ExecutableVariantOp variantOp,
IREE::HAL::ExecutableExportOp exportOp,
DispatchParams dispatchParams,
OpBuilder &moduleBuilder) {
auto loc = FusedLoc::get(executableOp.getContext(), dispatchParams.locs);
std::string baseName = (executableOp.getName() + "_" + variantOp.getName() +
"_" + exportOp.getName())
.str();
if (!dispatchParams.workload.empty()) {
baseName += "_" + std::to_string(dispatchParams.workload[0]);
for (size_t i = 1; i < dispatchParams.workload.size(); ++i) {
baseName += "x" + std::to_string(dispatchParams.workload[i]);
}
}
// Add a global variable holding an initialized buffer for the dispatch IO.
auto bufferGlobalOp = appendGlobalBuffer(loc, baseName, dispatchParams,
affinityAttr, moduleBuilder);
// Create an exported benchmark function that runs the dispatches.
auto funcType =
moduleBuilder.getFunctionType({moduleBuilder.getI32Type()}, {});
auto funcOp =
moduleBuilder.create<IREE::Util::FuncOp>(loc, baseName, funcType);
funcOp.setVisibility(SymbolTable::Visibility::Public);
// Mark the function as being a dispatch benchmark.
// This tells iree-benchmark-module to pass in the arguments we need.
funcOp->setAttr("iree.abi.stub", moduleBuilder.getUnitAttr());
funcOp->setAttr(
"iree.reflection",
moduleBuilder.getDictionaryAttr({
moduleBuilder.getNamedAttr("iree.benchmark",
moduleBuilder.getStringAttr("dispatch")),
}));
// Build the function that runs the dispatches.
auto *entryBlock = funcOp.addEntryBlock();
OpBuilder funcBuilder = OpBuilder::atBlockBegin(entryBlock);
IndexSet indexSet(loc, funcBuilder);
auto batchSizeArg = funcBuilder.create<arith::IndexCastOp>(
loc, funcBuilder.getIndexType(), entryBlock->getArgument(0));
// Resolve device for this particular benchmark.
auto [device, queueAffinity] =
getDeviceAndQueueAffinity(loc, affinityAttr, funcBuilder);
// Create and begin command buffer.
// TODO(benvanik): reuse the command buffer (initialize once and store).
auto commandBufferModes =
IREE::HAL::CommandBufferModeBitfield::OneShot |
IREE::HAL::CommandBufferModeBitfield::AllowInlineExecution;
auto commandBuffer =
funcBuilder
.create<IREE::HAL::CommandBufferCreateOp>(
loc, funcBuilder.getType<IREE::HAL::CommandBufferType>(), device,
commandBufferModes, IREE::HAL::CommandCategoryBitfield::Dispatch,
queueAffinity,
/*binding_capacity=*/Value{})
.getResult();
// Constant values.
auto layoutAttr = exportOp.getLayoutAttr();
SmallVector<Value> constantValues;
if (int64_t pushConstantCount = layoutAttr.getConstants()) {
constantValues.reserve(pushConstantCount);
for (int64_t i = 0; i < pushConstantCount; ++i) {
constantValues.push_back(funcBuilder.create<arith::ConstantOp>(
loc, dispatchParams.uniformOperands[i]));
}
}
// Binding values.
Value buffer =
bufferGlobalOp.createLoadOp(loc, funcBuilder).getLoadedGlobalValue();
SmallVector<BindingValue> bindingValues;
int64_t bufferOffset = 0;
for (auto binding : dispatchParams.bindings) {
BindingValue bindingValue;
bindingValue.buffer = buffer;
bindingValue.byteOffset = indexSet.get(bufferOffset);
bindingValue.byteLength = indexSet.get(binding.size);
bindingValues.push_back(bindingValue);
bufferOffset =
IREE::Util::align(bufferOffset + binding.size, kBufferAlignment);
}
// @executable::@variant::@export
auto exportRefAttr =
SymbolRefAttr::get(executableOp.getNameAttr(),
{
SymbolRefAttr::get(variantOp.getNameAttr()),
SymbolRefAttr::get(exportOp.getNameAttr()),
});
// Compute the workgroup parameters.
auto workload = llvm::map_to_vector(
dispatchParams.workload, [&](unsigned dim) { return indexSet.get(dim); });
auto workgroupCountOp =
funcBuilder.create<IREE::HAL::ExecutableCalculateWorkgroupsOp>(
loc, funcBuilder.getIndexType(), funcBuilder.getIndexType(),
funcBuilder.getIndexType(), device, exportRefAttr, workload);
// Get the executable/entry point ordinal used to dispatch.
Value executable = funcBuilder.create<IREE::HAL::ExecutableLookupOp>(
loc, funcBuilder.getType<IREE::HAL::ExecutableType>(), device,
exportRefAttr.getRootReference().getValue());
Value ordinal = funcBuilder.create<IREE::HAL::ExecutableExportOrdinalOp>(
loc, funcBuilder.getIndexType(), exportRefAttr);
// Loop around dispatches based on batch size.
// Note that we insert a barrier between each dispatch - we could make this
// optional so that concurrent utilization is measured.
funcBuilder.create<scf::ForOp>(
loc, indexSet.get(0), batchSizeArg, indexSet.get(1), ValueRange{},
[&](OpBuilder &forBuilder, Location loc, Value iv, ValueRange iters) {
// Dispatch.
forBuilder.create<IREE::HAL::CommandBufferDispatchOp>(
loc, commandBuffer, executable, ordinal,
workgroupCountOp.getResults(), constantValues, bindingValues,
IREE::HAL::DispatchFlags::None);
// Barrier following the dispatch to block the next dispatch.
auto sourceStage = IREE::HAL::ExecutionStageBitfield::CommandRetire |
IREE::HAL::ExecutionStageBitfield::Dispatch;
auto targetStage = IREE::HAL::ExecutionStageBitfield::CommandIssue |
IREE::HAL::ExecutionStageBitfield::Dispatch;
auto barrierFlags = IREE::HAL::ExecutionBarrierFlagBitfield::None;
forBuilder.create<IREE::HAL::CommandBufferExecutionBarrierOp>(
loc, commandBuffer, sourceStage, targetStage, barrierFlags);
forBuilder.create<scf::YieldOp>(loc);
});
funcBuilder.create<IREE::HAL::CommandBufferFinalizeOp>(loc, commandBuffer);
// We begin executing immediately and then wait on a fence.
// TODO(benvanik): add fences to ABI so the benchmark tool can pipeline.
Value waitFence = funcBuilder.create<IREE::Util::NullOp>(
loc, funcBuilder.getType<IREE::HAL::FenceType>());
Value signalFence = funcBuilder.create<IREE::HAL::FenceCreateOp>(
loc, funcBuilder.getType<IREE::HAL::FenceType>(), device,
IREE::HAL::FenceFlagBitfield::None);
// Queue execution.
funcBuilder.create<IREE::HAL::DeviceQueueExecuteOp>(
loc, device, queueAffinity, waitFence, signalFence,
ValueRange{commandBuffer});
// Block until it completes.
Value timeoutMillis = funcBuilder.create<arith::ConstantIntOp>(loc, -1, 32);
auto fenceOp = funcBuilder.create<IREE::HAL::FenceAwaitOp>(
loc, funcBuilder.getI32Type(), timeoutMillis, signalFence);
funcBuilder.create<IREE::Util::StatusCheckOkOp>(
loc, fenceOp.getStatus(), "failed to wait on timepoint");
funcBuilder.create<IREE::Util::ReturnOp>(loc);
}
// Builds a module exporting one function for each dispatch configuration
// targeting |sourceExecutableOp|.
static mlir::OwningOpRef<mlir::ModuleOp>
buildBenchmarkModule(IREE::HAL::ExecutableOp sourceExecutableOp,
IREE::HAL::ExecutableVariantOp sourceVariantOp,
const DispatchParamsMap &dispatchParamsMap,
DeviceAnalysis &deviceAnalysis) {
// Empty module with default name.
// We could use the original module name here to make tracking nicer.
mlir::OwningOpRef<mlir::ModuleOp> moduleOp =
mlir::ModuleOp::create(sourceExecutableOp.getLoc());
auto moduleBuilder = OpBuilder::atBlockBegin(moduleOp->getBody());
// Copy over the devices from the original module. Note that not all of the
// devices may be used and we should prune them, but even better than that
// would be to generate one module per device dispatches are made on such
// that users can isolate to individual devices. For now we just deal with
// it.
for (auto globalOp : deviceAnalysis.getDeviceGlobals()) {
moduleBuilder.clone(*globalOp.getOperation());
}
// Clone the executable variant into the new module.
auto executableOp = moduleBuilder.create<IREE::HAL::ExecutableOp>(
sourceExecutableOp.getLoc(), sourceExecutableOp.getName());
executableOp.setVisibility(sourceExecutableOp.getVisibility());
auto variantOp = cast<IREE::HAL::ExecutableVariantOp>(
OpBuilder::atBlockBegin(&executableOp.getBlock())
.clone(*sourceVariantOp.getOperation()));
// Add functions to test each entry point with its various dispatch
// parameters.
bool hasAnyBenchmarks = false;
for (auto exportOp : variantOp.getExportOps()) {
auto symbolRefAttr =
SymbolRefAttr::get(executableOp.getNameAttr(),
{
FlatSymbolRefAttr::get(variantOp.getNameAttr()),
FlatSymbolRefAttr::get(exportOp.getNameAttr()),
});
auto dispatchParamsSet = dispatchParamsMap.find(symbolRefAttr);
if (dispatchParamsSet != dispatchParamsMap.end()) {
for (auto &dispatchParams : dispatchParamsSet->second) {
if (dispatchParams.affinities.empty()) {
appendDispatchBenchmark({}, executableOp, variantOp, exportOp,
dispatchParams, moduleBuilder);
} else {
for (auto affinityAttr : dispatchParams.affinities) {
appendDispatchBenchmark(affinityAttr, executableOp, variantOp,
exportOp, dispatchParams, moduleBuilder);
}
}
hasAnyBenchmarks = true;
}
}
}
// Skip the file when we could not generate any benchmarks.
if (!hasAnyBenchmarks)
return {};
// Run CSE and the canonicalizer to pretty up the output.
PassManager passManager(moduleOp->getContext());
passManager.addPass(mlir::createCanonicalizerPass());
passManager.addPass(mlir::createCSEPass());
if (failed(passManager.run(*moduleOp))) {
moduleOp->emitError("failed to run canonicalizer; malformed output");
return {};
}
return moduleOp;
}
static void dumpModuleToStream(mlir::ModuleOp moduleOp, StringRef fileName,
llvm::raw_ostream &os) {
OpPrintingFlags flags;
flags.useLocalScope(); // could use global scope, but IR gets messy fast
moduleOp.print(os, flags);
os << "\n"; // newline at end of file
}
//===----------------------------------------------------------------------===//
// --iree-hal-dump-executable-benchmarks
//===----------------------------------------------------------------------===//
struct DumpExecutableBenchmarksPass
: public IREE::HAL::impl::DumpExecutableBenchmarksPassBase<
DumpExecutableBenchmarksPass> {
using IREE::HAL::impl::DumpExecutableBenchmarksPassBase<
DumpExecutableBenchmarksPass>::DumpExecutableBenchmarksPassBase;
void runOnOperation() override {
auto moduleOp = getOperation();
auto moduleName = moduleOp.getName().value_or("module");
SymbolTable symbolTable(moduleOp);
DeviceAnalysis deviceAnalysis(moduleOp);
if (failed(deviceAnalysis.run()))
return signalPassFailure();
if (deviceAnalysis.getDeviceGlobals().empty()) {
mlir::emitRemark(moduleOp.getLoc())
<< "Executable benchmarks were requested but no devices were "
"declared in the module.\n";
return;
} else if (deviceAnalysis.getDeviceGlobals().size() != 1) {
mlir::emitWarning(moduleOp.getLoc())
<< "Executable benchmarks were requested but there are multiple "
"devices in the module and the pass does not support that yet.\n";
return;
}
// Analyze the module to find dispatch parameters.
// This is a full walk of all stream.cmd.dispatch ops and will handle
// filtering out dispatches that have dynamic parameters we don't
// currently support.
auto dispatchParamsMap = gatherDispatchParams(moduleOp, symbolTable);
if (dispatchParamsMap.empty()) {
mlir::emitRemark(moduleOp.getLoc())
<< "Executable benchmarks were requested but none were generated. "
"Run with --debug-only=iree-dump-executable-benchmarks for more "
"details.\n";
return;
}
// Help people out and mkdir if needed.
if (!path.empty() && path != "-") {
llvm::sys::fs::create_directories(path);
}
// Produce one file per executable containing all exported entry points.
for (auto executableOp : moduleOp.getOps<IREE::HAL::ExecutableOp>()) {
for (auto variantOp :
executableOp.getOps<IREE::HAL::ExecutableVariantOp>()) {
auto benchmarkModuleOp = buildBenchmarkModule(
executableOp, variantOp, dispatchParamsMap, deviceAnalysis);
if (!benchmarkModuleOp)
continue;
auto fileName = (moduleName + "_" + executableOp.getName() + "_" +
variantOp.getName() + "_benchmark.mlir")
.str();
if (path.empty() || path == "-") {
dumpModuleToStream(*benchmarkModuleOp, fileName, llvm::outs());
} else {
auto filePath =
(path + llvm::sys::path::get_separator() + fileName).str();
std::string error;
auto file = mlir::openOutputFile(filePath, &error);
if (!file) {
executableOp.emitError()
<< "while dumping to " << path << ": " << error;
return signalPassFailure();
}
dumpModuleToStream(*benchmarkModuleOp, fileName, file->os());
file->keep();
}
}
}
}
};
} // namespace
} // namespace mlir::iree_compiler::IREE::HAL