| // 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 = ⁢ |
| 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 |