MLIR 24.0.0git
XeGPUToXeVM.cpp
Go to the documentation of this file.
1//===-- XeGPUToXeVM.cpp - XeGPU to XeVM dialect conversion ------*- C++ -*-===//
2//
3// This file is licensed 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
12
30#include "mlir/IR/IRMapping.h"
31#include "mlir/Pass/Pass.h"
32#include "mlir/Support/LLVM.h"
34#include "llvm/ADT/STLExtras.h"
35#include "llvm/ADT/SmallPtrSet.h"
36#include "llvm/Support/FormatVariadic.h"
37
39#include "mlir/IR/Types.h"
40
41#include "llvm/ADT/TypeSwitch.h"
42
43#include <string>
44
45namespace mlir {
46#define GEN_PASS_DEF_CONVERTXEGPUTOXEVMPASS
47#include "mlir/Conversion/Passes.h.inc"
48} // namespace mlir
49
50using namespace mlir;
51
52namespace {
53// TODO: Below are uArch dependent values, should move away from hardcoding
54static constexpr int32_t systolicDepth{8};
55static constexpr int32_t executionSize{16};
56
57// Offsets to individual fields of the 8xi32 layout nd tensor descriptor.
58enum class NdTdescOffset : uint32_t {
59 BasePtr = 0, // Base pointer (i64)
60 BaseShapeW = 2, // Base shape width (i32)
61 BaseShapeH = 3, // Base shape height (i32)
62 BasePitch = 4, // Base pitch/stride of dim rank-2 (i32)
63 LeadingStride0 = 5, // Row strides of the leading (batch) dims of a >2D
64 LeadingStride1 = 6, // descriptor (i32); added into offset_h by the load/store
65 LeadingStride2 = 7, // lowering. Left at 0 for 2D descriptors.
66};
67
68// Spare payload slots above, and the resulting max lowerable descriptor rank.
69static constexpr int64_t maxNdTdescLeadingDims{3};
70static constexpr int64_t maxNdTdescRank{2 + maxNdTdescLeadingDims};
71
72static int32_t getNumericXeVMAddrSpace(xegpu::MemorySpace xeGpuMemspace) {
73 switch (xeGpuMemspace) {
74 case xegpu::MemorySpace::Global:
75 return static_cast<int>(xevm::AddrSpace::GLOBAL);
76 case xegpu::MemorySpace::SLM:
77 return static_cast<int>(xevm::AddrSpace::SHARED);
78 }
79 llvm_unreachable("Unknown XeGPU memory space");
80}
81
82/// Translates a memref memory space attribute into XeVM's numeric address
83/// space, which follows the OpenCL/SPIR-V convention (0 = private, 1 =
84/// global, 2 = constant, 3 = shared/local, 4 = generic). A null attribute,
85/// meaning the memory space was left unspecified, maps to the default space
86/// 0. Returns failure if `memSpace` is a representation this pass does not
87/// know how to translate (e.g. a SPIR-V storage class or an arbitrary string
88/// attribute), rather than assuming it is an `IntegerAttr` and asserting.
89static FailureOr<unsigned> getNumericMemorySpace(Attribute memSpace) {
90 if (!memSpace)
91 return 0u;
92 if (auto intAttr = llvm::dyn_cast<IntegerAttr>(memSpace))
93 return static_cast<unsigned>(intAttr.getInt());
94 if (auto xevmSpace = llvm::dyn_cast<xevm::AddrSpaceAttr>(memSpace))
95 return static_cast<unsigned>(xevmSpace.getValue());
96 if (auto gpuSpace = llvm::dyn_cast<gpu::AddressSpaceAttr>(memSpace)) {
97 switch (gpuSpace.getValue()) {
98 case gpu::AddressSpace::Global:
99 return static_cast<unsigned>(xevm::AddrSpace::GLOBAL);
100 case gpu::AddressSpace::Workgroup:
101 return static_cast<unsigned>(xevm::AddrSpace::SHARED);
102 case gpu::AddressSpace::Private:
103 return static_cast<unsigned>(xevm::AddrSpace::PRIVATE);
104 case gpu::AddressSpace::Constant:
105 return static_cast<unsigned>(xevm::AddrSpace::CONSTANT);
106 }
107 llvm_unreachable("Unknown GPU address space");
108 }
109 return failure();
110}
111
112/// Checks if the given MemRefType refers to shared memory.
113static bool isSharedMemRef(const MemRefType &memrefTy) {
114 FailureOr<unsigned> addrSpace =
115 getNumericMemorySpace(memrefTy.getMemorySpace());
116 return succeeded(addrSpace) &&
117 *addrSpace == static_cast<unsigned>(xevm::AddrSpace::SHARED);
118}
119
120// Get same bitwidth flat vector type of new element type.
121static VectorType encodeVectorTypeTo(VectorType currentVecType,
122 Type toElemType) {
123 auto elemType = currentVecType.getElementType();
124 auto currentBitWidth = elemType.getIntOrFloatBitWidth();
125 auto newBitWidth = toElemType.getIntOrFloatBitWidth();
126 const int size =
127 currentVecType.getNumElements() * currentBitWidth / newBitWidth;
128 return VectorType::get(size, toElemType);
129}
130
131static xevm::LoadCacheControl
132translateLoadXeGPUCacheHint(std::optional<xegpu::CachePolicy> L1hint,
133 std::optional<xegpu::CachePolicy> L3hint) {
134 // If no hints are provided, use the default cache control.
135 if (!L1hint && !L3hint)
136 return xevm::LoadCacheControl::USE_DEFAULT;
137 // If only one of the hints is provided, use the default for the other level.
138 auto L1hintVal = L1hint.value_or(xegpu::CachePolicy::CACHED);
139 auto L3hintVal = L3hint.value_or(xegpu::CachePolicy::CACHED);
140 switch (L1hintVal) {
141 case xegpu::CachePolicy::CACHED:
142 if (L3hintVal == xegpu::CachePolicy::CACHED)
143 return xevm::LoadCacheControl::L1C_L2UC_L3C;
144 else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
145 return xevm::LoadCacheControl::L1C_L2UC_L3UC;
146 else
147 llvm_unreachable("Unsupported cache control.");
148 case xegpu::CachePolicy::UNCACHED:
149 if (L3hintVal == xegpu::CachePolicy::CACHED)
150 return xevm::LoadCacheControl::L1UC_L2UC_L3C;
151 else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
152 return xevm::LoadCacheControl::L1UC_L2UC_L3UC;
153 else
154 llvm_unreachable("Unsupported cache control.");
155 case xegpu::CachePolicy::STREAMING:
156 if (L3hintVal == xegpu::CachePolicy::CACHED)
157 return xevm::LoadCacheControl::L1S_L2UC_L3C;
158 else if (L3hintVal == xegpu::CachePolicy::UNCACHED)
159 return xevm::LoadCacheControl::L1S_L2UC_L3UC;
160 else
161 llvm_unreachable("Unsupported cache control.");
162 case xegpu::CachePolicy::READ_INVALIDATE:
163 return xevm::LoadCacheControl::INVALIDATE_READ;
164 default:
165 llvm_unreachable("Unsupported cache control.");
166 }
167}
168
169static xevm::StoreCacheControl
170translateStoreXeGPUCacheHint(std::optional<xegpu::CachePolicy> L1hint,
171 std::optional<xegpu::CachePolicy> L3hint) {
172 // If no hints are provided, use the default cache control.
173 if (!L1hint && !L3hint)
174 return xevm::StoreCacheControl::USE_DEFAULT;
175 // If only one of the hints is provided, use the default for the other level.
176 auto L1hintVal = L1hint.value_or(xegpu::CachePolicy::UNCACHED);
177 auto L3hintVal = L3hint.value_or(xegpu::CachePolicy::WRITE_BACK);
178 switch (L1hintVal) {
179 case xegpu::CachePolicy::UNCACHED:
180 if (L3hintVal == xegpu::CachePolicy::UNCACHED)
181 return xevm::StoreCacheControl::L1UC_L2UC_L3UC;
182 else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
183 return xevm::StoreCacheControl::L1UC_L2UC_L3WB;
184 else
185 llvm_unreachable("Unsupported cache control.");
186 case xegpu::CachePolicy::STREAMING:
187 if (L3hintVal == xegpu::CachePolicy::UNCACHED)
188 return xevm::StoreCacheControl::L1S_L2UC_L3UC;
189 else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
190 return xevm::StoreCacheControl::L1S_L2UC_L3WB;
191 else
192 llvm_unreachable("Unsupported cache control.");
193 case xegpu::CachePolicy::WRITE_BACK:
194 if (L3hintVal == xegpu::CachePolicy::UNCACHED)
195 return xevm::StoreCacheControl::L1WB_L2UC_L3UC;
196 else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
197 return xevm::StoreCacheControl::L1WB_L2UC_L3WB;
198 else
199 llvm_unreachable("Unsupported cache control.");
200 case xegpu::CachePolicy::WRITE_THROUGH:
201 if (L3hintVal == xegpu::CachePolicy::UNCACHED)
202 return xevm::StoreCacheControl::L1WT_L2UC_L3UC;
203 else if (L3hintVal == xegpu::CachePolicy::WRITE_BACK)
204 return xevm::StoreCacheControl::L1WT_L2UC_L3WB;
205 else
206 llvm_unreachable("Unsupported cache control.");
207 default:
208 llvm_unreachable("Unsupported cache control.");
209 }
210}
211
212//
213// Note:
214// Block operations for tile of sub byte element types are handled by
215// emulating with larger element types.
216// Tensor descriptor are keep intact and only ops consuming them are
217// emulated
218//
219
220//
221// High-D (>2D) nd descriptors are lowered by viewing the source as a single
222// flattened 2D plane, so the 2D-block surface covers every leading (batch)
223// plane at once and a batch position becomes a row offset into it. The surface
224// height is the row extent the leading dims reach:
225// base_height = size[R-2] + sum_d (size[d] - 1) * (stride[d] / stride[R-2])
226//
227// Leaving `base_ptr` at the true base means an out-of-range batch index lands
228// past `base_height`, where the HW boundary check handles it, instead of aiming
229// the surface at unmapped memory. Encoding the leading strides as row counts
230// (`stride[d] / stride[R-2]`) also makes them dimensionless, so they survive
231// element-type repacking (e.g. the f16 -> i32 transpose repack) without a unit
232// conversion.
233//
234// Limitations of the flattened-plane view:
235// 1. Each leading stride must be a whole number of rows. A source with gaps
236// between planes (`stride[d] % stride[R-2] != 0`) is not lowered. This can
237// only be checked when the strides are static; for dynamic strides the
238// divisibility is assumed.
239// 2. `base_height` grows with the leading extent, so a source with a large
240// batch x head x sequence extent can exceed the HW 2D-block surface height.
241// 3. Plane boundaries are invisible to the boundary check, which only knows
242// the surface: a tile whose rows run past `size[R-2]` spills into the
243// following rows of the surface -- the next plane, or the padding between
244// planes -- rather than being clipped there as it would be on a per-plane
245// surface. A load reads those rows instead of returning zeros, and a store
246// overwrites them, so it can corrupt the next plane. This only matters
247// when `size[R-2]` is not a multiple of the tile height.
248//
249
250class CreateNdDescToXeVMPattern
251 : public OpConversionPattern<xegpu::CreateNdDescOp> {
252 using Base::Base;
253
254 // Checks the surface width and pitch against the target's 2D block memory
255 // restrictions. Only these two are enforced: both are backed by the source's
256 // static shape and strides, whereas the base address alignment is a property
257 // of the allocation that is not visible here. The width and the pitch are
258 // checked independently, each only when it is static; a value known only at
259 // runtime is left alone, as guarding it inside the kernel would cost far more
260 // than it is worth.
261 //
262 // A violation is a dead end rather than a pattern that does not apply: the op
263 // has no other legalization path, so it is reported with an error that names
264 // the limit it misses.
265 LogicalResult checkMemoryRestrictions(xegpu::CreateNdDescOp op,
266 int64_t rank) const {
267 const auto *uArch =
269 // Without a known target chip there is no uArch to check against, so the
270 // descriptor must be accepted as-is.
271 if (!uArch)
272 return success();
273 const xegpu::uArch::BlockIOMemoryRestrictions &limits =
274 uArch->getBlockIOMemoryRestrictions();
275
276 // Recover the width and the pitch as a number of elements. A memref source
277 // carries both in its type, and either one may be dynamic independently of
278 // the other, so they are queried separately instead of being dropped
279 // together when the source is not fully static. `getMixedSizes` /
280 // `getMixedStrides` are only consulted for an integer source, where the op
281 // itself holds the extents.
282 std::optional<int64_t> widthElems;
283 std::optional<int64_t> pitchElems;
284 if (auto memrefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
285 if (!ShapedType::isDynamic(memrefTy.getDimSize(rank - 1)))
286 widthElems = memrefTy.getDimSize(rank - 1);
287 // The source is known to be strided; the caller checked that already.
288 SmallVector<int64_t> staticStrides;
289 int64_t staticOffset;
290 if (succeeded(
291 memrefTy.getStridesAndOffset(staticStrides, staticOffset)) &&
292 !ShapedType::isDynamic(staticStrides[rank - 2]))
293 pitchElems = staticStrides[rank - 2];
294 } else {
295 widthElems = getConstantIntValue(op.getMixedSizes()[rank - 1]);
296 pitchElems = getConstantIntValue(op.getMixedStrides()[rank - 2]);
297 }
298
299 unsigned elemBitSize =
300 op.getType().getElementType().getIntOrFloatBitWidth();
301 // The restrictions are expressed in bytes, but a sub-byte element type can
302 // leave the width or the pitch on a fraction of a byte, so the comparisons
303 // are done in bits. Such a size meets no byte alignment at all and is
304 // reported as a violation rather than waved through.
305 auto describe = [](int64_t bits) -> std::string {
306 if (bits % 8 == 0)
307 return llvm::formatv("{0} bytes", bits / 8).str();
308 return llvm::formatv("{0} bits", bits).str();
309 };
310 auto check = [&](std::optional<int64_t> elems, int64_t minBytes,
311 int64_t alignBytes,
312 llvm::StringRef what) -> LogicalResult {
313 if (!elems)
314 return success();
315 int64_t bits = *elems * elemBitSize;
316 if (bits < minBytes * 8)
317 return op.emitError(llvm::formatv(
318 "2D block surface {0} must be at least {1} bytes on this "
319 "target, got {2}.",
320 what, minBytes, describe(bits)));
321 if (alignBytes > 0 && bits % (alignBytes * 8) != 0)
322 return op.emitError(llvm::formatv(
323 "2D block surface {0} must be a multiple of {1} bytes on this "
324 "target, got {2}.",
325 what, alignBytes, describe(bits)));
326 return success();
327 };
328
329 if (failed(check(widthElems, limits.minBaseWidthBytes,
330 limits.baseWidthAlignmentBytes, "width")))
331 return failure();
332 // The pitch is the row stride of the innermost 2D tile.
333 if (failed(check(pitchElems, limits.minBasePitchBytes,
334 limits.basePitchAlignmentBytes, "pitch")))
335 return failure();
336 // Consecutive rows may not overlap, so the pitch has to span the full
337 // width.
338 if (widthElems && pitchElems && *pitchElems < *widthElems)
339 return op.emitError(
340 llvm::formatv("2D block surface pitch must be at least the surface "
341 "width, got a pitch of {0} for a surface {1} wide.",
342 describe(*pitchElems * elemBitSize),
343 describe(*widthElems * elemBitSize)));
344 return success();
345 }
346
347 LogicalResult
348 matchAndRewrite(xegpu::CreateNdDescOp op,
349 xegpu::CreateNdDescOp::Adaptor adaptor,
350 ConversionPatternRewriter &rewriter) const override {
351 auto loc = op.getLoc();
352 auto source = op.getSource();
353
354 // Check all failure conditions before generating any IR, so nothing has to
355 // be rolled back.
356 int64_t rank = op.getType().getRank();
357 int64_t sourceRank;
358 auto memrefTy = dyn_cast<MemRefType>(source.getType());
359 if (memrefTy) {
360 if (!memrefTy.isStrided())
361 return rewriter.notifyMatchFailure(op, "Expected strided Memref.");
362 sourceRank = memrefTy.getRank();
363 } else if (isa<IntegerType>(source.getType())) {
364 sourceRank = op.getMixedSizes().size();
365 } else {
366 return rewriter.notifyMatchFailure(op,
367 "Expected ranked Memref or integer.");
368 }
369 if (sourceRank != rank)
370 return rewriter.notifyMatchFailure(
371 op, "Expected descriptor rank to match source rank; subview the "
372 "source down to the descriptor rank.");
373 if (rank > maxNdTdescRank)
374 return rewriter.notifyMatchFailure(
375 op, "Batched nd descriptor supports at most " +
376 std::to_string(maxNdTdescLeadingDims) +
377 " leading dims (rank <= " + std::to_string(maxNdTdescRank) +
378 ").");
379 // Limitation 1 above; dynamic strides are assumed to divide evenly.
380 if (rank > 2) {
381 SmallVector<std::optional<int64_t>> constStrides(rank, std::nullopt);
382 if (memrefTy) {
383 SmallVector<int64_t> staticStrides;
384 int64_t staticOffset;
385 if (succeeded(
386 memrefTy.getStridesAndOffset(staticStrides, staticOffset)))
387 for (int64_t d = 0; d < rank; ++d)
388 if (!ShapedType::isDynamic(staticStrides[d]))
389 constStrides[d] = staticStrides[d];
390 } else {
391 SmallVector<OpFoldResult> mixed = op.getMixedStrides();
392 for (int64_t d = 0; d < rank; ++d)
393 constStrides[d] = getConstantIntValue(mixed[d]);
394 }
395 if (std::optional<int64_t> pitch = constStrides[rank - 2]) {
396 for (int64_t d = 0; d < rank - 2; ++d) {
397 std::optional<int64_t> leading = constStrides[d];
398 if (leading && (*pitch == 0 || *leading % *pitch != 0))
399 return rewriter.notifyMatchFailure(
400 op, "Expected each leading (batch) stride to be a multiple of "
401 "the row stride; the source has gaps between planes.");
402 }
403 }
404 }
405 // Enforce the target's 2D block memory restrictions. A rank-1 descriptor is
406 // just a base address and is not consumed by a 2D block op, so it is
407 // exempt.
408 if (rank >= 2 && failed(checkMemoryRestrictions(op, rank)))
409 return failure();
410
411 Type payloadElemTy = rewriter.getI32Type();
412 Type i64Ty = rewriter.getI64Type();
413
414 // Access the adaptor only after the failure checks, so a bail-out leaves no
415 // materialization cast behind.
416 Value baseAddr = adaptor.getSource();
417 if (isa<IntegerType>(source.getType()) && baseAddr.getType() != i64Ty) {
418 // Pointer type may be i32. Cast to i64 if needed.
419 baseAddr = arith::ExtUIOp::create(rewriter, loc, i64Ty, baseAddr);
420 }
421 // 1D tensor descriptor is just the base address.
422 if (rank == 1) {
423 rewriter.replaceOp(op, baseAddr);
424 return success();
425 }
426
427 SmallVector<OpFoldResult> mixedSizes;
428 SmallVector<OpFoldResult> mixedStrides;
429 if (memrefTy && !xegpu::hasStaticShapeAndStrides(memrefTy)) {
430 auto meta =
431 memref::ExtractStridedMetadataOp::create(rewriter, loc, source);
432 mixedSizes = meta.getConstifiedMixedSizes();
433 mixedStrides = meta.getConstifiedMixedStrides();
434 } else {
435 mixedSizes = op.getMixedSizes();
436 mixedStrides = op.getMixedStrides();
437 }
438
439 // Op is lowered to a code sequence that populates payload.
440 // Payload is a 8xi32 vector. Offset to individual fields are defined in
441 // NdTdescOffset enum.
442 VectorType payloadTy = VectorType::get(8, payloadElemTy);
443 // 4xi64 view is used for inserting the base pointer.
444 VectorType payloadI64Ty = VectorType::get(4, i64Ty);
445 // Initialize payload to zero.
446 Value payload = arith::ConstantOp::create(
447 rewriter, loc,
448 DenseElementsAttr::get(payloadTy, IntegerAttr::get(payloadElemTy, 0)));
449
450 // Utility for creating offset values from op fold result.
451 auto createOffset = [&](SmallVector<OpFoldResult> &ofrVec,
452 unsigned idx) -> Value {
453 Value val = getValueOrCreateConstantIntOp(rewriter, loc, ofrVec[idx]);
454 val = getValueOrCreateCastToIndexLike(rewriter, loc, payloadElemTy, val);
455 return val;
456 };
457 // The descriptor's innermost 2 dims are the 2D tile (H, W).
458 Value baseShapeW = createOffset(mixedSizes, rank - 1);
459 // Pitch is the stride of dim rank-2 (the row stride of the 2D tile).
460 Value basePitch = createOffset(mixedStrides, rank - 2);
461 // Leading (batch) strides as a number of rows (`stride[d] / pitch`).
462 SmallVector<Value> leadingRowStrides;
463 for (int64_t d = 0; d < rank - 2; ++d) {
464 std::optional<int64_t> leading = getConstantIntValue(mixedStrides[d]);
465 std::optional<int64_t> pitch =
466 getConstantIntValue(mixedStrides[rank - 2]);
467 if (leading && pitch && *pitch != 0)
468 leadingRowStrides.push_back(arith::ConstantIntOp::create(
469 rewriter, loc, payloadElemTy, *leading / *pitch));
470 else
471 leadingRowStrides.push_back(arith::DivUIOp::create(
472 rewriter, loc, createOffset(mixedStrides, d), basePitch));
473 }
474 // Height of the flattened plane: every leading (batch) plane is stacked
475 // into the surface, so the boundary check covers an out-of-range batch.
476 // base_height = size[R-2] + sum_d (size[d] - 1) * leadingRowStride[d]
477 // For rank 2 this is just size[0].
478 Value baseShapeH = createOffset(mixedSizes, rank - 2);
479 if (rank > 2) {
480 Value one = arith::ConstantIntOp::create(rewriter, loc, payloadElemTy, 1);
481 for (int64_t d = 0; d < rank - 2; ++d) {
482 Value planesBelow = rewriter.createOrFold<arith::SubIOp>(
483 loc, createOffset(mixedSizes, d), one);
484 Value rows = rewriter.createOrFold<arith::MulIOp>(loc, planesBelow,
485 leadingRowStrides[d]);
486 baseShapeH =
487 rewriter.createOrFold<arith::AddIOp>(loc, baseShapeH, rows);
488 }
489 }
490 // Populate payload.
491 Value payLoadAsI64 =
492 vector::BitCastOp::create(rewriter, loc, payloadI64Ty, payload);
493 payLoadAsI64 =
494 vector::InsertOp::create(rewriter, loc, baseAddr, payLoadAsI64,
495 static_cast<int>(NdTdescOffset::BasePtr));
496 payload = vector::BitCastOp::create(rewriter, loc, payloadTy, payLoadAsI64);
497 payload =
498 vector::InsertOp::create(rewriter, loc, baseShapeW, payload,
499 static_cast<int>(NdTdescOffset::BaseShapeW));
500 payload =
501 vector::InsertOp::create(rewriter, loc, baseShapeH, payload,
502 static_cast<int>(NdTdescOffset::BaseShapeH));
503 payload =
504 vector::InsertOp::create(rewriter, loc, basePitch, payload,
505 static_cast<int>(NdTdescOffset::BasePitch));
506 // The leading (batch) row strides go into the spare payload slots; the
507 // load/store/prefetch lowering turns the batch offsets into a row offset
508 // with them.
509 for (int64_t d = 0; d < rank - 2; ++d)
510 payload = vector::InsertOp::create(
511 rewriter, loc, leadingRowStrides[d], payload,
512 static_cast<int>(NdTdescOffset::LeadingStride0) + d);
513 rewriter.replaceOp(op, payload);
514 return success();
515 }
516};
517
518template <
519 typename OpType,
520 typename = std::enable_if_t<llvm::is_one_of<
521 OpType, xegpu::LoadNdOp, xegpu::StoreNdOp, xegpu::PrefetchNdOp>::value>>
522class LoadStorePrefetchNdToXeVMPattern : public OpConversionPattern<OpType> {
523 using OpConversionPattern<OpType>::OpConversionPattern;
524 LogicalResult
525 matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
526 ConversionPatternRewriter &rewriter) const override {
527 auto mixedOffsets = op.getMixedOffsets();
528 int64_t opOffsetsSize = mixedOffsets.size();
529 auto loc = op.getLoc();
530 auto ctxt = rewriter.getContext();
531
532 auto tdesc = adaptor.getTensorDesc();
533 auto tdescTy = op.getTensorDescType();
534 auto tileRank = tdescTy.getRank();
535 if (opOffsetsSize != tileRank)
536 return rewriter.notifyMatchFailure(
537 op, "Expected offset rank to match descriptor rank.");
538 if (tileRank > 2 && llvm::any_of(tdescTy.getShape().drop_back(2),
539 [](int64_t d) { return d != 1; }))
540 return rewriter.notifyMatchFailure(
541 op, "Expected leading (batch) descriptor dims to be unit.");
542 if (tileRank > maxNdTdescRank)
543 return rewriter.notifyMatchFailure(
544 op, "Expected descriptor rank <= " + std::to_string(maxNdTdescRank) +
545 ".");
546 auto elemType = tdescTy.getElementType();
547 auto elemBitSize = elemType.getIntOrFloatBitWidth();
548 bool isSubByte = elemBitSize < 8;
549 uint64_t wScaleFactor = 1;
550
551 if (!isSubByte && (elemBitSize % 8 != 0))
552 return rewriter.notifyMatchFailure(
553 op, "Expected element type bit width to be multiple of 8.");
554 auto tileW = tdescTy.getDimSize(tileRank - 1);
555 // For sub byte types, only 4bits are currently supported.
556 if (isSubByte) {
557 if (elemBitSize != 4)
558 return rewriter.notifyMatchFailure(
559 op, "Only sub byte types of 4bits are supported.");
560 if (tileRank != 2)
561 return rewriter.notifyMatchFailure(
562 op, "Sub byte types are only supported for 2D tensor descriptors.");
563 auto subByteFactor = 8 / elemBitSize;
564 auto tileH = tdescTy.getDimSize(0);
565 // Handle special case for packed load.
566 if constexpr (std::is_same_v<OpType, xegpu::LoadNdOp>) {
567 if (op.getPacked().value_or(false)) {
568 // packed load is implemented as packed loads of 8bit elements.
569 if (tileH == systolicDepth * 4 &&
570 tileW == executionSize * subByteFactor) {
571 // Usage case for loading as Matrix B with pack request.
572 // source is assumed to pre-packed into 8bit elements
573 // Emulate with 8bit loads with pack request.
574 // scaled_tileW = executionSize
575 elemType = rewriter.getIntegerType(8);
576 tileW = executionSize;
577 wScaleFactor = subByteFactor;
578 }
579 }
580 }
581 // If not handled by packed load case above, handle other cases.
582 if (wScaleFactor == 1) {
583 auto sub16BitFactor = subByteFactor * 2;
584 if (tileW == executionSize * sub16BitFactor) {
585 // Usage case for loading as Matrix A operand
586 // Emulate with 16bit loads/stores.
587 // scaled_tileW = executionSize
588 elemType = rewriter.getIntegerType(16);
589 tileW = executionSize;
590 wScaleFactor = sub16BitFactor;
591 } else {
592 return rewriter.notifyMatchFailure(
593 op, "Unsupported tile shape for sub byte types.");
594 }
595 }
596 // recompute element bit size for emulation.
597 elemBitSize = elemType.getIntOrFloatBitWidth();
598 }
599
600 // Get address space from tensor descriptor memory space.
601 auto ptrTypeLLVM = LLVM::LLVMPointerType::get(
602 ctxt, getNumericXeVMAddrSpace(tdescTy.getMemorySpace()));
603 if (tileRank >= 2) {
604 // Compute element byte size.
605 Value elemByteSize = arith::ConstantIntOp::create(
606 rewriter, loc, rewriter.getI32Type(), elemBitSize / 8);
607 VectorType payloadI64Ty = VectorType::get(4, rewriter.getI64Type());
608 Value payLoadAsI64 =
609 vector::BitCastOp::create(rewriter, loc, payloadI64Ty, tdesc);
610 Value basePtr =
611 vector::ExtractOp::create(rewriter, loc, payLoadAsI64,
612 static_cast<int>(NdTdescOffset::BasePtr));
613 Value baseShapeW = vector::ExtractOp::create(
614 rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BaseShapeW));
615 Value baseShapeH = vector::ExtractOp::create(
616 rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BaseShapeH));
617 Value basePitch = vector::ExtractOp::create(
618 rewriter, loc, tdesc, static_cast<int>(NdTdescOffset::BasePitch));
619
620 Value offsetW = getValueOrCreateConstantIntOp(rewriter, loc,
621 mixedOffsets[tileRank - 1]);
622 offsetW = getValueOrCreateCastToIndexLike(rewriter, loc,
623 rewriter.getI32Type(), offsetW);
624 Value offsetH = getValueOrCreateConstantIntOp(rewriter, loc,
625 mixedOffsets[tileRank - 2]);
626 offsetH = getValueOrCreateCastToIndexLike(rewriter, loc,
627 rewriter.getI32Type(), offsetH);
628 // Turn the leading (batch) offsets into a row offset into the flattened
629 // plane, using the row-unit batch strides encoded at create time:
630 // offsetH += sum_d offset[d] * leadingRowStride[d]
631 // The base pointer stays at the true base, so an out-of-range batch index
632 // is caught by the HW boundary check instead of moving the surface to
633 // unmapped memory.
634 for (int64_t d = 0; d < tileRank - 2; ++d) {
635 Value off =
636 getValueOrCreateConstantIntOp(rewriter, loc, mixedOffsets[d]);
637 off = getValueOrCreateCastToIndexLike(rewriter, loc,
638 rewriter.getI32Type(), off);
639 Value rowStride = vector::ExtractOp::create(
640 rewriter, loc, tdesc,
641 static_cast<int>(NdTdescOffset::LeadingStride0) + d);
642 Value term = arith::MulIOp::create(rewriter, loc, off, rowStride);
643 offsetH = arith::AddIOp::create(rewriter, loc, offsetH, term);
644 }
645 // Convert base pointer (i64) to LLVM pointer type.
646 Value basePtrLLVM =
647 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtr);
648 // FIXME: width or pitch is not the same as baseShapeW it should be the
649 // stride of the second to last dimension in row major layout.
650 // Compute width in bytes.
651 Value baseShapeWInBytes =
652 arith::MulIOp::create(rewriter, loc, baseShapeW, elemByteSize);
653 // Compute pitch in bytes.
654 Value basePitchBytes =
655 arith::MulIOp::create(rewriter, loc, basePitch, elemByteSize);
656
657 if (wScaleFactor > 1) {
658 // Scale offsetW, baseShapeWInBytes for sub byte emulation.
659 // Note: tileW is already scaled above.
660 Value wScaleFactorValLog2 = arith::ConstantIntOp::create(
661 rewriter, loc, rewriter.getI32Type(), llvm::Log2_64(wScaleFactor));
662 baseShapeWInBytes = arith::ShRSIOp::create(
663 rewriter, loc, baseShapeWInBytes, wScaleFactorValLog2);
664 basePitchBytes = arith::ShRSIOp::create(rewriter, loc, basePitchBytes,
665 wScaleFactorValLog2);
666 offsetW =
667 arith::ShRSIOp::create(rewriter, loc, offsetW, wScaleFactorValLog2);
668 }
669 // Get tile height from the tensor descriptor type (second-to-last dim).
670 auto tileH = tdescTy.getDimSize(tileRank - 2);
671 // Get vblocks from the tensor descriptor type.
672 int32_t vblocks = tdescTy.getArrayLength();
673 if constexpr (std::is_same_v<OpType, xegpu::StoreNdOp>) {
674 Value src = adaptor.getValue();
675 // If store value is a scalar, get value from op instead of adaptor.
676 // Adaptor might have optimized away single element vector
677 if (src.getType().isIntOrFloat()) {
678 src = op.getValue();
679 }
680 VectorType srcVecTy = dyn_cast<VectorType>(src.getType());
681 if (!srcVecTy)
682 return rewriter.notifyMatchFailure(
683 op, "Expected store value to be a vector type.");
684 // Get flat vector type of integer type with matching element bit size.
685 VectorType newSrcVecTy =
686 encodeVectorTypeTo(srcVecTy, rewriter.getIntegerType(elemBitSize));
687 if (srcVecTy != newSrcVecTy)
688 src = vector::BitCastOp::create(rewriter, loc, newSrcVecTy, src);
689 auto storeCacheControl =
690 translateStoreXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
691 xevm::BlockStore2dOp::create(
692 rewriter, loc, basePtrLLVM, baseShapeWInBytes, baseShapeH,
693 basePitchBytes, offsetW, offsetH, elemBitSize, tileW, tileH, src,
694 xevm::StoreCacheControlAttr::get(ctxt, storeCacheControl));
695 rewriter.eraseOp(op);
696 } else {
697 auto loadCacheControl =
698 translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
699 if constexpr (std::is_same_v<OpType, xegpu::PrefetchNdOp>) {
700 xevm::BlockPrefetch2dOp::create(
701 rewriter, loc, basePtrLLVM, baseShapeWInBytes, baseShapeH,
702 basePitchBytes, offsetW, offsetH, elemBitSize, tileW, tileH,
703 vblocks, xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
704 rewriter.eraseOp(op);
705 } else {
706 VectorType dstVecTy = cast<VectorType>(op.getValue().getType());
707 bool vnni = op.getPacked().value_or(false);
708 auto transposeValue = op.getTranspose();
709 bool transpose =
710 transposeValue.has_value() && transposeValue.value()[0] == 1;
711 // Handle special case of 32x16 and 8bit element load
712 // with no vnni, no transpose, no vblocks.
713 // For this special case, vnni and non vnni yields the same output
714 // and only the vnni variant is supported by HW.
715 // Check and set vnni of the special case.
716 if (elemBitSize == 8 && tileW == 16 && tileH == 32 && !vnni &&
717 !transpose) {
718 vnni = true;
719 }
720 // Handle tranpose request on small element size
721 // Transpose needs to be requested on 32bit element type.
722 // offsetW and tileW needs to be adjusted to account for element type
723 // change.
724 if (transpose && elemBitSize < 32) {
725 int32_t scale = 32 / elemBitSize;
726 Value scaleLog2 = arith::ConstantIntOp::create(
727 rewriter, loc, rewriter.getI32Type(), llvm::Log2_64(scale));
728 offsetW = arith::ShRSIOp::create(rewriter, loc, offsetW, scaleLog2);
729 tileW = tileW * elemBitSize / 32;
730 elemBitSize = 32;
731 }
732 VectorType loadedTy = encodeVectorTypeTo(
733 dstVecTy, vnni ? rewriter.getI32Type()
734 : rewriter.getIntegerType(elemBitSize));
735
736 Value resultFlatVec = xevm::BlockLoad2dOp::create(
737 rewriter, loc, loadedTy, basePtrLLVM, baseShapeWInBytes,
738 baseShapeH, basePitchBytes, offsetW, offsetH, elemBitSize, tileW,
739 tileH, vblocks, transpose, vnni,
740 xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
741 resultFlatVec = vector::BitCastOp::create(
742 rewriter, loc,
743 encodeVectorTypeTo(loadedTy, dstVecTy.getElementType()),
744 resultFlatVec);
745 rewriter.replaceOp(op, resultFlatVec);
746 }
747 }
748 } else {
749 // 1D tensor descriptor.
750 // `tdesc` represents base address as i64
751 // Offset in number of elements, need to multiply by element byte size.
752 // Compute byte offset.
753 // byteOffset = offset * elementByteSize
754 Value offset =
755 getValueOrCreateConstantIntOp(rewriter, loc, mixedOffsets[0]);
756 offset = getValueOrCreateCastToIndexLike(rewriter, loc,
757 rewriter.getI64Type(), offset);
758 // Compute element byte size.
759 Value elemByteSize = arith::ConstantIntOp::create(
760 rewriter, loc, rewriter.getI64Type(), elemBitSize / 8);
761 Value byteOffset =
762 rewriter.createOrFold<arith::MulIOp>(loc, offset, elemByteSize);
763 // Final address = basePtr + byteOffset
764 Value finalAddrI64 = rewriter.createOrFold<arith::AddIOp>(
765 loc, tdesc,
766 getValueOrCreateCastToIndexLike(rewriter, loc, rewriter.getI64Type(),
767 byteOffset));
768 // Convert base pointer (i64) to LLVM pointer type.
769 Value finalPtrLLVM =
770 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, finalAddrI64);
771 if constexpr (std::is_same_v<OpType, xegpu::StoreNdOp>) {
772 Value src = adaptor.getValue();
773 // If store value is a scalar, get value from op instead of adaptor.
774 // Adaptor might have optimized away single element vector
775 if (src.getType().isIntOrFloat()) {
776 src = op.getValue();
777 }
778 VectorType srcVecTy = dyn_cast<VectorType>(src.getType());
779 if (!srcVecTy)
780 return rewriter.notifyMatchFailure(
781 op, "Expected store value to be a vector type.");
782 // Get flat vector type of integer type with matching element bit size.
783 VectorType newSrcVecTy =
784 encodeVectorTypeTo(srcVecTy, rewriter.getIntegerType(elemBitSize));
785 if (srcVecTy != newSrcVecTy)
786 src = vector::BitCastOp::create(rewriter, loc, newSrcVecTy, src);
787 auto storeCacheControl =
788 translateStoreXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
789 rewriter.replaceOpWithNewOp<xevm::BlockStoreOp>(
790 op, finalPtrLLVM, src,
791 xevm::StoreCacheControlAttr::get(ctxt, storeCacheControl));
792 } else if constexpr (std::is_same_v<OpType, xegpu::LoadNdOp>) {
793 auto loadCacheControl =
794 translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint());
795 VectorType resTy = cast<VectorType>(op.getValue().getType());
796 VectorType loadedTy =
797 encodeVectorTypeTo(resTy, rewriter.getIntegerType(elemBitSize));
798 Value load = xevm::BlockLoadOp::create(
799 rewriter, loc, loadedTy, finalPtrLLVM,
800 xevm::LoadCacheControlAttr::get(ctxt, loadCacheControl));
801 if (loadedTy != resTy)
802 load = vector::BitCastOp::create(rewriter, loc, resTy, load);
803 rewriter.replaceOp(op, load);
804 } else {
805 return rewriter.notifyMatchFailure(
806 op, "Unsupported operation: xegpu.prefetch_nd with tensor "
807 "descriptor rank == 1");
808 }
809 }
810 return success();
811 }
812};
813
814// Add a builder that creates
815// offset * elemByteSize + baseAddr
816static Value addOffsetToBaseAddr(ConversionPatternRewriter &rewriter,
817 Location loc, Value baseAddr, Value offset,
818 int64_t elemByteSize) {
820 rewriter, loc, baseAddr.getType(), elemByteSize);
821 Value byteOffset = arith::MulIOp::create(rewriter, loc, offset, byteSize);
822 Value newAddr = arith::AddIOp::create(rewriter, loc, baseAddr, byteOffset);
823 return newAddr;
824}
825
826// Returns true when every element of `mask` carries the same bit, so gating a
827// whole contiguous block on element 0 is equivalent. Splat constants, an
828// all-ones or all-zeros `vector.constant_mask`, broadcasts of a scalar, and a
829// `vector.from_elements` of one repeated value qualify.
830//
831// TODO: this recognizes a fixed list of producers. A general uniformity query
832// on the vector dialect would cover more forms and would not need updating
833// every time a new producer shows up here.
834static bool isUniformMask(Value mask) {
835 if (!isa<VectorType>(mask.getType()))
836 return true;
837 DenseElementsAttr splat;
838 if (matchPattern(mask, m_Constant(&splat)) && splat.isSplat())
839 return true;
840 if (auto constantMask = mask.getDefiningOp<vector::ConstantMaskOp>()) {
841 // All-ones and all-zeros are uniform. A zero dim size is only legal when
842 // every dim is zero, so those are the only two uniform cases.
843 return constantMask.isAllOnesMask() ||
844 llvm::all_of(constantMask.getMaskDimSizes(),
845 [](int64_t size) { return size == 0; });
846 }
847 if (auto broadcast = mask.getDefiningOp<vector::BroadcastOp>())
848 return !isa<VectorType>(broadcast.getSource().getType());
849 if (auto fromElements = mask.getDefiningOp<vector::FromElementsOp>())
850 return llvm::all_equal(fromElements.getElements());
851 // Flattening a rank > 1 operand to rank 1 inserts a shape cast. It keeps the
852 // same elements, so it keeps uniformity.
853 if (auto shapeCast = mask.getDefiningOp<vector::ShapeCastOp>())
854 return isUniformMask(shapeCast.getSource());
855 return false;
856}
857
858template <typename OpType,
859 typename = std::enable_if_t<llvm::is_one_of<
860 OpType, xegpu::LoadGatherOp, xegpu::StoreScatterOp>::value>>
861class LoadStoreToXeVMPattern : public OpConversionPattern<OpType> {
862 using OpConversionPattern<OpType>::OpConversionPattern;
863 LogicalResult
864 matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
865 ConversionPatternRewriter &rewriter) const override {
866 Value offset = adaptor.getOffsets();
867 if (!offset)
868 return rewriter.notifyMatchFailure(op, "Expected offset to be provided.");
869 auto loc = op.getLoc();
870 auto ctxt = rewriter.getContext();
871 Value basePtrI64;
872 // Load result or Store valye Type can be vector or scalar.
873 Type valOrResTy;
874 if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>)
875 valOrResTy =
876 this->getTypeConverter()->convertType(op.getResult().getType());
877 else
878 valOrResTy = adaptor.getValue().getType();
879 VectorType valOrResVecTy = dyn_cast<VectorType>(valOrResTy);
880 bool hasScalarVal = !valOrResVecTy;
881 int64_t elemBitWidth =
882 hasScalarVal ? valOrResTy.getIntOrFloatBitWidth()
883 : valOrResVecTy.getElementType().getIntOrFloatBitWidth();
884 // Element type must be multiple of 8 bits.
885 if (elemBitWidth % 8 != 0)
886 return rewriter.notifyMatchFailure(
887 op, "Expected element type bit width to be multiple of 8.");
888 int64_t elemByteSize = elemBitWidth / 8;
889 // Default memory space is global.
890 LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
891 ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::Global));
892 // Base pointer can come from source (load) or dest (store).
893 // If they are memrefs, we use their memory space.
894 if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>) {
895 basePtrI64 = adaptor.getSource();
896 if (auto memRefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
897 FailureOr<unsigned> addrSpace =
898 getNumericMemorySpace(memRefTy.getMemorySpace());
899 if (failed(addrSpace))
900 return rewriter.notifyMatchFailure(
901 op, "Unsupported memref memory space attribute.");
902 if (*addrSpace != 0)
903 ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
904 }
905 } else {
906 basePtrI64 = adaptor.getDest();
907 if (auto memRefTy = dyn_cast<MemRefType>(op.getDest().getType())) {
908 FailureOr<unsigned> addrSpace =
909 getNumericMemorySpace(memRefTy.getMemorySpace());
910 if (failed(addrSpace))
911 return rewriter.notifyMatchFailure(
912 op, "Unsupported memref memory space attribute.");
913 if (*addrSpace != 0)
914 ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
915 }
916 }
917 // Base pointer is passed as i32 or i64 by adaptor, cast to i64 if needed.
918 if (basePtrI64.getType() != rewriter.getI64Type()) {
919 basePtrI64 = arith::ExtUIOp::create(rewriter, loc, rewriter.getI64Type(),
920 basePtrI64);
921 }
922 Value mask = adaptor.getMask();
923
924 // Coalesce a lane's multi-element access into one block access.
925 //
926 // Distribution gives every element its own offset and mask bit, so a lane
927 // that takes D neighbouring elements arrives with `vector<D>` offsets and
928 // mask. One block access needs one base offset and one mask bit, so take
929 // both from element 0. A non-uniform mask cannot be reduced to one bit, so
930 // it stays a vector and fails to match below.
931 auto origOffsetsTy = dyn_cast<VectorType>(op.getOffsets().getType());
932 if (isa<VectorType>(offset.getType()) && origOffsetsTy && valOrResVecTy &&
933 origOffsetsTy.getNumElements() == valOrResVecTy.getNumElements() &&
934 isUniformMask(op.getMask())) {
935 offset = vector::ExtractOp::create(rewriter, loc, offset,
936 ArrayRef<int64_t>{0});
937 if (isa<VectorType>(mask.getType()))
938 mask = vector::ExtractOp::create(rewriter, loc, mask,
939 ArrayRef<int64_t>{0});
940 }
941
942 if (dyn_cast<VectorType>(offset.getType())) {
943 // Offset needs be scalar. Single element vector is converted to scalar
944 // by type converter.
945 return rewriter.notifyMatchFailure(op, "Expected offset to be a scalar.");
946 } else {
947 // If offset is provided, we add them to the base pointer.
948 // Offset is in number of elements, we need to multiply by
949 // element byte size.
950 basePtrI64 =
951 addOffsetToBaseAddr(rewriter, loc, basePtrI64, offset, elemByteSize);
952 }
953 // Convert base pointer (i64) to LLVM pointer type.
954 Value basePtrLLVM =
955 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
956
957 Value maskForLane;
958 VectorType maskVecTy = dyn_cast<VectorType>(mask.getType());
959 if (maskVecTy) {
960 // Mask needs be scalar. Single element vector is converted to scalar by
961 // type converter.
962 return rewriter.notifyMatchFailure(op, "Expected mask to be a scalar.");
963 } else
964 maskForLane = mask;
965 if constexpr (std::is_same_v<OpType, xegpu::LoadGatherOp>) {
966 scf::IfOp ifOp = scf::IfOp::create(rewriter, loc, {valOrResTy},
967 maskForLane, true, true);
968 // If mask is true,- then clause - load from memory and yield.
969 rewriter.setInsertionPointToStart(&ifOp.getThenRegion().front());
970 if (!hasScalarVal)
971 valOrResTy = VectorType::get({valOrResVecTy.getNumElements()},
972 valOrResVecTy.getElementType());
973 Value loaded =
974 LLVM::LoadOp::create(rewriter, loc, valOrResTy, basePtrLLVM);
975 // Set cache control attribute on the load operation.
977 "cache_control", xevm::LoadCacheControlAttr::get(
978 ctxt, translateLoadXeGPUCacheHint(
979 op.getL1Hint(), op.getL3Hint())));
980 scf::YieldOp::create(rewriter, loc, ValueRange{loaded});
981 rewriter.setInsertionPointToStart(&ifOp.getElseRegion().front());
982 // If mask is false - else clause -yield a vector of zeros.
983 auto eTy = hasScalarVal ? valOrResTy : valOrResVecTy.getElementType();
984 TypedAttr eVal;
985 if (eTy.isFloat())
986 eVal = FloatAttr::get(eTy, 0.0);
987 else
988 eVal = IntegerAttr::get(eTy, 0);
989 if (hasScalarVal)
990 loaded = arith::ConstantOp::create(rewriter, loc, eVal);
991 else
992 loaded = arith::ConstantOp::create(
993 rewriter, loc, DenseElementsAttr::get(valOrResVecTy, eVal));
994 scf::YieldOp::create(rewriter, loc, ValueRange{loaded});
995 rewriter.replaceOp(op, ifOp.getResult(0));
996 } else {
997 // If mask is true, perform the store.
998 scf::IfOp ifOp = scf::IfOp::create(rewriter, loc, maskForLane, false);
999 auto body = ifOp.getBody();
1000 rewriter.setInsertionPointToStart(body);
1001 auto storeOp =
1002 LLVM::StoreOp::create(rewriter, loc, adaptor.getValue(), basePtrLLVM);
1003 // Set cache control attribute on the store operation.
1004 storeOp.getOperation()->setDiscardableAttr(
1005 "cache_control", xevm::StoreCacheControlAttr::get(
1006 ctxt, translateStoreXeGPUCacheHint(
1007 op.getL1Hint(), op.getL3Hint())));
1008 rewriter.eraseOp(op);
1009 }
1010 return success();
1011 }
1012};
1013
1014class CreateMemDescOpPattern final
1015 : public OpConversionPattern<xegpu::CreateMemDescOp> {
1016public:
1017 using OpConversionPattern<xegpu::CreateMemDescOp>::OpConversionPattern;
1018 LogicalResult
1019 matchAndRewrite(xegpu::CreateMemDescOp op, OpAdaptor adaptor,
1020 ConversionPatternRewriter &rewriter) const override {
1021
1022 rewriter.replaceOp(op, adaptor.getSource());
1023 return success();
1024 }
1025};
1026
1027/// Returns the shape of the largest piece of a matrix op payload that one LLVM
1028/// access can move, as an access only moves consecutive elements.
1029/// Examples:
1030/// mem_desc<16x2xf32> (stride [2, 1], row-major)
1031/// payload [1, 2] -> [1, 2] one access of 2 elements
1032/// payload [2, 1] -> [1, 1] 2 accesses of 1 element
1033/// mem_desc<16x2xf32, stride = [1, 16]> (column-major)
1034/// payload [4, 1] -> [4, 1] one access of 4 elements
1035/// payload [2, 2] -> [1, 1] 4 accesses of 1 element; each column is
1036/// consecutive in memory but not in the
1037/// row-major flattened vector
1038///
1039/// TODO: On a blocked layout memory is only consecutive inside a block, so a
1040/// piece that crosses a block boundary along the stride-1 dim is not
1041/// contiguous. Blocks are not taken into account yet.
1043getContiguousPayloadShape(ArrayRef<int64_t> payloadShape, size_t numOffsets,
1044 xegpu::MemDescType mdescTy) {
1045 SmallVector<int64_t> shape(payloadShape);
1046 int64_t rank = shape.size();
1047 if (rank != static_cast<int64_t>(numOffsets))
1048 return shape;
1049
1050 int64_t fastestDim = 0;
1051 for (auto [dim, strideAttr] :
1052 llvm::enumerate(mdescTy.getStrideAttr().getValue())) {
1053 if (cast<IntegerAttr>(strideAttr).getInt() == 1) {
1054 fastestDim = dim;
1055 break;
1056 }
1057 }
1058
1059 // A slice that runs along fastestDim is only consecutive in the vector if
1060 // every dim to its right has size 1.
1061 bool spansFastestDim =
1062 llvm::all_of(llvm::seq<int64_t>(fastestDim + 1, rank),
1063 [&](int64_t d) { return payloadShape[d] == 1; });
1064 for (int64_t d = 0; d < rank; ++d)
1065 shape[d] = spansFastestDim && d == fastestDim ? payloadShape[d] : 1;
1066
1067 return shape;
1068}
1069
1070template <typename OpType,
1071 typename = std::enable_if_t<llvm::is_one_of<
1072 OpType, xegpu::LoadMatrixOp, xegpu::StoreMatrixOp>::value>>
1073class LoadStoreMatrixToXeVMPattern : public OpConversionPattern<OpType> {
1074 using OpConversionPattern<OpType>::OpConversionPattern;
1075
1076 /// Lowers `op` to one LLVM access per `sliceShape`-sized piece of its
1077 /// payload, each at its own address. `basePtrLLVM` already addresses the
1078 /// first piece. A payload that is one contiguous piece, which is the common
1079 /// case, gives a single access of the whole payload.
1080 LogicalResult emitContiguousAccesses(
1081 OpType op, typename OpType::Adaptor adaptor,
1082 ConversionPatternRewriter &rewriter, xegpu::MemDescType mdescTy,
1083 ArrayRef<OpFoldResult> offsets, ArrayRef<int64_t> payloadShape,
1084 ArrayRef<int64_t> sliceShape, Value baseAddr32, Value basePtrLLVM,
1085 LLVM::LLVMPointerType ptrTypeLLVM, int64_t elemByteSize) const {
1086 Location loc = op.getLoc();
1087 int64_t rank = payloadShape.size();
1088 SmallVector<int64_t> sliceGrid(rank);
1089 for (int64_t d = 0; d < rank; ++d)
1090 sliceGrid[d] = payloadShape[d] / sliceShape[d];
1091 int64_t numSlices = computeProduct(sliceGrid);
1092 int64_t sliceLen = computeProduct(sliceShape);
1093 SmallVector<int64_t> payloadStrides = computeStrides(payloadShape);
1094 SmallVector<int64_t> gridStrides = computeStrides(sliceGrid);
1095
1096 // The payload type LLVM load/store accepts: f8E8M0FNU and friends become
1097 // same-width integers, vector<1> becomes a scalar. Operands arrive
1098 // converted in the adaptor, results do not.
1099 Type payloadTy;
1100 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>)
1101 payloadTy =
1102 this->getTypeConverter()->convertType(op.getResult().getType());
1103 else
1104 payloadTy = adaptor.getData().getType();
1105 auto payloadVecTy = dyn_cast<VectorType>(payloadTy);
1106
1107 Type sliceTy = payloadTy;
1108 if (numSlices != 1) {
1109 if (!payloadVecTy)
1110 return rewriter.notifyMatchFailure(
1111 op, "Expected a vector payload to slice.");
1112 sliceTy = sliceLen == 1 ? payloadVecTy.getElementType()
1113 : VectorType::get({sliceLen},
1114 payloadVecTy.getElementType());
1115 }
1116
1117 // Addresses the slice `delta` elements into the payload.
1118 auto slicePtr = [&](ArrayRef<int64_t> delta) -> Value {
1119 if (llvm::all_of(delta, [](int64_t d) { return d == 0; }))
1120 return basePtrLLVM;
1121 SmallVector<OpFoldResult> sliceOffsets;
1122 for (auto [base, d] : llvm::zip_equal(offsets, delta)) {
1123 if (d == 0)
1124 sliceOffsets.push_back(base);
1125 else if (auto attr = dyn_cast<Attribute>(base))
1126 sliceOffsets.push_back(
1127 rewriter.getIndexAttr(cast<IntegerAttr>(attr).getInt() + d));
1128 else
1129 sliceOffsets.push_back(
1130 OpFoldResult(arith::AddIOp::create(
1131 rewriter, loc, cast<Value>(base),
1132 arith::ConstantIndexOp::create(rewriter, loc, d))
1133 .getResult()));
1134 }
1135 Value sliceOffset = mdescTy.getLinearOffsets(rewriter, loc, sliceOffsets);
1136 sliceOffset = arith::IndexCastUIOp::create(
1137 rewriter, loc, rewriter.getI32Type(), sliceOffset);
1138 Value slicePtrI32 = addOffsetToBaseAddr(rewriter, loc, baseAddr32,
1139 sliceOffset, elemByteSize);
1140 return LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, slicePtrI32);
1141 };
1142
1143 Value result;
1144 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>)
1145 if (numSlices != 1)
1146 result = arith::ConstantOp::create(rewriter, loc, payloadVecTy,
1147 rewriter.getZeroAttr(payloadVecTy));
1148
1149 for (int64_t slice = 0; slice < numSlices; ++slice) {
1150 SmallVector<int64_t> gridCoords = delinearize(slice, gridStrides);
1151 SmallVector<int64_t> delta(rank);
1152 for (int64_t d = 0; d < rank; ++d)
1153 delta[d] = gridCoords[d] * sliceShape[d];
1154 int64_t payloadOffset = linearize(delta, payloadStrides);
1155 Value ptr = slicePtr(delta);
1156
1157 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
1158 Value loaded = LLVM::LoadOp::create(rewriter, loc, sliceTy, ptr);
1159 if (numSlices == 1)
1160 result = loaded;
1161 else if (sliceLen == 1)
1162 result = vector::InsertOp::create(rewriter, loc, loaded, result,
1163 ArrayRef<int64_t>{payloadOffset});
1164 else
1165 result = vector::InsertStridedSliceOp::create(
1166 rewriter, loc, loaded, result, ArrayRef<int64_t>{payloadOffset},
1167 ArrayRef<int64_t>{1});
1168 } else {
1169 Value data = adaptor.getData();
1170 if (numSlices != 1) {
1171 if (sliceLen == 1)
1172 data = vector::ExtractOp::create(rewriter, loc, data,
1173 ArrayRef<int64_t>{payloadOffset});
1174 else
1175 data = vector::ExtractStridedSliceOp::create(
1176 rewriter, loc, data, ArrayRef<int64_t>{payloadOffset},
1177 ArrayRef<int64_t>{sliceLen}, ArrayRef<int64_t>{1});
1178 }
1179 LLVM::StoreOp::create(rewriter, loc, data, ptr);
1180 }
1181 }
1182
1183 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>)
1184 rewriter.replaceOp(op, result);
1185 else
1186 rewriter.eraseOp(op);
1187 return success();
1188 }
1189 LogicalResult
1190 matchAndRewrite(OpType op, typename OpType::Adaptor adaptor,
1191 ConversionPatternRewriter &rewriter) const override {
1192
1193 SmallVector<OpFoldResult> offsets = op.getMixedOffsets();
1194 if (offsets.empty())
1195 return rewriter.notifyMatchFailure(op, "Expected offset to be provided.");
1196
1197 auto loc = op.getLoc();
1198 auto ctxt = rewriter.getContext();
1199 Value baseAddr32 = adaptor.getMemDesc();
1200 Value mdescVal = op.getMemDesc();
1201 // Load result or Store value Type can be vector or scalar.
1202 Type dataTy;
1203 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
1204 Type resType = op.getResult().getType();
1205 // Some transforms may leave unit dimension in the 2D vector, adaptors do
1206 // not catch it for results.
1207 if (auto vecType = dyn_cast<VectorType>(resType))
1208 resType = VectorType::get({vecType.getNumElements()},
1209 vecType.getElementType());
1210 dataTy = resType;
1211 } else
1212 dataTy = adaptor.getData().getType();
1213 VectorType valOrResVecTy = dyn_cast<VectorType>(dataTy);
1214 if (!valOrResVecTy)
1215 valOrResVecTy = VectorType::get(1, dataTy);
1216
1217 int64_t elemBitWidth =
1218 valOrResVecTy.getElementType().getIntOrFloatBitWidth();
1219 // Element type must be multiple of 8 bits.
1220 if (elemBitWidth % 8 != 0)
1221 return rewriter.notifyMatchFailure(
1222 op, "Expected element type bit width to be multiple of 8.");
1223 int64_t elemByteSize = elemBitWidth / 8;
1224
1225 // Default memory space is SLM.
1226 LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
1227 ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::SLM));
1228
1229 auto mdescTy = cast<xegpu::MemDescType>(mdescVal.getType());
1230
1231 Value linearOffset = mdescTy.getLinearOffsets(rewriter, loc, offsets);
1232 linearOffset = arith::IndexCastUIOp::create(
1233 rewriter, loc, rewriter.getI32Type(), linearOffset);
1234 Value basePtrI32 = addOffsetToBaseAddr(rewriter, loc, baseAddr32,
1235 linearOffset, elemByteSize);
1236
1237 // convert base pointer (i32) to LLVM pointer type
1238 Value basePtrLLVM =
1239 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI32);
1240
1241 if (op.getSubgroupBlockIoAttr()) {
1242 // if the attribute 'subgroup_block_io' is set to true, it lowers to
1243 // xevm.blockload
1244
1245 Type intElemTy = rewriter.getIntegerType(elemBitWidth);
1246 VectorType intVecTy =
1247 VectorType::get(valOrResVecTy.getShape(), intElemTy);
1248
1249 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>) {
1250 Value loadOp = xevm::BlockLoadOp::create(
1251 rewriter, loc, intVecTy, basePtrLLVM, /*cache_control=*/nullptr);
1252 if (intVecTy != valOrResVecTy) {
1253 loadOp =
1254 vector::BitCastOp::create(rewriter, loc, valOrResVecTy, loadOp);
1255 }
1256 rewriter.replaceOp(op, loadOp);
1257 } else {
1258 Value dataToStore = adaptor.getData();
1259 if (valOrResVecTy != intVecTy) {
1260 dataToStore =
1261 vector::BitCastOp::create(rewriter, loc, intVecTy, dataToStore);
1262 }
1263 xevm::BlockStoreOp::create(rewriter, loc, basePtrLLVM, dataToStore,
1264 nullptr);
1265 rewriter.eraseOp(op);
1266 }
1267 return success();
1268 }
1269
1270 if (valOrResVecTy.getNumElements() >= 1) {
1271 auto chipOpt = xegpu::getChipStr(op);
1272 if (!chipOpt ||
1273 (*chipOpt != "pvc" && *chipOpt != "bmg" && *chipOpt != "cri")) {
1274 // the lowering for chunk load only works for pvc, bmg or cri
1275 return rewriter.notifyMatchFailure(
1276 op, "The lowering is specific to pvc, bmg or cri.");
1277 }
1278 }
1279
1280 // The payload shape says where in the matrix each element lives, which the
1281 // adaptor has already linearized away.
1282 ArrayRef<int64_t> payloadShape;
1283 Type opPayloadTy;
1284 if constexpr (std::is_same_v<OpType, xegpu::LoadMatrixOp>)
1285 opPayloadTy = op.getResult().getType();
1286 else
1287 opPayloadTy = op.getData().getType();
1288 if (auto opPayloadVecTy = dyn_cast<VectorType>(opPayloadTy))
1289 payloadShape = opPayloadVecTy.getShape();
1290
1291 return emitContiguousAccesses(
1292 op, adaptor, rewriter, mdescTy, offsets, payloadShape,
1293 getContiguousPayloadShape(payloadShape, offsets.size(), mdescTy),
1294 baseAddr32, basePtrLLVM, ptrTypeLLVM, elemByteSize);
1295 }
1296};
1297
1298class PrefetchToXeVMPattern : public OpConversionPattern<xegpu::PrefetchOp> {
1299 using Base::Base;
1300 LogicalResult
1301 matchAndRewrite(xegpu::PrefetchOp op, xegpu::PrefetchOp::Adaptor adaptor,
1302 ConversionPatternRewriter &rewriter) const override {
1303 auto loc = op.getLoc();
1304 auto ctxt = rewriter.getContext();
1305 Value basePtrI64 = adaptor.getSource();
1306 // Base pointer is passed as i32 or i64 by adaptor, cast to i64 if needed.
1307 if (basePtrI64.getType() != rewriter.getI64Type())
1308 basePtrI64 = arith::ExtUIOp::create(rewriter, loc, rewriter.getI64Type(),
1309 basePtrI64);
1310 Value offsets = adaptor.getOffsets();
1311 if (offsets) {
1312 VectorType offsetsVecTy = dyn_cast<VectorType>(offsets.getType());
1313 if (offsetsVecTy) {
1314 // Offset needs be scalar.
1315 return rewriter.notifyMatchFailure(op,
1316 "Expected offsets to be a scalar.");
1317 } else {
1318 int64_t elemBitWidth{0};
1319 int64_t elemByteSize;
1320 // Element byte size can come from two sources:
1321 if (auto memRefTy = dyn_cast<MemRefType>(op.getSourceType())) {
1322 // If memref is available, we use its element type to
1323 // determine element byte size.
1324 elemBitWidth = memRefTy.getElementType().getIntOrFloatBitWidth();
1325 } else {
1326 // Otherwise, we use the provided offset byte alignment.
1327 elemByteSize = *op.getOffsetAlignByte();
1328 }
1329 if (elemBitWidth != 0) {
1330 if (elemBitWidth % 8 != 0)
1331 return rewriter.notifyMatchFailure(
1332 op, "Expected element type bit width to be multiple of 8.");
1333 elemByteSize = elemBitWidth / 8;
1334 }
1335 basePtrI64 = addOffsetToBaseAddr(rewriter, loc, basePtrI64, offsets,
1336 elemByteSize);
1337 }
1338 }
1339 // Default memory space is global.
1340 LLVM::LLVMPointerType ptrTypeLLVM = LLVM::LLVMPointerType::get(
1341 ctxt, getNumericXeVMAddrSpace(xegpu::MemorySpace::Global));
1342 // If source is a memref, we use its memory space.
1343 if (auto memRefTy = dyn_cast<MemRefType>(op.getSource().getType())) {
1344 FailureOr<unsigned> addrSpace =
1345 getNumericMemorySpace(memRefTy.getMemorySpace());
1346 if (failed(addrSpace))
1347 return rewriter.notifyMatchFailure(
1348 op, "Unsupported memref memory space attribute.");
1349 if (*addrSpace != 0)
1350 ptrTypeLLVM = LLVM::LLVMPointerType::get(ctxt, *addrSpace);
1351 }
1352 // Convert base pointer (i64) to LLVM pointer type.
1353 Value ptrLLVM =
1354 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
1355 // Create the prefetch op with cache control attribute.
1356 xevm::PrefetchOp::create(
1357 rewriter, loc, ptrLLVM,
1358 xevm::LoadCacheControlAttr::get(
1359 ctxt, translateLoadXeGPUCacheHint(op.getL1Hint(), op.getL3Hint())));
1360 rewriter.eraseOp(op);
1361 return success();
1362 }
1363};
1364
1365class FenceToXeVMPattern : public OpConversionPattern<xegpu::FenceOp> {
1366 using Base::Base;
1367 LogicalResult
1368 matchAndRewrite(xegpu::FenceOp op, xegpu::FenceOp::Adaptor adaptor,
1369 ConversionPatternRewriter &rewriter) const override {
1370 auto loc = op.getLoc();
1371 xevm::MemScope memScope{xevm::MemScope::WORKGROUP};
1372 switch (op.getFenceScope()) {
1373 case xegpu::FenceScope::Workgroup:
1374 memScope = xevm::MemScope::WORKGROUP;
1375 break;
1376 case xegpu::FenceScope::GPU:
1377 memScope = xevm::MemScope::DEVICE;
1378 break;
1379 }
1380 xevm::AddrSpace addrSpace{xevm::AddrSpace::GLOBAL};
1381 switch (op.getMemoryKind()) {
1382 case xegpu::MemorySpace::Global:
1383 addrSpace = xevm::AddrSpace::GLOBAL;
1384 break;
1385 case xegpu::MemorySpace::SLM:
1386 addrSpace = xevm::AddrSpace::SHARED;
1387 break;
1388 }
1389 xevm::MemfenceOp::create(rewriter, loc, memScope, addrSpace);
1390 rewriter.eraseOp(op);
1391 return success();
1392 }
1393};
1394
1395static auto encodePrecision = [](Type type) -> xevm::ElemType {
1396 if (type.isBF16())
1397 return xevm::ElemType::BF16;
1398 else if (type.isF16())
1399 return xevm::ElemType::F16;
1400 else if (type.isTF32())
1401 return xevm::ElemType::TF32;
1402 else if (type.isInteger(8)) {
1403 if (type.isUnsignedInteger())
1404 return xevm::ElemType::U8;
1405 return xevm::ElemType::S8;
1406 } else if (type.isF32())
1407 return xevm::ElemType::F32;
1408 else if (type.isInteger(32))
1409 return xevm::ElemType::S32;
1410 else if (type.isF8E5M2())
1411 return xevm::ElemType::BF8;
1412 else if (type.isF8E4M3FN())
1413 return xevm::ElemType::F8;
1414 else if (mlir::isa<Float4E2M1FNType>(type))
1415 return xevm::ElemType::E2M1;
1416 llvm_unreachable("add more support for ElemType");
1417};
1418
1419static unsigned getNumOperandsPerDword(xevm::ElemType pTy) {
1420 switch (pTy) {
1421 case xevm::ElemType::TF32:
1422 return 1;
1423 case xevm::ElemType::BF16:
1424 case xevm::ElemType::F16:
1425 return 2;
1426 case xevm::ElemType::U8:
1427 case xevm::ElemType::S8:
1428 case xevm::ElemType::F8:
1429 case xevm::ElemType::BF8:
1430 return 4;
1431 case xevm::ElemType::E2M1:
1432 return 8;
1433 default:
1434 llvm_unreachable("unsupported xevm::ElemType");
1435 }
1436}
1437
1438class DpasToXeVMPattern : public OpConversionPattern<xegpu::DpasOp> {
1439 using Base::Base;
1440 LogicalResult
1441 matchAndRewrite(xegpu::DpasOp op, xegpu::DpasOp::Adaptor adaptor,
1442 ConversionPatternRewriter &rewriter) const override {
1443 auto loc = op.getLoc();
1444 auto ctxt = rewriter.getContext();
1445 auto aTy = cast<VectorType>(op.getLhs().getType());
1446 auto bTy = cast<VectorType>(op.getRhs().getType());
1447 auto resultType = cast<VectorType>(op.getResultType());
1448
1449 // get the correct dpasInst by getting info from chip
1450 auto chipStr = xegpu::getChipStr(op);
1451 if (!chipStr)
1452 return rewriter.notifyMatchFailure(op, "cannot determine target chip");
1453
1454 const auto *uArch = mlir::xegpu::uArch::getUArch(*chipStr);
1455 if (!uArch)
1456 return rewriter.notifyMatchFailure(op, "unsupported target uArch");
1457
1458 auto *dpasInst = const_cast<xegpu::uArch::SubgroupMatrixMultiplyAcc *>(
1459 llvm::dyn_cast_or_null<xegpu::uArch::SubgroupMatrixMultiplyAcc>(
1460 uArch->getInstruction(
1461 xegpu::uArch::InstructionKind::SubgroupMatrixMultiplyAcc)));
1462 if (!dpasInst)
1463 return rewriter.notifyMatchFailure(op,
1464 "DPAS not supported by target uArch");
1465
1466 auto checkSupportedTypes = [&](VectorType vecTy,
1467 xegpu::uArch::MMAOpndKind kind) -> bool {
1468 auto supported = dpasInst->getSupportedTypes(*ctxt, kind);
1469 return llvm::find(supported, vecTy.getElementType()) != supported.end();
1470 };
1471
1472 if (!checkSupportedTypes(aTy, xegpu::uArch::MMAOpndKind::MatrixA))
1473 return rewriter.notifyMatchFailure(
1474 op, "A-matrix element type not supported by target uArch");
1475 if (!checkSupportedTypes(bTy, xegpu::uArch::MMAOpndKind::MatrixB))
1476 return rewriter.notifyMatchFailure(
1477 op, "B-matrix element type not supported by target uArch");
1478 // NOTE: Supported types for MatrixC and MatrixD are identical
1479 if (!checkSupportedTypes(resultType, xegpu::uArch::MMAOpndKind::MatrixD))
1480 return rewriter.notifyMatchFailure(
1481 op, "result/accumulator element type not supported by target uArch");
1482
1483 xevm::ElemType precATy = encodePrecision(aTy.getElementType());
1484 xevm::ElemType precBTy = encodePrecision(bTy.getElementType());
1485 Value c = op.getAcc();
1486 if (!c) {
1487 auto elementTy = resultType.getElementType();
1488 Attribute initValueAttr;
1489 if (isa<FloatType>(elementTy))
1490 initValueAttr = FloatAttr::get(elementTy, 0.0);
1491 else
1492 initValueAttr = IntegerAttr::get(elementTy, 0);
1493 c = arith::ConstantOp::create(
1494 rewriter, loc, DenseElementsAttr::get(resultType, initValueAttr));
1495 }
1496
1497 Value aVec = op.getLhs();
1498 Value bVec = op.getRhs();
1499 auto cvecty = cast<VectorType>(c.getType());
1500 xevm::ElemType precCTy = encodePrecision(cvecty.getElementType());
1501 xevm::ElemType precDTy = encodePrecision(resultType.getElementType());
1502 VectorType cNty =
1503 VectorType::get(cvecty.getNumElements(), cvecty.getElementType());
1504 if (cvecty != cNty)
1505 c = vector::ShapeCastOp::create(rewriter, loc, cNty, c);
1506 Value dpasRes = xevm::MMAOp::create(
1507 rewriter, loc, cNty, aVec, bVec, c,
1508 xevm::MMAShapeAttr::get(ctxt, cvecty.getNumElements(), executionSize,
1509 systolicDepth *
1510 getNumOperandsPerDword(precATy)),
1511 xevm::MMATypesAttr::get(ctxt, precDTy, precATy, precBTy, precCTy));
1512 if (cvecty != cNty)
1513 dpasRes = vector::ShapeCastOp::create(rewriter, loc, resultType, dpasRes);
1514 rewriter.replaceOp(op, dpasRes);
1515 return success();
1516 }
1517};
1518
1519static std::optional<LLVM::AtomicBinOp>
1520matchSimpleAtomicOp(arith::AtomicRMWKind arithKind) {
1521 switch (arithKind) {
1522 case arith::AtomicRMWKind::addf:
1523 return LLVM::AtomicBinOp::fadd;
1524 case arith::AtomicRMWKind::addi:
1525 return LLVM::AtomicBinOp::add;
1526 case arith::AtomicRMWKind::assign:
1527 return LLVM::AtomicBinOp::xchg;
1528 case arith::AtomicRMWKind::maximumf:
1529 return LLVM::AtomicBinOp::fmax;
1530 case arith::AtomicRMWKind::maxs:
1531 return LLVM::AtomicBinOp::max;
1532 case arith::AtomicRMWKind::maxu:
1533 return LLVM::AtomicBinOp::umax;
1534 case arith::AtomicRMWKind::minimumf:
1535 return LLVM::AtomicBinOp::fmin;
1536 case arith::AtomicRMWKind::mins:
1537 return LLVM::AtomicBinOp::min;
1538 case arith::AtomicRMWKind::minu:
1539 return LLVM::AtomicBinOp::umin;
1540 case arith::AtomicRMWKind::ori:
1541 return LLVM::AtomicBinOp::_or;
1542 case arith::AtomicRMWKind::andi:
1543 return LLVM::AtomicBinOp::_and;
1544 default:
1545 return std::nullopt;
1546 }
1547}
1548
1549class AtomicRMWToXeVMPattern : public OpConversionPattern<xegpu::AtomicRMWOp> {
1550 using Base::Base;
1551 LogicalResult
1552 matchAndRewrite(xegpu::AtomicRMWOp op, xegpu::AtomicRMWOp::Adaptor adaptor,
1553 ConversionPatternRewriter &rewriter) const override {
1554 auto loc = op.getLoc();
1555 auto ctxt = rewriter.getContext();
1556 auto tdesc = op.getTensorDesc().getType();
1557 auto ptrTypeLLVM = LLVM::LLVMPointerType::get(
1558 ctxt, getNumericXeVMAddrSpace(tdesc.getMemorySpace()));
1559 Value basePtrI64 = arith::IndexCastOp::create(
1560 rewriter, loc, rewriter.getI64Type(), adaptor.getTensorDesc());
1561 Value basePtrLLVM =
1562 LLVM::IntToPtrOp::create(rewriter, loc, ptrTypeLLVM, basePtrI64);
1563 VectorType srcOrDstVecTy = cast<VectorType>(op.getValue().getType());
1564 VectorType srcOrDstFlatVecTy = VectorType::get(
1565 srcOrDstVecTy.getNumElements(), srcOrDstVecTy.getElementType());
1566 Value srcFlatVec = vector::ShapeCastOp::create(
1567 rewriter, loc, srcOrDstFlatVecTy, op.getValue());
1568 auto atomicKind = matchSimpleAtomicOp(op.getKind());
1569 assert(atomicKind.has_value());
1570 Value resVec = srcFlatVec;
1571 for (int i = 0; i < srcOrDstVecTy.getNumElements(); i++) {
1572 auto val = vector::ExtractOp::create(rewriter, loc, resVec, i);
1573 Value idx = LLVM::ConstantOp::create(rewriter, loc, rewriter.getI64Type(),
1574 rewriter.getI64IntegerAttr(i));
1575 Value currPtr =
1576 LLVM::GEPOp::create(rewriter, loc, ptrTypeLLVM,
1577 srcOrDstVecTy.getElementType(), basePtrLLVM, idx);
1578 Value newVal =
1579 LLVM::AtomicRMWOp::create(rewriter, loc, atomicKind.value(), currPtr,
1580 val, LLVM::AtomicOrdering::seq_cst);
1581 resVec = vector::InsertOp::create(rewriter, loc, newVal, resVec, i);
1582 }
1583 rewriter.replaceOp(op, resVec);
1584 return success();
1585 }
1586};
1587
1588class DpasMxToXeVMPattern : public OpConversionPattern<xegpu::DpasMxOp> {
1589 using Base::Base;
1590 LogicalResult
1591 matchAndRewrite(xegpu::DpasMxOp op, xegpu::DpasMxOp::Adaptor adaptor,
1592 ConversionPatternRewriter &rewriter) const override {
1593 auto loc = op.getLoc();
1594 auto ctxt = rewriter.getContext();
1595 auto aTy = op.getA().getType();
1596 auto bTy = op.getB().getType();
1597 auto resVecTy =
1598 cast<VectorType>(getTypeConverter()->convertType(op.getType()));
1599
1600 auto chipStr = xegpu::getChipStr(op);
1601 if (!chipStr)
1602 return rewriter.notifyMatchFailure(op, "cannot determine target chip");
1603
1604 const auto *uArch = xegpu::uArch::getUArch(*chipStr);
1605 if (!uArch)
1606 return rewriter.notifyMatchFailure(op, "unsupported target uArch");
1607
1608 // TODO: Add supported shape check
1609
1610 xevm::ElemType precATy = encodePrecision(aTy.getElementType());
1611 xevm::ElemType precBTy = encodePrecision(bTy.getElementType());
1612 Value c = adaptor.getAcc();
1613 if (!c) {
1614 auto elementTy = resVecTy.getElementType();
1615 Attribute initValueAttr;
1616 if (isa<FloatType>(elementTy))
1617 initValueAttr = FloatAttr::get(elementTy, 0.0);
1618 else
1619 initValueAttr = IntegerAttr::get(elementTy, 0);
1620 c = arith::ConstantOp::create(
1621 rewriter, loc, DenseElementsAttr::get(resVecTy, initValueAttr));
1622 }
1623
1624 Value aVec = adaptor.getA();
1625 Value bVec = adaptor.getB();
1626 auto aVecTy = cast<VectorType>(aVec.getType());
1627 auto bVecTy = cast<VectorType>(bVec.getType());
1628 if (aVecTy.getElementTypeBitWidth() == 4)
1629 aVec = vector::BitCastOp::create(
1630 rewriter, loc,
1631 VectorType::get(aVecTy.getNumElements() / 2, rewriter.getI8Type()),
1632 aVec);
1633 if (bVecTy.getElementTypeBitWidth() == 4)
1634 bVec = vector::BitCastOp::create(
1635 rewriter, loc,
1636 VectorType::get(bVecTy.getNumElements() / 2, rewriter.getI8Type()),
1637 bVec);
1638 auto cVecTy = cast<VectorType>(c.getType());
1639 xevm::ElemType precCTy = encodePrecision(cVecTy.getElementType());
1640 xevm::ElemType precDTy = encodePrecision(resVecTy.getElementType());
1641 Value scaleA = adaptor.getScaleA();
1642 Value scaleB = adaptor.getScaleB();
1643 Value dpasMxRes = xevm::MMAMxOp::create(
1644 rewriter, loc, resVecTy, aVec, bVec, scaleA, scaleB, c,
1645 xevm::MMAShapeAttr::get(ctxt, cVecTy.getNumElements(), executionSize,
1646 systolicDepth *
1647 getNumOperandsPerDword(precATy)),
1648 xevm::MMATypesAttr::get(ctxt, precDTy, precATy, precBTy, precCTy));
1649 rewriter.replaceOp(op, dpasMxRes);
1650 return success();
1651 }
1652};
1653
1654//===----------------------------------------------------------------------===//
1655// arith.extf / arith.truncf to xevm.extf / xevm.truncf
1656//===----------------------------------------------------------------------===//
1657//
1658// Micro-scaling (MX) GEMM lowering breaks arith.scaling_extf/scaling_truncf
1659// into plain arith.extf/arith.truncf whose narrow side uses one of the MX float
1660// formats (f8E5M2, f8E4M3FN or f4E2M1FN). These narrow floats have no native
1661// LLVM support, so the conversions are mapped onto the dedicated xevm.extf /
1662// xevm.truncf ops which lower to hardware builtins. The f8E8M0FNU scale type is
1663// intentionally not handled here: it is expanded into integer arithmetic by
1664// arith-expand before this pass runs.
1665
1666// xevm.extf / xevm.truncf only convert between the MX narrow floats and
1667// f16/bf16, and the underlying builtins operate on exactly 16 f16/bf16 values.
1668static constexpr int64_t kXeVMExtfTruncfNumElems = 16;
1669
1670// Maps a narrow MX float element type to the matching xevm.extf source enum.
1671static std::optional<xevm::ExtfSrcElemTypes> getExtfNarrowType(Type etype) {
1672 if (isa<Float8E5M2Type>(etype))
1673 return xevm::ExtfSrcElemTypes::BF8;
1674 if (isa<Float8E4M3FNType>(etype))
1675 return xevm::ExtfSrcElemTypes::F8;
1676 if (isa<Float4E2M1FNType>(etype))
1677 return xevm::ExtfSrcElemTypes::E2M1;
1678 return std::nullopt;
1679}
1680
1681// Maps a narrow MX float element type to the matching xevm.truncf dest enum.
1682static std::optional<xevm::TruncfDstElemTypes> getTruncfNarrowType(Type etype) {
1683 if (isa<Float8E5M2Type>(etype))
1684 return xevm::TruncfDstElemTypes::BF8;
1685 if (isa<Float8E4M3FNType>(etype))
1686 return xevm::TruncfDstElemTypes::F8;
1687 if (isa<Float4E2M1FNType>(etype))
1688 return xevm::TruncfDstElemTypes::E2M1;
1689 return std::nullopt;
1690}
1691
1692// Returns true if `op` is an arith.extf that can be lowered to xevm.extf, i.e.
1693// a rank-1 widening from an MX narrow float to a 16-element f16/bf16 vector.
1694static bool isXeVMExtf(arith::ExtFOp op) {
1695 auto srcTy = dyn_cast<VectorType>(op.getIn().getType());
1696 auto dstTy = dyn_cast<VectorType>(op.getType());
1697 if (!srcTy || !dstTy || srcTy.getRank() != 1 || dstTy.getRank() != 1)
1698 return false;
1699 if (dstTy.getNumElements() != kXeVMExtfTruncfNumElems)
1700 return false;
1701 Type dstETy = dstTy.getElementType();
1702 if (!dstETy.isF16() && !dstETy.isBF16())
1703 return false;
1704 return getExtfNarrowType(srcTy.getElementType()).has_value();
1705}
1706
1707// Returns true if `op` is an arith.truncf that can be lowered to xevm.truncf,
1708// i.e. a rank-1 truncation from an f16/bf16 vector to an MX narrow float. The
1709// source has to hold a whole number of the fixed-size groups xevm.truncf
1710// converts at a time; wider vectors are converted in several steps.
1711static bool isXeVMTruncf(arith::TruncFOp op) {
1712 auto srcTy = dyn_cast<VectorType>(op.getIn().getType());
1713 auto dstTy = dyn_cast<VectorType>(op.getType());
1714 if (!srcTy || !dstTy || srcTy.getRank() != 1 || dstTy.getRank() != 1)
1715 return false;
1716 int64_t numElems = srcTy.getNumElements();
1717 if (numElems == 0 || numElems % kXeVMExtfTruncfNumElems != 0)
1718 return false;
1719 Type srcETy = srcTy.getElementType();
1720 if (!srcETy.isF16() && !srcETy.isBF16())
1721 return false;
1722 return getTruncfNarrowType(dstTy.getElementType()).has_value();
1723}
1724
1725class ExtfToXeVMPattern : public OpConversionPattern<arith::ExtFOp> {
1726 using Base::Base;
1727 LogicalResult
1728 matchAndRewrite(arith::ExtFOp op, OpAdaptor adaptor,
1729 ConversionPatternRewriter &rewriter) const override {
1730 if (!isXeVMExtf(op))
1731 return rewriter.notifyMatchFailure(op, "not a xevm.extf compatible extf");
1732 Location loc = op.getLoc();
1733 MLIRContext *ctx = op.getContext();
1734 auto srcVecTy = cast<VectorType>(op.getIn().getType());
1735 auto dstVecTy = cast<VectorType>(op.getType());
1736 xevm::ExtfSrcElemTypes srcEnum =
1737 *getExtfNarrowType(srcVecTy.getElementType());
1738 xevm::ExtfDstElemTypes dstEnum = dstVecTy.getElementType().isF16()
1739 ? xevm::ExtfDstElemTypes::F16
1740 : xevm::ExtfDstElemTypes::BF16;
1741 // The narrow float operand has already been type-converted to an integer
1742 // vector of the same bit width (i4 for fp4, i8 for fp8). xevm.extf takes
1743 // the values packed into an i8 vector, so re-pack fp4 (i4) operands.
1744 Value src = adaptor.getIn();
1745 auto convSrcTy = cast<VectorType>(src.getType());
1746 if (convSrcTy.getElementTypeBitWidth() == 4)
1747 src = vector::BitCastOp::create(
1748 rewriter, loc,
1749 VectorType::get(convSrcTy.getNumElements() / 2, rewriter.getI8Type()),
1750 src);
1751 Type resTy = getTypeConverter()->convertType(dstVecTy);
1752 Value res = xevm::ExtfOp::create(
1753 rewriter, loc, resTy, src, xevm::ExtfSrcElemTypeAttr::get(ctx, srcEnum),
1754 xevm::ExtfDstElemTypeAttr::get(ctx, dstEnum));
1755 rewriter.replaceOp(op, res);
1756 return success();
1757 }
1758};
1759
1760class TruncfToXeVMPattern : public OpConversionPattern<arith::TruncFOp> {
1761 using Base::Base;
1762 LogicalResult
1763 matchAndRewrite(arith::TruncFOp op, OpAdaptor adaptor,
1764 ConversionPatternRewriter &rewriter) const override {
1765 if (!isXeVMTruncf(op))
1766 return rewriter.notifyMatchFailure(op,
1767 "not a xevm.truncf compatible truncf");
1768 Location loc = op.getLoc();
1769 MLIRContext *ctx = op.getContext();
1770 auto srcVecTy = cast<VectorType>(op.getIn().getType());
1771 auto dstVecTy = cast<VectorType>(op.getType());
1772 xevm::TruncfSrcElemTypes srcEnum = srcVecTy.getElementType().isF16()
1773 ? xevm::TruncfSrcElemTypes::F16
1774 : xevm::TruncfSrcElemTypes::BF16;
1775 xevm::TruncfDstElemTypes dstEnum =
1776 *getTruncfNarrowType(dstVecTy.getElementType());
1777 auto srcEnumAttr = xevm::TruncfSrcElemTypeAttr::get(ctx, srcEnum);
1778 auto dstEnumAttr = xevm::TruncfDstElemTypeAttr::get(ctx, dstEnum);
1779
1780 // xevm.truncf lowers to instructions that convert a fixed number of
1781 // elements at a time, so a wider source is converted one group at a time
1782 // and the packed results are concatenated. Each group produces the narrow
1783 // floats packed into an i8 vector.
1784 int64_t numGroups = srcVecTy.getNumElements() / kXeVMExtfTruncfNumElems;
1785 int64_t groupBytes =
1786 kXeVMExtfTruncfNumElems * dstVecTy.getElementTypeBitWidth() / 8;
1787 Type groupTy = VectorType::get(groupBytes, rewriter.getI8Type());
1788
1789 Value src = adaptor.getIn();
1790 Value packed;
1791 if (numGroups == 1) {
1792 packed = xevm::TruncfOp::create(rewriter, loc, groupTy, src, srcEnumAttr,
1793 dstEnumAttr);
1794 } else {
1795 auto packedTy =
1796 VectorType::get(groupBytes * numGroups, rewriter.getI8Type());
1797 packed = arith::ConstantOp::create(rewriter, loc, packedTy,
1798 rewriter.getZeroAttr(packedTy));
1799 for (int64_t group = 0; group < numGroups; group++) {
1800 Value slice = vector::ExtractStridedSliceOp::create(
1801 rewriter, loc, src, group * kXeVMExtfTruncfNumElems,
1802 kXeVMExtfTruncfNumElems, /*strides=*/1);
1803 Value converted = xevm::TruncfOp::create(rewriter, loc, groupTy, slice,
1804 srcEnumAttr, dstEnumAttr);
1805 packed = vector::InsertStridedSliceOp::create(
1806 rewriter, loc, converted, packed, group * groupBytes,
1807 /*strides=*/1);
1808 }
1809 }
1810 // Re-shape to the type-converted result type (i4 vector for fp4).
1811 Type resTy = getTypeConverter()->convertType(dstVecTy);
1812 if (packed.getType() != resTy)
1813 packed = vector::BitCastOp::create(rewriter, loc, resTy, packed);
1814 rewriter.replaceOp(op, packed);
1815 return success();
1816 }
1817};
1818
1819// Hoist the i4-to-i8 part of compatible extensions through a closed
1820// data-movement graph and lane shuffle. Widening all the way to the extension's
1821// result type could exceed the bitcast-shuffle lowering's 64-bit fragment
1822// limit. Signed widening preserves the eventual signed interpretation of each
1823// nibble; unsigned widening preserves its unsigned interpretation.
1824template <typename ExtOp>
1825class HoistI4ExtensionAboveLaneShuffle final : public OpRewritePattern<ExtOp> {
1826public:
1827 using OpRewritePattern<ExtOp>::OpRewritePattern;
1828
1829 LogicalResult matchAndRewrite(ExtOp op,
1830 PatternRewriter &rewriter) const override {
1831 auto srcTy = dyn_cast<VectorType>(op.getIn().getType());
1832 auto dstTy = dyn_cast<VectorType>(op.getType());
1833 if (!srcTy || !srcTy.getElementType().isInteger(4) || !dstTy ||
1834 dstTy.getElementTypeBitWidth() < 8)
1835 return failure();
1836
1837 auto isMover = [](Operation *def) {
1838 return isa<vector::ShapeCastOp, vector::ExtractStridedSliceOp,
1839 vector::ExtractOp, vector::BroadcastOp, vector::TransposeOp>(
1840 def);
1841 };
1842 Value source = op.getIn();
1843 while (Operation *def = source.getDefiningOp()) {
1844 if (isa<xegpu::LaneShuffleOp>(def))
1845 break;
1846 if (!isMover(def))
1847 return failure();
1848 source = def->getOperand(0);
1849 }
1850 auto shuffle = source.getDefiningOp<xegpu::LaneShuffleOp>();
1851 if (!shuffle)
1852 return failure();
1853 VectorType fragmentTy = shuffle.getSourceType();
1854 int64_t numElements = fragmentTy.getNumElements();
1855 if (!fragmentTy.getElementType().isInteger(4) || numElements < 2 ||
1856 numElements > 8 || !llvm::isPowerOf2_64(numElements))
1857 return failure();
1858
1859 // Validate every use before changing IR. Widening a shared fragment is
1860 // safe only when all paths end in extensions with the same signedness.
1861 SmallVector<Operation *> movers;
1862 SmallVector<ExtOp> extensions;
1863 SmallPtrSet<Operation *, 16> visited;
1864 auto visit = [&](auto &&visit, Value value) -> LogicalResult {
1865 for (Operation *user : value.getUsers()) {
1866 if (!visited.insert(user).second)
1867 continue;
1868 if (auto ext = dyn_cast<ExtOp>(user)) {
1869 auto type = dyn_cast<VectorType>(ext.getType());
1870 if (!type || type.getElementTypeBitWidth() < 8)
1871 return failure();
1872 extensions.push_back(ext);
1873 continue;
1874 }
1875 if (!isMover(user) || user->getOperand(0) != value ||
1876 user->getNumResults() != 1 ||
1877 failed(visit(visit, user->getResult(0))))
1878 return failure();
1879 movers.push_back(user);
1880 }
1881 return success();
1882 };
1883 if (failed(visit(visit, shuffle.getResult())))
1884 return failure();
1885
1886 rewriter.setInsertionPoint(shuffle);
1887 VectorType wideTy = fragmentTy.clone(rewriter.getI8Type());
1888 Value wide =
1889 ExtOp::create(rewriter, op.getLoc(), wideTy, shuffle.getSource());
1890 Value result = xegpu::LaneShuffleOp::create(
1891 rewriter, shuffle.getLoc(), wideTy, wide, shuffle.getMode());
1892 IRMapping mapping;
1893 mapping.map(shuffle.getResult(), result);
1894 for (Operation *mover : llvm::reverse(movers)) {
1895 rewriter.setInsertionPoint(mover);
1896 Operation *clone = rewriter.clone(*mover, mapping);
1897 Type type = rewriter.getI8Type();
1898 if (auto vectorTy = dyn_cast<VectorType>(mover->getResult(0).getType()))
1899 type = vectorTy.clone(type);
1900 clone->getResult(0).setType(type);
1901 }
1902 for (ExtOp ext : extensions) {
1903 Value input = mapping.lookup(ext.getIn());
1904 if (input.getType() == ext.getType())
1905 rewriter.replaceOp(ext, input);
1906 else
1907 rewriter.modifyOpInPlace(ext,
1908 [&] { ext.getInMutable().assign(input); });
1909 }
1910 for (Operation *mover : movers)
1911 rewriter.eraseOp(mover);
1912 rewriter.eraseOp(shuffle);
1913 return success();
1914 }
1915};
1916
1917// Lowers `xegpu.lane_shuffle` to `xevm.bitcast_shuffle`.
1918//
1919// `xevm.bitcast_shuffle` concatenates the components of its operand across the
1920// subgroup, the first component of every lane first, and then hands chunks the
1921// size of a result component back out to the lanes in order. Numbering the
1922// elements of a `vector<NxT>` fragment held by lane `i` of a subgroup of size
1923// `S` by their logical position, that concatenation is exactly the `pack` mode
1924// input numbering `j * S + i`. Taking the result as a single `N * width(T)` bit
1925// scalar then hands lane `i` the logical positions `i * N .. i * N + N - 1`,
1926// which is the `pack` mode output numbering.
1927//
1928// So `pack` is a vector-to-scalar `xevm.bitcast_shuffle` followed by a bitcast
1929// back to the fragment type, and `unpack`, being its inverse, is a bitcast to
1930// the scalar followed by a scalar-to-vector `xevm.bitcast_shuffle`.
1931//
1932// `xevm.bitcast_shuffle` only accepts the integer types `i8`, `i16`, `i32` and
1933// `i64`, since it is bit-preserving and so does not depend on how the bits are
1934// interpreted. A fragment of a floating point type is therefore bitcast to a
1935// same-width integer vector on the way in and back on the way out.
1936class LaneShuffleToXeVMPattern
1937 : public OpConversionPattern<xegpu::LaneShuffleOp> {
1938 using Base::Base;
1939 LogicalResult
1940 matchAndRewrite(xegpu::LaneShuffleOp op, OpAdaptor adaptor,
1941 ConversionPatternRewriter &rewriter) const override {
1942 auto vecTy = dyn_cast<VectorType>(adaptor.getSource().getType());
1943 if (!vecTy)
1944 return rewriter.notifyMatchFailure(op, "Expected a vector fragment.");
1945 // The shuffle redistributes whole bytes between the lanes, so sub-byte
1946 // element types, fp4 in particular, cannot be shuffled. Widths without a
1947 // matching integer type the op accepts are rejected for the same reason.
1948 unsigned elemBits = vecTy.getElementTypeBitWidth();
1949 if (elemBits != 8 && elemBits != 16 && elemBits != 32 && elemBits != 64)
1950 return rewriter.notifyMatchFailure(
1951 op, "Expected an element type of 8, 16, 32 or 64 bits.");
1952 int64_t fragmentBits = vecTy.getNumElements() * elemBits;
1953 if (fragmentBits > 64 || !llvm::isPowerOf2_64(fragmentBits))
1954 return rewriter.notifyMatchFailure(
1955 op, "Expected a fragment of 8, 16, 32 or 64 bits.");
1956
1957 Location loc = op.getLoc();
1958 Type packedTy = rewriter.getIntegerType(fragmentBits);
1959 // The integer vector type the shuffle actually operates on. Equal to the
1960 // fragment type when that is already an integer vector.
1961 VectorType shuffleTy =
1962 VectorType::get(vecTy.getShape(), rewriter.getIntegerType(elemBits));
1963
1964 Value res;
1965 if (op.getMode() == xegpu::LaneShuffleMode::Pack) {
1966 Value src = adaptor.getSource();
1967 if (shuffleTy != vecTy)
1968 src = LLVM::BitcastOp::create(rewriter, loc, shuffleTy, src);
1969 res = xevm::BitcastShuffleOp::create(rewriter, loc, packedTy, src);
1970 res = LLVM::BitcastOp::create(rewriter, loc, vecTy, res);
1971 } else {
1972 Value packed =
1973 LLVM::BitcastOp::create(rewriter, loc, packedTy, adaptor.getSource());
1974 res = xevm::BitcastShuffleOp::create(rewriter, loc, shuffleTy, packed);
1975 if (shuffleTy != vecTy)
1976 res = LLVM::BitcastOp::create(rewriter, loc, vecTy, res);
1977 }
1978 rewriter.replaceOp(op, res);
1979 return success();
1980 }
1981};
1982
1983//===----------------------------------------------------------------------===//
1984// Pass Definition
1985//===----------------------------------------------------------------------===//
1986
1987struct ConvertXeGPUToXeVMPass
1988 : public impl::ConvertXeGPUToXeVMPassBase<ConvertXeGPUToXeVMPass> {
1989 using Base::Base;
1990
1991 void runOnOperation() override {
1992 MLIRContext *context = &getContext();
1993
1994 // Restrict greedy rewriting to casts and newly generated operations so
1995 // unrelated dead XeGPU ops still receive conversion diagnostics.
1996 GreedyRewriteConfig config;
1997 config.setStrictness(GreedyRewriteStrictness::ExistingAndNewOps);
1998 // Preserve extension consumers until hoisting has finished: narrow-type
1999 // legalization would otherwise obscure the chains this peephole matches.
2000 {
2001 RewritePatternSet patterns(context);
2002 patterns.add<HoistI4ExtensionAboveLaneShuffle<arith::ExtUIOp>,
2003 HoistI4ExtensionAboveLaneShuffle<arith::ExtSIOp>>(context);
2004 SmallVector<Operation *> extensions;
2005 getOperation()->walk([&](Operation *op) {
2006 if (isa<arith::ExtUIOp, arith::ExtSIOp>(op))
2007 extensions.push_back(op);
2008 });
2009 if (failed(
2010 applyOpPatternsGreedily(extensions, std::move(patterns), config)))
2011 return signalPassFailure();
2012 }
2013 // Expand the new i4-to-i8 casts into byte-level masks and shifts before
2014 // lowering. LLVM extensions on i4 vectors are not legal in the backend.
2015 {
2016 RewritePatternSet patterns(context);
2017 vector::populateVectorNarrowTypeRewritePatterns(patterns);
2020 SmallVector<Operation *> casts;
2021 getOperation()->walk([&](Operation *op) {
2022 if (isa<arith::ExtUIOp, arith::ExtSIOp, arith::TruncIOp,
2023 vector::BitCastOp>(op))
2024 casts.push_back(op);
2025 });
2026 if (failed(applyOpPatternsGreedily(casts, std::move(patterns), config)))
2027 return signalPassFailure();
2028 }
2029
2030 // XeVM type converter is based on LLVM type converter with the
2031 // following customizations.
2032 // First, type conversion rules are added for xegpu custom types,
2033 // TensorDescType and MemDescType.
2034 // Second, MemRefType is lowered to single integer type
2035 // Third, VectorType of single element or 0D is converted to vector
2036 // element type. Otherwise, vector type is flatten to 1D.
2037 LowerToLLVMOptions options(context);
2038 options.overrideIndexBitwidth(this->use64bitIndex ? 64 : 32);
2039 LLVMTypeConverter typeConverter(context, options);
2040
2041 Type xevmIndexType = typeConverter.convertType(IndexType::get(context));
2042 Type i32Type = IntegerType::get(context, 32);
2043 typeConverter.addConversion([&](VectorType type) -> Type {
2044 auto elemType = typeConverter.convertType(type.getElementType());
2045 // If the vector rank is 0 or has a single element, return the element
2046 unsigned rank = type.getRank();
2047 if (rank == 0 || type.getNumElements() == 1)
2048 return elemType;
2049 // Otherwise, convert the vector to a flat vector type.
2050 int64_t sum = llvm::product_of(type.getShape());
2051 return VectorType::get(sum, elemType);
2052 });
2053 typeConverter.addConversion([&](xegpu::TensorDescType type) -> Type {
2054 if (type.getRank() == 1)
2055 return xevmIndexType;
2056 return VectorType::get(8, i32Type);
2057 });
2058 // SLM access related type conversions.
2059 // TODO: LLVM DLTI provides clean way of representing different pointer size
2060 // based on address space. Currently pointer size of SLM access is hard
2061 // coded to 32bit. Update to use DLTI when switching overall XeGPU lowering
2062 // to use DLTI instead of use64bitIndex option used above.
2063
2064 // Convert MemDescType into i32 for SLM
2065 typeConverter.addConversion(
2066 [&](xegpu::MemDescType type) -> Type { return i32Type; });
2067
2068 typeConverter.addConversion([&](MemRefType type) -> Type {
2069 return isSharedMemRef(type) ? i32Type : xevmIndexType;
2070 });
2071
2072 // LLVM type converter puts unrealized casts for the following cases:
2073 // add materialization casts to handle them.
2074
2075 // Materialization to convert memref to i64 or i32 depending on global/SLM
2076 // Applies only to target materialization.
2077 // Note: int type to memref materialization is not required as xegpu ops
2078 // currently do not produce memrefs as result.
2079 auto memrefToIntMaterializationCast = [](OpBuilder &builder, Type type,
2080 ValueRange inputs,
2081 Location loc) -> Value {
2082 if (inputs.size() != 1)
2083 return {};
2084 auto input = inputs.front();
2085 if (auto memrefTy = dyn_cast<MemRefType>(input.getType())) {
2086 unsigned rank = memrefTy.getRank();
2087 Type indexType = builder.getIndexType();
2088
2089 int64_t intOffsets;
2090 SmallVector<int64_t> intStrides;
2091 Value addr;
2092 Value offset;
2093 if (succeeded(memrefTy.getStridesAndOffset(intStrides, intOffsets)) &&
2094 ShapedType::isStatic(intOffsets)) {
2095 addr = memref::ExtractAlignedPointerAsIndexOp::create(builder, loc,
2096 input);
2097 offset = arith::ConstantOp::create(builder, loc,
2098 builder.getIndexAttr(intOffsets));
2099 } else {
2100
2101 // Result types: [base_memref, offset, stride0, stride1, ...,
2102 // strideN-1, size0, size1, ..., sizeN-1]
2103 SmallVector<Type> resultTypes{
2104 MemRefType::get({}, memrefTy.getElementType(),
2105 MemRefLayoutAttrInterface(),
2106 memrefTy.getMemorySpace()),
2107 indexType};
2108 // strides + sizes
2109 resultTypes.append(2 * rank, indexType);
2110
2111 auto meta = memref::ExtractStridedMetadataOp::create(
2112 builder, loc, resultTypes, input);
2113
2114 addr = memref::ExtractAlignedPointerAsIndexOp::create(
2115 builder, loc, meta.getBaseBuffer());
2116 offset = meta.getOffset();
2117 }
2118
2119 auto addrCasted =
2120 arith::IndexCastUIOp::create(builder, loc, type, addr);
2121 auto offsetCasted =
2122 arith::IndexCastUIOp::create(builder, loc, type, offset);
2123
2124 // Compute the final address: base address + byte offset.
2125 return xegpu::addElemOffsetInBytes(builder, loc, addrCasted,
2126 offsetCasted, memrefTy);
2127 }
2128 return {};
2129 };
2130
2131 // Materialization to convert ui64 to i64
2132 // Applies only to target materialization.
2133 // Note: i64 to ui64 materialization is not required as xegpu ops
2134 // currently do not produce ui64 as result.
2135 auto ui64ToI64MaterializationCast = [](OpBuilder &builder, Type type,
2136 ValueRange inputs,
2137 Location loc) -> Value {
2138 if (inputs.size() != 1)
2139 return {};
2140 auto input = inputs.front();
2141 if (input.getType() == builder.getIntegerType(64, false)) {
2142 Value cast =
2143 index::CastUOp::create(builder, loc, builder.getIndexType(), input)
2144 .getResult();
2145 return arith::IndexCastUIOp::create(builder, loc, type, cast)
2146 .getResult();
2147 }
2148 return {};
2149 };
2150
2151 // Materialization to convert ui32 to i32
2152 // Applies only to target materialization.
2153 // Note: i32 to ui32 materialization is not required as xegpu ops
2154 // currently do not produce ui32 as result.
2155 auto ui32ToI32MaterializationCast = [](OpBuilder &builder, Type type,
2156 ValueRange inputs,
2157 Location loc) -> Value {
2158 if (inputs.size() != 1)
2159 return {};
2160 auto input = inputs.front();
2161 if (input.getType() == builder.getIntegerType(32, false)) {
2162 Value cast =
2163 index::CastUOp::create(builder, loc, builder.getIndexType(), input)
2164 .getResult();
2165 return arith::IndexCastUIOp::create(builder, loc, type, cast)
2166 .getResult();
2167 }
2168 return {};
2169 };
2170
2171 // Materialization to convert between vector types
2172 // - Add shape cast for different shapes
2173 // - Add bitcast for different element types
2174 // Applies to both source and target materialization.
2175 auto vectorToVectorMaterializationCast = [](OpBuilder &builder, Type type,
2176 ValueRange inputs,
2177 Location loc) -> Value {
2178 if (inputs.size() != 1)
2179 return {};
2180 auto input = inputs.front();
2181 if (auto vecTy = dyn_cast<VectorType>(input.getType())) {
2182 if (auto targetVecTy = dyn_cast<VectorType>(type)) {
2183 Value cast = input;
2184 // If the target type has a different shape, add a shape cast
2185 // If the target type has a different element type, add a bitcast
2186 if (targetVecTy.getShape() != vecTy.getShape()) {
2187 cast = vector::ShapeCastOp::create(
2188 builder, loc,
2189 VectorType::get(targetVecTy.getShape(),
2190 vecTy.getElementType()),
2191 cast)
2192 .getResult();
2193 }
2194 if (targetVecTy.getElementType() != vecTy.getElementType()) {
2195 cast = vector::BitCastOp::create(builder, loc, targetVecTy, cast)
2196 .getResult();
2197 }
2198 return cast;
2199 }
2200 }
2201 return {};
2202 };
2203
2204 // Materialization to convert
2205 // - single element vector to single element of vector element type
2206 // Applies only to target materialization.
2207 auto vectorToSingleElementMaterializationCast =
2208 [](OpBuilder &builder, Type type, ValueRange inputs,
2209 Location loc) -> Value {
2210 if (inputs.size() != 1)
2211 return {};
2212 auto input = inputs.front();
2213 if (auto vecTy = dyn_cast<VectorType>(input.getType())) {
2214 // Source needs to be single element vector
2215 auto rank = vecTy.getRank();
2216 if (rank != 0 && vecTy.getNumElements() != 1)
2217 return {};
2218 auto inElemTy = vecTy.getElementType();
2219 // extract scalar
2220 Value cast = input;
2221 if (rank == 0) {
2222 cast = vector::ExtractOp::create(builder, loc, cast, {}).getResult();
2223 } else {
2224 cast = vector::ExtractOp::create(builder, loc, cast,
2225 SmallVector<int64_t>(rank, 0))
2226 .getResult();
2227 }
2228 // Extracted element type may need conversion
2229 // Two cases
2230 // 1. Index type to integer type
2231 // 2. Other element type mismatch
2232 if (inElemTy.isIndex()) {
2233 cast = arith::IndexCastUIOp::create(builder, loc, type, cast)
2234 .getResult();
2235 } else if (inElemTy != type) {
2236 cast = arith::BitcastOp::create(builder, loc, type, cast).getResult();
2237 }
2238 return cast;
2239 }
2240 return {};
2241 };
2242
2243 // Materialization to convert
2244 // - single element of vector element type to single element vector
2245 // If result type of original op is single element vector and lowered type
2246 // is scalar. This materialization cast creates a single element vector by
2247 // First convert element type if needed and then broadcast to single
2248 // element vector.
2249 // Applies only to source materialization.
2250 auto singleElementToVectorMaterializationCast =
2251 [](OpBuilder &builder, Type type, ValueRange inputs,
2252 Location loc) -> Value {
2253 if (inputs.size() != 1)
2254 return {};
2255 auto input = inputs.front();
2256 auto inTy = input.getType();
2257 if (!inTy.isIntOrFloat())
2258 return {};
2259 // If the target type is a vector of rank 0 or single element vector
2260 // of element type matching input type, broadcast input to target type.
2261 if (auto vecTy = dyn_cast<VectorType>(type)) {
2262 if (vecTy.getRank() != 0 && vecTy.getNumElements() != 1)
2263 return {};
2264 auto outElemTy = vecTy.getElementType();
2265 Value cast = input;
2266 if (outElemTy.isIndex()) {
2267 cast = arith::IndexCastUIOp::create(builder, loc,
2268 builder.getIndexType(), cast)
2269 .getResult();
2270 } else if (inTy != outElemTy) {
2271 cast = arith::BitcastOp::create(builder, loc, outElemTy, cast)
2272 .getResult();
2273 }
2274 return vector::BroadcastOp::create(builder, loc, vecTy, cast)
2275 .getResult();
2276 }
2277 return {};
2278 };
2279 typeConverter.addSourceMaterialization(
2280 singleElementToVectorMaterializationCast);
2281 typeConverter.addSourceMaterialization(vectorToVectorMaterializationCast);
2282 typeConverter.addTargetMaterialization(memrefToIntMaterializationCast);
2283 typeConverter.addTargetMaterialization(ui32ToI32MaterializationCast);
2284 typeConverter.addTargetMaterialization(ui64ToI64MaterializationCast);
2285 typeConverter.addTargetMaterialization(
2286 vectorToSingleElementMaterializationCast);
2287 typeConverter.addTargetMaterialization(vectorToVectorMaterializationCast);
2288 ConversionTarget target(*context);
2289 target.addLegalDialect<xevm::XeVMDialect, LLVM::LLVMDialect,
2290 vector::VectorDialect, arith::ArithDialect,
2291 memref::MemRefDialect, gpu::GPUDialect,
2292 index::IndexDialect>();
2293 target.addIllegalDialect<xegpu::XeGPUDialect>();
2294 // arith.extf/arith.truncf between MX narrow floats and f16/bf16 are routed
2295 // to xevm.extf/xevm.truncf; all other arith float casts stay legal.
2296 target.addDynamicallyLegalOp<arith::ExtFOp>(
2297 [](arith::ExtFOp op) { return !isXeVMExtf(op); });
2298 target.addDynamicallyLegalOp<arith::TruncFOp>(
2299 [](arith::TruncFOp op) { return !isXeVMTruncf(op); });
2300
2301 RewritePatternSet patterns(context);
2302 populateXeGPUToXeVMConversionPatterns(typeConverter, patterns);
2304 patterns, target);
2305 if (failed(applyPartialConversion(getOperation(), target,
2306 std::move(patterns))))
2307 signalPassFailure();
2308 }
2309};
2310} // namespace
2311
2312//===----------------------------------------------------------------------===//
2313// Pattern Population
2314//===----------------------------------------------------------------------===//
2316 const LLVMTypeConverter &typeConverter, RewritePatternSet &patterns) {
2317 patterns.add<CreateNdDescToXeVMPattern,
2318 LoadStorePrefetchNdToXeVMPattern<xegpu::LoadNdOp>,
2319 LoadStorePrefetchNdToXeVMPattern<xegpu::StoreNdOp>,
2320 LoadStorePrefetchNdToXeVMPattern<xegpu::PrefetchNdOp>>(
2321 typeConverter, patterns.getContext());
2322 patterns.add<AtomicRMWToXeVMPattern, PrefetchToXeVMPattern,
2323 LoadStoreToXeVMPattern<xegpu::LoadGatherOp>,
2324 LoadStoreToXeVMPattern<xegpu::StoreScatterOp>>(
2325 typeConverter, patterns.getContext());
2326 patterns.add<LoadStoreMatrixToXeVMPattern<xegpu::LoadMatrixOp>,
2327 LoadStoreMatrixToXeVMPattern<xegpu::StoreMatrixOp>,
2328 CreateMemDescOpPattern>(typeConverter, patterns.getContext());
2329 patterns.add<FenceToXeVMPattern, DpasToXeVMPattern>(typeConverter,
2330 patterns.getContext());
2331 patterns.add<DpasMxToXeVMPattern>(typeConverter, patterns.getContext());
2332 patterns.add<ExtfToXeVMPattern, TruncfToXeVMPattern>(typeConverter,
2333 patterns.getContext());
2334 patterns.add<LaneShuffleToXeVMPattern>(typeConverter, patterns.getContext());
2335}
return success()
static void visit(Operation *op, DenseSet< Operation * > &visited)
Visits all the pdl.operand(s), pdl.result(s), and pdl.operation(s) connected to the given operation.
Definition PDL.cpp:61
b getContext())
auto load
static llvm::ManagedStatic< PassManagerOptions > options
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 SmallVector< Value > slice(OpBuilder &b, Location loc, Value value, VectorType nativeType)
Attributes are known-constant values of operations.
Definition Attributes.h:25
IntegerAttr getIndexAttr(int64_t value)
Definition Builders.cpp:116
IntegerType getIntegerType(unsigned width)
Definition Builders.cpp:75
IndexType getIndexType()
Definition Builders.cpp:59
IntegerType getI8Type()
Definition Builders.cpp:67
An attribute that represents a reference to a dense vector or tensor object.
bool isSplat() const
Returns true if this attribute corresponds to a splat, i.e.
static DenseElementsAttr get(ShapedType type, ArrayRef< Attribute > values)
Constructs a dense elements attribute from an array of element values.
GreedyRewriteConfig & setStrictness(GreedyRewriteStrictness mode)
auto lookup(T from) const
Lookup a mapped value within the map.
Definition IRMapping.h:72
void map(Value from, Value to)
Inserts a new mapping for 'from' to 'to'.
Definition IRMapping.h:30
Conversion from types to the LLVM IR dialect.
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
MLIRContext * getContext() const
Return the context this location is uniqued in.
Definition Location.h:86
Operation * clone(Operation &op, IRMapping &mapper)
Creates a deep copy of the specified operation, remapping any operands that use values outside of the...
Definition Builders.cpp:581
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
Definition Builders.h:401
void setDiscardableAttr(StringAttr name, Attribute value)
Set a discardable attribute by name.
Definition Operation.h:514
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:434
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.
void modifyOpInPlace(Operation *root, CallableT &&callable)
This method is a utility wrapper around an in-place modification of an operation.
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
bool isIntOrFloat() const
Return true if this is an integer (of any signedness) or a float type.
Definition Types.cpp:118
bool isF16() const
Definition Types.cpp:38
unsigned getIntOrFloatBitWidth() const
Return the bit width of an integer or a float type, assert failure on other types.
Definition Types.cpp:124
bool isBF16() const
Definition Types.cpp:37
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
void setType(Type newType)
Mutate the type of this Value to be of the specified type.
Definition Value.h:116
Type getType() const
Return the type of this value.
Definition Value.h:105
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
static ConstantIntOp create(OpBuilder &builder, Location location, int64_t value, unsigned width)
Definition ArithOps.cpp:333
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:853
void populateSCFStructuralTypeConversionsAndLegality(const TypeConverter &typeConverter, RewritePatternSet &patterns, ConversionTarget &target, PatternBenefit benefit=1)
Populates patterns for SCF structural type conversions and sets up the provided ConversionTarget with...
void populateVectorInterleaveToShufflePatterns(RewritePatternSet &patterns, PatternBenefit benefit=1)
void populateVectorDeinterleaveToShufflePatterns(RewritePatternSet &patterns, PatternBenefit benefit=1)
const uArch * getUArch(llvm::StringRef archName)
Definition uArchCommon.h:24
bool hasStaticShapeAndStrides(MemRefType type)
Returns true if type has a static shape and static strides.
Value addElemOffsetInBytes(OpBuilder &builder, Location loc, Value baseAddr, Value elemOffset, MemRefType memrefTy)
Returns baseAddr + elemOffset * bitWidth / 8, i.e.
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
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
SmallVector< int64_t > computeStrides(ArrayRef< int64_t > sizes)
SmallVector< int64_t > delinearize(int64_t linearIndex, ArrayRef< int64_t > strides)
Given the strides together with a linear index in the dimension space, return the vector-space offset...
Value getValueOrCreateConstantIntOp(OpBuilder &b, Location loc, OpFoldResult ofr)
Converts an OpFoldResult to a Value.
Definition Utils.cpp:105
LogicalResult applyOpPatternsGreedily(ArrayRef< Operation * > ops, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr, bool *allErased=nullptr)
Rewrite the specified ops by repeatedly applying the highest benefit patterns in a greedy worklist dr...
int64_t computeProduct(ArrayRef< int64_t > basis)
Self-explicit.
Value getValueOrCreateCastToIndexLike(OpBuilder &b, Location loc, Type targetType, Value value)
Create a cast from an index-like value (index or integer) to another index-like value.
Definition Utils.cpp:122
void populateXeGPUToXeVMConversionPatterns(const LLVMTypeConverter &typeConverter, RewritePatternSet &patterns)
Operation * clone(OpBuilder &b, Operation *op, TypeRange newResultTypes, ValueRange newOperands)
int64_t linearize(ArrayRef< int64_t > offsets, ArrayRef< int64_t > basis)
Return the linearized index of 'offsets' w.r.t.
detail::constant_op_matcher m_Constant()
Matches a constant foldable operation.
Definition Matchers.h:369
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...