blob: 3272b1e588ae4ce23878993965efe2aeb7c11774 [file] [edit]
// Copyright 2025 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/ExternalInterfaces/HALExternalModels.h"
#include "iree/compiler/Dialect/HAL/IR/HALDialect.h"
#include "iree/compiler/Dialect/HAL/IR/HALOps.h"
#include "iree/compiler/Dialect/HAL/IR/HALTypes.h"
#include "iree/compiler/Dialect/Stream/IR/StreamDialect.h"
#include "iree/compiler/Dialect/Stream/IR/StreamOps.h"
#include "iree/compiler/Dialect/Stream/IR/StreamTypes.h"
#include "iree/compiler/Dialect/Util/IR/UtilDialect.h"
#include "iree/compiler/Dialect/Util/IR/UtilOps.h"
#include "mlir/IR/Builders.h"
#include "mlir/Interfaces/ValueBoundsOpInterface.h"
namespace mlir::iree_compiler {
namespace {
//===----------------------------------------------------------------------===//
// mlir::ValueBoundsOpInterface
//===----------------------------------------------------------------------===//
template <typename IDOp>
struct IDOpValueBoundsInterface
: ValueBoundsOpInterface::ExternalModel<IDOpValueBoundsInterface<IDOp>,
IDOp> {
void populateBoundsForIndexValue(Operation *op, Value value,
ValueBoundsConstraintSet &cstr) const {
auto boundOp = cast<IDOp>(op);
assert(value == boundOp.getResult() && "value must be op result");
cstr.bound(value) >= 0;
if (boundOp.getUpperBound()) {
cstr.bound(value) < boundOp.getUpperBound()->getSExtValue();
}
}
};
template <typename CountOp>
struct CountOpValueBoundsInterface
: ValueBoundsOpInterface::ExternalModel<
CountOpValueBoundsInterface<CountOp>, CountOp> {
void populateBoundsForIndexValue(Operation *op, Value value,
ValueBoundsConstraintSet &cstr) const {
auto boundOp = cast<CountOp>(op);
assert(value == boundOp.getResult() && "value must be op result");
cstr.bound(value) >= 1;
if (boundOp.getUpperBound()) {
cstr.bound(value) <= boundOp.getUpperBound()->getSExtValue();
}
}
};
//===----------------------------------------------------------------------===//
// IREE::Stream::TimelineAwareOpInterface
//===----------------------------------------------------------------------===//
// External model to make util.call ops with fence arguments timeline-aware.
struct TimelineAwareCallExternalModel
: IREE::Stream::TimelineAwareOpInterface::ExternalModel<
TimelineAwareCallExternalModel, IREE::Util::CallOp> {
static void add(MLIRContext *context) {
IREE::Util::CallOp::attachInterface<TimelineAwareCallExternalModel>(
*context);
}
// Returns that we participate if we have coarse-fences model with fences.
bool participatesInTimeline(Operation *op) const {
auto callOp = cast<IREE::Util::CallOp>(op);
// Check for HAL ABI convention attribute.
if (IREE::HAL::ABIConventionAttr::lookupExecutionModel(callOp) !=
IREE::HAL::ExecutionModel::CoarseFences) {
return false;
}
// Must have fence operands or results to participate.
for (auto type : callOp.getOperandTypes()) {
if (isa<IREE::HAL::FenceType>(type)) {
return true;
}
}
for (auto type : callOp.getResultTypes()) {
if (isa<IREE::HAL::FenceType>(type)) {
return true;
}
}
return false;
}
// Returns fence operands that will be awaited (excludes signal fence).
SmallVector<Value> getAwaitFences(Operation *op) const {
auto callOp = cast<IREE::Util::CallOp>(op);
// First identify the signal fence.
Value signalFence = getSignalFence(op);
// Return all fence operands EXCEPT the signal fence.
SmallVector<Value> fences;
for (auto operand : callOp.getOperands()) {
if (isa<IREE::HAL::FenceType>(operand.getType()) &&
operand != signalFence) {
fences.push_back(operand);
}
}
return fences;
}
// Returns the signal fence (last fence operand or fence result).
//
// TODO(benvanik): better handle wait/signal fences - today we cannot tell
// what a fence is for and go by convention: last operand if no result, result
// otherwise. We should really have a way to associate the fences with
// operands/results and whether they are waits or signals (possibly by having
// a type parameter like !hal.fence<signal> vs !hal.fence<wait>).
Value getSignalFence(Operation *op) const {
auto callOp = cast<IREE::Util::CallOp>(op);
// Check operands first (in/out fence pattern) - use last fence.
Value signalFence;
for (auto operand : callOp.getOperands()) {
if (isa<IREE::HAL::FenceType>(operand.getType())) {
signalFence = operand;
}
}
// If no operand fence or if there's a result fence, prefer result.
for (auto result : callOp.getResults()) {
if (isa<IREE::HAL::FenceType>(result.getType())) {
return result;
}
}
return signalFence;
}
// Looks for preceding stream.timepoint.import ops for fence operands.
// We check if imports already exist to avoid duplicates (CSE will handle).
SmallVector<Value> buildAwaitTimepoints(Operation *op,
OpBuilder &builder) const {
auto callOp = cast<IREE::Util::CallOp>(op);
SmallVector<Value> timepoints;
for (auto operand : callOp.getOperands()) {
if (!isa<IREE::HAL::FenceType>(operand.getType())) {
continue;
}
// Look for existing import.
// TODO(benvanik): RegionBranchOpInterface-aware detection (blocks
// differ).
IREE::Stream::TimepointImportOp existingImport;
for (auto *user : operand.getUsers()) {
if (auto importOp = dyn_cast<IREE::Stream::TimepointImportOp>(user)) {
if (importOp->getBlock() == callOp->getBlock()) {
assert(importOp->isBeforeInBlock(callOp) &&
"SSA dominance required");
existingImport = importOp;
break;
}
}
}
if (existingImport) {
timepoints.push_back(existingImport.getResultTimepoint());
} else {
// Create new import before the op.
auto importOp = IREE::Stream::TimepointImportOp::create(
builder, callOp.getLoc(),
builder.getType<IREE::Stream::TimepointType>(), ValueRange{operand},
/*affinity=*/IREE::Stream::AffinityAttr{});
timepoints.push_back(importOp.getResultTimepoint());
}
}
return timepoints;
}
// Looks for a signal fence in operands (common pattern for imported funcs).
// Heuristic: last fence argument is typically the signal fence.
Value buildResultTimepoint(Operation *op, OpBuilder &builder) const {
auto callOp = cast<IREE::Util::CallOp>(op);
Value signalFence;
for (auto operand : llvm::reverse(callOp.getOperands())) {
if (isa<IREE::HAL::FenceType>(operand.getType())) {
signalFence = operand;
break;
}
}
if (!signalFence) {
for (auto result : callOp.getResults()) {
if (isa<IREE::HAL::FenceType>(result.getType())) {
signalFence = result;
break;
}
}
}
if (!signalFence) {
return {};
}
// Check if import already exists.
// TODO(benvanik): RegionBranchOpInterface-aware detection (blocks differ).
for (auto *user : signalFence.getUsers()) {
if (auto importOp = dyn_cast<IREE::Stream::TimepointImportOp>(user)) {
if (importOp->getBlock() == callOp->getBlock()) {
assert(callOp->isBeforeInBlock(importOp) && "SSA dominance required");
return importOp.getResultTimepoint();
}
}
}
// Create new import after the call.
auto importOp = IREE::Stream::TimepointImportOp::create(
builder, callOp.getLoc(),
builder.getType<IREE::Stream::TimepointType>(), ValueRange{signalFence},
/*affinity=*/IREE::Stream::AffinityAttr{});
return importOp.getResultTimepoint();
}
};
} // namespace
void registerHALExternalModels(DialectRegistry &registry) {
registry.addExtension(+[](MLIRContext *context,
IREE::HAL::HALDialect *dialect) {
IREE::HAL::InterfaceWorkgroupIDOp::attachInterface<
IDOpValueBoundsInterface<IREE::HAL::InterfaceWorkgroupIDOp>>(*context);
IREE::HAL::InterfaceWorkgroupSizeOp::attachInterface<
CountOpValueBoundsInterface<IREE::HAL::InterfaceWorkgroupSizeOp>>(
*context);
IREE::HAL::InterfaceWorkgroupCountOp::attachInterface<
CountOpValueBoundsInterface<IREE::HAL::InterfaceWorkgroupCountOp>>(
*context);
});
registry.insert<IREE::Util::UtilDialect>();
registry.addExtension(
+[](MLIRContext *context, IREE::Util::UtilDialect *dialect) {
TimelineAwareCallExternalModel::add(context);
});
}
} // namespace mlir::iree_compiler