21#define DEBUG_TYPE "x86-vector-contract-multi-level-unroll"
36 bool isInVnniLayout =
false;
45 VectorType accRegTileType;
46 VectorType lhsRegTileType;
47 VectorType rhsRegTileType;
51 VectorType accNativeType;
52 VectorType lhsNativeType;
53 VectorType rhsNativeType;
60 bool accIsZeroInit =
false;
61 vector::TransferReadOp lhsRead;
62 vector::TransferReadOp rhsRead;
69 scf::ForOp slicedAccLoop;
80using MatchTypesAndShapeFn = std::function<LogicalResult(
81 Type inputElemType,
Type accumulatorElemType,
bool isInVnniLayout,
106 auto denseAttr = dyn_cast<DenseElementsAttr>(constOp.getValue());
107 if (!denseAttr || !denseAttr.isSplat())
116 return op.getPermutationMap().isMinorIdentity() && !op.getMask() &&
117 !op.hasOutOfBoundsDim();
126 for (
Operation *op = start->getNextNode(); op && op != end;
127 op = op->getNextNode()) {
142 VectorType targetType) {
143 auto *ctx =
b.getContext();
145 auto baseType = dyn_cast<MemRefType>(base.
getType());
146 assert(baseType &&
"base must be a MemRefType");
148 int64_t baseRank = baseType.getRank();
149 int64_t targetRank = targetType.getRank();
150 assert(targetRank <= baseRank &&
"target shape's rank > memref's rank");
155 copy(targetShape, sizesInt.begin() + baseRank - targetRank);
162 MemRefType rankReducedType = memref::SubViewOp::inferRankReducedResultType(
163 targetShape, baseType, offsets, sizes, strides);
164 return memref::SubViewOp::create(
b, loc, rankReducedType, base, offsets,
170 VectorType nativeType) {
171 VectorType regTileType = dyn_cast<VectorType>(value.
getType());
172 assert(regTileType &&
"can only slice vector values");
176 assert(regTileShape.size() == 2 && nativeShape.size() == 2 &&
177 "Only 2D shapes are supported");
179 int64_t regTileM = regTileShape[0];
180 int64_t regTileN = regTileShape[1];
181 int64_t nativeM = nativeShape[0];
182 int64_t nativeN = nativeShape[1];
185 result.reserve((regTileM / nativeM) * (regTileN / nativeN));
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(
198 VectorType regTileType) {
200 assert(!types.empty() && all_equal(types) &&
201 "All values must have the same type");
203 VectorType nativeType = dyn_cast<VectorType>(types.front());
204 assert(nativeType &&
"can only splice vector values");
209 assert(nativeShape.size() == 2 && regTileShape.size() == 2 &&
210 "Only 2D shapes are supported");
212 int64_t nativeM = nativeShape[0];
213 int64_t nativeN = nativeShape[1];
214 int64_t regTileM = regTileShape[0];
215 int64_t regTileN = regTileShape[1];
217 assert((regTileM / nativeM) * (regTileN / nativeN) ==
218 static_cast<int64_t>(values.size()) &&
219 "The number of values must match the number of slices");
221 Value result = ub::PoisonOp::create(
b, loc, regTileType);
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,
235static std::optional<SmallVector<int64_t>>
244 VectorType nativeType) {
245 if (op.getVectorType() != nativeType)
278 vector::ContractionOp
contract = candidate.contract;
279 if (
contract.getKind() != vector::CombiningKind::ADD)
281 "not using ADD combining kind");
283 VectorType accType = dyn_cast<VectorType>(
contract.getAccType());
284 if (!accType || accType.getRank() != 2)
286 "accumulator is not a 2D vector");
288 VectorType lhsType =
contract.getLhsType();
289 VectorType rhsType =
contract.getRhsType();
290 if (lhsType.getElementType() != rhsType.getElementType())
292 contract,
"LHS and RHS element types do not match");
294 auto iteratorTypes =
contract.getIteratorTypesArray();
295 auto indexingMaps =
contract.getIndexingMapsArray();
296 assert(indexingMaps.size() == 3 &&
"expected 3 indexing maps");
299 auto par = vector::IteratorType::parallel;
300 auto red = vector::IteratorType::reduction;
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}))
311 "indexing maps are not canonical");
313 if (!equal(iteratorTypes,
ArrayRef{par, par, red}))
315 "iterator types are not canonical");
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}))
321 contract,
"indexing maps are not canonical (VNNI)");
323 if (!equal(iteratorTypes,
ArrayRef{par, par, red, red}))
325 contract,
"iterator types are not canonical (VNNI)");
328 candidate.isInVnniLayout = isInVnniLayout;
329 candidate.accType = accType;
330 candidate.lhsType = lhsType;
331 candidate.rhsType = rhsType;
343 const MatchTypesAndShapeFn &matchTypesAndShapeForTarget,
345 vector::ContractionOp
contract = candidate.contract;
347 Type inpElemType = candidate.lhsType.getElementType();
348 Type accElemType = candidate.accType.getElementType();
355 if (candidate.isInVnniLayout)
356 contractShape.push_back(lhsShape[2]);
359 if (failed(matchTypesAndShapeForTarget(
360 inpElemType, accElemType, candidate.isInVnniLayout, contractShape,
361 contractRegTileShape, contractNativeShape)) ||
362 contractRegTileShape.size() < contractShape.size() ||
363 contractNativeShape.size() < contractShape.size())
365 contract,
"no tiling and unrolling strategy found");
367 auto maybeTileShapeRatio =
369 auto maybeUnrollShapeRatio =
372 if (!maybeTileShapeRatio || !maybeUnrollShapeRatio)
374 contract,
"incompatible or inconsistent tiling and unrolling strategy");
376 auto isOne = [](
int64_t v) {
return v == 1; };
377 if (all_of(*maybeTileShapeRatio, isOne) &&
378 all_of(*maybeUnrollShapeRatio, isOne))
380 contract,
"contraction is already in native shape");
382 if (!all_of(drop_begin(*maybeTileShapeRatio, 2), isOne) ||
383 !all_of(drop_begin(*maybeUnrollShapeRatio, 2), isOne))
385 contract,
"cannot tile/unroll on the reduction dimensions");
387 int64_t regTileM = contractRegTileShape[0];
388 int64_t regTileN = contractRegTileShape[1];
389 int64_t regTileK = contractRegTileShape[2];
391 int64_t nativeM = contractNativeShape[0];
392 int64_t nativeN = contractNativeShape[1];
393 int64_t nativeK = contractNativeShape[2];
395 candidate.accRegTileType = VectorType::get({regTileM, regTileN}, accElemType);
396 candidate.accNativeType = VectorType::get({nativeM, nativeN}, accElemType);
399 if (candidate.isInVnniLayout) {
401 newShape.push_back(contractNativeShape[3]);
402 return VectorType::get(newShape, inpElemType);
404 return VectorType::get(
shape, inpElemType);
407 candidate.lhsRegTileType = getOperandType({regTileM, regTileK});
408 candidate.rhsRegTileType = getOperandType({regTileK, regTileN});
410 candidate.lhsNativeType = getOperandType({nativeM, nativeK});
411 candidate.rhsNativeType = getOperandType({nativeK, nativeN});
413 candidate.contractNativeShape.append(contractNativeShape.begin(),
414 contractNativeShape.end());
450 auto &
contract = candidate.contract;
452 auto accLoop = dyn_cast_if_present<scf::ForOp>(
contract->getParentOp());
453 if (!accLoop || !accLoop.getInductionVar().getType().isIndex())
455 "is not in an index-based for-loop");
458 !step || (*step != 1 && *step != candidate.contractNativeShape[2]))
460 "accumulation loop step is not 1 or K");
463 if (!accIterArg || accIterArg.
getOwner() != accLoop.getBody())
465 "accumulator is not a block argument");
467 if (accLoop.getNumRegionIterArgs() != 1 ||
468 accLoop.getBody()->getOperations().size() != 4)
470 contract,
"accumulation loop has additional iter_args or operations");
472 if (!
contract->getResult(0).hasOneUse())
474 "result does not have exactly one use");
477 if (!isa<scf::YieldOp>(user))
479 "accumulator is not loop-carried");
481 Value accInitVal = accLoop.getTiedLoopInit(accIterArg)->get();
484 auto lhsRead =
contract.getLhs().getDefiningOp<vector::TransferReadOp>();
485 auto rhsRead =
contract.getRhs().getDefiningOp<vector::TransferReadOp>();
486 if (!lhsRead || !rhsRead)
488 contract,
"LHS/RHS are not vector.transfer_read ops");
490 auto checkTransferOp = [&](vector::TransferReadOp transferOp) {
493 diag <<
"transfer op has non-identity permutation, mask, or "
494 "out-of-bounds check: "
498 if (count(transferOp.getIndices(), accLoop.getInductionVar()) != 1)
500 contract,
"transfer op does not use accumulator index");
505 if (failed(checkTransferOp(lhsRead)) || failed(checkTransferOp(rhsRead)))
508 candidate.accLoop = accLoop;
509 candidate.accInitVal = accInitVal;
510 candidate.accIsZeroInit = accIsZeroInit;
511 candidate.lhsRead = lhsRead;
512 candidate.rhsRead = rhsRead;
531 if (candidate.accIsZeroInit)
536 auto loc = candidate.accLoop.getLoc();
539 candidate.accInitVal.
getDefiningOp<vector::TransferReadOp>();
544 accRead.getIndices(), accRead.getVectorType());
548 auto accBufferType = MemRefType::get(candidate.accType.getShape(),
549 candidate.accType.getElementType());
550 candidate.accMemIn = memref::AllocaOp::create(rewriter, loc, accBufferType);
554 vector::TransferWriteOp::create(rewriter, loc, candidate.accInitVal,
555 candidate.accMemIn, zeroIndices);
573 auto loc = candidate.accLoop.getLoc();
575 auto accRes = candidate.accLoop.getResult(0);
576 vector::TransferWriteOp accWrite;
577 if (accRes.hasOneUse())
578 accWrite = dyn_cast<vector::TransferWriteOp>(*accRes.getUsers().begin());
580 Block *accLoopBlock = candidate.accLoop->getBlock();
584 Value accWriteBase = accWrite.getBase();
585 ValueRange accWriteIndices = accWrite.getIndices();
587 auto isSafeToUseView = [&]() {
588 if (candidate.accIsZeroInit)
590 auto accMemInView = candidate.accMemIn.
getDefiningOp<memref::SubViewOp>();
594 return accMemInView.getSource() == accWriteBase &&
599 auto isDefinedBeforeAccLoop = [&](
Value v) {
603 return !defOp || defOp->
getBlock() != accLoopBlock ||
607 if (isSafeToUseView() && isDefinedBeforeAccLoop(accWriteBase) &&
608 all_of(accWriteIndices, isDefinedBeforeAccLoop)) {
609 candidate.accMemOut =
611 accWrite.getVectorType());
612 candidate.opsToDelete.push_back(accWrite);
613 candidate.opsToDelete.push_back(candidate.accLoop);
618 if (!candidate.accIsZeroInit &&
620 candidate.accMemOut = candidate.accMemIn;
622 auto accBufferType = MemRefType::get(candidate.accType.getShape(),
623 candidate.accType.getElementType());
624 candidate.accMemOut =
625 memref::AllocaOp::create(rewriter, loc, accBufferType);
631 auto accRemat = vector::TransferReadOp::create(
632 rewriter, loc, candidate.accType, candidate.accMemOut, zeroIndices,
636 candidate.opsToDelete.push_back(candidate.accLoop);
645 auto loc = candidate.accLoop.getLoc();
650 auto makeIndexConst = [&](
int64_t v) {
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]);
661 rewriter, loc, c0, ubM, stepM, {},
664 b, loc, c0, ubN, stepN, {},
667 scf::YieldOp::create(
b, loc);
669 scf::YieldOp::create(
b, loc);
694 b.getI64IntegerAttr(accRegTileShape[0]),
695 b.getI64IntegerAttr(accRegTileShape[1])};
697 b.getI64IntegerAttr(1)};
700 if (candidate.accIsZeroInit) {
701 accInit = arith::ConstantOp::create(
702 b, loc,
b.getZeroAttr(candidate.accRegTileType));
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},
711 accInit = accInitRead;
716 candidate.slicedAccLoop = scf::ForOp::create(
717 b, loc, candidate.accLoop.getLowerBound(),
718 candidate.accLoop.getUpperBound(), candidate.accLoop.getStep(),
721 SmallVector<Value> newIterArgs =
722 buildKLoopBody(candidate, b, loc, ivM, ivN, ivK, iterArgs);
723 scf::YieldOp::create(b, loc, newIterArgs);
726 Value accSpliced =
splice(
b, loc, candidate.slicedAccLoop.getResults(),
727 candidate.accRegTileType);
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});
746 Value accSpliced =
splice(
b, loc, iterArgs, candidate.accRegTileType);
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);
758 lhsOffsets, candidate.lhsRegTileType);
760 rhsOffsets, candidate.rhsRegTileType);
764 auto lhsRead = vector::TransferReadOp::create(
765 b, candidate.lhsRead.getLoc(), candidate.lhsRegTileType, lhsView,
766 zeroIndices, std::nullopt);
767 auto rhsRead = vector::TransferReadOp::create(
768 b, candidate.rhsRead.getLoc(), candidate.rhsRegTileType, rhsView,
769 zeroIndices, std::nullopt);
770 auto contract = vector::ContractionOp::create(
771 b, candidate.contract.getLoc(), lhsRead, rhsRead, accSpliced,
772 candidate.contract.getIndexingMaps(),
773 candidate.contract.getIteratorTypes());
778 b.getDenseI64ArrayAttr(candidate.contractNativeShape));
806struct VectorContractMultiLevelUnroll
808 VectorContractMultiLevelUnroll(
809 MLIRContext *context,
810 const MatchTypesAndShapeFn &matchTypesAndShapeForTarget,
811 PatternBenefit benefit = 1)
812 : OpRewritePattern<vector::ContractionOp>(context, benefit),
813 matchTypesAndShapeForTarget(matchTypesAndShapeForTarget) {}
815 LogicalResult matchAndRewrite(vector::ContractionOp
contract,
816 PatternRewriter &rewriter)
const override {
822 MLUCandidate candidate;
834 for (
auto *op : candidate.opsToDelete)
841 MatchTypesAndShapeFn matchTypesAndShapeForTarget;
859 if (!((inputElemType.
isBF16() && accumulatorElemType.
isF32()) ||
863 int64_t origM = contractShape[0];
864 int64_t origN = contractShape[1];
866 int64_t regTileM, regTileN, regTileK;
867 int64_t nativeM, nativeN, nativeK, vnni;
870 nativeM = nativeN = 16;
871 nativeK = isInVnniLayout ? 16 : 16 * vnni;
875 regTileM = origM % (2 * nativeM) == 0 ? 2 * nativeM : nativeM;
877 origN % (2 * nativeN) == 0 || !isInVnniLayout ? 2 * nativeN : nativeN;
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);
896 patterns.
add<VectorContractMultiLevelUnroll>(patterns.
getContext(),
900 vector::populateVectorUnrollPatterns(patterns, nativeUnrollOptions);
static void copy(Location loc, Value dst, Value src, Value size, OpBuilder &builder)
Copies the given number of bytes from src to dst pointers.
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.
This class represents an argument of a Block.
Block * getOwner() const
Returns the block that owns this argument.
Block represents an ordered list of Operations.
AffineExpr getAffineDimExpr(unsigned position)
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...
RAII guard to reset the insertion point of the builder when destroyed.
This class helps build Operations.
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
void setInsertionPointAfter(Operation *op)
Sets the insertion point to the node after the specified operation, which will cause subsequent inser...
Operation is the basic unit of execution within MLIR.
AttrClass getAttrOfType(StringAttr name)
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.
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
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...
bool isInteger() const
Return true if this is an integer type (with the specified width).
unsigned getIntOrFloatBitWidth() const
Return the bit width of an integer or a float type, assert failure on other types.
This class provides an abstraction over the different types of ranges over Values.
type_range getTypes() const
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Type getType() const
Return the type of this value.
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
static DenseArrayAttrImpl get(MLIRContext *context, ArrayRef< int64_t > content)
void populateVectorContractMultiLevelUnrollForAMXPatterns(RewritePatternSet &patterns)
bool isInVnniLayout(Operation *op, llvm::ArrayRef< AffineMap > indexingMaps, std::optional< unsigned > blockingFactor=std::nullopt)
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)