MLIR 24.0.0git
VectorContractMultiLevelUnroll.cpp
Go to the documentation of this file.
1//===- VectorContractMultiLevelUnroll.cpp ---------------------------------===//
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
16
20
21#define DEBUG_TYPE "x86-vector-contract-multi-level-unroll"
22
23using namespace mlir;
24
25using llvm::all_of;
26using llvm::count;
27
28// Discardable attribute to control vector unroll patterns.
29static constexpr auto nativeShapeAttrName = "x86_vcmlu_native_shape";
30
31namespace {
32// Shared state for the pattern match and IR transformation.
33struct MLUCandidate {
34 // Anchor operation.
35 vector::ContractionOp contract;
36 bool isInVnniLayout = false;
37
38 // Shape of the original contraction.
39 VectorType accType;
40 VectorType lhsType;
41 VectorType rhsType;
42
43 // Shape of the register-tiled contraction. We'll insert a loop nest to cover
44 // the original shape.
45 VectorType accRegTileType;
46 VectorType lhsRegTileType;
47 VectorType rhsRegTileType;
48
49 // Shape of the native contraction (for the X86 nanokernel patterns). We'll
50 // unroll the register-tiled contraction to this shape.
51 VectorType accNativeType;
52 VectorType lhsNativeType;
53 VectorType rhsNativeType;
54 SmallVector<int64_t> contractNativeShape;
55
56 // Bookkeeping for the accumulation loop and operands.
57 scf::ForOp accLoop;
58 Value accInitVal;
59
60 bool accIsZeroInit = false;
61 vector::TransferReadOp lhsRead;
62 vector::TransferReadOp rhsRead;
63
64 // Memory backing the accumulator.
65 Value accMemIn;
66 Value accMemOut;
67
68 // Reconstructed accumulation loop with native-shaped iter_args.
69 scf::ForOp slicedAccLoop;
70
71 // Keep track of operations that should be deleted after transformation
72 // (depending on which ops could be integrated into the new loop nest).
73 SmallVector<Operation *> opsToDelete;
74};
75
76// A target-specific function that inspects the given types and shape of a
77// candidate contraction, and tries to compute a decomposition strategy encoded
78// as the desired register-tiled and native shapes. If successful, the result is
79// filled into the `contractRegTileShape` and `contractNativeShape` containers.
80using MatchTypesAndShapeFn = std::function<LogicalResult(
81 Type inputElemType, Type accumulatorElemType, bool isInVnniLayout,
82 ArrayRef<int64_t> contractShape,
83 SmallVectorImpl<int64_t> &contractRegTileShape,
84 SmallVectorImpl<int64_t> &contractNativeShape)>;
85} // anonymous namespace
86
87//----------------------------------------------------------------------------//
88// Forward declarations //
89//----------------------------------------------------------------------------//
90
91static void buildMNLoopBody(MLUCandidate &candidate, OpBuilder &b, Location loc,
92 Value ivM, Value ivN);
93static SmallVector<Value> buildKLoopBody(MLUCandidate &candidate, OpBuilder &b,
94 Location loc, Value ivM, Value ivN,
95 Value ivK, ValueRange iterArgs);
96
97//----------------------------------------------------------------------------//
98// Utilities //
99//----------------------------------------------------------------------------//
100
101// Return true if the argument is an all-zero vector.
102static bool isZeroVector(Value v) {
103 auto constOp = v.getDefiningOp<arith::ConstantOp>();
104 if (!constOp)
105 return false;
106 auto denseAttr = dyn_cast<DenseElementsAttr>(constOp.getValue());
107 if (!denseAttr || !denseAttr.isSplat())
108 return false;
109
110 return isZeroIntegerOrFloat(denseAttr.getSplatValue<Attribute>());
111}
112
113// Return true if given vector.transfer_read/write has a default permutation
114// map, no mask and no out-of-bounds check.
115static bool isSimpleTransferOp(VectorTransferOpInterface op) {
116 return op.getPermutationMap().isMinorIdentity() && !op.getMask() &&
117 !op.hasOutOfBoundsDim();
118}
119
120// Return true if start and end are in the same block and there are no ops with
121// memory effects between them.
122static bool noMemoryEffectsBetween(Operation *start, Operation *end) {
123 if (start->getBlock() != end->getBlock() || !start->isBeforeInBlock(end))
124 return false;
125
126 for (Operation *op = start->getNextNode(); op && op != end;
127 op = op->getNextNode()) {
128 if (!isMemoryEffectFree(op))
129 return false;
130 }
131 return true;
132}
133
134// Create a rank-reducing subview of a memref that matches the target vector
135// type, e.g. with targetType = vector<64x32xbf16>:
136//
137// memref.subview %A[%block_m, %block_k, 0, 0] [1, 1, 64, 32] [1, 1, 1, 1]
138// : memref<?x?x64x32xbf16> to
139// memref<64x32xbf16, strided<[32, 1], offset: ?>>
142 VectorType targetType) {
143 auto *ctx = b.getContext();
144
145 auto baseType = dyn_cast<MemRefType>(base.getType());
146 assert(baseType && "base must be a MemRefType");
147
148 int64_t baseRank = baseType.getRank();
149 int64_t targetRank = targetType.getRank();
150 assert(targetRank <= baseRank && "target shape's rank > memref's rank");
151
152 ArrayRef<int64_t> targetShape = targetType.getShape();
153
154 SmallVector<int64_t> sizesInt(baseType.getRank(), 1);
155 copy(targetShape, sizesInt.begin() + baseRank - targetRank);
156 SmallVector<int64_t> stridesInt(baseRank, 1);
157
158 auto offsets = getAsOpFoldResult(indices);
159 auto sizes = getAsIndexOpFoldResult(ctx, sizesInt);
160 auto strides = getAsIndexOpFoldResult(ctx, stridesInt);
161
162 MemRefType rankReducedType = memref::SubViewOp::inferRankReducedResultType(
163 targetShape, baseType, offsets, sizes, strides);
164 return memref::SubViewOp::create(b, loc, rankReducedType, base, offsets,
165 sizes, strides);
166}
167
168// Slice a 2D vector into smaller tiles of the given native type.
170 VectorType nativeType) {
171 VectorType regTileType = dyn_cast<VectorType>(value.getType());
172 assert(regTileType && "can only slice vector values");
173 ArrayRef<int64_t> regTileShape = regTileType.getShape();
174 ArrayRef<int64_t> nativeShape = nativeType.getShape();
175
176 assert(regTileShape.size() == 2 && nativeShape.size() == 2 &&
177 "Only 2D shapes are supported");
178
179 int64_t regTileM = regTileShape[0];
180 int64_t regTileN = regTileShape[1];
181 int64_t nativeM = nativeShape[0];
182 int64_t nativeN = nativeShape[1];
183
185 result.reserve((regTileM / nativeM) * (regTileN / nativeN));
186
187 for (int64_t m = 0; m < regTileM; m += nativeM)
188 for (int64_t n = 0; n < regTileN; n += nativeN)
189 result.push_back(vector::ExtractStridedSliceOp::create(
190 b, loc, value, ArrayRef<int64_t>{m, n}, nativeShape,
191 ArrayRef<int64_t>{1, 1}));
192
193 return result;
194}
195
196// Splice smaller tiles back into a 2D vector of the given register tile type.
198 VectorType regTileType) {
199 auto types = values.getTypes();
200 assert(!types.empty() && all_equal(types) &&
201 "All values must have the same type");
202
203 VectorType nativeType = dyn_cast<VectorType>(types.front());
204 assert(nativeType && "can only splice vector values");
205
206 ArrayRef<int64_t> nativeShape = nativeType.getShape();
207 ArrayRef<int64_t> regTileShape = regTileType.getShape();
208
209 assert(nativeShape.size() == 2 && regTileShape.size() == 2 &&
210 "Only 2D shapes are supported");
211
212 int64_t nativeM = nativeShape[0];
213 int64_t nativeN = nativeShape[1];
214 int64_t regTileM = regTileShape[0];
215 int64_t regTileN = regTileShape[1];
216
217 assert((regTileM / nativeM) * (regTileN / nativeN) ==
218 static_cast<int64_t>(values.size()) &&
219 "The number of values must match the number of slices");
220
221 Value result = ub::PoisonOp::create(b, loc, regTileType);
222
223 auto valueIt = values.begin();
224 for (int64_t m = 0; m < regTileM; m += nativeM)
225 for (int64_t n = 0; n < regTileN; n += nativeN) {
226 result = vector::InsertStridedSliceOp::create(b, loc, *valueIt, result,
227 ArrayRef<int64_t>{m, n},
228 ArrayRef<int64_t>{1, 1});
229 ++valueIt;
230 }
231
232 return result;
233}
234
235static std::optional<SmallVector<int64_t>>
238 if (!attr)
239 return std::nullopt;
240 return SmallVector<int64_t>(attr.asArrayRef());
241}
242
243static void setNativeShapeAttr(VectorTransferOpInterface op,
244 VectorType nativeType) {
245 if (op.getVectorType() != nativeType)
246 op->setAttr(
248 DenseI64ArrayAttr::get(nativeType.getContext(), nativeType.getShape()));
249}
250
251//----------------------------------------------------------------------------//
252// Pattern match and target shape computation //
253//----------------------------------------------------------------------------//
254
255// Establish whether the candidate contraction is a canonical matrix
256// multiplication on flat or VNNI-packed vectors. If successful, the discovered
257// operand and accumulator types are recorded in the candidate struct.
258//
259// Example (flat):
260// vector.contract {
261// kind = #vector.kind<add>, indexing_maps = [
262// affine_map<(dm, dn, dk) -> (dm, dk)>,
263// affine_map<(dm, dn, dk) -> (dk, dn)>,
264// affine_map<(dm, dn, dk) -> (dm, dn)>],
265// iterator_types = ["parallel", "parallel", "reduction"],
266// } %lhs, %rhs, %acc : !lhsType, !rhsType into !accType
267//
268// Example (VNNI-packed):
269// vector.contract {
270// kind = #vector.kind<add>, indexing_maps = [
271// affine_map<(dm, dn, dk, dvnni) -> (dm, dk, dvnni)>,
272// affine_map<(dm, dn, dk, dvnni) -> (dk, dn, dvnni)>,
273// affine_map<(dm, dn, dk, dvnni) -> (dm, dn)>],
274// iterator_types = ["parallel", "parallel", "reduction", "reduction"],
275// } %lhs, %rhs, %acc : !lhsType, !rhsType into !accType
276static LogicalResult matchCanonicalMatmul(MLUCandidate &candidate,
277 PatternRewriter &rewriter) {
278 vector::ContractionOp contract = candidate.contract;
279 if (contract.getKind() != vector::CombiningKind::ADD)
280 return rewriter.notifyMatchFailure(contract,
281 "not using ADD combining kind");
282
283 VectorType accType = dyn_cast<VectorType>(contract.getAccType());
284 if (!accType || accType.getRank() != 2)
285 return rewriter.notifyMatchFailure(contract,
286 "accumulator is not a 2D vector");
287
288 VectorType lhsType = contract.getLhsType();
289 VectorType rhsType = contract.getRhsType();
290 if (lhsType.getElementType() != rhsType.getElementType())
291 return rewriter.notifyMatchFailure(
292 contract, "LHS and RHS element types do not match");
293
294 auto iteratorTypes = contract.getIteratorTypesArray();
295 auto indexingMaps = contract.getIndexingMapsArray();
296 assert(indexingMaps.size() == 3 && "expected 3 indexing maps");
297 bool isInVnniLayout = x86::isInVnniLayout(contract, indexingMaps);
298
299 auto par = vector::IteratorType::parallel;
300 auto red = vector::IteratorType::reduction;
301 auto m = rewriter.getAffineDimExpr(0);
302 auto n = rewriter.getAffineDimExpr(1);
303 auto k = rewriter.getAffineDimExpr(2);
304 auto vnni = rewriter.getAffineDimExpr(3);
305
306 if (!isInVnniLayout) {
307 if (!equal(indexingMaps[0].getResults(), ArrayRef{m, k}) ||
308 !equal(indexingMaps[1].getResults(), ArrayRef{k, n}) ||
309 !equal(indexingMaps[2].getResults(), ArrayRef{m, n}))
310 return rewriter.notifyMatchFailure(contract,
311 "indexing maps are not canonical");
312
313 if (!equal(iteratorTypes, ArrayRef{par, par, red}))
314 return rewriter.notifyMatchFailure(contract,
315 "iterator types are not canonical");
316 } else {
317 if (!equal(indexingMaps[0].getResults(), ArrayRef{m, k, vnni}) ||
318 !equal(indexingMaps[1].getResults(), ArrayRef{k, n, vnni}) ||
319 !equal(indexingMaps[2].getResults(), ArrayRef{m, n}))
320 return rewriter.notifyMatchFailure(
321 contract, "indexing maps are not canonical (VNNI)");
322
323 if (!equal(iteratorTypes, ArrayRef{par, par, red, red}))
324 return rewriter.notifyMatchFailure(
325 contract, "iterator types are not canonical (VNNI)");
326 }
327
328 candidate.isInVnniLayout = isInVnniLayout;
329 candidate.accType = accType;
330 candidate.lhsType = lhsType;
331 candidate.rhsType = rhsType;
332
333 return success();
334}
335
336// Invoke the target-specific hook to compute a tiling and unrolling strategy
337// for the contraction, and check that it is consistent and compatible with the
338// current capabilities of this pattern. The result is recorded in the
339// `*regTileType`, `*nativeType`, and `nativeContractShape` fields of the
340// candidate struct.
341static LogicalResult
342matchTypesAndShape(MLUCandidate &candidate,
343 const MatchTypesAndShapeFn &matchTypesAndShapeForTarget,
344 PatternRewriter &rewriter) {
345 vector::ContractionOp contract = candidate.contract;
346
347 Type inpElemType = candidate.lhsType.getElementType();
348 Type accElemType = candidate.accType.getElementType();
349
350 // Contraction shape is {m, n, k} or {m, n, k, vnni}.
351 ArrayRef<int64_t> rhsShape = candidate.rhsType.getShape();
352 ArrayRef<int64_t> lhsShape = candidate.lhsType.getShape();
353 SmallVector<int64_t, 4> contractShape = {lhsShape[0], rhsShape[1],
354 lhsShape[1]};
355 if (candidate.isInVnniLayout)
356 contractShape.push_back(lhsShape[2]);
357
358 SmallVector<int64_t, 4> contractRegTileShape, contractNativeShape;
359 if (failed(matchTypesAndShapeForTarget(
360 inpElemType, accElemType, candidate.isInVnniLayout, contractShape,
361 contractRegTileShape, contractNativeShape)) ||
362 contractRegTileShape.size() < contractShape.size() ||
363 contractNativeShape.size() < contractShape.size())
364 return rewriter.notifyMatchFailure(
365 contract, "no tiling and unrolling strategy found");
366
367 auto maybeTileShapeRatio =
368 computeShapeRatio(contractShape, contractRegTileShape);
369 auto maybeUnrollShapeRatio =
370 computeShapeRatio(contractRegTileShape, contractNativeShape);
371
372 if (!maybeTileShapeRatio || !maybeUnrollShapeRatio)
373 return rewriter.notifyMatchFailure(
374 contract, "incompatible or inconsistent tiling and unrolling strategy");
375
376 auto isOne = [](int64_t v) { return v == 1; };
377 if (all_of(*maybeTileShapeRatio, isOne) &&
378 all_of(*maybeUnrollShapeRatio, isOne))
379 return rewriter.notifyMatchFailure(
380 contract, "contraction is already in native shape");
381
382 if (!all_of(drop_begin(*maybeTileShapeRatio, 2), isOne) ||
383 !all_of(drop_begin(*maybeUnrollShapeRatio, 2), isOne))
384 return rewriter.notifyMatchFailure(
385 contract, "cannot tile/unroll on the reduction dimensions");
386
387 int64_t regTileM = contractRegTileShape[0];
388 int64_t regTileN = contractRegTileShape[1];
389 int64_t regTileK = contractRegTileShape[2];
390
391 int64_t nativeM = contractNativeShape[0];
392 int64_t nativeN = contractNativeShape[1];
393 int64_t nativeK = contractNativeShape[2];
394
395 candidate.accRegTileType = VectorType::get({regTileM, regTileN}, accElemType);
396 candidate.accNativeType = VectorType::get({nativeM, nativeN}, accElemType);
397
398 auto getOperandType = [&](ArrayRef<int64_t> shape) {
399 if (candidate.isInVnniLayout) {
400 SmallVector<int64_t, 3> newShape(shape.begin(), shape.end());
401 newShape.push_back(contractNativeShape[3]);
402 return VectorType::get(newShape, inpElemType);
403 }
404 return VectorType::get(shape, inpElemType);
405 };
406
407 candidate.lhsRegTileType = getOperandType({regTileM, regTileK});
408 candidate.rhsRegTileType = getOperandType({regTileK, regTileN});
409
410 candidate.lhsNativeType = getOperandType({nativeM, nativeK});
411 candidate.rhsNativeType = getOperandType({nativeK, nativeN});
412
413 candidate.contractNativeShape.append(contractNativeShape.begin(),
414 contractNativeShape.end());
415
416 return success();
417}
418
419// Check constraints on the accumulation loop. We're looking for an index-based
420// for-loop with a step of 1 or K, a single accumulator block argument. The body
421// shall only contain the transfer_reads for the operands, the contraction and
422// the yield operation. This restriction is in place because the pattern
423// rebuilds the accumulation loop from scratch with the known operations.
424//
425// For the operand reads, we require that they have a minor identity permutation
426// map, impose no bounds checks and don't carry a mask. In addition, they shall
427// use the induction variable as exactly one of their indices. This covers plain
428// 2D memrefs as well as block-based layouts.
429//
430// If all these conditions are met, the loop op, initial value of the
431// accumulator and the operand reads are recorded in the candidate struct.
432//
433// Example:
434// !accType = vector<64x128xf32>
435// !lhsType = vector<64x32xbf16>
436// !rhsType = vector<32x128xbf16>
437// ...
438// %res = scf.for %k = %k_start to %k_end step %c1 iter_args(%acc = %c)
439// -> (!accType) {
440// %a = vector.transfer_read %A[%m, %k, %c0, %c0], %c0_0
441// {in_bounds = [true, true]} : memref<?x?x64x32xbf16>, !lhsType
442// %b = vector.transfer_read %B[%n, %k, %c0, %c0], %c0_0
443// {in_bounds = [true, true]} : memref<?x?x32x128xbf16>, !rhsType
444// %d = vector.contract { ... } %a, %b, %acc
445// : !lhsType, !rhsType into !accType
446// scf.yield %d : !accType
447// }
448static LogicalResult matchAccumulationLoop(MLUCandidate &candidate,
449 PatternRewriter &rewriter) {
450 auto &contract = candidate.contract;
451
452 auto accLoop = dyn_cast_if_present<scf::ForOp>(contract->getParentOp());
453 if (!accLoop || !accLoop.getInductionVar().getType().isIndex())
454 return rewriter.notifyMatchFailure(contract,
455 "is not in an index-based for-loop");
456
457 if (auto step = getConstantIntValue(accLoop.getStep());
458 !step || (*step != 1 && *step != candidate.contractNativeShape[2]))
459 return rewriter.notifyMatchFailure(contract,
460 "accumulation loop step is not 1 or K");
461
462 BlockArgument accIterArg = dyn_cast<BlockArgument>(contract.getAcc());
463 if (!accIterArg || accIterArg.getOwner() != accLoop.getBody())
464 return rewriter.notifyMatchFailure(contract,
465 "accumulator is not a block argument");
466
467 if (accLoop.getNumRegionIterArgs() != 1 ||
468 accLoop.getBody()->getOperations().size() != 4)
469 return rewriter.notifyMatchFailure(
470 contract, "accumulation loop has additional iter_args or operations");
471
472 if (!contract->getResult(0).hasOneUse())
473 return rewriter.notifyMatchFailure(contract,
474 "result does not have exactly one use");
475
476 Operation *user = *contract->getResult(0).getUsers().begin();
477 if (!isa<scf::YieldOp>(user))
478 return rewriter.notifyMatchFailure(contract,
479 "accumulator is not loop-carried");
480
481 Value accInitVal = accLoop.getTiedLoopInit(accIterArg)->get();
482 bool accIsZeroInit = isZeroVector(accInitVal);
483
484 auto lhsRead = contract.getLhs().getDefiningOp<vector::TransferReadOp>();
485 auto rhsRead = contract.getRhs().getDefiningOp<vector::TransferReadOp>();
486 if (!lhsRead || !rhsRead)
487 return rewriter.notifyMatchFailure(
488 contract, "LHS/RHS are not vector.transfer_read ops");
489
490 auto checkTransferOp = [&](vector::TransferReadOp transferOp) {
491 if (!isSimpleTransferOp(transferOp))
492 return rewriter.notifyMatchFailure(contract, [&](Diagnostic &diag) {
493 diag << "transfer op has non-identity permutation, mask, or "
494 "out-of-bounds check: "
495 << *transferOp;
496 });
497
498 if (count(transferOp.getIndices(), accLoop.getInductionVar()) != 1)
499 return rewriter.notifyMatchFailure(
500 contract, "transfer op does not use accumulator index");
501
502 return success();
503 };
504
505 if (failed(checkTransferOp(lhsRead)) || failed(checkTransferOp(rhsRead)))
506 return failure();
507
508 candidate.accLoop = accLoop;
509 candidate.accInitVal = accInitVal;
510 candidate.accIsZeroInit = accIsZeroInit;
511 candidate.lhsRead = lhsRead;
512 candidate.rhsRead = rhsRead;
513
514 return success();
515}
516
517//----------------------------------------------------------------------------//
518// Transformation
519//----------------------------------------------------------------------------//
520
521// Ensure that the initial accumulator value can be sliced into register-tile
522// shaped vectors. Unless it is zero-initialized, we need it to be backed
523// by memory so that the slice can be loaded via a vector.transfer_read inside
524// the loop nest.
525//
526// If the initial value is produced by a simple vector.transfer_read, we can
527// simply derive a subview from its base and indices. Otherwise, we fall back to
528// allocating a new buffer on stack and store the initial value into it.
529static void makeAccInitializationBackedByMemory(MLUCandidate &candidate,
530 PatternRewriter &rewriter) {
531 if (candidate.accIsZeroInit)
532 return;
533
534 OpBuilder::InsertionGuard guard(rewriter);
535 rewriter.setInsertionPoint(candidate.accLoop);
536 auto loc = candidate.accLoop.getLoc();
537
538 if (auto accRead =
539 candidate.accInitVal.getDefiningOp<vector::TransferReadOp>();
540 accRead && isSimpleTransferOp(accRead) &&
541 noMemoryEffectsBetween(accRead, candidate.accLoop)) {
542 candidate.accMemIn =
543 makeRankReducingSubview(rewriter, loc, accRead.getBase(),
544 accRead.getIndices(), accRead.getVectorType());
545 return;
546 }
547
548 auto accBufferType = MemRefType::get(candidate.accType.getShape(),
549 candidate.accType.getElementType());
550 candidate.accMemIn = memref::AllocaOp::create(rewriter, loc, accBufferType);
551
552 Value c0 = arith::ConstantIndexOp::create(rewriter, loc, 0);
553 SmallVector<Value, 2> zeroIndices(candidate.accType.getRank(), c0);
554 vector::TransferWriteOp::create(rewriter, loc, candidate.accInitVal,
555 candidate.accMemIn, zeroIndices);
556}
557
558// Ensure that slices of the result of the accumulator loop can be stored, so
559// again we need it to be backed by memory.
560//
561// If the only user of the result is a simple vector.transfer_write, and its
562// base and indices are defined before the accumulator loop, we can derive a
563// subview. The transfer_write will be deleted after building the loop nest,
564// which at that point incorporates its effect.
565//
566// Otherwise, we fall back to buffering the result and rematerializing it after
567// the loop. If we already had a stack allocation for the initial value, we
568// reuse it.
569static void makeAccResultBackedByMemory(MLUCandidate &candidate,
570 PatternRewriter &rewriter) {
571 OpBuilder::InsertionGuard guard(rewriter);
572 rewriter.setInsertionPoint(candidate.accLoop);
573 auto loc = candidate.accLoop.getLoc();
574
575 auto accRes = candidate.accLoop.getResult(0);
576 vector::TransferWriteOp accWrite;
577 if (accRes.hasOneUse())
578 accWrite = dyn_cast<vector::TransferWriteOp>(*accRes.getUsers().begin());
579
580 Block *accLoopBlock = candidate.accLoop->getBlock();
581 if (accWrite && isSimpleTransferOp(accWrite) &&
582 noMemoryEffectsBetween(candidate.accLoop, accWrite)) {
583
584 Value accWriteBase = accWrite.getBase();
585 ValueRange accWriteIndices = accWrite.getIndices();
586
587 auto isSafeToUseView = [&]() {
588 if (candidate.accIsZeroInit)
589 return true;
590 auto accMemInView = candidate.accMemIn.getDefiningOp<memref::SubViewOp>();
591 if (!accMemInView)
592 return true;
593
594 return accMemInView.getSource() == accWriteBase &&
595 isEqualConstantIntOrValueArray(accMemInView.getMixedOffsets(),
596 getAsOpFoldResult(accWriteIndices));
597 };
598
599 auto isDefinedBeforeAccLoop = [&](Value v) {
600 Operation *defOp = v.getDefiningOp();
601 // No need for full dominance check, as we already know that accLoop and
602 // accWrite are in the same block.
603 return !defOp || defOp->getBlock() != accLoopBlock ||
604 defOp->isBeforeInBlock(candidate.accLoop);
605 };
606
607 if (isSafeToUseView() && isDefinedBeforeAccLoop(accWriteBase) &&
608 all_of(accWriteIndices, isDefinedBeforeAccLoop)) {
609 candidate.accMemOut =
610 makeRankReducingSubview(rewriter, loc, accWriteBase, accWriteIndices,
611 accWrite.getVectorType());
612 candidate.opsToDelete.push_back(accWrite);
613 candidate.opsToDelete.push_back(candidate.accLoop);
614 return;
615 }
616 }
617
618 if (!candidate.accIsZeroInit &&
619 isa<memref::AllocaOp>(candidate.accMemIn.getDefiningOp())) {
620 candidate.accMemOut = candidate.accMemIn;
621 } else {
622 auto accBufferType = MemRefType::get(candidate.accType.getShape(),
623 candidate.accType.getElementType());
624 candidate.accMemOut =
625 memref::AllocaOp::create(rewriter, loc, accBufferType);
626 }
627
628 rewriter.setInsertionPointAfter(candidate.accLoop);
629 Value c0 = arith::ConstantIndexOp::create(rewriter, loc, 0);
630 SmallVector<Value, 2> zeroIndices(candidate.accType.getRank(), c0);
631 auto accRemat = vector::TransferReadOp::create(
632 rewriter, loc, candidate.accType, candidate.accMemOut, zeroIndices,
633 /*padding=*/std::nullopt);
634
635 rewriter.replaceAllUsesWith(candidate.accLoop.getResult(0), accRemat);
636 candidate.opsToDelete.push_back(candidate.accLoop);
637}
638
639// Create the loop nest for iterating over the M and N dimensions of the
640// accumulator. Construction continues in buildMNLoopBody.
641static void buildLoopNestForCandidate(MLUCandidate &candidate,
642 PatternRewriter &rewriter) {
643 OpBuilder::InsertionGuard guard(rewriter);
644 rewriter.setInsertionPoint(candidate.accLoop);
645 auto loc = candidate.accLoop.getLoc();
646
647 ArrayRef<int64_t> accShape = candidate.accType.getShape();
648 ArrayRef<int64_t> accRegTileShape = candidate.accRegTileType.getShape();
649
650 auto makeIndexConst = [&](int64_t v) {
651 return arith::ConstantIndexOp::create(rewriter, loc, v);
652 };
653
654 Value c0 = makeIndexConst(0);
655 Value ubM = makeIndexConst(accShape[0]);
656 Value stepM = makeIndexConst(accRegTileShape[0]);
657 Value ubN = makeIndexConst(accShape[1]);
658 Value stepN = makeIndexConst(accRegTileShape[1]);
659
660 scf::ForOp::create(
661 rewriter, loc, c0, ubM, stepM, {},
662 [&](OpBuilder &b, Location loc, Value ivM, ValueRange iterArgs) {
663 scf::ForOp::create(
664 b, loc, c0, ubN, stepN, {},
665 [&](OpBuilder &b, Location loc, Value ivN, ValueRange iterArgs) {
666 buildMNLoopBody(candidate, b, loc, ivM, ivN);
667 scf::YieldOp::create(b, loc);
668 });
669 scf::YieldOp::create(b, loc);
670 });
671}
672
673// Create the body for M-N-loop nest according to the following scheme:
674//
675// 1. Load a accRegTileType-shaped slice of the accumulator from memory (or make
676// a zero constant).
677// 2. Split the initial value into accNativeType-shaped slices.
678// 3. Create the K-loop with the initial value slices as loop-carried arguments,
679// i.e. the iter_args have accNativeType.
680// 4. -> buildKLoopBody
681// 5. Splice the final value slices back into an accRegTileType-shaped value and
682// write it back to memory.
683//
684// The accRegTileType-shaped operations are annotated with an attribute in order
685// to unroll them with the vector-dialect's unroll patterns.
686static void buildMNLoopBody(MLUCandidate &candidate, OpBuilder &b, Location loc,
687 Value ivM, Value ivN) {
688 ArrayRef<int64_t> accRegTileShape = candidate.accRegTileType.getShape();
689
691
692 SmallVector<OpFoldResult, 2> accViewOffsets = {ivM, ivN};
693 SmallVector<OpFoldResult, 2> accViewSizes = {
694 b.getI64IntegerAttr(accRegTileShape[0]),
695 b.getI64IntegerAttr(accRegTileShape[1])};
696 SmallVector<OpFoldResult, 2> accViewStrides = {b.getI64IntegerAttr(1),
697 b.getI64IntegerAttr(1)};
698
699 Operation *accInit;
700 if (candidate.accIsZeroInit) {
701 accInit = arith::ConstantOp::create(
702 b, loc, b.getZeroAttr(candidate.accRegTileType));
703 } else {
704 Value accViewIn =
705 memref::SubViewOp::create(b, loc, candidate.accMemIn, accViewOffsets,
706 accViewSizes, accViewStrides);
707 auto accInitRead = vector::TransferReadOp::create(
708 b, loc, candidate.accRegTileType, accViewIn, ValueRange{c0, c0},
709 /*padding=*/std::nullopt);
710 setNativeShapeAttr(accInitRead, candidate.accNativeType);
711 accInit = accInitRead;
712 }
713
714 SmallVector<Value> accInitSliced =
715 slice(b, loc, accInit->getResult(0), candidate.accNativeType);
716 candidate.slicedAccLoop = scf::ForOp::create(
717 b, loc, candidate.accLoop.getLowerBound(),
718 candidate.accLoop.getUpperBound(), candidate.accLoop.getStep(),
719 accInitSliced,
720 [&](OpBuilder &b, Location loc, Value ivK, ValueRange iterArgs) {
721 SmallVector<Value> newIterArgs =
722 buildKLoopBody(candidate, b, loc, ivM, ivN, ivK, iterArgs);
723 scf::YieldOp::create(b, loc, newIterArgs);
724 });
725
726 Value accSpliced = splice(b, loc, candidate.slicedAccLoop.getResults(),
727 candidate.accRegTileType);
728 Value accViewOut =
729 memref::SubViewOp::create(b, loc, candidate.accMemOut, accViewOffsets,
730 accViewSizes, accViewStrides);
731 auto accWrite = vector::TransferWriteOp::create(
732 b, loc, accSpliced, accViewOut, ValueRange{c0, c0});
733
734 setNativeShapeAttr(accWrite, candidate.accNativeType);
735}
736
737// Rebuild the accumulation loop with [lhs|rhs]RegTileType-shaped operations,
738// taking the offsets from the M-N-loop nests into account. The accumulator
739// iter_args are spliced into a accRegTileType-shaped value, and the contraction
740// result is split into accNativeType-shaped yield operands. Again, the
741// operations are annotated with an attribute for unrolling, after which the
742// splice/split logic will fold away completely.
743static SmallVector<Value> buildKLoopBody(MLUCandidate &candidate, OpBuilder &b,
744 Location loc, Value ivM, Value ivN,
745 Value ivK, ValueRange iterArgs) {
746 Value accSpliced = splice(b, loc, iterArgs, candidate.accRegTileType);
747
748 SmallVector<Value, 4> lhsOffsets = candidate.lhsRead.getIndices();
749 SmallVector<Value, 4> rhsOffsets = candidate.rhsRead.getIndices();
750 Value accLoopIV = candidate.accLoop.getInductionVar();
751 replace(lhsOffsets, accLoopIV, ivK);
752 replace(rhsOffsets, accLoopIV, ivK);
753 unsigned mPos = lhsOffsets.size() - 2 - candidate.isInVnniLayout;
754 unsigned nPos = rhsOffsets.size() - 1 - candidate.isInVnniLayout;
755 lhsOffsets[mPos] = arith::AddIOp::create(b, loc, lhsOffsets[mPos], ivM);
756 rhsOffsets[nPos] = arith::AddIOp::create(b, loc, rhsOffsets[nPos], ivN);
757 Value lhsView = makeRankReducingSubview(b, loc, candidate.lhsRead.getBase(),
758 lhsOffsets, candidate.lhsRegTileType);
759 Value rhsView = makeRankReducingSubview(b, loc, candidate.rhsRead.getBase(),
760 rhsOffsets, candidate.rhsRegTileType);
761
762 SmallVector<Value, 3> zeroIndices(2 + candidate.isInVnniLayout,
764 auto lhsRead = vector::TransferReadOp::create(
765 b, candidate.lhsRead.getLoc(), candidate.lhsRegTileType, lhsView,
766 zeroIndices, /*padding=*/std::nullopt);
767 auto rhsRead = vector::TransferReadOp::create(
768 b, candidate.rhsRead.getLoc(), candidate.rhsRegTileType, rhsView,
769 zeroIndices, /*padding=*/std::nullopt);
770 auto contract = vector::ContractionOp::create(
771 b, candidate.contract.getLoc(), lhsRead, rhsRead, accSpliced,
772 candidate.contract.getIndexingMaps(),
773 candidate.contract.getIteratorTypes());
774
775 setNativeShapeAttr(lhsRead, candidate.lhsNativeType);
776 setNativeShapeAttr(rhsRead, candidate.rhsNativeType);
778 b.getDenseI64ArrayAttr(candidate.contractNativeShape));
779
780 SmallVector<Value> accSliced =
781 slice(b, loc, contract, candidate.accNativeType);
782 return accSliced;
783}
784
785//----------------------------------------------------------------------------//
786// Pattern implementation //
787//----------------------------------------------------------------------------//
788
789namespace {
790// Prepares a contraction inside an accumulation loop for the nanokernel
791// lowerings.
792//
793// The pattern determines a register tile size for the contraction that can be
794// fully unrolled to a target-specific native shape without exceeding the
795// target's vector registers or AMX tile registers. It then introduces a
796// M-N-loop nest to iterate over such register tiles, and applies the vector
797// dialect's unroll patterns to produce native-shaped operations that the
798// nanokernel patterns can match.
799//
800// Currently, the match is deliberately strict (see the helpers above):
801// The contraction must be a canonical matrix multiplication, have
802// target-compatible types and shapes that can be tiled and unrolled cleanly,
803// and be embedded in an accumulation loop containing only the operand reads and
804// itself. This lets the rewrite regenerate the loop from scratch instead of
805// transforming it in place.
806struct VectorContractMultiLevelUnroll
807 : public OpRewritePattern<vector::ContractionOp> {
808 VectorContractMultiLevelUnroll(
809 MLIRContext *context,
810 const MatchTypesAndShapeFn &matchTypesAndShapeForTarget,
811 PatternBenefit benefit = 1)
812 : OpRewritePattern<vector::ContractionOp>(context, benefit),
813 matchTypesAndShapeForTarget(matchTypesAndShapeForTarget) {}
814
815 LogicalResult matchAndRewrite(vector::ContractionOp contract,
816 PatternRewriter &rewriter) const override {
817 // Bail out early if this op is the result of a previous application of the
818 // pattern.
819 if (contract->hasAttr(nativeShapeAttrName))
820 return failure();
821
822 MLUCandidate candidate;
823 candidate.contract = contract;
824 if (failed(matchCanonicalMatmul(candidate, rewriter)) ||
825 failed(matchTypesAndShape(candidate, matchTypesAndShapeForTarget,
826 rewriter)) ||
827 failed(matchAccumulationLoop(candidate, rewriter)))
828 return failure();
829
830 makeAccInitializationBackedByMemory(candidate, rewriter);
831 makeAccResultBackedByMemory(candidate, rewriter);
832 buildLoopNestForCandidate(candidate, rewriter);
833 moveLoopInvariantCode(candidate.slicedAccLoop);
834 for (auto *op : candidate.opsToDelete)
835 rewriter.eraseOp(op);
836
837 return success();
838 }
839
840private:
841 MatchTypesAndShapeFn matchTypesAndShapeForTarget;
842};
843} // namespace
844
845//----------------------------------------------------------------------------//
846// Target-specific matchers //
847//----------------------------------------------------------------------------//
848
849// Tiling strategy for AMX tile dot-product: The preferred native layout is
850// to keep the accumulator on a 2x2 grid of tile registers, though 1x2 and
851// VNNI-prepacked 1x1 and 2x1 configurations are also supported (NB: online
852// packing requires two neighboring tiles in the N-dimension).
853static LogicalResult
854matchTypesAndShapeForAMX(Type inputElemType, Type accumulatorElemType,
855 bool isInVnniLayout, ArrayRef<int64_t> contractShape,
856 SmallVectorImpl<int64_t> &contractRegTileShape,
857 SmallVectorImpl<int64_t> &contractNativeShape) {
858
859 if (!((inputElemType.isBF16() && accumulatorElemType.isF32()) ||
860 (inputElemType.isInteger(8) && accumulatorElemType.isInteger(32))))
861 return failure();
862
863 int64_t origM = contractShape[0];
864 int64_t origN = contractShape[1];
865
866 int64_t regTileM, regTileN, regTileK;
867 int64_t nativeM, nativeN, nativeK, vnni;
868
869 vnni = 32 / inputElemType.getIntOrFloatBitWidth();
870 nativeM = nativeN = 16;
871 nativeK = isInVnniLayout ? 16 : 16 * vnni;
872
873 // Prefer 2x2 tiling if shapes allow. Online packing requires a contract pair
874 // in the N-dimension.
875 regTileM = origM % (2 * nativeM) == 0 ? 2 * nativeM : nativeM;
876 regTileN =
877 origN % (2 * nativeN) == 0 || !isInVnniLayout ? 2 * nativeN : nativeN;
878 regTileK = nativeK;
879
880 contractRegTileShape.append({regTileM, regTileN, regTileK});
881 contractNativeShape.append({nativeM, nativeN, nativeK});
882 if (isInVnniLayout) {
883 contractRegTileShape.push_back(vnni);
884 contractNativeShape.push_back(vnni);
885 }
886
887 return success();
888}
889
890//----------------------------------------------------------------------------//
891// Public interface //
892//----------------------------------------------------------------------------//
893
895 RewritePatternSet &patterns) {
896 patterns.add<VectorContractMultiLevelUnroll>(patterns.getContext(),
898 vector::UnrollVectorOptions nativeUnrollOptions;
899 nativeUnrollOptions.setNativeShapeFn(getFromNativeShapeAttr);
900 vector::populateVectorUnrollPatterns(patterns, nativeUnrollOptions);
901}
return success()
static void copy(Location loc, Value dst, Value src, Value size, OpBuilder &builder)
Copies the given number of bytes from src to dst pointers.
b
Return true if permutation is a valid permutation of the outer_dims_perm (case OuterOrInnerPerm::Oute...
static void contract(RootOrderingGraph &graph, ArrayRef< Value > cycle, const DenseMap< Value, unsigned > &parentDepths, DenseMap< Value, Value > &actualSource, DenseMap< Value, Value > &actualTarget)
Contracts the specified cycle in the given graph in-place.
static void buildLoopNestForCandidate(MLUCandidate &candidate, PatternRewriter &rewriter)
static SmallVector< Value > buildKLoopBody(MLUCandidate &candidate, OpBuilder &b, Location loc, Value ivM, Value ivN, Value ivK, ValueRange iterArgs)
static std::optional< SmallVector< int64_t > > getFromNativeShapeAttr(Operation *op)
static Value makeRankReducingSubview(OpBuilder &b, Location loc, Value base, ValueRange indices, VectorType targetType)
static void makeAccInitializationBackedByMemory(MLUCandidate &candidate, PatternRewriter &rewriter)
static Value splice(OpBuilder &b, Location loc, ValueRange values, VectorType regTileType)
static void buildMNLoopBody(MLUCandidate &candidate, OpBuilder &b, Location loc, Value ivM, Value ivN)
static void makeAccResultBackedByMemory(MLUCandidate &candidate, PatternRewriter &rewriter)
static void setNativeShapeAttr(VectorTransferOpInterface op, VectorType nativeType)
static bool isZeroVector(Value v)
static constexpr auto nativeShapeAttrName
static LogicalResult matchTypesAndShape(MLUCandidate &candidate, const MatchTypesAndShapeFn &matchTypesAndShapeForTarget, PatternRewriter &rewriter)
static LogicalResult matchAccumulationLoop(MLUCandidate &candidate, PatternRewriter &rewriter)
static bool noMemoryEffectsBetween(Operation *start, Operation *end)
static LogicalResult matchTypesAndShapeForAMX(Type inputElemType, Type accumulatorElemType, bool isInVnniLayout, ArrayRef< int64_t > contractShape, SmallVectorImpl< int64_t > &contractRegTileShape, SmallVectorImpl< int64_t > &contractNativeShape)
static LogicalResult matchCanonicalMatmul(MLUCandidate &candidate, PatternRewriter &rewriter)
static SmallVector< Value > slice(OpBuilder &b, Location loc, Value value, VectorType nativeType)
static bool isSimpleTransferOp(VectorTransferOpInterface op)
Attributes are known-constant values of operations.
Definition Attributes.h:25
This class represents an argument of a Block.
Definition Value.h:306
Block * getOwner() const
Returns the block that owns this argument.
Definition Value.h:315
Block represents an ordered list of Operations.
Definition Block.h:34
AffineExpr getAffineDimExpr(unsigned position)
Definition Builders.cpp:373
This class contains all of the information necessary to report a diagnostic to the DiagnosticEngine.
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
RAII guard to reset the insertion point of the builder when destroyed.
Definition Builders.h:351
This class helps build Operations.
Definition Builders.h:210
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
Definition Builders.h:401
void setInsertionPointAfter(Operation *op)
Sets the insertion point to the node after the specified operation, which will cause subsequent inser...
Definition Builders.h:415
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
AttrClass getAttrOfType(StringAttr name)
Definition Operation.h:602
bool isBeforeInBlock(Operation *other)
Given an operation 'other' that is within the same parent block, return whether the current operation...
Block * getBlock()
Returns the operation block that contains this operation.
Definition Operation.h:230
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
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 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,...
virtual void replaceAllUsesWith(Value from, Value to)
Find uses of from and replace them with to.
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
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 provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
type_range getTypes() const
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
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:398
static DenseArrayAttrImpl get(MLIRContext *context, ArrayRef< int64_t > content)
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:734
void populateVectorContractMultiLevelUnrollForAMXPatterns(RewritePatternSet &patterns)
bool isInVnniLayout(Operation *op, llvm::ArrayRef< AffineMap > indexingMaps, std::optional< unsigned > blockingFactor=std::nullopt)
Definition X86Utils.cpp:42
Include the generated interface declarations.
OpFoldResult getAsIndexOpFoldResult(MLIRContext *ctx, int64_t val)
Convert int64_t to integer attributes of index type and return them as OpFoldResult.
detail::DenseArrayAttrImpl< int64_t > DenseI64ArrayAttr
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
bool isMemoryEffectFree(Operation *op)
Returns true if the given operation is free of memory effects.
bool isZeroIntegerOrFloat(OpFoldResult v)
Return "true" if v is an integer/float value/attribute with constant value zero.
bool isEqualConstantIntOrValueArray(ArrayRef< OpFoldResult > ofrs1, ArrayRef< OpFoldResult > ofrs2)
OpFoldResult getAsOpFoldResult(Value val)
Given a value, try to extract a constant Attribute.
std::optional< SmallVector< int64_t > > computeShapeRatio(ArrayRef< int64_t > shape, ArrayRef< int64_t > subShape)
Return the multi-dimensional integral ratio of subShape to the trailing dimensions of shape.
size_t moveLoopInvariantCode(ArrayRef< Region * > regions, function_ref< bool(Value, Region *)> isDefinedOutsideRegion, function_ref< bool(Operation *, Region *)> shouldMoveOutOfRegion, function_ref< void(Operation *, Region *)> moveOutOfRegion)
Given a list of regions, perform loop-invariant code motion.
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...
Options that control the vector unrolling.
UnrollVectorOptions & setNativeShapeFn(NativeShapeFnType fn)