| //===---- 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 ®ion, |
| 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(®ion); |
| 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 ®ion : regionOp->getRegions()) { |
| RegionSuccessor regionSuccessor(®ion); |
| // 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); |
| } |