Files
derek.holloway 7502bf64a4
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
Flatten subprojects into a single base monorepo
2026-09-02 16:34:47 -07:00

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