blob: 952681ed095e44d12b29bc8879e50afb44ee7d0d [file]
// Copyright 2023 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/Conversion/StreamToHAL/Utils.h"
#include "iree/compiler/Dialect/HAL/Analysis/Captures.h"
#include "iree/compiler/Dialect/Util/IR/UtilOps.h"
#include "llvm/Support/CommandLine.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
static llvm::cl::opt<bool> clExternalResourcesMappable(
"iree-stream-external-resources-mappable",
llvm::cl::desc("Allocates external resources as host-visible and mappable. "
"This can degrade performance and introduce allocation "
"overhead and staging buffers for readback on the host "
"should be managed by the calling application instead."),
llvm::cl::init(false));
namespace mlir::iree_compiler {
Value lookupDeviceFor(Operation *op, OpBuilder &builder) {
auto affinityAttr = IREE::Stream::AffinityAttr::lookupOrDefault(op);
auto resolveOp = builder.create<IREE::Stream::ContextResolveOp>(
op->getLoc(),
TypeRange{
builder.getType<IREE::HAL::DeviceType>(),
},
affinityAttr);
return resolveOp.getResult(0);
}
std::tuple<Value, Value> lookupDeviceAndQueueAffinityFor(Operation *op,
OpBuilder &builder) {
auto affinityAttr = IREE::Stream::AffinityAttr::lookupOrDefault(op);
auto resolveOp = builder.create<IREE::Stream::ContextResolveOp>(
op->getLoc(),
TypeRange{
builder.getType<IREE::HAL::DeviceType>(),
builder.getI64Type(),
},
affinityAttr);
return std::make_tuple(resolveOp.getResult(0), resolveOp.getResult(1));
}
Value lookupAllocatorFor(Operation *op, OpBuilder &builder) {
auto affinityAttr = IREE::Stream::AffinityAttr::lookupOrDefault(op);
auto resolveOp = builder.create<IREE::Stream::ContextResolveOp>(
op->getLoc(),
TypeRange{
builder.getType<IREE::HAL::AllocatorType>(),
},
affinityAttr);
return resolveOp.getResult(0);
}
std::tuple<Value, Value>
lookupAllocatorAndQueueAffinityFor(Operation *op, OpBuilder &builder) {
auto affinityAttr = IREE::Stream::AffinityAttr::lookupOrDefault(op);
auto resolveOp = builder.create<IREE::Stream::ContextResolveOp>(
op->getLoc(),
TypeRange{
builder.getType<IREE::HAL::AllocatorType>(),
builder.getI64Type(),
},
affinityAttr);
return std::make_tuple(resolveOp.getResult(0), resolveOp.getResult(1));
}
Value getOrCreateWaitFence(Location loc, Value timepointFence,
PatternRewriter &rewriter) {
if (timepointFence)
return timepointFence;
return rewriter.create<IREE::Util::NullOp>(
loc, rewriter.getType<IREE::HAL::FenceType>());
}
// Finds a !hal.fence bound to |timepoint| via a chain op and returns it if
// it is usable at the builder insertion point. The chain ops is only used if
// it is the only consumer of the timepoint and it is removed upon return.
static Value consumeBoundFence(Value timepoint, PatternRewriter &rewriter) {
// Must only have one use. We can't consume a fence multiple times.
if (!timepoint.hasOneUse())
return nullptr; // >1 use
// The use must be an export to a fence.
auto chainOp = dyn_cast<IREE::Stream::TimepointChainExternalOp>(
*timepoint.getUsers().begin());
if (!chainOp)
return nullptr; // non-export use
assert(!chainOp.getExternalValues().empty());
auto fence = chainOp.getExternalValues().front();
if (!fence || !llvm::isa<IREE::HAL::FenceType>(fence.getType()))
return nullptr;
// Try really hard to figure out if the fence can be used. A larger analysis
// pass running prior to conversion that did some code motion could help
// ensure the fence SSA value is usable in the places it is needed - for now
// we just do this local check that satisfies most common programs today. IPO
// would do something like add the fence as an argument to function calls so
// that the functions could consume it but inlining is pretty aggressive now.
if (!IREE::Util::isValueUsableForOp(fence, rewriter.getBlock(),
rewriter.getInsertionPoint())) {
return nullptr; // unusable
}
// Consume the op by erasing it.
rewriter.eraseOp(chainOp);
return fence; // usable
}
Value getOrCreateSignalFence(Location loc, Value device, Value timepoint,
PatternRewriter &rewriter) {
// Check to see if anyone is consuming the timepoint - if not then we don't
// need create a fence.
if (timepoint.use_empty()) {
return rewriter.create<IREE::Util::NullOp>(
loc, rewriter.getType<IREE::HAL::FenceType>());
}
// Check to see if the timepoint is associated with a fence. In common cases
// when along ABI boundaries we can usually find an association.
auto fence = consumeBoundFence(timepoint, rewriter);
if (fence)
return fence;
// Create a new fence.
return rewriter.create<IREE::HAL::FenceCreateOp>(
loc, rewriter.getType<IREE::HAL::FenceType>(), device,
IREE::HAL::FenceFlagBitfield::None);
}
IREE::HAL::CommandCategoryBitfield deriveCommandCategories(Region &region) {
auto bits = IREE::HAL::CommandCategoryBitfield::None;
for (auto &block : region) {
for (auto &op : block) {
if (isa<IREE::Stream::CmdCollectiveOp>(op) ||
isa<IREE::Stream::CmdCallOp>(op)) {
// Calls may do anything and collectives may be implemented as either
// transfers or dispatches.
bits = bits | IREE::HAL::CommandCategoryBitfield::Dispatch |
IREE::HAL::CommandCategoryBitfield::Transfer;
} else if (isa<IREE::Stream::CmdDispatchOp>(op)) {
bits = bits | IREE::HAL::CommandCategoryBitfield::Dispatch;
} else {
bits = bits | IREE::HAL::CommandCategoryBitfield::Transfer;
}
for (auto &nestedRegion : op.getRegions()) {
bits = bits | deriveCommandCategories(nestedRegion);
}
}
}
return bits;
}
LogicalResult
deriveRequiredResourceBufferBits(Location loc,
IREE::Stream::ResourceType resourceType,
IREE::HAL::MemoryTypeBitfield &memoryTypes,
IREE::HAL::BufferUsageBitfield &bufferUsage) {
memoryTypes = IREE::HAL::MemoryTypeBitfield::None;
bufferUsage = IREE::HAL::BufferUsageBitfield::None;
switch (resourceType.getLifetime()) {
default:
return mlir::emitError(loc)
<< "unsupported resource lifetime: "
<< IREE::Stream::stringifyLifetime(resourceType.getLifetime());
case IREE::Stream::Lifetime::Constant:
// Device local; copies required to get into external resources.
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceLocal;
bufferUsage =
bufferUsage | IREE::HAL::BufferUsageBitfield::SharingImmutable;
break;
case IREE::Stream::Lifetime::Variable:
// Device local; copies required to get into external resources.
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceLocal;
break;
case IREE::Stream::Lifetime::External:
// We only require device-visible for external buffers (as we don't today
// do anything else with them on the host). They may be mappable for user
// convenience. Ideally they would have been placed in device-local memory
// but so long as they are device visible the program will execute
// correctly.
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceVisible;
break;
case IREE::Stream::Lifetime::Staging:
// Host local; copies required to get into device resources.
// We could vary this based on staging usage (upload/download) by
// making it device-local|host-visible, but host-local means we have
// a better chance of mapping it during uploads.
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::HostLocal |
IREE::HAL::MemoryTypeBitfield::DeviceVisible;
bufferUsage = bufferUsage | IREE::HAL::BufferUsageBitfield::Transfer |
IREE::HAL::BufferUsageBitfield::Mapping;
break;
case IREE::Stream::Lifetime::Transient:
// Device local; copies required to get into external resources.
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceLocal;
break;
}
// TODO(benvanik): refine usage based on analysis.
bufferUsage = bufferUsage | IREE::HAL::BufferUsageBitfield::Transfer |
IREE::HAL::BufferUsageBitfield::DispatchStorage;
return success();
}
LogicalResult
deriveAllowedResourceBufferBits(Location loc,
IREE::Stream::ResourceType resourceType,
IREE::HAL::MemoryTypeBitfield &memoryTypes,
IREE::HAL::BufferUsageBitfield &bufferUsage) {
memoryTypes = IREE::HAL::MemoryTypeBitfield::None;
bufferUsage = IREE::HAL::BufferUsageBitfield::None;
if (failed(deriveRequiredResourceBufferBits(loc, resourceType, memoryTypes,
bufferUsage))) {
return failure();
}
switch (resourceType.getLifetime()) {
default:
break;
case IREE::Stream::Lifetime::External:
if (clExternalResourcesMappable) {
// #yolo; these come from/go to outside the program.
// Today we assume they are device-local|host-visible just for
// practical purposes but that does not have to be true. We really
// want this to be something we analyze and handle on the edges
// (transferring devices/etc if needed).
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceLocal |
IREE::HAL::MemoryTypeBitfield::HostVisible;
// NOTE: we may not map it but users may after they get them back.
// Another reason we should annotate this - having a buffer be
// mappable is potentially expensive (may get a 2nd copy in memory!).
bufferUsage = bufferUsage | IREE::HAL::BufferUsageBitfield::Mapping;
} else {
memoryTypes = memoryTypes | IREE::HAL::MemoryTypeBitfield::DeviceLocal;
}
break;
}
return success();
}
BindingTable::BindingTable(IREE::Stream::CmdExecuteOp executeOp,
ValueRange bufferValues, ValueRange bufferSizes,
IndexSet &indexSet) {
auto resourceValues = executeOp.getResourceOperands();
auto capturedValues = executeOp.getBody().getArguments();
// Categorize each resource value and add it to the table.
for (auto [resourceValue, capturedValue, bufferValue, bufferSize] :
llvm::zip_equal(resourceValues, capturedValues, bufferValues,
bufferSizes)) {
switch (IREE::HAL::categorizeValue(resourceValue)) {
default:
case IREE::HAL::ValueOrigin::Unknown:
case IREE::HAL::ValueOrigin::MutableGlobal: {
// Indirect reference. Add to the table unless it already exists.
Value slot = indexSet.get(indirectBuffers.size());
if (indirectSlots.try_emplace(capturedValue, slot).second) {
// TODO(benvanik): subset the range bound by the min/max used within the
// consumer region. This would require emitting ops that track that
// information (probably via util.range.min/max). For now we bind the
// entire buffer range and let the individual commands subrange them.
IREE::HAL::BindingValue bindingValue;
bindingValue.buffer = bufferValue;
bindingValue.byteOffset = indexSet.get(0);
bindingValue.byteLength = bufferSize;
indirectBuffers.push_back(bindingValue);
}
break;
}
case IREE::HAL::ValueOrigin::LocalConstant:
case IREE::HAL::ValueOrigin::ImmutableGlobal: {
// Direct reference; not in the table.
break;
}
}
}
}
std::optional<Value> BindingTable::lookupResourceSlot(Value resourceValue) {
auto it = indirectSlots.find(resourceValue);
if (it != indirectSlots.end()) {
return it->second; // found a slot
}
return std::nullopt;
}
IREE::HAL::BindingValue CommandBufferConversionMapping::resolveBinding(
Location loc, Value resourceValue, Value bufferValue, Value useOffset,
Value useLength, OpBuilder &builder) {
IREE::HAL::BindingValue bindingValue;
// Try to resolve the resource to a slot. If not found then it's a direct
// reference and we use the buffer provided.
auto slot = bindingTable.lookupResourceSlot(resourceValue);
if (slot.has_value()) {
bindingValue.buffer = slot.value();
} else {
bindingValue.buffer = bufferValue;
}
// TODO(benvanik): adjust range by the binding table base index. Today all
// binding table entries are the full buffers starting at zero.
bindingValue.byteOffset = useOffset;
bindingValue.byteLength = useLength;
return bindingValue;
}
void StreamConversionMapping::mapCommandBuffer(
IREE::Stream::CmdExecuteOp executeOp, Value commandBuffer,
BindingTable bindingTable) {
// Wrap metadata for the mapping.
auto commandBufferMapping = std::make_shared<CommandBufferConversionMapping>(
commandBuffer, std::move(bindingTable));
// Map all ops nested within the command buffer so we can query later.
opCommandBufferMap.insert(
std::make_pair(executeOp, commandBufferMapping.get()));
executeOp.walk([&](Operation *op) {
opCommandBufferMap.insert(std::make_pair(op, commandBufferMapping.get()));
return WalkResult::advance();
});
// Keep the mapping data alive until conversion completes.
commandBuffers.push_back(std::move(commandBufferMapping));
}
CommandBufferConversionMapping &
StreamConversionMapping::lookupCommandBufferFor(Operation *cmdOp) const {
auto it = opCommandBufferMap.find(cmdOp);
assert(it != opCommandBufferMap.end() &&
"command buffer must have been registered during conversion");
return *it->second;
}
} // namespace mlir::iree_compiler