CodeQL / Analyze (actions) (push) Canceled after 0s
CodeQL / Analyze (python) (push) Canceled after 0s
pre-commit / pre-commit (push) Canceled after 0s
Multi-Arch CI / setup (push) Canceled after 0s
Publish build_manylinux_x86_64 images / publish_build_manylinux_x86_64 (push) Canceled after 0s
Setup (Multi-Arch) / setup (push) Canceled after 0s
Multi-Arch CI / Linux::${{ fromJSON(needs.setup.outputs.linux_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubi10 image / publish_no_rocm_image_ubi10 (push) Canceled after 0s
Multi-Arch CI / Windows::${{ fromJSON(needs.setup.outputs.windows_build_config || '{}').build_variant_label || 'skip' }} (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Multi-Arch CI / Manifest Diff (push) Canceled after 0s
TheRock Manifest Diff Report / Generate Manifest Diff Report (push) Canceled after 0s
Multi-Arch CI / CI Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_ocl_rt images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04_rocgdb images / publish_no_rocm_image_ubuntu24_04_rocgdb (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_ocl_rt (push) Canceled after 0s
Publish TheRock Docker image / build-and-push-image (push) Canceled after 0s
Unit Tests / Unit Tests :: ubuntu-24.04 (push) Canceled after 0s
Unit Tests / Unit Tests :: windows-2022 (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_media (push) Canceled after 0s
Unit Tests / Unit Tests Summary (push) Canceled after 0s
Publish no_rocm_image_ubuntu24_04 images / publish_no_rocm_image_ubuntu24_04_openmpi (push) Canceled after 0s
434 lines
15 KiB
C++
434 lines
15 KiB
C++
//===- 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 <cassert>
|
|
|
|
namespace mlir {
|
|
namespace x86 {
|
|
|
|
static FailureOr<SmallVector<mlir::utils::IteratorType>>
|
|
inferIteratorsFromOutMap(AffineMap map) {
|
|
if (!map.isProjectedPermutation())
|
|
return failure();
|
|
SmallVector<mlir::utils::IteratorType> iterators(
|
|
map.getNumDims(), mlir::utils::IteratorType::reduction);
|
|
for (auto expr : map.getResults())
|
|
if (auto dim = dyn_cast<AffineDimExpr>(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<AffineMap> indexingMaps,
|
|
std::optional<unsigned> blockingFactor) {
|
|
// Narrow down type operations - VNNI only applies to contractions.
|
|
FailureOr<linalg::ContractionDimensions> dims =
|
|
linalg::inferContractionDims(indexingMaps);
|
|
if (failed(dims))
|
|
return false;
|
|
|
|
auto matA = op->getOperand(0);
|
|
auto matB = op->getOperand(1);
|
|
auto typeA = dyn_cast<ShapedType>(matA.getType());
|
|
auto typeB = dyn_cast<ShapedType>(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<mlir::utils::IteratorType> iteratorTypes = *maybeIters;
|
|
AffineMap mapA = indexingMaps[0];
|
|
AffineMap mapB = indexingMaps[1];
|
|
|
|
auto vnniDimA = dyn_cast<AffineDimExpr>(mapA.getResult(rankA - 1));
|
|
auto vnniDimB = dyn_cast<AffineDimExpr>(mapB.getResult(rankB - 1));
|
|
if (!vnniDimA || !vnniDimB || vnniDimA != vnniDimB ||
|
|
iteratorTypes[vnniDimA.getPosition()] !=
|
|
mlir::utils::IteratorType::reduction)
|
|
return false;
|
|
auto redDimA = dyn_cast<AffineDimExpr>(mapA.getResult(rankA - 2));
|
|
auto redDimB = dyn_cast<AffineDimExpr>(mapB.getResult(rankB - 3));
|
|
if (!redDimA || !redDimB || redDimA != redDimB ||
|
|
iteratorTypes[redDimA.getPosition()] !=
|
|
mlir::utils::IteratorType::reduction)
|
|
return false;
|
|
auto parallelDimB = dyn_cast<AffineDimExpr>(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<int64_t> maskLo;
|
|
llvm::ArrayRef<int64_t> 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<vector::TransferReadOp, vector::LoadOp>(defOp))
|
|
return defOp;
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
// Case 2: BlockArgument (scf.for iter_arg)
|
|
if (auto barg = dyn_cast<BlockArgument>(v)) {
|
|
auto *parentOp = barg.getOwner()->getParentOp();
|
|
|
|
if (auto forOp = dyn_cast<scf::ForOp>(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<vector::TransferWriteOp>(user) || isa<vector::StoreOp>(user))
|
|
return user;
|
|
|
|
if (isa<vector::ShuffleOp>(user))
|
|
return nullptr;
|
|
|
|
// --- SCF YIELD ---
|
|
if (auto yield = dyn_cast<scf::YieldOp>(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<scf::ForOp>(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<vector::TransferReadOp, vector::LoadOp>(opA) ||
|
|
!isa<vector::TransferReadOp, vector::LoadOp>(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<vector::ContractionOp, scf::ForOp>(use.getOwner());
|
|
});
|
|
|
|
rewriter.replaceUsesWithIf(
|
|
opB->getResult(0), newAccB.getResult(), [&](OpOperand &use) {
|
|
return isa<vector::ContractionOp, scf::ForOp>(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<vector::TransferWriteOp>(op))
|
|
return write.getVector();
|
|
if (auto store = dyn_cast<vector::StoreOp>(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<OpFoldResult> indexVals;
|
|
llvm::TypeSwitch<Operation *>(nonUnitOperand.getDefiningOp())
|
|
.Case<vector::TransferReadOp, vector::LoadOp>([&](auto readOp) {
|
|
srcBuff = readOp.getOperand(0);
|
|
indexVals = SmallVector<OpFoldResult>(readOp.getIndices().begin(),
|
|
readOp.getIndices().end());
|
|
})
|
|
.Case<vector::ShapeCastOp>([&](vector::ShapeCastOp op) {
|
|
srcBuff = op.getSource();
|
|
indexVals.clear();
|
|
});
|
|
|
|
Value srcBuffPairContOp;
|
|
SmallVector<OpFoldResult> indexValsPairContOp;
|
|
llvm::TypeSwitch<Operation *>(nonUnitOperandPairContOp.getDefiningOp())
|
|
.Case<vector::TransferReadOp, vector::LoadOp>([&](auto readOp) {
|
|
srcBuffPairContOp = readOp.getOperand(0);
|
|
indexValsPairContOp = SmallVector<OpFoldResult>(
|
|
readOp.getIndices().begin(), readOp.getIndices().end());
|
|
})
|
|
.Case<vector::ShapeCastOp>([&](vector::ShapeCastOp op) {
|
|
srcBuffPairContOp = op.getSource();
|
|
indexVals.clear();
|
|
});
|
|
|
|
if (!srcBuff || !srcBuffPairContOp)
|
|
return false;
|
|
|
|
auto shuffleLw = srcBuff.getDefiningOp<vector::ShuffleOp>();
|
|
auto shuffleHw = srcBuffPairContOp.getDefiningOp<vector::ShuffleOp>();
|
|
|
|
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
|