blob: c0b3adf8402e03b613121be29781c03a2fc9fa2d [file] [edit]
//===-- XeGPUToXeVM.cpp - XeGPU to XeVM dialect conversion ------*- C++ -*-===//
//
// This file is 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 "mlir/Conversion/XeGPUToXeVM/XeGPUToXeVM.h"
#include "mlir/Dialect/LLVMIR/LLVMTypes.h"
#include "mlir/Dialect/LLVMIR/XeVMDialect.h"
#include "mlir/Conversion/LLVMCommon/Pattern.h"
#include "mlir/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/Index/IR/IndexDialect.h"
#include "mlir/Dialect/Index/IR/IndexOps.h"
#include "mlir/Dialect/LLVMIR/FunctionCallUtils.h"
#include "mlir/Dialect/LLVMIR/LLVMDialect.h"
#include "mlir/Dialect/MemRef/IR/MemRef.h"
#include "mlir/Dialect/SCF/IR/SCF.h"
#include "mlir/Dialect/SCF/Transforms/Patterns.h"
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/Dialect/XeGPU/IR/XeGPU.h"
#include "mlir/Dialect/XeGPU/Utils/XeGPUUtils.h"
#include "mlir/Dialect/XeGPU/uArch/uArchCommon.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Support/LLVM.h"
#include "llvm/ADT/STLExtras.h"
#include "llvm/Support/FormatVariadic.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Types.h"
#include "llvm/ADT/TypeSwitch.h"
#include <string>
namespace mlir {
#define GEN_PASS_DEF_CONVERTXEGPUTOXEVMPASS
#include "mlir/Conversion/Passes.h.inc"
} // namespace mlir
using namespace mlir;
namespace {
// TODO: Below are uArch dependent values, should move away from hardcoding
static constexpr int32_t systolicDepth{8};
static constexpr int32_t executionSize{16};
// Offsets to individual fields of the 8xi32 layout nd tensor descriptor.
enum class NdTdescOffset : uint32_t {
BasePtr = 0, // Base pointer (i64)
BaseShapeW = 2, // Base shape width (i32)
BaseShapeH = 3, // Base shape height (i32)
BasePitch = 4, // Base pitch/stride of dim rank-2 (i32)
LeadingStride0 = 5, // Row strides of the leading (batch) dims of a >2D
LeadingStride1 = 6, // descriptor (i32); added into offset_h by the load/store
LeadingStride2 = 7, // lowering. Left at 0 for 2D descriptors.
};
// Spare payload slots above, and the resulting max lowerable descriptor rank.
static constexpr int64_t maxNdTdescLeadingDims{3};
static constexpr int64_t maxNdTdescRank{2 + maxNdTdescLeadingDims};
static int32_t getNumericXeVMAddrSpace(xegpu::MemorySpace xeGpuMemspace) {
switch (xeGpuMemspace) {
case xegpu::MemorySpace::Global:
return static_cast<int>(xevm::AddrSpace::GLOBAL);
case xegpu::MemorySpace::SLM:
return static_cast<int>(xevm::AddrSpace::SHARED);
}
llvm_unreachable("Unknown XeGPU memory space");
}
/// Translates a memref memory space attribute into XeVM's numeric address
/// space, which follows the OpenCL/SPIR-V convention (0 = private, 1 =
/// global, 2 = constant, 3 = shared/local, 4 = generic). A null attribute,
/// meaning the memory space was left unspecified, maps to the default space
/// 0. Returns failure if `memSpace` is a representation this pass does not
/// know how to translate (e.g. a SPIR-V storage class or an arbitrary string
/// attribute), rather than assuming it is an `IntegerAttr` and asserting.
static FailureOr<unsigned> getNumericMemorySpace(Attribute memSpace) {
if (!memSpace)
return 0u;
if (auto intAttr = llvm::dyn_cast<IntegerAttr>(memSpace))
return static_cast<unsigned>(intAttr.getInt());
if (auto xevmSpace = llvm::dyn_cast<xevm::AddrSpaceAttr>(memSpace))
return static_cast<unsigned>(xevmSpace.getValue());
if (auto gpuSpace = llvm::dyn_cast<gpu::AddressSpaceAttr>(memSpace)) {
switch (gpuSpace.getValue()) {
case gpu::AddressSpace::Global:
return static_cast<unsigned>(xevm::AddrSpace::GLOBAL);
case gpu::AddressSpace::Workgroup:
return static_cast<unsigned>(xevm::AddrSpace::SHARED);
case gpu::AddressSpace::Private:
return static_cast<unsigned>(xevm::AddrSpace::PRIVATE);
case gpu::AddressSpace::Constant:
return static_cast<unsigned>(xevm::AddrSpace::CONSTANT);
}
llvm_unreachable("Unknown GPU address space");
}
return failure();
}
/// Checks if the given MemRefType refers to shared memory.
static bool isSharedMemRef(const MemRefType &memrefTy) {
FailureOr<unsigned> addrSpace =
getNumericMemorySpace(memrefTy.getMemorySpace());
return succeeded(addrSpace) &&
*addrSpace == static_cast<unsigned>(xevm::AddrSpace::SHARED);
}
// Get same bitwidth flat vector type of new element type.
static VectorType encodeVectorTypeTo(VectorType currentVecType,
Type toElemType) {
auto elemType = currentVecType.getElementType();
auto currentBitWidth = elemType.getIntOrFloatBitWidth();
auto newBitWidth = toElemType.getIntOrFloatBitWidth();
const int size =
currentVecType.getNumElements() * currentBitWidth / newBitWidth;
return VectorType::get(size, toElemType);
}
static xevm::LoadCacheControl
translateLoadXeGPUCacheHint(std::optional<xegpu::CachePolicy> L1hint,
std::optional<xegpu::CachePolicy> L3hint) {
// If no hints are provided, use the default cache control.
if (!L1hint && !L3hint)
return xevm::LoadCacheControl::USE_DEFAULT;
// If only one of the hints is provided, use the default for the other level.
auto L1hintVal = L1hint.value_or(xegpu::CachePolicy::CACHED);
auto L3hintVal = L3hint.value_or(xegpu::CachePolicy::CACHED);
switch (L1hintVal) {
case xegpu::CachePolicy::CACHED:
if (L3hintVal == xegpu::CachePolicy::CACHED)
return xevm::LoadCacheControl::L1C_L2UC_L3C;
else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::LoadCacheControl::L1C_L2UC_L3UC;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::UNCACHED:
if (L3hintVal == xegpu::CachePolicy::CACHED)
return xevm::LoadCacheControl::L1UC_L2UC_L3C;
else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::LoadCacheControl::L1UC_L2UC_L3UC;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::STREAMING:
if (L3hintVal == xegpu::CachePolicy::CACHED)
return xevm::LoadCacheControl::L1S_L2UC_L3C;
else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::LoadCacheControl::L1S_L2UC_L3UC;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::READ_INVALIDATE:
return xevm::LoadCacheControl::INVALIDATE_READ;
default:
llvm_unreachable("Unsupported cache control.");
}
}
static xevm::StoreCacheControl
translateStoreXeGPUCacheHint(std::optional<xegpu::CachePolicy> L1hint,
std::optional<xegpu::CachePolicy> L3hint) {
// If no hints are provided, use the default cache control.
if (!L1hint && !L3hint)
return xevm::StoreCacheControl::USE_DEFAULT;
// If only one of the hints is provided, use the default for the other level.
auto L1hintVal = L1hint.value_or(xegpu::CachePolicy::UNCACHED);
auto L3hintVal = L3hint.value_or(xegpu::CachePolicy::WRITE_BACK);
switch (L1hintVal) {
case xegpu::CachePolicy::UNCACHED:
if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::StoreCacheControl::L1UC_L2UC_L3UC;
else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
return xevm::StoreCacheControl::L1UC_L2UC_L3WB;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::STREAMING:
if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::StoreCacheControl::L1S_L2UC_L3UC;
else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
return xevm::StoreCacheControl::L1S_L2UC_L3WB;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::WRITE_BACK:
if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::StoreCacheControl::L1WB_L2UC_L3UC;
else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
return xevm::StoreCacheControl::L1WB_L2UC_L3WB;
else
llvm_unreachable("Unsupported cache control.");
case xegpu::CachePolicy::WRITE_THROUGH:
if (L3hintVal == xegpu::CachePolicy::UNCACHED)
return xevm::StoreCacheControl::L1WT_L2UC_L3UC;
else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
return xevm::StoreCacheControl::L1WT_L2UC_L3WB;
else
llvm_unreachable("Unsupported cache control.");
default:
llvm_unreachable("Unsupported cache control.");
}
}
//
// Note:
// Block operations for tile of sub byte element types are handled by
// emulating with larger element types.
// Tensor descriptor are keep intact and only ops consuming them are
// emulated
//
//
// High-D (>2D) nd descriptors are lowered by viewing the source as a single
// flattened 2D plane, so the 2D-block surface covers every leading (batch)
// plane at once and a batch position becomes a row offset into it. The surface
// height is the row extent the leading dims reach:
// base_height = size[R-2] + sum_d (size[d] - 1) * (stride[d] / stride[R-2])
//
// Leaving `base_ptr` at the true base means an out-of-range batch index lands
// past `base_height`, where the HW boundary check handles it, instead of aiming
// the surface at unmapped memory. Encoding the leading strides as row counts
// (`stride[d] / stride[R-2]`) also makes them dimensionless, so they survive
// element-type repacking (e.g. the f16 -> i32 transpose repack) without a unit
// conversion.
//
// Limitations of the flattened-plane view:
// 1. Each leading stride must be a whole number of rows. A source with gaps
// between planes (`stride[d] % stride[R-2] != 0`) is not lowered. This can
// only be checked when the strides are static; for dynamic strides the
// divisibility is assumed.
// 2. `base_height` grows with the leading extent, so a source with a large
// batch x head x sequence extent can exceed the HW 2D-block surface height.
// 3. Plane boundaries are invisible to the boundary check, which only knows
// the surface: a tile whose rows run past `size[R-2]` spills into the
// following rows of the surface -- the next plane, or the padding between
// planes -- rather than being clipped there as it would be on a per-plane
// surface. A load reads those rows instead of returning zeros, and a store
// overwrites them, so it can corrupt the next plane. This only matters
// when `size[R-2]` is not a multiple of the tile height.
//
class CreateNdDescToXeVMPattern
: public OpConversionPattern<xegpu::CreateNdDescOp> {
using OpConversionPattern::OpConversionPattern;
// Checks the surface width and pitch against the target's 2D block memory
// restrictions. Only these two are enforced: both are backed by the source's
// static shape and strides, whereas the base address alignment is a property
// of the allocation that is not visible here. The width and the pitch are
// checked independently, each only when it is static; a value known only at
// runtime is left alone, as guarding it inside the kernel would cost far more
// than it is worth.
//
// A violation is a dead end rather than a pattern that does not apply: the op
// has no other legalization path, so it is reported with an error that names
// the limit it misses.
LogicalResult checkMemoryRestrictions(xegpu::CreateNdDescOp op,
int64_t rank) const {
const auto *uArch =
xegpu::uArch::getUArch(xegpu::getChipStr(op).value_or(""));
// Without a known target chip there is no uArch to check against, so the
// descriptor must be accepted as-is.
if (!uArch)
return success();
const xegpu::uArch::BlockIOMemoryRestrictions &limits =
uArch->getBlockIOMemoryRestrictions();
// Recover the width and the pitch as a number of elements. A memref source
// carries both in its type, and either one may be dynamic independently of
// the other, so they are queried separately instead of being dropped
// together when the source is not fully static. `getMixedSizes` /
// `getMixedStrides` are only consulted for an integer source, where the op
// itself holds the extents.
std::optional<int64_t> widthElems;
std::optional<int64_t> pitchElems;
if (auto memrefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
if (!ShapedType::isDynamic(memrefTy.getDimSize(rank - 1)))
widthElems = memrefTy.getDimSize(rank - 1);
// The source is known to be strided; the caller checked that already.
SmallVector<int64_t> staticStrides;
int64_t staticOffset;
if (succeeded(
memrefTy.getStridesAndOffset(staticStrides, staticOffset)) &&
!ShapedType::isDynamic(staticStrides[rank - 2]))
pitchElems = staticStrides[rank - 2];
} else {
widthElems = getConstantIntValue(op.getMixedSizes()[rank - 1]);
pitchElems = getConstantIntValue(op.getMixedStrides()[rank - 2]);
}
unsigned elemBitSize =
op.getType().getElementType().getIntOrFloatBitWidth();
// The restrictions are expressed in bytes, but a sub-byte element type can
// leave the width or the pitch on a fraction of a byte, so the comparisons
// are done in bits. Such a size meets no byte alignment at all and is
// reported as a violation rather than waved through.
auto describe = [](int64_t bits) -> std::string {
if (bits % 8 == 0)
return llvm::formatv("{0} bytes", bits / 8).str();
return llvm::formatv("{0} bits", bits).str();
};
auto check = [&](std::optional<int64_t> elems, int64_t minBytes,
int64_t alignBytes,
llvm::StringRef what) -> LogicalResult {
if (!elems)
return success();
int64_t bits = *elems * elemBitSize;
if (bits < minBytes * 8)
return op.emitError(llvm::formatv(
"2D block surface {0} must be at least {1} bytes on this "
"target, got {2}.",
what, minBytes, describe(bits)));
if (alignBytes > 0 && bits % (alignBytes * 8) != 0)
return op.emitError(llvm::formatv(
"2D block surface {0} must be a multiple of {1} bytes on this "
"target, got {2}.",
what, alignBytes, describe(bits)));
return success();
};
if (failed(check(widthElems, limits.minBaseWidthBytes,
limits.baseWidthAlignmentBytes, "width")))
return failure();
// The pitch is the row stride of the innermost 2D tile.
if (failed(check(pitchElems, limits.minBasePitchBytes,
limits.basePitchAlignmentBytes, "pitch")))
return failure();
// Consecutive rows may not overlap, so the pitch has to span the full
// width.
if (widthElems && pitchElems && *pitchElems < *widthElems)
return op.emitError(
llvm::formatv("2D block surface pitch must be at least the surface "
"width, got a pitch of {0} for a surface {1} wide.",
describe(*pitchElems * elemBitSize),
describe(*widthElems * elemBitSize)));
return success();
}
LogicalResult
matchAndRewrite(xegpu::CreateNdDescOp op,
xegpu::CreateNdDescOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
auto source = op.getSource();
// Check all failure conditions before generating any IR, so nothing has to
// be rolled back.
int64_t rank = op.getType().getRank();
int64_t sourceRank;
auto memrefTy = dyn_cast<MemRefType>(source.getType());
if (memrefTy) {
if (!memrefTy.isStrided())
return rewriter.notifyMatchFailure(op, "Expected strided Memref.");
sourceRank = memrefTy.getRank();
} else if (isa<IntegerType>(source.getType())) {
sourceRank = op.getMixedSizes().size();
} else {
return rewriter.notifyMatchFailure(op,
"Expected ranked Memref or integer.");
}
if (sourceRank != rank)
return rewriter.notifyMatchFailure(
op, "Expected descriptor rank to match source rank; subview the "
"source down to the descriptor rank.");
if (rank > maxNdTdescRank)
return rewriter.notifyMatchFailure(
op, "Batched nd descriptor supports at most " +
std::to_string(maxNdTdescLeadingDims) +
" leading dims (rank <= " + std::to_string(maxNdTdescRank) +
").");
// Limitation 1 above; dynamic strides are assumed to divide evenly.
if (rank > 2) {
SmallVector<std::optional<int64_t>> constStrides(rank, std::nullopt);
if (memrefTy) {
SmallVector<int64_t> staticStrides;
int64_t staticOffset;
if (succeeded(
memrefTy.getStridesAndOffset(staticStrides, staticOffset)))
for (int64_t d = 0; d < rank; ++d)
if (!ShapedType::isDynamic(staticStrides[d]))
constStrides[d] = staticStrides[d];
} else {
SmallVector<OpFoldResult> mixed = op.getMixedStrides();
for (int64_t d = 0; d < rank; ++d)
constStrides[d] = getConstantIntValue(mixed[d]);
}
if (std::optional<int64_t> pitch = constStrides[rank - 2]) {
for (int64_t d = 0; d < rank - 2; ++d) {
std::optional<int64_t> leading = constStrides[d];
if (leading && (*pitch == 0 || *leading % *pitch != 0))
return rewriter.notifyMatchFailure(
op, "Expected each leading (batch) stride to be a multiple of "
"the row stride; the source has gaps between planes.");
}
}
}
// Enforce the target's 2D block memory restrictions. A rank-1 descriptor is
// just a base address and is not consumed by a 2D block op, so it is
// exempt.
if (rank >= 2 && failed(checkMemoryRestrictions(op, rank)))
return failure();
Type payloadElemTy = rewriter.getI32Type();
Type i64Ty = rewriter.getI64Type();
// Access the adaptor only after the failure checks, so a bail-out leaves no
// materialization cast behind.
Value baseAddr = adaptor.getSource();
if (isa<IntegerType>(source.getType()) && baseAddr.getType() != i64Ty) {
// Pointer type may be i32. Cast to i64 if needed.
baseAddr = arith::ExtUIOp::create(rewriter, loc, i64Ty, baseAddr);
}
// 1D tensor descriptor is just the base address.
if (rank == 1) {
rewriter.replaceOp(op, baseAddr);
return success();
}
SmallVector<OpFoldResult> mixedSizes;
SmallVector<OpFoldResult> mixedStrides;
if (memrefTy && !xegpu::hasStaticShapeAndStrides(memrefTy)) {
auto meta =
memref::ExtractStridedMetadataOp::create(rewriter, loc, source);
mixedSizes = meta.getConstifiedMixedSizes();
mixedStrides = meta.getConstifiedMixedStrides();
} else {
mixedSizes = op.getMixedSizes();
mixedStrides = op.getMixedStrides();
}
// Op is lowered to a code sequence that populates payload.
// Payload is a 8xi32 vector. Offset to individual fields are defined in
// NdTdescOffset enum.
VectorType payloadTy = VectorType::get(8, payloadElemTy);
// 4xi64 view is used for inserting the base pointer.
VectorType payloadI64Ty = VectorType::get(4, i64Ty);
// Initialize payload to zero.
Value payload = arith::ConstantOp::create(
rewriter, loc,
DenseElementsAttr::get(payloadTy, IntegerAttr::get(payloadElemTy, 0)));
// Utility for creating offset values from op fold result.
auto createOffset = [&](SmallVector<OpFoldResult> &ofrVec,
unsigned idx) -> Value {
Value val = getValueOrCreateConstantIntOp(rewriter, loc, ofrVec[idx]);
val = getValueOrCreateCastToIndexLike(rewriter, loc, payloadElemTy, val);
return val;
};
// The descriptor's innermost 2 dims are the 2D tile (H, W).
Value baseShapeW = createOffset(mixedSizes, rank - 1);
// Pitch is the stride of dim rank-2 (the row stride of the 2D tile).
Value basePitch = createOffset(mixedStrides, rank - 2);
// Leading (batch) strides as a number of rows (`stride[d] / pitch`).
SmallVector<Value> leadingRowStrides;
for (int64_t d = 0; d < rank - 2; ++d) {
std::optional<int64_t> leading = getConstantIntValue(mixedStrides[d]);
std::optional<int64_t> pitch =
getConstantIntValue(mixedStrides[rank - 2]);
if (leading && pitch && *pitch != 0)
leadingRowStrides.push_back(arith::ConstantIntOp::create(
rewriter, loc, payloadElemTy, *leading / *pitch));
else
leadingRowStrides.push_back(arith::DivUIOp::create(
rewriter, loc, createOffset(mixedStrides, d), basePitch));
}
// Height of the flattened plane: every leading (batch) plane is stacked
// into the surface, so the boundary check covers an out-of-range batch.
// base_height = size[R-2] + sum_d (size[d] - 1) * leadingRowStride[d]
// For rank 2 this is just size[0].
Value baseShapeH = createOffset(mixedSizes, rank - 2);
if (rank > 2) {
Value one = arith::ConstantIntOp::create(rewriter, loc, payloadElemTy, 1);
for (int64_t d = 0; d < rank - 2; ++d) {
Value planesBelow = rewriter.createOrFold<arith::SubIOp>(
loc, createOffset(mixedSizes, d), one);
Value rows = rewriter.createOrFold<arith::MulIOp>(loc, planesBelow,
leadingRowStrides[d]);
baseShapeH =
rewriter.createOrFold<arith::AddIOp>(loc, baseShapeH, rows);
}
}
// Populate payload.
Value payLoadAsI64 =
vector::BitCastOp::create(rewriter, loc, payloadI64Ty, payload);
payLoadAsI64 =
vector::InsertOp::create(rewriter, loc, baseAddr, payLoadAsI64,
static_cast<int>(NdTdescOffset::BasePtr));
payload = vector::BitCastOp::create(rewriter, loc, payloadTy, payLoadAsI64);
payload =
vector::InsertOp::create(rewriter, loc, baseShapeW, payload,
static_cast<int>(NdTdescOffset::BaseShapeW));
payload =
vector::InsertOp::create(rewriter, loc, baseShapeH, payload,
static_cast<int>(NdTdescOffset::BaseShapeH));
payload =
vector::InsertOp::create(rewriter, loc, basePitch, payload,
static_cast<int>(NdTdescOffset::BasePitch));
// The leading (batch) row strides go into the spare payload slots; the
// load/store/prefetch lowering turns the batch offsets into a row offset
// with them.
for (int64_t d = 0; d < rank - 2; ++d)
payload = vector::InsertOp::create(
rewriter, loc, leadingRowStrides[d], payload,
static_cast<int>(NdTdescOffset::LeadingStride0) + d);
rewriter.replaceOp(op, payload);
return success();
}
};
template <
typename OpType,
typename = std::enable_if_t<llvm::is_one_of<
OpType, xegpu::LoadNdOp, xegpu::StoreNdOp, xegpu::PrefetchNdOp>::value>>
class LoadStorePrefetchNdToXeVMPattern : public OpConversionPattern<OpType> {
using OpConversionPattern<OpType>::OpConversionPattern;
LogicalResult
matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto mixedOffsets = op.getMixedOffsets();
int64_t opOffsetsSize = mixedOffsets.size();
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
auto tdesc = adaptor.getTensorDesc();
auto tdescTy = op.getTensorDescType();
auto tileRank = tdescTy.getRank();
if (opOffsetsSize != tileRank)
return rewriter.notifyMatchFailure(
op, "Expected offset rank to match descriptor rank.");
if (tileRank > 2 && llvm::any_of(tdescTy.getShape().drop_back(2),
[](int64_t d) { return d != 1; }))
return rewriter.notifyMatchFailure(
op, "Expected leading (batch) descriptor dims to be unit.");
if (tileRank > maxNdTdescRank)
return rewriter.notifyMatchFailure(
op, "Expected descriptor rank <= " + std::to_string(maxNdTdescRank) +
".");
auto elemType = tdescTy.getElementType();
auto elemBitSize = elemType.getIntOrFloatBitWidth();
bool isSubByte = elemBitSize < 8;
uint64_t wScaleFactor = 1;
if (!isSubByte && (elemBitSize % 8 != 0))
return rewriter.notifyMatchFailure(
op, "Expected element type bit width to be multiple of 8.");
auto tileW = tdescTy.getDimSize(tileRank - 1);
// For sub byte types, only 4bits are currently supported.
if (isSubByte) {
if (elemBitSize != 4)
return rewriter.notifyMatchFailure(
op, "Only sub byte types of 4bits are supported.");
if (tileRank != 2)
return rewriter.notifyMatchFailure(
op, "Sub byte types are only supported for 2D tensor descriptors.");
auto subByteFactor = 8 / elemBitSize;
auto tileH = tdescTy.getDimSize(0);
// Handle special case for packed load.
if constexpr (std::is_same_v<OpType, xegpu::LoadNdOp>) {
if (op.getPacked().value_or(false)) {
// packed load is implemented as packed loads of 8bit elements.
if (tileH == systolicDepth * 4 &&
tileW == executionSize * subByteFactor) {
// Usage case for loading as Matrix B with pack request.
// source is assumed to pre-packed into 8bit elements
// Emulate with 8bit loads with pack request.
// scaled_tileW = executionSize
elemType = rewriter.getIntegerType(8);
tileW = executionSize;
wScaleFactor = subByteFactor;
}
}
}
// If not handled by packed load case above, handle other cases.
if (wScaleFactor == 1) {
auto sub16BitFactor = subByteFactor * 2;
if (tileW == executionSize * sub16BitFactor) {
// Usage case for loading as Matrix A operand
// Emulate with 16bit loads/stores.
// scaled_tileW = executionSize
elemType = rewriter.getIntegerType(16);
tileW = executionSize;
wScaleFactor = sub16BitFactor;
} else {
return rewriter.notifyMatchFailure(
op, "Unsupported tile shape for sub byte types.");
}
}
// recompute element bit size for emulation.
elemBitSize = elemType.getIntOrFloatBitWidth();
}
// Get address space from tensor descriptor memory space.
auto ptrTypeLLVM = LLVM::LLVMPointerType::get(
ctxt, getNumericXeVMAddrSpace(tdescTy.getMemorySpace()));
if (tileRank >= 2) {
// Compute element byte size.
Value elemByteSize = arith::ConstantIntOp::create(
rewriter, loc, rewriter.getI32Type(), elemBitSize / 8);
VectorType payloadI64Ty = VectorType::get(4, rewriter.getI64Type());
Value payLoadAsI64 =
vector::BitCastOp::create(rewriter, loc, payloadI64Ty, tdesc);
Value basePtr =
vector::ExtractOp::create(rewriter, loc, payLoadAsI64,
static_cast<int>(NdTdescOffset::BasePtr));
Value baseShapeW = vector::ExtractOp::create(
rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BaseShapeW));
Value baseShapeH = vector::ExtractOp::create(
rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BaseShapeH));
Value basePitch = vector::ExtractOp::create(
rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BasePitch));
Value offsetW = getValueOrCreateConstantIntOp(rewriter, loc,
mixedOffsets[tileRank - 1]);
offsetW = getValueOrCreateCastToIndexLike(rewriter, loc,
rewriter.getI32Type(), offsetW);
Value offsetH = getValueOrCreateConstantIntOp(rewriter, loc,
mixedOffsets[tileRank - 2]);
offsetH = getValueOrCreateCastToIndexLike(rewriter, loc,
rewriter.getI32Type(), offsetH);
// Turn the leading (batch) offsets into a row offset into the flattened
// plane, using the row-unit batch strides encoded at create time:
// offsetH += sum_d offset[d] * leadingRowStride[d]
// The base pointer stays at the true base, so an out-of-range batch index
// is caught by the HW boundary check instead of moving the surface to
// unmapped memory.
for (int64_t d = 0; d < tileRank - 2; ++d) {
Value off =
getValueOrCreateConstantIntOp(rewriter, loc, mixedOffsets[d]);
off = getValueOrCreateCastToIndexLike(rewriter, loc,
rewriter.getI32Type(), off);
Value rowStride = vector::ExtractOp::create(
rewriter, loc, tdesc,
static_cast<int>(NdTdescOffset::LeadingStride0) + d);
Value term = arith::MulIOp::create(rewriter, loc, off, rowStride);
offsetH = arith::AddIOp::create(rewriter, loc, offsetH, term);
}
// Convert base pointer (i64) to LLVM pointer type.
Value basePtrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtr);
// FIXME: width or pitch is not the same as baseShapeW it should be the
// stride of the second to last dimension in row major layout.
// Compute width in bytes.
Value baseShapeWInBytes =
arith::MulIOp::create(rewriter, loc, baseShapeW, elemByteSize);
// Compute pitch in bytes.
Value basePitchBytes =
arith::MulIOp::create(rewriter, loc, basePitch, elemByteSize);
if (wScaleFactor > 1) {
// Scale offsetW, baseShapeWInBytes for sub byte emulation.
// Note: tileW is already scaled above.
Value wScaleFactorValLog2 = arith::ConstantIntOp::create(
rewriter, loc, rewriter.getI32Type(), llvm::Log2_64(wScaleFactor));
baseShapeWInBytes = arith::ShRSIOp::create(
rewriter, loc, baseShapeWInBytes, wScaleFactorValLog2);
basePitchBytes = arith::ShRSIOp::create(rewriter, loc, basePitchBytes,
wScaleFactorValLog2);
offsetW =
arith::ShRSIOp::create(rewriter, loc, offsetW, wScaleFactorValLog2);
}
// Get tile height from the tensor descriptor type (second-to-last dim).
auto tileH = tdescTy.getDimSize(tileRank - 2);
// Get vblocks from the tensor descriptor type.
int32_t vblocks = tdescTy.getArrayLength();
if constexpr (std::is_same_v<OpType, xegpu::StoreNdOp>) {
Value src = adaptor.getValue();
// If store value is a scalar, get value from op instead of adaptor.
// Adaptor might have optimized away single element vector
if (src.getType().isIntOrFloat()) {
src = op.getValue();
}
VectorType srcVecTy = dyn_cast<VectorType>(src.getType());
if (!srcVecTy)
return rewriter.notifyMatchFailure(
op, "Expected store value to be a vector type.");
// Get flat vector type of integer type with matching element bit size.
VectorType newSrcVecTy =
encodeVectorTypeTo(srcVecTy, rewriter.getIntegerType(elemBitSize));
if (srcVecTy != newSrcVecTy)
src = vector::BitCastOp::create(rewriter, loc, newSrcVecTy, src);
auto storeCacheControl =
translateStoreXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
xevm::BlockStore2dOp::create(
rewriter, loc, basePtrLLVM, baseShapeWInBytes, baseShapeH,
basePitchBytes, offsetW, offsetH, elemBitSize, tileW, tileH, src,
xevm::StoreCacheControlAttr::get(ctxt, storeCacheControl));
rewriter.eraseOp(op);
} else {
auto loadCacheControl =
translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
if constexpr (std::is_same_v<OpType, xegpu::PrefetchNdOp>) {
xevm::BlockPrefetch2dOp::create(
rewriter, loc, basePtrLLVM, baseShapeWInBytes, baseShapeH,
basePitchBytes, offsetW, offsetH, elemBitSize, tileW, tileH,
vblocks, xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
rewriter.eraseOp(op);
} else {
VectorType dstVecTy = cast<VectorType>(op.getValue().getType());
bool vnni = op.getPacked().value_or(false);
auto transposeValue = op.getTranspose();
bool transpose =
transposeValue.has_value() && transposeValue.value()[0] == 1;
// Handle special case of 32x16 and 8bit element load
// with no vnni, no transpose, no vblocks.
// For this special case, vnni and non vnni yields the same output
// and only the vnni variant is supported by HW.
// Check and set vnni of the special case.
if (elemBitSize == 8 && tileW == 16 && tileH == 32 && !vnni &&
!transpose) {
vnni = true;
}
// Handle tranpose request on small element size
// Transpose needs to be requested on 32bit element type.
// offsetW and tileW needs to be adjusted to account for element type
// change.
if (transpose && elemBitSize < 32) {
int32_t scale = 32 / elemBitSize;
Value scaleLog2 = arith::ConstantIntOp::create(
rewriter, loc, rewriter.getI32Type(), llvm::Log2_64(scale));
offsetW = arith::ShRSIOp::create(rewriter, loc, offsetW, scaleLog2);
tileW = tileW * elemBitSize / 32;
elemBitSize = 32;
}
VectorType loadedTy = encodeVectorTypeTo(
dstVecTy, vnni ? rewriter.getI32Type()
: rewriter.getIntegerType(elemBitSize));
Value resultFlatVec = xevm::BlockLoad2dOp::create(
rewriter, loc, loadedTy, basePtrLLVM, baseShapeWInBytes,
baseShapeH, basePitchBytes, offsetW, offsetH, elemBitSize, tileW,
tileH, vblocks, transpose, vnni,
xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
resultFlatVec = vector::BitCastOp::create(
rewriter, loc,
encodeVectorTypeTo(loadedTy, dstVecTy.getElementType()),
resultFlatVec);
rewriter.replaceOp(op, resultFlatVec);
}
}
} else {
// 1D tensor descriptor.
// `tdesc` represents base address as i64
// Offset in number of elements, need to multiply by element byte size.
// Compute byte offset.
// byteOffset = offset * elementByteSize
Value offset =
getValueOrCreateConstantIntOp(rewriter, loc, mixedOffsets[0]);
offset = getValueOrCreateCastToIndexLike(rewriter, loc,
rewriter.getI64Type(), offset);
// Compute element byte size.
Value elemByteSize = arith::ConstantIntOp::create(
rewriter, loc, rewriter.getI64Type(), elemBitSize / 8);
Value byteOffset =
rewriter.createOrFold<arith::MulIOp>(loc, offset, elemByteSize);
// Final address = basePtr + byteOffset
Value finalAddrI64 = rewriter.createOrFold<arith::AddIOp>(
loc, tdesc,
getValueOrCreateCastToIndexLike(rewriter, loc, rewriter.getI64Type(),
byteOffset));
// Convert base pointer (i64) to LLVM pointer type.
Value finalPtrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, finalAddrI64);
if constexpr (std::is_same_v<OpType, xegpu::StoreNdOp>) {
Value src = adaptor.getValue();
// If store value is a scalar, get value from op instead of adaptor.
// Adaptor might have optimized away single element vector
if (src.getType().isIntOrFloat()) {
src = op.getValue();
}
VectorType srcVecTy = dyn_cast<VectorType>(src.getType());
if (!srcVecTy)
return rewriter.notifyMatchFailure(
op, "Expected store value to be a vector type.");
// Get flat vector type of integer type with matching element bit size.
VectorType newSrcVecTy =
encodeVectorTypeTo(srcVecTy, rewriter.getIntegerType(elemBitSize));
if (srcVecTy != newSrcVecTy)
src = vector::BitCastOp::create(rewriter, loc, newSrcVecTy, src);
auto storeCacheControl =
translateStoreXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
rewriter.replaceOpWithNewOp<xevm::BlockStoreOp>(
op, finalPtrLLVM, src,
xevm::StoreCacheControlAttr::get(ctxt, storeCacheControl));
} else if constexpr (std::is_same_v<OpType, xegpu::LoadNdOp>) {
auto loadCacheControl =
translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
VectorType resTy = cast<VectorType>(op.getValue().getType());
VectorType loadedTy =
encodeVectorTypeTo(resTy, rewriter.getIntegerType(elemBitSize));
Value load = xevm::BlockLoadOp::create(
rewriter, loc, loadedTy, finalPtrLLVM,
xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
if (loadedTy != resTy)
load = vector::BitCastOp::create(rewriter, loc, resTy, load);
rewriter.replaceOp(op, load);
} else {
return rewriter.notifyMatchFailure(
op, "Unsupported operation: xegpu.prefetch_nd with tensor "
"descriptor rank == 1");
}
}
return success();
}
};
// Add a builder that creates
// offset * elemByteSize + baseAddr
static Value addOffsetToBaseAddr(ConversionPatternRewriter &rewriter,
Location loc, Value baseAddr, Value offset,
int64_t elemByteSize) {
Value byteSize = arith::ConstantIntOp::create(
rewriter, loc, baseAddr.getType(), elemByteSize);
Value byteOffset = arith::MulIOp::create(rewriter, loc, offset, byteSize);
Value newAddr = arith::AddIOp::create(rewriter, loc, baseAddr, byteOffset);
return newAddr;
}
// Returns true when every element of `mask` carries the same bit, so gating a
// whole contiguous block on element 0 is equivalent. Splat constants, an
// all-ones or all-zeros `vector.constant_mask`, broadcasts of a scalar, and a
// `vector.from_elements` of one repeated value qualify.
//
// TODO: this recognizes a fixed list of producers. A general uniformity query
// on the vector dialect would cover more forms and would not need updating
// every time a new producer shows up here.
static bool isUniformMask(Value mask) {
if (!isa<VectorType>(mask.getType()))
return true;
DenseElementsAttr splat;
if (matchPattern(mask, m_Constant(&splat)) && splat.isSplat())
return true;
if (auto constantMask = mask.getDefiningOp<vector::ConstantMaskOp>()) {
// All-ones and all-zeros are uniform. A zero dim size is only legal when
// every dim is zero, so those are the only two uniform cases.
return constantMask.isAllOnesMask() ||
llvm::all_of(constantMask.getMaskDimSizes(),
[](int64_t size) { return size == 0; });
}
if (auto broadcast = mask.getDefiningOp<vector::BroadcastOp>())
return !isa<VectorType>(broadcast.getSource().getType());
if (auto fromElements = mask.getDefiningOp<vector::FromElementsOp>())
return llvm::all_equal(fromElements.getElements());
// Flattening a rank > 1 operand to rank 1 inserts a shape cast. It keeps the
// same elements, so it keeps uniformity.
if (auto shapeCast = mask.getDefiningOp<vector::ShapeCastOp>())
return isUniformMask(shapeCast.getSource());
return false;
}
template <typename OpType,
typename = std::enable_if_t<llvm::is_one_of<
OpType, xegpu::LoadGatherOp, xegpu::StoreScatterOp>::value>>
class LoadStoreToXeVMPattern : public OpConversionPattern<OpType> {
using OpConversionPattern<OpType>::OpConversionPattern;
LogicalResult
matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
Value offset = adaptor.getOffsets();
if (!offset)
return rewriter.notifyMatchFailure(op, "Expected offset to be provided.");
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
Value basePtrI64;
// Load result or Store valye Type can be vector or scalar.
Type valOrResTy;
if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>)
valOrResTy =
this->getTypeConverter()->convertType(op.getResult().getType());
else
valOrResTy = adaptor.getValue().getType();
VectorType valOrResVecTy = dyn_cast<VectorType>(valOrResTy);
bool hasScalarVal = !valOrResVecTy;
int64_t elemBitWidth =
hasScalarVal ? valOrResTy.getIntOrFloatBitWidth()
: valOrResVecTy.getElementType().getIntOrFloatBitWidth();
// Element type must be multiple of 8 bits.
if (elemBitWidth % 8 != 0)
return rewriter.notifyMatchFailure(
op, "Expected element type bit width to be multiple of 8.");
int64_t elemByteSize = elemBitWidth / 8;
// Default memory space is global.
LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::Global));
// Base pointer can come from source (load) or dest (store).
// If they are memrefs, we use their memory space.
if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>) {
basePtrI64 = adaptor.getSource();
if (auto memRefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
FailureOr<unsigned> addrSpace =
getNumericMemorySpace(memRefTy.getMemorySpace());
if (failed(addrSpace))
return rewriter.notifyMatchFailure(
op, "Unsupported memref memory space attribute.");
if (*addrSpace != 0)
ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
}
} else {
basePtrI64 = adaptor.getDest();
if (auto memRefTy = dyn_cast<MemRefType>(op.getDest().getType())) {
FailureOr<unsigned> addrSpace =
getNumericMemorySpace(memRefTy.getMemorySpace());
if (failed(addrSpace))
return rewriter.notifyMatchFailure(
op, "Unsupported memref memory space attribute.");
if (*addrSpace != 0)
ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
}
}
// Base pointer is passed as i32 or i64 by adaptor, cast to i64 if needed.
if (basePtrI64.getType() != rewriter.getI64Type()) {
basePtrI64 = arith::ExtUIOp::create(rewriter, loc, rewriter.getI64Type(),
basePtrI64);
}
Value mask = adaptor.getMask();
// Coalesce a lane's multi-element access into one block access.
//
// Distribution gives every element its own offset and mask bit, so a lane
// that takes D neighbouring elements arrives with `vector<D>` offsets and
// mask. One block access needs one base offset and one mask bit, so take
// both from element 0. A non-uniform mask cannot be reduced to one bit, so
// it stays a vector and fails to match below.
auto origOffsetsTy = dyn_cast<VectorType>(op.getOffsets().getType());
if (isa<VectorType>(offset.getType()) && origOffsetsTy && valOrResVecTy &&
origOffsetsTy.getNumElements() == valOrResVecTy.getNumElements() &&
isUniformMask(op.getMask())) {
offset = vector::ExtractOp::create(rewriter, loc, offset,
ArrayRef<int64_t>{0});
if (isa<VectorType>(mask.getType()))
mask = vector::ExtractOp::create(rewriter, loc, mask,
ArrayRef<int64_t>{0});
}
if (dyn_cast<VectorType>(offset.getType())) {
// Offset needs be scalar. Single element vector is converted to scalar
// by type converter.
return rewriter.notifyMatchFailure(op, "Expected offset to be a scalar.");
} else {
// If offset is provided, we add them to the base pointer.
// Offset is in number of elements, we need to multiply by
// element byte size.
basePtrI64 =
addOffsetToBaseAddr(rewriter, loc, basePtrI64, offset, elemByteSize);
}
// Convert base pointer (i64) to LLVM pointer type.
Value basePtrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
Value maskForLane;
VectorType maskVecTy = dyn_cast<VectorType>(mask.getType());
if (maskVecTy) {
// Mask needs be scalar. Single element vector is converted to scalar by
// type converter.
return rewriter.notifyMatchFailure(op, "Expected mask to be a scalar.");
} else
maskForLane = mask;
if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>) {
scf::IfOp ifOp = scf::IfOp::create(rewriter, loc, {valOrResTy},
maskForLane, true, true);
// If mask is true,- then clause - load from memory and yield.
rewriter.setInsertionPointToStart(&ifOp.getThenRegion().front());
if (!hasScalarVal)
valOrResTy = VectorType::get({valOrResVecTy.getNumElements()},
valOrResVecTy.getElementType());
Value loaded =
LLVM::LoadOp::create(rewriter, loc, valOrResTy, basePtrLLVM);
// Set cache control attribute on the load operation.
loaded.getDefiningOp()->setDiscardableAttr(
"cache_control", xevm::LoadCacheControlAttr::get(
ctxt, translateLoadXeGPUCacheHint(
op.getL1Hint(), op.getL3Hint())));
scf::YieldOp::create(rewriter, loc, ValueRange{loaded});
rewriter.setInsertionPointToStart(&ifOp.getElseRegion().front());
// If mask is false - else clause -yield a vector of zeros.
auto eTy = hasScalarVal ? valOrResTy : valOrResVecTy.getElementType();
TypedAttr eVal;
if (eTy.isFloat())
eVal = FloatAttr::get(eTy, 0.0);
else
eVal = IntegerAttr::get(eTy, 0);
if (hasScalarVal)
loaded = arith::ConstantOp::create(rewriter, loc, eVal);
else
loaded = arith::ConstantOp::create(
rewriter, loc, DenseElementsAttr::get(valOrResVecTy, eVal));
scf::YieldOp::create(rewriter, loc, ValueRange{loaded});
rewriter.replaceOp(op, ifOp.getResult(0));
} else {
// If mask is true, perform the store.
scf::IfOp ifOp = scf::IfOp::create(rewriter, loc, maskForLane, false);
auto body = ifOp.getBody();
rewriter.setInsertionPointToStart(body);
auto storeOp =
LLVM::StoreOp::create(rewriter, loc, adaptor.getValue(), basePtrLLVM);
// Set cache control attribute on the store operation.
storeOp.getOperation()->setDiscardableAttr(
"cache_control", xevm::StoreCacheControlAttr::get(
ctxt, translateStoreXeGPUCacheHint(
op.getL1Hint(), op.getL3Hint())));
rewriter.eraseOp(op);
}
return success();
}
};
class CreateMemDescOpPattern final
: public OpConversionPattern<xegpu::CreateMemDescOp> {
public:
using OpConversionPattern<xegpu::CreateMemDescOp>::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::CreateMemDescOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
rewriter.replaceOp(op, adaptor.getSource());
return success();
}
};
template <typename OpType,
typename = std::enable_if_t<llvm::is_one_of<
OpType, xegpu::LoadMatrixOp, xegpu::StoreMatrixOp>::value>>
class LoadStoreMatrixToXeVMPattern : public OpConversionPattern<OpType> {
using OpConversionPattern<OpType>::OpConversionPattern;
LogicalResult
matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
SmallVector<OpFoldResult> offsets = op.getMixedOffsets();
if (offsets.empty())
return rewriter.notifyMatchFailure(op, "Expected offset to be provided.");
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
Value baseAddr32 = adaptor.getMemDesc();
Value mdescVal = op.getMemDesc();
// Load result or Store value Type can be vector or scalar.
Type dataTy;
if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
Type resType = op.getResult().getType();
// Some transforms may leave unit dimension in the 2D vector, adaptors do
// not catch it for results.
if (auto vecType = dyn_cast<VectorType>(resType)) {
assert(llvm::count_if(vecType.getShape(),
[](int64_t d) { return d != 1; }) <= 1 &&
"Expected either 1D vector or nD with unit dimensions");
resType = VectorType::get({vecType.getNumElements()},
vecType.getElementType());
}
dataTy = resType;
} else
dataTy = adaptor.getData().getType();
VectorType valOrResVecTy = dyn_cast<VectorType>(dataTy);
if (!valOrResVecTy)
valOrResVecTy = VectorType::get(1, dataTy);
int64_t elemBitWidth =
valOrResVecTy.getElementType().getIntOrFloatBitWidth();
// Element type must be multiple of 8 bits.
if (elemBitWidth % 8 != 0)
return rewriter.notifyMatchFailure(
op, "Expected element type bit width to be multiple of 8.");
int64_t elemByteSize = elemBitWidth / 8;
// Default memory space is SLM.
LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::SLM));
auto mdescTy = cast<xegpu::MemDescType>(mdescVal.getType());
Value linearOffset = mdescTy.getLinearOffsets(rewriter, loc, offsets);
linearOffset = arith::IndexCastUIOp::create(
rewriter, loc, rewriter.getI32Type(), linearOffset);
Value basePtrI32 = addOffsetToBaseAddr(rewriter, loc, baseAddr32,
linearOffset, elemByteSize);
// convert base pointer (i32) to LLVM pointer type
Value basePtrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI32);
if (op.getSubgroupBlockIoAttr()) {
// if the attribute 'subgroup_block_io' is set to true, it lowers to
// xevm.blockload
Type intElemTy = rewriter.getIntegerType(elemBitWidth);
VectorType intVecTy =
VectorType::get(valOrResVecTy.getShape(), intElemTy);
if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
Value loadOp = xevm::BlockLoadOp::create(
rewriter, loc, intVecTy, basePtrLLVM, /*cache_control=*/nullptr);
if (intVecTy != valOrResVecTy) {
loadOp =
vector::BitCastOp::create(rewriter, loc, valOrResVecTy, loadOp);
}
rewriter.replaceOp(op, loadOp);
} else {
Value dataToStore = adaptor.getData();
if (valOrResVecTy != intVecTy) {
dataToStore =
vector::BitCastOp::create(rewriter, loc, intVecTy, dataToStore);
}
xevm::BlockStoreOp::create(rewriter, loc, basePtrLLVM, dataToStore,
nullptr);
rewriter.eraseOp(op);
}
return success();
}
if (valOrResVecTy.getNumElements() >= 1) {
auto chipOpt = xegpu::getChipStr(op);
if (!chipOpt ||
(*chipOpt != "pvc" && *chipOpt != "bmg" && *chipOpt != "cri")) {
// the lowering for chunk load only works for pvc, bmg or cri
return rewriter.notifyMatchFailure(
op, "The lowering is specific to pvc, bmg or cri.");
}
}
if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
// The load result type is taken from the type converter. This maps
// element types that are not directly representable in LLVM (e.g.
// f8E8M0FNU) to an integer storage type of the same bit width, and
// collapses single-element vectors to a scalar, since LLVM load/store
// does not support vectors of size 1.
Type loadTy =
this->getTypeConverter()->convertType(op.getResult().getType());
auto loadOp = LLVM::LoadOp::create(rewriter, loc, loadTy, basePtrLLVM);
rewriter.replaceOp(op, loadOp);
} else {
LLVM::StoreOp::create(rewriter, loc, adaptor.getData(), basePtrLLVM);
rewriter.eraseOp(op);
}
return success();
}
};
class PrefetchToXeVMPattern : public OpConversionPattern<xegpu::PrefetchOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::PrefetchOp op, xegpu::PrefetchOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
Value basePtrI64 = adaptor.getSource();
// Base pointer is passed as i32 or i64 by adaptor, cast to i64 if needed.
if (basePtrI64.getType() != rewriter.getI64Type())
basePtrI64 = arith::ExtUIOp::create(rewriter, loc, rewriter.getI64Type(),
basePtrI64);
Value offsets = adaptor.getOffsets();
if (offsets) {
VectorType offsetsVecTy = dyn_cast<VectorType>(offsets.getType());
if (offsetsVecTy) {
// Offset needs be scalar.
return rewriter.notifyMatchFailure(op,
"Expected offsets to be a scalar.");
} else {
int64_t elemBitWidth{0};
int64_t elemByteSize;
// Element byte size can come from two sources:
if (auto memRefTy = dyn_cast<MemRefType>(op.getSourceType())) {
// If memref is available, we use its element type to
// determine element byte size.
elemBitWidth = memRefTy.getElementType().getIntOrFloatBitWidth();
} else {
// Otherwise, we use the provided offset byte alignment.
elemByteSize = *op.getOffsetAlignByte();
}
if (elemBitWidth != 0) {
if (elemBitWidth % 8 != 0)
return rewriter.notifyMatchFailure(
op, "Expected element type bit width to be multiple of 8.");
elemByteSize = elemBitWidth / 8;
}
basePtrI64 = addOffsetToBaseAddr(rewriter, loc, basePtrI64, offsets,
elemByteSize);
}
}
// Default memory space is global.
LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::Global));
// If source is a memref, we use its memory space.
if (auto memRefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
FailureOr<unsigned> addrSpace =
getNumericMemorySpace(memRefTy.getMemorySpace());
if (failed(addrSpace))
return rewriter.notifyMatchFailure(
op, "Unsupported memref memory space attribute.");
if (*addrSpace != 0)
ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
}
// Convert base pointer (i64) to LLVM pointer type.
Value ptrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
// Create the prefetch op with cache control attribute.
xevm::PrefetchOp::create(
rewriter, loc, ptrLLVM,
xevm::LoadCacheControlAttr::get(
ctxt, translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint())));
rewriter.eraseOp(op);
return success();
}
};
class FenceToXeVMPattern : public OpConversionPattern<xegpu::FenceOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::FenceOp op, xegpu::FenceOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
xevm::MemScope memScope{xevm::MemScope::WORKGROUP};
switch (op.getFenceScope()) {
case xegpu::FenceScope::Workgroup:
memScope = xevm::MemScope::WORKGROUP;
break;
case xegpu::FenceScope::GPU:
memScope = xevm::MemScope::DEVICE;
break;
}
xevm::AddrSpace addrSpace{xevm::AddrSpace::GLOBAL};
switch (op.getMemoryKind()) {
case xegpu::MemorySpace::Global:
addrSpace = xevm::AddrSpace::GLOBAL;
break;
case xegpu::MemorySpace::SLM:
addrSpace = xevm::AddrSpace::SHARED;
break;
}
xevm::MemfenceOp::create(rewriter, loc, memScope, addrSpace);
rewriter.eraseOp(op);
return success();
}
};
static auto encodePrecision = [](Type type) -> xevm::ElemType {
if (type.isBF16())
return xevm::ElemType::BF16;
else if (type.isF16())
return xevm::ElemType::F16;
else if (type.isTF32())
return xevm::ElemType::TF32;
else if (type.isInteger(8)) {
if (type.isUnsignedInteger())
return xevm::ElemType::U8;
return xevm::ElemType::S8;
} else if (type.isF32())
return xevm::ElemType::F32;
else if (type.isInteger(32))
return xevm::ElemType::S32;
else if (type.isF8E5M2())
return xevm::ElemType::BF8;
else if (type.isF8E4M3FN())
return xevm::ElemType::F8;
else if (mlir::isa<Float4E2M1FNType>(type))
return xevm::ElemType::E2M1;
llvm_unreachable("add more support for ElemType");
};
static unsigned getNumOperandsPerDword(xevm::ElemType pTy) {
switch (pTy) {
case xevm::ElemType::TF32:
return 1;
case xevm::ElemType::BF16:
case xevm::ElemType::F16:
return 2;
case xevm::ElemType::U8:
case xevm::ElemType::S8:
case xevm::ElemType::F8:
case xevm::ElemType::BF8:
return 4;
case xevm::ElemType::E2M1:
return 8;
default:
llvm_unreachable("unsupported xevm::ElemType");
}
}
class DpasToXeVMPattern : public OpConversionPattern<xegpu::DpasOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::DpasOp op, xegpu::DpasOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
auto aTy = cast<VectorType>(op.getLhs().getType());
auto bTy = cast<VectorType>(op.getRhs().getType());
auto resultType = cast<VectorType>(op.getResultType());
// get the correct dpasInst by getting info from chip
auto chipStr = xegpu::getChipStr(op);
if (!chipStr)
return rewriter.notifyMatchFailure(op, "cannot determine target chip");
const auto *uArch = mlir::xegpu::uArch::getUArch(*chipStr);
if (!uArch)
return rewriter.notifyMatchFailure(op, "unsupported target uArch");
auto *dpasInst = const_cast<xegpu::uArch::SubgroupMatrixMultiplyAcc *>(
llvm::dyn_cast_or_null<xegpu::uArch::SubgroupMatrixMultiplyAcc>(
uArch->getInstruction(
xegpu::uArch::InstructionKind::SubgroupMatrixMultiplyAcc)));
if (!dpasInst)
return rewriter.notifyMatchFailure(op,
"DPAS not supported by target uArch");
auto checkSupportedTypes = [&](VectorType vecTy,
xegpu::uArch::MMAOpndKind kind) -> bool {
auto supported = dpasInst->getSupportedTypes(*ctxt, kind);
return llvm::find(supported, vecTy.getElementType()) != supported.end();
};
if (!checkSupportedTypes(aTy, xegpu::uArch::MMAOpndKind::MatrixA))
return rewriter.notifyMatchFailure(
op, "A-matrix element type not supported by target uArch");
if (!checkSupportedTypes(bTy, xegpu::uArch::MMAOpndKind::MatrixB))
return rewriter.notifyMatchFailure(
op, "B-matrix element type not supported by target uArch");
// NOTE: Supported types for MatrixC and MatrixD are identical
if (!checkSupportedTypes(resultType, xegpu::uArch::MMAOpndKind::MatrixD))
return rewriter.notifyMatchFailure(
op, "result/accumulator element type not supported by target uArch");
xevm::ElemType precATy = encodePrecision(aTy.getElementType());
xevm::ElemType precBTy = encodePrecision(bTy.getElementType());
Value c = op.getAcc();
if (!c) {
auto elementTy = resultType.getElementType();
Attribute initValueAttr;
if (isa<FloatType>(elementTy))
initValueAttr = FloatAttr::get(elementTy, 0.0);
else
initValueAttr = IntegerAttr::get(elementTy, 0);
c = arith::ConstantOp::create(
rewriter, loc, DenseElementsAttr::get(resultType, initValueAttr));
}
Value aVec = op.getLhs();
Value bVec = op.getRhs();
auto cvecty = cast<VectorType>(c.getType());
xevm::ElemType precCTy = encodePrecision(cvecty.getElementType());
xevm::ElemType precDTy = encodePrecision(resultType.getElementType());
VectorType cNty =
VectorType::get(cvecty.getNumElements(), cvecty.getElementType());
if (cvecty != cNty)
c = vector::ShapeCastOp::create(rewriter, loc, cNty, c);
Value dpasRes = xevm::MMAOp::create(
rewriter, loc, cNty, aVec, bVec, c,
xevm::MMAShapeAttr::get(ctxt, cvecty.getNumElements(), executionSize,
systolicDepth *
getNumOperandsPerDword(precATy)),
xevm::MMATypesAttr::get(ctxt, precDTy, precATy, precBTy, precCTy));
if (cvecty != cNty)
dpasRes = vector::ShapeCastOp::create(rewriter, loc, resultType, dpasRes);
rewriter.replaceOp(op, dpasRes);
return success();
}
};
static std::optional<LLVM::AtomicBinOp>
matchSimpleAtomicOp(arith::AtomicRMWKind arithKind) {
switch (arithKind) {
case arith::AtomicRMWKind::addf:
return LLVM::AtomicBinOp::fadd;
case arith::AtomicRMWKind::addi:
return LLVM::AtomicBinOp::add;
case arith::AtomicRMWKind::assign:
return LLVM::AtomicBinOp::xchg;
case arith::AtomicRMWKind::maximumf:
return LLVM::AtomicBinOp::fmax;
case arith::AtomicRMWKind::maxs:
return LLVM::AtomicBinOp::max;
case arith::AtomicRMWKind::maxu:
return LLVM::AtomicBinOp::umax;
case arith::AtomicRMWKind::minimumf:
return LLVM::AtomicBinOp::fmin;
case arith::AtomicRMWKind::mins:
return LLVM::AtomicBinOp::min;
case arith::AtomicRMWKind::minu:
return LLVM::AtomicBinOp::umin;
case arith::AtomicRMWKind::ori:
return LLVM::AtomicBinOp::_or;
case arith::AtomicRMWKind::andi:
return LLVM::AtomicBinOp::_and;
default:
return std::nullopt;
}
}
class AtomicRMWToXeVMPattern : public OpConversionPattern<xegpu::AtomicRMWOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::AtomicRMWOp op, xegpu::AtomicRMWOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
auto tdesc = op.getTensorDesc().getType();
auto ptrTypeLLVM = LLVM::LLVMPointerType::get(
ctxt, getNumericXeVMAddrSpace(tdesc.getMemorySpace()));
Value basePtrI64 = arith::IndexCastOp::create(
rewriter, loc, rewriter.getI64Type(), adaptor.getTensorDesc());
Value basePtrLLVM =
LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
VectorType srcOrDstVecTy = cast<VectorType>(op.getValue().getType());
VectorType srcOrDstFlatVecTy = VectorType::get(
srcOrDstVecTy.getNumElements(), srcOrDstVecTy.getElementType());
Value srcFlatVec = vector::ShapeCastOp::create(
rewriter, loc, srcOrDstFlatVecTy, op.getValue());
auto atomicKind = matchSimpleAtomicOp(op.getKind());
assert(atomicKind.has_value());
Value resVec = srcFlatVec;
for (int i = 0; i < srcOrDstVecTy.getNumElements(); i++) {
auto val = vector::ExtractOp::create(rewriter, loc, resVec, i);
Value idx = LLVM::ConstantOp::create(rewriter, loc, rewriter.getI64Type(),
rewriter.getI64IntegerAttr(i));
Value currPtr =
LLVM::GEPOp::create(rewriter, loc, ptrTypeLLVM,
srcOrDstVecTy.getElementType(), basePtrLLVM, idx);
Value newVal =
LLVM::AtomicRMWOp::create(rewriter, loc, atomicKind.value(), currPtr,
val, LLVM::AtomicOrdering::seq_cst);
resVec = vector::InsertOp::create(rewriter, loc, newVal, resVec, i);
}
rewriter.replaceOp(op, resVec);
return success();
}
};
class DpasMxToXeVMPattern : public OpConversionPattern<xegpu::DpasMxOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::DpasMxOp op, xegpu::DpasMxOp::Adaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto loc = op.getLoc();
auto ctxt = rewriter.getContext();
auto aTy = op.getA().getType();
auto bTy = op.getB().getType();
auto resVecTy =
cast<VectorType>(getTypeConverter()->convertType(op.getType()));
auto chipStr = xegpu::getChipStr(op);
if (!chipStr)
return rewriter.notifyMatchFailure(op, "cannot determine target chip");
const auto *uArch = xegpu::uArch::getUArch(*chipStr);
if (!uArch)
return rewriter.notifyMatchFailure(op, "unsupported target uArch");
// TODO: Add supported shape check
xevm::ElemType precATy = encodePrecision(aTy.getElementType());
xevm::ElemType precBTy = encodePrecision(bTy.getElementType());
Value c = adaptor.getAcc();
if (!c) {
auto elementTy = resVecTy.getElementType();
Attribute initValueAttr;
if (isa<FloatType>(elementTy))
initValueAttr = FloatAttr::get(elementTy, 0.0);
else
initValueAttr = IntegerAttr::get(elementTy, 0);
c = arith::ConstantOp::create(
rewriter, loc, DenseElementsAttr::get(resVecTy, initValueAttr));
}
Value aVec = adaptor.getA();
Value bVec = adaptor.getB();
auto aVecTy = cast<VectorType>(aVec.getType());
auto bVecTy = cast<VectorType>(bVec.getType());
if (aVecTy.getElementTypeBitWidth() == 4)
aVec = vector::BitCastOp::create(
rewriter, loc,
VectorType::get(aVecTy.getNumElements() / 2, rewriter.getI8Type()),
aVec);
if (bVecTy.getElementTypeBitWidth() == 4)
bVec = vector::BitCastOp::create(
rewriter, loc,
VectorType::get(bVecTy.getNumElements() / 2, rewriter.getI8Type()),
bVec);
auto cVecTy = cast<VectorType>(c.getType());
xevm::ElemType precCTy = encodePrecision(cVecTy.getElementType());
xevm::ElemType precDTy = encodePrecision(resVecTy.getElementType());
Value scaleA = adaptor.getScaleA();
Value scaleB = adaptor.getScaleB();
Value dpasMxRes = xevm::MMAMxOp::create(
rewriter, loc, resVecTy, aVec, bVec, scaleA, scaleB, c,
xevm::MMAShapeAttr::get(ctxt, cVecTy.getNumElements(), executionSize,
systolicDepth *
getNumOperandsPerDword(precATy)),
xevm::MMATypesAttr::get(ctxt, precDTy, precATy, precBTy, precCTy));
rewriter.replaceOp(op, dpasMxRes);
return success();
}
};
//===----------------------------------------------------------------------===//
// arith.extf / arith.truncf to xevm.extf / xevm.truncf
//===----------------------------------------------------------------------===//
//
// Micro-scaling (MX) GEMM lowering breaks arith.scaling_extf/scaling_truncf
// into plain arith.extf/arith.truncf whose narrow side uses one of the MX float
// formats (f8E5M2, f8E4M3FN or f4E2M1FN). These narrow floats have no native
// LLVM support, so the conversions are mapped onto the dedicated xevm.extf /
// xevm.truncf ops which lower to hardware builtins. The f8E8M0FNU scale type is
// intentionally not handled here: it is expanded into integer arithmetic by
// arith-expand before this pass runs.
// xevm.extf / xevm.truncf only convert between the MX narrow floats and
// f16/bf16, and the underlying builtins operate on exactly 16 f16/bf16 values.
static constexpr int64_t kXeVMExtfTruncfNumElems = 16;
// Maps a narrow MX float element type to the matching xevm.extf source enum.
static std::optional<xevm::ExtfSrcElemTypes> getExtfNarrowType(Type etype) {
if (isa<Float8E5M2Type>(etype))
return xevm::ExtfSrcElemTypes::BF8;
if (isa<Float8E4M3FNType>(etype))
return xevm::ExtfSrcElemTypes::F8;
if (isa<Float4E2M1FNType>(etype))
return xevm::ExtfSrcElemTypes::E2M1;
return std::nullopt;
}
// Maps a narrow MX float element type to the matching xevm.truncf dest enum.
static std::optional<xevm::TruncfDstElemTypes> getTruncfNarrowType(Type etype) {
if (isa<Float8E5M2Type>(etype))
return xevm::TruncfDstElemTypes::BF8;
if (isa<Float8E4M3FNType>(etype))
return xevm::TruncfDstElemTypes::F8;
if (isa<Float4E2M1FNType>(etype))
return xevm::TruncfDstElemTypes::E2M1;
return std::nullopt;
}
// Returns true if `op` is an arith.extf that can be lowered to xevm.extf, i.e.
// a rank-1 widening from an MX narrow float to a 16-element f16/bf16 vector.
static bool isXeVMExtf(arith::ExtFOp op) {
auto srcTy = dyn_cast<VectorType>(op.getIn().getType());
auto dstTy = dyn_cast<VectorType>(op.getType());
if (!srcTy || !dstTy || srcTy.getRank() != 1 || dstTy.getRank() != 1)
return false;
if (dstTy.getNumElements() != kXeVMExtfTruncfNumElems)
return false;
Type dstETy = dstTy.getElementType();
if (!dstETy.isF16() && !dstETy.isBF16())
return false;
return getExtfNarrowType(srcTy.getElementType()).has_value();
}
// Returns true if `op` is an arith.truncf that can be lowered to xevm.truncf,
// i.e. a rank-1 truncation from an f16/bf16 vector to an MX narrow float. The
// source has to hold a whole number of the fixed-size groups xevm.truncf
// converts at a time; wider vectors are converted in several steps.
static bool isXeVMTruncf(arith::TruncFOp op) {
auto srcTy = dyn_cast<VectorType>(op.getIn().getType());
auto dstTy = dyn_cast<VectorType>(op.getType());
if (!srcTy || !dstTy || srcTy.getRank() != 1 || dstTy.getRank() != 1)
return false;
int64_t numElems = srcTy.getNumElements();
if (numElems == 0 || numElems % kXeVMExtfTruncfNumElems != 0)
return false;
Type srcETy = srcTy.getElementType();
if (!srcETy.isF16() && !srcETy.isBF16())
return false;
return getTruncfNarrowType(dstTy.getElementType()).has_value();
}
class ExtfToXeVMPattern : public OpConversionPattern<arith::ExtFOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(arith::ExtFOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
if (!isXeVMExtf(op))
return rewriter.notifyMatchFailure(op, "not a xevm.extf compatible extf");
Location loc = op.getLoc();
MLIRContext *ctx = op.getContext();
auto srcVecTy = cast<VectorType>(op.getIn().getType());
auto dstVecTy = cast<VectorType>(op.getType());
xevm::ExtfSrcElemTypes srcEnum =
*getExtfNarrowType(srcVecTy.getElementType());
xevm::ExtfDstElemTypes dstEnum = dstVecTy.getElementType().isF16()
? xevm::ExtfDstElemTypes::F16
: xevm::ExtfDstElemTypes::BF16;
// The narrow float operand has already been type-converted to an integer
// vector of the same bit width (i4 for fp4, i8 for fp8). xevm.extf takes
// the values packed into an i8 vector, so re-pack fp4 (i4) operands.
Value src = adaptor.getIn();
auto convSrcTy = cast<VectorType>(src.getType());
if (convSrcTy.getElementTypeBitWidth() == 4)
src = vector::BitCastOp::create(
rewriter, loc,
VectorType::get(convSrcTy.getNumElements() / 2, rewriter.getI8Type()),
src);
Type resTy = getTypeConverter()->convertType(dstVecTy);
Value res = xevm::ExtfOp::create(
rewriter, loc, resTy, src, xevm::ExtfSrcElemTypeAttr::get(ctx, srcEnum),
xevm::ExtfDstElemTypeAttr::get(ctx, dstEnum));
rewriter.replaceOp(op, res);
return success();
}
};
class TruncfToXeVMPattern : public OpConversionPattern<arith::TruncFOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(arith::TruncFOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
if (!isXeVMTruncf(op))
return rewriter.notifyMatchFailure(op,
"not a xevm.truncf compatible truncf");
Location loc = op.getLoc();
MLIRContext *ctx = op.getContext();
auto srcVecTy = cast<VectorType>(op.getIn().getType());
auto dstVecTy = cast<VectorType>(op.getType());
xevm::TruncfSrcElemTypes srcEnum = srcVecTy.getElementType().isF16()
? xevm::TruncfSrcElemTypes::F16
: xevm::TruncfSrcElemTypes::BF16;
xevm::TruncfDstElemTypes dstEnum =
*getTruncfNarrowType(dstVecTy.getElementType());
auto srcEnumAttr = xevm::TruncfSrcElemTypeAttr::get(ctx, srcEnum);
auto dstEnumAttr = xevm::TruncfDstElemTypeAttr::get(ctx, dstEnum);
// xevm.truncf lowers to instructions that convert a fixed number of
// elements at a time, so a wider source is converted one group at a time
// and the packed results are concatenated. Each group produces the narrow
// floats packed into an i8 vector.
int64_t numGroups = srcVecTy.getNumElements() / kXeVMExtfTruncfNumElems;
int64_t groupBytes =
kXeVMExtfTruncfNumElems * dstVecTy.getElementTypeBitWidth() / 8;
Type groupTy = VectorType::get(groupBytes, rewriter.getI8Type());
Value src = adaptor.getIn();
Value packed;
if (numGroups == 1) {
packed = xevm::TruncfOp::create(rewriter, loc, groupTy, src, srcEnumAttr,
dstEnumAttr);
} else {
auto packedTy =
VectorType::get(groupBytes * numGroups, rewriter.getI8Type());
packed = arith::ConstantOp::create(rewriter, loc, packedTy,
rewriter.getZeroAttr(packedTy));
for (int64_t group = 0; group < numGroups; group++) {
Value slice = vector::ExtractStridedSliceOp::create(
rewriter, loc, src, group * kXeVMExtfTruncfNumElems,
kXeVMExtfTruncfNumElems, /*strides=*/1);
Value converted = xevm::TruncfOp::create(rewriter, loc, groupTy, slice,
srcEnumAttr, dstEnumAttr);
packed = vector::InsertStridedSliceOp::create(
rewriter, loc, converted, packed, group * groupBytes,
/*strides=*/1);
}
}
// Re-shape to the type-converted result type (i4 vector for fp4).
Type resTy = getTypeConverter()->convertType(dstVecTy);
if (packed.getType() != resTy)
packed = vector::BitCastOp::create(rewriter, loc, resTy, packed);
rewriter.replaceOp(op, packed);
return success();
}
};
// Lowers `xegpu.lane_shuffle` to `xevm.bitcast_shuffle`.
//
// `xevm.bitcast_shuffle` concatenates the components of its operand across the
// subgroup, the first component of every lane first, and then hands chunks the
// size of a result component back out to the lanes in order. Numbering the
// elements of a `vector<NxT>` fragment held by lane `i` of a subgroup of size
// `S` by their logical position, that concatenation is exactly the `pack` mode
// input numbering `j * S + i`. Taking the result as a single `N * width(T)` bit
// scalar then hands lane `i` the logical positions `i * N .. i * N + N - 1`,
// which is the `pack` mode output numbering.
//
// So `pack` is a vector-to-scalar `xevm.bitcast_shuffle` followed by a bitcast
// back to the fragment type, and `unpack`, being its inverse, is a bitcast to
// the scalar followed by a scalar-to-vector `xevm.bitcast_shuffle`.
//
// `xevm.bitcast_shuffle` only accepts the integer types `i8`, `i16`, `i32` and
// `i64`, since it is bit-preserving and so does not depend on how the bits are
// interpreted. A fragment of a floating point type is therefore bitcast to a
// same-width integer vector on the way in and back on the way out.
class LaneShuffleToXeVMPattern
: public OpConversionPattern<xegpu::LaneShuffleOp> {
using OpConversionPattern::OpConversionPattern;
LogicalResult
matchAndRewrite(xegpu::LaneShuffleOp op, OpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto vecTy = dyn_cast<VectorType>(adaptor.getSource().getType());
if (!vecTy)
return rewriter.notifyMatchFailure(op, "Expected a vector fragment.");
// The shuffle redistributes whole bytes between the lanes, so sub-byte
// element types, fp4 in particular, cannot be shuffled. Widths without a
// matching integer type the op accepts are rejected for the same reason.
unsigned elemBits = vecTy.getElementTypeBitWidth();
if (elemBits != 8 && elemBits != 16 && elemBits != 32 && elemBits != 64)
return rewriter.notifyMatchFailure(
op, "Expected an element type of 8, 16, 32 or 64 bits.");
int64_t fragmentBits = vecTy.getNumElements() * elemBits;
if (fragmentBits > 64 || !llvm::isPowerOf2_64(fragmentBits))
return rewriter.notifyMatchFailure(
op, "Expected a fragment of 8, 16, 32 or 64 bits.");
Location loc = op.getLoc();
Type packedTy = rewriter.getIntegerType(fragmentBits);
// The integer vector type the shuffle actually operates on. Equal to the
// fragment type when that is already an integer vector.
VectorType shuffleTy =
VectorType::get(vecTy.getShape(), rewriter.getIntegerType(elemBits));
Value res;
if (op.getMode() == xegpu::LaneShuffleMode::Pack) {
Value src = adaptor.getSource();
if (shuffleTy != vecTy)
src = LLVM::BitcastOp::create(rewriter, loc, shuffleTy, src);
res = xevm::BitcastShuffleOp::create(rewriter, loc, packedTy, src);
res = LLVM::BitcastOp::create(rewriter, loc, vecTy, res);
} else {
Value packed =
LLVM::BitcastOp::create(rewriter, loc, packedTy, adaptor.getSource());
res = xevm::BitcastShuffleOp::create(rewriter, loc, shuffleTy, packed);
if (shuffleTy != vecTy)
res = LLVM::BitcastOp::create(rewriter, loc, vecTy, res);
}
rewriter.replaceOp(op, res);
return success();
}
};
//===----------------------------------------------------------------------===//
// Pass Definition
//===----------------------------------------------------------------------===//
struct ConvertXeGPUToXeVMPass
: public impl::ConvertXeGPUToXeVMPassBase<ConvertXeGPUToXeVMPass> {
using Base::Base;
void runOnOperation() override {
MLIRContext *context = &getContext();
// XeVM type converter is based on LLVM type converter with the
// following customizations.
// First, type conversion rules are added for xegpu custom types,
// TensorDescType and MemDescType.
// Second, MemRefType is lowered to single integer type
// Third, VectorType of single element or 0D is converted to vector
// element type. Otherwise, vector type is flatten to 1D.
LowerToLLVMOptions options(context);
options.overrideIndexBitwidth(this->use64bitIndex ? 64 : 32);
LLVMTypeConverter typeConverter(context, options);
Type xevmIndexType = typeConverter.convertType(IndexType::get(context));
Type i32Type = IntegerType::get(context, 32);
typeConverter.addConversion([&](VectorType type) -> Type {
auto elemType = typeConverter.convertType(type.getElementType());
// If the vector rank is 0 or has a single element, return the element
unsigned rank = type.getRank();
if (rank == 0 || type.getNumElements() == 1)
return elemType;
// Otherwise, convert the vector to a flat vector type.
int64_t sum = llvm::product_of(type.getShape());
return VectorType::get(sum, elemType);
});
typeConverter.addConversion([&](xegpu::TensorDescType type) -> Type {
if (type.getRank() == 1)
return xevmIndexType;
return VectorType::get(8, i32Type);
});
// SLM access related type conversions.
// TODO: LLVM DLTI provides clean way of representing different pointer size
// based on address space. Currently pointer size of SLM access is hard
// coded to 32bit. Update to use DLTI when switching overall XeGPU lowering
// to use DLTI instead of use64bitIndex option used above.
// Convert MemDescType into i32 for SLM
typeConverter.addConversion(
[&](xegpu::MemDescType type) -> Type { return i32Type; });
typeConverter.addConversion([&](MemRefType type) -> Type {
return isSharedMemRef(type) ? i32Type : xevmIndexType;
});
// LLVM type converter puts unrealized casts for the following cases:
// add materialization casts to handle them.
// Materialization to convert memref to i64 or i32 depending on global/SLM
// Applies only to target materialization.
// Note: int type to memref materialization is not required as xegpu ops
// currently do not produce memrefs as result.
auto memrefToIntMaterializationCast = [](OpBuilder &builder, Type type,
ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
if (auto memrefTy = dyn_cast<MemRefType>(input.getType())) {
unsigned rank = memrefTy.getRank();
Type indexType = builder.getIndexType();
int64_t intOffsets;
SmallVector<int64_t> intStrides;
Value addr;
Value offset;
if (succeeded(memrefTy.getStridesAndOffset(intStrides, intOffsets)) &&
ShapedType::isStatic(intOffsets)) {
addr = memref::ExtractAlignedPointerAsIndexOp::create(builder, loc,
input);
offset = arith::ConstantOp::create(builder, loc,
builder.getIndexAttr(intOffsets));
} else {
// Result types: [base_memref, offset, stride0, stride1, ...,
// strideN-1, size0, size1, ..., sizeN-1]
SmallVector<Type> resultTypes{
MemRefType::get({}, memrefTy.getElementType(),
MemRefLayoutAttrInterface(),
memrefTy.getMemorySpace()),
indexType};
// strides + sizes
resultTypes.append(2 * rank, indexType);
auto meta = memref::ExtractStridedMetadataOp::create(
builder, loc, resultTypes, input);
addr = memref::ExtractAlignedPointerAsIndexOp::create(
builder, loc, meta.getBaseBuffer());
offset = meta.getOffset();
}
auto addrCasted =
arith::IndexCastUIOp::create(builder, loc, type, addr);
auto offsetCasted =
arith::IndexCastUIOp::create(builder, loc, type, offset);
// Compute the final address: base address + byte offset
auto byteSize = arith::ConstantOp::create(
builder, loc, type,
builder.getIntegerAttr(type,
memrefTy.getElementTypeBitWidth() / 8));
auto byteOffset =
arith::MulIOp::create(builder, loc, offsetCasted, byteSize);
auto addrWithOffset =
arith::AddIOp::create(builder, loc, addrCasted, byteOffset);
return addrWithOffset.getResult();
}
return {};
};
// Materialization to convert ui64 to i64
// Applies only to target materialization.
// Note: i64 to ui64 materialization is not required as xegpu ops
// currently do not produce ui64 as result.
auto ui64ToI64MaterializationCast = [](OpBuilder &builder, Type type,
ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
if (input.getType() == builder.getIntegerType(64, false)) {
Value cast =
index::CastUOp::create(builder, loc, builder.getIndexType(), input)
.getResult();
return arith::IndexCastUIOp::create(builder, loc, type, cast)
.getResult();
}
return {};
};
// Materialization to convert ui32 to i32
// Applies only to target materialization.
// Note: i32 to ui32 materialization is not required as xegpu ops
// currently do not produce ui32 as result.
auto ui32ToI32MaterializationCast = [](OpBuilder &builder, Type type,
ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
if (input.getType() == builder.getIntegerType(32, false)) {
Value cast =
index::CastUOp::create(builder, loc, builder.getIndexType(), input)
.getResult();
return arith::IndexCastUIOp::create(builder, loc, type, cast)
.getResult();
}
return {};
};
// Materialization to convert between vector types
// - Add shape cast for different shapes
// - Add bitcast for different element types
// Applies to both source and target materialization.
auto vectorToVectorMaterializationCast = [](OpBuilder &builder, Type type,
ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
if (auto vecTy = dyn_cast<VectorType>(input.getType())) {
if (auto targetVecTy = dyn_cast<VectorType>(type)) {
Value cast = input;
// If the target type has a different shape, add a shape cast
// If the target type has a different element type, add a bitcast
if (targetVecTy.getShape() != vecTy.getShape()) {
cast = vector::ShapeCastOp::create(
builder, loc,
VectorType::get(targetVecTy.getShape(),
vecTy.getElementType()),
cast)
.getResult();
}
if (targetVecTy.getElementType() != vecTy.getElementType()) {
cast = vector::BitCastOp::create(builder, loc, targetVecTy, cast)
.getResult();
}
return cast;
}
}
return {};
};
// Materialization to convert
// - single element vector to single element of vector element type
// Applies only to target materialization.
auto vectorToSingleElementMaterializationCast =
[](OpBuilder &builder, Type type, ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
if (auto vecTy = dyn_cast<VectorType>(input.getType())) {
// Source needs to be single element vector
auto rank = vecTy.getRank();
if (rank != 0 && vecTy.getNumElements() != 1)
return {};
auto inElemTy = vecTy.getElementType();
// extract scalar
Value cast = input;
if (rank == 0) {
cast = vector::ExtractOp::create(builder, loc, cast, {}).getResult();
} else {
cast = vector::ExtractOp::create(builder, loc, cast,
SmallVector<int64_t>(rank, 0))
.getResult();
}
// Extracted element type may need conversion
// Two cases
// 1. Index type to integer type
// 2. Other element type mismatch
if (inElemTy.isIndex()) {
cast = arith::IndexCastUIOp::create(builder, loc, type, cast)
.getResult();
} else if (inElemTy != type) {
cast = arith::BitcastOp::create(builder, loc, type, cast).getResult();
}
return cast;
}
return {};
};
// Materialization to convert
// - single element of vector element type to single element vector
// If result type of original op is single element vector and lowered type
// is scalar. This materialization cast creates a single element vector by
// First convert element type if needed and then broadcast to single
// element vector.
// Applies only to source materialization.
auto singleElementToVectorMaterializationCast =
[](OpBuilder &builder, Type type, ValueRange inputs,
Location loc) -> Value {
if (inputs.size() != 1)
return {};
auto input = inputs.front();
auto inTy = input.getType();
if (!inTy.isIntOrFloat())
return {};
// If the target type is a vector of rank 0 or single element vector
// of element type matching input type, broadcast input to target type.
if (auto vecTy = dyn_cast<VectorType>(type)) {
if (vecTy.getRank() != 0 && vecTy.getNumElements() != 1)
return {};
auto outElemTy = vecTy.getElementType();
Value cast = input;
if (outElemTy.isIndex()) {
cast = arith::IndexCastUIOp::create(builder, loc,
builder.getIndexType(), cast)
.getResult();
} else if (inTy != outElemTy) {
cast = arith::BitcastOp::create(builder, loc, outElemTy, cast)
.getResult();
}
return vector::BroadcastOp::create(builder, loc, vecTy, cast)
.getResult();
}
return {};
};
typeConverter.addSourceMaterialization(
singleElementToVectorMaterializationCast);
typeConverter.addSourceMaterialization(vectorToVectorMaterializationCast);
typeConverter.addTargetMaterialization(memrefToIntMaterializationCast);
typeConverter.addTargetMaterialization(ui32ToI32MaterializationCast);
typeConverter.addTargetMaterialization(ui64ToI64MaterializationCast);
typeConverter.addTargetMaterialization(
vectorToSingleElementMaterializationCast);
typeConverter.addTargetMaterialization(vectorToVectorMaterializationCast);
ConversionTarget target(*context);
target.addLegalDialect<xevm::XeVMDialect, LLVM::LLVMDialect,
vector::VectorDialect, arith::ArithDialect,
memref::MemRefDialect, gpu::GPUDialect,
index::IndexDialect>();
target.addIllegalDialect<xegpu::XeGPUDialect>();
// arith.extf/arith.truncf between MX narrow floats and f16/bf16 are routed
// to xevm.extf/xevm.truncf; all other arith float casts stay legal.
target.addDynamicallyLegalOp<arith::ExtFOp>(
[](arith::ExtFOp op) { return !isXeVMExtf(op); });
target.addDynamicallyLegalOp<arith::TruncFOp>(
[](arith::TruncFOp op) { return !isXeVMTruncf(op); });
RewritePatternSet patterns(context);
populateXeGPUToXeVMConversionPatterns(typeConverter, patterns);
scf::populateSCFStructuralTypeConversionsAndLegality(typeConverter,
patterns, target);
if (failed(applyPartialConversion(getOperation(), target,
std::move(patterns))))
signalPassFailure();
}
};
} // namespace
//===----------------------------------------------------------------------===//
// Pattern Population
//===----------------------------------------------------------------------===//
void mlir::populateXeGPUToXeVMConversionPatterns(
const LLVMTypeConverter &typeConverter, RewritePatternSet &patterns) {
patterns.add<CreateNdDescToXeVMPattern,
LoadStorePrefetchNdToXeVMPattern<xegpu::LoadNdOp>,
LoadStorePrefetchNdToXeVMPattern<xegpu::StoreNdOp>,
LoadStorePrefetchNdToXeVMPattern<xegpu::PrefetchNdOp>>(
typeConverter, patterns.getContext());
patterns.add<AtomicRMWToXeVMPattern, PrefetchToXeVMPattern,
LoadStoreToXeVMPattern<xegpu::LoadGatherOp>,
LoadStoreToXeVMPattern<xegpu::StoreScatterOp>>(
typeConverter, patterns.getContext());
patterns.add<LoadStoreMatrixToXeVMPattern<xegpu::LoadMatrixOp>,
LoadStoreMatrixToXeVMPattern<xegpu::StoreMatrixOp>,
CreateMemDescOpPattern>(typeConverter, patterns.getContext());
patterns.add<FenceToXeVMPattern, DpasToXeVMPattern>(typeConverter,
patterns.getContext());
patterns.add<DpasMxToXeVMPattern>(typeConverter, patterns.getContext());
patterns.add<ExtfToXeVMPattern, TruncfToXeVMPattern>(typeConverter,
patterns.getContext());
patterns.add<LaneShuffleToXeVMPattern>(typeConverter, patterns.getContext());
}