blob: ac4aea9d60d54aeec150a05e7c6f4ee9fe1e7e48 [file] [edit]
//===---- XeGPULayoutImpl.cpp - MLIR Utilities for XeGPUOps
//------------------===//
//
// Part of the MLIR 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
//
//===----------------------------------------------------------------------===//
//
// This file implements layout utility functions for XeGPU dialect
// transformation.
//
//===----------------------------------------------------------------------===//
#include "mlir/Dialect/XeGPU/Transforms/XeGPULayoutImpl.h"
#include "mlir/Dialect/Func/IR/FuncOps.h"
#include "mlir/Dialect/GPU/IR/GPUDialect.h"
#include "mlir/Dialect/LLVMIR/XeVMDialect.h"
#include "mlir/Dialect/SCF/Transforms/Patterns.h"
#include "mlir/Dialect/Utils/IndexingUtils.h"
#include "mlir/Dialect/Vector/IR/VectorOps.h"
#include "mlir/Dialect/XeGPU/IR/XeGPU.h"
#include "mlir/IR/Builders.h"
#include "mlir/IR/Operation.h"
#include "mlir/IR/ValueRange.h"
#include "mlir/Interfaces/ControlFlowInterfaces.h"
#include "mlir/Interfaces/LoopLikeInterface.h"
#include "mlir/Interfaces/SideEffectInterfaces.h"
#include "mlir/Transforms/DialectConversion.h"
#include "llvm/ADT/PostOrderIterator.h"
#include "llvm/Support/FormatVariadic.h"
#include <cstdint>
#include <numeric>
using namespace mlir;
SmallVector<NamedAttribute>
xegpu::dropSgLayoutAndDataOnAttrs(ArrayRef<NamedAttribute> attrs) {
SmallVector<NamedAttribute> out;
out.reserve(attrs.size());
for (auto attr : attrs) {
if (auto dist = dyn_cast<xegpu::DistributeLayoutAttr>(attr.getValue())) {
auto newLayout = dist.dropSgLayoutAndData();
if (newLayout)
out.emplace_back(attr.getName(), newLayout);
} else {
out.push_back(attr);
}
}
return out;
}
SmallVector<NamedAttribute>
xegpu::dropInstDataOnAttrs(ArrayRef<NamedAttribute> attrs) {
SmallVector<NamedAttribute> out;
out.reserve(attrs.size());
for (auto attr : attrs) {
if (auto dist = dyn_cast<xegpu::DistributeLayoutAttr>(attr.getValue())) {
auto newLayout = dist.dropInstData();
if (newLayout)
out.emplace_back(attr.getName(), newLayout);
} else {
out.push_back(attr);
}
}
return out;
}
// Sets the layout on a TensorDesc value by updating its type to include
// the given layout, if the type does not already have a layout attached.
static void setTensorDescLayout(Value val, xegpu::DistributeLayoutAttr layout) {
auto tensorDescTy = dyn_cast<xegpu::TensorDescType>(val.getType());
if (!tensorDescTy || tensorDescTy.getLayoutAttr())
return;
auto typeWithLayout = xegpu::TensorDescType::get(
tensorDescTy.getContext(), tensorDescTy.getShape(),
tensorDescTy.getElementType(), tensorDescTy.getEncoding(), layout);
val.setType(typeWithLayout);
}
// the walkRegionBackward() is a recursive function
// the input rootOp is the function operation, which is also a region op.
// it recursively processes the region op in reverse topological order.
static void walkRegionBackward(Region &region,
llvm::function_ref<void(Operation *)> visit) {
// Use post-order traversal to process blocks in reverse topological order.
// This ensures that use blocks are visited before def blocks, which is
// required for backward layout propagation.
if (region.empty())
return;
llvm::ReversePostOrderTraversal<Region *> rpot(&region);
SmallVector<Block *> blocks(rpot.begin(), rpot.end());
for (Block *block : llvm::reverse(blocks)) {
// ops: back -> front
for (Operation &op : llvm::reverse(*block)) {
// make sure we first visit inside the region op (so yield op first)
// and then move to region op itself
// Regions are iterated in forward order so that for multi-region ops
// like scf.while, earlier regions (e.g., "before/cond") are processed
// first. This ensures that when a later region's terminator (e.g., "do"
// yield) needs the layout of an earlier region's block args, those
// layouts are already available from use points.
for (Region &nested : op.getRegions())
walkRegionBackward(nested, visit);
visit(&op);
}
}
}
static xegpu::DistributeLayoutAttr getLayoutFromUsePoints(Value result) {
xegpu::DistributeLayoutAttr layout = nullptr;
for (OpOperand &use : result.getUses()) {
if (auto tmpLayout = xegpu::getDistributeLayoutAttr(use)) {
if (!layout)
layout = tmpLayout;
break;
}
}
return layout;
}
// Returns true if `op` is safe and cheap to clone (no side effects, no
// regions, and all operands are themselves trivially rematerializable, e.g.
// block-arg-free pure value generators such as `vector.step`, splat
// `arith.constant`, or `vector.create_mask` whose operands are constants).
bool xegpu::isTriviallyRematerializable(Operation *op) {
if (!op || op->getNumRegions() != 0)
return false;
if (!isMemoryEffectFree(op))
return false;
for (Value v : op->getOperands()) {
Operation *defOp = v.getDefiningOp();
if (!defOp)
return false;
if (!isTriviallyRematerializable(defOp))
return false;
}
return true;
}
// For regular operations: First the result layouts are propagated from uses.
// Then the result layouts are propagated to uses (operands).
static void propagateResultsToRegularOperands(Operation *op) {
if (op->getNumResults() == 0)
return;
if (op->getNumResults() > 1 && !isa<vector::DeinterleaveOp>(op))
return;
OpResult result = op->getResult(0);
xegpu::DistributeLayoutAttr resLayout = getLayoutFromUsePoints(result);
Type resultType = result.getType();
if (!resLayout)
return;
// Recover layout for TensorDesc type results by updating the type to include
// the layout. For vector type
if (isa<xegpu::TensorDescType>(resultType))
setTensorDescLayout(result, resLayout);
// Recover layout for vector type results, or for multi-reduction ops which
// may reduce to a scalar that still needs a layout.
if (isa<VectorType>(resultType) || isa<vector::MultiDimReductionOp>(op))
xegpu::setTemporaryLayout(result, resLayout);
if (isa<vector::DeinterleaveOp>(op))
xegpu::setTemporaryLayout(op->getResult(1), resLayout);
for (OpOperand &opr : op->getOpOperands()) {
xegpu::DistributeLayoutAttr operandLayout =
xegpu::inferSourceLayoutFromResultForNonAnchorOp(opr, resLayout);
if (isa<VectorType>(opr.get().getType()) && operandLayout)
xegpu::setTemporaryLayout(opr, operandLayout);
}
}
// Propagate layout from region op results and sibling region block args
// to yield/condition operands. For each successor of this terminator:
// - Parent successor: propagate from parent op's result layouts (use points).
// - Region successor: propagate from target region's block arg layouts (use
// points), e.g., scf.yield in "after/do" region propagates to "before/cond"
// block args.
static void propagateRegionResultsToYieldOperands(
mlir::RegionBranchTerminatorOpInterface yieldOp) {
auto regionBranchOp =
dyn_cast<RegionBranchOpInterface>(yieldOp->getParentOp());
if (!regionBranchOp)
return;
SmallVector<RegionSuccessor> successors;
SmallVector<Attribute> operandAttrs(yieldOp->getNumOperands(), nullptr);
yieldOp.getSuccessorRegions(operandAttrs, successors);
for (const RegionSuccessor &successor : successors) {
OperandRange succOps = yieldOp.getSuccessorOperands(successor);
if (succOps.empty())
continue;
unsigned beginIdx = succOps.getBeginOperandIndex();
ValueRange successorInputs = regionBranchOp.getSuccessorInputs(successor);
unsigned count = std::min<unsigned>(succOps.size(), successorInputs.size());
for (unsigned i = 0; i < count; ++i) {
xegpu::DistributeLayoutAttr layout;
if (successor.isOperation()) {
// For parent successor, get layout from external use points of the
// parent op's results.
auto regionResult = regionBranchOp->getResult(i);
layout = getLayoutFromUsePoints(regionResult);
if (layout) {
// set layout for the region op, like scf.loop
xegpu::setTemporaryLayout(regionResult, layout);
if (isa<xegpu::TensorDescType>(regionResult.getType()))
setTensorDescLayout(regionResult, layout);
}
} else {
// For region successor, get layout from the target region's block
// arg use points (e.g., "before/cond" region args for scf.while
// "after/do" yield).
layout = getLayoutFromUsePoints(successorInputs[i]);
}
if (!layout)
continue;
auto operandType = succOps[i].getType();
if (isa<VectorType>(operandType) ||
dyn_cast<xegpu::TensorDescType>(operandType))
// recover layout for yield op operands
xegpu::setTemporaryLayout(yieldOp->getOpOperand(beginIdx + i), layout);
}
}
}
/// Assign a layout to a region op's results (e.g. scf.for) using the layout of
/// the terminator operands that the region forwards to them. For each operand a
/// terminator (e.g. scf.yield) forwards to a successor input, if that input is
/// a region op result, the operand's layout is written onto the result.
/// clang-format off
/// Example: scf.for ... iter_args(...) -> (out types) {
/// ...
/// scf.yield ... : (yield types)
/// }
/// clang-format on
/// Having a layout on the region op result lets a later step attach a
/// convert_layout as a use to resolve the region op's no-use case.
/// Block-argument successors are left untouched.
LogicalResult xegpu::propagateYieldOperandsToRegionResults(
mlir::RegionBranchTerminatorOpInterface terminator,
xegpu::GetLayoutFnTy getLayoutOfValue) {
// Only process if the terminator is inside a region branch op.
auto branchOp = dyn_cast<RegionBranchOpInterface>(terminator->getParentOp());
if (!branchOp)
return success();
RegionBranchSuccessorMapping mapping;
branchOp.getSuccessorOperandInputMapping(mapping,
RegionBranchPoint(terminator));
for (const auto &[successorOperand, successorInputs] : mapping) {
for (Value successorInput : successorInputs) {
Type inputType = successorInput.getType();
// We only need to operate on vector types.
if (!isa<VectorType>(inputType))
continue;
xegpu::DistributeLayoutAttr successorOperandLayout =
getLayoutOfValue(successorOperand->get());
// The forwarded operand must carry a layout to propagate.
if (!successorOperandLayout)
return failure();
// Assign the yield operand's layout to the region op result it feeds.
if (auto result = dyn_cast<OpResult>(successorInput))
xegpu::setDistributeLayoutAttr(result, successorOperandLayout);
}
}
return success();
}
// Propagate layout from region arguments to region op's init operands. This
// sets the temporary layout for region arguments and init operands.
LogicalResult
xegpu::propagateRegionArgsToInits(mlir::RegionBranchOpInterface regionOp,
xegpu::GetLayoutFnTy getLayoutOfValue) {
// Iterate all regions of the region op. For each block argument that has a
// layout (obtained via `getLayoutOfValue`), trace back to find the
// corresponding init operand of the regionOp and set the layout on it.
// This works generically for scf.for, scf.while, and other
// RegionBranchOpInterface ops.
for (Region &region : regionOp->getRegions()) {
RegionSuccessor regionSuccessor(&region);
// Use getSuccessorInputs to get the block arguments that correspond to
// predecessor operands. This correctly handles ops like scf.for where
// the induction variable is a block arg but not a successor input.
ValueRange successorInputs = regionOp.getSuccessorInputs(regionSuccessor);
for (auto [inputIdx, regionArg] : llvm::enumerate(successorInputs)) {
auto layout = getLayoutOfValue(regionArg);
if (!layout)
continue;
// Recover layout for tensor_desc block args by updating the type.
if (isa<xegpu::TensorDescType>(regionArg.getType()))
setTensorDescLayout(regionArg, layout);
// Recover layout for region op operands, like scf.for's init operands.
// Find all predecessor values that flow into this block argument.
SmallVector<Value> predValues;
regionOp.getPredecessorValues(regionSuccessor, inputIdx, predValues);
for (Value predVal : predValues) {
// Match predecessor value to an operand of the regionOp.
for (OpOperand &operand : regionOp->getOpOperands()) {
if (operand.get() == predVal)
xegpu::setTemporaryLayout(operand, layout);
}
}
}
}
return success();
}
// Prerequisite for Layout Recovery
// It relies on the following invariant:
// 1. there is no layout conflict between different uses of the same definition.
// 2. each definition has a well-defined layout requirement at its use point.
// - Every definition must have at least one use that appears after it in
// topological order.
// - TODO: If a definition has no such use (e.g., a loop result or region
// output), an explicit convert_layout operation is inserted to create a
// use.
// - Only the result of convert_layout is permitted to have no subsequent
// use.
//
// The recovery proceeds by scanning the operation in reverse topological order
// as follows:
// For regular operations: First the result layouts are propagated from uses.
// Then the result layouts are propagated to operands.
//
// For region operations (e.g., loops):
// - When backward propagation reaches a region op, it sets the layout of
// the region op’s results according to use points like regular ops.
// - Then, the result layouts (such as a loop output) are propagated to
// their corresponding operands in the yield.
// - When backward propagation reaches the first operation inside the
// region, the pass examines the region op’s initialization list,
// propagating from region arguments to the corresponding initialization
// operands.
// - This ensures that layouts are consistently propagated
// across region boundaries while preserving a single well-defined use for
// each definition at the region-op level.
bool xegpu::recoverTemporaryLayouts(Operation *rootOp) {
auto processFunc = [&](Region &body, StringRef funcName) {
walkRegionBackward(body, [&](Operation *op) {
if (auto regionOp = dyn_cast<mlir::RegionBranchOpInterface>(op)) {
(void)xegpu::propagateRegionArgsToInits(regionOp,
getLayoutFromUsePoints);
} else if (auto yieldOp =
dyn_cast<mlir::RegionBranchTerminatorOpInterface>(op)) {
propagateRegionResultsToYieldOperands(yieldOp);
} else if (!dyn_cast<xegpu::AnchorLayoutInterface>(op)) {
propagateResultsToRegularOperands(op);
}
});
};
removeTemporaryLayoutAttrs(rootOp);
rootOp->walk([&](func::FuncOp func) {
processFunc(func.getBody(), func.getSymName());
});
rootOp->walk([&](gpu::GPUFuncOp func) {
processFunc(func.getBody(), func.getName());
});
return true;
}
template <typename T, typename>
void xegpu::removeLayoutAttr(const T &operandOrResult) {
Operation *owner = operandOrResult.getOwner();
std::string name = xegpu::getTemporaryLayoutName(operandOrResult);
if (owner->hasAttrOfType<DistributeLayoutAttr>(name))
owner->removeAttr(name);
}
// Explicit instantiation for OpResult
template void
xegpu::removeLayoutAttr<mlir::OpResult>(const mlir::OpResult &result);
// Explicit instantiation for OpOperand
template void
xegpu::removeLayoutAttr<mlir::OpOperand>(const mlir::OpOperand &operand);
void xegpu::removeLayoutAttrs(Operation *op) {
op->walk([&](Operation *nestOp) {
// Remove all attributes of DistributeLayoutAttr type
SmallVector<StringAttr> attrsToRemove;
for (auto namedAttr : nestOp->getAttrs()) {
if (isa<DistributeLayoutAttr>(namedAttr.getValue()))
attrsToRemove.push_back(namedAttr.getName());
}
for (auto attrName : attrsToRemove)
nestOp->removeAttr(attrName);
});
}
void xegpu::removeTemporaryLayoutAttrs(Operation *op) {
op->walk([&](Operation *nestOp) {
SmallVector<StringAttr> attrsToRemove;
for (auto namedAttr : nestOp->getDiscardableAttrs()) {
if (isa<xegpu::DistributeLayoutAttr>(namedAttr.getValue()))
attrsToRemove.push_back(namedAttr.getName());
}
for (auto attrName : attrsToRemove)
nestOp->removeDiscardableAttr(attrName);
});
}
/// Returns true if every dimension of `shape` except the innermost
/// `numInnerDims` is a unit (size-1) dimension.
[[maybe_unused]] static bool leadingDimsAreUnit(ArrayRef<int64_t> shape,
int numInnerDims) {
int numLeading = static_cast<int>(shape.size()) - numInnerDims;
if (numLeading <= 0)
return true;
return llvm::all_of(shape.take_front(numLeading),
[](int64_t dim) { return dim == 1; });
}
static xegpu::LayoutAttr buildInstDataLayoutWithLane(
mlir::MLIRContext *context, ArrayRef<int64_t> instData,
ArrayRef<int64_t> laneLayout, ArrayRef<int64_t> laneData,
DenseI32ArrayAttr orderAttr = nullptr) {
auto toI32Attr = [&](auto range) {
SmallVector<int32_t> v(range.begin(), range.end());
return DenseI32ArrayAttr::get(context, v);
};
return xegpu::LayoutAttr::get(context, /*sg_layout=*/nullptr,
/*sg_data=*/nullptr, toI32Attr(instData),
toI32Attr(laneLayout), toI32Attr(laneData),
orderAttr);
}
static bool isValidLaneLayout(ArrayRef<int64_t> dataShape,
ArrayRef<int64_t> laneLayout,
ArrayRef<int64_t> laneData) {
return !llvm::any_of(llvm::seq<int>(0, dataShape.size()), [&](int dim) {
return dataShape[dim] % (laneLayout[dim] * laneData[dim]) != 0;
});
}
static xegpu::LayoutAttr
buildLaneLayout(mlir::MLIRContext *context, ArrayRef<int64_t> laneLayout,
ArrayRef<int64_t> laneData,
DenseI32ArrayAttr orderAttr = nullptr) {
auto toI32Attr = [&](auto range) {
SmallVector<int32_t> v(range.begin(), range.end());
return DenseI32ArrayAttr::get(context, v);
};
return xegpu::LayoutAttr::get(context, /*sg_layout=*/nullptr,
/*sg_data=*/nullptr,
/*inst_data=*/nullptr, toI32Attr(laneLayout),
toI32Attr(laneData), orderAttr);
}
static xegpu::LayoutAttr
buildLayout(mlir::MLIRContext *context, ArrayRef<int64_t> sgLayout,
ArrayRef<int64_t> sgData, ArrayRef<int64_t> instData,
ArrayRef<int64_t> laneLayout, ArrayRef<int64_t> laneData,
DenseI32ArrayAttr orderAttr = nullptr) {
auto toI32Attr = [&](auto range) {
SmallVector<int32_t> v(range.begin(), range.end());
return DenseI32ArrayAttr::get(context, v);
};
return xegpu::LayoutAttr::get(
context, sgLayout.empty() ? nullptr : toI32Attr(sgLayout),
sgData.empty() ? nullptr : toI32Attr(sgData),
instData.empty() ? nullptr : toI32Attr(instData),
laneLayout.empty() ? nullptr : toI32Attr(laneLayout),
laneData.empty() ? nullptr : toI32Attr(laneData), orderAttr);
}
static xegpu::LayoutAttr buildSgLayout(mlir::MLIRContext *context,
ArrayRef<int64_t> wgTileShape,
ArrayRef<int64_t> sgLayout,
int dimK = -1,
DenseI32ArrayAttr orderAttr = nullptr) {
SmallVector<int64_t> sgData(sgLayout.size());
for (int dim = 0; dim < (int)sgLayout.size(); ++dim) {
if (dim == dimK)
sgData[dim] = wgTileShape[dim];
else
sgData[dim] = wgTileShape[dim] / sgLayout[dim];
}
return buildLayout(context, sgLayout, sgData,
/*inst_data=*/{}, /*lane_layout=*/{},
/*lane_data=*/{}, /*order=*/nullptr);
}
/// Infers the source layout attribute for a broadcast operation given the
/// result layout attribute, result shape, source shape.
xegpu::DistributeLayoutAttr
xegpu::inferBroadcastSourceLayout(xegpu::DistributeLayoutAttr resLayout,
ArrayRef<int64_t> resShape,
ArrayRef<int64_t> srcShape) {
SmallVector<int64_t> bcastDims;
size_t dimDiff = resShape.size() - srcShape.size();
auto bcastSourceLayout = resLayout;
// Right-aligned source in result, look for stretched unit dims.
for (size_t i = dimDiff; i < resShape.size(); i++) {
if ((srcShape[i - dimDiff] == 1) && (resShape[i] != 1))
bcastDims.push_back(i);
}
// Case UnitDimStretch (e.g., 1x4 -> 4x4): the source layout data field must
// be 1.
if (!bcastDims.empty())
bcastSourceLayout = bcastSourceLayout.setUnitDimData(bcastDims);
// Case RankDiff:
if (dimDiff) {
SmallVector<int64_t> sliceDims;
bool isOuterDimDiffUnitDims = llvm::all_of(
resShape.take_front(dimDiff), [&](int64_t dim) { return dim == 1; });
if (dimDiff && bcastDims.size() == dimDiff && isOuterDimDiffUnitDims) {
// Case RankDiffInnerDims (e.g., 1x4 -> 1x16x4):
// slice the expanded inner dims
sliceDims.assign(bcastDims.begin(), bcastDims.end());
} else {
// Case RankDiffOuterDims (e.g., 1x4 -> 1x1x4):
// slice the outer dims
llvm::append_range(sliceDims, llvm::seq<int64_t>(0, dimDiff));
}
bcastSourceLayout = xegpu::SliceAttr::get(
resLayout.getContext(), bcastSourceLayout,
DenseI64ArrayAttr::get(resLayout.getContext(), sliceDims));
}
return bcastSourceLayout;
}
/// Infers the source layout attribute for a reduction operation given the
/// result layout attribute and reduced dims.
xegpu::DistributeLayoutAttr
xegpu::inferMultiReductionSourceLayout(xegpu::DistributeLayoutAttr resLayout,
SmallVector<int64_t> reduceDims) {
assert(isa<xegpu::SliceAttr>(resLayout) &&
"reduction result layout must be slice layout");
xegpu::SliceAttr sliceLayout = dyn_cast<xegpu::SliceAttr>(resLayout);
assert((reduceDims == sliceLayout.getDims().asArrayRef()) &&
"reduction dims must match with slice dims");
return sliceLayout.getParent();
}
xegpu::DistributeLayoutAttr
xegpu::inferReductionSourceLayout(xegpu::DistributeLayoutAttr resLayout) {
return xegpu::inferMultiReductionSourceLayout(resLayout, {0});
}
/// Infers the source layout attribute for a transpose operation given the
/// result layout attribute and permutation.
///
/// vector.transpose semantics is `result[i] = source[permutation[i]]`, so
/// `result_layout[i] = source_layout[permutation[i]]`. To recover the source
/// layout from the result layout we must apply the inverse permutation.
xegpu::DistributeLayoutAttr
xegpu::inferTransposeSourceLayout(xegpu::DistributeLayoutAttr resLayout,
ArrayRef<int64_t> permutation) {
SmallVector<int64_t> inversePermutation =
invertPermutationVector(permutation);
return resLayout.transposeDims(inversePermutation);
}
/// Infers the source layout attribute for a bitcast operation given the
/// result layout attribute, result element type bitwidth, and source element
/// type bitwidth.
xegpu::DistributeLayoutAttr
xegpu::inferBitCastSourceLayout(xegpu::DistributeLayoutAttr resLayout,
int resElemTyBitWidth, int srcElemTyBitWidth) {
SmallVector<int64_t> sgData = resLayout.getEffectiveSgDataAsInt();
SmallVector<int64_t> instData = resLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> laneData = resLayout.getEffectiveLaneDataAsInt();
size_t sgDataSize = sgData.size();
size_t instDataSize = instData.size();
size_t laneDataSize = laneData.size();
int64_t sgDataValue = -1;
int64_t instDataValue = -1;
int64_t laneDataValue = -1;
int64_t dim = resLayout.getRank() - 1;
if (srcElemTyBitWidth <= resElemTyBitWidth) {
int bitWidthRatio = resElemTyBitWidth / srcElemTyBitWidth;
if (sgDataSize)
sgDataValue = sgData.back() * bitWidthRatio;
if (instDataSize)
instDataValue = instData.back() * bitWidthRatio;
if (laneDataSize)
laneDataValue = laneData.back() * bitWidthRatio;
} else {
int bitWidthRatio = srcElemTyBitWidth / resElemTyBitWidth;
if (sgDataSize) {
assert((sgData.back() % bitWidthRatio) == 0 &&
"sgData not divisible by bitWidthRatio");
sgDataValue = sgData.back() / bitWidthRatio;
}
if (instDataSize) {
assert((instData.back() % bitWidthRatio) == 0 &&
"instData not divisible by bitWidthRatio");
instDataValue = instData.back() / bitWidthRatio;
}
if (laneDataSize) {
assert((laneData.back() % bitWidthRatio) == 0 &&
"laneData not divisible by bitWidthRatio");
laneDataValue = laneData.back() / bitWidthRatio;
}
}
xegpu::DistributeLayoutAttr finalSrcLayout;
finalSrcLayout =
resLayout.setDimData(dim, sgDataValue, instDataValue, laneDataValue);
return finalSrcLayout;
}
/// Infers the source layout attribute for an interleave operation given the
/// result layout attribute. Interleave doubles the size of the innermost
/// dimension, so the layout inference is similar to bitcast where the source
/// element type is larger than the result element type (ratio = 2).
xegpu::DistributeLayoutAttr
xegpu::inferInterleaveSourceLayout(xegpu::DistributeLayoutAttr resLayout) {
SmallVector<int64_t> sgData = resLayout.getEffectiveSgDataAsInt();
SmallVector<int64_t> instData = resLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> laneData = resLayout.getEffectiveLaneDataAsInt();
size_t sgDataSize = sgData.size();
size_t instDataSize = instData.size();
size_t laneDataSize = laneData.size();
int64_t sgDataValue = -1;
int64_t instDataValue = -1;
int64_t laneDataValue = -1;
int64_t dim = resLayout.getRank() - 1;
// Interleave doubles the innermost dimension, so we need to halve the
// layout values (similar to bitcast with ratio = 2)
constexpr int ratio = 2;
if (sgDataSize) {
assert((sgData.back() % ratio) == 0 &&
"sgData not divisible by interleave ratio");
sgDataValue = sgData.back() / ratio;
}
if (instDataSize) {
assert((instData.back() % ratio) == 0 &&
"instData not divisible by interleave ratio");
instDataValue = instData.back() / ratio;
}
if (laneDataSize) {
assert((laneData.back() % ratio) == 0 &&
"laneData not divisible by interleave ratio");
laneDataValue = laneData.back() / ratio;
}
return resLayout.setDimData(dim, sgDataValue, instDataValue, laneDataValue);
}
/// Infers the source layout attribute for a deinterleave operation given the
/// result layout attribute. Deinterleave halves the size of the innermost
/// dimension, so the layout inference is similar to bitcast where the source
/// element type is smaller than the result element type (ratio = 2).
xegpu::DistributeLayoutAttr
xegpu::inferDeinterleaveSourceLayout(xegpu::DistributeLayoutAttr resLayout) {
SmallVector<int64_t> sgData = resLayout.getEffectiveSgDataAsInt();
SmallVector<int64_t> instData = resLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> laneData = resLayout.getEffectiveLaneDataAsInt();
size_t sgDataSize = sgData.size();
size_t instDataSize = instData.size();
size_t laneDataSize = laneData.size();
int64_t sgDataValue = -1;
int64_t instDataValue = -1;
int64_t laneDataValue = -1;
int64_t dim = resLayout.getRank() - 1;
// Deinterleave halves the innermost dimension, so we need to double the
// layout values (similar to bitcast with ratio = 2)
constexpr int ratio = 2;
if (sgDataSize)
sgDataValue = sgData.back() * ratio;
if (instDataSize)
instDataValue = instData.back() * ratio;
if (laneDataSize)
laneDataValue = laneData.back() * ratio;
return resLayout.setDimData(dim, sgDataValue, instDataValue, laneDataValue);
}
/// Infers the source layout attribute for an insert strided slice operation
/// given the result layout attribute, result shape, and source shape. Removes
/// leading dimensions from the result layout to match the source shape size.
xegpu::DistributeLayoutAttr xegpu::inferInsertStridedSliceSourceLayout(
xegpu::DistributeLayoutAttr resLayout, ArrayRef<int64_t> resShape,
ArrayRef<int64_t> srcShape) {
int srcShapeSize = srcShape.size();
int resShapeSize = resShape.size();
int dimDiff = resShapeSize - srcShapeSize;
if (dimDiff > 0) {
// assert that the leading dimensions being sliced off are not distributed
// (i.e. sg_layout and lane_layout for those dimensions are all 1)
auto resSgLayout = resLayout.getEffectiveSgLayoutAsInt();
auto resLaneLayout = resLayout.getEffectiveLaneLayoutAsInt();
for (int i = 0; i < dimDiff; i++) {
assert((resSgLayout.size() == 0 || resSgLayout[i] == 1) &&
(resLaneLayout.size() == 0 || resLaneLayout[i] == 1) &&
"Leading dimensions being sliced off must not be distributed");
}
return resLayout.dropDims(llvm::to_vector(llvm::seq<int64_t>(0, dimDiff)));
}
return resLayout;
}
/// Infers the source layout attribute for an insert operation
/// given the result layout attribute, result shape, and source shape. Removes
/// leading dimensions from the result layout to match the source shape size.
// TODO: add propagation support for insert op
xegpu::DistributeLayoutAttr
xegpu::inferInsertSourceLayout(xegpu::DistributeLayoutAttr resLayout,
ArrayRef<int64_t> resShape,
ArrayRef<int64_t> srcShape) {
int srcShapeSize = srcShape.size();
int resShapeSize = resShape.size();
int dimDiff = resShapeSize - srcShapeSize;
if (dimDiff > 0) {
// assert that the leading dimensions being sliced off are not distributed
// (i.e. sg_layout and lane_layout for those dimensions are all 1)
auto resSgLayout = resLayout.getEffectiveSgLayoutAsInt();
auto resLaneLayout = resLayout.getEffectiveLaneLayoutAsInt();
for (int i = 0; i < dimDiff; i++) {
assert((resSgLayout.size() == 0 || resSgLayout[i] == 1) &&
(resLaneLayout.size() == 0 || resLaneLayout[i] == 1) &&
"Leading dimensions being sliced off must not be distributed");
}
return resLayout.dropDims(llvm::to_vector(llvm::seq<int64_t>(0, dimDiff)));
}
return resLayout;
}
/// Infers the source layout attribute for extract operation
/// given the result layout attribute, result shape, and source shape. Adds
/// leading dimensions to the source layout to match the source shape size.
// TODO: add layout attribute interface: expandDim() and use it here.
// TODO: add propagation support for extract op
xegpu::DistributeLayoutAttr
xegpu::inferExtractSourceLayout(xegpu::DistributeLayoutAttr resLayout,
ArrayRef<int64_t> resShape,
ArrayRef<int64_t> srcShape) {
int srcShapeSize = srcShape.size();
int resShapeSize = resShape.size();
int dimDiff = srcShapeSize - resShapeSize;
auto context = resLayout.getContext();
// construct the source layout by adding unit dimensions to the front of
// result layout
if (dimDiff > 0) {
auto sgLayout = resLayout.getEffectiveSgLayoutAsInt();
auto sgData = resLayout.getEffectiveSgDataAsInt();
auto instData = resLayout.getEffectiveInstDataAsInt();
auto laneLayout = resLayout.getEffectiveLaneLayoutAsInt();
auto laneData = resLayout.getEffectiveLaneDataAsInt();
auto order = resLayout.getEffectiveOrderAsInt();
// Example: result shape is 3D with order [1, 2, 0], source shape is 5D
// (adding 2 leading dimensions). Expected source order: [3, 4, 2, 1, 0]
// Step 1: shift existing order by dimDiff: [1, 2, 0] -> [3, 4, 2]
// Step 2: append new leading dims in reverse (slowest first): [3, 4, 2, 1,
// 0]
// Shift existing dimension indices in order by dimDiff to account for the
// new leading dimensions being added to the source shape
for (auto &o : order)
o += dimDiff;
// Add unit dimensions to the front of non-empty layout vectors and append
// the new dimension indices to the order array in reverse (slowest
// dimension has the lowest index and appears last in the order array)
for (int i = 0; i < dimDiff; i++) {
if (!sgLayout.empty())
sgLayout.insert(sgLayout.begin(), 1);
if (!sgData.empty())
sgData.insert(sgData.begin(), 1);
if (!instData.empty())
instData.insert(instData.begin(), 1);
if (!laneLayout.empty())
laneLayout.insert(laneLayout.begin(), 1);
if (!laneData.empty())
laneData.insert(laneData.begin(), 1);
order.push_back(dimDiff - 1 - i);
}
DenseI32ArrayAttr orderAttr = DenseI32ArrayAttr::get(
context, SmallVector<int32_t>(order.begin(), order.end()));
if (!resLayout.getOrder())
orderAttr = nullptr;
return buildLayout(context, sgLayout, sgData, instData, laneLayout,
laneData, orderAttr);
}
return resLayout;
}
/// Infers the source layout attribute for a shape cast operation given the
/// result layout attribute, result shape, and source shape.
xegpu::DistributeLayoutAttr
xegpu::inferShapeCastSourceLayout(xegpu::DistributeLayoutAttr resLayout,
ArrayRef<int64_t> resShape,
ArrayRef<int64_t> srcShape) {
// There are three use cases:
// 1. expand dims of low-rank dimensions (e.g., 1D to 2D): to set up the
// tensor before broadcast
// 2. split dim of a high-rank dimension (e.g., 1D to 2D): to setup tensor
// for multi-stage reduction
// 3. combines all dims to a single dim and put in the innermost dim in 2d as
// [1, combinedData] or [combinedData]. Say, [2, 4, 8] -> [1, 64] or [64]
// Use cases are only supported after workgroup distribution,
// like cross-sg reduction saves multidimension data to
// 1D slm buffer, shapecast inserted by cse/canonicalization passes.
// Use case 1: Shapes only differ by expanding unit dimensions, for broadcast
SmallVector<int64_t> expandedUnitDims;
if (xegpu::matchUnitDimExpansion(srcShape, resShape, expandedUnitDims)) {
// create a slice layout for the source by removing the expanded unit dims
auto sliceDimsAttr = DenseI64ArrayAttr::get(
resLayout.getContext(), ArrayRef<int64_t>(expandedUnitDims));
auto srcLayout =
xegpu::SliceAttr::get(resLayout.getContext(), resLayout, sliceDimsAttr);
return srcLayout;
}
// Use case 2: Dim split from source to result, for multi-stage reduction
SmallVector<SmallVector<int64_t>> splitDimGroups;
if (xegpu::matchSplitDimExpansion(srcShape, resShape, splitDimGroups)) {
auto srcLayout = resLayout;
for (const auto &dimGroup : splitDimGroups)
srcLayout = srcLayout.collapseDims(dimGroup);
return srcLayout;
}
// Use case 3: General dim collapse, for cross-sg reduction to SLM and other
// shape casts where consecutive src dims fold into a single dst dim.
SmallVector<SmallVector<int64_t>> collapseDims;
if (xegpu::matchDimCollapse(srcShape, resShape, collapseDims)) {
auto srcLayout = resLayout;
for (int64_t dstIdx = static_cast<int64_t>(collapseDims.size()) - 1;
dstIdx >= 0; --dstIdx) {
ArrayRef<int64_t> srcDims = collapseDims[dstIdx];
if (srcDims.empty()) {
srcLayout = srcLayout.dropDims({dstIdx});
continue;
}
if (srcDims.size() == 1)
continue;
SmallVector<int64_t> targetShape;
targetShape.reserve(srcDims.size());
for (int64_t d : srcDims)
targetShape.push_back(srcShape[d]);
srcLayout = srcLayout.expandDim(dstIdx, targetShape);
}
return srcLayout;
}
return nullptr;
}
//===----------------------------------------------------------------------===//
// Forward layout inference (source layout -> result layout)
//===----------------------------------------------------------------------===//
/// Infers the result layout attribute for a transpose operation given the
/// source layout attribute and permutation.
///
/// vector.transpose semantics is `result[i] = source[permutation[i]]`, so
/// `result_layout[i] = source_layout[permutation[i]]`, which is exactly
/// `srcLayout.transposeDims(permutation)`. This is the inverse of
/// inferTransposeSourceLayout (which applies the inverse permutation).
xegpu::DistributeLayoutAttr
xegpu::inferTransposeResultLayout(xegpu::DistributeLayoutAttr srcLayout,
ArrayRef<int64_t> permutation) {
return srcLayout.transposeDims(permutation);
}
/// Infers the result layout attribute for a shape cast operation given the
/// source layout attribute, source shape, and result shape. This is the
/// inverse of inferShapeCastSourceLayout: a dim-split (src -> res) is undone by
/// collapsing the split groups, and a dim-collapse (src -> res) is undone by
/// expanding the collapsed groups. The unit-dim-expansion case is not inverted
/// here because recovering which result dims are the expanded unit dims would
/// require the SliceAttr the backward direction produces; such patterns return
/// nullptr (leaving the result un-laid-out).
xegpu::DistributeLayoutAttr
xegpu::inferShapeCastResultLayout(xegpu::DistributeLayoutAttr srcLayout,
ArrayRef<int64_t> srcShape,
ArrayRef<int64_t> resShape) {
// Case: source dims were split into result dims (forward of use case 2 in
// inferShapeCastSourceLayout). Undo by expanding each source dim into its
// group of result dims.
SmallVector<SmallVector<int64_t>> splitDimGroups;
if (xegpu::matchSplitDimExpansion(srcShape, resShape, splitDimGroups)) {
auto resLayout = srcLayout;
// Process source dims from innermost to outermost so that expanding a dim
// does not shift the indices of dims not yet processed.
for (int64_t srcIdx = static_cast<int64_t>(splitDimGroups.size()) - 1;
srcIdx >= 0; --srcIdx) {
ArrayRef<int64_t> resDims = splitDimGroups[srcIdx];
if (resDims.size() <= 1)
continue;
SmallVector<int64_t> targetShape;
targetShape.reserve(resDims.size());
for (int64_t d : resDims)
targetShape.push_back(resShape[d]);
resLayout = resLayout.expandDim(srcIdx, targetShape);
}
return resLayout;
}
// Case: source dims were collapsed into result dims (forward of use case 3).
// Undo by collapsing each group of source dims into its single result dim.
SmallVector<SmallVector<int64_t>> collapseDims;
if (xegpu::matchDimCollapse(srcShape, resShape, collapseDims)) {
auto resLayout = srcLayout;
// Process result dims from innermost to outermost so that collapsing a
// group does not shift the indices of groups not yet processed.
for (int64_t dstIdx = static_cast<int64_t>(collapseDims.size()) - 1;
dstIdx >= 0; --dstIdx) {
ArrayRef<int64_t> srcDims = collapseDims[dstIdx];
// A result dim with no backing source dims is a trailing/leading unit
// dim; its forward inference is ambiguous, so bail out.
if (srcDims.empty())
return nullptr;
if (srcDims.size() == 1)
continue;
resLayout = resLayout.collapseDims(llvm::to_vector(srcDims));
}
return resLayout;
}
return nullptr;
}
/// Infers the result layout attribute for a non-anchor operation from the
/// layouts of its source operands. Forward counterpart of
/// inferSourceLayoutFromResultForNonAnchorOp.
xegpu::DistributeLayoutAttr xegpu::inferResultLayoutFromSourceForNonAnchorOp(
Operation *op, ArrayRef<xegpu::DistributeLayoutAttr> operandLayouts) {
if (op->getNumResults() != 1)
return nullptr;
// For vector::TransposeOp, infer the result layout from the source layout.
if (auto transpose = dyn_cast<vector::TransposeOp>(op)) {
if (!operandLayouts[0])
return nullptr;
return xegpu::inferTransposeResultLayout(operandLayouts[0],
transpose.getPermutation());
}
// For vector::ShapeCastOp, infer the result layout from the source layout.
if (auto shapeCast = dyn_cast<vector::ShapeCastOp>(op)) {
if (!operandLayouts[0])
return nullptr;
return xegpu::inferShapeCastResultLayout(
operandLayouts[0], shapeCast.getSourceVectorType().getShape(),
shapeCast.getResultVectorType().getShape());
}
// For elementwise operations, all operands and the result share the same
// layout. Use the first operand that carries a layout.
if (OpTrait::hasElementwiseMappableTraits(op)) {
for (xegpu::DistributeLayoutAttr layout : operandLayouts)
if (layout)
return layout;
return nullptr;
}
// TODO: add forward inference rules for the remaining ops; their result is
// left un-laid-out until then.
// - vector::BroadcastOp: the forward direction is under-determined. The
// backward rule (inferBroadcastSourceLayout) either sets broadcast dims to
// unit data (losing the original data on those dims) or wraps the result
// in a SliceAttr; neither is generally invertible from the source layout
// alone, so a forward rule must decide how to distribute the new/stretched
// dims.
// - vector::BitCastOp, vector::MultiDimReductionOp / vector::ReductionOp,
// vector::InterleaveOp / vector::DeinterleaveOp, and the insert / extract
// / strided-slice family.
return nullptr;
}
/// Infers the layout attribute for mask and offset operand for Chunked load
/// and store, given the anchor layout attribute for the value being load/store.
xegpu::DistributeLayoutAttr xegpu::inferMaskOffsetLayoutForScatterIO(
xegpu::DistributeLayoutAttr payloadLayout, int chunkSize) {
auto rank = payloadLayout.getRank();
if (chunkSize > 1)
return payloadLayout.dropDims(
llvm::to_vector(llvm::seq<int64_t>(rank - 1, rank)));
return payloadLayout;
}
//===----------------------------------------------------------------------===//
// Layout derivation helpers: factorize sgCount into
// sg_layout candidates, then
// compute per-subgroup (sgData) and per-lane
// (lane_layout/lane_data/inst_data).
//===----------------------------------------------------------------------===//
using LayoutRepresentation = SmallVector<int64_t>;
/// Enumerates all ways to split `total` into `rank` factors whose product
/// equals `total`. Returns the list of all such factorizations.
static SmallVector<LayoutRepresentation> enumerateFactorizations(int64_t total,
int64_t rank) {
SmallVector<LayoutRepresentation> results;
SmallVector<int64_t> current(rank, 0);
// Returns all divisors of `n` in ascending order.
auto getDivisors = [](int64_t n) {
SmallVector<int64_t> divs;
for (int64_t i = 1; i * i <= n; ++i) {
if (n % i == 0) {
divs.push_back(i);
if (i != n / i)
divs.push_back(n / i);
}
}
llvm::sort(divs);
return divs;
};
std::function<void(int64_t, int64_t)> generate = [&](int64_t dim,
int64_t remaining) {
if (dim == rank - 1) {
current[dim] = remaining;
results.push_back(LayoutRepresentation(current));
return;
}
for (int64_t factor : getDivisors(remaining)) {
current[dim] = factor;
generate(dim + 1, remaining / factor);
}
};
generate(0, total);
return results;
}
// Computes all valid N-dimensional sg_layout candidates for the given
// sgCount, whose sgData (= wgShape / sgLayout):
// 1. Evenly divides wgShape (i.e., wgShape[d] % sgLayout[d] == 0).
// 2. Is a multiple of instData (i.e., sgData[d] % instData[d] == 0).
// Results are sorted by balance (smallest max-min spread first), with
// lexicographic order as a tiebreaker.
//
// `broadcastDim` (default -1 = none) marks a dimension broadcast across
// subgroups rather than distributed (e.g. the K/contraction dim of a DPAS
// operand). Its full extent stays in every subgroup, so rule 1 is skipped for
// it, but rule 2 (multiple of instData) still applies.
//
// Example (2D):
// wgShape = [128, 64], instData = [8, 16], sgCount = 32
// Returns: [[8,4], [16,2]], corresponding to sgData [16,16] and [8,32].
static SmallVector<LayoutRepresentation>
getSgLayoutCandidates(ArrayRef<int64_t> wgShape, ArrayRef<int64_t> instData,
int64_t sgCount, int64_t broadcastDim = -1) {
int64_t rank = wgShape.size();
assert(rank > 0 && "wgShape must be non-empty");
assert(static_cast<int64_t>(instData.size()) == rank &&
"instData rank must match wgShape rank");
// Step 1: Get all N-D factorizations of sgCount.
auto allFactorizations = enumerateFactorizations(sgCount, rank);
// Step 2: Filter to keep only valid candidates.
SmallVector<LayoutRepresentation> candidates;
for (const auto &sgLayout : allFactorizations) {
bool valid = true;
for (int64_t dim = 0; dim < rank; ++dim) {
// A broadcast dim keeps its full extent in every subgroup; others are
// split evenly by sgLayout[dim].
int64_t sgData;
if (dim == broadcastDim) {
sgData = wgShape[dim];
} else {
if (wgShape[dim] % sgLayout[dim] != 0) {
valid = false;
break;
}
sgData = wgShape[dim] / sgLayout[dim];
}
if (sgData % instData[dim] != 0) {
valid = false;
break;
}
}
if (valid)
candidates.push_back(sgLayout);
}
// Step 3: Sort by balance (smallest max-min spread), then lexicographic.
llvm::sort(candidates, [](const LayoutRepresentation &lhs,
const LayoutRepresentation &rhs) {
int64_t spreadLhs = *llvm::max_element(lhs) - *llvm::min_element(lhs);
int64_t spreadRhs = *llvm::max_element(rhs) - *llvm::min_element(rhs);
if (spreadLhs != spreadRhs)
return spreadLhs < spreadRhs;
return lhs < rhs;
});
return candidates;
}
/// Helper function to compute inst_data vectors for DPAS operands A, B, and
/// C/D.
static std::optional<SmallVector<int64_t>> get2DBlockIOInstDataLayout(
ArrayRef<int64_t> dataShape, Type elemTy,
const xegpu::uArch::BlockIOInstructionInterface *uArchInstruction,
bool transform = false, bool transpose = false) {
int rank = dataShape.size();
auto blockWHC =
uArchInstruction->getBlockWidthHeightCount(elemTy, transform, transpose);
if (!blockWHC)
return std::nullopt;
auto [bWidths, bHeights, bCounts] = blockWHC.value();
// Compute inst_data from hardware block params. For Nd ops, the lane
// factorization above (laneLayout / laneData) is rigid; inst_data must be
// a multiple of lane_layout * lane_data on each dim (Category A
// invariant).
SmallVector<int64_t> instData(rank, 1);
assert(rank >= 2 && "dataShape must be at least 2D for 2D-block IO");
int instWidth =
xegpu::getLargestDivisor(static_cast<int>(dataShape.back()), bWidths);
int instHeight =
xegpu::getLargestDivisor(static_cast<int>(dataShape[rank - 2]), bHeights);
// No supported hardware block size divides the data dim (e.g. innermost dim
// of 1 vs. minimum block width 16): not realizable as a 2D-block instruction.
if (instWidth < 0 || instHeight < 0)
return std::nullopt;
instData.back() = instWidth;
instData[rank - 2] = instHeight;
return instData;
}
/// Helper function to compute inst_data vectors for DPAS operands A, B, and
/// C/D. Look up the uArch table and search for the largest supported block size
/// that divides the data shape
static std::optional<std::tuple<SmallVector<int64_t>, SmallVector<int64_t>,
SmallVector<int64_t>>>
getDpasInstDataLayouts(
VectorType aTy, VectorType bTy, VectorType cdTy,
const xegpu::uArch::MMAInstructionInterface *uArchInstruction) {
// M dimension is the second-to-last dim of A (handles batch dims).
const unsigned dataALen = aTy.getShape()[aTy.getRank() - 2];
auto supportedALen = uArchInstruction->getSupportedM(aTy.getElementType());
const int maxALen =
xegpu::getLargestDivisor(dataALen, ArrayRef<unsigned>(supportedALen));
// N dimension is the last dim of B.
const unsigned dataBLen = bTy.getShape().back();
auto supportedBLen = uArchInstruction->getSupportedN(bTy.getElementType());
const int maxBLen =
xegpu::getLargestDivisor(dataBLen, ArrayRef<unsigned>(supportedBLen));
auto supportedCLen = uArchInstruction->getSupportedN(cdTy.getElementType());
const int maxCLen =
xegpu::getLargestDivisor(dataBLen, ArrayRef<unsigned>(supportedCLen));
if (maxALen == -1 || maxBLen == -1 || maxCLen == -1)
return std::nullopt;
auto supportedKLen = uArchInstruction->getSupportedK(aTy.getElementType());
if (supportedKLen.empty())
return std::nullopt;
auto kDimSize = supportedKLen[0];
SmallVector<int64_t> instDataA(aTy.getRank(), 1);
instDataA[aTy.getRank() - 2] = maxALen;
instDataA[aTy.getRank() - 1] = kDimSize;
SmallVector<int64_t> instDataB(bTy.getRank(), 1);
instDataB[bTy.getRank() - 2] = kDimSize;
instDataB[bTy.getRank() - 1] = maxBLen;
SmallVector<int64_t> instDataCD(cdTy.getRank(), 1);
instDataCD[cdTy.getRank() - 2] = maxALen;
instDataCD[cdTy.getRank() - 1] = maxCLen;
return std::make_tuple(instDataA, instDataB, instDataCD);
}
/// Computes lane_layout and lane_data for scatter-style store anchor layouts
/// (store scatter, store matrix). Lanes and the per-lane vector both live on
/// the innermost dim:
/// - laneLayout[innermost] = min(subgroupSize, srcShape[innermost])
/// - laneData[innermost] = min(srcShape[innermost] / laneLayout[innermost],
/// maxChunkSize)
/// All other entries are 1.
static std::pair<SmallVector<int64_t>, SmallVector<int64_t>>
computeScatterIOLaneLayoutAndData(ArrayRef<int64_t> instShape,
int64_t subgroupSize, int64_t maxChunkSize) {
int64_t rank = instShape.size();
SmallVector<int64_t> laneLayout(rank, 1), laneData(rank, 1);
int64_t innermost = rank - 1;
laneLayout[innermost] = std::min(subgroupSize, instShape[innermost]);
laneData[innermost] =
std::min(instShape[innermost] / laneLayout[innermost], maxChunkSize);
return {laneLayout, laneData};
}
// Computes the per-lane layout and data for a 2D block load/store/prefetch:
// lanes are spread across the subgroup along the last dim (or rank-2 if
// transposed), and laneData packs sub-bitwidth elements along the packing dim.
static std::pair<SmallVector<int64_t>, SmallVector<int64_t>>
compute2DBlockIOLaneLayoutAndData(ArrayRef<int64_t> instShape,
int64_t subgroupSize, int64_t bitwidth,
int64_t packingSize, bool transform = false) {
int64_t rank = instShape.size();
SmallVector<int64_t> laneLayout(rank, 1), laneData(rank, 1);
int kDim = transform ? rank - 2 : rank - 1;
unsigned vnniFactor = packingSize / bitwidth;
laneData[kDim] = bitwidth < packingSize ? vnniFactor : 1;
laneLayout.back() =
std::min(subgroupSize, instShape.back() / laneData.back());
// assert that the lane layout and data fit in the inst shape
for (int64_t i = 0; i < rank; ++i) {
int64_t laneProduct = laneLayout[i] * laneData[i];
assert(instShape[i] % laneProduct == 0 &&
"lane_layout * lane_data must evenly divide the inst shape");
(void)laneProduct;
}
return {laneLayout, laneData};
}
/// Computes the (lane_layout, lane_data) for a multi-reduction's source layout.
/// Only the innermost two dims are distributed; leading dims are assumed unit.
/// `subgroupSize` lanes go on one dim; up to `maxReduceVectorSize` elements are
/// packed into lane_data on the other. To minimize cross-lane reduction, lanes
/// are spread across a non-reduction dim when possible so the reduction happens
/// within a lane. inst_data is the element-wise product lane_layout *
/// lane_data.
///
/// e.g. with srcShape=[32, 128], subgroupSize=16, maxReduceVectorSize=2:
/// - Switch: reductionDims=[1] and consumerReductionDims=[] -> lanes move
/// to the non-reduction dim 0: lane_layout=[16, 1], lane_data=[1, 2].
/// - Default: reductionDims=[0, 1] (both reduced) -> lanes stay on the
/// innermost dim: lane_layout=[1, 16], lane_data=[2, 1].
static std::pair<SmallVector<int64_t>, SmallVector<int64_t>>
computeReductionLaneLayoutAndData(ArrayRef<int64_t> srcShape,
ArrayRef<int64_t> reductionDims,
int subgroupSize, int64_t maxReduceVectorSize,
bool verticalLaneLayout = false) {
int srcRank = srcShape.size();
SmallVector<int64_t> laneLayout(srcRank, 1), laneData(srcRank, 1);
int innermost = srcRank - 1;
int secondInnermost = srcRank - 2;
if (verticalLaneLayout && secondInnermost >= 0) {
std::swap(innermost, secondInnermost);
}
int laneDim = innermost;
int vectorDim = secondInnermost; // negative for rank 1
laneLayout[laneDim] =
std::min(static_cast<int64_t>(subgroupSize), srcShape[laneDim]);
if (vectorDim >= 0)
laneData[vectorDim] = std::min(maxReduceVectorSize, srcShape[vectorDim]);
return {laneLayout, laneData};
}
//===----------------------------------------------------------------------===//
// Result/anchor-layout setup. Each op category derives lane_layout/lane_data
// (and inst_data / sgData) differently. Two things vary across ops:
//
// * Consumer dependence: consumer-driven ops prefer the layout requested by
// their downstream uses and fall back to uArch defaults only when it is
// absent/invalid; sinks (StoreNd, PrefetchNd) have no consumer and always
// pick their own layout from uArch.
//
// * Derivation direction between inst_data and lane_layout/lane_data. Both
// obey the invariant inst_data = k * lane_layout * lane_data, where `k` is
// a per-dim integer >= 1 giving how many times each lane repeats its
// access to cover one instruction's data tile (k == 1 means one lane
// position per element; k > 1 means the instruction loads/stores several
// elements per lane along that dim). Ops solve this invariant from
// opposite ends:
// - Rigid-lane ops (Nd block IO, DPAS): hardware fixes lane_layout /
// lane_data first, then inst_data is built as a multiple of their
// product (using get2DBlockIOInstDataLayout / getDpasInstDataLayouts).
// - inst_data-first ops (scatter load): take inst_data from the consumer
// and derive lane_layout/lane_data underneath it.
//
// - DPAS (+DPAS_MX) : rigid lanes — inst_data from HW block dims; A/B/C/D
// lanes/data follow each operand's matmul role; DPAS_MX
// additionally lays out the scale operand.
// - LoadNd : consumer-driven, rigid lanes — honors the consumer's
// inst_data / lane / sg_layout (incl. transpose & VNNI
// packing) when it satisfies uArch block constraints,
// else falls back to the default 2D-block scheme (lanes
// on the last dim, rank-2 if transposed). The fallback
// picks the LARGEST uArch block that divides the data
// shape, so the resulting inst_data block can be bigger
// than what the consumer asked for (fewer, wider
// loads).
// - StoreNd/PrefetchNd: data sinks, no consumer, rigid lanes — pick the
// 2D-block layout directly from uArch (no VNNI
// packing).
// - Load (scatter) : load_gather / load_matrix, consumer-driven,
// inst_data-first — reuse the consumer's inst_data and
// derive lane_layout/lane_data, else default to lanes +
// per-lane chunk on the innermost dim (chunk capped by
// maxChunkSize).
// - Store (scatter) : store_scatter / store_matrix — same scatter scheme,
// but always self-derived from the scatter default.
// - Reduction : (multi_)reduction, consumer-driven — distribute the
// inner two dims, with lanes on the innermost dim by
// default (reducing across lanes) and switched to a
// non-reduction dim only when that keeps the reduction
// within a lane. Reuses the consumer's slice layout
// when it slices exactly the reduction dims, otherwise
// re-derives. See setupMultiReductionResultLayout for
// the exact switch condition and worked examples.
// - BitCast/Interleave: scale the innermost data field by the bitwidth /
// interleave ratio so the source layout divides back
// out.
// - InsertStridedSlice: clamp lane_data per dim to fit the inserted slice
// (Lane kind only; sg/inst layouts unsupported).
//===----------------------------------------------------------------------===//
/// Helper function to set up subgroup layouts for DPAS operands A, B, and
/// C/D. Compute subgroup layout candidates based on wgtile and instData, and
/// then pick the best one that satisfies all operands and the consumer (if
/// specified).
static std::optional<
std::tuple<xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr>>
getDpasSubgroupLayouts(
mlir::MLIRContext *context, VectorType aTy, VectorType bTy, VectorType cdTy,
xegpu::DistributeLayoutAttr consumerLayout, int numSg,
std::tuple<SmallVector<int64_t>, SmallVector<int64_t>, SmallVector<int64_t>>
instDataVecs) {
auto [instDataA, instDataB, instDataCD] = instDataVecs;
std::optional<LayoutRepresentation> consumerSgLayout = std::nullopt;
if (consumerLayout && consumerLayout.isForWorkgroup()) {
consumerSgLayout = consumerLayout.getEffectiveSgLayoutAsInt();
}
// Get all valid layouts for A, B and C/D operands
auto layoutsA = getSgLayoutCandidates(aTy.getShape(), instDataA, numSg,
/*broadcastDim=*/aTy.getRank() - 1);
auto layoutsB = getSgLayoutCandidates(bTy.getShape(), instDataB, numSg,
/*broadcastDim=*/bTy.getRank() - 2);
auto layoutsCD = getSgLayoutCandidates(cdTy.getShape(), instDataCD, numSg);
if (layoutsA.empty() || layoutsB.empty() || layoutsCD.empty())
return std::nullopt;
// Pick the best subgroup layout
std::optional<LayoutRepresentation> bestPick;
for (auto &sgLayout : layoutsB) {
if (llvm::is_contained(layoutsA, sgLayout) &&
llvm::is_contained(layoutsCD, sgLayout)) {
// Is in (A and B and CD) and matches consumer -> best pick
if (consumerSgLayout.has_value() && sgLayout == *consumerSgLayout) {
bestPick = sgLayout;
break;
}
// Is in (A and B and CD) layoutsB is ordered from most
// balanced to least. So the first one we see is the most balanced one,
// remember it and later only update if there is one that matches the
// consumer.
if (!bestPick)
bestPick = sgLayout;
}
}
if (!bestPick)
return std::nullopt;
const auto &picked = *bestPick;
auto dpasALayout = buildSgLayout(context, aTy.getShape(), picked,
/*dimK=*/aTy.getRank() - 1);
auto dpasBLayout = buildSgLayout(context, bTy.getShape(), picked,
/*dimK=*/bTy.getRank() - 2);
auto dpasCDLayout = buildSgLayout(context, cdTy.getShape(), picked);
return std::make_tuple(dpasALayout, dpasBLayout, dpasCDLayout);
}
/// Sets up the anchor layouts for dpas operands (A, B, and C/D).
/// The numSg and consumerLayout (optional) are only used by sg layout
/// creation.
std::optional<
std::tuple<xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr>>
xegpu::setupDpasLayout(xegpu::LayoutKind layoutKind, VectorType aTy,
VectorType bTy, VectorType cdTy,
xegpu::DistributeLayoutAttr consumerLayout, int numSg,
const xegpu::uArch::uArch *uArch) {
auto context = aTy.getContext();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::SubgroupMatrixMultiplyAcc>(uArch->getInstruction(
xegpu::uArch::InstructionKind::SubgroupMatrixMultiplyAcc));
if (!uArchInstruction)
return std::nullopt;
auto subgroupSize = uArch->getSubgroupSize();
auto [laneLayoutA, laneDataA] = compute2DBlockIOLaneLayoutAndData(
aTy.getShape(), subgroupSize,
aTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeA());
auto [laneLayoutB, laneDataB] = compute2DBlockIOLaneLayoutAndData(
bTy.getShape(), subgroupSize,
bTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeB(), /*vnni=*/true);
auto [laneLayoutCD, laneDataCD] = compute2DBlockIOLaneLayoutAndData(
cdTy.getShape(), subgroupSize,
cdTy.getElementType().getIntOrFloatBitWidth(),
cdTy.getElementType().getIntOrFloatBitWidth());
auto instDataVecs = getDpasInstDataLayouts(aTy, bTy, cdTy, uArchInstruction);
if (!instDataVecs)
return std::nullopt;
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(numSg > 0 &&
"Number of subgroups must be provided for sg layout creation.");
return getDpasSubgroupLayouts(context, aTy, bTy, cdTy, consumerLayout,
numSg, *instDataVecs);
} else if (layoutKind == xegpu::LayoutKind::InstData) {
auto [instDataA, instDataB, instDataCD] = *instDataVecs;
return std::make_tuple(
buildInstDataLayoutWithLane(context, instDataA, laneLayoutA, laneDataA),
buildInstDataLayoutWithLane(context, instDataB, laneLayoutB, laneDataB),
buildInstDataLayoutWithLane(context, instDataCD, laneLayoutCD,
laneDataCD));
} else if (layoutKind == xegpu::LayoutKind::Lane) {
auto aLayout = buildLaneLayout(context, laneLayoutA, laneDataA);
auto bLayout = buildLaneLayout(context, laneLayoutB, laneDataB);
auto cdLayout = buildLaneLayout(context, laneLayoutCD, laneDataCD);
return std::make_tuple(aLayout, bLayout, cdLayout);
}
return std::nullopt;
}
/// Helper to create a scale layout derived from a matrix operand layout.
/// The scale layout is computed by mapping each dimension of the matrix
/// layout to the corresponding scale tensor dimension using the ratio
/// between the matrix and scale shapes.
static xegpu::DistributeLayoutAttr
createScaleLayout(mlir::MLIRContext *context, VectorType matrixTy,
VectorType scaleTy, xegpu::DistributeLayoutAttr matrixLayout,
bool isBScale, const xegpu::uArch::uArch *uArch) {
if (!scaleTy || !matrixLayout)
return nullptr;
// Calculate scaling factor by dividing matrix shape by scale shape
ArrayRef<int64_t> matrixShape = matrixTy.getShape();
ArrayRef<int64_t> scaleShape = scaleTy.getShape();
// Scale shapes can be 1D or 2D, handle both cases
if (scaleShape.empty())
return nullptr;
auto uArchInstruction =
dyn_cast<xegpu::uArch::SubgroupScaledMatrixMultiplyAcc>(
uArch->getInstruction(
xegpu::uArch::InstructionKind::SubgroupScaledMatrixMultiplyAcc));
int64_t rank = matrixLayout.getRank();
assert(rank >= 2 && "dpas layouts must be at least two dimensions");
SmallVector<int64_t> sgLayout = matrixLayout.getEffectiveSgLayoutAsInt();
SmallVector<int64_t> sgData = matrixLayout.getEffectiveSgDataAsInt();
SmallVector<int64_t> instData = matrixLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> laneLayout = matrixLayout.getEffectiveLaneLayoutAsInt();
SmallVector<int64_t> laneData = matrixLayout.getEffectiveLaneDataAsInt();
auto order = matrixLayout.getOrder();
SmallVector<int64_t> scaleSgLayout;
SmallVector<int64_t> scaleSgData;
if (!sgLayout.empty() && !sgData.empty()) {
scaleSgLayout.assign(sgLayout.begin(), sgLayout.end());
scaleSgData.assign(sgData.begin(), sgData.end());
scaleSgData[rank - 2] = std::max<int64_t>(
scaleShape[rank - 2] / (matrixShape[rank - 2] / sgData[rank - 2]), 1);
scaleSgData[rank - 1] = std::max<int64_t>(
scaleShape[rank - 1] / (matrixShape[rank - 1] / sgData[rank - 1]), 1);
}
// For DPAS_MX scales: if matrix has inst_data, scale needs adjusted
// inst_data. Scale inst_data is derived from matrix inst_data divided by
// scale factor.
SmallVector<int64_t> scaleInstData;
if (!instData.empty()) {
scaleInstData.assign(instData.begin(), instData.end());
if (isBScale)
scaleInstData[rank - 2] = std::max<int64_t>(
scaleShape[rank - 2] / (matrixShape[rank - 2] / instData[rank - 2]),
1);
else
scaleInstData[rank - 1] = std::max<int64_t>(
scaleShape[rank - 1] / (matrixShape[rank - 1] / instData[rank - 1]),
1);
}
SmallVector<int64_t> scaleLaneLayout;
SmallVector<int64_t> scaleLaneData;
if (!laneLayout.empty() && !laneData.empty()) {
scaleLaneLayout.assign(laneLayout.begin(), laneLayout.end());
scaleLaneData.assign(laneData.size(), 1);
bool isRowMajor = uArchInstruction->isLaneLayoutRowMajorOrder();
if (isBScale ^ isRowMajor)
std::swap(scaleLaneLayout[rank - 2], scaleLaneLayout[rank - 1]);
// Cap lane_layout by the per-instruction tile (inst_data) on each dim.
// Then derive lane_data = inst_data / lane_layout so the Category A
// invariant inst_data = lane_layout * lane_data * k (with k = 1) holds
// for the scale operand's load_nd consumer.
auto layoutCap = scaleInstData.empty() ? scaleShape : scaleInstData;
for (int64_t d = rank - 2; d < rank; ++d)
scaleLaneLayout[d] = std::min<int64_t>(layoutCap[d], scaleLaneLayout[d]);
}
return buildLayout(context, scaleSgLayout, scaleSgData, scaleInstData,
scaleLaneLayout, scaleLaneData, order);
}
/// Sets up the anchor layouts for dpas_mx operands (A, B, C/D, A_scale, and
/// B_scale). The numSg and consumerLayout (optional) are only used by sg
/// layout creation.
std::optional<
std::tuple<xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr>>
xegpu::setupDpasMxLayout(xegpu::LayoutKind layoutKind, VectorType aTy,
VectorType bTy, VectorType cdTy, VectorType aScaleTy,
VectorType bScaleTy,
xegpu::DistributeLayoutAttr consumerLayout, int numSg,
const xegpu::uArch::uArch *uArch) {
auto context = aTy.getContext();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::SubgroupMatrixMultiplyAcc>(uArch->getInstruction(
xegpu::uArch::InstructionKind::SubgroupMatrixMultiplyAcc));
if (!uArchInstruction)
return std::nullopt;
auto subgroupSize = uArch->getSubgroupSize();
auto [laneLayoutA, laneDataA] = compute2DBlockIOLaneLayoutAndData(
aTy.getShape(), subgroupSize,
aTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeA());
auto [laneLayoutB, laneDataB] = compute2DBlockIOLaneLayoutAndData(
bTy.getShape(), subgroupSize,
bTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeB(), /*vnni=*/true);
auto [laneLayoutCD, laneDataCD] = compute2DBlockIOLaneLayoutAndData(
cdTy.getShape(), subgroupSize,
cdTy.getElementType().getIntOrFloatBitWidth(),
cdTy.getElementType().getIntOrFloatBitWidth());
auto instDataVecs = getDpasInstDataLayouts(aTy, bTy, cdTy, uArchInstruction);
if (!instDataVecs)
return std::nullopt;
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(numSg > 0 &&
"Number of subgroups must be provided for sg layout creation.");
auto dpasLayouts = getDpasSubgroupLayouts(
context, aTy, bTy, cdTy, consumerLayout, numSg, *instDataVecs);
if (!dpasLayouts)
return std::nullopt;
auto [dpasALayout, dpasBLayout, dpasCDLayout] = *dpasLayouts;
// Create scale layouts
auto aScaleLayout =
createScaleLayout(context, aTy, aScaleTy, dpasALayout, false, uArch);
auto bScaleLayout =
createScaleLayout(context, bTy, bScaleTy, dpasBLayout, true, uArch);
return std::make_tuple(dpasALayout, dpasBLayout, dpasCDLayout, aScaleLayout,
bScaleLayout);
} else if (layoutKind == xegpu::LayoutKind::InstData) {
auto [instDataA, instDataB, instDataCD] = *instDataVecs;
auto dpasALayout =
buildInstDataLayoutWithLane(context, instDataA, laneLayoutA, laneDataA);
auto dpasBLayout =
buildInstDataLayoutWithLane(context, instDataB, laneLayoutB, laneDataB);
auto dpasCDLayout = buildInstDataLayoutWithLane(context, instDataCD,
laneLayoutCD, laneDataCD);
auto aScaleLayout =
createScaleLayout(context, aTy, aScaleTy, dpasALayout, false, uArch);
auto bScaleLayout =
createScaleLayout(context, bTy, bScaleTy, dpasBLayout, true, uArch);
return std::make_tuple(dpasALayout, dpasBLayout, dpasCDLayout, aScaleLayout,
bScaleLayout);
} else if (layoutKind == xegpu::LayoutKind::Lane) {
auto dpasALayout = buildLaneLayout(context, laneLayoutA, laneDataA);
auto dpasBLayout = buildLaneLayout(context, laneLayoutB, laneDataB);
auto dpasCDLayout = buildLaneLayout(context, laneLayoutCD, laneDataCD);
auto aScaleLayout =
createScaleLayout(context, aTy, aScaleTy, dpasALayout, false, uArch);
auto bScaleLayout =
createScaleLayout(context, bTy, bScaleTy, dpasBLayout, true, uArch);
return std::make_tuple(dpasALayout, dpasBLayout, dpasCDLayout, aScaleLayout,
bScaleLayout);
}
return std::nullopt;
}
/// Sets up the anchor layout for a store_nd operation. StoreNd picks its
/// own layout based on uArch block parameters (it does not take a consumer
/// layout, since it is a data sink).
xegpu::DistributeLayoutAttr
xegpu::setupStoreNdAnchorLayout(xegpu::LayoutKind layoutKind,
VectorType srcVecTy, int numSg,
const xegpu::uArch::uArch *uArch) {
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::Subgroup2DBlockStoreInstruction>(
uArch->getInstruction(
xegpu::uArch::InstructionKind::Subgroup2DBlockStore));
if (!uArchInstruction)
return nullptr;
auto context = srcVecTy.getContext();
Type elemTy = srcVecTy.getElementType();
auto subgroupSize = uArch->getSubgroupSize();
auto dataShape = srcVecTy.getShape();
[[maybe_unused]] int rank = srcVecTy.getRank();
assert(rank >= 2 && "Expected at least 2D shape for ND op");
// Compute the default 2D block IO lane layout / lane data.
unsigned bitwidth = elemTy.getIntOrFloatBitWidth();
auto [laneLayout, laneData] = compute2DBlockIOLaneLayoutAndData(
dataShape, subgroupSize, bitwidth,
uArchInstruction->getPackedFormatBitSize());
if (layoutKind == xegpu::LayoutKind::Lane)
return buildLaneLayout(context, laneLayout, laneData);
auto instData =
get2DBlockIOInstDataLayout(dataShape, elemTy, uArchInstruction);
// Shape not realizable as a 2D-block instruction; let the caller report it.
if (!instData)
return nullptr;
if (layoutKind == xegpu::LayoutKind::InstData) {
assert(isValidLaneLayout(*instData, laneLayout, laneData) &&
"Expected the store layout to satisfy uArch block constraints");
return buildInstDataLayoutWithLane(context, *instData, laneLayout,
laneData);
}
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(numSg > 0 &&
"Number of subgroups must be provided for sg layout creation.");
auto sgLayouts = getSgLayoutCandidates(dataShape, *instData, numSg);
if (sgLayouts.empty())
return nullptr;
return buildSgLayout(context, dataShape, sgLayouts.front(), /*dimK=*/-1);
}
return nullptr;
}
/// Sets up the anchor layout for a prefetch_nd operation. PrefetchNd has no
/// consumer (it produces no value), so it picks its own layout from uArch
/// block parameters.
xegpu::DistributeLayoutAttr
xegpu::setupPrefetchNdAnchorLayout(xegpu::LayoutKind layoutKind,
xegpu::TensorDescType tdescTy, int numSg,
const xegpu::uArch::uArch *uArch) {
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::Subgroup2DBlockPrefetchInstruction>(
uArch->getInstruction(
xegpu::uArch::InstructionKind::Subgroup2DBlockPrefetch));
if (!uArchInstruction)
return nullptr;
auto context = tdescTy.getContext();
Type elemTy = tdescTy.getElementType();
auto subgroupSize = uArch->getSubgroupSize();
auto dataShape = tdescTy.getShape();
[[maybe_unused]] int rank = tdescTy.getRank();
assert(rank >= 2 && "Expected at least 2D shape for ND op");
// Compute the default 2D block IO lane layout / lane data.
unsigned bitwidth = elemTy.getIntOrFloatBitWidth();
auto [laneLayout, laneData] = compute2DBlockIOLaneLayoutAndData(
dataShape, subgroupSize, bitwidth,
uArchInstruction->getPackedFormatBitSize());
if (layoutKind == xegpu::LayoutKind::Lane)
return buildLaneLayout(context, laneLayout, laneData);
auto instData =
get2DBlockIOInstDataLayout(dataShape, elemTy, uArchInstruction);
// Shape not realizable as a 2D-block instruction; let the caller report it.
if (!instData)
return nullptr;
if (layoutKind == xegpu::LayoutKind::InstData) {
assert(isValidLaneLayout(*instData, laneLayout, laneData) &&
"Expected the prefetch layout to satisfy uArch block constraints");
return buildInstDataLayoutWithLane(context, *instData, laneLayout,
laneData);
}
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(numSg > 0 &&
"Number of subgroups must be provided for sg layout creation.");
auto sgLayouts = getSgLayoutCandidates(dataShape, *instData, numSg);
if (sgLayouts.empty())
return nullptr;
return buildSgLayout(context, dataShape, sgLayouts.front(), /*dimK=*/-1);
}
return nullptr;
}
/// Sets up the anchor layout for a load_nd operation. LoadNd takes a
/// consumer layout (from its result's downstream uses) and validates it
/// against uArch constraints; if valid, the consumer's `inst_data` /
/// `sg_layout` are honored. Otherwise the helper falls back to defaults
/// derived from uArch block parameters.
xegpu::DistributeLayoutAttr
xegpu::setupLoadNdAnchorLayout(xegpu::LayoutKind layoutKind,
VectorType resVecTy,
xegpu::DistributeLayoutAttr consumerLayout,
int numSg, const xegpu::uArch::uArch *uArch) {
assert(consumerLayout && "Expected a valid consumer layout");
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(consumerLayout.isForWorkgroup() &&
"Expected consumer layout to be a complete workgroup-level layout");
return consumerLayout;
}
auto context = resVecTy.getContext();
Type elemTy = resVecTy.getElementType();
auto subgroupSize = uArch->getSubgroupSize();
auto dataShape = resVecTy.getShape();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::Subgroup2DBlockLoadInstruction>(
uArch->getInstruction(
xegpu::uArch::InstructionKind::Subgroup2DBlockLoad));
if (!uArchInstruction)
return nullptr;
int rank = resVecTy.getRank();
SmallVector<int64_t> consumerInstData =
consumerLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> consumerLaneLayout =
consumerLayout.getEffectiveLaneLayoutAsInt();
SmallVector<int64_t> consumerLaneData =
consumerLayout.getEffectiveLaneDataAsInt();
auto consumerOrderAttr = consumerLayout.getOrder();
assert(!consumerLaneLayout.empty() && !consumerLaneData.empty() &&
"Expected consumer layout to have lane_layout and lane_data");
// vertical lane layout means that the blockload must be transposed
// note scaleA on PVC has vertical lane layout even without transposed order
// attr
bool hasTranspose =
consumerLaneLayout[rank - 2] > 1 && consumerLaneLayout[rank - 1] == 1;
bool hasTransform = !hasTranspose && consumerLaneData[rank - 2] > 1 &&
consumerLaneData[rank - 1] == 1;
assert((consumerLaneData[rank - 2] == 1 || consumerLaneData[rank - 1] == 1) &&
"Expected consumer lane data to have at most one non-unit dim");
if (layoutKind == xegpu::LayoutKind::InstData) {
auto blockWHC = uArchInstruction->getBlockWidthHeightCount(
elemTy, hasTransform, hasTranspose,
/*upConv=*/false);
if (!blockWHC)
return nullptr;
auto [bWidths, bHeights, bCounts] = blockWHC.value();
SmallVector<int64_t> laneLayout;
// set the laneLayout to use consumer's LaneLayout as base, but adjust its
// size to match the subgroupsize in case its original value is larger than
// 1
for (int i = 0; i < rank; i++) {
if (consumerLaneLayout[i] > 1)
laneLayout.push_back(std::max(static_cast<int64_t>(subgroupSize),
consumerLaneLayout[i]));
else
laneLayout.push_back(1);
}
// See whether the consumer's inst_data satisfies the block constraints.
int64_t height = consumerInstData[rank - 2];
int64_t width = consumerInstData[rank - 1];
auto maxBlockCount = *llvm::max_element(bCounts);
auto maxWidth = *llvm::max_element(bWidths);
if (llvm::is_contained(bWidths, static_cast<int>(width)) ||
(width % maxWidth == 0 && width / maxWidth < maxBlockCount)) {
if (llvm::is_contained(bHeights, static_cast<int>(height))) {
return buildInstDataLayoutWithLane(context, consumerInstData,
laneLayout, consumerLaneData,
consumerOrderAttr);
}
}
// if consumer instData size too small, try the larger one. like DPAS_MX's
// scale is smaller than block load
auto instData = get2DBlockIOInstDataLayout(
dataShape, elemTy, uArchInstruction, hasTransform, hasTranspose);
// Shape not realizable as a 2D-block instruction; let the caller report it.
if (!instData)
return nullptr;
assert(isValidLaneLayout(*instData, laneLayout, consumerLaneData) &&
"Expected the load layout to satisfy uArch block constraints");
return buildInstDataLayoutWithLane(context, *instData, laneLayout,
consumerLaneData, consumerOrderAttr);
}
if (layoutKind == xegpu::LayoutKind::Lane) {
assert(isValidLaneLayout(dataShape, consumerLaneLayout, consumerLaneData) &&
"Expected the lane layout to satisfy uArch block constraints");
return consumerLayout;
}
return nullptr;
}
/// Sets up the anchor layout for load gather and load matrix operation.
/// load matrix lowers to load gather and 1d block load. All of them share the
/// same layout setup logic.
///
/// For Subgroup layout, uses the consumer layout directly.
///
/// For InstData layout, takes consumer's inst_data as-is. lane_layout and
/// lane_data are taken from the consumer when present; otherwise the helper
/// derives the standard scatter-style default (subgroupSize lanes on the
/// innermost dim, per-lane vector capped by maxChunkSize).
///
/// For Lane layout, lane_layout/lane_data are taken from the consumer when
/// present; otherwise derived from the same default.
static xegpu::DistributeLayoutAttr setupGenericLoadAnchorLayout(
xegpu::LayoutKind layoutKind, mlir::MLIRContext *context,
xegpu::DistributeLayoutAttr consumerLayout, int maxChunkSize,
ArrayRef<int64_t> resShape, int subgroupSize) {
if (layoutKind == xegpu::LayoutKind::Subgroup)
return consumerLayout;
SmallVector<int64_t> consumerInstData =
consumerLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> consumerLaneLayout =
consumerLayout.getEffectiveLaneLayoutAsInt();
SmallVector<int64_t> consumerLaneData =
consumerLayout.getEffectiveLaneDataAsInt();
SmallVector<int64_t> laneLayout;
SmallVector<int64_t> laneData;
assert(!consumerLaneLayout.empty() && !consumerLaneData.empty() &&
"Expected consumer layout to have lane_layout and lane_data");
laneLayout.assign(consumerLaneLayout.begin(), consumerLaneLayout.end());
laneData.assign(consumerLaneData.begin(), consumerLaneData.end());
if (layoutKind == xegpu::LayoutKind::InstData) {
SmallVector<int64_t> instData;
instData.resize(resShape.size());
for (size_t i = 0; i < resShape.size(); ++i)
instData[i] = laneLayout[i] * laneData[i];
return buildInstDataLayoutWithLane(context, instData, laneLayout, laneData);
}
if (layoutKind == xegpu::LayoutKind::Lane)
return buildLaneLayout(context, laneLayout, laneData);
return nullptr;
}
/// Sets up the anchor layout for a load gather operation.
xegpu::DistributeLayoutAttr xegpu::setupLoadGatherAnchorLayout(
xegpu::LayoutKind layoutKind, VectorType resVecTy, int contigChunkSize,
xegpu::DistributeLayoutAttr consumerLayout, const uArch::uArch *uArch) {
const int subgroupSize = uArch->getSubgroupSize();
ArrayRef<int64_t> resShape = resVecTy.getShape();
auto context = resVecTy.getContext();
const auto *uArchInstruction = dyn_cast<xegpu::uArch::LoadGatherInstruction>(
uArch->getInstruction(xegpu::uArch::InstructionKind::LoadGather));
int maxChunkSize =
std::min(uArchInstruction->getMaxLaneAccessSizeBytes(), contigChunkSize);
return setupGenericLoadAnchorLayout(layoutKind, context, consumerLayout,
maxChunkSize, resShape, subgroupSize);
}
/// Sets up the anchor layout for load matrix operation.
/// TODO: enhance load matrix to indicate lowering to chunked load or not.
xegpu::DistributeLayoutAttr
xegpu::setupLoadMatrixAnchorLayout(xegpu::LayoutKind layoutKind,
VectorType resVecTy, int contigChunkSize,
xegpu::DistributeLayoutAttr consumerLayout,
const xegpu::uArch::uArch *uArch) {
const int subgroupSize = uArch->getSubgroupSize();
ArrayRef<int64_t> resShape = resVecTy.getShape();
auto context = resVecTy.getContext();
const auto *uArchInstruction = dyn_cast<xegpu::uArch::LoadGatherInstruction>(
uArch->getInstruction(xegpu::uArch::InstructionKind::LoadGather));
int maxChunkSize =
std::min(uArchInstruction->getMaxLaneAccessSizeBytes(), contigChunkSize);
return setupGenericLoadAnchorLayout(layoutKind, context, consumerLayout,
maxChunkSize, resShape, subgroupSize);
}
/// Picks the subgroup layout for a scatter-style store (store_scatter /
/// store_matrix): the most balanced `numSg` factorization that divides
/// `wgShape` with sg_data a multiple of `instData`. A store has no consumer.
static xegpu::DistributeLayoutAttr
getStoreSubgroupLayouts(mlir::MLIRContext *context, ArrayRef<int64_t> wgShape,
ArrayRef<int64_t> instData, int numSg) {
auto candidates = getSgLayoutCandidates(wgShape, instData, numSg);
if (candidates.empty())
return nullptr;
// Candidates are ordered most-balanced first.
return buildSgLayout(context, wgShape, candidates.front(), /*dimK=*/-1);
}
/// Sets up the anchor layout for store scatter and store matrix operation,
/// which share the same logic. Lane layout comes from
/// `computeScatterIOLaneLayoutAndData`; inst_data is lane_layout * lane_data.
static xegpu::DistributeLayoutAttr setupGenericStoreAnchorLayout(
xegpu::LayoutKind layoutKind, mlir::MLIRContext *context, int maxChunkSize,
ArrayRef<int64_t> srcShape, int subgroupSize, int numSg) {
auto [laneLayout, laneData] =
computeScatterIOLaneLayoutAndData(srcShape, subgroupSize, maxChunkSize);
SmallVector<int64_t> instData(srcShape.size());
for (size_t i = 0; i < srcShape.size(); ++i)
instData[i] = laneLayout[i] * laneData[i];
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(numSg > 0 &&
"Number of subgroups must be provided for sg layout creation.");
return getStoreSubgroupLayouts(context, srcShape, instData, numSg);
}
if (layoutKind == xegpu::LayoutKind::InstData) {
return buildInstDataLayoutWithLane(context, instData, laneLayout, laneData);
}
if (layoutKind == xegpu::LayoutKind::Lane) {
return buildLaneLayout(context, laneLayout, laneData);
}
return nullptr;
}
/// Sets up the anchor layout for a store scatter operation.
xegpu::DistributeLayoutAttr
xegpu::setupStoreScatterAnchorLayout(xegpu::LayoutKind layoutKind,
VectorType srcVecTy, int contigChunkSize,
int numSg, const uArch::uArch *uArch) {
const int subgroupSize = uArch->getSubgroupSize();
ArrayRef<int64_t> srcShape = srcVecTy.getShape();
auto context = srcVecTy.getContext();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::StoreScatterInstruction>(
uArch->getInstruction(xegpu::uArch::InstructionKind::StoreScatter));
int maxChunkSize =
std::min(uArchInstruction->getMaxLaneAccessSizeBytes(), contigChunkSize);
return setupGenericStoreAnchorLayout(layoutKind, context, maxChunkSize,
srcShape, subgroupSize, numSg);
}
/// Sets up the anchor layout for a store matrix operation.
xegpu::DistributeLayoutAttr xegpu::setupStoreMatrixAnchorLayout(
xegpu::LayoutKind layoutKind, VectorType srcVecTy, int contigChunkSize,
int numSg, const xegpu::uArch::uArch *uArch) {
const int subgroupSize = uArch->getSubgroupSize();
ArrayRef<int64_t> srcShape = srcVecTy.getShape();
auto context = srcVecTy.getContext();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::StoreScatterInstruction>(
uArch->getInstruction(xegpu::uArch::InstructionKind::StoreScatter));
int maxChunkSize =
std::min(uArchInstruction->getMaxLaneAccessSizeBytes(), contigChunkSize);
return setupGenericStoreAnchorLayout(layoutKind, context, maxChunkSize,
srcShape, subgroupSize, numSg);
}
/// Completes a scatter IO layout by deriving lane_layout and lane_data from
/// `specifiedLayout`'s inst_data when they are missing. The layout is returned
/// unchanged if `specifiedLayout` is null, carries no inst_data, or already has
/// both lane_layout and lane_data.
///
/// When lane info is absent, inst_data is treated as the effective shape and
/// the lane factorization is filled in as follows:
/// - If `consumerLayout` is present and its lane_layout / lane_data are a
/// valid factorization of inst_data, that consumer lane info is reused so
/// the completed layout matches the consumer (avoiding a relayout).
/// - Otherwise a standard scatter-style factorization is computed via
/// `computeScatterIOLaneLayoutAndData`, bounded by `maxChunkSize` — the
/// per-lane load width reported by the uArch's LoadGather instruction
/// (`getMaxLaneAccessSizeBytes`).
///
std::optional<xegpu::DistributeLayoutAttr>
xegpu::completeScatterLoadLaneLayoutFromInstData(
xegpu::DistributeLayoutAttr specifiedLayout,
xegpu::DistributeLayoutAttr consumerLayout, Type elemTy,
const xegpu::uArch::LoadGatherInstruction *uArchInstruction,
const int subgroupSize) {
if (!specifiedLayout)
return specifiedLayout;
SmallVector<int64_t> specifiedInstData =
specifiedLayout.getEffectiveInstDataAsInt();
if (specifiedInstData.empty())
return specifiedLayout;
if (!specifiedLayout.getEffectiveLaneLayoutAsInt().empty() &&
!specifiedLayout.getEffectiveLaneDataAsInt().empty())
return specifiedLayout;
// Reuse the load-side setup with inst_data as the destination shape.
auto *context = specifiedLayout.getContext();
int maxChunkSize = uArchInstruction->getMaxLaneAccessSizeBytes();
if (consumerLayout) {
auto consumerLaneLayout = consumerLayout.getEffectiveLaneLayoutAsInt();
auto consumerLaneData = consumerLayout.getEffectiveLaneDataAsInt();
if (!consumerLaneLayout.empty() && !consumerLaneData.empty() &&
isValidLaneLayout(specifiedInstData, consumerLaneLayout,
consumerLaneData))
return buildInstDataLayoutWithLane(context, specifiedInstData,
consumerLaneLayout, consumerLaneData);
}
auto [defLaneLayout, defLaneData] = computeScatterIOLaneLayoutAndData(
specifiedInstData, subgroupSize, maxChunkSize);
if (!isValidLaneLayout(specifiedInstData, defLaneLayout, defLaneData))
return std::nullopt;
return buildInstDataLayoutWithLane(context, specifiedInstData, defLaneLayout,
defLaneData);
}
/// Like completeScatterLoadLaneLayoutFromInstData, but for scatter stores. A
/// store is a data sink, so lane info is derived purely from inst_data (bounded
/// by the uArch's per-lane store width); there is no consumer layout to reuse.
std::optional<xegpu::DistributeLayoutAttr>
xegpu::completeScatterStoreLaneLayoutFromInstData(
xegpu::DistributeLayoutAttr specifiedLayout, Type elemTy,
const xegpu::uArch::StoreScatterInstruction *uArchInstruction,
const int subgroupSize) {
if (!specifiedLayout)
return specifiedLayout;
SmallVector<int64_t> specifiedInstData =
specifiedLayout.getEffectiveInstDataAsInt();
if (specifiedInstData.empty())
return specifiedLayout;
if (!specifiedLayout.getEffectiveLaneLayoutAsInt().empty() &&
!specifiedLayout.getEffectiveLaneDataAsInt().empty())
return specifiedLayout;
// Reuse the store-side setup with inst_data as the source shape.
auto *context = specifiedLayout.getContext();
int maxChunkSize = uArchInstruction->getMaxLaneAccessSizeBytes();
auto [defLaneLayout, defLaneData] = computeScatterIOLaneLayoutAndData(
specifiedInstData, subgroupSize, maxChunkSize);
if (!isValidLaneLayout(specifiedInstData, defLaneLayout, defLaneData))
return std::nullopt;
return buildInstDataLayoutWithLane(context, specifiedInstData, defLaneLayout,
defLaneData);
}
/// Completes a 2D-block store/prefetch layout from its inst_data. store_nd and
/// prefetch_nd are data sinks, so lane info is derived purely from inst_data
/// (no consumer to reuse). One helper serves both via
/// BlockIOInstructionInterface.
std::optional<xegpu::DistributeLayoutAttr>
xegpu::completeBlockStoreLaneLayoutFromInstData(
xegpu::DistributeLayoutAttr specifiedLayout, Type elemTy,
const xegpu::uArch::BlockIOInstructionInterface *uArchInstruction,
const int subgroupSize) {
if (!specifiedLayout)
return specifiedLayout;
SmallVector<int64_t> specifiedInstData =
specifiedLayout.getEffectiveInstDataAsInt();
if (specifiedInstData.empty())
return specifiedLayout;
if (!specifiedLayout.getEffectiveLaneLayoutAsInt().empty() &&
!specifiedLayout.getEffectiveLaneDataAsInt().empty())
return specifiedLayout;
auto *context = specifiedLayout.getContext();
auto [laneLayout, laneData] = compute2DBlockIOLaneLayoutAndData(
specifiedInstData, subgroupSize, elemTy.getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSize());
if (!isValidLaneLayout(specifiedInstData, laneLayout, laneData))
return std::nullopt;
return buildInstDataLayoutWithLane(context, specifiedInstData, laneLayout,
laneData);
}
/// Like completeBlockStoreLaneLayoutFromInstData, but for load_nd. The
/// consumer's lane_data and order are reused as-is; lane_layout is rebuilt from
/// the consumer's lane_layout, bumping every non-unit dim up to the subgroup
/// size. The user-provided inst_data is preserved.
std::optional<xegpu::DistributeLayoutAttr>
xegpu::completeBlockLoadLaneLayoutFromInstData(
xegpu::DistributeLayoutAttr specifiedLayout,
xegpu::DistributeLayoutAttr consumerLayout, Type elemTy,
const xegpu::uArch::BlockIOInstructionInterface *uArchInstruction,
const int subgroupSize) {
if (!specifiedLayout)
return specifiedLayout;
SmallVector<int64_t> specifiedInstData =
specifiedLayout.getEffectiveInstDataAsInt();
if (specifiedInstData.empty())
return specifiedLayout;
if (!specifiedLayout.getEffectiveLaneLayoutAsInt().empty() &&
!specifiedLayout.getEffectiveLaneDataAsInt().empty())
return specifiedLayout;
if (!consumerLayout)
return specifiedLayout;
SmallVector<int64_t> consumerLaneLayout =
consumerLayout.getEffectiveLaneLayoutAsInt();
SmallVector<int64_t> consumerLaneData =
consumerLayout.getEffectiveLaneDataAsInt();
if (consumerLaneLayout.empty() || consumerLaneData.empty())
return specifiedLayout;
auto *context = specifiedLayout.getContext();
int rank = specifiedInstData.size();
SmallVector<int64_t> laneLayout;
// set the laneLayout to use consumer's LaneLayout as base, but adjust its
// size to match the subgroupsize in case its original value is larger than 1
for (int i = 0; i < rank; i++) {
if (consumerLaneLayout[i] > 1) {
laneLayout.push_back(
std::max(static_cast<int64_t>(subgroupSize), consumerLaneLayout[i]));
} else {
laneLayout.push_back(1);
}
}
if (!isValidLaneLayout(specifiedInstData, laneLayout, consumerLaneData))
return std::nullopt;
return buildInstDataLayoutWithLane(context, specifiedInstData, laneLayout,
consumerLaneData,
consumerLayout.getOrder());
}
/// Completes user-provided DPAS A/B/C-D anchors that carry only inst_data by
/// filling in lane_layout / lane_data. The lane factorization mirrors the
/// InstData branch of `setupDpasLayout` (derived from each operand's shape and
/// matmul role, B using VNNI packing); the user's inst_data is preserved.
std::optional<
std::tuple<xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr>>
xegpu::completeDpasLaneLayoutFromInstData(xegpu::DistributeLayoutAttr aLayout,
xegpu::DistributeLayoutAttr bLayout,
xegpu::DistributeLayoutAttr cdLayout,
VectorType aTy, VectorType bTy,
VectorType cdTy,
const xegpu::uArch::uArch *uArch) {
auto context = aTy.getContext();
const auto *uArchInstruction =
dyn_cast<xegpu::uArch::SubgroupMatrixMultiplyAcc>(uArch->getInstruction(
xegpu::uArch::InstructionKind::SubgroupMatrixMultiplyAcc));
if (!uArchInstruction)
return std::nullopt;
auto subgroupSize = uArch->getSubgroupSize();
llvm::SmallVector<int64_t> laneLayoutA, laneDataA, laneLayoutB, laneDataB,
laneLayoutCD, laneDataCD;
SmallVector<int64_t> instDataA = aLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> instDataB = bLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> instDataCD = cdLayout.getEffectiveInstDataAsInt();
if (isa<xegpu::uArch::Xe2, xegpu::uArch::Xe3>(uArch)) {
std::tie(laneLayoutA, laneDataA) = compute2DBlockIOLaneLayoutAndData(
aTy.getShape(), subgroupSize,
aTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeA());
std::tie(laneLayoutB, laneDataB) = compute2DBlockIOLaneLayoutAndData(
bTy.getShape(), subgroupSize,
bTy.getElementType().getIntOrFloatBitWidth(),
uArchInstruction->getPackedFormatBitSizeB(), /*vnni=*/true);
std::tie(laneLayoutCD, laneDataCD) = compute2DBlockIOLaneLayoutAndData(
cdTy.getShape(), subgroupSize,
cdTy.getElementType().getIntOrFloatBitWidth(),
cdTy.getElementType().getIntOrFloatBitWidth());
} else {
assert(false && "Unsupported uArch for DPAS lane layout completion");
}
if (!isValidLaneLayout(instDataA, laneLayoutA, laneDataA) ||
!isValidLaneLayout(instDataB, laneLayoutB, laneDataB) ||
!isValidLaneLayout(instDataCD, laneLayoutCD, laneDataCD))
return std::nullopt;
return std::make_tuple(
buildInstDataLayoutWithLane(context, instDataA, laneLayoutA, laneDataA,
aLayout.getOrder()),
buildInstDataLayoutWithLane(context, instDataB, laneLayoutB, laneDataB,
bLayout.getOrder()),
buildInstDataLayoutWithLane(context, instDataCD, laneLayoutCD, laneDataCD,
cdLayout.getOrder()));
}
/// Like completeDpasLaneLayoutFromInstData, but for dpas_mx: also re-derives
/// the A_scale / B_scale layouts from the completed A / B layouts via
/// `createScaleLayout`, matching the default path of `setupDpasMxLayout`.
std::optional<
std::tuple<xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr, xegpu::DistributeLayoutAttr,
xegpu::DistributeLayoutAttr>>
xegpu::completeDpasMxLaneLayoutFromInstData(
xegpu::DistributeLayoutAttr aLayout, xegpu::DistributeLayoutAttr bLayout,
xegpu::DistributeLayoutAttr cdLayout, VectorType aTy, VectorType bTy,
VectorType cdTy, VectorType aScaleTy, VectorType bScaleTy,
const xegpu::uArch::uArch *uArch) {
auto completed = completeDpasLaneLayoutFromInstData(
aLayout, bLayout, cdLayout, aTy, bTy, cdTy, uArch);
if (!completed)
return std::nullopt;
auto context = aTy.getContext();
auto [completedA, completedB, completedCD] = *completed;
auto aScaleLayout =
createScaleLayout(context, aTy, aScaleTy, completedA, false, uArch);
auto bScaleLayout =
createScaleLayout(context, bTy, bScaleTy, completedB, true, uArch);
return std::make_tuple(completedA, completedB, completedCD, aScaleLayout,
bScaleLayout);
}
/// Sets up layout for reduction operations by creating a SliceAttr for the
/// result.
///
/// Algorithm Overview:
/// This function attempts to construct a source layout that, when sliced along
/// reduction dimensions, produces a result layout compatible with the
/// consumer layout.
///
/// For subgroup layouts, it first tries to align the source layout's subgroup
/// layout and data with the consumer's layout on non-reduction dimensions.
/// Then, it distributes remaining subgroups across reduction dimensions. This
/// avoids subgroup data redistribution overhead between the reduced result and
/// its consumer. When the consumer layout is a slice layout, it attempts to
/// reuse the slice layout's parent layout for the source to further minimize
/// potential data redistribution.
///
/// This is a best-effort alignment, not a hard constraint: the goal is only to
/// pick a *legal* source layout that minimizes redistribution against the
/// (single, first-arriving) consumer layout. There is no failure path - when
/// the consumer's slice layout cannot be reused as-is (example 2 below), the
/// function falls back to distributing all subgroups on the non-reduction
/// dimensions first and the remainder on the reduction dimensions, which always
/// yields a valid source layout. If the resulting source layout still differs
/// from what some consumer expects (e.g. a second, inconsistent consumer), that
/// mismatch is reconciled later by the layout conflict resolution process
/// (`ResolveLayoutConflicts`), which inserts a `convert_layout` op - this
/// function never has to give up.
///
/// For the InstData and Lane layout kinds only the innermost two dimensions
/// are distributed; all leading dimensions are assumed to be unit dimensions.
/// This assumption is checked via `leadingDimsAreUnit`. The lane_layout and
/// lane_data are computed by `computeReductionLaneLayoutAndData`, which picks
/// a layout that minimizes cross-lane reduction (reducing within a lane when
/// only one of the innermost two dims is a reduction dim). The inst_data is
/// simply the element-wise product lane_layout * lane_data.
///
/// The function returns the *result* layout (the SliceAttr). The *source*
/// layout it decides on is the parent of that slice; both are listed below so
/// the relationship is explicit.
///
/// Examples:
/// 1. Subgroup layout - Row reduction on 2D tensor:
/// srcShape=[32, 128], reductionDims=[1], resShape=[32], subgroupSize=16,
/// NumSg=32
/// * Consumer Layout:
/// #xegpu.slice<#xegpu.layout<sg_layout=[4, 8], sg_data=[8, 8]>, dims =
/// [1]>}
/// * Source Layout (decided by this function):
/// #xegpu.layout<sg_layout=[4, 8], sg_data=[8, 16]>
/// * Result Layout (returned):
/// #xegpu.slice<#xegpu.layout<sg_layout=[4, 8], sg_data=[8, 16]>, dims =
/// [1]>}
/// The consumer slices exactly the reduction dim, so its parent layout is
/// reused for the source: sg_layout is kept, but the source's sg_data on
/// the reduction dim is grown from 8 to 16 (= srcShape[1] / sg_layout[1] =
/// 128 / 8) so the source tile is evenly distributed over the reduction
/// dim. Slicing that source over dim 1 reproduces the consumer.
///
/// 2. Subgroup layout - Same shapes as above but consumer doesn't have a
/// reusable slice layout, so the algorithm distributes all subgroups on the
/// non-reduction dims first and the remainder on the reduction dims.
/// 2a. * Consumer Layout:
/// #xegpu.layout<sg_layout=[32], sg_data=[1]>
/// * Source Layout (decided by this function):
/// #xegpu.layout<sg_layout=[32, 1], sg_data=[1, 128]>
/// * Result Layout (returned):
/// #xegpu.slice<#xegpu.layout<sg_layout=[32, 1], sg_data=[1, 128]>,
/// dims = [1]>}
/// All 32 subgroups land on the non-reduction dim 0; the reduction dim
/// 1 gets the leftover (sg_layout=1, so the whole length 128 lives in
/// one subgroup's sg_data).
/// 2b. * Consumer Layout:
/// #xegpu.slice<#xegpu.layout<sg_layout=[8, 2, 4], sg_data=[4, 64,
/// 32]>, dims = [1, 2]>}
/// * Source Layout (decided by this function):
/// #xegpu.layout<sg_layout=[8, 4], sg_data=[4, 32]>
/// * Result Layout (returned):
/// #xegpu.slice<#xegpu.layout<sg_layout=[8, 4], sg_data=[4, 32]>,
/// dims = [1]>}
/// The consumer slices dims [1, 2] which do not match this op's
/// reductionDims, so it can't be reused as-is; subgroups are
/// re-distributed (non-reduction dim first, then reduction dim).
///
/// 3. Lane layout - Default (lanes on innermost dim):
/// srcShape=[32, 64], reductionDims=[0], subgroupSize=16
/// * Source Layout (decided by this function):
/// laneLayout=[1, 16], laneData=[1, 1] (returned sliced over dim 0).
/// The innermost dim is not reduced, so lanes stay on it.
///
/// 4. Lane layout - Switch (lanes moved off the reduction dim):
/// srcShape=[32, 64], reductionDims=[1], subgroupSize=16
/// * Source Layout (decided by this function):
/// laneLayout=[16, 1], laneData=[1, 1] (returned sliced over dim 1).
/// The innermost dim is the sole reduction dim, so lanes move to the
/// non-reduction dim to reduce within a lane. This switch only happens
/// when the consumer has no reduction dims to broadcast the result back
/// along (i.e. the consumer layout is not a slice over this reduction);
/// otherwise the default (example 3) is used.
///
/// 5. Lane layout - No switch when both inner dims are reduced (reduction to
/// scalar):
/// srcShape=[32, 64], reductionDims=[0, 1], subgroupSize=16
/// * Source Layout (decided by this function):
/// laneLayout=[1, 16], laneData=[1, 1] (returned sliced over dims
/// [0,1]).
/// Both dims are reduced, so this is not a *sole* innermost reduction; the
/// switch condition (example 4) does not apply and lanes stay on the
/// innermost dim. The cross-lane reduction here is unavoidable.
///
/// 6. Lane layout - No switch when the consumer slices the reduction dim:
/// srcShape=[32, 64], reductionDims=[1], subgroupSize=16
/// * Consumer Layout:
/// #xegpu.slice<#xegpu.layout<laneLayout=[1, 16], laneData=[1, 1]>,
/// dims = [1]>}
/// * Source Layout (decided by this function):
/// #xegpu.layout<laneLayout=[1, 16], laneData=[1, 1]> (the consumer
/// slice's parent, reused directly; returned sliced over dim 1).
/// Same shape/reductionDims as example 4, but here the consumer is a slice
/// over the reduction dim, so it can broadcast the result back along that
/// dim. The slice's parent layout is reused as the source (no switch, no
/// re-derivation); the inst_data propagation step has already inserted a
/// convert_layout if needed, so the lane-level layout can be reused as-is.
xegpu::SliceAttr xegpu::setupMultiReductionResultLayout(
xegpu::LayoutKind layoutKind, VectorType srcVecTy,
DistributeLayoutAttr consumerLayout, SmallVector<int64_t> reductionDims,
int numSg, const xegpu::uArch::uArch *uArch) {
auto srcShape = srcVecTy.getShape();
int srcRank = srcShape.size();
auto context = srcVecTy.getContext();
const int subgroupSize = uArch->getSubgroupSize();
int64_t maxReduceVectorSize = 1; // could extend to spirv vector Size
xegpu::DistributeLayoutAttr srcLayout;
if (layoutKind == xegpu::LayoutKind::Subgroup) {
xegpu::SliceAttr consumerSliceLayout =
dyn_cast_if_present<xegpu::SliceAttr>(consumerLayout);
if (consumerSliceLayout &&
consumerSliceLayout.getDims().asArrayRef().equals(reductionDims)) {
srcLayout = consumerSliceLayout.getParent();
SmallVector<int64_t> sgLayoutFromConsumer =
srcLayout.getEffectiveSgLayoutAsInt();
auto srcSgData = computeShapeRatio(srcShape, sgLayoutFromConsumer);
if (srcSgData)
for (int dim = 0; dim < srcRank; dim++) {
if (llvm::is_contained(reductionDims, dim))
srcLayout =
srcLayout.setDimData(dim, srcSgData.value()[dim], -1, -1);
}
} else {
SmallVector<int64_t> consumerSgLayout =
consumerLayout ? consumerLayout.getEffectiveSgLayoutAsInt()
: SmallVector<int64_t>();
SmallVector<int64_t> consumerSgData =
consumerLayout ? consumerLayout.getEffectiveSgDataAsInt()
: SmallVector<int64_t>();
SmallVector<int64_t> consumerOrder =
consumerLayout ? consumerLayout.getEffectiveOrderAsInt()
: SmallVector<int64_t>();
DenseI32ArrayAttr orderAttr =
consumerLayout ? consumerLayout.getOrder() : nullptr;
SmallVector<int64_t> sgLayout(srcRank), sgData(srcRank), order(srcRank);
int remainingSgCount =
consumerLayout ? consumerLayout.getNumSubgroups() : numSg;
int consumerIdx = 0;
// First pass: Match consumer's layout on non-reduction dimensions
for (int i = 0; i < srcRank; i++) {
if (!llvm::is_contained(reductionDims, i) &&
consumerIdx < static_cast<int>(consumerSgLayout.size())) {
sgLayout[i] = consumerSgLayout[consumerIdx];
sgData[i] = consumerSgData[consumerIdx];
remainingSgCount /= sgLayout[i];
order[i] = consumerOrder[consumerIdx];
consumerIdx++;
}
}
// Second pass: Distribute remaining subgroups across reduction dimensions
// the reduction to scalar case is handled only by this loop
int64_t remainOrder = consumerSgLayout.size();
for (int i = 0; i < srcRank; i++) {
if (llvm::is_contained(reductionDims, i)) {
sgLayout[i] =
std::min(srcShape[i], static_cast<int64_t>(remainingSgCount));
assert((srcShape[i] % sgLayout[i] == 0) &&
"source shape not divisible by sg_layout");
sgData[i] = srcShape[i] / sgLayout[i];
remainingSgCount /= sgLayout[i];
order[i] = remainOrder++;
}
}
DenseI32ArrayAttr resOrderAttr = DenseI32ArrayAttr::get(
context, SmallVector<int32_t>(order.begin(), order.end()));
if (!orderAttr || orderAttr.empty())
resOrderAttr = nullptr;
assert(remainingSgCount == 1 && "not all subgroups distributed");
srcLayout = buildLayout(context, sgLayout, sgData,
/*instData=*/{}, /*laneLayout=*/{},
/*laneData=*/{}, resOrderAttr);
}
} else if (layoutKind == xegpu::LayoutKind::InstData) {
xegpu::SliceAttr consumerSliceLayout =
dyn_cast_if_present<xegpu::SliceAttr>(consumerLayout);
auto consumerReductionDims =
consumerSliceLayout
? SmallVector<int64_t>(consumerSliceLayout.getDims().asArrayRef())
: SmallVector<int64_t>({});
// A[i] reduced from A[i, j] is stored out directly, use vertical Lane
// layout like [16, 1]
bool verticalLaneLayout = consumerReductionDims.empty() &&
reductionDims.size() == 1 &&
reductionDims[0] == (srcRank - 1);
auto [laneLayout, laneData] = computeReductionLaneLayoutAndData(
srcShape, reductionDims, subgroupSize, maxReduceVectorSize,
verticalLaneLayout);
// inst_data is the per-instruction data, i.e. the element-wise product of
// lane_layout and lane_data.
SmallVector<int64_t> instData(srcRank);
for (int i = 0; i < srcRank; i++)
instData[i] = laneLayout[i] * laneData[i];
srcLayout =
buildInstDataLayoutWithLane(context, instData, laneLayout, laneData);
} else if (layoutKind == xegpu::LayoutKind::Lane) {
// Only the innermost two dimensions are distributed; all leading dimensions
// are assumed to be unit dimensions.
assert(leadingDimsAreUnit(srcShape, /*numInnerDims=*/2) &&
"Lane reduction layout assumes all leading (non-innermost-two) "
"dimensions are unit dimensions");
xegpu::SliceAttr consumerSliceLayout =
dyn_cast_if_present<xegpu::SliceAttr>(consumerLayout);
auto consumerReductionDims =
consumerSliceLayout
? SmallVector<int64_t>(consumerSliceLayout.getDims().asArrayRef())
: SmallVector<int64_t>({});
if (consumerSliceLayout &&
consumerSliceLayout.getDims().asArrayRef().equals(reductionDims)) {
// at the lane level, the consumerSliceLayout can be directly reused
// since the inst_data propagation already insert convert_layout if
// the layout is not consistent
srcLayout = consumerSliceLayout.getParent();
} else {
bool verticalLaneLayout = consumerReductionDims.empty() &&
reductionDims.size() == 1 &&
reductionDims[0] == (srcRank - 1);
auto [laneLayout, laneData] = computeReductionLaneLayoutAndData(
srcShape, reductionDims, subgroupSize, maxReduceVectorSize,
verticalLaneLayout);
srcLayout = buildLaneLayout(context, laneLayout, laneData);
}
}
return xegpu::SliceAttr::get(context, srcLayout,
DenseI64ArrayAttr::get(context, reductionDims));
}
/// Sets up layout for Reduction operations by creating a SliceAttr for the
/// result.
xegpu::SliceAttr
xegpu::setupReductionResultLayout(xegpu::LayoutKind layoutKind,
VectorType srcVecTy,
const xegpu::uArch::uArch *uArch) {
auto srcShape = srcVecTy.getShape();
auto context = srcVecTy.getContext();
auto subgroupSize = uArch->getSubgroupSize();
xegpu::LayoutAttr srcLayout;
if (layoutKind == xegpu::LayoutKind::Subgroup) {
assert(false &&
"subgroup layout assignment not supported for reduction (op "
"is not expected at this level).");
} else if (layoutKind == xegpu::LayoutKind::InstData) {
assert(false &&
"instData layout assignment not supported for reduction (op "
"is not expected at this level).");
} else if (layoutKind == xegpu::LayoutKind::Lane) {
SmallVector<int64_t> laneLayout(1), laneData(1);
laneLayout[0] = std::min(static_cast<int64_t>(subgroupSize), srcShape[0]);
laneData[0] = 1;
srcLayout = buildLaneLayout(context, laneLayout, laneData);
}
auto result = xegpu::SliceAttr::get(context, srcLayout,
DenseI64ArrayAttr::get(context, 0));
return result;
}
/// Adjusts `consumerLayout`'s innermost-dim data field selected by
/// `layoutKind` so that the source layout can be safely inferred by dividing
/// that value by `ratio`. Doubles the value until the divisibility constraint
/// is met, bounded above by `bound` like result-shape.
///
/// Used by ops whose source relates to the result by a fixed factor along the
/// innermost dim (e.g., bitcast: bitwidth ratio; interleave: 2x).
///
/// Divisibility constraints per LayoutKind:
/// - Subgroup: sgData[innermost] % ratio == 0
/// - InstData: instData[innermost] % (laneLayout[innermost] * ratio) == 0
/// (laneLayout falls back to subgroupSize if absent)
/// - Lane: laneData[innermost] % ratio == 0
static xegpu::DistributeLayoutAttr
adjustInnermostDimForDivisibility(xegpu::DistributeLayoutAttr consumerLayout,
xegpu::LayoutKind layoutKind,
size_t innerMostDim, int ratio, int64_t bound,
const xegpu::uArch::uArch *uArch) {
SmallVector<int64_t> sgData = consumerLayout.getEffectiveSgDataAsInt();
SmallVector<int64_t> instData = consumerLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> laneData = consumerLayout.getEffectiveLaneDataAsInt();
SmallVector<int64_t> laneLayout =
consumerLayout.getEffectiveLaneLayoutAsInt();
int64_t sgDataValue = -1;
int64_t instDataValue = -1;
int64_t laneDataValue = -1;
if (layoutKind == xegpu::LayoutKind::Subgroup) {
sgDataValue = sgData[innerMostDim];
while ((sgDataValue <= bound) && (sgDataValue % ratio) != 0)
sgDataValue *= 2;
} else if (layoutKind == xegpu::LayoutKind::InstData) {
instDataValue = instData[innerMostDim];
const int innermostDimLaneLayout = laneLayout.empty()
? uArch->getSubgroupSize()
: laneLayout[innerMostDim];
while ((instDataValue <= bound) &&
(instDataValue % (innermostDimLaneLayout * ratio) != 0))
instDataValue *= 2;
assert((bound % instDataValue) == 0 &&
"bound, instData, and laneLayout for innermost must be 2^n!");
} else if (layoutKind == xegpu::LayoutKind::Lane) {
laneDataValue = laneData[innerMostDim];
while ((laneDataValue <= bound) && (laneDataValue % ratio) != 0)
laneDataValue *= 2;
}
return consumerLayout.setDimData(innerMostDim, sgDataValue, instDataValue,
laneDataValue);
}
/// Sets up the result layout for a bitcast operation.
/// When casting to a smaller bitwidth, adjusts the layout dimensions (sgData,
/// instData, or laneData) by multiplying by the bitwidth ratio to ensure the
/// result layout can be correctly divided back to the source layout during
/// inference.
///
/// Examples:
/// 1. Casting f32 -> f16 (32-bit to 16-bit, bitWidthRatio = 2):
/// Consumer layout: instData=[1, 16], subgroupSize=16
/// Source shape: [8, 32]
/// Result layout: instData=[1, 32] (16 * 2)
/// The innermost dimension is multiplied by 2 to maintain consistency.
///
/// 2. Casting f32 -> i8 (32-bit to 8-bit, bitWidthRatio = 4):
/// Consumer instData=[1, 16], subgroupSize=16
/// Source shape: [4, 128]
/// adjust the instData from [1, 16] to [1, 16 * 4 = 64]
///
/// 3. Casting i8 -> i32 (8-bit to 32-bit, bitWidthRatio = 1/4):
/// Consumer layout: laneLayout=[1, 16], laneData=[1, 4]
/// No adjustment needed - returns consumer layout directly.
///
xegpu::DistributeLayoutAttr xegpu::setupBitCastResultLayout(
xegpu::LayoutKind layoutKind, VectorType srcVecTy, VectorType resVecTy,
DistributeLayoutAttr consumerLayout, const xegpu::uArch::uArch *uArch) {
int srcElemTyBitWidth = srcVecTy.getElementType().getIntOrFloatBitWidth();
int resElemTyBitWidth = resVecTy.getElementType().getIntOrFloatBitWidth();
ArrayRef<int64_t> srcShape = srcVecTy.getShape();
ArrayRef<int64_t> resShape = resVecTy.getShape();
assert(consumerLayout.getRank() == static_cast<int64_t>(srcShape.size()) &&
"laneData must be available for all dimensions");
// Casting to same/larger element type: result has fewer (or equal) elements
// along the innermost dim, no adjustment needed.
if (srcElemTyBitWidth <= resElemTyBitWidth)
return consumerLayout;
// Casting to smaller element type: result has more elements along innermost
// dim. Adjust the innermost data field upward so the source layout can be
// recovered by dividing by bitWidthRatio.
size_t innerMostDim = srcShape.size() - 1;
int bitWidthRatio = srcElemTyBitWidth / resElemTyBitWidth;
return adjustInnermostDimForDivisibility(consumerLayout, layoutKind,
innerMostDim, bitWidthRatio,
resShape[innerMostDim], uArch);
}
/// Sets up the result layout for an interleave operation to ensure the source
/// layout can be safely derived. Interleave doubles the innermost dimension,
/// so the result layout must ensure that laneData is a multiple
/// of 2, and instData must be divisible by innermostDimLaneLayout * 2.
///
/// Example:
/// Interleave: vector<128x256xf4> -> vector<128x512xf4>
/// Consumer layout: laneLayout=[1, 16], laneData=[1, 4], instData=[1, 64]
/// Result layout adjustment to ensure source can be safely inferred:
/// - laneData must be >= 2 and multiple of 2 (so source = laneData/2 is
/// valid)
/// - instData must be divisible by (16 * 2 = 32) (so source = instData/2 is
/// valid)
/// - Adjusted instData: ensure (instData % 32 == 0)
///
xegpu::DistributeLayoutAttr xegpu::setupInterleaveResultLayout(
xegpu::LayoutKind layoutKind, VectorType srcVecTy, VectorType resVecTy,
DistributeLayoutAttr consumerLayout, const xegpu::uArch::uArch *uArch) {
ArrayRef<int64_t> resShape = resVecTy.getShape();
assert(consumerLayout.getRank() == static_cast<int64_t>(resShape.size()) &&
"consumer layout rank must match source shape rank");
// Interleave doubles the innermost dimension (ratio = 2). Adjust the
// innermost data field so the source layout can be recovered by dividing
// by 2.
const size_t innerMostDim = resShape.size() - 1;
constexpr int ratio = 2;
return adjustInnermostDimForDivisibility(consumerLayout, layoutKind,
innerMostDim, ratio,
resShape[innerMostDim], uArch);
}
/// Sets up the result layout for an insert strided slice operation.
/// Creates a result layout based on the specified layout kind (InstData or
/// Lane).
xegpu::DistributeLayoutAttr xegpu::setupInsertStridedSliceResultLayout(
xegpu::LayoutKind layoutKind, VectorType srcVectorTy,
VectorType resVectorTy, xegpu::DistributeLayoutAttr consumerLayout,
const xegpu::uArch::uArch *uArch) {
xegpu::DistributeLayoutAttr requiredResLayout;
SmallVector<int64_t> consumerInstData =
consumerLayout.getEffectiveInstDataAsInt();
SmallVector<int64_t> consumerLaneData =
consumerLayout.getEffectiveLaneDataAsInt();
SmallVector<int64_t> consumerLaneLayout =
consumerLayout.getEffectiveLaneLayoutAsInt();
ArrayRef<int64_t> srcShape = srcVectorTy.getShape();
int64_t laneDataValue = -1;
requiredResLayout = consumerLayout;
int srcRank = srcShape.size();
if (layoutKind == xegpu::LayoutKind::Subgroup ||
layoutKind == xegpu::LayoutKind::InstData) {
assert(false && "subgroup/instData layout assignment not supported for "
"insertStridedSlice.");
} else if (layoutKind == xegpu::LayoutKind::Lane) {
for (int dim = 0; dim < srcRank; dim++) {
// A size-1 source dim is broadcast across the lanes of that dim.
if (srcShape[dim] == 1) {
laneDataValue = 1;
} else {
assert(srcShape[dim] % consumerLaneLayout[dim] == 0 &&
"srcShape must be divisible by laneLayout for all dimensions");
laneDataValue = std::min(srcShape[dim] / consumerLaneLayout[dim],
consumerLaneData[dim]);
}
requiredResLayout =
requiredResLayout.setDimData(dim, -1, -1, laneDataValue);
}
}
return requiredResLayout;
}
/// Back-propagates a known result layout to the layout required on `operand`
/// for a non-anchor (layout-propagating) vector op. Dispatches on the op kind —
/// broadcast, (multi)reduction, bitcast, shape/transpose, insert/extract,
/// interleave, etc. — applying the shape/permutation/bitwidth transform to
/// derive the source layout; elementwise and pass-through ops reuse resLayout
/// as-is. Returns nullptr for unknown ops or an absent result layout.
xegpu::DistributeLayoutAttr xegpu::inferSourceLayoutFromResultForNonAnchorOp(
OpOperand &operand, xegpu::DistributeLayoutAttr resLayout) {
if (!resLayout)
return nullptr;
Operation *op = operand.getOwner();
unsigned idx = operand.getOperandNumber();
// For vector::BroadcastOp, infer the source layout from the result layout.
if (auto broadcast = dyn_cast<vector::BroadcastOp>(op)) {
auto srcTy = dyn_cast<VectorType>(broadcast.getSourceType());
if (!srcTy)
return nullptr;
return xegpu::inferBroadcastSourceLayout(
resLayout, broadcast.getResultVectorType().getShape(),
srcTy.getShape());
}
// For vector::MultiDimReductionOp, infer source layout from result layout
// using reduction dims. Acc operand is expected to have the same layout as
// the result.
if (auto reduction = dyn_cast<vector::MultiDimReductionOp>(op)) {
if (idx == 0) {
SmallVector<int64_t> reductionDims(reduction.getReductionDims());
return xegpu::inferMultiReductionSourceLayout(resLayout, reductionDims);
}
if (idx == 1)
return resLayout;
}
if (auto reduction = dyn_cast<vector::ReductionOp>(op))
return xegpu::inferReductionSourceLayout(resLayout);
// For vector::BitCastOp, infer source layout from result layout using
// element type bitwidths.
if (auto bitcast = dyn_cast<vector::BitCastOp>(op)) {
int resElemBitWidth =
bitcast.getResultVectorType().getElementType().getIntOrFloatBitWidth();
int srcElemBitWidth =
bitcast.getSourceVectorType().getElementType().getIntOrFloatBitWidth();
return xegpu::inferBitCastSourceLayout(resLayout, resElemBitWidth,
srcElemBitWidth);
}
// For vector::ShapeCastOp, infer source layout from result layout using
// shapes.
if (auto shapeCast = dyn_cast<vector::ShapeCastOp>(op)) {
return xegpu::inferShapeCastSourceLayout(
resLayout, shapeCast.getResultVectorType().getShape(),
shapeCast.getSourceVectorType().getShape());
}
// For vector::InsertStridedSliceOp, infer source layout from result
// layout. Dest vector must have the same layout as the result.
if (auto insertSlice = dyn_cast<vector::InsertStridedSliceOp>(op)) {
if (idx == 0) {
return xegpu::inferInsertStridedSliceSourceLayout(
resLayout, insertSlice.getDestVectorType().getShape(),
insertSlice.getSourceVectorType().getShape());
}
if (idx == 1)
return resLayout;
}
// For vector::Insert Op, infer source layout from result layout using
// shapes.
if (auto insert = dyn_cast<vector::InsertOp>(op)) {
VectorType resVecTy = dyn_cast<VectorType>(insert.getResult().getType());
VectorType valueToStoreTy =
dyn_cast<VectorType>(insert.getValueToStore().getType());
if ((idx == 0) && valueToStoreTy) {
return xegpu::inferInsertSourceLayout(resLayout, resVecTy.getShape(),
valueToStoreTy.getShape());
}
if (idx == 1)
return resLayout;
}
// For vector::Extract Op, infer source layout from result layout using
// shapes.
if (auto extract = dyn_cast<vector::ExtractOp>(op)) {
VectorType srcVecTy = dyn_cast<VectorType>(extract.getSource().getType());
VectorType resVecTy = dyn_cast<VectorType>(extract.getResult().getType());
if (!srcVecTy || !resVecTy)
return nullptr;
return xegpu::inferExtractSourceLayout(resLayout, resVecTy.getShape(),
srcVecTy.getShape());
}
// For vector::TransposeOp, infer source layout from result layout using
// permutation.
if (auto transpose = dyn_cast<vector::TransposeOp>(op)) {
return xegpu::inferTransposeSourceLayout(resLayout,
transpose.getPermutation());
}
// For vector::BitCastOp, infer source layout from result layout using
// element type bitwidths.
if (auto bitcast = dyn_cast<vector::BitCastOp>(op)) {
int resElemBitWidth =
bitcast.getResultVectorType().getElementType().getIntOrFloatBitWidth();
int srcElemBitWidth =
bitcast.getSourceVectorType().getElementType().getIntOrFloatBitWidth();
return xegpu::inferBitCastSourceLayout(resLayout, resElemBitWidth,
srcElemBitWidth);
}
// for vector::interleave
if (auto interleave = dyn_cast<vector::InterleaveOp>(op)) {
return xegpu::inferInterleaveSourceLayout(resLayout);
}
// for vector::deinterleave
if (auto deinterleave = dyn_cast<vector::DeinterleaveOp>(op)) {
return xegpu::inferDeinterleaveSourceLayout(resLayout);
}
// For vector::ExtractStridedSliceOp, simply return result layout
if (dyn_cast<vector::ExtractStridedSliceOp>(op))
return resLayout;
// For elementwise operations, all operands must have the same layout as
// the result.
if (OpTrait::hasElementwiseMappableTraits(op) && op->getNumResults() == 1)
return resLayout;
return nullptr;
}
// For a loop terminator operand (scf.for's scf.yield, scf.while's
// scf.condition), returns the layout of the region iter_arg it forwards into,
// which is the authoritative loop-carried layout, or nullptr when that position
// was never assigned a layout.
static xegpu::DistributeLayoutAttr getLoopCarriedLayoutForYieldOperand(
RegionBranchTerminatorOpInterface terminator, OpOperand &operand) {
auto branch = dyn_cast<RegionBranchOpInterface>(terminator->getParentOp());
if (!branch)
return nullptr;
RegionBranchSuccessorMapping mapping;
branch.getSuccessorOperandInputMapping(mapping,
RegionBranchPoint(terminator));
auto it = mapping.find(&operand);
if (it == mapping.end())
return nullptr;
xegpu::DistributeLayoutAttr iterArgLayout;
for (Value input : it->second) {
auto arg = dyn_cast<BlockArgument>(input);
if (!arg)
continue;
xegpu::DistributeLayoutAttr layout = xegpu::getDistributeLayoutAttr(arg);
assert((!iterArgLayout || !layout || iterArgLayout.isEqualTo(layout)) &&
"region inputs fed by one terminator operand disagree on layout");
if (!iterArgLayout)
iterArgLayout = layout;
}
return iterArgLayout;
}
// For the terminator of a region op that carries nothing back into its regions
// (scf.if), returns the layout of the parent result the operand feeds.
static xegpu::DistributeLayoutAttr getParentResultLayoutForYieldOperand(
RegionBranchTerminatorOpInterface terminator, OpOperand &operand) {
auto branch = dyn_cast<RegionBranchOpInterface>(terminator->getParentOp());
if (!branch)
return nullptr;
RegionBranchSuccessorMapping mapping;
branch.getSuccessorOperandInputMapping(mapping,
RegionBranchPoint(terminator));
auto it = mapping.find(&operand);
if (it == mapping.end())
return nullptr;
for (Value input : it->second)
if (auto result = dyn_cast<OpResult>(input))
return xegpu::getDistributeLayoutAttr(result);
return nullptr;
}
/// Returns the layout required on `operand`: anchor ops report their declared
/// per-operand layout directly; non-anchor ops back-derive it from their result
/// layout via inferSourceLayoutFromResultForNonAnchorOp.
xegpu::DistributeLayoutAttr xegpu::getConsumerLayoutAt(OpOperand &operand) {
Operation *op = operand.getOwner();
// Anchor ops declare the layout they
// require on each operand. Trust that declaration directly so that
// ResolveLayoutConflicts compares producer-vs-declared
if (isa<xegpu::AnchorLayoutInterface>(op))
return xegpu::getDistributeLayoutAttr(operand);
// Region ops with forwarded operands (scf.for's and scf.while's inits) carry
// the required operand layout as the layout_operand_N that
// propagateRegionArgsToInits back-propagated from the region argument.
// TODO: derive that layout from the region argument here instead, so this
// function is the only place an operand's required layout comes from.
if (isa<RegionBranchOpInterface>(op))
return xegpu::getDistributeLayoutAttr(operand);
// A region terminator requires the layout of the successor input its operand
// feeds: the region iter_arg for a loop, and the parent result for a region
// op with no loop-carried values (scf.if).
if (auto terminator = dyn_cast<RegionBranchTerminatorOpInterface>(op)) {
if (isa<LoopLikeOpInterface>(op->getParentOp()))
return getLoopCarriedLayoutForYieldOperand(terminator, operand);
return getParentResultLayoutForYieldOperand(terminator, operand);
}
// For non-anchor ops, derive the operand layout from the op's result
// layout via op-specific semantics.
xegpu::DistributeLayoutAttr resLayout;
if (op->getNumResults() == 1 || isa<vector::DeinterleaveOp>(op))
resLayout = xegpu::getDistributeLayoutAttr(op->getResult(0));
return inferSourceLayoutFromResultForNonAnchorOp(operand, resLayout);
}