blob: a4813528e98dd0a6c447c00cc38bd452fe832f0f [file]
//===- TestXeGPUTransforms.cpp -- Test Vector transforms and lowerings ----===//
//
// Part of the LLVM Project, 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/Dialect/Arith/IR/Arith.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/Index/IR/IndexDialect.h"
#include "mlir/Dialect/Math/IR/Math.h"
#include "mlir/Dialect/SCF/Transforms/Patterns.h"
#include "mlir/Dialect/Vector/Transforms/VectorTransforms.h"
#include "mlir/Dialect/XeGPU/IR/XeGPU.h"
#include "mlir/Dialect/XeGPU/Transforms/Transforms.h"
#include "mlir/Dialect/XeGPU/Transforms/XeGPULayoutImpl.h"
#include "mlir/Dialect/XeGPU/Utils/XeGPUUtils.h"
#include "mlir/Dialect/XeGPU/uArch/IntelGpuXe2.h"
#include "mlir/IR/BuiltinTypes.h"
#include "mlir/IR/Value.h"
#include "mlir/Pass/Pass.h"
#include "mlir/Pass/PassManager.h"
#include "mlir/Transforms/DialectConversion.h"
#include "mlir/Transforms/GreedyPatternRewriteDriver.h"
#include "llvm/Support/raw_ostream.h"
#include <optional>
using namespace mlir;
using namespace mlir::xegpu;
namespace {
#define DEBUG_TYPE "test-xegpu-unroll"
struct TestXeGPUUnrollingPatterns
: public PassWrapper<TestXeGPUUnrollingPatterns,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUUnrollingPatterns)
StringRef getArgument() const final {
return "test-xegpu-unrolling-patterns";
}
StringRef getDescription() const final {
return "Test lowering patterns to unroll ops in the xegpu dialect";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<memref::MemRefDialect>();
registry.insert<xegpu::XeGPUDialect>();
registry.insert<vector::VectorDialect>();
}
TestXeGPUUnrollingPatterns() = default;
TestXeGPUUnrollingPatterns(const TestXeGPUUnrollingPatterns &pass) = default;
void runOnOperation() override {
MLIRContext *ctx = &getContext();
xegpu::UnrollOptions options;
options.setNativeShapeFn([&](Operation *op)
-> std::optional<SmallVector<int64_t>> {
if (isa<xegpu::CreateNdDescOp, xegpu::PrefetchNdOp, xegpu::LoadNdOp,
xegpu::StoreNdOp, xegpu::PrefetchOp, xegpu::LoadGatherOp,
xegpu::StoreScatterOp>(op)) {
xegpu::TensorDescType tdescTy;
if (auto createNdOp = dyn_cast<xegpu::CreateNdDescOp>(op)) {
tdescTy = createNdOp.getType();
} else if (auto prefetchNdOp = dyn_cast<xegpu::PrefetchNdOp>(op)) {
tdescTy = prefetchNdOp.getTensorDescType();
} else if (auto loadNdOp = dyn_cast<xegpu::LoadNdOp>(op)) {
tdescTy = loadNdOp.getTensorDescType();
} else if (auto storeNdOp = dyn_cast<xegpu::StoreNdOp>(op)) {
tdescTy = storeNdOp.getTensorDescType();
} else if (isa<xegpu::PrefetchOp, xegpu::LoadGatherOp,
xegpu::StoreScatterOp>(op)) {
auto anchorOp = cast<xegpu::AnchorLayoutInterface>(op);
auto layout =
dyn_cast_or_null<xegpu::LayoutAttr>(anchorOp.getAnchorLayout());
if (layout && layout.isForSubgroup()) {
auto inst_data = layout.getEffectiveInstDataAsInt();
if (!inst_data.empty())
return SmallVector<int64_t>(inst_data.begin(), inst_data.end());
}
return std::nullopt;
}
if (auto layout = tdescTy.getLayoutAttr()) {
auto inst_data = layout.getEffectiveInstDataAsInt();
if (!inst_data.empty() && layout.isForSubgroup())
return SmallVector<int64_t>(inst_data.begin(), inst_data.end());
}
}
if (isa<xegpu::DpasOp>(op))
return SmallVector<int64_t>{8, 16, 16};
// For vector.multi_reduction, read tile shape from the layout attribute
// on the source operand (layout_operand_0).
if (isa<vector::MultiDimReductionOp>(op)) {
xegpu::DistributeLayoutAttr layout =
xegpu::getDistributeLayoutAttr(op->getOpOperand(0));
if (layout) {
auto instData = layout.getEffectiveInstDataAsInt();
if (!instData.empty())
return instData;
}
return std::nullopt;
}
return std::nullopt;
});
options.setUnrolledTypesFn(
[&](ShapedType type, ArrayRef<int64_t> tileShape) -> SmallVector<Type> {
Type elemTy = type.getElementType();
Type newTy;
// TensorDescType needs to drop the inst_data field in the layout
// attribute
if (auto tdescTy = dyn_cast<xegpu::TensorDescType>(type)) {
Attribute encoding = tdescTy.getEncoding();
auto layout = tdescTy.getLayoutAttr();
if (layout) {
if (layout.getEffectiveLaneLayoutAsInt().empty())
layout = xegpu::LayoutAttr();
else
layout = layout.dropInstData();
}
newTy = xegpu::TensorDescType::get(ctx, tileShape, elemTy, encoding,
layout);
// compute the product of batch (higher) dimensions
ArrayRef<int64_t> shape = type.getShape();
int64_t batchCount =
shape.size() > 2 ? computeProduct(shape.drop_back(2)) : 1;
return SmallVector<Type>(batchCount, newTy);
}
newTy = type.clone(tileShape, elemTy);
std::optional<SmallVector<int64_t>> ratio =
computeShapeRatio(type.getShape(), tileShape);
assert(ratio && "Expecting the ratio to be valid.");
return SmallVector<Type>(computeProduct(*ratio), newTy);
});
RewritePatternSet patterns(ctx);
populateXeGPUUnrollPatterns(patterns, options);
(void)applyPatternsGreedily(getOperation(), std::move(patterns));
}
};
#undef DEBUG_TYPE
#define DEBUG_TYPE "test-xegpu-layout-interface"
// Test pattern for distributing vector::StepOp from workgroup to subgroup.
// Validates DistributeLayoutAttr interfaces for offset computation
// abstraction between LayoutAttr and SliceAttr.
class TestStepOpPattern : public OpConversionPattern<vector::StepOp> {
using OpConversionPattern<vector::StepOp>::OpConversionPattern;
LogicalResult
matchAndRewrite(vector::StepOp op, OneToNOpAdaptor adaptor,
ConversionPatternRewriter &rewriter) const override {
auto layoutName = xegpu::getTemporaryLayoutName(op->getResult(0));
auto sliceAttr = op->getDiscardableAttrOfType<xegpu::SliceAttr>(layoutName);
if (!sliceAttr || sliceAttr.getRank() != 1)
return failure();
std::optional<SmallVector<int64_t>> sgShape =
sliceAttr.getEffectiveSgDataAsInt();
if (!sgShape)
return failure();
Location loc = op.getLoc();
VectorType type = op.getResult().getType();
auto wgShape = type.getShape();
Value sgId =
gpu::SubgroupIdOp::create(rewriter, loc, /*upper_bound=*/nullptr);
auto maybeOffsets =
sliceAttr.computeDistributedCoords(rewriter, loc, sgId, wgShape);
if (failed(maybeOffsets))
return failure();
VectorType newTy = type.cloneWith(*sgShape, type.getElementType());
Value base = vector::StepOp::create(rewriter, loc, newTy);
SmallVector<Value> newOps;
for (auto offsets : *maybeOffsets) {
Value bcast =
vector::BroadcastOp::create(rewriter, loc, newTy, offsets[0]);
Value add = arith::AddIOp::create(rewriter, loc, base, bcast);
newOps.push_back(add);
}
rewriter.replaceOpWithMultiple(op, {newOps});
return success();
}
};
struct TestXeGPURecoverTemporaryLayouts
: public PassWrapper<TestXeGPURecoverTemporaryLayouts,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPURecoverTemporaryLayouts)
StringRef getArgument() const final {
return "test-xegpu-recover-temporary-layouts";
}
StringRef getDescription() const final {
return "Test the implementation of XeGPU temporary layout recovery";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<arith::ArithDialect>();
registry.insert<memref::MemRefDialect>();
registry.insert<xegpu::XeGPUDialect>();
registry.insert<vector::VectorDialect>();
registry.insert<gpu::GPUDialect>();
}
TestXeGPURecoverTemporaryLayouts() = default;
TestXeGPURecoverTemporaryLayouts(const TestXeGPURecoverTemporaryLayouts &pass)
: PassWrapper(pass) {}
void runOnOperation() override {
Operation *op = getOperation();
if (!xegpu::recoverTemporaryLayouts(op))
signalPassFailure();
}
};
/// This test pass is intended to test the subgroup to lane distribution of
/// xegpu/vector/arith operations in isolation, it does not handle any
/// structural ops like scf.for etc.
struct TestXeGPUSgToLaneDistribute
: public PassWrapper<TestXeGPUSgToLaneDistribute,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUSgToLaneDistribute)
StringRef getArgument() const final {
return "test-xegpu-sg-to-lane-distribute";
}
StringRef getDescription() const final {
return "Test the implementation of XeGPU Subgroup to Lane Distribution";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<arith::ArithDialect>();
registry.insert<memref::MemRefDialect>();
registry.insert<xegpu::XeGPUDialect>();
registry.insert<vector::VectorDialect>();
registry.insert<index::IndexDialect>();
registry.insert<gpu::GPUDialect>();
}
TestXeGPUSgToLaneDistribute() = default;
TestXeGPUSgToLaneDistribute(const TestXeGPUSgToLaneDistribute &pass)
: PassWrapper(pass) {}
void runOnOperation() override {
Operation *op = getOperation();
if (!xegpu::recoverTemporaryLayouts(op)) {
signalPassFailure();
return;
}
MLIRContext *ctx = &getContext();
TypeConverter typeConverter;
// Define type materializations using UnrealizedConversionCastOp.
auto materializeCast = [&](mlir::OpBuilder &builder, mlir::Type type,
mlir::ValueRange inputs,
mlir::Location loc) -> mlir::Value {
return UnrealizedConversionCastOp::create(builder, loc, type, inputs)
.getResult(0);
};
typeConverter.addSourceMaterialization(materializeCast);
typeConverter.addTargetMaterialization(materializeCast);
ConversionTarget target(*ctx);
RewritePatternSet patterns(ctx);
xegpu::populateXeGPUSgToLaneDistributeTypeConversionAndLegality(
typeConverter, patterns, target, op);
(void)applyPartialConversion(op, target, std::move(patterns));
}
};
struct TestXeGPUPropagateLayouts
: public PassWrapper<TestXeGPUPropagateLayouts,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUPropagateLayouts)
StringRef getArgument() const final { return "test-xegpu-propagate-layouts"; }
StringRef getDescription() const final {
return "Test the implementation of XeGPU propagate layouts.";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<xegpu::XeGPUDialect>();
registry.insert<gpu::GPUDialect>();
}
TestXeGPUPropagateLayouts() = default;
TestXeGPUPropagateLayouts(const TestXeGPUPropagateLayouts &pass)
: PassWrapper(pass) {}
Option<std::string> layoutKind{
*this, "layout-kind",
llvm::cl::desc("Propagate `subgroup` / `inst` / `lane` level of xegpu "
"layouts."),
llvm::cl::init("lane")};
void runOnOperation() override {
OpBuilder builder(getOperation());
LayoutKind kind;
if (layoutKind == "subgroup")
kind = LayoutKind::Subgroup;
else if (layoutKind == "inst")
kind = LayoutKind::InstData;
else if (layoutKind == "lane")
kind = LayoutKind::Lane;
else {
signalPassFailure();
return;
}
if (failed(xegpu::propagateLayouts(builder, getOperation(), kind, 32))) {
signalPassFailure();
}
}
};
struct TestXeGPUResolveLayoutConflicts
: public PassWrapper<TestXeGPUResolveLayoutConflicts,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUResolveLayoutConflicts)
StringRef getArgument() const final {
return "test-xegpu-resolve-layout-conflicts";
}
StringRef getDescription() const final {
return "Test the implementation of XeGPU layout conflict resolution.";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<xegpu::XeGPUDialect>();
registry.insert<gpu::GPUDialect>();
}
TestXeGPUResolveLayoutConflicts() = default;
TestXeGPUResolveLayoutConflicts(const TestXeGPUResolveLayoutConflicts &pass) =
default;
void runOnOperation() override {
if (failed(xegpu::resolveLayoutConflicts(getOperation())))
signalPassFailure();
}
};
struct TestXeGPUArrayLengthOptimization
: public PassWrapper<TestXeGPUArrayLengthOptimization,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUArrayLengthOptimization)
StringRef getArgument() const final {
return "test-xegpu-array-length-optimization";
}
StringRef getDescription() const final {
return "Test XeGPU 2D block array load optimization patterns in isolation";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<xegpu::XeGPUDialect>();
registry.insert<vector::VectorDialect>();
}
TestXeGPUArrayLengthOptimization() = default;
TestXeGPUArrayLengthOptimization(const TestXeGPUArrayLengthOptimization &pass)
: PassWrapper(pass) {}
void runOnOperation() override {
RewritePatternSet patterns(&getContext());
xegpu::populateXeGPUArrayLengthOptimizationPatterns(patterns);
if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
signalPassFailure();
}
};
struct TestXeGPULayoutInterface
: public PassWrapper<TestXeGPULayoutInterface,
OperationPass<gpu::GPUModuleOp>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPULayoutInterface)
StringRef getArgument() const final { return "test-xegpu-layout-interface"; }
StringRef getDescription() const final {
return "Test the implementation of XeGPU Layout interfaces";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<arith::ArithDialect>();
registry.insert<memref::MemRefDialect>();
registry.insert<xegpu::XeGPUDialect>();
registry.insert<vector::VectorDialect>();
registry.insert<index::IndexDialect>();
}
TestXeGPULayoutInterface() = default;
TestXeGPULayoutInterface(const TestXeGPULayoutInterface &pass) = default;
void runOnOperation() override {
MLIRContext *ctx = &getContext();
TypeConverter typeConverter;
auto materializeCast = [&](mlir::OpBuilder &builder, mlir::Type type,
mlir::ValueRange inputs,
mlir::Location loc) -> mlir::Value {
return UnrealizedConversionCastOp::create(builder, loc, type, inputs)
.getResult(0);
};
typeConverter.addSourceMaterialization(materializeCast);
typeConverter.addTargetMaterialization(materializeCast);
RewritePatternSet patterns(ctx);
patterns.add<TestStepOpPattern>(typeConverter, ctx);
ConversionTarget target(*ctx);
auto isLegal = [&](xegpu::SliceAttr layout) -> bool {
return !layout || !layout.isForWorkgroup();
};
target.addDynamicallyLegalOp<vector::StepOp>(
[&](vector::StepOp op) -> bool {
auto layoutName = xegpu::getTemporaryLayoutName(op->getResult(0));
auto sliceAttr =
op->getDiscardableAttrOfType<xegpu::SliceAttr>(layoutName);
return isLegal(sliceAttr);
});
target.markUnknownOpDynamicallyLegal([](Operation *op) { return true; });
(void)applyPartialConversion(getOperation(), target, std::move(patterns));
}
};
struct TestXeGPUCoalesceGatherScatter
: public PassWrapper<TestXeGPUCoalesceGatherScatter, OperationPass<>> {
MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(TestXeGPUCoalesceGatherScatter)
StringRef getArgument() const final {
return "test-xegpu-coalesce-gather-scatter";
}
StringRef getDescription() const final {
return "Test driver that turns the contiguity attribute into a lane_data "
"layout on gather/scatter ops.";
}
void getDependentDialects(::mlir::DialectRegistry &registry) const override {
registry.insert<arith::ArithDialect>();
registry.insert<vector::VectorDialect>();
registry.insert<xegpu::XeGPUDialect>();
}
TestXeGPUCoalesceGatherScatter() = default;
TestXeGPUCoalesceGatherScatter(const TestXeGPUCoalesceGatherScatter &pass)
: PassWrapper(pass) {}
Option<unsigned> maxChunkSize{
*this, "max-chunk-size",
llvm::cl::desc("Upper bound on the produced lane_data FCD."),
llvm::cl::init(8)};
Option<bool> analyzeOnly{
*this, "analyze-only",
llvm::cl::desc("Only run the analysis (stamp the contiguity attribute); "
"do not apply."),
llvm::cl::init(false)};
void runOnOperation() override {
xegpu::runContiguityAnalysis(getOperation());
if (analyzeOnly)
return;
getOperation()->walk([&](Operation *op) {
if (auto load = dyn_cast<xegpu::LoadGatherOp>(op))
applyContiguity(load, maxChunkSize);
else if (auto store = dyn_cast<xegpu::StoreScatterOp>(op))
applyContiguity(store, maxChunkSize);
});
}
private:
/// Build a `lane_layout`/`lane_data`/`inst_data` layout of rank `rank`, with
/// the given lane_layout / lane_data on the innermost dim (1 elsewhere).
/// `inst_data` is `lane_layout * lane_data` per dim.
static xegpu::LayoutAttr buildLaneDataLayout(MLIRContext *ctx, unsigned rank,
int64_t innerLaneLayout,
int64_t innerLaneData) {
SmallVector<int32_t> laneLayout(rank, 1);
SmallVector<int32_t> laneData(rank, 1);
SmallVector<int32_t> instData(rank, 1);
laneLayout.back() = static_cast<int32_t>(innerLaneLayout);
laneData.back() = static_cast<int32_t>(innerLaneData);
instData.back() = static_cast<int32_t>(innerLaneLayout * innerLaneData);
return xegpu::LayoutAttr::get(ctx, instData, laneLayout, laneData);
}
/// Minimal driver: read the `contiguity` attribute the analysis stamped and
/// turn it into a `lane_data` layout. This is only a stand-in for the real
/// consumer (layout propagation) so the analysis output can be checked
/// end-to-end; it handles just the simple power-of-two case.
template <typename OpTy>
static void applyContiguity(OpTy op, unsigned maxChunkSize) {
std::optional<uint64_t> contiguity = op.getContiguity();
if (!contiguity)
return;
op.removeContiguityAttr();
auto offsetsTy = dyn_cast<VectorType>(op.getOffsets().getType());
auto valueTy = op.getValueType();
if (!offsetsTy || !valueTy)
return;
const auto *uArch =
xegpu::uArch::getUArch(xegpu::getChipStr(op).value_or(""));
if (!uArch)
return;
int64_t subgroupSize = uArch->getSubgroupSize();
// Tile size and subgroup size are powers of two, so the smaller is already
// the largest power-of-two divisor; min() suffices throughout.
int64_t inner = offsetsTy.getShape().back();
int64_t laneLayout = std::min<int64_t>(subgroupSize, inner);
int64_t perLane = inner / laneLayout;
int64_t laneData =
std::min<int64_t>({static_cast<int64_t>(*contiguity),
static_cast<int64_t>(maxChunkSize), perLane});
if (laneData < 2)
return;
op.setLayoutAttr(buildLaneDataLayout(op.getContext(), valueTy.getRank(),
laneLayout, laneData));
}
};
} // namespace
namespace mlir {
namespace test {
void registerTestXeGPULowerings() {
PassRegistration<TestXeGPUUnrollingPatterns>();
PassRegistration<TestXeGPULayoutInterface>();
PassRegistration<TestXeGPURecoverTemporaryLayouts>();
PassRegistration<TestXeGPUSgToLaneDistribute>();
PassRegistration<TestXeGPUPropagateLayouts>();
PassRegistration<TestXeGPUResolveLayoutConflicts>();
PassRegistration<TestXeGPUArrayLengthOptimization>();
PassRegistration<TestXeGPUCoalesceGatherScatter>();
}
} // namespace test
} // namespace mlir