MLIR 24.0.0git
VectorToAMX.cpp
Go to the documentation of this file.
1//===- VectorToAMX.cpp - Convert vector to X86 dialect AMX ops --*- C++ -*-===//
2//
3// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4// See https://llvm.org/LICENSE.txt for license information.
5// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6//
7//===----------------------------------------------------------------------===//
8
10
20#include "mlir/IR/Builders.h"
21#include "mlir/Pass/Pass.h"
23
24#include "llvm/ADT/STLExtras.h"
25#include "llvm/Support/DebugLog.h"
26
27#include <numeric>
28
29namespace mlir {
30#define GEN_PASS_DEF_CONVERTVECTORTOAMX
31#include "mlir/Conversion/Passes.h.inc"
32} // namespace mlir
33
34using namespace mlir;
35
36#define DEBUG_TYPE "vector-to-amx"
37
38namespace {
39
40/// Return true if vector shape is compatible with AMX tiles.
41/// The validation accounts for VNNI packing.
42static bool verifyAmxShape(VectorType vec) {
43 // Check overall shape:
44 // - 2D for plain layout input or output
45 // - 3D for VNNI packed input
46 if (vec.getRank() != 2 && vec.getRank() != 3)
47 return false;
48
49 ArrayRef<int64_t> shape = vec.getShape();
50 int64_t rows = shape[0];
51 int64_t cols = shape[1];
52 unsigned elemBitWidth = vec.getElementType().getIntOrFloatBitWidth();
53
54 // 3D shape indicates VNNI packed layout.
55 if (vec.getRank() == 3) {
56 int64_t vnniFactor = 32 / elemBitWidth;
57 if (shape.back() != vnniFactor) {
58 LDBG() << "invalid VNNI packing factor";
59 return false;
60 }
61 cols *= vnniFactor;
62 }
63
64 // AMX tile supports up to 16 rows of 64 bytes each.
65 constexpr unsigned maxRows = 16;
66 constexpr unsigned maxBitsPerRow = 64 * 8;
67 return rows <= maxRows && (cols * elemBitWidth) <= maxBitsPerRow;
68}
69
70/// Check if contraction operands are in AMX-compatible packed VNNI layout.
71static LogicalResult isAmxVnniLayout(PatternRewriter &rewriter,
72 vector::ContractionOp contractOp) {
73 VectorType accType = dyn_cast<VectorType>(contractOp.getAcc().getType());
74 if (!accType || accType.getRank() != 2)
75 return rewriter.notifyMatchFailure(contractOp, "Expects acc 2D vector");
76
77 // Expect 3D inputs for VNNI packed data.
78 VectorType lhsType = contractOp.getLhs().getType();
79 VectorType rhsType = contractOp.getRhs().getType();
80 if (lhsType.getRank() != 3 || rhsType.getRank() != 3)
81 return rewriter.notifyMatchFailure(contractOp,
82 "Expects lhs and rhs 3D vectors");
83
84 // Check if shapes are compatible with AMX tile.
85 if (!verifyAmxShape(lhsType) || !verifyAmxShape(rhsType) ||
86 !verifyAmxShape(accType))
87 return rewriter.notifyMatchFailure(contractOp, "Invalid operand shape");
88
89 // Validate affine maps.
90 //
91 // Iterators can be ordered arbitrarily. Indexing map positions are based on
92 // operands' target shapes.
93 // The matrix layouts must match the following:
94 // - matrix A - [M]x[K/vnniFactor]x[vnniFactor]
95 // - matrix B - [K/vnniFactor]x[N]x[vnniFactor]
96 // - matrix C - [M]x[N]
97 SmallVector<AffineMap, 4> indexingMaps = contractOp.getIndexingMapsArray();
98 AffineMap mapA = indexingMaps[0];
99 AffineMap mapB = indexingMaps[1];
100 if (mapA.getNumInputs() != 4 || mapA.getNumResults() != 3 ||
101 mapB.getNumResults() != 3)
102 return rewriter.notifyMatchFailure(contractOp,
103 "Invalid input indexing maps");
104 FailureOr<linalg::ContractionDimensions> dims =
105 linalg::inferContractionDims(indexingMaps);
106 if (failed(dims))
107 return rewriter.notifyMatchFailure(contractOp,
108 "Failed to infer contraction dims");
109 // Two reduction dimensions are expected:
110 // - one for the K dimension
111 // - one for the VNNI factor
112 if (dims->k.size() != 2)
113 return rewriter.notifyMatchFailure(contractOp,
114 "Expected two reduction dims");
115 assert(dims->m.size() == 1 && dims->n.size() == 1 &&
116 "Invalid parallel contraction dims");
117
119 contractOp.getIteratorTypesArray();
120 // Check VNNI dim maps - the innermost dim for A and B inputs.
121 auto vnniDimA = dyn_cast<AffineDimExpr>(mapA.getResult(2));
122 auto vnniDimB = dyn_cast<AffineDimExpr>(mapB.getResult(2));
123 if (!vnniDimA || !vnniDimB || vnniDimA != vnniDimB ||
124 iteratorTypes[vnniDimA.getPosition()] != vector::IteratorType::reduction)
125 return rewriter.notifyMatchFailure(contractOp, "Invalid VNNI dim map");
126 // Check K dim maps - non-transposed row-major layout.
127 auto redDimA = dyn_cast<AffineDimExpr>(mapA.getResult(1));
128 auto redDimB = dyn_cast<AffineDimExpr>(mapB.getResult(0));
129 if (!redDimA || !redDimB || redDimA != redDimB ||
130 iteratorTypes[redDimA.getPosition()] != vector::IteratorType::reduction)
131 return rewriter.notifyMatchFailure(contractOp, "Invalid K dim map");
132 // Check M and N dim maps - map to non-transposed output.
133 AffineMap mapC = indexingMaps[2];
134 auto mDimC = dyn_cast<AffineDimExpr>(mapC.getResult(0));
135 auto nDimC = dyn_cast<AffineDimExpr>(mapC.getResult(1));
136 if (!mDimC || !nDimC)
137 return rewriter.notifyMatchFailure(contractOp, "Invalid acc maps");
138 auto parallelDimA = dyn_cast<AffineDimExpr>(mapA.getResult(0));
139 if (!parallelDimA ||
140 iteratorTypes[parallelDimA.getPosition()] !=
141 vector::IteratorType::parallel ||
142 parallelDimA != mDimC)
143 return rewriter.notifyMatchFailure(contractOp, "Invalid M dim map");
144 auto parallelDimB = dyn_cast<AffineDimExpr>(mapB.getResult(1));
145 if (!parallelDimB ||
146 iteratorTypes[parallelDimB.getPosition()] !=
147 vector::IteratorType::parallel ||
148 parallelDimB != nDimC)
149 return rewriter.notifyMatchFailure(contractOp, "Invalid N dim map");
150
151 return success();
152}
153
154/// Validate contraction operands for AMX lowering.
155static LogicalResult validateOperands(PatternRewriter &rewriter,
156 vector::ContractionOp contractOp) {
157 VectorType accType = dyn_cast<VectorType>(contractOp.getAcc().getType());
158 if (!accType)
159 return rewriter.notifyMatchFailure(contractOp, "Expects vector acc");
160
161 // Check if operand types are compatible with AMX compute ops.
162 bool validElemTypes = false;
163 Type lhsElemType = contractOp.getLhs().getType().getElementType();
164 Type rhsElemType = contractOp.getRhs().getType().getElementType();
165 Type accElemType = accType.getElementType();
166 if (accElemType.isInteger(32)) {
167 validElemTypes = lhsElemType.isInteger(8) && rhsElemType.isInteger(8);
168 } else if (accElemType.isF32()) {
169 validElemTypes = (lhsElemType.isF16() && rhsElemType.isF16()) ||
170 (lhsElemType.isBF16() && rhsElemType.isBF16());
171 }
172 if (!validElemTypes)
173 return rewriter.notifyMatchFailure(contractOp,
174 "Invalid combination of operand types");
175
176 if (failed(isAmxVnniLayout(rewriter, contractOp)))
177 return failure();
178
179 return success();
180}
181
182/// Collapse the two innermost dimensions together.
183static TypedValue<MemRefType> collapseLastDim(PatternRewriter &rewriter,
185 int64_t rank = memref.getType().getRank();
187 for (auto i : llvm::seq<int64_t>(0, rank - 2))
188 reassocIndices.push_back({i});
189 reassocIndices.push_back({rank - 2, rank - 1});
190 return memref::CollapseShapeOp::create(rewriter, memref.getLoc(), memref,
191 reassocIndices);
192}
193
194/// Attempt to create an AMX tile load/store operation equivalent to the given
195/// vector transfer `xfer` op.
196/// This approach allows to skip longer route through registers and a temporary
197/// buffer otherwise required to move data to/from an AMX tile.
198static Operation *
199loadStoreFromTransfer(PatternRewriter &rewriter,
200 VectorTransferOpInterface xferOp, bool isPacked,
201 TypedValue<x86::amx::TileType> tileToStore = nullptr) {
202 if (!xferOp || !isa<vector::TransferReadOp, vector::TransferWriteOp>(xferOp))
203 return nullptr;
204 if (xferOp.hasOutOfBoundsDim() ||
205 !xferOp.getPermutationMap().isMinorIdentity())
206 return nullptr;
207
208 // Extra checks in case of a write op.
209 // Stores must not be packed.
210 if (isa<vector::TransferWriteOp>(xferOp) &&
211 (!tileToStore || isPacked ||
212 tileToStore.getType().getShape() != xferOp.getVectorType().getShape()))
213 return nullptr;
214
215 // Check for a memref source buffer.
216 // AMX data transfer requires at least 2D shape to correctly
217 // infer stride between rows.
218 Value base = xferOp.getBase();
219 auto memTy = dyn_cast<MemRefType>(base.getType());
220 int64_t memRank = memTy.getRank();
221 if (!memTy || memRank < 2)
222 return nullptr;
223
224 // Check that the source buffer has enough contiguous elements to load whole
225 // AMX tile row.
226 //
227 // To ensure correctness, the validation is conservative and expects the
228 // buffer's innermost dimensions to be statically known, equal to or larger
229 // than the vector row length, and equal to the VNNI dimension if applicable.
230 //
231 // This check could be relaxed to accept more arbitrarily shaped buffers as
232 // long as there are enough contiguous elements to load a whole row.
233 if (!memTy.areTrailingDimsContiguous(isPacked ? 2 : 1))
234 return nullptr;
235 VectorType vecTy = xferOp.getVectorType();
236 ArrayRef<int64_t> vecShape = vecTy.getShape();
237 ArrayRef<int64_t> memShape = memTy.getShape();
238 if (memShape.back() == ShapedType::kDynamic ||
239 memShape.back() < vecShape.back())
240 return nullptr;
241 if (isPacked &&
242 (memShape.back() != vecShape.back() ||
243 memShape[memShape.size() - 2] == ShapedType::kDynamic ||
244 memShape[memShape.size() - 2] < vecShape[vecShape.size() - 2]))
245 return nullptr;
246
247 // Load values directly from the buffer to an AMX tile.
249 rewriter.setInsertionPoint(xferOp);
250 Location loc = xferOp.getLoc();
251
252 // Create a subview of the source buffer based on the transfer op to resolve
253 // offsets.
254 SmallVector<OpFoldResult> strides(memRank, rewriter.getIndexAttr(1));
255 int64_t vecRank = vecTy.getRank();
256 assert(memRank >= vecRank &&
257 "Expects buffer to be the same or greater rank than vector");
258 SmallVector<int64_t> shape(memRank - vecRank, 1);
259 shape.append(vecShape.begin(), vecShape.end());
261 memref::SubViewOp::create(
262 rewriter, loc, base, getAsOpFoldResult(xferOp.getIndices()),
263 getAsOpFoldResult(rewriter.getI64ArrayAttr(shape)), strides)
264 .getResult();
265
266 // Collapse the VNNI dimension in case of packing.
267 if (isPacked)
268 src = collapseLastDim(rewriter, src);
269 int64_t rows = vecShape[0];
270 int64_t cols = llvm::product_of(vecShape.drop_front());
271 auto tileType = x86::amx::TileType::get({rows, cols}, vecTy.getElementType());
272
273 Value zeroIndex = rewriter.createOrFold<arith::ConstantIndexOp>(loc, 0);
274 SmallVector<Value> tileIndicides(src.getType().getRank(), zeroIndex);
275
276 Operation *amxTileOp = nullptr;
277 if (isa<vector::TransferReadOp>(xferOp)) {
278 amxTileOp = x86::amx::TileLoadOp::create(rewriter, loc, tileType, src,
279 tileIndicides);
280 } else if (isa<vector::TransferWriteOp>(xferOp)) {
281 amxTileOp = x86::amx::TileStoreOp::create(rewriter, loc, src, tileIndicides,
282 tileToStore);
283 } else {
284 llvm_unreachable("unsupported vector transfer op");
285 }
286
287 return amxTileOp;
288}
289
290/// Attempt to create an AMX tile load operation equivalent to the given
291/// vector transfer `readOp`.
292/// Returns loaded AMX tile if successful.
293static FailureOr<TypedValue<x86::amx::TileType>>
294loadFromTransfer(PatternRewriter &rewriter, vector::TransferReadOp readOp,
295 bool isPacked) {
296 x86::amx::TileLoadOp loadOp = dyn_cast_if_present<x86::amx::TileLoadOp>(
297 loadStoreFromTransfer(rewriter, readOp, isPacked));
298 if (!loadOp)
299 return failure();
300 return loadOp.getRes();
301}
302
303/// Attempt to create an AMX tile store operation equivalent to the given
304/// vector transfer `writeOp`.
305static LogicalResult
306storeFromTransfer(PatternRewriter &rewriter, vector::TransferWriteOp writeOp,
307 TypedValue<x86::amx::TileType> tileToStore) {
308 return success(loadStoreFromTransfer(rewriter, writeOp, /*isPacked=*/false,
309 tileToStore));
310}
311
312/// Load vector values to an AMX tile.
313static TypedValue<x86::amx::TileType> loadTile(PatternRewriter &rewriter,
315 Location loc = vec.getLoc();
316
317 VectorType vecTy = vec.getType();
318 bool isPacked = vecTy.getRank() == 3;
319
320 // Try to load tile directly from vector producer's buffer.
321 auto readOp = vec.getDefiningOp<vector::TransferReadOp>();
322 FailureOr<TypedValue<x86::amx::TileType>> tile =
323 loadFromTransfer(rewriter, readOp, isPacked);
324 if (succeeded(tile))
325 return *tile;
326
327 // Transfer the vector to a tile through an intermediate buffer.
328 Value buf = memref::AllocaOp::create(
329 rewriter, loc, MemRefType::get(vecTy.getShape(), vecTy.getElementType()));
330 Value zeroIndex = rewriter.createOrFold<arith::ConstantIndexOp>(loc, 0);
331 SmallVector<Value> indices(vecTy.getRank(), zeroIndex);
332 vector::TransferWriteOp::create(rewriter, loc, vec, buf, indices);
333
334 // Collapse the VNNI dimension in case of packing.
335 if (isPacked)
336 buf = collapseLastDim(rewriter, cast<TypedValue<MemRefType>>(buf));
337
338 ArrayRef<int64_t> shape = vecTy.getShape();
339 int64_t rows = shape[0];
340 int64_t cols = llvm::product_of(shape.drop_front());
341 auto tileType = x86::amx::TileType::get({rows, cols}, vecTy.getElementType());
342
343 return x86::amx::TileLoadOp::create(rewriter, loc, tileType, buf,
344 {zeroIndex, zeroIndex});
345}
346
347/// Store an AMX tile in a vector.
348static TypedValue<VectorType> storeTile(PatternRewriter &rewriter,
350 Location loc = tile.getLoc();
351
352 // Transfer the tile to a vector through an intermediate buffer.
353 x86::amx::TileType tileTy = tile.getType();
354 Value buf = memref::AllocaOp::create(
355 rewriter, loc,
356 MemRefType::get(tileTy.getShape(), tileTy.getElementType()));
357 Value zeroIndex = rewriter.createOrFold<arith::ConstantIndexOp>(loc, 0);
358 Repeated<Value> indices(2, zeroIndex);
359 x86::amx::TileStoreOp::create(rewriter, loc, buf, indices, tile);
360
361 auto vecTy = VectorType::get(tileTy.getShape(), tileTy.getElementType());
362 return vector::TransferReadOp::create(rewriter, loc, vecTy, buf, indices, {});
363}
364
365struct ContractionToAMX : public vector::AddContractionOpRewritePattern {
366 using Base::Base;
367
368 LogicalResult
369 matchAndRewriteAddContractOp(vector::ContractionOp contractOp,
370 PatternRewriter &rewriter) const override {
371 Location loc = contractOp.getLoc();
372
373 if (failed(validateOperands(rewriter, contractOp)))
374 return failure();
375
377 loadTile(rewriter, contractOp.getLhs());
379 loadTile(rewriter, contractOp.getRhs());
380 auto acc = dyn_cast<TypedValue<VectorType>>(contractOp.getAcc());
381 assert(acc && "Invalid accumulator type");
382 TypedValue<x86::amx::TileType> accTile = loadTile(rewriter, acc);
383
385 if (acc.getType().getElementType().isFloat()) {
386 tileMul = x86::amx::TileMulFOp::create(rewriter, loc, accTile.getType(),
387 lhsTile, rhsTile, accTile);
388 } else {
389 tileMul = x86::amx::TileMulIOp::create(rewriter, loc, accTile.getType(),
390 lhsTile, rhsTile, accTile);
391 }
392
393 // If the contraction result is only written back to memory, try to replace
394 // the vector op with an AMX store directly.
395 Value res = contractOp.getResult();
396 if (res.hasOneUse()) {
397 auto writeOp = dyn_cast<vector::TransferWriteOp>(*res.getUsers().begin());
398 LogicalResult storeRes = storeFromTransfer(rewriter, writeOp, tileMul);
399 if (succeeded(storeRes)) {
400 rewriter.eraseOp(writeOp);
401 rewriter.eraseOp(contractOp);
402 return success();
403 }
404 }
405
406 // Load the result back into a vector.
407 Value newResult = storeTile(rewriter, tileMul);
408 rewriter.replaceOp(contractOp, newResult);
409
410 return success();
411 }
412};
413
414struct ConvertVectorToAMXPass
415 : public impl::ConvertVectorToAMXBase<ConvertVectorToAMXPass> {
416 void runOnOperation() override {
417 MLIRContext &ctx = getContext();
418 RewritePatternSet patterns(&ctx);
420 if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
421 return signalPassFailure();
422 }
423};
424
425} // namespace
426
428 patterns.add<ContractionToAMX>(patterns.getContext());
429}
return success()
b getContext())
A multi-dimensional affine map Affine map's are immutable like Type's, and they are uniqued.
Definition AffineMap.h:46
unsigned getNumResults() const
unsigned getNumInputs() const
AffineExpr getResult(unsigned idx) const
IntegerAttr getIndexAttr(int64_t value)
Definition Builders.cpp:116
ArrayAttr getI64ArrayAttr(ArrayRef< int64_t > values)
Definition Builders.cpp:290
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
MLIRContext is the top-level object for a collection of MLIR operations.
Definition MLIRContext.h:71
RAII guard to reset the insertion point of the builder when destroyed.
Definition Builders.h:351
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
Definition Builders.h:401
void createOrFold(SmallVectorImpl< Value > &results, Location location, Args &&...args)
Create an operation of specific op type at the current insertion point, and immediately try to fold i...
Definition Builders.h:528
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
A special type of RewriterBase that coordinates the application of a rewrite pattern on the current I...
MLIRContext * getContext() const
RewritePatternSet & add(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
virtual void eraseOp(Operation *op)
This method erases an operation that is known to have no uses.
std::enable_if_t<!std::is_convertible< CallbackT, Twine >::value, LogicalResult > notifyMatchFailure(Location loc, CallbackT &&reasonCallback)
Used to notify the listener that the IR failed to be rewritten because of a match failure,...
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
bool isF32() const
Definition Types.cpp:40
bool isInteger() const
Return true if this is an integer type (with the specified width).
Definition Types.cpp:58
bool isF16() const
Definition Types.cpp:38
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
Type getType() const
Return the type of this value.
Definition Value.h:105
Specialization of arith.constant op that returns an integer of index type.
Definition Arith.h:85
FailureOr< ContractionDimensions > inferContractionDims(LinalgOp linalgOp)
Find at least 2 parallel (m and n) and 1 reduction (k) dimension candidates that form a matmul subcom...
mlir::x86::AMXTileType TileType
Definition X86Dialect.h:33
Include the generated interface declarations.
void populateVectorToAMXConversionPatterns(RewritePatternSet &patterns)
Collect a set of patterns to convert from the vector to X86 AMX ops.
LogicalResult applyPatternsGreedily(Region &region, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr)
Rewrite ops in the given region, which must be isolated from above, by repeatedly applying the highes...
std::conditional_t< std::is_same_v< Ty, mlir::Type >, mlir::Value, detail::TypedValue< Ty > > TypedValue
If Ty is mlir::Type this will select Value instead of having a wrapper around it.
Definition Value.h:496
SmallVector< Loops, 8 > tile(ArrayRef< scf::ForOp > forOps, ArrayRef< Value > sizes, ArrayRef< scf::ForOp > targets)
Performs tiling fo imperfectly nested loops (with interchange) by strip-mining the forOps by sizes an...
Definition Utils.cpp:1389
OpFoldResult getAsOpFoldResult(Value val)
Given a value, try to extract a constant Attribute.
Convenience wrapper for patterns that are only intended for default contractions, i....