blob: 1e539bc831f9b01f37ad2776cda4381a64ca6b79 [file]
// Copyright 2021 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/Codegen/Utils/GPUUtils.h"
#include "iree/compiler/Codegen/Dialect/Codegen/IR/IREECodegenAttrs.h"
#include "iree/compiler/Codegen/Dialect/GPU/IR/IREEGPUAttrs.h"
#include "iree/compiler/Codegen/Dialect/GPU/TargetUtils/KnownTargets.h"
#include "iree/compiler/Codegen/Utils/MarkerUtils.h"
#include "iree/compiler/Codegen/Utils/Utils.h"
#include "iree/compiler/Dialect/HAL/IR/HALTypes.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/DebugLog.h"
#include "llvm/Support/ErrorHandling.h"
#include "llvm/Support/InterleavedRange.h"
#include "mlir/Dialect/AMDGPU/IR/AMDGPUDialect.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/Arith/Utils/Utils.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/LLVMIR/ROCDLTargetInfo.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/Utils/IndexingUtils.h"
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Matchers.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"
#include <cassert>
#include <cstdint>
#include <optional>
#define DEBUG_TYPE "iree-codegen-gpu-utils"
#define DBGS() (llvm::dbgs() << "[" DEBUG_TYPE "]: ")
#define DBGSNL() (llvm::dbgs() << "\n")
constexpr unsigned kShuffleBitWidth = 32;
// TODO: These are AMD GPU specific. These need to find a better home.
constexpr char kWavesPerEuAttrName[] = "waves_per_eu";
static llvm::cl::opt<std::string> clTestTarget(
"iree-gpu-test-target",
llvm::cl::desc(
"The target for IR LIT tests. Format is '<arch>:<feature>@<api>', "
"where <feature> and <api> are optional; e.g., "
"'gfx942:+sramecc,-xnack@hip'. If <api> is missing, it will be deduced "
"from <arch>; e.g., 'gfx*' defaults to HIP, 'sm_*' defaults to CUDA"),
llvm::cl::init(""));
namespace mlir::iree_compiler {
//===----------------------------------------------------------------------===//
// GPU processor IDs and sizes
//===----------------------------------------------------------------------===//
llvm::SmallVector<mlir::linalg::ProcInfo, 2>
getGPUThreadIdsAndCounts(mlir::OpBuilder &builder, mlir::Location loc,
unsigned numDims,
llvm::ArrayRef<int64_t> workgroupSize) {
assert(numDims <= kNumGPUDims);
llvm::SmallVector<mlir::linalg::ProcInfo, 2> procInfo(numDims);
std::array<gpu::Dimension, kNumGPUDims> dimAttr{
gpu::Dimension::x, gpu::Dimension::y, gpu::Dimension::z};
mlir::Type indexType = builder.getIndexType();
for (unsigned i = 0; i < numDims; ++i) {
procInfo[numDims - 1 - i] = {
mlir::gpu::ThreadIdOp::create(builder, loc, indexType, dimAttr[i]),
mlir::arith::ConstantOp::create(builder, loc,
builder.getIndexAttr(workgroupSize[i])),
linalg::DistributionMethod::Cyclic};
}
return procInfo;
}
llvm::SmallVector<mlir::linalg::ProcInfo, 2>
getSubgroupIdsAndCounts(mlir::OpBuilder &builder, mlir::Location loc,
unsigned warpSize, unsigned numDims,
llvm::ArrayRef<int64_t> numSubgroups) {
assert(numDims <= kNumGPUDims);
llvm::SmallVector<mlir::linalg::ProcInfo, 2> procInfo(numDims);
std::array<gpu::Dimension, kNumGPUDims> dimAttr{
gpu::Dimension::x, gpu::Dimension::y, gpu::Dimension::z};
mlir::Type indexType = builder.getIndexType();
for (unsigned i = 0; i < numDims; ++i) {
mlir::Value subgroupId =
mlir::gpu::ThreadIdOp::create(builder, loc, indexType, dimAttr[i]);
if (i == 0) {
subgroupId = affine::AffineDelinearizeIndexOp::create(
builder, loc, subgroupId,
ArrayRef<int64_t>{numSubgroups[i], warpSize})
.getResult(0);
}
procInfo[numDims - 1 - i] = {
subgroupId,
mlir::arith::ConstantOp::create(builder, loc,
builder.getIndexAttr(numSubgroups[i])),
linalg::DistributionMethod::Cyclic};
}
return procInfo;
}
bool isDescendingRelativeMappingIndices(ArrayRef<Attribute> array) {
int64_t prev = cast<DeviceMappingAttrInterface>(array[0]).getRelativeIndex();
for (Attribute attr : array.drop_front()) {
int64_t relativeIndex =
cast<DeviceMappingAttrInterface>(attr).getRelativeIndex();
if (relativeIndex != prev - 1) {
return false;
}
prev = relativeIndex;
}
return true;
}
//===----------------------------------------------------------------------===//
// GPU vectorization
//===----------------------------------------------------------------------===//
bool canPerformVectorAccessUsingAllThreads(ArrayRef<int64_t> shape,
int64_t threadCount,
int64_t vectorSize) {
// Verify that each dimension of the shape can be distributed on the
// threads
// For zero dim tensor, consider it's too small to access using all threads.
if (shape.empty()) {
return false;
}
int64_t threadsAvailable = threadCount;
for (const auto &[index, dim] : llvm::enumerate(llvm::reverse(shape))) {
int64_t numElementPerThread = index == 0 ? vectorSize : 1;
int64_t numThreads = dim / numElementPerThread;
if (numThreads == 0) {
return false;
}
if (numThreads > threadsAvailable) {
// If there are no enough remaining threads to distribute the current
// dimension, try to use all remaining threads. But we still need to make
// sure all work can be distributed to these threads evenly.
if (numThreads % threadsAvailable != 0) {
return false;
}
numThreads = threadsAvailable;
}
if (threadsAvailable % numThreads != 0) {
return false;
}
threadsAvailable = threadsAvailable / numThreads;
if (threadsAvailable == 1) {
break;
}
}
return threadsAvailable == 1;
}
/// Pick an unrolling order that will allow tensorcore operation to reuse LHS
/// register. This is needed to get good performance on sm_80 target.
std::optional<SmallVector<int64_t>>
gpuMmaUnrollOrder(vector::ContractionOp contract) {
SmallVector<int64_t> order;
// First make reduction the outer dimensions.
for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
if (vector::isReductionIterator(iter)) {
order.push_back(index);
}
}
llvm::SmallDenseSet<int64_t> dims;
for (AffineExpr expr : contract.getIndexingMapsArray()[0].getResults()) {
dims.insert(cast<AffineDimExpr>(expr).getPosition());
}
// Then parallel dimensions that are part of Lhs as we want to re-use Lhs.
for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
if (vector::isParallelIterator(iter) && dims.count(index)) {
order.push_back(index);
}
}
// Then the remaining parallel loops.
for (auto [index, iter] : llvm::enumerate(contract.getIteratorTypes())) {
if (vector::isParallelIterator(iter) && !dims.count(index)) {
order.push_back(index);
}
}
return order;
}
//===----------------------------------------------------------------------===//
// GPU tiling and distribution
//===----------------------------------------------------------------------===//
const char *getGPUDistributeAttrName() { return "iree.gpu.distribute_dim"; }
FailureOr<SmallVector<int64_t>> getGPUTileSize(mlir::FunctionOpInterface funcOp,
int tilingLevel) {
SmallVector<Operation *> computeOps = getComputeOps(funcOp);
FailureOr<IREE::Codegen::LoweringConfigAttr> config =
getFirstLoweringConfig<IREE::Codegen::LoweringConfigAttr>(computeOps);
if (failed(config)) {
return funcOp.emitOpError("failed to get lowering configuration");
}
return config->getTileSizeVals(tilingLevel);
}
FailureOr<scf::SCFTileSizeComputationFunction>
getGPUScfTileSizeComputeFn(mlir::FunctionOpInterface funcOp, int tilingLevel) {
FailureOr<SmallVector<int64_t>> tileSizes =
getGPUTileSize(funcOp, tilingLevel);
if (failed(tileSizes)) {
return failure();
}
scf::SCFTileSizeComputationFunction computeFn =
[tileSizes](OpBuilder &builder,
Operation *op) -> SmallVector<OpFoldResult> {
auto tileSizesOfr = getAsIndexOpFoldResult(op->getContext(), *tileSizes);
auto zeroAttr = builder.getIndexAttr(0);
int numLoops = cast<TilingInterface>(op).getLoopIteratorTypes().size();
tileSizesOfr.resize(numLoops, zeroAttr);
return tileSizesOfr;
};
return computeFn;
}
bool isNonZeroRank(TypedValue<VectorType> val) {
return val.getType().getRank() != 0;
}
//===----------------------------------------------------------------------===//
// GPU workgroup memory
//===----------------------------------------------------------------------===//
std::optional<Value> allocateWorkgroupMemory(OpBuilder &builder,
memref::SubViewOp subview,
ArrayRef<Value> sizeBounds,
DataLayout &) {
OpBuilder::InsertionGuard guard(builder);
mlir::FunctionOpInterface funcOp =
subview->getParentOfType<mlir::FunctionOpInterface>();
if (!funcOp) {
return std::nullopt;
}
// The subview size bounds are expected to be constant; they specify the shape
// of the allocation.
SmallVector<int64_t, 2> shape;
for (Value bound : sizeBounds) {
APInt value;
if (!matchPattern(bound, m_ConstantInt(&value))) {
return std::nullopt;
}
shape.push_back(value.getSExtValue());
}
builder.setInsertionPoint(&funcOp.front(), funcOp.front().begin());
auto type = MemRefType::get(
shape, subview.getType().getElementType(), MemRefLayoutAttrInterface{},
gpu::AddressSpaceAttr::get(builder.getContext(),
gpu::GPUDialect::getWorkgroupAddressSpace()));
Value buffer = memref::AllocOp::create(builder, funcOp.getLoc(), type);
return buffer;
}
LogicalResult deallocateWorkgroupMemory(OpBuilder &, Value /*buffer*/) {
return success();
}
LogicalResult copyToWorkgroupMemory(OpBuilder &b, Value src, Value dst) {
Operation *copyOp = memref::CopyOp::create(b, src.getLoc(), src, dst);
setMarker(copyOp, getCopyToWorkgroupMemoryMarker());
return success();
}
static bool propagateCopyDestIntoProducerFill(memref::CopyOp copyOp) {
// Look for a fill Op writing into the copyOp source.
Operation *prevOp = copyOp->getPrevNode();
while (prevOp) {
if (isMemoryEffectFree(prevOp)) {
prevOp = prevOp->getPrevNode();
continue;
}
auto fillOp = dyn_cast<linalg::FillOp>(prevOp);
if (!fillOp) {
break;
}
if (fillOp.output() != copyOp.getSource()) {
break;
}
// Move the fillOp and change the destination to the copy destination.
fillOp->moveBefore(copyOp);
fillOp.getOutputsMutable().assign(copyOp.getTarget());
return true;
}
return false;
}
// Split input/output operand from copy from shared memory into a separate
// input.
static void insertInputValueIntoGeneric(Value source, linalg::GenericOp op) {
SmallVector<Value> newOperands;
SmallVector<AffineMap> maps;
for (OpOperand *in : op.getDpsInputOperands()) {
newOperands.push_back(in->get());
maps.push_back(op.getMatchingIndexingMap(in));
}
newOperands.push_back(source);
assert(op.getNumDpsInits() == 1);
OpOperand *outOperand = op.getDpsInitOperand(0);
maps.push_back(op.getMatchingIndexingMap(outOperand));
maps.push_back(op.getMatchingIndexingMap(outOperand));
Location loc = op.getLoc();
SmallVector<utils::IteratorType> iterTypes(op.getNumLoops(),
utils::IteratorType::parallel);
OpBuilder builder(op);
auto newOp = linalg::GenericOp::create(builder, loc, newOperands,
outOperand->get(), maps, iterTypes);
newOp.getRegion().getBlocks().splice(newOp.getRegion().begin(),
op.getRegion().getBlocks());
Block &payload = newOp.getRegion().front();
payload.addArgument(payload.getArguments().back().getType(), loc);
setMarker(newOp, getCopyToWorkgroupMemoryMarker());
}
/// Propagate the shared memory copy into the consumer op if it's a fully
/// parallel linalg.generic.
static bool
propagateCopySourceIntoConsumerGeneric(memref::CopyOp copyOp,
SmallVector<Operation *> &toDelete) {
// Look for a generic Op reading the copyOp target.
Operation *nextOp = copyOp->getNextNode();
while (nextOp) {
if (isMemoryEffectFree(nextOp)) {
nextOp = nextOp->getNextNode();
continue;
}
auto consumer = dyn_cast<linalg::GenericOp>(nextOp);
if (!consumer || consumer.getNumDpsInits() != 1 ||
!consumer.getMatchingIndexingMap(consumer.getDpsInitOperand(0))
.isIdentity()) {
break;
}
if (*consumer.getOutputs().begin() != copyOp.getTarget()) {
break;
}
insertInputValueIntoGeneric(copyOp.getSource(), consumer);
toDelete.push_back(consumer);
return true;
}
return false;
}
/// This is needed because we are doing promotion to shared memory on buffers.
/// This is a fragile and temporary solution until we move to be able to do this
/// kind of transformations on tensors.
void propagateSharedMemoryCopy(mlir::FunctionOpInterface funcOp) {
SmallVector<Operation *> toDelete;
funcOp.walk([&toDelete](memref::CopyOp copyOp) {
if (hasMarker(copyOp, getCopyToWorkgroupMemoryMarker())) {
if (propagateCopyDestIntoProducerFill(copyOp) ||
propagateCopySourceIntoConsumerGeneric(copyOp, toDelete)) {
toDelete.push_back(copyOp.getOperation());
}
}
});
for (Operation *op : toDelete) {
op->erase();
}
}
void insertBarriersAroundSharedMemoryCopy(mlir::FunctionOpInterface funcOp) {
OpBuilder builder(funcOp.getContext());
// Insert barriers before and after copies to workgroup memory and skip
// insert barriers between back to back copy to workgroup memory.
funcOp.walk([&builder](Operation *copyOp) {
if (hasMarker(copyOp, getCopyToWorkgroupMemoryMarker())) {
Operation *prevOp = copyOp->getPrevNode();
if (!prevOp || !hasMarker(prevOp, getCopyToWorkgroupMemoryMarker())) {
builder.setInsertionPoint(copyOp);
gpu::BarrierOp::create(builder, copyOp->getLoc(),
gpu::AddressSpace::Workgroup);
}
Operation *nextOp = copyOp->getNextNode();
if (!nextOp || !hasMarker(nextOp, getCopyToWorkgroupMemoryMarker())) {
builder.setInsertionPointAfter(copyOp);
gpu::BarrierOp::create(builder, copyOp->getLoc(),
gpu::AddressSpace::Workgroup);
}
}
});
}
//===----------------------------------------------------------------------===//
// Reduction utils
//===----------------------------------------------------------------------===//
/// Packs scalar element to its vector equivalent.
/// (i.e f16 -> vector<1xf16> and f32 -> vector<1xf32>)
static Value promoteElementToVector(Location loc, OpBuilder &builder,
Value input) {
VectorType vectorTypeBroadcast = VectorType::get({1}, input.getType());
Value vectorInput =
vector::BroadcastOp::create(builder, loc, vectorTypeBroadcast, input);
return vectorInput;
}
Value packVectorToSupportedWidth(Location loc, OpBuilder &builder,
Value input) {
LLVM_DEBUG({
auto vecType = cast<VectorType>(input.getType());
Type elementType = vecType.getElementType();
assert(vecType.getDimSize(0) * elementType.getIntOrFloatBitWidth() ==
kShuffleBitWidth &&
"vecSize * vecBitWidth needs to packable into 32-bitwidth.");
assert(elementType.isIntOrFloat() &&
"Only int and float packing is supported.");
});
VectorType packed32Type = VectorType::get({1}, builder.getI32Type());
Value packedInputVec =
vector::BitCastOp::create(builder, loc, packed32Type, input);
Value packedInput =
vector::ExtractOp::create(builder, loc, packedInputVec, 0);
return packedInput;
}
Value unpackToVector(Location loc, OpBuilder &builder, Value packedInput,
VectorType targetVecType) {
LLVM_DEBUG({
Type packedType = packedInput.getType();
assert(packedType.isIntOrFloat() && "Only ints and floats are unpackable.");
Type elementType = targetVecType.getElementType();
assert(targetVecType.getDimSize(0) * elementType.getIntOrFloatBitWidth() ==
packedType.getIntOrFloatBitWidth() &&
"packed width needs to be unpackable to vecSize * vecBitWidth.");
});
Value packedVector = promoteElementToVector(loc, builder, packedInput);
Value unpackedVector =
vector::BitCastOp::create(builder, loc, targetVecType, packedVector);
return unpackedVector;
}
/// Emit warp reduction code sequence for a given scalar input value.
static Value warpReduction(Location loc, OpBuilder &builder, Value input,
vector::CombiningKind kind, uint32_t warpSize,
uint32_t numLaneToReduce,
bool expandSubgroupReduce) {
assert(llvm::isPowerOf2_32(numLaneToReduce));
assert((isa<IntegerType, FloatType>(input.getType())) &&
"Input must be a scalar");
IntegerType shuffleIntType = builder.getIntegerType(kShuffleBitWidth);
Type origInputType = input.getType();
const unsigned origBitWidth = origInputType.getIntOrFloatBitWidth();
assert(origBitWidth <= kShuffleBitWidth && "Unsupported input type bitwidth");
const bool needsPacking = kShuffleBitWidth != origBitWidth;
IntegerType equivIntType = builder.getIntegerType(origBitWidth);
// Defer expansion of subgroup reduction until later in pass pipeline to
// enable conditional lowering to DPP ops, for potential perf gains over
// gpu.shuffle ops.
if (!expandSubgroupReduce && numLaneToReduce <= warpSize &&
warpSize % numLaneToReduce == 0) {
gpu::AllReduceOperation gpuReduceKind = combiningKindToAllReduce(kind);
// SPIRV currently doesn't have a lowering for clustered reduction,
// so if possible avoid adding problematic attribute until it is supported.
if (numLaneToReduce == warpSize) {
return gpu::SubgroupReduceOp::create(builder, loc, input, gpuReduceKind,
/*uniform=*/false);
}
return gpu::SubgroupReduceOp::create(builder, loc, input, gpuReduceKind,
/*uniform=*/false, numLaneToReduce);
}
// Otherwise, perform the shuffles over the supported scalar type. For inputs
// of smaller bitwidth, perform packing and unpacking via the supported
// integer type.
auto unpack = [loc, &builder, needsPacking, equivIntType,
origInputType](Value packedVal) -> Value {
if (!needsPacking) {
return packedVal;
}
auto asInt = arith::TruncIOp::create(builder, loc, equivIntType, packedVal);
return arith::BitcastOp::create(builder, loc, origInputType, asInt);
};
auto pack = [loc, &builder, needsPacking, equivIntType,
shuffleIntType](Value unpackedVal) -> Value {
if (!needsPacking) {
return unpackedVal;
}
auto asInt =
arith::BitcastOp::create(builder, loc, equivIntType, unpackedVal);
return arith::ExtUIOp::create(builder, loc, shuffleIntType, asInt);
};
// Lane value always stays in the original type. We use it to perform arith
// reductions.
Value laneVal = input;
// Parallel reduction using butterfly shuffles.
for (uint64_t i = 1; i < numLaneToReduce; i <<= 1) {
Value shuffled = gpu::ShuffleOp::create(builder, loc, pack(laneVal), i,
/*width=*/warpSize,
/*mode=*/gpu::ShuffleMode::XOR)
.getShuffleResult();
laneVal = makeArithReduction(builder, loc, kind, laneVal, unpack(shuffled));
}
// Broadcast the result to all the lanes.
if (warpSize != numLaneToReduce) {
Value shuffled = gpu::ShuffleOp::create(builder, loc, pack(laneVal), 0,
/*width=*/warpSize,
/*mode=*/gpu::ShuffleMode::IDX)
.getShuffleResult();
laneVal = unpack(shuffled);
}
return laneVal;
}
// List of identity elements by operation.
// https://en.wikipedia.org/wiki/Identity_element
static TypedAttr getCombiningKindIdentity(OpBuilder &builder,
vector::CombiningKind combiningKind,
Type type) {
switch (combiningKind) {
case vector::CombiningKind::ADD:
return builder.getZeroAttr(type);
case vector::CombiningKind::MUL: {
if (type.isIntOrIndex()) {
return builder.getIntegerAttr(type, 1);
}
return builder.getFloatAttr(type, 1);
}
case vector::CombiningKind::MINUI:
case vector::CombiningKind::MINSI:
return builder.getIntegerAttr(type, std::numeric_limits<int64_t>::max());
case vector::CombiningKind::MAXUI:
case vector::CombiningKind::MAXSI:
return builder.getIntegerAttr(type, std::numeric_limits<int64_t>::min());
case vector::CombiningKind::AND:
return builder.getIntegerAttr(type, 1);
case vector::CombiningKind::OR:
case vector::CombiningKind::XOR:
return builder.getZeroAttr(type);
case vector::CombiningKind::MINIMUMNUMF:
case vector::CombiningKind::MAXIMUMNUMF:
return builder.getFloatAttr(
type, APFloat::getQNaN(cast<FloatType>(type).getFloatSemantics()));
case vector::CombiningKind::MINIMUMF:
case vector::CombiningKind::MINNUMF: {
auto posInfApFloat = APFloat::getInf(
cast<FloatType>(type).getFloatSemantics(), /*Negative=*/false);
return builder.getFloatAttr(type, posInfApFloat);
}
case vector::CombiningKind::MAXIMUMF:
case vector::CombiningKind::MAXNUMF: {
auto negInfApFloat = APFloat::getInf(
cast<FloatType>(type).getFloatSemantics(), /*Negative=*/true);
return builder.getFloatAttr(type, negInfApFloat);
}
}
return TypedAttr();
}
/// Emit identity variable.
Value getCombiningIdentityValue(Location loc, OpBuilder &builder,
vector::CombiningKind kind, Type identityType) {
auto vectorType = dyn_cast<VectorType>(identityType);
Type elementType = identityType;
if (vectorType) {
elementType = vectorType.getElementType();
}
TypedAttr identityAttr = getCombiningKindIdentity(builder, kind, elementType);
if (vectorType) {
identityAttr = DenseElementsAttr::get(vectorType, identityAttr);
}
assert(identityAttr && "Unknown identity value for the reduction");
Value identity =
arith::ConstantOp::create(builder, loc, identityType, identityAttr);
return identity;
}
/// Returns the matching GPU reduction operation.
gpu::AllReduceOperation combiningKindToAllReduce(vector::CombiningKind kind) {
switch (kind) {
#define MAP_CASE(X) \
case vector::CombiningKind::X: \
return gpu::AllReduceOperation::X
MAP_CASE(ADD);
MAP_CASE(MUL);
MAP_CASE(MINUI);
MAP_CASE(MINSI);
MAP_CASE(MINNUMF);
MAP_CASE(MAXSI);
MAP_CASE(MAXUI);
MAP_CASE(MAXNUMF);
MAP_CASE(AND);
MAP_CASE(OR);
MAP_CASE(XOR);
MAP_CASE(MINIMUMF);
MAP_CASE(MAXIMUMF);
MAP_CASE(MINIMUMNUMF);
MAP_CASE(MAXIMUMNUMF);
#undef MAP_CASE
}
// Upstream LLVM has the same assertion for the reverse direction (see
// mlir::gpu::convertReductionKind).
llvm_unreachable("Vector and GPU reduction kinds should match 1:1");
}
/// Emit reduction across a group for a given input.
Value emitGPUGroupReduction(Location loc, OpBuilder &builder, Value input,
vector::CombiningKind kind, uint32_t size,
int warpSize, bool expandSubgroupReduce) {
assert(
size % warpSize == 0 &&
"Group reduction only support for sizes aligned on warp size for now.");
// First reduce on a single thread to get per lane reduction value.
Value laneVal = vector::ReductionOp::create(builder, loc, kind, input);
laneVal = warpReduction(loc, builder, laneVal, kind, warpSize, warpSize,
expandSubgroupReduce);
// Simple case -- emit `gpu.subgroup_reduce` directly.
if (!expandSubgroupReduce && size == warpSize) {
return laneVal;
}
// More-involved case -- generate `gpu.shuffle` ops over i32 values (using the
// butterfly shuffle algorithm).
// if we have more than one warp, reduce across warps.
if (size > warpSize) {
uint32_t numWarp = size / warpSize;
assert(numWarp <= warpSize &&
"Only support 1 level, need to implement recursive/loop for this "
"case.");
auto addressSpaceAttr = gpu::AddressSpaceAttr::get(
builder.getContext(), gpu::GPUDialect::getWorkgroupAddressSpace());
MemRefType memrefType =
MemRefType::get(numWarp, laneVal.getType(), MemRefLayoutAttrInterface{},
addressSpaceAttr);
Value alloc = memref::AllocOp::create(builder, loc, memrefType);
Value threadX = gpu::ThreadIdOp::create(
builder, loc, builder.getIndexType(), gpu::Dimension::x);
Value cstWarpSize = arith::ConstantIndexOp::create(builder, loc, warpSize);
Value warpId = arith::DivUIOp::create(builder, loc, threadX, cstWarpSize);
Value laneId = arith::RemUIOp::create(builder, loc, threadX, cstWarpSize);
Value zero = arith::ConstantIndexOp::create(builder, loc, 0);
Value lane0 = arith::CmpIOp::create(builder, loc, arith::CmpIPredicate::eq,
laneId, zero);
// Store the reduction for each warp.
SmallVector<Value> indices = {warpId};
scf::IfOp::create(builder, loc, lane0, [&](OpBuilder &b, Location l) {
memref::StoreOp::create(b, l, laneVal, alloc, indices);
scf::YieldOp::create(b, l);
});
gpu::BarrierOp::create(builder, loc, gpu::AddressSpace::Workgroup);
// Further reduce the outputs from each warps with a single warp reduce.
Value memrefSize =
arith::ConstantIndexOp::create(builder, loc, numWarp - 1);
Value laneIdInBounds =
arith::MinUIOp::create(builder, loc, laneId, memrefSize);
Value loadVal = memref::LoadOp::create(builder, loc, alloc, laneIdInBounds);
Value cstNumWarp = arith::ConstantIndexOp::create(builder, loc, numWarp);
if (!llvm::isPowerOf2_32(numWarp)) {
// Pad with identity element if numel < warpSize for valid warp reduction.
Value useIdentityElement = arith::CmpIOp::create(
builder, loc, arith::CmpIPredicate::sge, laneId, cstNumWarp);
numWarp = llvm::PowerOf2Ceil(numWarp);
Value identity =
getCombiningIdentityValue(loc, builder, kind, loadVal.getType());
loadVal = arith::SelectOp::create(builder, loc, useIdentityElement,
identity, loadVal);
}
laneVal = warpReduction(loc, builder, loadVal, kind, warpSize, numWarp,
/*expandSubgroupReduce=*/true);
}
return laneVal;
}
std::optional<SmallVector<int64_t>> getWmmaNativeVectorSize(Operation *op) {
// Currently hardcode the size of wmma operation. When more cases are
// supported this should be picked based on what the backend supports.
int64_t m = 16;
int64_t n = 16;
if (auto contract = dyn_cast<vector::ContractionOp>(op)) {
int64_t k = contract.getLhsType().getElementType().isF16() ? 16 : 8;
SmallVector<int64_t> nativeSize(contract.getIteratorTypes().size() - 3, 1);
nativeSize.append({m, n, k});
return nativeSize;
}
if (auto writeOp = dyn_cast<vector::TransferWriteOp>(op)) {
if (writeOp.getVectorType().getRank() < 2) {
return std::nullopt;
}
SmallVector<int64_t> nativeSize(writeOp.getVectorType().getRank() - 2, 1);
nativeSize.append({m, n});
return nativeSize;
}
if (auto readOp = dyn_cast<vector::TransferReadOp>(op)) {
// Transfer read ops may need different shapes based on how they are being
// used. For simplicity just match the shape used by the extract strided op.
VectorType sliceType;
for (Operation *users : op->getUsers()) {
auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
if (!extract) {
return std::nullopt;
}
auto vecType = cast<VectorType>(extract.getResult().getType());
if (sliceType && sliceType != vecType) {
return std::nullopt;
}
sliceType = vecType;
}
return llvm::to_vector(sliceType.getShape());
}
if ((OpTrait::hasElementwiseMappableTraits(op) && op->getNumResults() == 1)) {
if (auto vecType = dyn_cast<VectorType>(op->getResultTypes()[0])) {
// TODO: The condition for unrolling elementwise should be restricted
// only to operations that need unrolling (connected to the contract).
if (vecType.getRank() < 2) {
return std::nullopt;
}
// First check whether there is a slice to infer the shape from. This is
// required for cases where the accumulator type differs from the input
// types, in which case we will see an `arith.ext_` between the contract
// and transfer_read which needs to be unrolled.
VectorType sliceType;
for (Operation *users : op->getUsers()) {
auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
if (!extract) {
return std::nullopt;
}
auto vecType = cast<VectorType>(extract.getResult().getType());
if (sliceType && sliceType != vecType) {
return std::nullopt;
}
sliceType = vecType;
}
if (sliceType) {
return llvm::to_vector(sliceType.getShape());
}
// Else unroll for trailing elementwise.
SmallVector<int64_t> nativeSize(vecType.getRank() - 2, 1);
// Map elementwise ops to the output shape.
nativeSize.append({m, n});
return nativeSize;
}
}
return std::nullopt;
}
static FailureOr<int64_t>
getOperandBitwidth(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
int operandIndex) {
SmallVector<Type> elementTypes;
intrinsic.getElementTypes(elementTypes);
assert(operandIndex >= 0 && "operand index must be non-negative");
return elementTypes[operandIndex].getIntOrFloatBitWidth();
}
/// Returns the number of elements along the K dimensions (all reduction
/// dimensions) of the given intrinsic. For intrinsics with multiple K
/// dimensions, returns the product of all K dimensions. This is determined by
/// looking at all reduction dimensions in the undistributed tile shape.
static FailureOr<int64_t>
getKSize(IREE::Codegen::InnerTileDescAttrInterface intrinsic) {
int64_t kSize = 1;
int64_t lhsOperandIndex = 0;
SmallVector<VectorType> undistributedTypes;
intrinsic.getUndistributedTileTypes(undistributedTypes);
ArrayRef<int64_t> shape = undistributedTypes[lhsOperandIndex].getShape();
SmallVector<utils::IteratorType> lhsIteratorTypes =
intrinsic.getOperandIteratorTypes()[lhsOperandIndex];
for (auto [dim, iteratorType] : llvm::zip_equal(shape, lhsIteratorTypes)) {
if (iteratorType == utils::IteratorType::reduction) {
kSize *= dim;
}
}
return kSize;
}
/// Returns the number of elements each thread accesses for the given intrinsic
/// and operand index. This is determined by the `element` field of the
/// subgroup layout.
static FailureOr<int64_t>
getNumAccessElems(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
int operandIndex) {
IREE::GPU::MMASingleSubgroupLayout layout =
IREE::GPU::getSingleSubgroupLayout(intrinsic, operandIndex);
return llvm::product_of(layout.element);
}
/// Returns the total number of elements in the shape of the given intrinsic
/// with the given operand index. This is the product of all dimensions in the
/// distributed shape: outer × thread × element.
static FailureOr<int64_t>
getTotalTileElems(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
int operandIndex) {
IREE::GPU::MMASingleSubgroupLayout layout =
IREE::GPU::getSingleSubgroupLayout(intrinsic, operandIndex);
return llvm::product_of(layout.outer) * llvm::product_of(layout.thread) *
llvm::product_of(layout.element);
}
static FailureOr<XorShuffleParams> getXorShuffleParamsForGfx950(
IREE::GPU::TargetAttr target,
IREE::Codegen::InnerTileDescAttrInterface intrinsic) {
if (auto smma = dyn_cast<IREE::GPU::ScaledMMAAttr>(intrinsic)) {
switch (smma.getIntrinsic()) {
case IREE::GPU::ScaledMMAIntrinsic::MFMA_SCALE_F32_16x16x128_B32:
return XorShuffleParams({/*rowElems=*/256,
/*accessElems=*/32});
default:
// TODO(muzasyed): Add more intrinsics for gfx950.
return failure();
}
}
if (auto mma = dyn_cast<IREE::GPU::MMAAttr>(intrinsic)) {
switch (mma.getIntrinsic()) {
case IREE::GPU::MMAIntrinsic::MFMA_F32_16x16x32_F16:
case IREE::GPU::MMAIntrinsic::MFMA_F32_32x32x16_F16:
case IREE::GPU::MMAIntrinsic::MFMA_F32_16x16x32_BF16:
case IREE::GPU::MMAIntrinsic::MFMA_F32_32x32x16_BF16:
return XorShuffleParams({/*rowElems=*/64,
/*accessElems=*/8});
default:
return failure();
}
}
return failure();
}
/// Validate the XOR shuffle parameters for the given intrinsic and operand
/// index. If the parameters produce an XOR Shuffle that is not valid, return
/// failure. Else, return the swizzle parameters.
static FailureOr<XorShuffleParams>
validateXorShuffle(FailureOr<XorShuffleParams> swizzle,
IREE::Codegen::InnerTileDescAttrInterface intrinsic,
int operandIndex) {
FailureOr<int64_t> maybeTotalTileElems =
getTotalTileElems(intrinsic, operandIndex);
if (failed(maybeTotalTileElems)) {
return failure();
}
int64_t totalTileElems = *maybeTotalTileElems;
if (!isXORShuffleValid(swizzle->rowElems, swizzle->accessElems,
totalTileElems)) {
return failure();
}
return swizzle;
}
// Disabling clang-tidy for the following functions, as it will be externally
// linked to the CAPI in a future PR.
// NOLINTBEGIN(misc-use-internal-linkage)
FailureOr<XorShuffleBounds>
getXorShuffleBounds(IREE::Codegen::InnerTileDescAttrInterface intrinsic,
int operandIndex) {
FailureOr<int64_t> maybeMinimumAccessElems =
getNumAccessElems(intrinsic, operandIndex);
FailureOr<int64_t> maybeTotalTileElems =
getTotalTileElems(intrinsic, operandIndex);
if (failed(maybeMinimumAccessElems) || failed(maybeTotalTileElems)) {
return failure();
}
return XorShuffleBounds{*maybeMinimumAccessElems, *maybeTotalTileElems};
}
bool isXORShuffleValid(int64_t numRowElems, int64_t numAccessElems,
int64_t totalTileElems) {
// The number of total tile elements we want to swizzle must be greater than
// or equal to the number of elements in a row.
if (totalTileElems < numRowElems) {
LDBG()
<< "The number of row elements exceed the total elements in the tile";
return false;
}
// We need at least one column of access elements to swizzle within a row.
if (numRowElems < numAccessElems) {
LDBG()
<< "The number of access elements exceed the total elements in the row";
return false;
}
// The size of a row must evenly divide the total number of tile elements.
if (totalTileElems % numRowElems != 0) {
LDBG() << "The number of row elements does not evenly divide the total "
"elements in the tile";
return false;
}
// The number of access elements must evenly divide the number of row
// elements.
if (numRowElems % numAccessElems != 0) {
LDBG() << "The number of access elements does not evenly divide the number "
"of row elements";
return false;
}
// The number of columns in a row must evenly divide the total number of tile
// elements. This is to avoid incomplete rows at the end of the tile.
if (totalTileElems % (numRowElems / numAccessElems) != 0) {
LDBG() << "The number of columns in a row does not evenly divide the total "
"elements in the tile";
return false;
}
return true;
}
FailureOr<XorShuffleParams> getXorShuffleParamsForTunedChipset(
IREE::GPU::TargetAttr target,
IREE::Codegen::InnerTileDescAttrInterface intrinsic, int operandIndex) {
FailureOr<ROCDL::TargetInfo> maybeTargetInfo =
ROCDL::TargetInfo::get(target.getArch());
if (failed(maybeTargetInfo)) {
return failure();
}
if (maybeTargetInfo->has(llvm::AMDGPU::FEAT_GFX950_INSTS)) {
return validateXorShuffle(getXorShuffleParamsForGfx950(target, intrinsic),
intrinsic, operandIndex);
}
return failure();
}
FailureOr<XorShuffleParams> getXorShuffleParamsForUntunedChipset(
IREE::GPU::TargetAttr target,
IREE::Codegen::InnerTileDescAttrInterface intrinsic,
ArrayRef<int64_t> reductionTileSizes, int operandIndex) {
// Compute XOR shuffle swizzle parameters for bank conflict avoidance.
// - rowElems: Select entirety of K Tile size, may not prevent bank
// conflicts if the K tile size is too small.
// - accessElems: number of contiguous elements each thread accesses,
// derived from the MMA intrinsic's element layout.
int64_t numAccessElems = getNumAccessElems(intrinsic, operandIndex).value();
// Calculate K tile size (total K elements in shared memory) to use as row
// width. For small K tiles, this may not reduce bank conflicts effectively.
int64_t kTileSize =
llvm::product_of(reductionTileSizes) * getKSize(intrinsic).value();
// Figure out how many elements can fit across all banks of LDS.
IREE::GPU::TargetWgpAttr wgp = target.getWgp();
FailureOr<int64_t> bitwidth = getOperandBitwidth(intrinsic, operandIndex);
if (failed(bitwidth)) {
return failure();
}
std::optional<int64_t> workgroupMemoryBankCount =
wgp.getWorkgroupMemoryBankCount();
if (!workgroupMemoryBankCount.has_value()) {
return failure();
}
// Assuming each bank is 4 bytes wide (32 bits).
int64_t ldsBankWidthBits =
(workgroupMemoryBankCount.value() * int64_t(32)) / *bitwidth;
// Row width must be less than or equal to the row size (in elements) of LDS
// bank width to prevent bank conflicts.
int64_t effectiverowElems = std::min(ldsBankWidthBits, kTileSize);
// Ensure row width is at least access width (minimum 1 column).
effectiverowElems = std::max(effectiverowElems, numAccessElems);
return validateXorShuffle(XorShuffleParams({/*rowElems=*/effectiverowElems,
/*accessElems=*/numAccessElems}),
intrinsic, operandIndex);
}
FailureOr<XorShuffleParams>
getXorShuffleParams(IREE::GPU::TargetAttr target,
IREE::Codegen::InnerTileDescAttrInterface intrinsic,
ArrayRef<int64_t> reductionTileSizes, int operandIndex,
bool skipUntunedFallback) {
FailureOr<XorShuffleParams> xorShuffleAttr =
getXorShuffleParamsForTunedChipset(target, intrinsic, operandIndex);
if (failed(xorShuffleAttr) && !skipUntunedFallback) {
xorShuffleAttr = getXorShuffleParamsForUntunedChipset(
target, intrinsic, reductionTileSizes, operandIndex);
}
return xorShuffleAttr;
}
// NOLINTEND(misc-use-internal-linkage)
FailureOr<Attribute>
getXorShuffleAttr(MLIRContext *context, Attribute baseConfigAttr,
IREE::GPU::TargetAttr target,
IREE::Codegen::InnerTileDescAttrInterface intrinsic,
ArrayRef<int64_t> reductionTileSizes, int operandIndex,
bool skipUntunedFallback) {
FailureOr<XorShuffleParams> xorShuffleParams = getXorShuffleParams(
target, intrinsic, reductionTileSizes, operandIndex, skipUntunedFallback);
if (failed(xorShuffleParams)) {
return failure();
}
int64_t effectiverowElems = xorShuffleParams.value().rowElems;
int64_t numAccessElems = xorShuffleParams.value().accessElems;
auto swizzleAttr = IREE::Codegen::XORShuffleAttr::get(
context, effectiverowElems, numAccessElems,
/*row_stride=*/int64_t(0),
/*per_phase=*/int64_t(0));
return IREE::GPU::SwizzleOperandAttr::get(context, baseConfigAttr,
swizzleAttr);
}
//===----------------------------------------------------------------------===//
// getMmaNativeVectorSize
//===----------------------------------------------------------------------===//
/// Returns vector::ContractionOp operand's index where the result is used.
static std::optional<int>
getVectorContractOpOperandId(vector::ContractionOp contractOp,
OpResult result) {
if (contractOp.getLhs() == result) {
return 0;
}
if (contractOp.getRhs() == result) {
return 1;
}
if (contractOp.getAcc() == result) {
return 2;
}
return std::nullopt;
}
/// Returns vector::ContractionOp operand's index where the
/// vector::TransferReadOp is consumed either consumed directly or via
/// vector::ExtractStridedSliceOp.
static std::optional<int>
getVectorContractOpOperandIdForVectorReadOp(Operation *op) {
vector::ContractionOp contractOp;
// Check if the vector::TransferReadOp is consumed directly by
// vector::ContractionOp.
if (op->use_empty()) {
return std::nullopt;
}
Operation *firstLevelUser = *((op->getUsers()).begin());
if (!firstLevelUser) {
return std::nullopt;
}
if (auto contractOp = dyn_cast<vector::ContractionOp>(firstLevelUser)) {
return getVectorContractOpOperandId(contractOp, op->getResult(0));
}
// Check if the vector::TransferReadOp is consumed indirectly by
// vector::ContractionOp. Only check until the second level of use-def chain.
if (firstLevelUser->use_empty()) {
return std::nullopt;
}
Operation *secondLevelUser = *((firstLevelUser->getUsers()).begin());
if (!secondLevelUser) {
return std::nullopt;
}
if (auto contractOp = dyn_cast<vector::ContractionOp>(secondLevelUser)) {
return getVectorContractOpOperandId(contractOp,
firstLevelUser->getResult(0));
}
return std::nullopt;
}
/// Helper function to return native size for MMA.SYNC-based operations.
std::optional<SmallVector<int64_t>> getMmaNativeVectorSize(Operation *op) {
// Shape of native Tensor Core GPU mma.sync operations.
int64_t mmaShapeM = 16;
int64_t mmaShapeN = 8;
int64_t mmaShapeK;
// Shape the mma.sync warp-level operation.
if (auto contract = dyn_cast<vector::ContractionOp>(op)) {
Type sourceType = contract.getLhsType().getElementType();
// Set mmaShapeK based on sourceType.
if (sourceType.isInteger(4)) {
mmaShapeK = 64;
} else if (sourceType.isInteger(8)) {
mmaShapeK = 32;
} else if (sourceType.isF16() || sourceType.isBF16()) {
mmaShapeK = 16;
} else if (sourceType.isF32()) {
mmaShapeK = 8;
} else {
LDBG() << "unsupported shape for vector.contract: ";
return std::nullopt;
}
// Initialize/set the starting dims of the ranked shape, such as batch,
// to 1.
SmallVector<int64_t> mmaShape(contract.getIteratorTypes().size() - 3, 1);
mmaShape.append({mmaShapeM, mmaShapeN, mmaShapeK});
LDBG() << "shape for vector.contract: " << llvm::interleaved(mmaShape);
return mmaShape;
}
// Shape of warp-level vector write operation.
if (auto writeOp = dyn_cast<vector::TransferWriteOp>(op)) {
if (writeOp.getVectorType().getRank() < 2) {
return std::nullopt;
}
SmallVector<int64_t> outputShape(writeOp.getVectorType().getRank() - 2, 1);
outputShape.append({mmaShapeM, mmaShapeN});
LDBG() << "shape for vector.xfer_write: " << llvm::interleaved(outputShape);
return outputShape;
}
// Shape of warp-level vector read (load) operation.
if (auto readOp = dyn_cast<vector::TransferReadOp>(op)) {
auto resultVectorType = cast<VectorType>(readOp.getVector().getType());
Type resultElementType = resultVectorType.getElementType();
std::optional<int> operandId =
getVectorContractOpOperandIdForVectorReadOp(op);
if (!operandId) {
LDBG() << "Failed to get operandId for vector::xfer_read: " << *op;
return std::nullopt;
}
// Loading F16 values from Shared Memory to Registers.
if (resultElementType.isF16() || resultElementType.isBF16()) {
// For matrixC.
if (*operandId == 2) {
SmallVector<int64_t> readShape;
readShape.append({mmaShapeM, mmaShapeN});
LLVM_DEBUG({
llvm::interleaveComma(readShape,
DBGS() << "shape for vector.xfer_read: ");
llvm::dbgs() << "\n";
});
return readShape;
}
// For matrixA and matrixB.
if (*operandId == 0 || *operandId == 1) {
// MmaSyncOp input operands: matrixA and matrixB.
// LDSMx1, x2, x4:
// - LDSMx1 loads a 1 tile of 8x8.
// - LDSMx2 loads a 2 tiles of 8x8.
// - LDSMx4 loads a 4 tiles of 8x8. (in use)
// IREE uses the largest tiled load, i.e., LDSMx4.
// MmaSyncOp source operand: matrixC.
// matrixC is also read/written in tiled block of 16x16. In the pass
// OptimizeVectorTransfer, matrixC reads are moved above the mainloop
// and writes are moved below the mainloop. Thus, mma.sync read/write
// accumulator inplace.
SmallVector<int64_t> readShape;
readShape.append({16, 16});
LLVM_DEBUG({
llvm::interleaveComma(readShape,
DBGS() << "shape for vector.xfer_read: ");
llvm::dbgs() << "\n";
});
return readShape;
}
}
// Loading F32 values from Shared Memory to Registers.
if (resultElementType.isF32()) {
// Set mmaShapeK for F32 datatype mma.sync.f32.tf32.m16n8k8.
mmaShapeK = 8;
// For matrixC.
if (*operandId == 2) {
SmallVector<int64_t> readShape;
readShape.append({mmaShapeM, mmaShapeN});
LDBG() << "shape for vector.xfer_read: "
<< llvm::interleaved(readShape);
return readShape;
}
// For matrixA.
if (*operandId == 0) {
SmallVector<int64_t> readShape;
readShape.append({mmaShapeM, mmaShapeK});
LDBG() << "shape for vector.xfer_read: "
<< llvm::interleaved(readShape);
return readShape;
}
// For matrixB.
if (*operandId == 1) {
// Do not use ldmatrix for matrixB.
// Transfer read ops may need different shapes based on how they are
// being used. For simplicity just match the shape used by the extract
// strided op.
VectorType sliceType;
for (Operation *users : op->getUsers()) {
auto extract = dyn_cast<vector::ExtractStridedSliceOp>(users);
if (!extract) {
return std::nullopt;
}
auto vecType = cast<VectorType>(extract.getResult().getType());
if (sliceType && sliceType != vecType) {
return std::nullopt;
}
sliceType = vecType;
}
LDBG() << "shape for vector.xfer_read: "
<< llvm::interleaved(sliceType.getShape());
return llvm::to_vector(sliceType.getShape());
}
}
}
LDBG() << "unsupported shape for " << op->getName().getStringRef();
return std::nullopt;
}
bool hasGlobalMemoryAddressSpace(MemRefType memrefType) {
Attribute addrSpace = memrefType.getMemorySpace();
if (!addrSpace) {
return true;
}
auto intAttr = dyn_cast<IntegerAttr>(addrSpace);
// Accept both default numeric address space and HAL descriptor type address
// space--the former is used by LLVMGPU while the latter is used by SPIR-V.
if (intAttr && intAttr.getInt() == 0) {
return true;
}
auto gpuAttr = dyn_cast<gpu::AddressSpaceAttr>(addrSpace);
if (gpuAttr && gpuAttr.getValue() == gpu::AddressSpace::Global) {
return true;
}
auto amdgpuAttr = dyn_cast<amdgpu::AddressSpaceAttr>(addrSpace);
if (amdgpuAttr &&
amdgpuAttr.getValue() == amdgpu::AddressSpace::FatRawBuffer) {
return true;
}
return isa<IREE::HAL::DescriptorTypeAttr>(addrSpace);
}
bool hasAMDGPUFatRawBufferAddressSpace(MemRefType memrefType) {
Attribute addrSpace = memrefType.getMemorySpace();
if (!addrSpace) {
return false;
}
auto amdgpuAttr = dyn_cast<amdgpu::AddressSpaceAttr>(addrSpace);
if (amdgpuAttr &&
amdgpuAttr.getValue() == amdgpu::AddressSpace::FatRawBuffer) {
return true;
}
return false;
}
bool hasSharedMemoryAddressSpace(MemRefType memrefType) {
auto addrSpace =
dyn_cast_if_present<gpu::AddressSpaceAttr>(memrefType.getMemorySpace());
return addrSpace &&
addrSpace.getValue() == gpu::GPUDialect::getWorkgroupAddressSpace();
}
//===----------------------------------------------------------------------===//
// GPU CodeGen op filter
//===----------------------------------------------------------------------===//
/// Returns true if the index map represents a transpose that benefits from
/// shared mem.
bool sharedMemTransposeFilter(AffineMap indexMap) {
if (!indexMap.isEmpty() && indexMap.isPermutation()) {
// Ensure that the fasted moving dimension (the last one) is permuted,
// Otherwise shared memory promotion will not benefit the operation.
if (indexMap.getDimPosition(indexMap.getNumDims() - 1) !=
indexMap.getNumDims() - 1) {
return true;
}
}
return false;
}
//===----------------------------------------------------------------------===//
// GPU Target Information
//===----------------------------------------------------------------------===//
IREE::GPU::TargetAttr getCLGPUTarget(MLIRContext *context) {
if (clTestTarget.empty()) {
return nullptr;
}
auto [archAndFeatures, backend] = StringRef(clTestTarget).split("@");
if (backend.empty()) {
// Guess what the target API is based on common scheme. This does not work
// for cases like "ampere" which can be accepted by both CUDA and Vulkan;
// it's very limited. So it's targeting common cases to make writing tests
// simpler.
if (StringRef(clTestTarget).starts_with("sm_")) {
backend = "cuda";
} else if (StringRef(clTestTarget).starts_with("gfx")) {
backend = "hip";
} else if (StringRef(clTestTarget).starts_with("adreno")) {
backend = "vulkan";
} else if (StringRef(clTestTarget).starts_with("apple")) {
backend = "vulkan";
} else if (StringRef(clTestTarget).starts_with("valhall")) {
backend = "vulkan";
} else if (StringRef(clTestTarget).starts_with("videocore")) {
backend = "vulkan";
}
}
auto [arch, features] = StringRef(archAndFeatures).split(':');
// Use the target specified in the command line for testing purposes.
return IREE::GPU::getFullTarget(backend, arch, features, context);
}
IREE::GPU::TargetAttr getGPUTargetAttr(DictionaryAttr attr) {
return dyn_cast_if_present<IREE::GPU::TargetAttr>(getConfigTargetInfo(attr));
}
IREE::GPU::TargetAttr getGPUTargetAttr(MLIRContext *context,
IREE::HAL::ExecutableTargetAttr target) {
IREE::GPU::TargetAttr gpuTargetAttr;
if (target) {
gpuTargetAttr = getGPUTargetAttr(target.getConfiguration());
}
if (!gpuTargetAttr) {
gpuTargetAttr = getCLGPUTarget(context);
}
return gpuTargetAttr;
}
IREE::GPU::TargetAttr getGPUTargetAttr(Operation *op) {
return getGPUTargetAttr(op->getContext(),
IREE::HAL::ExecutableTargetAttr::lookup(op));
}
bool targetSupportsGlobalLoadDMA(IREE::GPU::TargetAttr target) {
if (!target) {
return false;
}
FailureOr<ROCDL::TargetInfo> maybeTargetInfo =
ROCDL::TargetInfo::get(target.getArch());
if (failed(maybeTargetInfo)) {
return false;
}
return maybeTargetInfo->has(llvm::AMDGPU::FEAT_GFX950_INSTS);
}
bool targetSupportsShuffleBitwidth(IREE::GPU::TargetAttr target,
unsigned bitwidth) {
if (bitwidth <= kShuffleNativeBits) {
return true;
}
return target && target.isAMD();
}
void addConfigGPUTarget(MLIRContext *context,
IREE::GPU::TargetAttr gpuTargetAttr,
SmallVectorImpl<NamedAttribute> &config) {
addConfigTargetInfo(context, gpuTargetAttr, config);
}
IntegerAttr getConfigWavesPerEuAttr(DictionaryAttr targetConfig) {
return targetConfig.getAs<IntegerAttr>(kWavesPerEuAttrName);
}
std::optional<int64_t> getConfigWavesPerEu(DictionaryAttr targetConfig) {
auto attr = getConfigWavesPerEuAttr(targetConfig);
if (attr) {
return attr.getInt();
}
return std::nullopt;
}
void addConfigWavesPerEu(MLIRContext *context, int64_t wavesPerEu,
SmallVectorImpl<NamedAttribute> &config) {
config.emplace_back(
StringAttr::get(context, kWavesPerEuAttrName),
IntegerAttr::get(IntegerType::get(context, 64), wavesPerEu));
}
std::optional<int> getGPUSubgroupSize(mlir::FunctionOpInterface func) {
// First try to see if there is a subgroup size chosen in the CodeGen pipeline
// configuration.
if (std::optional<int64_t> subgroupSize = getSubgroupSize(func)) {
return subgroupSize.value();
}
// Then try to find the subgroup size from the target description.
if (IREE::GPU::TargetAttr target = getGPUTargetAttr(func)) {
return target.getPreferredSubgroupSize();
}
return std::nullopt;
}
SmallVector<IREE::HAL::ExecutableVariantOp>
getExecutableVariantOps(mlir::ModuleOp moduleOp) {
// The variant ops must have a ExecutableOp parent. Thus we iterate on the
// ExecutableOp using `getOps()` for efficiency. We do not need to walk
// through all the ops in the `moduleOp` in a nested fashion.
SmallVector<IREE::HAL::ExecutableVariantOp> executableVariantOps;
for (auto executableOp : moduleOp.getOps<IREE::HAL::ExecutableOp>()) {
auto iter = executableOp.getOps<IREE::HAL::ExecutableVariantOp>();
executableVariantOps.append(iter.begin(), iter.end());
}
return executableVariantOps;
}
Value applyInverseXorSwizzleToDMASourceOffset(
OpBuilder &builder, Location loc, Value srcLinearOffset,
IREE::Codegen::XORShuffleAttr swizzle, Value dest) {
// Compute the subgroup's base offset within the full allocation by tracing
// through view-like ops and accumulating the linear element offset.
Value destTrace = dest;
OpFoldResult totalOffset = builder.getIndexAttr(0);
while (auto viewOp = destTrace.getDefiningOp<ViewLikeOpInterface>()) {
if (auto subviewOp = dyn_cast<memref::SubViewOp>(viewOp.getOperation())) {
auto parentType = cast<MemRefType>(subviewOp.getSource().getType());
SmallVector<int64_t> strides;
int64_t offset;
LogicalResult res = parentType.getStridesAndOffset(strides, offset);
assert(succeeded(res) && "expected strided layout on subview source");
(void)res;
SmallVector<OpFoldResult> strideOFRs =
getAsIndexOpFoldResult(builder.getContext(), strides);
auto &&[expr, values] = computeLinearIndex(totalOffset, strideOFRs,
subviewOp.getMixedOffsets());
totalOffset =
affine::makeComposedFoldedAffineApply(builder, loc, expr, values);
} else {
assert((isa<memref::ExpandShapeOp, memref::CollapseShapeOp>(
viewOp.getOperation())) &&
"unexpected view-like op in dest chain");
}
destTrace = viewOp.getViewSource();
}
// Only the offset unaligned to the swizzle period affects the XOR
// computation.
auto cst = getConstantIntValue(totalOffset);
Value swizzleBaseOffset;
int64_t swizzlePeriod = swizzle.getSwizzlePeriod();
if (!cst || (*cst % swizzlePeriod != 0)) {
swizzleBaseOffset =
getValueOrCreateConstantIndexOp(builder, loc, totalOffset);
}
// Add the subgroup's base offset within the full allocation.
Value swizzleInput = srcLinearOffset;
if (swizzleBaseOffset) {
swizzleInput =
arith::AddIOp::create(builder, loc, swizzleBaseOffset, srcLinearOffset);
}
// Apply the swizzle (handles access-width alignment internally).
Value swizzled = getValueOrCreateConstantIndexOp(
builder, loc, swizzle.swizzleOffset(builder, loc, swizzleInput, dest));
// Subtract the subgroup base offset.
if (swizzleBaseOffset) {
return arith::SubIOp::create(builder, loc, swizzled, swizzleBaseOffset);
}
return swizzled;
}
} // namespace mlir::iree_compiler