MLIR 24.0.0git
X86Utils.cpp
Go to the documentation of this file.
1//===- X86Utils.cpp - MLIR Utilities for X86Ops -------------------------===//
2//
3// Part of the MLIR 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
10
17#include "mlir/IR/Types.h"
18
19#include "llvm/ADT/TypeSwitch.h"
20#include "llvm/Support/Casting.h"
21
22#include "llvm/ADT/ArrayRef.h"
23#include <cassert>
24
25namespace mlir {
26namespace x86 {
27
28static FailureOr<SmallVector<mlir::utils::IteratorType>>
30 if (!map.isProjectedPermutation())
31 return failure();
33 map.getNumDims(), mlir::utils::IteratorType::reduction);
34 for (auto expr : map.getResults())
35 if (auto dim = dyn_cast<AffineDimExpr>(expr))
36 iterators[dim.getPosition()] = mlir::utils::IteratorType::parallel;
37 return iterators;
38}
39
40// Returns true if the operation is in VNNI layout.
41// Optionally, the check can be constrained to a specific VNNI blocking factor.
43 std::optional<unsigned> blockingFactor) {
44 // Narrow down type operations - VNNI only applies to contractions.
45 FailureOr<linalg::ContractionDimensions> dims =
46 linalg::inferContractionDims(indexingMaps);
47 if (failed(dims))
48 return false;
49
50 auto matA = op->getOperand(0);
51 auto matB = op->getOperand(1);
52 auto typeA = dyn_cast<ShapedType>(matA.getType());
53 auto typeB = dyn_cast<ShapedType>(matB.getType());
54 unsigned rankA = typeA.getRank();
55 unsigned rankB = typeB.getRank();
56 // VNNI format requires at least 1 parallel and 2 reduction dimensions.
57 if (rankA < 3 || rankB < 3)
58 return false;
59
60 // At least two reduction dimensions are expected:
61 // one for the VNNI factor and one for the K dimension
62 if (dims->k.size() < 2)
63 return false;
64
65 // Validate affine maps - VNNI computation should be defined by the two
66 // innermost reduction iterators.
67 // The input matrix dimensions layout must match the following:
68 // - matrix A - [...][K/vnniFactor][vnniFactor]
69 // - matrix B - [...][K/vnniFactor][N][vnniFactor]
70 auto maybeIters = inferIteratorsFromOutMap(indexingMaps[2] /* outs */);
71 if (failed(maybeIters))
72 return false;
73 SmallVector<mlir::utils::IteratorType> iteratorTypes = *maybeIters;
74 AffineMap mapA = indexingMaps[0];
75 AffineMap mapB = indexingMaps[1];
76
77 auto vnniDimA = dyn_cast<AffineDimExpr>(mapA.getResult(rankA - 1));
78 auto vnniDimB = dyn_cast<AffineDimExpr>(mapB.getResult(rankB - 1));
79 if (!vnniDimA || !vnniDimB || vnniDimA != vnniDimB ||
80 iteratorTypes[vnniDimA.getPosition()] !=
81 mlir::utils::IteratorType::reduction)
82 return false;
83 auto redDimA = dyn_cast<AffineDimExpr>(mapA.getResult(rankA - 2));
84 auto redDimB = dyn_cast<AffineDimExpr>(mapB.getResult(rankB - 3));
85 if (!redDimA || !redDimB || redDimA != redDimB ||
86 iteratorTypes[redDimA.getPosition()] !=
87 mlir::utils::IteratorType::reduction)
88 return false;
89 auto parallelDimB = dyn_cast<AffineDimExpr>(mapB.getResult(rankB - 2));
90 if (!parallelDimB || iteratorTypes[parallelDimB.getPosition()] !=
91 mlir::utils::IteratorType::parallel)
92 return false;
93
94 // VNNI factor must be:
95 // - the innermost inputs' dimension
96 // - statically known
97 // - multiple of 2 or equal to the specified factor
98 auto vnniDimSize = typeB.getShape().back();
99 if (vnniDimSize == ShapedType::kDynamic || vnniDimSize == 0 ||
100 vnniDimSize % 2 != 0)
101 return false;
102 if (typeA.getShape().back() != vnniDimSize)
103 return false;
104 if (blockingFactor && vnniDimSize != *blockingFactor)
105 return false;
106
107 // The split reduction dimension size should also match.
108 if (typeA.getShape().end()[-2] != typeB.getShape().end()[-3])
109 return false;
110
111 return true;
112}
113
118
119inline ShuffleMasks getShuffleMasks(int64_t nonUnitDimAcc, bool isInt8Avx2) {
120 // We only support these two layouts for now.
121 assert((nonUnitDimAcc == 8 || nonUnitDimAcc == 16) &&
122 "Unsupported nonUnitDimAcc value");
123
124 // Do interleaving between two <8xf32> targeting AVX2.
125 static constexpr int64_t maskLo8[] = {0, 8, 1, 9, 2, 10, 3, 11};
126 static constexpr int64_t maskHi8[] = {4, 12, 5, 13, 6, 14, 7, 15};
127
128 // Do interleaving between two <8xi32> targeting AVX2.
129 static constexpr int64_t maskLo8_avx2_int8[] = {0, 1, 2, 3, 8, 9, 10, 11};
130 static constexpr int64_t maskHi8_avx2_int8[] = {4, 5, 6, 7, 12, 13, 14, 15};
131
132 // Shuffle two <16xf32/i32> as below targeting AVX512.
133 static constexpr int64_t maskLo16[] = {0, 1, 2, 3, 16, 17, 18, 19,
134 4, 5, 6, 7, 20, 21, 22, 23};
135 static constexpr int64_t maskHi16[] = {8, 9, 10, 11, 24, 25, 26, 27,
136 12, 13, 14, 15, 28, 29, 30, 31};
137
138 if (nonUnitDimAcc == 16)
139 return {maskLo16, maskHi16};
140
141 if (isInt8Avx2)
142 return {maskLo8_avx2_int8, maskHi8_avx2_int8};
143
144 return {maskLo8, maskHi8};
145}
146
147// Recursively follows single-use values through scf.yield operations
148// and returns the first non-yield user result in the contraction chain.
150 if (!v || v.getNumUses() != 1)
151 return nullptr;
152
153 OpOperand &use = *v.use_begin();
154 Operation *user = use.getOwner();
155
156 if (!isa<scf::YieldOp>(user))
157 return v;
158
159 auto yield = cast<scf::YieldOp>(user);
160 Operation *parent = yield->getParentOp();
161 unsigned idx = use.getOperandNumber();
162
163 return contractionUsersAfterYield(parent->getResult(idx));
164}
165
166// This function walks backward from a value to locate its originating
167// vector read-like operation (`vector.transfer_read` or `vector.load`).
168// It follows simple forwarding through unary ops and across `scf.for`
169// loop iter-arguments, while stopping if layout-transforming ops such
170// as `shape_cast` or `shuffle` are encountered. The traversal returns
171// the read-like defining operation or `nullptr` if no valid source
172// is found.
174 while (true) {
175 // Case 1: Value defined by an operation
176 if (Operation *defOp = v.getDefiningOp()) {
177 if (isa<vector::TransferReadOp, vector::LoadOp, arith::ConstantOp>(
178 defOp)) {
179
180 if (auto constOp = dyn_cast<arith::ConstantOp>(defOp)) {
181 if (auto denseAttr =
182 dyn_cast<DenseElementsAttr>(constOp.getValue())) {
183 if (!denseAttr.isSplat())
184 return nullptr;
185
186 Attribute splat = denseAttr.getSplatValue<Attribute>();
187
188 if (auto floatAttr = dyn_cast<FloatAttr>(splat))
189 return floatAttr.getValue().isZero() ? defOp : nullptr;
190
191 if (auto intAttr = dyn_cast<IntegerAttr>(splat))
192 return intAttr.getValue().isZero() ? defOp : nullptr;
193 }
194
195 return nullptr;
196 }
197 return defOp;
198 }
199
200 return nullptr;
201 }
202
203 // Case 2: BlockArgument (scf.for iter_arg)
204 if (auto barg = dyn_cast<BlockArgument>(v)) {
205 auto *parentOp = barg.getOwner()->getParentOp();
206
207 if (auto forOp = dyn_cast<scf::ForOp>(parentOp)) {
208 unsigned argNum = barg.getArgNumber();
209
210 // arg0 = induction variable (not an iter_arg)
211 if (argNum == 0)
212 return nullptr;
213
214 unsigned iterIdx = argNum - 1;
215 v = forOp.getInitArgs()[iterIdx];
216 continue;
217 }
218
219 return nullptr;
220 }
221
222 return nullptr;
223 }
224}
225
226// This function recursively traces a value through its uses to find
227// a downstream vector write-like operation (`vector.transfer_write`
228// or `vector.store`). It transparently follows values across `scf.for`
229// and `scf.yield` boundaries while stopping if layout-altering ops
230// like `shuffle` are encountered. The traversal returns
231// the matching write-like user. Returns `nullptr` if none is found or
232// the value has multiple users.
234
235 if (v.getNumUses() > 1)
236 return nullptr;
237
238 for (OpOperand &use : v.getUses()) {
239 Operation *user = use.getOwner();
240
241 // --- TERMINAL OPS ---
242 if (isa<vector::TransferWriteOp>(user) || isa<vector::StoreOp>(user))
243 return user;
244
245 if (isa<vector::ShuffleOp>(user))
246 return nullptr;
247
248 // --- SCF YIELD ---
249 if (auto yield = dyn_cast<scf::YieldOp>(user)) {
250 Operation *parent = yield->getParentOp();
251 unsigned idx = use.getOperandNumber();
252 if (auto *res =
254 return res;
255 continue;
256 }
257
258 // --- SCF FOR ---
259 if (auto forOp = dyn_cast<scf::ForOp>(user)) {
260 unsigned idx = use.getOperandNumber();
261 if (auto *res = traceToVectorWriteLikeUserOperation(forOp.getResult(idx)))
262 return res;
263 continue;
264 }
265
266 // --- GENERIC CASE ---
267 for (Value res : user->getResults()) {
268 if (auto *found = traceToVectorWriteLikeUserOperation(res))
269 return found;
270 }
271 }
272
273 return nullptr;
274}
275
276// This function packs the accumulator of two flat BF16 vector.contract
277// operations into VNNI packed and are then replaced in their respective
278// contraction ops, enabling post-read layout or packing transformations.
279// TODO: replace all use with the packed value along with contration
280// and for op.
282 Operation *opB,
283 vector::ContractionOp contractA,
284 vector::ContractionOp contractB,
285 int64_t nonUnitDimAcc, VectorType accTy) {
286
287 if (!isa<vector::TransferReadOp, vector::LoadOp>(opA) ||
288 !isa<vector::TransferReadOp, vector::LoadOp>(opB)) {
289 return failure();
290 }
291
292 Operation *insertAfter = opA->isBeforeInBlock(opB) ? opB : opA;
293
294 rewriter.setInsertionPointAfter(insertAfter);
295 Location loc = insertAfter->getLoc();
296
297 auto elemTy = accTy.getElementType();
298 auto flatTy = VectorType::get(nonUnitDimAcc, elemTy);
299
300 auto castA =
301 vector::ShapeCastOp::create(rewriter, loc, flatTy, opA->getResult(0));
302 auto castB =
303 vector::ShapeCastOp::create(rewriter, loc, flatTy, opB->getResult(0));
304
305 auto masks = getShuffleMasks(
306 nonUnitDimAcc, (elemTy.isSignlessInteger(32) && nonUnitDimAcc == 8));
307
308 auto shuffleLo = vector::ShuffleOp::create(rewriter, loc, flatTy, castA,
309 castB, masks.maskLo);
310 auto shuffleHi = vector::ShuffleOp::create(rewriter, loc, flatTy, castA,
311 castB, masks.maskHi);
312
313 auto newAccA = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffleLo);
314 auto newAccB = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffleHi);
315
316 rewriter.replaceUsesWithIf(
317 opA->getResult(0), newAccA.getResult(), [&](OpOperand &use) {
318 return isa<vector::ContractionOp, scf::ForOp>(use.getOwner());
319 });
320
321 rewriter.replaceUsesWithIf(
322 opB->getResult(0), newAccB.getResult(), [&](OpOperand &use) {
323 return isa<vector::ContractionOp, scf::ForOp>(use.getOwner());
324 });
325
326 return success();
327}
328
329// This function shuffles the vectors written by vector.contract operation
330// as a flat layout structure before they are stored.
332 Value contractARes, Value contractBRes,
333 int64_t nonUnitDimAcc,
334 VectorType accTy) {
335
336 Value vecA = contractionUsersAfterYield(contractARes);
337 Value vecB = contractionUsersAfterYield(contractBRes);
338
339 if (!vecA || !vecB)
340 return failure();
341
342 Operation *resultWriteOpA = *vecA.getUsers().begin();
343 Operation *resultWriteOpB = *vecB.getUsers().begin();
344
345 // Decide insertion point and location
346 Operation *insertBefore = resultWriteOpA->isBeforeInBlock(resultWriteOpB)
347 ? resultWriteOpA
348 : resultWriteOpB;
349
350 rewriter.setInsertionPoint(insertBefore);
351 Location loc = insertBefore->getLoc();
352
353 auto elemTy = accTy.getElementType();
354 auto flatTy = VectorType::get(nonUnitDimAcc, elemTy);
355
356 // Flatten vectors
357 auto castA = vector::ShapeCastOp::create(rewriter, loc, flatTy, vecA);
358 auto castB = vector::ShapeCastOp::create(rewriter, loc, flatTy, vecB);
359
360 // TODO: derive shuffle masks instead of hard-coding
361 auto masks = getShuffleMasks(
362 nonUnitDimAcc, (elemTy.isSignlessInteger(32) && nonUnitDimAcc == 8));
363
364 auto shuffledLo = vector::ShuffleOp::create(rewriter, loc, flatTy, castA,
365 castB, masks.maskLo);
366 auto shuffledHi = vector::ShuffleOp::create(rewriter, loc, flatTy, castA,
367 castB, masks.maskHi);
368
369 // Cast back to accumulator type
370 auto newVecA = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffledLo);
371 auto newVecB = vector::ShapeCastOp::create(rewriter, loc, accTy, shuffledHi);
372
373 // Update write operands in place via the rewriter to notify it of changes.
374 resultWriteOpA->replaceUsesOfWith(vecA, newVecA);
375 resultWriteOpB->replaceUsesOfWith(vecB, newVecB);
376
377 return success();
378}
379
380// Return true if vector.contract operations matches on below conditions:
381// (1) - the unitDim operand Lhs or Rhs should be same,
382// (2) - the defining source memref should be same for nonUnitDim
383// operation,
384// (3) - the nonUnit dim offset difference between the
385// vector.contracts should be 8 or 16.
386bool validatePairVectorContract(vector::ContractionOp contractOp,
387 vector::ContractionOp pairContOp,
388 bool rhsHasMultipleNonUnitDims,
389 int64_t nonUnitDimValue) {
390 if (contractOp == pairContOp)
391 return false;
392
393 if (rhsHasMultipleNonUnitDims &&
394 !(contractOp.getLhs() == pairContOp.getLhs()))
395 return false;
396
397 if (!rhsHasMultipleNonUnitDims &&
398 !(contractOp.getRhs() == pairContOp.getRhs()))
399 return false;
400
401 auto nonUnitOperand =
402 rhsHasMultipleNonUnitDims ? contractOp.getRhs() : contractOp.getLhs();
403 auto nonUnitOperandPairContOp =
404 rhsHasMultipleNonUnitDims ? pairContOp.getRhs() : pairContOp.getLhs();
405
406 Value srcBuff;
408 llvm::TypeSwitch<Operation *>(nonUnitOperand.getDefiningOp())
409 .Case<vector::TransferReadOp, vector::LoadOp>([&](auto readOp) {
410 srcBuff = readOp.getOperand(0);
411 indexVals = SmallVector<OpFoldResult>(readOp.getIndices().begin(),
412 readOp.getIndices().end());
413 })
414 .Case<vector::ShapeCastOp>([&](vector::ShapeCastOp op) {
415 srcBuff = op.getSource();
416 indexVals.clear();
417 });
418
419 Value srcBuffPairContOp;
420 SmallVector<OpFoldResult> indexValsPairContOp;
421 llvm::TypeSwitch<Operation *>(nonUnitOperandPairContOp.getDefiningOp())
422 .Case<vector::TransferReadOp, vector::LoadOp>([&](auto readOp) {
423 srcBuffPairContOp = readOp.getOperand(0);
424 indexValsPairContOp = SmallVector<OpFoldResult>(
425 readOp.getIndices().begin(), readOp.getIndices().end());
426 })
427 .Case<vector::ShapeCastOp>([&](vector::ShapeCastOp op) {
428 srcBuffPairContOp = op.getSource();
429 indexVals.clear();
430 });
431
432 if (!srcBuff || !srcBuffPairContOp)
433 return false;
434
435 auto shuffleLw = srcBuff.getDefiningOp<vector::ShuffleOp>();
436 auto shuffleHw = srcBuffPairContOp.getDefiningOp<vector::ShuffleOp>();
437
438 if (shuffleLw && shuffleHw)
439 return shuffleLw.getV1() == shuffleHw.getV1() &&
440 shuffleLw.getV2() == shuffleHw.getV2();
441
442 if (srcBuff != srcBuffPairContOp)
443 return false;
444
445 bool oneConstantOffset = false;
446 for (size_t i = 0; i < indexVals.size(); i++) {
447
448 if (indexVals[i] == indexValsPairContOp[i])
449 continue;
450
451 auto v0 = getConstantIntValue(indexVals[i]);
452 auto v1 = getConstantIntValue(indexValsPairContOp[i]);
453
454 if (!v0 || !v1)
455 return false;
456
457 if ((*v1 - *v0) != nonUnitDimValue)
458 return false;
459
460 oneConstantOffset = true;
461 }
462
463 return oneConstantOffset;
464}
465
466} // namespace x86
467} // namespace mlir
return success()
A multi-dimensional affine map Affine map's are immutable like Type's, and they are uniqued.
Definition AffineMap.h:46
bool isProjectedPermutation(bool allowZeroInResults=false) const
Returns true if the AffineMap represents a subset (i.e.
unsigned getNumDims() const
ArrayRef< AffineExpr > getResults() const
AffineExpr getResult(unsigned idx) const
Attributes are known-constant values of operations.
Definition Attributes.h:25
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
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
This class represents an operand of an operation.
Definition Value.h:254
unsigned getOperandNumber() const
Return which operand this is in the OpOperand list of the Operation.
Definition Value.cpp:226
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
void replaceUsesOfWith(Value from, Value to)
Replace any uses of 'from' with 'to' within this operation.
Value getOperand(unsigned idx)
Definition Operation.h:375
bool isBeforeInBlock(Operation *other)
Given an operation 'other' that is within the same parent block, return whether the current operation...
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
Location getLoc()
The source location the operation was defined or derived from.
Definition Operation.h:240
Operation * getParentOp()
Returns the closest surrounding operation that contains this operation or nullptr if this is a top-le...
Definition Operation.h:251
result_range getResults()
Definition Operation.h:440
A special type of RewriterBase that coordinates the application of a rewrite pattern on the current I...
virtual void replaceUsesWithIf(Value from, Value to, function_ref< bool(OpOperand &)> functor, bool *allUsesReplaced=nullptr)
Find uses of from and replace them with to if the functor returns true.
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
use_range getUses() const
Returns a range of all uses, which is useful for iterating over all uses.
Definition Value.h:188
unsigned getNumUses() const
This method computes the number of uses of this Value.
Definition Value.cpp:52
user_range getUsers() const
Definition Value.h:218
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
use_iterator use_begin() const
Definition Value.h:184
Operation * getOwner() const
Return the owner of this operand.
Definition UseDefLists.h:38
FailureOr< ContractionDimensions > inferContractionDims(LinalgOp linalgOp)
Find at least 2 parallel (m and n) and 1 reduction (k) dimension candidates that form a matmul subcom...
LogicalResult shuffleBeforeWriteLikeOp(PatternRewriter &rewriter, Value contractARes, Value contractBRes, int64_t nonUnitDimAcc, VectorType accTy)
Definition X86Utils.cpp:331
Operation * traceToVectorWriteLikeUserOperation(Value v)
Definition X86Utils.cpp:233
static FailureOr< SmallVector< mlir::utils::IteratorType > > inferIteratorsFromOutMap(AffineMap map)
Definition X86Utils.cpp:29
bool isInVnniLayout(Operation *op, llvm::ArrayRef< AffineMap > indexingMaps, std::optional< unsigned > blockingFactor=std::nullopt)
Definition X86Utils.cpp:42
Value contractionUsersAfterYield(Value v)
Definition X86Utils.cpp:149
Operation * traceToVectorReadLikeParentOperation(Value v)
Definition X86Utils.cpp:173
ShuffleMasks getShuffleMasks(int64_t nonUnitDimAcc, bool isInt8Avx2)
Definition X86Utils.cpp:119
LogicalResult shuffleAfterReadLikeOp(PatternRewriter &rewriter, Operation *opA, Operation *opB, vector::ContractionOp contractA, vector::ContractionOp contractB, int64_t nonUnitDimAcc, VectorType accTy)
Definition X86Utils.cpp:281
bool validatePairVectorContract(vector::ContractionOp contractOp, vector::ContractionOp pairContOp, bool rhsHasMultipleNonUnitDims, int64_t nonUnitDimValue)
Definition X86Utils.cpp:386
Include the generated interface declarations.
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
llvm::ArrayRef< int64_t > maskHi
Definition X86Utils.cpp:116
llvm::ArrayRef< int64_t > maskLo
Definition X86Utils.cpp:115