MLIR 24.0.0git
VectorToXeGPU.cpp
Go to the documentation of this file.
1//===- VectorToXeGPU.cpp - Convert vector to XeGPU dialect ------*- C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8//
9// This file implements lowering of vector operations to XeGPU dialect ops.
10//
11//===----------------------------------------------------------------------===//
12
15
26#include "mlir/IR/Matchers.h"
28#include "mlir/Pass/Pass.h"
30#include "llvm/ADT/TypeSwitch.h"
31
32#include <algorithm>
33#include <optional>
34
35namespace mlir {
36#define GEN_PASS_DEF_CONVERTVECTORTOXEGPU
37#include "mlir/Conversion/Passes.h.inc"
38} // namespace mlir
39
40using namespace mlir;
41
42namespace {
43// Return true if value represents a zero constant.
44static bool isZeroConstant(Value val) {
45 auto constant = val.getDefiningOp<arith::ConstantOp>();
46 if (!constant)
47 return false;
48
49 return TypeSwitch<Attribute, bool>(constant.getValue())
50 .Case([](FloatAttr floatAttr) { return floatAttr.getValue().isZero(); })
51 .Case([](IntegerAttr intAttr) { return intAttr.getValue().isZero(); })
52 .Default(false);
53}
54
55// Return true if the transfer padding value is compatible with the implicit
56// padding of an nd block load. LoadNdOp fills out-of-bounds elements with zero,
57// so a zero constant matches its semantics exactly. A poison padding means the
58// out-of-bounds elements are "don't care", so any implicit padding (including
59// zero) is also acceptable.
60static bool isZeroOrPoisonPadding(Value val) {
61 return isZeroConstant(val) || val.getDefiningOp<ub::PoisonOp>();
62}
63
64// Return true if the permutation map keeps every dimension in place except the
65// innermost two, which are swapped, e.g.:
66// (d0, d1) -> (d1, d0)
67// (d0, d1, d2) -> (d2, d1)
68// (d0, d1, d2, d3) -> (d0, d1, d3, d2)
69// This is the only non-identity permutation an nd block load can realize (by
70// loading the untransposed block and applying a trailing vector.transpose).
71static bool isInnermostTwoDimsTransposed(AffineMap map) {
72 unsigned numResults = map.getNumResults();
73 if (numResults < 2)
74 return false;
75 MLIRContext *ctx = map.getContext();
76 unsigned numInputs = map.getNumInputs();
77 // All but the innermost two results must match the minor-identity map.
78 for (unsigned i = 0; i + 2 < numResults; ++i)
79 if (map.getResult(i) != getAffineDimExpr(numInputs - numResults + i, ctx))
80 return false;
81 // The innermost two results must be the last two input dims, swapped.
82 return map.getResult(numResults - 2) ==
83 getAffineDimExpr(numInputs - 1, ctx) &&
84 map.getResult(numResults - 1) == getAffineDimExpr(numInputs - 2, ctx);
85}
86
87// Return true if `uArch` can transfer a `shape`-shaped tile of `elemTy`
88// elements with the subgroup 2D block instruction `instKind`.
89//
90// A 2D block instruction accesses the two innermost dimensions of the tile;
91// leading dimensions are unrolled into a sequence of 2D accesses. The
92// innermost 2D tile does not have to match a hardware block exactly, because
93// the later XeGPU passes (layout propagation and blocking) split it into
94// hardware-sized blocks - but that split only exists when each of the two
95// innermost extents is a multiple of a supported block extent. Transfers
96// without such a split cannot be expressed with block instructions at all and
97// must use a different lowering.
98static bool isSupportedBlockShape(const xegpu::uArch::uArch *uArch,
101 bool hasTranspose = false) {
102 if (shape.size() < 2)
103 return false;
104 if (!uArch || !uArch->isSupportedInstruction(instKind))
105 return false;
106
107 const auto *blockInst = dyn_cast<xegpu::uArch::BlockIOInstructionInterface>(
108 uArch->getInstruction(instKind));
109 if (!blockInst)
110 return false;
111
112 int width = static_cast<int>(shape.back());
113 int height = static_cast<int>(shape[shape.size() - 2]);
114
115 // A tile is accessible when both of its extents are multiples of a supported
116 // block extent, so that the later XeGPU passes can split it into hardware
117 // blocks. A missing entry means this variant does not support the element
118 // type.
119 auto fitsBlockShapes = [&](bool hasTransform) {
120 std::optional<xegpu::uArch::BlockIOInstructionInterface::BlockShapes>
121 blockShapes = blockInst->getBlockWidthHeightCount(elemTy, hasTransform,
122 hasTranspose);
123 if (!blockShapes)
124 return false;
125 auto [widths, heights, counts] = *blockShapes;
126 return xegpu::getLargestDivisor(width, widths) != -1 &&
127 xegpu::getLargestDivisor(height, heights) != -1;
128 };
129
130 // Whether a tile is eventually loaded with the transformed (VNNI) variant is
131 // only decided later, when layout propagation knows whether it feeds the B
132 // operand of a matrix operation, so accept a tile that either variant can
133 // access.
134 return fitsBlockShapes(/*hasTransform=*/false) ||
135 fitsBlockShapes(/*hasTransform=*/true);
136}
137
138static LogicalResult transferPreconditions(PatternRewriter &rewriter,
139 VectorTransferOpInterface xferOp) {
140 if (xferOp.getMask())
141 return rewriter.notifyMatchFailure(xferOp,
142 "Masked transfer is not supported");
143
144 auto srcTy = dyn_cast<MemRefType>(xferOp.getShapedType());
145 if (!srcTy)
146 return rewriter.notifyMatchFailure(xferOp, "Expects memref source");
147 if (isa<VectorType>(srcTy.getElementType()))
148 return rewriter.notifyMatchFailure(xferOp,
149 "Expects a memref of scalar elements");
150
151 // Validate further transfer op semantics.
152 SmallVector<int64_t> strides;
153 int64_t offset;
154 if (failed(srcTy.getStridesAndOffset(strides, offset)))
155 return rewriter.notifyMatchFailure(xferOp,
156 "The memref strides cannot be inferred");
157 if (strides.empty())
158 return rewriter.notifyMatchFailure(xferOp, "0D memref is not supported");
159 if (strides.back() != 1)
160 return rewriter.notifyMatchFailure(
161 xferOp, "Buffer must be contiguous in the innermost dimension");
162
163 VectorType vecTy = xferOp.getVectorType();
164 unsigned vecRank = vecTy.getRank();
165 if (vecRank == 0)
166 return rewriter.notifyMatchFailure(xferOp, "0D vectors are not supported");
167
168 AffineMap map = xferOp.getPermutationMap();
169 if (!map.isProjectedPermutation(/*allowZeroInResults=*/true))
170 return rewriter.notifyMatchFailure(xferOp, "Unsupported permutation map");
171 // The transfer is built from a run of innermost source dimensions, as wide as
172 // the vector, with one dimension of slack for a 1D transfer: dropping the
173 // broadcast results of a map like (d1, 0) leaves a rank-1 transfer that still
174 // reads d1, and the scattered path reads along any dimension anyway.
175 unsigned numInputDims = map.getNumInputs();
176 unsigned numAccessibleDims = std::max(vecRank, 2u);
177 unsigned firstAccessibleDim =
178 numInputDims > numAccessibleDims ? numInputDims - numAccessibleDims : 0;
179 for (AffineExpr expr : map.getResults().take_back(vecRank)) {
180 // A broadcast result names no source dimension, so it constrains nothing.
181 auto dim = dyn_cast<AffineDimExpr>(expr);
182 if (!dim)
183 continue;
184 if (dim.getPosition() < firstAccessibleDim)
185 return rewriter.notifyMatchFailure(
186 xferOp, "Only the innermost dimensions can be accessed");
187 }
188
189 return success();
190}
191
192// Returns the memref strides in vector-dimension order: the stride at vector
193// dimension `v` is the stride of the memref dimension the permutation map has
194// `v` read, so that stepping one element along `v` steps that far in memory.
195//
196// Example:
197// For a memref of rank 3 with strides `[s0, s1, s2]` read into a rank-2
198// vector through `(d0, d1, d2) -> (d2, d1)`, the result is `[s2, s1]`.
199//
200static SmallVector<Value> getStridesInVectorOrder(AffineMap permMap,
201 ArrayRef<Value> strides) {
202 return llvm::map_to_vector(permMap.getResults(), [&](AffineExpr expr) {
203 return strides[cast<AffineDimExpr>(expr).getPosition()];
204 });
205}
206
207// Computes memory strides and a memref offset for vector transfer operations,
208// handling both static and dynamic memrefs while applying permutation
209// transformations for XeGPU lowering.
210template <
211 typename OpType,
212 typename = std::enable_if_t<llvm::is_one_of<
213 std::decay_t<OpType>, vector::TransferReadOp, vector::TransferWriteOp,
214 vector::GatherOp, vector::ScatterOp>::value>>
215static std::pair<SmallVector<Value>, Value>
216computeMemrefMeta(OpType xferOp, PatternRewriter &rewriter) {
217 SmallVector<Value> strides;
218 Value baseMemref = xferOp.getBase();
219 MemRefType memrefType = dyn_cast<MemRefType>(baseMemref.getType());
220
221 Location loc = xferOp.getLoc();
222 Value offsetVal = nullptr;
223 if (memrefType.hasStaticShape()) {
224 int64_t offset;
225 SmallVector<int64_t> intStrides;
226 if (failed(memrefType.getStridesAndOffset(intStrides, offset)))
227 return {{}, offsetVal};
228 bool hasDynamicStrides = llvm::any_of(intStrides, [](int64_t strideVal) {
229 return ShapedType::isDynamic(strideVal);
230 });
231
232 if (!hasDynamicStrides)
233 for (int64_t s : intStrides)
234 strides.push_back(arith::ConstantIndexOp::create(rewriter, loc, s));
235
236 if (!ShapedType::isDynamic(offset))
237 offsetVal = arith::ConstantIndexOp::create(rewriter, loc, offset);
238 }
239
240 if (strides.empty() || !offsetVal) {
241 // For dynamic shape memref, use memref.extract_strided_metadata to get
242 // stride values
243 unsigned rank = memrefType.getRank();
244 Type indexType = rewriter.getIndexType();
245
246 // Result types: [base_memref, offset, stride0, stride1, ..., strideN-1,
247 // size0, size1, ..., sizeN-1]
248 SmallVector<Type> resultTypes;
249 resultTypes.push_back(MemRefType::get(
250 {}, memrefType.getElementType())); // base memref (unranked)
251 resultTypes.push_back(indexType); // offset
252
253 for (unsigned i = 0; i < rank; ++i)
254 resultTypes.push_back(indexType); // strides
255
256 for (unsigned i = 0; i < rank; ++i)
257 resultTypes.push_back(indexType); // sizes
258
259 auto meta = memref::ExtractStridedMetadataOp::create(
260 rewriter, loc, resultTypes, baseMemref);
261
262 if (strides.empty())
263 strides.append(meta.getStrides().begin(), meta.getStrides().end());
264
265 if (!offsetVal)
266 offsetVal = meta.getOffset();
267 }
268
269 // Strides are returned in original memref order; permutation is applied in
270 // computeOffsets only where offsets are indexed in vector order.
271 return {strides, offsetVal};
272}
273
274// Adds the transfer's indices to `baseOffset`, the memref's own element offset:
275// the element at `indices` sits at `baseOffset + sum(indices[d] * strides[d])`.
276static Value computeBaseOffset(VectorTransferOpInterface xferOp,
277 PatternRewriter &rewriter,
278 ArrayRef<Value> strides, Value baseOffset) {
279 Location loc = xferOp.getLoc();
280 for (auto [index, stride] : llvm::zip_equal(xferOp.getIndices(), strides)) {
281 Value contrib = arith::MulIOp::create(rewriter, loc, index, stride);
282 baseOffset = arith::AddIOp::create(rewriter, loc, baseOffset, contrib);
283 }
284 return baseOffset;
285}
286
287// This function compute the vectors of localOffsets for scattered load/stores.
288// It is used in the lowering of vector.transfer_read/write to
289// load_gather/store_scatter Example:
290// %0 = vector.transfer_read %expand_shape[%block_id_y, %c0, %c0, %c0, %c0],
291// %cst {in_bounds = [true, true, true, true]}>} :
292// memref<8x4x2x6x32xbf16>, vector<4x2x6x32xbf16>
293//
294// %6 = vector.step: vector<4xindex>
295// %7 = vector.step: vector<2xindex>
296// %8 = vector.step: vector<6xindex>
297// %9 = vector.step: vector<32xindex>
298// %10 = arith.mul %6, 384
299// %11 = arith.mul %7, 192
300// %12 = arith.mul %8, 32
301// %13 = arith.mul %9, 1
302// %14 = vector.shape_cast %10: vector<4xindex> -> vector<4x1x1x1xbf16>
303// %15 = vector.shape_cast %11: vector<2xindex> -> vector<1x2x1x1xbf16>
304// %16 = vector.shape_cast %12: vector<6xindex> -> vector<1x1x6x1xbf16>
305// %17 = vector.shape_cast %13: vector<32xindex> -> vector<1x1x1x32xbf16>
306// %18 = vector.broadcast %14: vector<4x1x1x1xbf16> -> vector<4x2x6x32xindex>
307// %19 = vector.broadcast %15: vector<1x2x1x1xbf16> -> vector<4x2x6x32xindex>
308// %20 = vector.broadcast %16: vector<1x1x6x1xbf16> -> vector<4x2x6x32xindex>
309// %21 = vector.broadcast %17: vector<1x1x1x32xbf16> -> vector<4x2x6x32xindex>
310// %22 = arith.add %18, %19
311// %23 = arith.add %20, %21
312// %local_offsets = arith.add %22, %23
313// %orig_offset = %block_id_y * 4x2x6x32 // consider using affine map
314// %offsets = memref_offset + orig_offset + local_offsets
315static Value computeOffsets(VectorTransferOpInterface xferOp,
316 PatternRewriter &rewriter, ArrayRef<Value> strides,
317 Value baseOffset) {
318 Location loc = xferOp.getLoc();
319 VectorType vectorType = xferOp.getVectorType();
320 ArrayRef<int64_t> vectorShape = vectorType.getShape();
321
322 // Create vector.step operations for each dimension
323 SmallVector<Value> stepVectors;
324 llvm::map_to_vector(vectorShape, [&](int64_t dim) {
325 auto stepType = VectorType::get({dim}, rewriter.getIndexType());
326 auto stepOp = vector::StepOp::create(rewriter, loc, stepType);
327 stepVectors.push_back(stepOp);
328 return stepOp;
329 });
330
331 SmallVector<Value> vectorStrides =
332 getStridesInVectorOrder(xferOp.getPermutationMap(), strides);
333
334 // Multiply step vectors by corresponding strides
335 size_t vectorRank = vectorShape.size();
336 SmallVector<Value> strideMultiplied;
337 for (size_t i = 0; i < vectorRank; ++i) {
338 Value strideValue = vectorStrides[i];
339 auto mulType = dyn_cast<VectorType>(stepVectors[i].getType());
340 auto bcastOp =
341 vector::BroadcastOp::create(rewriter, loc, mulType, strideValue);
342 auto mulOp = arith::MulIOp::create(rewriter, loc, stepVectors[i], bcastOp);
343 strideMultiplied.push_back(mulOp);
344 }
345
346 // Shape cast each multiplied vector to add singleton dimensions
347 SmallVector<Value> shapeCasted;
348 for (size_t i = 0; i < vectorRank; ++i) {
349 SmallVector<int64_t> newShape(vectorRank, 1);
350 newShape[i] = vectorShape[i];
351 auto newType = VectorType::get(newShape, rewriter.getIndexType());
352 auto castOp = vector::ShapeCastOp::create(rewriter, loc, newType,
353 strideMultiplied[i]);
354 shapeCasted.push_back(castOp);
355 }
356
357 // Broadcast each shape-casted vector to full vector shape
358 SmallVector<Value> broadcasted;
359 auto fullIndexVectorType =
360 VectorType::get(vectorShape, rewriter.getIndexType());
361 for (Value shapeCastVal : shapeCasted) {
362 auto broadcastOp = vector::BroadcastOp::create(
363 rewriter, loc, fullIndexVectorType, shapeCastVal);
364 broadcasted.push_back(broadcastOp);
365 }
366
367 // Add all broadcasted vectors together to compute local offsets
368 Value localOffsets = broadcasted[0];
369 for (size_t i = 1; i < broadcasted.size(); ++i)
370 localOffsets =
371 arith::AddIOp::create(rewriter, loc, localOffsets, broadcasted[i]);
372
373 // Broadcast base offset to match vector shape
374 baseOffset = computeBaseOffset(xferOp, rewriter, strides, baseOffset);
375 Value bcastBase = vector::BroadcastOp::create(
376 rewriter, loc, fullIndexVectorType, baseOffset);
377 localOffsets = arith::AddIOp::create(rewriter, loc, bcastBase, localOffsets);
378 return localOffsets;
379}
380
381// Returns the size of memref dimension `dim` as a value.
382static Value getMemrefDimSize(VectorTransferOpInterface xferOp, unsigned dim,
383 PatternRewriter &rewriter) {
384 Location loc = xferOp.getLoc();
385 auto memrefTy = cast<MemRefType>(xferOp.getShapedType());
386 if (memrefTy.isDynamicDim(dim))
387 return memref::DimOp::create(rewriter, loc, xferOp.getBase(), dim)
388 .getResult();
389 return arith::ConstantIndexOp::create(rewriter, loc, memrefTy.getDimSize(dim))
390 .getResult();
391}
392
393// Builds the mask a scattered access needs to stay within the source bounds.
394//
395// The element at vector position `i` along vector dimension `v` accesses memref
396// dimension `d` (the one `v` maps to) at `indices[d] + i`, so it is in bounds
397// iff `i < dim(d) - indices[d]`. Dimensions the transfer declares in-bounds are
398// skipped; non-vector dimensions are always in bounds by the op's contract. The
399// per-dimension masks are spread over the full vector shape and combined, the
400// same way `computeOffsets` spreads the per-dimension offsets.
401//
402// Example, for a `vector<8xf32>` read of a `memref<?xf32>` at `%off`:
403// %dim = memref.dim %src, %c0
404// %limit = arith.subi %dim, %off
405// %step = vector.step : vector<8xindex>
406// %bcast = vector.broadcast %limit : index to vector<8xindex>
407// %mask = arith.cmpi slt, %step, %bcast : vector<8xindex>
408static Value computeInBoundsMask(VectorTransferOpInterface xferOp,
409 PatternRewriter &rewriter) {
410 Location loc = xferOp.getLoc();
411 ArrayRef<int64_t> vectorShape = xferOp.getVectorType().getShape();
412 auto maskType = VectorType::get(vectorShape, rewriter.getI1Type());
413 AffineMap map = xferOp.getPermutationMap();
414 OperandRange indices = xferOp.getIndices();
415
416 Value mask;
417 for (unsigned v = 0, e = vectorShape.size(); v < e; ++v) {
418 if (xferOp.isDimInBounds(v))
419 continue;
420 unsigned d = cast<AffineDimExpr>(map.getResult(v)).getPosition();
421 Value bound = getMemrefDimSize(xferOp, d, rewriter);
422 // A negative limit masks the dimension off entirely, which is what a
423 // starting index past the end of the dimension should do.
424 Value limit = arith::SubIOp::create(rewriter, loc, bound, indices[d]);
425 auto stepType = VectorType::get({vectorShape[v]}, rewriter.getIndexType());
426 Value step = vector::StepOp::create(rewriter, loc, stepType);
427 Value limitVec =
428 vector::BroadcastOp::create(rewriter, loc, stepType, limit);
429 Value dimMask = arith::CmpIOp::create(
430 rewriter, loc, arith::CmpIPredicate::slt, step, limitVec);
431 if (vectorShape.size() > 1) {
432 SmallVector<int64_t> expandedShape(vectorShape.size(), 1);
433 expandedShape[v] = vectorShape[v];
434 dimMask = vector::ShapeCastOp::create(
435 rewriter, loc, VectorType::get(expandedShape, rewriter.getI1Type()),
436 dimMask);
437 dimMask = vector::BroadcastOp::create(rewriter, loc, maskType, dimMask);
438 }
439 mask = mask
440 ? arith::AndIOp::create(rewriter, loc, mask, dimMask).getResult()
441 : dimMask;
442 }
443 if (mask)
444 return mask;
445 return vector::ConstantMaskOp::create(rewriter, loc, maskType, vectorShape);
446}
447
448// Builds that mask as a scalar `i1`, for a transfer of a single element. The
449// element sits at `indices`, so `i < dim(d) - indices[d]` at `i == 0` reduces
450// to `indices[d] < dim(d)`, which needs neither the limit subtraction nor the
451// step vector. The dimensions checked are the same.
452//
453// Example, for a `vector<1xf32>` read of a `memref<?xf32>` at `%off`:
454// %dim = memref.dim %src, %c0
455// %mask = arith.cmpi slt, %off, %dim : index
456static Value computeUnitInBoundsMask(VectorTransferOpInterface xferOp,
457 PatternRewriter &rewriter) {
458 Location loc = xferOp.getLoc();
459 AffineMap map = xferOp.getPermutationMap();
460 OperandRange indices = xferOp.getIndices();
461
462 Value mask;
463 for (unsigned v = 0, e = xferOp.getVectorType().getRank(); v < e; ++v) {
464 if (xferOp.isDimInBounds(v))
465 continue;
466 unsigned d = cast<AffineDimExpr>(map.getResult(v)).getPosition();
467 Value bound = getMemrefDimSize(xferOp, d, rewriter);
468 Value dimMask = arith::CmpIOp::create(
469 rewriter, loc, arith::CmpIPredicate::slt, indices[d], bound);
470 mask = mask
471 ? arith::AndIOp::create(rewriter, loc, mask, dimMask).getResult()
472 : dimMask;
473 }
474 if (mask)
475 return mask;
476 return arith::ConstantOp::create(rewriter, loc, rewriter.getBoolAttr(true));
477}
478
479// Compute the element-wise offsets for vector.gather or vector.scatter ops.
480//
481// This function linearizes the base offsets of the gather/scatter operation
482// and combines them with the per-element indices to produce a final vector of
483// memory offsets.
484template <
485 typename OpType,
486 typename = std::enable_if_t<llvm::is_one_of<
487 std::decay_t<OpType>, vector::GatherOp, vector::ScatterOp>::value>>
488static Value computeOffsets(PatternRewriter &rewriter, OpType gatScatOp,
489 ArrayRef<Value> strides, Value baseOffset) {
490 Location loc = gatScatOp.getLoc();
491 SmallVector<Value> offsets = gatScatOp.getOffsets();
492 for (size_t i = 0; i < offsets.size(); ++i) {
493 Value offsetContrib =
494 arith::MulIOp::create(rewriter, loc, offsets[i], strides[i]);
495 baseOffset =
496 arith::AddIOp::create(rewriter, loc, baseOffset, offsetContrib);
497 }
498 Value indices = gatScatOp.getIndices();
499 VectorType vecType = cast<VectorType>(indices.getType());
500
501 Value strideVector =
502 vector::BroadcastOp::create(rewriter, loc, vecType, strides.back())
503 .getResult();
504 Value stridedIndices =
505 arith::MulIOp::create(rewriter, loc, strideVector, indices).getResult();
506
507 Value baseVector =
508 vector::BroadcastOp::create(
509 rewriter, loc,
510 VectorType::get(vecType.getShape(), rewriter.getIndexType()),
511 baseOffset)
512 .getResult();
513 return arith::AddIOp::create(rewriter, loc, baseVector, stridedIndices)
514 .getResult();
515}
516
517// Collapses shapes of a nD memref to the target rank while applying offsets for
518// the collapsed dimensions. Returns the new memref value and the remaining
519// offsets for the last targetRank dimensions. For example:
520// input: %memref = memref<2x4x8x32xf32>, offsets=[%i0, %i1, %i2, %i3],
521// output: %memref[%i0, %i1, 0, 0] -> memref<8x32xf32>, offsets: [%i2, %i3]
522static std::pair<Value, SmallVector<OpFoldResult>>
523convertMemrefAndOffsetsToTargetRank(PatternRewriter &rewriter, Location loc,
526 int64_t targetRank) {
527 auto memrefType = cast<MemRefType>(memref.getType());
528 unsigned rank = memrefType.getRank();
529
530 if (rank <= targetRank)
531 return {memref, offsets};
532
533 int64_t numCombinedDims = rank - targetRank;
534 SmallVector<OpFoldResult> subviewOffsets;
535 SmallVector<OpFoldResult> subviewSizes;
536 SmallVector<OpFoldResult> subviewStrides;
537
538 // For the combined dimensions: use the provided offsets, size=1, stride=1
539 for (unsigned i = 0; i < numCombinedDims; ++i) {
540 subviewOffsets.push_back(offsets[i]);
541 subviewSizes.push_back(rewriter.getI64IntegerAttr(1));
542 subviewStrides.push_back(rewriter.getI64IntegerAttr(1));
543 }
544
545 // For the last targetRank dimensions: offset=0, use full size, stride=1
546 SmallVector<int64_t> resultShape;
547 auto originalShape = memrefType.getShape();
548 auto meta = memref::ExtractStridedMetadataOp::create(rewriter, loc, memref);
549 for (unsigned i = numCombinedDims; i < rank; ++i) {
550 subviewOffsets.push_back(rewriter.getI64IntegerAttr(0));
551 if (ShapedType::isDynamic(originalShape[i])) {
552 subviewSizes.push_back(meta.getSizes()[i]);
553 resultShape.push_back(ShapedType::kDynamic);
554 } else {
555 subviewSizes.push_back(rewriter.getI64IntegerAttr(originalShape[i]));
556 resultShape.push_back(originalShape[i]);
557 }
558 subviewStrides.push_back(rewriter.getI64IntegerAttr(1));
559 }
560
561 auto resultType = memref::SubViewOp::inferRankReducedResultType(
562 resultShape, memrefType, subviewOffsets, subviewSizes, subviewStrides);
563 auto subviewOp =
564 memref::SubViewOp::create(rewriter, loc, resultType, memref,
565 subviewOffsets, subviewSizes, subviewStrides);
566
567 // Return the remaining offsets for the last targetRank dimensions
568 SmallVector<OpFoldResult> newOffsets(offsets.begin() + numCombinedDims,
569 offsets.end());
570 return {subviewOp.getResult(), newOffsets};
571}
572
573template <
574 typename OpType,
575 typename = std::enable_if_t<llvm::is_one_of<
576 std::decay_t<OpType>, vector::TransferReadOp, vector::TransferWriteOp,
577 vector::GatherOp, vector::ScatterOp>::value>>
578// Convert memref to i64 base pointer
579static Value memrefToIndexPtr(OpType xferOp, PatternRewriter &rewriter) {
580 Location loc = xferOp.getLoc();
581 auto indexPtr = memref::ExtractAlignedPointerAsIndexOp::create(
582 rewriter, loc, xferOp.getBase())
583 .getResult();
584 return arith::IndexCastOp::create(rewriter, loc, rewriter.getI64Type(),
585 indexPtr)
586 .getResult();
587}
588
589// Returns true if every use of `vec` extracts a scalar element from it. An
590// unused value does not qualify.
591static bool isUsedAsScalar(Value vec) {
592 if (vec.use_empty())
593 return false;
594 return llvm::all_of(vec.getUsers(), [](Operation *user) {
595 auto extractOp = dyn_cast<vector::ExtractOp>(user);
596 return extractOp && !isa<VectorType>(extractOp.getResult().getType());
597 });
598}
599
600// Lowers a transfer of a single element to a scalar `xegpu.load`.
601//
602// The transfer touches one location, which its own indices name, so the load
603// takes a scalar offset and a scalar mask - no descriptor, no lane vectors -
604// and the result is broadcast back to the transfer's unit-size vector type.
605//
606// %off = arith.addi %base, %contrib : index
607// %inb = arith.cmpi slt, %idx, %dim : index
608// %val = xegpu.load %src[%off], %inb : i64, index, i1 -> f32
609// %vec = vector.broadcast %val : f32 to vector<1xf32>
610static LogicalResult lowerToScalarLoadOp(vector::TransferReadOp readOp,
611 PatternRewriter &rewriter) {
612 Location loc = readOp.getLoc();
613 VectorType vectorType = readOp.getVectorType();
614 if (!isa<MemRefType>(readOp.getShapedType()))
615 return rewriter.notifyMatchFailure(readOp, "Expected memref source");
616
617 auto meta = computeMemrefMeta(readOp, rewriter);
618 if (meta.first.empty())
619 return rewriter.notifyMatchFailure(readOp, "Failed to compute strides");
620
621 Value offset = computeBaseOffset(readOp, rewriter, meta.first, meta.second);
622 Value flatMemref = memrefToIndexPtr(readOp, rewriter);
623
624 Value mask = computeUnitInBoundsMask(readOp, rewriter);
625 auto loadOp = xegpu::LoadGatherOp::create(
626 rewriter, loc, vectorType.getElementType(), flatMemref, offset, mask,
627 /*l1_hint=*/xegpu::CachePolicyAttr{},
628 /*l2_hint=*/xegpu::CachePolicyAttr{},
629 /*l3_hint=*/xegpu::CachePolicyAttr{},
630 /*layout=*/nullptr, /*contiguity=*/nullptr);
631
632 // A masked-off xegpu.load is unspecified, so the padding has to be applied
633 // explicitly, as in lowerToScatteredLoadOp. A poison padding is "don't care".
634 Value scalar = loadOp.getResult();
635 if (readOp.hasOutOfBoundsDim() &&
636 !readOp.getPadding().getDefiningOp<ub::PoisonOp>())
637 scalar = arith::SelectOp::create(rewriter, loc, mask, scalar,
638 readOp.getPadding());
639
640 rewriter.replaceOpWithNewOp<vector::BroadcastOp>(readOp, vectorType, scalar);
641 return success();
642}
643
644static LogicalResult lowerToScatteredLoadOp(vector::TransferReadOp readOp,
645 PatternRewriter &rewriter) {
646
647 Location loc = readOp.getLoc();
648 VectorType vectorType = readOp.getVectorType();
649 auto memrefType = dyn_cast<MemRefType>(readOp.getShapedType());
650 if (!memrefType)
651 return rewriter.notifyMatchFailure(readOp, "Expected memref source");
652
653 auto meta = computeMemrefMeta(readOp, rewriter);
654 if (meta.first.empty())
655 return rewriter.notifyMatchFailure(readOp, "Failed to compute strides");
656
657 Value localOffsets =
658 computeOffsets(readOp, rewriter, meta.first, meta.second);
659
660 Value flatMemref = memrefToIndexPtr(readOp, rewriter);
661
662 Value mask = computeInBoundsMask(readOp, rewriter);
663 auto gatherOp = xegpu::LoadGatherOp::create(
664 rewriter, loc, vectorType, flatMemref, localOffsets, mask,
665 /*l1_hint=*/xegpu::CachePolicyAttr{},
666 /*l2_hint=*/xegpu::CachePolicyAttr{},
667 /*l3_hint=*/xegpu::CachePolicyAttr{},
668 /*layout=*/nullptr, /*contiguity=*/nullptr);
669
670 // The masked-off lanes of an xegpu.load are unspecified, so the transfer's
671 // padding has to be applied explicitly. A poison padding leaves them "don't
672 // care", so the select can be skipped.
673 Value result = gatherOp.getResult();
674 if (readOp.hasOutOfBoundsDim() &&
675 !readOp.getPadding().getDefiningOp<ub::PoisonOp>()) {
676 Value padding = vector::BroadcastOp::create(rewriter, loc, vectorType,
677 readOp.getPadding());
678 result = arith::SelectOp::create(rewriter, loc, mask, result, padding);
679 }
680
681 rewriter.replaceOp(readOp, result);
682 return success();
683}
684
685// An nd block load fetches a 2D block, which a 1D transfer does not ask for -
686// but a leading unit dimension gives it one:
687//
688// %v = vector.transfer_read %src[%i0, %i1] : memref<32x64xf32>, vector<8xf32>
689// becomes
690// %b = vector.transfer_read %src[%i0, %i1]
691// : memref<32x64xf32>, vector<1x8xf32>
692// %v = vector.shape_cast %b : vector<1x8xf32> to vector<8xf32>
693//
694// Only a transfer running along the innermost source dimension qualifies. That
695// dimension is contiguous, which transferPreconditions has already checked, so
696// the <1xN> block it asks for is one the hardware can fetch. A transfer along
697// any other dimension would need the unit dimension on the other side, and a
698// <Nx1> block is not a shape the layouts downstream can distribute - the
699// scattered path is the one that fits it.
700static LogicalResult lowerToUnitDimRead(vector::TransferReadOp readOp,
701 PatternRewriter &rewriter) {
702 VectorType vecTy = readOp.getVectorType();
703 AffineMap map = readOp.getPermutationMap();
704 int64_t numInputs = map.getNumInputs();
705 int64_t pos = cast<AffineDimExpr>(map.getResult(0)).getPosition();
706 if (numInputs < 2 || pos != numInputs - 1)
707 return rewriter.notifyMatchFailure(
708 readOp, "1D transfer does not run along the innermost dimension");
709
710 Value block = vector::TransferReadOp::create(
711 rewriter, readOp.getLoc(),
712 VectorType::get({1, vecTy.getDimSize(0)}, vecTy.getElementType()),
713 readOp.getBase(), readOp.getIndices(),
714 AffineMapAttr::get(
715 AffineMap::getMinorIdentityMap(numInputs, 2, map.getContext())),
716 readOp.getPadding(), /*mask=*/Value(),
717 rewriter.getBoolArrayAttr({true, readOp.isDimInBounds(0)}));
718
719 rewriter.replaceOpWithNewOp<vector::ShapeCastOp>(readOp, vecTy, block);
720 return success();
721}
722
723// Lowers a transfer whose permutation map broadcasts some of the vector
724// dimensions into a transfer of just the data in memory - the map with its
725// broadcast results dropped - plus the vector ops that expand it back:
726//
727// %r = vector.transfer_read %src[%i0, %i1, %i2], %pad
728// {permutation_map = affine_map<(d0, d1, d2) -> (d2, 0)>,
729// in_bounds = [false, false]} : memref<256x32x512xf16>, vector<64x64xf16>
730// becomes
731// %v = vector.transfer_read %src[%i0, %i1, %i2], %pad
732// {permutation_map = affine_map<(d0, d1, d2) -> (d2)>,
733// in_bounds = [false]} : memref<256x32x512xf16>, vector<64xf16>
734// %c = vector.shape_cast %v : vector<64xf16> to vector<1x64xf16>
735// %t = vector.transpose %c, [1, 0] : vector<1x64xf16> to vector<64x1xf16>
736// %r = vector.broadcast %t : vector<64x1xf16> to vector<64x64xf16>
737//
738// A broadcast result names no source dimension, so only the dimensions the map
739// does name hold data; dropping the rest leaves the transfer that reads it, at
740// the extents and bounds checks those dimensions had. The dropped results come
741// back as unit dimensions, which the transpose moves to the positions those
742// results held, and vector.broadcast stretches to their final extents.
743static LogicalResult lowerBroadcastRead(vector::TransferReadOp readOp,
744 PatternRewriter &rewriter) {
745 Location loc = readOp.getLoc();
746 VectorType vecTy = readOp.getVectorType();
747 ArrayRef<int64_t> vecShape = vecTy.getShape();
748 AffineMap map = readOp.getPermutationMap();
749 int64_t vecRank = vecTy.getRank();
750
751 SmallVector<int64_t> dataPos, bcastPos;
752 for (int64_t i = 0; i < vecRank; ++i) {
753 if (isa<AffineDimExpr>(map.getResult(i)))
754 dataPos.push_back(i);
755 else
756 bcastPos.push_back(i);
757 }
758
759 SmallVector<AffineExpr> readExprs;
760 SmallVector<int64_t> readShape;
761 SmallVector<bool> readInBounds;
762 for (int64_t i : dataPos) {
763 readExprs.push_back(map.getResult(i));
764 readShape.push_back(vecShape[i]);
765 readInBounds.push_back(readOp.isDimInBounds(i));
766 }
767 // An all-broadcast map leaves no data dimension to read. Read one element of
768 // the innermost source dimension instead - a 0D transfer is not supported,
769 // and one element is all the data such a transfer holds.
770 if (readExprs.empty()) {
771 readExprs.push_back(rewriter.getAffineDimExpr(map.getNumInputs() - 1));
772 readShape.push_back(1);
773 readInBounds.push_back(true);
774 }
775
776 auto readTy = VectorType::get(readShape, vecTy.getElementType());
777 Value read = vector::TransferReadOp::create(
778 rewriter, loc, readTy, readOp.getBase(), readOp.getIndices(),
779 AffineMapAttr::get(
780 AffineMap::get(map.getNumInputs(), 0, readExprs, map.getContext())),
781 readOp.getPadding(), /*mask=*/Value(),
782 rewriter.getBoolArrayAttr(readInBounds));
783
784 // Grow the transfer back to the requested rank, one unit dimension per
785 // dropped result. The shape_cast adds them innermost, where they leave the
786 // dimension the data came in on innermost too, and the transpose moves them
787 // to the positions their results held. Only unit dimensions move, so the
788 // transpose permutes dimensions without shuffling elements between them.
789 SmallVector<int64_t> order(bcastPos);
790 llvm::append_range(order, dataPos);
791 SmallVector<int64_t> castShape(bcastPos.size(), 1);
792 for (int64_t i : dataPos)
793 castShape.push_back(vecShape[i]);
794 auto castTy = VectorType::get(castShape, vecTy.getElementType());
795 Value cast = castTy == readTy
796 ? read
797 : vector::ShapeCastOp::create(rewriter, loc, castTy, read);
798
799 if (!llvm::is_sorted(order)) {
800 SmallVector<int64_t> perm(vecRank);
801 for (int64_t i = 0; i < vecRank; ++i)
802 perm[order[i]] = i;
803 cast = vector::TransposeOp::create(rewriter, loc, cast, perm);
804 }
805
806 if (cast.getType() == vecTy)
807 rewriter.replaceOp(readOp, cast);
808 else
809 rewriter.replaceOpWithNewOp<vector::BroadcastOp>(readOp, vecTy, cast);
810 return success();
811}
812
813static LogicalResult lowerToScatteredStoreOp(vector::TransferWriteOp writeOp,
814 PatternRewriter &rewriter) {
815
816 Location loc = writeOp.getLoc();
817 auto memrefType = dyn_cast<MemRefType>(writeOp.getShapedType());
818 if (!memrefType)
819 return rewriter.notifyMatchFailure(writeOp, "Expected memref source");
820
821 auto meta = computeMemrefMeta(writeOp, rewriter);
822 if (meta.first.empty())
823 return rewriter.notifyMatchFailure(writeOp, "Failed to compute strides");
824
825 Value localOffsets =
826 computeOffsets(writeOp, rewriter, meta.first, meta.second);
827
828 Value flatMemref = memrefToIndexPtr(writeOp, rewriter);
829
830 // Out-of-bounds elements are simply not stored, so the mask is all this
831 // needs - no counterpart to the read's padding.
832 Value mask = computeInBoundsMask(writeOp, rewriter);
833 xegpu::StoreScatterOp::create(rewriter, loc, writeOp.getVector(), flatMemref,
834 localOffsets, mask,
835 /*l1_hint=*/xegpu::CachePolicyAttr{},
836 /*l2_hint=*/xegpu::CachePolicyAttr{},
837 /*l3_hint=*/xegpu::CachePolicyAttr{},
838 /*layout=*/nullptr, /*contiguity=*/nullptr);
839 rewriter.eraseOp(writeOp);
840 return success();
841}
842
843// Rewrites a transfer_read whose permutation map broadcasts some vector
844// dimensions into a transfer of just the data in memory plus the vector ops
845// that expand it back (see lowerBroadcastRead), so that the conversion
846// patterns only see maps without broadcast results.
847struct LegalizeBroadcastTransferRead
848 : public OpRewritePattern<vector::TransferReadOp> {
849 using Base::Base;
850
851 LogicalResult matchAndRewrite(vector::TransferReadOp readOp,
852 PatternRewriter &rewriter) const override {
853 if (readOp.getPermutationMap().getBroadcastDims().empty())
854 return rewriter.notifyMatchFailure(readOp, "No broadcast dimension");
855 if (failed(transferPreconditions(rewriter, readOp)))
856 return failure();
857 return lowerBroadcastRead(readOp, rewriter);
858 }
859};
860
861// Returns true if the target's 2D block load can fetch the `tileShape` tile of
862// a non-SLM transfer_read: a scalar int/float element type, out-of-bounds
863// padding that matches load_nd's implicit zero padding, and a tile shape the
864// block load can access.
865static bool is2DBlockLoadable(vector::TransferReadOp readOp,
866 ArrayRef<int64_t> tileShape,
867 bool hasTranspose = false) {
868 auto readMemTy = cast<MemRefType>(readOp.getShapedType());
869 if (!readMemTy.getElementType().isIntOrFloat())
870 return false;
871 if (readOp.hasOutOfBoundsDim() && !isZeroOrPoisonPadding(readOp.getPadding()))
872 return false;
873 const xegpu::uArch::uArch *uArch =
874 xegpu::uArch::getUArch(xegpu::getChipStr(readOp).value_or(""));
875 return isSupportedBlockShape(
877 readOp.getVectorType().getElementType(), hasTranspose);
878}
879
880// Gives a 1D transfer_read the leading unit dimension an nd block load needs
881// (see lowerToUnitDimRead). Only worth it where the target's 2D block load can
882// access the resulting <1xN> tile; otherwise TransferReadLowering takes the 1D
883// vector as is on the scattered path.
884struct PromoteTransferReadToUnitDim
885 : public OpRewritePattern<vector::TransferReadOp> {
886 using Base::Base;
887
888 LogicalResult matchAndRewrite(vector::TransferReadOp readOp,
889 PatternRewriter &rewriter) const override {
890 VectorType vecTy = readOp.getVectorType();
891 if (vecTy.getRank() != 1)
892 return rewriter.notifyMatchFailure(readOp, "Not a 1D transfer");
893 // A broadcast map is left to LegalizeBroadcastTransferRead.
894 if (!readOp.getPermutationMap().getBroadcastDims().empty())
895 return rewriter.notifyMatchFailure(readOp, "Broadcast permutation map");
896 if (failed(transferPreconditions(rewriter, readOp)))
897 return failure();
898
899 // SLM reads lower to load_matrix, which takes the 1D vector as is.
900 if (xegpu::XeGPUDialect::isSharedMemory(
901 cast<MemRefType>(readOp.getShapedType())))
902 return rewriter.notifyMatchFailure(readOp, "SLM transfer");
903 // A single element only extracted to a scalar becomes a scalar load.
904 if (vecTy.getNumElements() == 1 && isUsedAsScalar(readOp.getResult()))
905 return rewriter.notifyMatchFailure(readOp, "Used as a scalar");
906 if (!is2DBlockLoadable(readOp, {1, vecTy.getDimSize(0)}))
907 return rewriter.notifyMatchFailure(
908 readOp, "<1xN> tile is not accessible by a 2D block load");
909
910 return lowerToUnitDimRead(readOp, rewriter);
911 }
912};
913
914struct TransferReadLowering : public OpRewritePattern<vector::TransferReadOp> {
915 using Base::Base;
916
917 LogicalResult matchAndRewrite(vector::TransferReadOp readOp,
918 PatternRewriter &rewriter) const override {
919 Location loc = readOp.getLoc();
920
921 if (failed(transferPreconditions(rewriter, readOp)))
922 return failure();
923
924 // Broadcast maps are rewritten away by the transfer_read legalization that
925 // runs ahead of this pattern.
926 if (!readOp.getPermutationMap().getBroadcastDims().empty())
927 return rewriter.notifyMatchFailure(
928 readOp, "Broadcast permutation map must be legalized first");
929
930 auto readMemTy = cast<MemRefType>(readOp.getShapedType());
931 VectorType loadedVecTy = readOp.getVectorType();
932 bool isOutOfBounds = readOp.hasOutOfBoundsDim();
933 // Check if the memref has address space 3 (shared local memory)
934 bool isSharedMemory = xegpu::XeGPUDialect::isSharedMemory(readMemTy);
935 // Handle the SLM case.
936 if (isSharedMemory) {
937 // load_matrix supports 1D and 2D loads from SLM.
938 if (loadedVecTy.getRank() != 1 && loadedVecTy.getRank() != 2)
939 return rewriter.notifyMatchFailure(
940 readOp, "Only 1D and 2D vector loads are supported for SLM");
941 AffineMap readMap = readOp.getPermutationMap();
942 if (!readMap.isMinorIdentity())
943 return rewriter.notifyMatchFailure(
944 readOp,
945 "Non identity transposition is not supported for SLM loads.");
946 // Out of bounds case is not supported for SLM loads.
947 if (isOutOfBounds)
948 return rewriter.notifyMatchFailure(
949 readOp, "Out-of-bounds access is not supported for SLM loads");
950
951 // Create mem_desc for SLM
952 auto memDescType =
953 xegpu::MemDescType::get(rewriter.getContext(), readMemTy.getShape(),
954 readMemTy.getElementType(),
955 /*mem_layout=*/nullptr);
956 auto createMemDescOp = xegpu::CreateMemDescOp::create(
957 rewriter, loc, memDescType, readOp.getBase());
958 // Convert indices to OpFoldResult for LoadMatrixOp
959 SmallVector<OpFoldResult> indices =
960 getAsOpFoldResult(readOp.getIndices());
961 auto loadMatrixOp = xegpu::LoadMatrixOp::create(
962 rewriter, loc, loadedVecTy, createMemDescOp.getResult(), indices,
963 /*layout=*/nullptr);
964
965 rewriter.replaceOp(readOp, loadMatrixOp.getResult());
966 return success();
967 }
968
969 // A transfer of a single element that is only ever extracted to a scalar
970 // becomes a scalar load, at any rank: the vector is a wrapper its consumers
971 // undo, so neither an nd descriptor nor a lane-vector gather buys anything.
972 // A unit-size vector genuinely used as a vector keeps the paths below.
973 if (loadedVecTy.getNumElements() == 1 && isUsedAsScalar(readOp.getResult()))
974 return lowerToScalarLoadOp(readOp, rewriter);
975
976 // An nd block load can realize a minor-identity map directly, or an
977 // innermost-two-dims transpose via a trailing vector.transpose. Any other
978 // permutation (e.g. a mid-vector transpose of a high-dim load) is left to
979 // the scattered path, which permutes strides explicitly.
980 AffineMap readMap = readOp.getPermutationMap();
981 bool isTransposeLoad = isInnermostTwoDimsTransposed(readMap);
982
983 // A block load transfers the tile as it is laid out in memory, so a
984 // transposing read describes a tile whose innermost two dims are swapped.
985 Type elementType = loadedVecTy.getElementType();
986 SmallVector<int64_t> descShape(loadedVecTy.getShape());
987 if (isTransposeLoad) {
988 size_t rank = descShape.size();
989 assert(rank >= 2 && "Transpose requires at least 2 dimensions");
990 std::swap(descShape[rank - 1], descShape[rank - 2]);
991 }
992 // Swapping a unit dimension moves no data - e.g. a <1x64> tile read as
993 // <64x1> - so the tile is loaded as is and the trailing vector.transpose is
994 // only a reshape, with no need for the transposing block load variant.
995 bool needsTransposedBlockLoad = isTransposeLoad &&
996 descShape[descShape.size() - 1] != 1 &&
997 descShape[descShape.size() - 2] != 1;
998
999 // Prefer an nd block load. It requires a vector of rank >= 2 backed by a
1000 // scalar-element memref, a map the block load can realize, and a tile shape
1001 // the target's 2D block load can access. 1D vectors use the scattered
1002 // xegpu.load path instead, which has a richer interface (e.g. layout
1003 // capabilities). Out-of-bounds reads are allowed as long as the padding
1004 // matches load_nd's implicit zero padding.
1005 bool canLowerToLoadNd =
1006 loadedVecTy.getRank() > 1 &&
1007 (readMap.isMinorIdentity() || isTransposeLoad) &&
1008 is2DBlockLoadable(readOp, descShape,
1009 /*hasTranspose=*/needsTransposedBlockLoad);
1010
1011 if (canLowerToLoadNd) {
1012 // The load produces the memory-ordered tile; the transpose below restores
1013 // the shape the transfer_read asked for.
1014 if (isTransposeLoad)
1015 loadedVecTy = VectorType::get(descShape, elementType);
1016 auto descType = xegpu::TensorDescType::get(
1017 descShape, elementType, /*array_length=*/1,
1018 /*boundary_check=*/isOutOfBounds, xegpu::MemorySpace::Global);
1019 auto [src, indices] = convertMemrefAndOffsetsToTargetRank(
1020 rewriter, loc, readOp.getBase(),
1021 getAsOpFoldResult(readOp.getIndices()), loadedVecTy.getRank());
1022 // By default, no specific caching policy is assigned.
1023 xegpu::CachePolicyAttr hint = nullptr;
1024 xegpu::CreateNdDescOp ndDesc = xegpu::CreateNdDescOp::create(
1025 rewriter, loc, descType, dyn_cast<TypedValue<MemRefType>>(src));
1026
1027 Operation *loadedOp =
1028 xegpu::LoadNdOp::create(rewriter, loc, loadedVecTy, ndDesc, indices,
1029 /*packed=*/nullptr, /*transpose=*/nullptr,
1030 /*l1_hint=*/hint,
1031 /*l2_hint=*/hint, /*l3_hint=*/hint,
1032 /*layout=*/nullptr);
1033 if (isTransposeLoad) {
1034 // Undo the innermost-two-dims swap with a trailing vector.transpose:
1035 // keep the leading dimensions in place and interchange only the last
1036 // two.
1037 int64_t rank = loadedVecTy.getRank();
1038 SmallVector<int64_t> perm(llvm::to_vector(llvm::seq<int64_t>(0, rank)));
1039 std::swap(perm[rank - 1], perm[rank - 2]);
1040 loadedOp = vector::TransposeOp::create(rewriter, loc,
1041 loadedOp->getResult(0), perm);
1042 }
1043 rewriter.replaceOp(readOp, loadedOp);
1044 return success();
1045 }
1046
1047 // Fall back to a scattered load. It supports arbitrary permutations and any
1048 // rank, and masks off the out-of-bounds elements.
1049 return lowerToScatteredLoadOp(readOp, rewriter);
1050 }
1051};
1052
1053struct TransferWriteLowering
1054 : public OpRewritePattern<vector::TransferWriteOp> {
1055 using Base::Base;
1056
1057 LogicalResult matchAndRewrite(vector::TransferWriteOp writeOp,
1058 PatternRewriter &rewriter) const override {
1059 Location loc = writeOp.getLoc();
1060
1061 if (failed(transferPreconditions(rewriter, writeOp)))
1062 return failure();
1063 // Perform common data transfer checks.
1064 VectorType vecTy = writeOp.getVectorType();
1065 auto writeMemTy = cast<MemRefType>(writeOp.getShapedType());
1066 // Check if the memref has address space 3 (shared local memory)
1067 bool isSharedMemory = xegpu::XeGPUDialect::isSharedMemory(writeMemTy);
1068
1069 // For shared local memory (address space 3), use create_mem_desc +
1070 // store_matrix
1071 if (isSharedMemory) {
1072 // store_matrix supports 1D and 2D stores to SLM.
1073 if (vecTy.getRank() != 1 && vecTy.getRank() != 2)
1074 return rewriter.notifyMatchFailure(
1075 writeOp, "Only 1D and 2D vector stores are supported for SLM");
1076 // store_matrix writes the payload along the innermost dimensions of the
1077 // mem_desc, so it cannot express a transposed or strided store, e.g. a
1078 // 1D vector written along a non-innermost dimension.
1079 if (!writeOp.getPermutationMap().isMinorIdentity())
1080 return rewriter.notifyMatchFailure(
1081 writeOp,
1082 "Non identity transposition is not supported for SLM stores.");
1083 // Out of bounds case is not supported for SLM stores.
1084 if (writeOp.hasOutOfBoundsDim())
1085 return rewriter.notifyMatchFailure(
1086 writeOp, "Out-of-bounds access is not supported for SLM stores");
1087 // Create mem_desc for SLM
1088 auto memDescType =
1089 xegpu::MemDescType::get(rewriter.getContext(), writeMemTy.getShape(),
1090 writeMemTy.getElementType(),
1091 /*mem_layout=*/nullptr);
1092
1093 auto createMemDescOp = xegpu::CreateMemDescOp::create(
1094 rewriter, loc, memDescType, writeOp.getBase());
1095
1096 // Convert indices to OpFoldResult for StoreMatrixOp
1097 SmallVector<OpFoldResult> indices =
1098 getAsOpFoldResult(writeOp.getIndices());
1099
1100 xegpu::StoreMatrixOp::create(rewriter, loc, writeOp.getVector(),
1101 createMemDescOp.getResult(), indices,
1102 /*layout=*/nullptr);
1103
1104 rewriter.eraseOp(writeOp);
1105 return success();
1106 }
1107
1108 const xegpu::uArch::uArch *uArch =
1109 xegpu::uArch::getUArch(xegpu::getChipStr(writeOp).value_or(""));
1110
1111 // Prefer an nd block store. It requires a vector of rank >= 2 backed by a
1112 // scalar-element memref, a minor-identity map (block stores have no
1113 // transpose support), and a tile shape the target's 2D block store can
1114 // access. 1D vectors use the scattered xegpu.store path instead, which has
1115 // a richer interface. Out-of-bounds writes are handled by the descriptor's
1116 // boundary check.
1117 AffineMap map = writeOp.getPermutationMap();
1118 bool canLowerToStoreNd =
1119 vecTy.getRank() > 1 && map.isMinorIdentity() &&
1120 writeMemTy.getElementType().isIntOrFloat() &&
1121 isSupportedBlockShape(
1122 uArch, xegpu::uArch::InstructionKind::Subgroup2DBlockStore,
1123 vecTy.getShape(), vecTy.getElementType());
1124
1125 if (canLowerToStoreNd) {
1126 auto [src, indices] = convertMemrefAndOffsetsToTargetRank(
1127 rewriter, loc, writeOp.getBase(),
1128 getAsOpFoldResult(writeOp.getIndices()), vecTy.getRank());
1129
1130 auto descType = xegpu::TensorDescType::get(
1131 vecTy.getShape(), vecTy.getElementType(),
1132 /*array_length=*/1, /*boundary_check=*/writeOp.hasOutOfBoundsDim(),
1133 xegpu::MemorySpace::Global);
1134 // By default, no specific caching policy is assigned.
1135 xegpu::CachePolicyAttr hint = nullptr;
1136 xegpu::CreateNdDescOp ndDesc = xegpu::CreateNdDescOp::create(
1137 rewriter, loc, descType, dyn_cast<TypedValue<MemRefType>>(src));
1138
1139 auto storeOp = xegpu::StoreNdOp::create(
1140 rewriter, loc, writeOp.getVector(), ndDesc, indices,
1141 /*l1_hint=*/hint,
1142 /*l2_hint=*/hint, /*l3_hint=*/hint,
1143 /*layout=*/nullptr);
1144 rewriter.replaceOp(writeOp, storeOp);
1145 return success();
1146 }
1147
1148 // Fall back to a scattered store. It supports arbitrary permutations and
1149 // any rank, and masks off the out-of-bounds elements.
1150 return lowerToScatteredStoreOp(writeOp, rewriter);
1151 }
1152};
1153
1154struct GatherLowering : public OpRewritePattern<vector::GatherOp> {
1155 using Base::Base;
1156
1157 LogicalResult matchAndRewrite(vector::GatherOp gatherOp,
1158 PatternRewriter &rewriter) const override {
1159 auto srcTy = dyn_cast<MemRefType>(gatherOp.getBase().getType());
1160 if (!srcTy)
1161 return rewriter.notifyMatchFailure(gatherOp, "Expects memref source");
1162
1163 if (gatherOp.getVectorType().isScalable())
1164 return rewriter.notifyMatchFailure(gatherOp,
1165 "Scalable vectors are not supported");
1166
1167 Location loc = gatherOp.getLoc();
1168 VectorType vectorType = gatherOp.getVectorType();
1169
1170 auto meta = computeMemrefMeta(gatherOp, rewriter);
1171 if (meta.first.empty())
1172 return rewriter.notifyMatchFailure(gatherOp, "Failed to compute strides");
1173
1174 Value localOffsets =
1175 computeOffsets(rewriter, gatherOp, meta.first, meta.second);
1176 Value flatMemref = memrefToIndexPtr(gatherOp, rewriter);
1177
1178 auto xeGatherOp = xegpu::LoadGatherOp::create(
1179 rewriter, loc, vectorType, flatMemref, localOffsets, gatherOp.getMask(),
1180 /*l1_hint=*/xegpu::CachePolicyAttr{},
1181 /*l2_hint=*/xegpu::CachePolicyAttr{},
1182 /*l3_hint=*/xegpu::CachePolicyAttr{},
1183 /*layout=*/nullptr, /*contiguity=*/nullptr);
1184
1185 auto selectOp =
1186 arith::SelectOp::create(rewriter, loc, gatherOp.getMask(),
1187 xeGatherOp.getResult(), gatherOp.getPassThru());
1188 rewriter.replaceOp(gatherOp, selectOp.getResult());
1189 return success();
1190 }
1191};
1192
1193struct ScatterLowering : public OpRewritePattern<vector::ScatterOp> {
1194 using Base::Base;
1195
1196 LogicalResult matchAndRewrite(vector::ScatterOp scatterOp,
1197 PatternRewriter &rewriter) const override {
1198 auto srcTy = dyn_cast<MemRefType>(scatterOp.getBase().getType());
1199 if (!srcTy)
1200 return rewriter.notifyMatchFailure(scatterOp, "Expects memref source");
1201
1202 if (scatterOp.getVectorType().isScalable())
1203 return rewriter.notifyMatchFailure(scatterOp,
1204 "Scalable vectors are not supported");
1205
1206 Location loc = scatterOp.getLoc();
1207 auto meta = computeMemrefMeta(scatterOp, rewriter);
1208 if (meta.first.empty())
1209 return rewriter.notifyMatchFailure(scatterOp,
1210 "Failed to compute strides");
1211
1212 Value localOffsets =
1213 computeOffsets(rewriter, scatterOp, meta.first, meta.second);
1214 Value flatMemref = memrefToIndexPtr(scatterOp, rewriter);
1215
1216 xegpu::StoreScatterOp::create(rewriter, loc, scatterOp.getValueToStore(),
1217 flatMemref, localOffsets, scatterOp.getMask(),
1218 /*l1_hint=*/xegpu::CachePolicyAttr{},
1219 /*l2_hint=*/xegpu::CachePolicyAttr{},
1220 /*l3_hint=*/xegpu::CachePolicyAttr{},
1221 /*layout=*/nullptr,
1222 /*contiguity=*/nullptr);
1223 rewriter.eraseOp(scatterOp);
1224 return success();
1225 }
1226};
1227
1228struct LoadLowering : public OpRewritePattern<vector::LoadOp> {
1229 using Base::Base;
1230
1231 LogicalResult matchAndRewrite(vector::LoadOp loadOp,
1232 PatternRewriter &rewriter) const override {
1233 Location loc = loadOp.getLoc();
1234
1235 VectorType vecTy = loadOp.getResult().getType();
1236 MemRefType memTy = loadOp.getBase().getType();
1237 // The plain vector.load lowering only supports 1D/2D block loads.
1238 if (vecTy.getRank() != 1 && vecTy.getRank() != 2)
1239 return rewriter.notifyMatchFailure(loadOp, "Expects 1D or 2D vector");
1240 if (!memTy.getElementType().isIntOrFloat())
1241 return rewriter.notifyMatchFailure(
1242 loadOp, "Unsupported memref element type: expected integer or float");
1243
1244 // Boundary check is available only for block instructions.
1245 bool boundaryCheck = vecTy.getRank() > 1;
1246 // By default, no specific caching policy is assigned.
1247 xegpu::CachePolicyAttr hint = nullptr;
1248
1249 auto [src, indices] = convertMemrefAndOffsetsToTargetRank(
1250 rewriter, loc, loadOp.getBase(), getAsOpFoldResult(loadOp.getIndices()),
1251 vecTy.getRank());
1252
1253 auto descType = xegpu::TensorDescType::get(
1254 vecTy.getShape(), vecTy.getElementType(), /*array_length=*/1,
1255 boundaryCheck, xegpu::MemorySpace::Global);
1256
1257 xegpu::CreateNdDescOp ndDesc = xegpu::CreateNdDescOp::create(
1258 rewriter, loc, descType, dyn_cast<TypedValue<MemRefType>>(src));
1259 auto loadNdOp =
1260 xegpu::LoadNdOp::create(rewriter, loc, vecTy, ndDesc, indices,
1261 /*packed=*/nullptr, /*transpose=*/nullptr,
1262 /*l1_hint=*/hint,
1263 /*l2_hint=*/hint, /*l3_hint=*/hint,
1264 /*layout=*/nullptr);
1265 rewriter.replaceOp(loadOp, loadNdOp);
1266
1267 return success();
1268 }
1269};
1270
1271struct StoreLowering : public OpRewritePattern<vector::StoreOp> {
1272 using Base::Base;
1273
1274 LogicalResult matchAndRewrite(vector::StoreOp storeOp,
1275 PatternRewriter &rewriter) const override {
1276 Location loc = storeOp.getLoc();
1277
1278 TypedValue<VectorType> vector = storeOp.getValueToStore();
1279 VectorType vecTy = vector.getType();
1280 MemRefType memTy = storeOp.getBase().getType();
1281 // The plain vector.store lowering only supports 1D/2D block stores.
1282 if (vecTy.getRank() != 1 && vecTy.getRank() != 2)
1283 return rewriter.notifyMatchFailure(storeOp, "Expects 1D or 2D vector");
1284 if (!memTy.getElementType().isIntOrFloat())
1285 return rewriter.notifyMatchFailure(
1286 storeOp,
1287 "Unsupported memref element type: expected integer or float");
1288
1289 // Boundary check is available only for block instructions.
1290 bool boundaryCheck = vecTy.getRank() > 1;
1291
1292 auto [src, indices] = convertMemrefAndOffsetsToTargetRank(
1293 rewriter, loc, storeOp.getBase(),
1294 getAsOpFoldResult(storeOp.getIndices()), vecTy.getRank());
1295
1296 auto descType = xegpu::TensorDescType::get(
1297 vecTy.getShape(), vecTy.getElementType(),
1298 /*array_length=*/1, boundaryCheck, xegpu::MemorySpace::Global);
1299
1300 // By default, no specific caching policy is assigned.
1301 xegpu::CachePolicyAttr hint = nullptr;
1302 xegpu::CreateNdDescOp ndDesc = xegpu::CreateNdDescOp::create(
1303 rewriter, loc, descType, dyn_cast<TypedValue<MemRefType>>(src));
1304
1305 auto storeNdOp =
1306 xegpu::StoreNdOp::create(rewriter, loc, vector, ndDesc, indices,
1307 /*l1_hint=*/hint,
1308 /*l2_hint=*/hint, /*l3_hint=*/hint,
1309 /*layout=*/nullptr);
1310
1311 rewriter.replaceOp(storeOp, storeNdOp);
1312
1313 return success();
1314 }
1315};
1316
1317// If `indexingMaps` describe a (batched) row-major matmul
1318// lhs[b..., m, k], rhs[b..., k, n], acc[b..., m, n]
1319// return the number of leading batch dims (0 for a plain 2D matmul);
1320// otherwise return std::nullopt.
1321static std::optional<int64_t>
1322getRowMajorMatmulBatchRank(ArrayAttr indexingMaps) {
1323 if (indexingMaps.size() != 3)
1324 return std::nullopt;
1325
1326 AffineMap mapA = cast<AffineMapAttr>(indexingMaps[0]).getValue();
1327 AffineMap mapB = cast<AffineMapAttr>(indexingMaps[1]).getValue();
1328 AffineMap mapC = cast<AffineMapAttr>(indexingMaps[2]).getValue();
1329
1330 // The result map exposes the batch dims followed by the core (m, n) dims.
1331 if (mapC.getNumResults() < 2)
1332 return std::nullopt;
1333 int64_t batchRank = mapC.getNumResults() - 2;
1334
1335 // A single `k` reduction gives batchRank + 3 iteration dims; each operand
1336 // map exposes batchRank + 2 dims (batch dims + 2 core dims).
1337 unsigned numDims = static_cast<unsigned>(batchRank) + 3;
1338 unsigned numOperandResults = static_cast<unsigned>(batchRank) + 2;
1339 if (mapA.getNumInputs() != numDims || mapB.getNumInputs() != numDims ||
1340 mapC.getNumInputs() != numDims)
1341 return std::nullopt;
1342 if (mapA.getNumResults() != numOperandResults ||
1343 mapB.getNumResults() != numOperandResults)
1344 return std::nullopt;
1345
1346 // Reconstruct the canonical maps from the batch/m/n dims of the result and
1347 // the k dim of lhs, then compare against the actual maps.
1348 MLIRContext *context = indexingMaps.getContext();
1349 ArrayRef<AffineExpr> batchDims = mapC.getResults().take_front(batchRank);
1350 AffineExpr m = mapC.getResult(batchRank);
1351 AffineExpr n = mapC.getResult(batchRank + 1);
1352 AffineExpr k = mapA.getResult(batchRank + 1);
1353
1354 SmallVector<AffineExpr> aDims = llvm::to_vector(batchDims);
1355 aDims.push_back(m);
1356 aDims.push_back(k);
1357 SmallVector<AffineExpr> bDims = llvm::to_vector(batchDims);
1358 bDims.push_back(k);
1359 bDims.push_back(n);
1360 SmallVector<AffineExpr> cDims = llvm::to_vector(batchDims);
1361 cDims.push_back(m);
1362 cDims.push_back(n);
1363
1364 auto expected = ArrayAttr::get(
1365 context,
1366 {AffineMapAttr::get(AffineMap::get(numDims, 0, aDims, context)),
1367 AffineMapAttr::get(AffineMap::get(numDims, 0, bDims, context)),
1368 AffineMapAttr::get(AffineMap::get(numDims, 0, cDims, context))});
1369 if (indexingMaps != expected)
1370 return std::nullopt;
1371 return batchRank;
1372}
1373
1374struct ContractionLowering : public vector::AddContractionOpRewritePattern {
1375 using Base::Base;
1376
1377 LogicalResult
1378 matchAndRewriteAddContractOp(vector::ContractionOp contractOp,
1379 PatternRewriter &rewriter) const override {
1380 Location loc = contractOp.getLoc();
1381
1382 TypedValue<VectorType> lhs = contractOp.getLhs();
1383 TypedValue<VectorType> rhs = contractOp.getRhs();
1384 TypedValue<Type> acc = contractOp.getAcc();
1385 VectorType accType = dyn_cast<VectorType>(acc.getType());
1386 if (!accType)
1387 return rewriter.notifyMatchFailure(contractOp, "Expects vector acc");
1388
1389 std::optional<int64_t> batchRank =
1390 getRowMajorMatmulBatchRank(contractOp.getIndexingMapsAttr());
1391 if (!batchRank)
1392 return rewriter.notifyMatchFailure(
1393 contractOp,
1394 "Expects a (batched) row-major matmul: leading dims must "
1395 "be batch dims shared by lhs, rhs, and acc; innermost two "
1396 "dims must be (M, K), (K, N), and (M, N)");
1397
1398 // xegpu.dpas operands are limited to 2 batch + 2 core dims.
1399 if (*batchRank > 2)
1400 return rewriter.notifyMatchFailure(contractOp,
1401 "Expects operands of rank 4 or less");
1402
1403 auto dpasOp = xegpu::DpasOp::create(
1404 rewriter, loc, contractOp.getResultType(), lhs, rhs, acc,
1405 /*layout_a=*/nullptr, /*layout_b=*/nullptr, /*layout_cd=*/nullptr);
1406 rewriter.replaceOp(contractOp, dpasOp);
1407
1408 return success();
1409 }
1410};
1411
1412// Returns the `vector.shape_cast` that flattened a value of type `ndType` into
1413// `flat`, if that is how `flat` was produced.
1414static vector::ShapeCastOp getFlattenCast(Value flat, VectorType ndType) {
1415 auto shapeCast = flat.getDefiningOp<vector::ShapeCastOp>();
1416 if (shapeCast && shapeCast.getSourceVectorType() == ndType)
1417 return shapeCast;
1418 return nullptr;
1419}
1420
1421static DenseElementsAttr getDenseConstant(Value flat) {
1422 DenseElementsAttr elements;
1423 if (matchPattern(flat, m_Constant(&elements)))
1424 return elements;
1425 return nullptr;
1426}
1427
1428static vector::BroadcastOp getSplatBroadcast(Value flat) {
1429 auto broadcast = flat.getDefiningOp<vector::BroadcastOp>();
1430 if (broadcast && !isa<VectorType>(broadcast.getSourceType()))
1431 return broadcast;
1432 return nullptr;
1433}
1434
1435// Returns true if the cast `unflatten` creates will fold away, i.e. if `flat`
1436// is a flattening cast, a constant or a splat.
1437static bool canUnflatten(Value flat, VectorType ndType) {
1438 return getFlattenCast(flat, ndType) || getDenseConstant(flat) ||
1439 getSplatBroadcast(flat);
1440}
1441
1442// Reshape `flat` to `ndType`; `canUnflatten` must hold so the cast folds away.
1443static Value unflatten(PatternRewriter &rewriter, Value flat,
1444 VectorType ndType) {
1445 assert(canUnflatten(flat, ndType) && "expected the cast to fold away");
1446 return vector::ShapeCastOp::create(rewriter, flat.getLoc(), ndType, flat);
1447}
1448
1449// Restore the N-D form of a flattened `vector.gather` / `vector.scatter`.
1450//
1451// XeGPU layouts are expressed in terms of the N-D shape of the accessed data,
1452// so a flattened gather/scatter forces layout propagation to reason through the
1453// surrounding `vector.shape_cast` ops. That adds complexity and tends to yield
1454// layouts that lower to unoptimized code.
1455//
1456// Before:
1457// %cst = arith.constant dense<0.0> : vector<8192xbf16>
1458// %fi = vector.shape_cast %idx : vector<128x64xindex> to vector<8192xindex>
1459// %fm = vector.shape_cast %mask : vector<128x64xi1> to vector<8192xi1>
1460// %fr = vector.gather %src[%c0] [%fi], %fm, %cst : memref<?xbf16>,
1461// vector<8192xindex>, vector<8192xi1>, vector<8192xbf16>
1462// into vector<8192xbf16>
1463// %res = vector.shape_cast %fr : vector<8192xbf16> to vector<128x64xbf16>
1464//
1465// After:
1466// %cst = arith.constant dense<0.0> : vector<128x64xbf16>
1467// %res = vector.gather %src[%c0] [%idx], %mask, %cst : memref<?xbf16>,
1468// vector<128x64xindex>, vector<128x64xi1>, vector<128x64xbf16>
1469// into vector<128x64xbf16>
1470template <typename OpTy>
1471struct UnflattenGatherScatter : public OpRewritePattern<OpTy> {
1472 using OpRewritePattern<OpTy>::OpRewritePattern;
1473
1474 LogicalResult matchAndRewrite(OpTy op,
1475 PatternRewriter &rewriter) const override {
1476 constexpr bool isGather = std::is_same_v<OpTy, vector::GatherOp>;
1477
1478 if (!isa<MemRefType>(op.getBase().getType()))
1479 return rewriter.notifyMatchFailure(op, "expects a memref source");
1480
1481 if (op.getIndexVectorType().getRank() != 1)
1482 return rewriter.notifyMatchFailure(op, "index vector is not 1-D");
1483
1484 // The N-D shape comes from the index operand's producer: this only undoes
1485 // a flattening that already happened, it never invents a shape.
1486 auto indexCast =
1487 op.getIndices().template getDefiningOp<vector::ShapeCastOp>();
1488 if (!indexCast || indexCast.getSourceVectorType().getRank() < 2)
1489 return rewriter.notifyMatchFailure(
1490 op, "index vector is not a shape_cast of an N-D vector");
1491 VectorType ndIndexType = indexCast.getSourceVectorType();
1492 VectorType ndMaskType =
1493 ndIndexType.cloneWith(std::nullopt, rewriter.getI1Type());
1494 VectorType ndType = ndIndexType.cloneWith(
1495 std::nullopt, op.getVectorType().getElementType());
1496
1497 // Check everything before creating any IR: a partially applied rewrite
1498 // would leave dead ops behind.
1499 if (!canUnflatten(op.getMask(), ndMaskType))
1500 return rewriter.notifyMatchFailure(op, "cannot un-flatten the mask");
1501
1502 if constexpr (isGather) {
1503 if (!canUnflatten(op.getPassThru(), ndType))
1504 return rewriter.notifyMatchFailure(op,
1505 "cannot un-flatten the pass-thru");
1506
1507 Value mask = unflatten(rewriter, op.getMask(), ndMaskType);
1508 Value passThru = unflatten(rewriter, op.getPassThru(), ndType);
1509 auto ndGather = vector::GatherOp::create(
1510 rewriter, op.getLoc(), ndType, op.getBase(), op.getOffsets(),
1511 indexCast.getSource(), mask, passThru, op.getAlignmentAttr());
1512 ndGather->setDiscardableAttrs(op->getDiscardableAttrDictionary());
1513 rewriter.replaceOpWithNewOp<vector::ShapeCastOp>(op, op.getVectorType(),
1514 ndGather);
1515 } else {
1516 if (!canUnflatten(op.getValueToStore(), ndType))
1517 return rewriter.notifyMatchFailure(
1518 op, "cannot un-flatten the stored value");
1519
1520 Value mask = unflatten(rewriter, op.getMask(), ndMaskType);
1521 Value valueToStore = unflatten(rewriter, op.getValueToStore(), ndType);
1522 // Only operand types change, and this keeps the optional tensor result
1523 // untouched.
1524 rewriter.modifyOpInPlace(op, [&] {
1525 op.getIndicesMutable().assign(indexCast.getSource());
1526 op.getMaskMutable().assign(mask);
1527 op.getValueToStoreMutable().assign(valueToStore);
1528 });
1529 }
1530 return success();
1531 }
1532};
1533
1534// Rewrite a broadcast that is immediately transposed into a shape_cast that
1535// inserts the new dims where they finally belong, followed by a broadcast that
1536// stretches them in place.
1537//
1538// This lets layout propagation give %v the slice dim of the row reduction
1539// that typically produces it (dims = [1] below, not [0]):
1540// #row = #xegpu.layout<lane_layout = [1, 16], lane_data = [1, 1]>
1541// #col = #xegpu.layout<lane_layout = [16, 1], lane_data = [1, 1],
1542// order = [0, 1]>
1543//
1544// Before:
1545// // %v : #xegpu.slice<#col, dims = [0]>
1546// %b = vector.broadcast %v : vector<16xf32> to vector<64x16xf32> // #col
1547// %t = vector.transpose %b, [1, 0]
1548// : vector<64x16xf32> to vector<16x64xf32> // #row
1549//
1550// After:
1551// // %v : #xegpu.slice<#row, dims = [1]>
1552// %c = vector.shape_cast %v : vector<16xf32> to vector<16x1xf32> // #row
1553// %t = vector.broadcast %c
1554// : vector<16x1xf32> to vector<16x64xf32> // #row
1555struct TransposedBroadcastToStretch
1556 : public OpRewritePattern<vector::TransposeOp> {
1557 using OpRewritePattern<vector::TransposeOp>::OpRewritePattern;
1558
1559 LogicalResult matchAndRewrite(vector::TransposeOp op,
1560 PatternRewriter &rewriter) const override {
1561 auto bcast = op.getVector().getDefiningOp<vector::BroadcastOp>();
1562 if (!bcast)
1563 return rewriter.notifyMatchFailure(op, "not transposing a broadcast");
1564
1565 // Scalar sources are left to FoldTransposeBroadcast.
1566 auto srcTy = dyn_cast<VectorType>(bcast.getSourceType());
1567 if (!srcTy || srcTy.getRank() < 1)
1568 return rewriter.notifyMatchFailure(op,
1569 "broadcast source is not a vector");
1570
1571 VectorType bcastTy = bcast.getResultVectorType();
1572 if (srcTy.isScalable() || bcastTy.isScalable())
1573 return rewriter.notifyMatchFailure(op, "scalable vectors");
1574
1575 int64_t srcRank = srcTy.getRank();
1576 int64_t numNewDims = bcastTy.getRank() - srcRank;
1577 if (numNewDims < 1)
1578 return rewriter.notifyMatchFailure(op, "broadcast adds no dims");
1579
1580 // Only a pure prepend; no source dim may be stretched.
1581 if (srcTy.getShape() != bcastTy.getShape().take_back(srcRank))
1582 return rewriter.notifyMatchFailure(op,
1583 "broadcast stretches a source dim");
1584
1585 // Source dims must move to the front and new dims to the back, both in
1586 // order.
1587 ArrayRef<int64_t> perm = op.getPermutation();
1588 for (int64_t i = 0; i < srcRank; ++i)
1589 if (perm[i] != numNewDims + i)
1590 return rewriter.notifyMatchFailure(
1591 op, "source dims do not move to the front in order");
1592 for (int64_t i = 0; i < numNewDims; ++i)
1593 if (perm[srcRank + i] != i)
1594 return rewriter.notifyMatchFailure(
1595 op, "broadcast dims do not stay trailing in order");
1596
1597 // Source shape followed by one unit dim per new dim.
1598 SmallVector<int64_t> castShape(srcTy.getShape());
1599 castShape.append(numNewDims, 1);
1600 auto castTy = VectorType::get(castShape, srcTy.getElementType());
1601
1602 DictionaryAttr attrs = op->getDiscardableAttrDictionary();
1603 auto cast = vector::ShapeCastOp::create(rewriter, op.getLoc(), castTy,
1604 bcast.getSource());
1605 auto stretch = rewriter.replaceOpWithNewOp<vector::BroadcastOp>(
1606 op, op.getResultVectorType(), cast);
1607 stretch->setDiscardableAttrs(attrs);
1608 return success();
1609 }
1610};
1611
1612// Bring the vector ops into a form the conversion patterns lower directly (see
1613// populateVectorToXeGPULegalizationPatterns).
1614static LogicalResult legalizeForVectorToXeGPU(Operation *root) {
1615 RewritePatternSet patterns(root->getContext());
1617 return applyPatternsGreedily(root, std::move(patterns));
1618}
1619
1620// Returns `memrefTy` with its memory space replaced by `newMemSpace`.
1621static MemRefType withMemorySpace(MemRefType memrefTy, Attribute newMemSpace) {
1622 return MemRefType::get(memrefTy.getShape(), memrefTy.getElementType(),
1623 memrefTy.getLayout(), newMemSpace);
1624}
1625
1626// Rewrite every `memref.alloca` not already in shared local memory (SLM) to
1627// be in SLM (address space 3), and propagate the new memory space through
1628// memref-producing aliasing users (e.g. memref.cast, memref.subview,
1629// memref.expand_shape, ...). Consumers that take a memref operand but
1630// produce a non-memref result (e.g. vector.transfer_read, vector.load) are
1631// left untouched: their operand type simply reflects the new memory space.
1632//
1633// This makes `xegpu.load_matrix`/`xegpu.store_matrix` lowering work end-to-end
1634// for IR coming from bufferization, which by default assigns memory space 0/1
1635// to allocations.
1636static void promoteAllocasToSLM(Operation *root) {
1637 MLIRContext *ctx = root->getContext();
1638 Attribute slmAttr = IntegerAttr::get(IntegerType::get(ctx, 64), 3);
1639
1640 // A user is treated as a memref-producing alias (e.g. memref.cast,
1641 // memref.subview, memref.expand_shape, ...) if it is side-effect free and
1642 // produces at least one memref result. This excludes ops like memref.copy
1643 // that have memory effects.
1644 auto isMemrefResultOp = [](Operation *op) {
1645 if (!isMemoryEffectFree(op))
1646 return false;
1647 return llvm::any_of(op->getResultTypes(),
1648 [](Type t) { return isa<MemRefType>(t); });
1649 };
1650
1651 // Update `v`'s type to have SLM memory space, then walk forward through
1652 // memref-producing users and update their result types accordingly.
1653 std::function<void(Value)> propagate = [&](Value v) {
1654 auto memrefTy = dyn_cast<MemRefType>(v.getType());
1655 if (!memrefTy || xegpu::XeGPUDialect::isSharedMemory(memrefTy))
1656 return;
1657 v.setType(withMemorySpace(memrefTy, slmAttr));
1658 for (Operation *user : v.getUsers()) {
1659 if (!isMemrefResultOp(user))
1660 continue;
1661 for (Value result : user->getResults())
1662 propagate(result);
1663 }
1664 };
1665
1667 root->walk([&](memref::AllocaOp op) {
1668 auto memrefTy = dyn_cast<MemRefType>(op.getResult().getType());
1669 if (!memrefTy || xegpu::XeGPUDialect::isSharedMemory(memrefTy))
1670 return;
1671 allocas.push_back(op);
1672 });
1673
1674 for (memref::AllocaOp alloca : allocas) {
1675 OpBuilder builder(alloca);
1676 auto memrefTy = cast<MemRefType>(alloca.getResult().getType());
1677 auto newTy = withMemorySpace(memrefTy, slmAttr);
1678 auto newOp = memref::AllocaOp::create(
1679 builder, alloca.getLoc(), newTy, alloca.getDynamicSizes(),
1680 alloca.getSymbolOperands(), alloca.getAlignmentAttr());
1681 alloca.getResult().replaceAllUsesWith(newOp.getResult());
1682 alloca.erase();
1683 // Propagate the new memory space through memref-producing consumers.
1684 for (Operation *user : newOp.getResult().getUsers()) {
1685 if (!isMemrefResultOp(user))
1686 continue;
1687 for (Value result : user->getResults())
1688 propagate(result);
1689 }
1690 }
1691}
1692
1693struct ConvertVectorToXeGPUPass
1694 : public impl::ConvertVectorToXeGPUBase<ConvertVectorToXeGPUPass> {
1695 void runOnOperation() override {
1696 // Promote local allocations to SLM (address space 3) so that
1697 // load_matrix/store_matrix lowerings have well-typed memref operands.
1698 promoteAllocasToSLM(getOperation());
1699
1700 // Undo any flattening of gather/scatter operands, so that the conversion
1701 // below sees the N-D shape the XeGPU layouts are expressed in, and rewrite
1702 // transfer_reads the conversion patterns cannot lower as they are.
1703 if (failed(legalizeForVectorToXeGPU(getOperation())))
1704 return signalPassFailure();
1705
1706 RewritePatternSet patterns(&getContext());
1709 if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
1710 return signalPassFailure();
1711 }
1712};
1713} // namespace
1714
1716 RewritePatternSet &patterns) {
1717 MLIRContext *ctx = patterns.getContext();
1718 patterns
1719 .add<TransferReadLowering, TransferWriteLowering, LoadLowering,
1720 ScatterLowering, GatherLowering, StoreLowering, ContractionLowering>(
1721 ctx);
1722 // Tried ahead of TransferReadLowering, which would otherwise take a 1D
1723 // transfer on the scattered path.
1724 patterns.add<PromoteTransferReadToUnitDim>(ctx, /*benefit=*/2);
1725}
1726
1728 RewritePatternSet &patterns) {
1729 MLIRContext *ctx = patterns.getContext();
1730 // Un-flatten every gather/scatter that was flattened to 1-D operands, so
1731 // that the conversion patterns and the XeGPU layouts downstream of them see
1732 // the N-D shape of the accessed data. The shape_cast canonicalizations clean
1733 // up the casts the un-flattening leaves behind.
1734 patterns.add<UnflattenGatherScatter<vector::GatherOp>,
1735 UnflattenGatherScatter<vector::ScatterOp>,
1736 TransposedBroadcastToStretch>(ctx);
1737 vector::ShapeCastOp::getCanonicalizationPatterns(patterns, ctx);
1738 // Rewrite transfer_reads with a broadcast permutation map into a transfer of
1739 // the data plus the vector ops that expand it.
1740 patterns.add<LegalizeBroadcastTransferRead>(ctx);
1741}
return success()
lhs
ArrayAttr()
b getContext())
static std::optional< VectorShape > vectorShape(Type type)
static Value broadcast(Location loc, Value toBroadcast, unsigned numElements, const TypeConverter &typeConverter, ConversionPatternRewriter &rewriter)
Broadcasts the value to vector with numElements number of elements.
static bool isSharedMemory(MemRefType type)
Return true if this is a shared memory memref type.
Base type for affine expression.
Definition AffineExpr.h:68
A multi-dimensional affine map Affine map's are immutable like Type's, and they are uniqued.
Definition AffineMap.h:46
static AffineMap getMinorIdentityMap(unsigned dims, unsigned results, MLIRContext *context)
Returns an identity affine map (d0, ..., dn) -> (dp, ..., dn) on the most minor dimensions.
MLIRContext * getContext() const
bool isMinorIdentity() const
Returns true if this affine map is a minor identity, i.e.
static AffineMap get(MLIRContext *context)
Returns a zero result affine map with no dimensions or symbols: () -> ().
bool isProjectedPermutation(bool allowZeroInResults=false) const
Returns true if the AffineMap represents a subset (i.e.
ArrayRef< AffineExpr > getResults() const
unsigned getNumResults() const
unsigned getNumInputs() const
AffineExpr getResult(unsigned idx) const
static AffineMap getPermutationMap(ArrayRef< unsigned > permutation, MLIRContext *context)
Returns an AffineMap representing a permutation.
Attributes are known-constant values of operations.
Definition Attributes.h:25
IntegerType getI64Type()
Definition Builders.cpp:73
IntegerAttr getI64IntegerAttr(int64_t value)
Definition Builders.cpp:120
BoolAttr getBoolAttr(bool value)
Definition Builders.cpp:108
AffineExpr getAffineDimExpr(unsigned position)
Definition Builders.cpp:373
IntegerType getI1Type()
Definition Builders.cpp:61
MLIRContext * getContext() const
Definition Builders.h:56
IndexType getIndexType()
Definition Builders.cpp:59
ArrayAttr getBoolArrayAttr(ArrayRef< bool > values)
Definition Builders.cpp:279
An attribute that represents a reference to a dense vector or tensor object.
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
MLIRContext is the top-level object for a collection of MLIR operations.
Definition MLIRContext.h:71
This class helps build Operations.
Definition Builders.h:210
This class implements the operand iterators for the Operation class.
Definition ValueRange.h:44
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:434
std::enable_if_t< llvm::function_traits< std::decay_t< FnT > >::num_args==1, RetT > walk(FnT &&callback)
Walk the operation by calling the callback for each nested operation (including this one),...
Definition Operation.h:851
user_range getUsers()
Returns a range of all users.
Definition Operation.h:927
MLIRContext * getContext()
Return the context this operation is associated with.
Definition Operation.h:233
A special type of RewriterBase that coordinates the application of a rewrite pattern on the current I...
MLIRContext * getContext() const
RewritePatternSet & add(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
virtual void eraseOp(Operation *op)
This method erases an operation that is known to have no uses.
std::enable_if_t<!std::is_convertible< CallbackT, Twine >::value, LogicalResult > notifyMatchFailure(Location loc, CallbackT &&reasonCallback)
Used to notify the listener that the IR failed to be rewritten because of a match failure,...
void modifyOpInPlace(Operation *root, CallableT &&callable)
This method is a utility wrapper around an in-place modification of an operation.
OpTy replaceOpWithNewOp(Operation *op, Args &&...args)
Replace the results of the given (original) op with a new op that is created without verification (re...
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
bool use_empty() const
Returns true if this value has no uses.
Definition Value.h:208
Type getType() const
Return the type of this value.
Definition Value.h:105
user_range getUsers() const
Definition Value.h:218
Location getLoc() const
Return the location of this value.
Definition Value.cpp:24
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
Definition ArithOps.cpp:434
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:853
const uArch * getUArch(llvm::StringRef archName)
Definition uArchCommon.h:24
int getLargestDivisor(T dim, ArrayRef< T > candidates, ArrayRef< T > candidateMultiples={})
Helper Function to find a proper instruction multiple for the user-supplied sg-level data shape (dive...
std::optional< std::string > getChipStr(Operation *op)
Retrieves the chip string from the XeVM target attribute of the parent GPU module operation.
Include the generated interface declarations.
bool matchPattern(Value value, const Pattern &pattern)
Entry point for matching a pattern over a Value.
Definition Matchers.h:490
void populateVectorToXeGPULegalizationPatterns(RewritePatternSet &patterns)
Collect a set of patterns that bring vector ops into a form the conversion patterns lower directly: g...
Type getType(OpFoldResult ofr)
Returns the int type of the integer in ofr.
Definition Utils.cpp:311
LogicalResult applyPatternsGreedily(Region &region, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr)
Rewrite ops in the given region, which must be isolated from above, by repeatedly applying the highes...
bool isMemoryEffectFree(Operation *op)
Returns true if the given operation is free of memory effects.
std::conditional_t< std::is_same_v< Ty, mlir::Type >, mlir::Value, detail::TypedValue< Ty > > TypedValue
If Ty is mlir::Type this will select Value instead of having a wrapper around it.
Definition Value.h:496
void populateVectorToXeGPUConversionPatterns(RewritePatternSet &patterns)
Collect a set of patterns to convert from the vector to XeGPU ops.
llvm::TypeSwitch< T, ResultT > TypeSwitch
Definition LLVM.h:147
OpFoldResult getAsOpFoldResult(Value val)
Given a value, try to extract a constant Attribute.
detail::constant_op_matcher m_Constant()
Matches a constant foldable operation.
Definition Matchers.h:369
AffineExpr getAffineDimExpr(unsigned position, MLIRContext *context)
These free functions allow clients of the API to not use classes in detail.
void populatePrepareVectorToMMAPatterns(RewritePatternSet &patterns)
Populate patterns that prepare vector operations for lowering to GPU dialect MMA matrix operations.
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...
Convenience wrapper for patterns that are only intended for default contractions, i....
bool isSupportedInstruction(InstructionKind instr) const
Definition uArchBase.h:153
const Instruction * getInstruction(InstructionKind instKind) const
Definition uArchBase.h:146