blob: 8f09f6f932f8adcee4df02c22f1bee6f2b8e9391 [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/Debug.h"
#include "llvm/Support/ErrorHandling.h"
#include "mlir/Dialect/Affine/IR/AffineOps.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.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")
#define LDBG(X) LLVM_DEBUG(DBGS() << X << "\n")
static constexpr unsigned kShuffleBitWidth = 32;
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] = {
builder.create<mlir::gpu::ThreadIdOp>(loc, indexType, dimAttr[i]),
builder.create<mlir::arith::ConstantOp>(
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 =
builder.create<mlir::gpu::ThreadIdOp>(loc, indexType, dimAttr[i]);
if (i == 0) {
mlir::AffineExpr d0 = builder.getAffineDimExpr(0);
subgroupId = mlir::affine::makeComposedAffineApply(
builder, loc, d0.floorDiv(builder.getAffineConstantExpr(warpSize)),
{subgroupId});
}
procInfo[numDims - 1 - i] = {
subgroupId,
builder.create<mlir::arith::ConstantOp>(
loc, builder.getIndexAttr(numSubgroups[i])),
linalg::DistributionMethod::Cyclic};
}
return procInfo;
}
bool isDescendingRelativeMappingIndices(ArrayRef<Attribute> array) {
int64_t prev =
llvm::cast<DeviceMappingAttrInterface>(array[0]).getRelativeIndex();
for (Attribute attr : array.drop_front()) {
int64_t relativeIndex =
llvm::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.size() == 0)
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 = builder.create<memref::AllocOp>(funcOp.getLoc(), type);
return buffer;
}
LogicalResult deallocateWorkgroupMemory(OpBuilder &, Value /*buffer*/) {
return success();
}
LogicalResult copyToWorkgroupMemory(OpBuilder &b, Value src, Value dst) {
Operation *copyOp = b.create<memref::CopyOp>(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 = builder.create<linalg::GenericOp>(
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);
builder.create<gpu::BarrierOp>(copyOp->getLoc());
}
Operation *nextOp = copyOp->getNextNode();
if (!nextOp || !hasMarker(nextOp, getCopyToWorkgroupMemoryMarker())) {
builder.setInsertionPointAfter(copyOp);
builder.create<gpu::BarrierOp>(copyOp->getLoc());
}
}
});
}
//===----------------------------------------------------------------------===//
// 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 =
builder.create<vector::BroadcastOp>(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 =
builder.create<vector::BitCastOp>(loc, packed32Type, input);
Value packedInput = builder.create<vector::ExtractOp>(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 =
builder.create<vector::BitCastOp>(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) {
assert(llvm::isPowerOf2_32(numLaneToReduce));
assert((llvm::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);
// Always 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 = builder.create<arith::TruncIOp>(loc, equivIntType, packedVal);
return builder.create<arith::BitcastOp>(loc, origInputType, asInt);
};
auto pack = [loc, &builder, needsPacking, equivIntType,
shuffleIntType](Value unpackedVal) -> Value {
if (!needsPacking)
return unpackedVal;
auto asInt =
builder.create<arith::BitcastOp>(loc, equivIntType, unpackedVal);
return builder.create<arith::ExtUIOp>(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 = builder
.create<gpu::ShuffleOp>(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 = builder
.create<gpu::ShuffleOp>(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::MINIMUMF:
case vector::CombiningKind::MINNUMF: {
auto posInfApFloat = APFloat::getInf(
llvm::cast<FloatType>(type).getFloatSemantics(), /*Negative=*/false);
return builder.getFloatAttr(type, posInfApFloat);
}
case vector::CombiningKind::MAXIMUMF:
case vector::CombiningKind::MAXNUMF: {
auto negInfApFloat = APFloat::getInf(
llvm::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 = llvm::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 =
builder.create<arith::ConstantOp>(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);
#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.");
if (!expandSubgroupReduce && size == warpSize) {
auto gpuReduceKind = combiningKindToAllReduce(kind);
// Simple case -- emit `gpu.subgroup_reduce` directly.
Value laneVal = builder.create<vector::ReductionOp>(loc, kind, input);
return builder.create<gpu::SubgroupReduceOp>(loc, laneVal, gpuReduceKind,
/*uniform=*/false);
}
// More-involved case -- generate `gpu.shuffle` ops over i32 values (using the
// butterfly shuffle algorithm).
//
// First reduce on a single thread to get per lane reduction value.
Value laneVal = builder.create<vector::ReductionOp>(loc, kind, input);
laneVal = warpReduction(loc, builder, laneVal, kind, warpSize, warpSize);
// 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 = builder.create<memref::AllocOp>(loc, memrefType);
Value threadX = builder.create<gpu::ThreadIdOp>(loc, builder.getIndexType(),
gpu::Dimension::x);
Value cstWarpSize = builder.create<arith::ConstantIndexOp>(loc, warpSize);
Value warpId = builder.create<arith::DivUIOp>(loc, threadX, cstWarpSize);
Value laneId = builder.create<arith::RemUIOp>(loc, threadX, cstWarpSize);
Value zero = builder.create<arith::ConstantIndexOp>(loc, 0);
Value lane0 = builder.create<arith::CmpIOp>(loc, arith::CmpIPredicate::eq,
laneId, zero);
// Store the reduction for each warp.
SmallVector<Value> indices = {warpId};
builder.create<scf::IfOp>(loc, lane0, [&](OpBuilder &b, Location l) {
b.create<memref::StoreOp>(l, laneVal, alloc, indices);
b.create<scf::YieldOp>(l, std::nullopt);
});
builder.create<gpu::BarrierOp>(loc);
// Further reduce the outputs from each warps with a single warp reduce.
Value memrefSize = builder.create<arith::ConstantIndexOp>(loc, numWarp - 1);
Value laneIdInBounds =
builder.create<arith::MinUIOp>(loc, laneId, memrefSize);
Value loadVal = builder.create<memref::LoadOp>(loc, alloc, laneIdInBounds);
Value cstNumWarp = builder.create<arith::ConstantIndexOp>(loc, numWarp);
if (!llvm::isPowerOf2_32(numWarp)) {
// Pad with identity element if numel < warpSize for valid warp reduction.
Value useIdentityElement = builder.create<arith::CmpIOp>(
loc, arith::CmpIPredicate::sge, laneId, cstNumWarp);
numWarp = llvm::PowerOf2Ceil(numWarp);
Value identity =
getCombiningIdentityValue(loc, builder, kind, loadVal.getType());
loadVal = builder.create<arith::SelectOp>(loc, useIdentityElement,
identity, loadVal);
}
laneVal = warpReduction(loc, builder, loadVal, kind, warpSize, numWarp);
}
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 = llvm::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 = llvm::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 = llvm::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;
}
//===----------------------------------------------------------------------===//
// 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});
LLVM_DEBUG({
llvm::interleaveComma(mmaShape, DBGS() << "shape for vector.contract: ");
llvm::dbgs() << "\n";
});
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});
LLVM_DEBUG({
llvm::interleaveComma(outputShape,
DBGS() << "shape for vector.xfer_write: ");
llvm::dbgs() << "\n";
});
return outputShape;
}
// Shape of warp-level vector read (load) operation.
if (auto readOp = dyn_cast<vector::TransferReadOp>(op)) {
auto resultVectorType =
llvm::cast<VectorType>(readOp.getVector().getType());
Type resultElementType = resultVectorType.getElementType();
std::optional<int> operandId =
getVectorContractOpOperandIdForVectorReadOp(op);
if (!operandId) {
LLVM_DEBUG({
DBGS() << "Failed to get operandId for vector::xfer_read: " << *op
<< "\n";
});
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});
LLVM_DEBUG({
llvm::interleaveComma(readShape,
DBGS() << "shape for vector.xfer_read: ");
llvm::dbgs() << "\n";
});
return readShape;
}
// For matrixA.
if (*operandId == 0) {
SmallVector<int64_t> readShape;
readShape.append({mmaShapeM, mmaShapeK});
LLVM_DEBUG({
llvm::interleaveComma(readShape,
DBGS() << "shape for vector.xfer_read: ");
llvm::dbgs() << "\n";
});
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 = llvm::cast<VectorType>(extract.getResult().getType());
if (sliceType && sliceType != vecType)
return std::nullopt;
sliceType = vecType;
}
LLVM_DEBUG({
llvm::interleaveComma(sliceType.getShape(),
DBGS() << "shape for vector.xfer_read: ");
llvm::dbgs() << "\n";
});
return llvm::to_vector(sliceType.getShape());
}
}
}
LDBG("unsupported shape for " << op->getName().getStringRef());
return std::nullopt;
}
bool hasSharedMemoryAddressSpace(MemRefType memrefType) {
auto addrSpace = llvm::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";
}
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(IREE::HAL::ExecutableTargetAttr target) {
if (auto config = target.getConfiguration()) {
if (auto attr = config.getAs<IREE::GPU::TargetAttr>("iree.gpu.target"))
return attr;
}
return getCLGPUTarget(target.getContext());
}
IREE::GPU::TargetAttr getGPUTargetAttr(Operation *op) {
if (auto target = IREE::HAL::ExecutableTargetAttr::lookup(op)) {
return getGPUTargetAttr(target);
}
return getCLGPUTarget(op->getContext());
}
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) {
SmallVector<IREE::HAL::ExecutableVariantOp> executableVariantOps;
moduleOp.walk([&](IREE::HAL::ExecutableVariantOp executableOp) {
executableVariantOps.push_back(executableOp);
});
return executableVariantOps;
}
SmallVector<IREE::GPU::MMAIntrinsic>
queryMMAIntrinsics(IREE::HAL::ExecutableVariantOp executableOp) {
SmallVector<IREE::GPU::MMAIntrinsic> mmaIntrinsics;
if (IREE::GPU::TargetAttr target = getGPUTargetAttr(executableOp)) {
mmaIntrinsics = llvm::map_to_vector(
target.getWgp().getMma(),
[](IREE::GPU::MMAAttr attr) { return attr.getIntrinsic().getValue(); });
}
return mmaIntrinsics;
}
} // namespace mlir::iree_compiler