//===- X86Utils.cpp - MLIR Utilities for X86Ops -------------------------===// // // Part of the MLIR Project, under the Apache License v2.0 with LLVM Exceptions. // See https://llvm.org/LICENSE.txt for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // //===----------------------------------------------------------------------===// #include "mlir/Dialect/X86/Utils/X86Utils.h" #include "mlir/Dialect/Linalg/IR/Linalg.h" #include "mlir/Dialect/Linalg/IR/LinalgInterfaces.h" #include "mlir/Dialect/SCF/IR/SCF.h" #include "mlir/Dialect/Vector/IR/VectorOps.h" #include "mlir/IR/BuiltinTypes.h" #include "mlir/IR/TypeUtilities.h" #include "mlir/IR/Types.h" #include "llvm/ADT/TypeSwitch.h" #include "llvm/Support/Casting.h" #include "llvm/ADT/ArrayRef.h" #include namespace mlir { namespace x86 { static FailureOr> inferIteratorsFromOutMap(AffineMap map) { if (!map.isProjectedPermutation()) return failure(); SmallVector iterators( map.getNumDims(), mlir::utils::IteratorType::reduction); for (auto expr : map.getResults()) if (auto dim = dyn_cast(expr)) iterators[dim.getPosition()] = mlir::utils::IteratorType::parallel; return iterators; } // Returns true if the operation is in VNNI layout. // Optionally, the check can be constrained to a specific VNNI blocking factor. bool isInVnniLayout(Operation *op, ArrayRef indexingMaps, std::optional blockingFactor) { // Narrow down type operations - VNNI only applies to contractions. FailureOr dims = linalg::inferContractionDims(indexingMaps); if (failed(dims)) return false; auto matA = op->getOperand(0); auto matB = op->getOperand(1); auto typeA = dyn_cast(matA.getType()); auto typeB = dyn_cast(matB.getType()); unsigned rankA = typeA.getRank(); unsigned rankB = typeB.getRank(); // VNNI format requires at least 1 parallel and 2 reduction dimensions. if (rankA < 3 || rankB < 3) return false; // At least two reduction dimensions are expected: // one for the VNNI factor and one for the K dimension if (dims->k.size() < 2) return false; // Validate affine maps - VNNI computation should be defined by the two // innermost reduction iterators. // The input matrix dimensions layout must match the following: // - matrix A - [...][K/vnniFactor][vnniFactor] // - matrix B - [...][K/vnniFactor][N][vnniFactor] auto maybeIters = inferIteratorsFromOutMap(indexingMaps[2] /* outs */); if (failed(maybeIters)) return false; SmallVector iteratorTypes = *maybeIters; AffineMap mapA = indexingMaps[0]; AffineMap mapB = indexingMaps[1]; auto vnniDimA = dyn_cast(mapA.getResult(rankA - 1)); auto vnniDimB = dyn_cast(mapB.getResult(rankB - 1)); if (!vnniDimA || !vnniDimB || vnniDimA != vnniDimB || iteratorTypes[vnniDimA.getPosition()] != mlir::utils::IteratorType::reduction) return false; auto redDimA = dyn_cast(mapA.getResult(rankA - 2)); auto redDimB = dyn_cast(mapB.getResult(rankB - 3)); if (!redDimA || !redDimB || redDimA != redDimB || iteratorTypes[redDimA.getPosition()] != mlir::utils::IteratorType::reduction) return false; auto parallelDimB = dyn_cast(mapB.getResult(rankB - 2)); if (!parallelDimB || iteratorTypes[parallelDimB.getPosition()] != mlir::utils::IteratorType::parallel) return false; // VNNI factor must be: // - the innermost inputs' dimension // - statically known // - multiple of 2 or equal to the specified factor auto vnniDimSize = typeB.getShape().back(); if (vnniDimSize == ShapedType::kDynamic || vnniDimSize == 0 || vnniDimSize % 2 != 0) return false; if (typeA.getShape().back() != vnniDimSize) return false; if (blockingFactor && vnniDimSize != *blockingFactor) return false; // The split reduction dimension size should also match. if (typeA.getShape().end()[-2] != typeB.getShape().end()[-3]) return false; return true; } struct ShuffleMasks { llvm::ArrayRef maskLo; llvm::ArrayRef maskHi; }; inline ShuffleMasks getShuffleMasks(int64_t nonUnitDimAcc, bool isInt8Avx2) { // We only support these two layouts for now. assert((nonUnitDimAcc == 8 || nonUnitDimAcc == 16) && "Unsupported nonUnitDimAcc value"); // Do interleaving between two <8xf32> targeting AVX2. static constexpr int64_t maskLo8[] = {0, 8, 1, 9, 2, 10, 3, 11}; static constexpr int64_t maskHi8[] = {4, 12, 5, 13, 6, 14, 7, 15}; // Do interleaving between two <8xi32> targeting AVX2. static constexpr int64_t maskLo8_avx2_int8[] = {0, 1, 2, 3, 8, 9, 10, 11}; static constexpr int64_t maskHi8_avx2_int8[] = {4, 5, 6, 7, 12, 13, 14, 15}; // Shuffle two <16xf32/i32> as below targeting AVX512. static constexpr int64_t maskLo16[] = {0, 1, 2, 3, 16, 17, 18, 19, 4, 5, 6, 7, 20, 21, 22, 23}; static constexpr int64_t maskHi16[] = {8, 9, 10, 11, 24, 25, 26, 27, 12, 13, 14, 15, 28, 29, 30, 31}; if (nonUnitDimAcc == 16) return {maskLo16, maskHi16}; if (isInt8Avx2) return {maskLo8_avx2_int8, maskHi8_avx2_int8}; return {maskLo8, maskHi8}; } // This function walks backward from a value to locate its originating // vector read-like operation (`vector.transfer_read` or `vector.load`). // It follows simple forwarding through unary ops and across `scf.for` // loop iter-arguments, while stopping if layout-transforming ops such // as `shape_cast` or `shuffle` are encountered. The traversal returns // the read-like defining operation or `nullptr` if no valid source // is found. Operation *traceToVectorReadLikeParentOperation(Value v) { while (true) { // Case 1: Value defined by an operation if (Operation *defOp = v.getDefiningOp()) { if (isa(defOp)) return defOp; return nullptr; } // Case 2: BlockArgument (scf.for iter_arg) if (auto barg = dyn_cast(v)) { auto *parentOp = barg.getOwner()->getParentOp(); if (auto forOp = dyn_cast(parentOp)) { unsigned argNum = barg.getArgNumber(); // arg0 = induction variable (not an iter_arg) if (argNum == 0) return nullptr; unsigned iterIdx = argNum - 1; v = forOp.getInitArgs()[iterIdx]; continue; } return nullptr; } return nullptr; } } // This function recursively traces a value through its uses to find // a downstream vector write-like operation (`vector.transfer_write` // or `vector.store`). It transparently follows values across `scf.for` // and `scf.yield` boundaries while stopping if layout-altering ops // like `shuffle` are encountered. The traversal returns // the matching write-like user. Returns `nullptr` if none is found or // the value has multiple users. Operation *traceToVectorWriteLikeUserOperation(Value v) { if (v.getNumUses() > 1) return nullptr; for (OpOperand &use : v.getUses()) { Operation *user = use.getOwner(); // --- TERMINAL OPS --- if (isa(user) || isa(user)) return user; if (isa(user)) return nullptr; // --- SCF YIELD --- if (auto yield = dyn_cast(user)) { Operation *parent = yield->getParentOp(); unsigned idx = use.getOperandNumber(); if (auto *res = traceToVectorWriteLikeUserOperation(parent->getResult(idx))) return res; continue; } // --- SCF FOR --- if (auto forOp = dyn_cast(user)) { unsigned idx = use.getOperandNumber(); if (auto *res = traceToVectorWriteLikeUserOperation(forOp.getResult(idx))) return res; continue; } // --- GENERIC CASE --- for (Value res : user->getResults()) { if (auto *found = traceToVectorWriteLikeUserOperation(res)) return found; } } return nullptr; } // This function packs the accumulator of two flat BF16 vector.contract // operations into VNNI packed and are then replaced in their respective // contraction ops, enabling post-read layout or packing transformations. // TODO: replace all use with the packed value along with contration // and for op. LogicalResult shuffleAfterReadLikeOp(PatternRewriter &rewriter, Operation *opA, Operation *opB, vector::ContractionOp contractA, vector::ContractionOp contractB, int64_t nonUnitDimAcc, VectorType accTy) { if (!isa(opA) || !isa(opB)) { return failure(); } Operation *insertAfter = opA->isBeforeInBlock(opB) ? opB : opA; rewriter.setInsertionPointAfter(insertAfter); Location loc = insertAfter->getLoc(); auto elemTy = accTy.getElementType(); auto flatTy = VectorType::get(nonUnitDimAcc, elemTy); auto castA = vector::ShapeCastOp::create(rewriter, loc, flatTy, opA->getResult(0)); auto castB = vector::ShapeCastOp::create(rewriter, loc, flatTy, opB->getResult(0)); auto masks = getShuffleMasks( nonUnitDimAcc, (elemTy.isSignlessInteger(32) && nonUnitDimAcc == 8)); auto shuffleLo = vector::ShuffleOp::create(rewriter, loc, flatTy, castA, castB, masks.maskLo); auto shuffleHi = vector::ShuffleOp::create(rewriter, loc, flatTy, castA, castB, masks.maskHi); auto newAccA = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffleLo); auto newAccB = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffleHi); rewriter.replaceUsesWithIf( opA->getResult(0), newAccA.getResult(), [&](OpOperand &use) { return isa(use.getOwner()); }); rewriter.replaceUsesWithIf( opB->getResult(0), newAccB.getResult(), [&](OpOperand &use) { return isa(use.getOwner()); }); return success(); } // This function shuffles the vectors written by vector.contract operation // as a flat layout structure before they are stored. LogicalResult shuffleBeforeWriteLikeOp(PatternRewriter &rewriter, Operation *opA, Operation *opB, int64_t nonUnitDimAcc, VectorType accTy) { // Helper to extract vector operand from write-like ops auto getWrittenVector = [](Operation *op) -> Value { if (auto write = dyn_cast(op)) return write.getVector(); if (auto store = dyn_cast(op)) return store.getValueToStore(); return nullptr; }; Value vecA = getWrittenVector(opA); Value vecB = getWrittenVector(opB); if (!vecA || !vecB) return failure(); // Decide insertion point and location Operation *insertBefore = opA->isBeforeInBlock(opB) ? opA : opB; rewriter.setInsertionPoint(insertBefore); Location loc = insertBefore->getLoc(); auto elemTy = accTy.getElementType(); auto flatTy = VectorType::get(nonUnitDimAcc, elemTy); // Flatten vectors auto castA = vector::ShapeCastOp::create(rewriter, loc, flatTy, vecA); auto castB = vector::ShapeCastOp::create(rewriter, loc, flatTy, vecB); // TODO: derive shuffle masks instead of hard-coding auto masks = getShuffleMasks( nonUnitDimAcc, (elemTy.isSignlessInteger(32) && nonUnitDimAcc == 8)); auto shuffledLo = vector::ShuffleOp::create(rewriter, loc, flatTy, castA, castB, masks.maskLo); auto shuffledHi = vector::ShuffleOp::create(rewriter, loc, flatTy, castA, castB, masks.maskHi); // Cast back to accumulator type auto newVecA = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffledLo); auto newVecB = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffledHi); // Update write operands in place via the rewriter to notify it of changes. rewriter.modifyOpInPlace(opA, [&]() { opA->setOperand(0, newVecA.getResult()); }); rewriter.modifyOpInPlace(opB, [&]() { opB->setOperand(0, newVecB.getResult()); }); return success(); } // Return true if vector.contract operations matches on below conditions: // (1) - the unitDim operand Lhs or Rhs should be same, // (2) - the defining source memref should be same for nonUnitDim // operation, // (3) - the nonUnit dim offset difference between the // vector.contracts should be 8 or 16. bool validatePairVectorContract(vector::ContractionOp contractOp, vector::ContractionOp pairContOp, bool rhsHasMultipleNonUnitDims, int64_t nonUnitDimValue) { if (contractOp == pairContOp) return false; if (rhsHasMultipleNonUnitDims && !(contractOp.getLhs() == pairContOp.getLhs())) return false; if (!rhsHasMultipleNonUnitDims && !(contractOp.getRhs() == pairContOp.getRhs())) return false; auto nonUnitOperand = rhsHasMultipleNonUnitDims ? contractOp.getRhs() : contractOp.getLhs(); auto nonUnitOperandPairContOp = rhsHasMultipleNonUnitDims ? pairContOp.getRhs() : pairContOp.getLhs(); Value srcBuff; SmallVector indexVals; llvm::TypeSwitch(nonUnitOperand.getDefiningOp()) .Case([&](auto readOp) { srcBuff = readOp.getOperand(0); indexVals = SmallVector(readOp.getIndices().begin(), readOp.getIndices().end()); }) .Case([&](vector::ShapeCastOp op) { srcBuff = op.getSource(); indexVals.clear(); }); Value srcBuffPairContOp; SmallVector indexValsPairContOp; llvm::TypeSwitch(nonUnitOperandPairContOp.getDefiningOp()) .Case([&](auto readOp) { srcBuffPairContOp = readOp.getOperand(0); indexValsPairContOp = SmallVector( readOp.getIndices().begin(), readOp.getIndices().end()); }) .Case([&](vector::ShapeCastOp op) { srcBuffPairContOp = op.getSource(); indexVals.clear(); }); if (!srcBuff || !srcBuffPairContOp) return false; auto shuffleLw = srcBuff.getDefiningOp(); auto shuffleHw = srcBuffPairContOp.getDefiningOp(); if (shuffleLw && shuffleHw) return shuffleLw.getV1() == shuffleHw.getV1() && shuffleLw.getV2() == shuffleHw.getV2(); if (srcBuff != srcBuffPairContOp) return false; bool oneConstantOffset = false; for (size_t i = 0; i < indexVals.size(); i++) { if (indexVals[i] == indexValsPairContOp[i]) continue; auto v0 = getConstantIntValue(indexVals[i]); auto v1 = getConstantIntValue(indexValsPairContOp[i]); if (!v0 || !v1) return false; if ((*v1 - *v0) != nonUnitDimValue) return false; oneConstantOffset = true; } return oneConstantOffset; } } // namespace x86 } // namespace mlir