MLIR 23.0.0git
VectorEmulateNarrowType.cpp
Go to the documentation of this file.
1//===- VectorEmulateNarrowType.cpp - Narrow type emulation ----------------===//
2//
3// Part of the LLVM 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//
9// This file implements target-independent rewrites and utilities to emulate
10// narrow types that are not supported by the target hardware, e.g. i4
11// ("emulated type"), using wider types, e.g. i8 ("container type").
12//
13/// Currently, only power-of-two integer types are supported. These are
14/// converted to wider integers that are either 8 bits wide or wider.
15///
16/// TODO: Support for non-powers-of-two.
17//===----------------------------------------------------------------------===//
18
32#include "mlir/IR/Value.h"
34#include "llvm/ADT/SmallVector.h"
35#include "llvm/Support/DebugLog.h"
36#include "llvm/Support/MathExtras.h"
37#include "llvm/Support/raw_ostream.h"
38#include <cstdint>
39#include <optional>
40
42
43using namespace mlir;
44
45#define DEBUG_TYPE "vector-narrow-type-emulation"
46
49
50//===----------------------------------------------------------------------===//
51// Utils
52//===----------------------------------------------------------------------===//
53
54/// Returns a compressed mask for the emulated vector. For example, when
55/// emulating an eight-element `i8` vector with `i32` (i.e. when the source
56/// elements span two dest elements), this method compresses `vector<8xi1>`
57/// into `vector<2xi1>`.
58///
59/// The compressed/output mask value is set iff any mask in the corresponding
60/// `numSrcElemsPerDest` range of uncompressed/input masks is set. E.g., if
61/// `numSrcElemsPerDest` equals to 2, and `numFrontPadElems` equals to 1, the
62/// following mask:
63///
64/// %mask = [1, 1, 0, 0, 0, 0]
65///
66/// will first be padded in the front with `numFrontPadElems` zeros, and zeros
67/// will be added in the back to make the number of elements a multiple of
68/// `numSrcElemsPerDest` (for easier computation). The resulting mask will be:
69///
70/// %mask = [0, 1, 1, 0, 0, 0, 0, 0]
71///
72/// then it will return the following new compressed mask:
73///
74/// %mask = [1, 1, 0, 0]
75///
76/// NOTE: `numFrontPadElems` is assumed to be strictly smaller than
77/// `numSrcElemsPerDest`.
78static FailureOr<Operation *> getCompressedMaskOp(OpBuilder &rewriter,
79 Location loc, Value mask,
80 int numSrcElems,
81 int numSrcElemsPerDest,
82 int numFrontPadElems = 0) {
83
84 assert(numFrontPadElems < numSrcElemsPerDest &&
85 "numFrontPadElems must be less than numSrcElemsPerDest");
86
87 auto numDestElems =
88 (numFrontPadElems + numSrcElems + numSrcElemsPerDest - 1) /
89 numSrcElemsPerDest;
90
91 Operation *maskOp = mask.getDefiningOp();
92 // Chain of `vector.extract` ops that lead to the op that created the mask.
93 // TODO: add support to `vector.broadcast`.
95
96 // Trace the mask back to its creation op, looking through `vector.extract`
97 // ops. Any other defining op is unsupported.
98 while (maskOp &&
99 !isa<arith::ConstantOp, vector::CreateMaskOp, vector::ConstantMaskOp>(
100 maskOp)) {
101 auto extractOp = dyn_cast<vector::ExtractOp>(maskOp);
102 if (!extractOp)
103 return failure();
104 maskOp = extractOp.getSource().getDefiningOp();
105 extractOps.push_back(extractOp);
106 }
107
108 // `maskOp` is null when the mask is a block argument, which has no defining
109 // op.
110 if (!isa_and_present<arith::ConstantOp, vector::CreateMaskOp,
111 vector::ConstantMaskOp>(maskOp))
112 return failure();
113
114 // Computing the "compressed" mask. All the emulation logic (i.e. computing
115 // new mask index) only happens on the last dimension of the vectors.
116 SmallVector<int64_t> maskShape(
117 cast<VectorType>(maskOp->getResultTypes()[0]).getShape());
118 maskShape.back() = numDestElems;
119 auto newMaskType = VectorType::get(maskShape, rewriter.getI1Type());
120 std::optional<Operation *> newMask =
122 .Case(
123 [&](vector::CreateMaskOp createMaskOp)
124 -> std::optional<Operation *> {
125 OperandRange maskOperands = createMaskOp.getOperands();
126 // The `vector.create_mask` op creates a mask arrangement
127 // without any zeros at the front. Also, because
128 // `numFrontPadElems` is strictly smaller than
129 // `numSrcElemsPerDest`, the compressed mask generated by
130 // padding the original mask by `numFrontPadElems` will not
131 // have any zeros at the front as well.
132 AffineExpr s0;
133 bindSymbols(rewriter.getContext(), s0);
134 s0 = (s0 + numFrontPadElems).ceilDiv(numSrcElemsPerDest);
135 OpFoldResult origIndex = getAsOpFoldResult(maskOperands.back());
137 rewriter, loc, s0, origIndex);
138 SmallVector<Value> newMaskOperands(maskOperands.drop_back());
139 newMaskOperands.push_back(
140 getValueOrCreateConstantIndexOp(rewriter, loc, maskIndex));
141 return vector::CreateMaskOp::create(rewriter, loc, newMaskType,
142 newMaskOperands);
143 })
144 .Case([&](vector::ConstantMaskOp constantMaskOp)
145 -> std::optional<Operation *> {
146 // Take the shape of mask, compress its trailing dimension:
147 SmallVector<int64_t> maskDimSizes(constantMaskOp.getMaskDimSizes());
148 int64_t &maskIndex = maskDimSizes.back();
149 maskIndex = llvm::divideCeil(numFrontPadElems + maskIndex,
150 numSrcElemsPerDest);
151 return vector::ConstantMaskOp::create(rewriter, loc, newMaskType,
152 maskDimSizes);
153 })
154 .Case([&](arith::ConstantOp constantOp)
155 -> std::optional<Operation *> {
156 // TODO: Support multiple dimensions.
157 if (maskShape.size() != 1)
158 return std::nullopt;
159 // Rearrange the original mask values to cover the whole potential
160 // loading region. For example, in the case of using byte-size for
161 // emulation, given the following mask:
162 //
163 // %mask = [0, 1, 0, 1, 0, 0]
164 //
165 // With front offset of 1, the mask will be padded 0s in the front
166 // and back so that:
167 // 1. It is aligned with the effective loading bits
168 // 2. Its length is multiple of `numSrcElemPerDest` (and the total
169 // coverage size is mulitiple of bytes). The new mask will be like
170 // this before compressing:
171 //
172 // %new_mask = [0, 0, 1, 0, 1, 0, 0, 0]
173 auto originalMask =
174 cast<DenseIntElementsAttr>(constantOp.getValue());
175 SmallVector<bool> paddedMaskValues(numFrontPadElems, false);
176 paddedMaskValues.append(originalMask.template value_begin<bool>(),
177 originalMask.template value_end<bool>());
178 paddedMaskValues.resize(numDestElems * numSrcElemsPerDest, false);
179
180 // Compressing by combining every `numSrcElemsPerDest` elements:
181 SmallVector<bool> compressedMaskValues;
182 for (size_t i = 0; i < paddedMaskValues.size();
183 i += numSrcElemsPerDest) {
184 bool combinedValue = false;
185 for (int j = 0; j < numSrcElemsPerDest; ++j) {
186 combinedValue |= paddedMaskValues[i + j];
187 }
188 compressedMaskValues.push_back(combinedValue);
189 }
190 return arith::ConstantOp::create(
191 rewriter, loc,
192 DenseElementsAttr::get(newMaskType, compressedMaskValues));
193 });
194
195 if (!newMask)
196 return failure();
197
198 while (!extractOps.empty()) {
199 newMask =
200 vector::ExtractOp::create(rewriter, loc, (*newMask)->getResults()[0],
201 extractOps.back().getMixedPosition());
202 extractOps.pop_back();
203 }
204
205 return *newMask;
206}
207
208/// Extracts 1-D subvector from a 1-D vector.
209///
210/// Given the input rank-1 source vector, extracts `numElemsToExtract` elements
211/// from `src`, starting at `offset`. The result is also a rank-1 vector:
212///
213/// vector<numElemsToExtract x !elemType>
214///
215/// (`!elType` is the element type of the source vector). As `offset` is a known
216/// _static_ value, this helper hook emits `vector.extract_strided_slice`.
217///
218/// EXAMPLE:
219/// %res = vector.extract_strided_slice %src
220/// { offsets = [offset], sizes = [numElemsToExtract], strides = [1] }
222 Value src, int64_t offset,
223 int64_t numElemsToExtract) {
224 auto vectorType = cast<VectorType>(src.getType());
225 assert(vectorType.getRank() == 1 && "expected source to be rank-1-D vector ");
226 assert(offset + numElemsToExtract <= vectorType.getNumElements() &&
227 "subvector out of bounds");
228
229 // When extracting all available elements, just use the source vector as the
230 // result.
231 if (vectorType.getNumElements() == numElemsToExtract)
232 return src;
233
234 auto offsets = rewriter.getI64ArrayAttr({offset});
235 auto sizes = rewriter.getI64ArrayAttr({numElemsToExtract});
236 auto strides = rewriter.getI64ArrayAttr({1});
237
238 auto resultVectorType =
239 VectorType::get({numElemsToExtract}, vectorType.getElementType());
240 return vector::ExtractStridedSliceOp::create(rewriter, loc, resultVectorType,
241 src, offsets, sizes, strides)
242 ->getResult(0);
243}
244
245/// Inserts 1-D subvector into a 1-D vector.
246///
247/// Inserts the input rank-1 source vector into the destination vector starting
248/// at `offset`. As `offset` is a known _static_ value, this helper hook emits
249/// `vector.insert_strided_slice`.
250///
251/// EXAMPLE:
252/// %res = vector.insert_strided_slice %src, %dest
253/// {offsets = [%offset], strides [1]}
255 Value src, Value dest, int64_t offset) {
256 [[maybe_unused]] auto srcVecTy = cast<VectorType>(src.getType());
257 [[maybe_unused]] auto destVecTy = cast<VectorType>(dest.getType());
258 assert(srcVecTy.getRank() == 1 && destVecTy.getRank() == 1 &&
259 "expected source and dest to be rank-1 vector types");
260
261 // If overwritting the destination vector, just return the source.
262 if (srcVecTy.getNumElements() == destVecTy.getNumElements() && offset == 0)
263 return src;
264
265 auto offsets = rewriter.getI64ArrayAttr({offset});
266 auto strides = rewriter.getI64ArrayAttr({1});
267 return vector::InsertStridedSliceOp::create(rewriter, loc, destVecTy, src,
268 dest, offsets, strides);
269}
270
271/// Extracts 1-D subvector from a 1-D vector.
272///
273/// Given the input rank-1 source vector, extracts `numElemsToExtact` elements
274/// from `src`, starting at `offset`. The result is also a rank-1 vector:
275///
276/// vector<numElemsToExtact x !elType>
277///
278/// (`!elType` is the element type of the source vector). As `offset` is assumed
279/// to be a _dynamic_ SSA value, this helper method generates a sequence of
280/// `vector.extract` + `vector.insert` pairs.
281///
282/// EXAMPLE:
283/// %v1 = vector.extract %src[%offset] : i2 from vector<8xi2>
284/// %r1 = vector.insert %v1, %dest[0] : i2 into vector<3xi2>
285/// %c1 = arith.constant 1 : index
286/// %idx2 = arith.addi %offset, %c1 : index
287/// %v2 = vector.extract %src[%idx2] : i2 from vector<8xi2>
288/// %r2 = vector.insert %v2, %r1 [1] : i2 into vector<3xi2>
289/// (...)
291 Value src, Value dest,
292 OpFoldResult offset,
293 int64_t numElemsToExtract) {
294 auto srcVecTy = cast<VectorType>(src.getType());
295 assert(srcVecTy.getRank() == 1 && "expected source to be rank-1-D vector ");
296 // NOTE: We are unable to take the offset into account in the following
297 // assert, hence its still possible that the subvector is out-of-bounds even
298 // if the condition is true.
299 assert(numElemsToExtract <= srcVecTy.getNumElements() &&
300 "subvector out of bounds");
301
302 // When extracting all available elements, just use the source vector as the
303 // result.
304 if (srcVecTy.getNumElements() == numElemsToExtract)
305 return src;
306
307 for (int i = 0; i < numElemsToExtract; ++i) {
308 Value extractLoc =
309 (i == 0) ? dyn_cast<Value>(offset)
310 : arith::AddIOp::create(
311 rewriter, loc, rewriter.getIndexType(),
312 dyn_cast<Value>(offset),
313 arith::ConstantIndexOp::create(rewriter, loc, i));
314 auto extractOp = vector::ExtractOp::create(rewriter, loc, src, extractLoc);
315 dest = vector::InsertOp::create(rewriter, loc, extractOp, dest, i);
316 }
317 return dest;
318}
319
320/// Inserts 1-D subvector into a 1-D vector.
321///
322/// Inserts the input rank-1 source vector into the destination vector starting
323/// at `offset`. As `offset` is assumed to be a _dynamic_ SSA value, this hook
324/// uses a sequence of `vector.extract` + `vector.insert` pairs.
325///
326/// EXAMPLE:
327/// %v1 = vector.extract %src[0] : i2 from vector<8xi2>
328/// %r1 = vector.insert %v1, %dest[%offset] : i2 into vector<3xi2>
329/// %c1 = arith.constant 1 : index
330/// %idx2 = arith.addi %offset, %c1 : index
331/// %v2 = vector.extract %src[1] : i2 from vector<8xi2>
332/// %r2 = vector.insert %v2, %r1 [%idx2] : i2 into vector<3xi2>
333/// (...)
335 Value src, Value dest,
336 OpFoldResult offset,
337 int64_t numElemsToInsert) {
338 auto srcVecTy = cast<VectorType>(src.getType());
339 auto destVecTy = cast<VectorType>(dest.getType());
340 assert(srcVecTy.getRank() == 1 && destVecTy.getRank() == 1 &&
341 "expected source and dest to be rank-1 vector types");
342 (void)srcVecTy;
343 (void)destVecTy;
344 assert(numElemsToInsert > 0 &&
345 "the number of elements to insert must be greater than 0");
346 // NOTE: We are unable to take the offset into account in the following
347 // assert, hence its still possible that the subvector is out-of-bounds even
348 // if the condition is true.
349 assert(numElemsToInsert <= destVecTy.getNumElements() &&
350 "subvector out of bounds");
351
352 Value destOffsetVal = getValueOrCreateConstantIndexOp(rewriter, loc, offset);
353 for (int64_t i = 0; i < numElemsToInsert; ++i) {
354 auto insertLoc =
355 i == 0 ? destOffsetVal
356 : arith::AddIOp::create(
357 rewriter, loc, rewriter.getIndexType(), destOffsetVal,
358 arith::ConstantIndexOp::create(rewriter, loc, i));
359 auto extractOp = vector::ExtractOp::create(rewriter, loc, src, i);
360 dest = vector::InsertOp::create(rewriter, loc, extractOp, dest, insertLoc);
361 }
362 return dest;
363}
364
365/// Emulate a vector load for `emulatedElemTy` using `containerElemTy`
366///
367/// Specifically, use `containerElemTy` for loading a vector of
368/// `emulatedElemTy`. The load location is given by `base` and
369/// `linearizedIndices`, and the load size is given by
370/// `numEmulatedElementsToLoad`.
372 Value base,
373 OpFoldResult linearizedIndices,
374 int64_t numContainerElemsToLoad,
375 Type emulatedElemTy,
376 Type containerElemTy) {
377 auto emulatedPerContainerElem = containerElemTy.getIntOrFloatBitWidth() /
378 emulatedElemTy.getIntOrFloatBitWidth();
379 auto newLoad = vector::LoadOp::create(
380 rewriter, loc, VectorType::get(numContainerElemsToLoad, containerElemTy),
381 base, getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices));
382 return vector::BitCastOp::create(
383 rewriter, loc,
384 VectorType::get(numContainerElemsToLoad * emulatedPerContainerElem,
385 emulatedElemTy),
386 newLoad);
387}
388
389/// Downcast two values to `downcastType`, then select values
390/// based on `mask`, and casts the result to `upcastType`.
392 VectorType downcastType,
393 VectorType upcastType, Value mask,
394 Value trueValue, Value falseValue) {
395 assert(
396 downcastType.getNumElements() * downcastType.getElementTypeBitWidth() ==
397 upcastType.getNumElements() * upcastType.getElementTypeBitWidth() &&
398 "expected input and output number of bits to match");
399 if (trueValue.getType() != downcastType) {
400 trueValue =
401 vector::BitCastOp::create(builder, loc, downcastType, trueValue);
402 }
403 if (falseValue.getType() != downcastType) {
404 falseValue =
405 vector::BitCastOp::create(builder, loc, downcastType, falseValue);
406 }
407 Value selectedType =
408 arith::SelectOp::create(builder, loc, mask, trueValue, falseValue);
409 // Upcast the selected value to the new type.
410 return vector::BitCastOp::create(builder, loc, upcastType, selectedType);
411}
412
413/// Emits `memref.generic_atomic_rmw` op to store a subbyte-sized value to a
414/// byte in `linearizedMemref`, with a mask. The `valueToStore` is a vector of
415/// subbyte-sized elements, with size of 8 bits, and the mask is used to select
416/// which elements to store.
417///
418/// Inputs:
419/// linearizedMemref = |2|2|2|2| : <4xi2> (<1xi8>)
420/// storeIdx = 2
421/// valueToStore = |3|3|3|3| : vector<4xi2>
422/// mask = |0|0|1|1| : vector<4xi1>
423///
424/// Result:
425/// linearizedMemref = |2|2|3|3| : <4xi2> (<1xi8>)
426static void atomicRMW(OpBuilder &builder, Location loc,
427 MemRefValue linearizedMemref, Value storeIdx,
428 VectorValue valueToStore, Value mask) {
429 assert(valueToStore.getType().getRank() == 1 && "expected 1-D vector");
430
431 // Create an atomic load-modify-write region using
432 // `memref.generic_atomic_rmw`.
433 auto atomicOp = memref::GenericAtomicRMWOp::create(
434 builder, loc, linearizedMemref, ValueRange{storeIdx});
435 Value origValue = atomicOp.getCurrentValue();
436
437 OpBuilder::InsertionGuard guard(builder);
438 builder.setInsertionPointToStart(atomicOp.getBody());
439
440 // Load the original value from memory, and cast it to the original element
441 // type.
442 auto oneElemVecType = VectorType::get({1}, origValue.getType());
443 Value origVecValue = vector::FromElementsOp::create(
444 builder, loc, oneElemVecType, ValueRange{origValue});
445
446 // Construct the final masked value and yield it.
447 Value maskedValue =
448 downcastSelectAndUpcast(builder, loc, valueToStore.getType(),
449 oneElemVecType, mask, valueToStore, origVecValue);
450 auto scalarMaskedValue =
451 vector::ExtractOp::create(builder, loc, maskedValue, 0);
452 memref::AtomicYieldOp::create(builder, loc, scalarMaskedValue);
453}
454
455/// Generate a non-atomic read-modify-write sequence for storing to the emulated
456/// type. It has similar logic to `atomicRMWStore`, but without atomicity.
457static void nonAtomicRMW(OpBuilder &builder, Location loc,
458 MemRefValue linearizedMemref, Value linearizedIndex,
459 VectorValue valueToStore, Value mask) {
460 assert(valueToStore.getType().getRank() == 1 && "expected 1-D vector");
461
462 auto oneElemVecType =
463 VectorType::get({1}, linearizedMemref.getType().getElementType());
464 Value origVecValue =
465 vector::LoadOp::create(builder, loc, oneElemVecType, linearizedMemref,
466 ValueRange{linearizedIndex});
467 origVecValue = vector::BitCastOp::create(builder, loc, valueToStore.getType(),
468 origVecValue);
469
470 Value maskedValue =
471 downcastSelectAndUpcast(builder, loc, valueToStore.getType(),
472 oneElemVecType, mask, valueToStore, origVecValue);
473 vector::StoreOp::create(builder, loc, maskedValue, linearizedMemref,
474 linearizedIndex);
475}
476
477/// Extract `sliceNumElements` from source `vector` at `extractOffset`,
478/// and insert it into an empty vector at `insertOffset`.
479/// Inputs:
480/// vec_in = |0|1|2|3| : vector<4xi2>
481/// extractOffset = 1
482/// sliceNumElements = 2
483/// insertOffset = 2
484/// Output:
485/// vec_out = |0|0|1|2| : vector<4xi2>
486static Value extractSliceIntoByte(ConversionPatternRewriter &rewriter,
488 int64_t extractOffset,
489 int64_t sliceNumElements,
490 int64_t insertOffset) {
491 assert(vector.getType().getRank() == 1 && "expected 1-D vector");
492 auto vectorElementType = vector.getType().getElementType();
493 // TODO: update and use `alignedConversionPrecondition` in the place of
494 // these asserts.
495 assert(
496 sliceNumElements * vectorElementType.getIntOrFloatBitWidth() <= 8 &&
497 "sliceNumElements * vector element size must be less than or equal to 8");
498 assert(8 % vectorElementType.getIntOrFloatBitWidth() == 0 &&
499 "vector element must be a valid sub-byte type");
500 auto emulatedPerContainerElem = 8 / vectorElementType.getIntOrFloatBitWidth();
501 auto emptyByteVector = arith::ConstantOp::create(
502 rewriter, loc,
503 VectorType::get({emulatedPerContainerElem}, vectorElementType),
504 rewriter.getZeroAttr(
505 VectorType::get({emulatedPerContainerElem}, vectorElementType)));
506 auto extracted = staticallyExtractSubvector(rewriter, loc, vector,
507 extractOffset, sliceNumElements);
508 return staticallyInsertSubvector(rewriter, loc, extracted, emptyByteVector,
509 insertOffset);
510}
511
512namespace {
513
514//===----------------------------------------------------------------------===//
515// ConvertVectorStore
516//===----------------------------------------------------------------------===//
517
518// Emulate `vector.store` using a multi-byte container type.
519//
520// When `assumeAligned` is true, store offsets are assumed to be aligned to
521// container element boundaries, so a store whose source vector fills whole
522// container elements (isDivisibleInSize) is emitted as a simple bitcast +
523// store without checking the offset. Stores that are not divisible in size
524// are rejected. This is useful for downstream users that have already
525// ensured alignment.
526//
527// The container type is obtained through Op adaptor and would normally be
528// generated via `NarrowTypeEmulationConverter`.
529//
530// EXAMPLE 1
531// (aligned store of i4, emulated using i8 as the container type)
532//
533// vector.store %src, %dest[%idx_1, %idx_2] : memref<4x8xi4>, vector<8xi4>
534//
535// is rewritten as:
536//
537// %src_bitcast = vector.bitcast %src : vector<8xi4> to vector<4xi8>
538// vector.store %src_bitcast, %dest_bitcast[%idx]
539// : memref<16xi8>, vector<4xi8>
540//
541// EXAMPLE 2
542// (unaligned store of i2, emulated using i8 as the container type)
543//
544// vector.store %src, %dest[%c2, %c0] :memref<3x3xi2>, vector<3xi2>
545//
546// The i2 store is emulated through 2 x RMW sequences. The destination i2 memref
547// is modelled using 3 bytes:
548//
549// Byte 0 Byte 1 Byte 2
550// +----------+----------+----------+
551// | oooooooo | ooooNNNN | NNoooooo |
552// +----------+----------+----------+
553//
554// N - (N)ew entries (i.e. to be overwritten by vector.store)
555// o - (o)ld entries (to be preserved)
556//
557// For the generated output in the non-atomic case, see:
558// * @vector_store_i2_const_index_two_partial_stores`
559// in:
560// * "vector-emulate-narrow-type-unaligned-non-atomic.mlir".
561//
562// NOTE: By default, all RMW sequences are atomic. Set `disableAtomicRMW` to
563// `false` to generate non-atomic RMW sequences.
564struct ConvertVectorStore final : OpConversionPattern<vector::StoreOp> {
565 using Base::Base;
566
567 ConvertVectorStore(MLIRContext *context, bool disableAtomicRMW,
568 bool assumeAligned)
569 : OpConversionPattern<vector::StoreOp>(context),
570 disableAtomicRMW(disableAtomicRMW), assumeAligned(assumeAligned) {}
571
572 LogicalResult
573 matchAndRewrite(vector::StoreOp op, OpAdaptor adaptor,
574 ConversionPatternRewriter &rewriter) const override {
575
576 if (op.getValueToStore().getType().getRank() != 1)
577 return rewriter.notifyMatchFailure(op,
578 "only 1-D vectors are supported ATM");
579
580 auto loc = op.getLoc();
581
582 auto valueToStore = cast<VectorValue>(op.getValueToStore());
583 auto containerElemTy =
584 cast<MemRefType>(adaptor.getBase().getType()).getElementType();
585 Type emulatedElemTy = op.getValueToStore().getType().getElementType();
586 int emulatedBits = emulatedElemTy.getIntOrFloatBitWidth();
587 int containerBits = containerElemTy.getIntOrFloatBitWidth();
588
589 // Check per-element alignment.
590 if (containerBits % emulatedBits != 0) {
591 return rewriter.notifyMatchFailure(
592 op, "impossible to pack emulated elements into container elements "
593 "(bit-wise misalignment)");
594 }
595 int emulatedPerContainerElem = containerBits / emulatedBits;
596
597 // Adjust the number of elements to store when emulating narrow types.
598 // Here only the 1-D vector store is considered, and the N-D memref types
599 // should be linearized.
600 // For example, to emulate i4 to i8, the following op:
601 //
602 // vector.store %arg1, %0[%arg2, %arg3] : memref<4x8xi4>, vector<8xi4>
603 //
604 // can be replaced with
605 //
606 // %bitcast = vector.bitcast %arg1 : vector<8xi4> to vector<4xi8>
607 // vector.store %bitcast, %alloc[%linear_index] : memref<16xi8>,
608 // vector<4xi8>
609
610 auto origElements = valueToStore.getType().getNumElements();
611 // Note, per-element-alignment was already verified above.
612 bool isDivisibleInSize = origElements % emulatedPerContainerElem == 0;
613
614 // In assume-aligned mode, isDivisibleInSize alone is sufficient — the
615 // caller guarantees that store offsets are aligned to container element
616 // boundaries.
617 if (assumeAligned) {
618 if (!isDivisibleInSize)
619 return rewriter.notifyMatchFailure(
620 op, "the source vector does not fill whole container elements "
621 "(not divisible in size)");
622
623 auto stridedMetadata =
624 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
625 OpFoldResult linearizedIndices;
626 std::tie(std::ignore, linearizedIndices) =
628 rewriter, loc, emulatedBits, containerBits,
629 stridedMetadata.getConstifiedMixedOffset(),
630 stridedMetadata.getConstifiedMixedSizes(),
631 stridedMetadata.getConstifiedMixedStrides(),
632 getAsOpFoldResult(adaptor.getIndices()));
633 auto memrefBase = cast<MemRefValue>(adaptor.getBase());
634 int numElements = origElements / emulatedPerContainerElem;
635 auto bitCast = vector::BitCastOp::create(
636 rewriter, loc, VectorType::get(numElements, containerElemTy),
637 op.getValueToStore());
638 rewriter.replaceOpWithNewOp<vector::StoreOp>(
639 op, bitCast.getResult(), memrefBase,
640 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices));
641 return success();
642 }
643
644 auto stridedMetadata =
645 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
646
647 OpFoldResult linearizedIndices;
648 memref::LinearizedMemRefInfo linearizedInfo;
649 std::tie(linearizedInfo, linearizedIndices) =
651 rewriter, loc, emulatedBits, containerBits,
652 stridedMetadata.getConstifiedMixedOffset(),
653 stridedMetadata.getConstifiedMixedSizes(),
654 stridedMetadata.getConstifiedMixedStrides(),
655 getAsOpFoldResult(adaptor.getIndices()));
656
657 // Use the exact intraDataOffset when it can be folded. Dynamic values are
658 // rejected in this path because a dynamic offset is not necessarily aligned
659 // to a container element boundary. Callers that can guarantee alignment
660 // should use assumeAligned.
661 std::optional<int64_t> foldedNumFrontPadElems =
662 getConstantIntValue(linearizedInfo.intraDataOffset);
663
664 if (!foldedNumFrontPadElems) {
665 return rewriter.notifyMatchFailure(
666 op, "subbyte store emulation: dynamic front padding size is "
667 "not yet implemented");
668 }
669
670 auto memrefBase = cast<MemRefValue>(adaptor.getBase());
671
672 // RMWs are not needed when:
673 // * no _partial_ stores are required.
674 // A partial store is defined as a store in which only a part of the
675 // container element is overwritten, e.g.
676 //
677 // Dest before (8 bits)
678 // +----------+
679 // | 11000000 |
680 // +----------+
681 //
682 // Dest after storing 0xF at offset 4 (in bits)
683 // +----------+
684 // | 11001111 |
685 // +----------+
686 //
687 // At a higher level, this translats to:
688 // 1. The source vector size (in bits) is a multiple of byte size.
689 // 2. The address of the store is aligned to the container type width
690 // boundary.
691 //
692 // EXAMPLE 1:
693 // Requires partial store:
694 // vector.store %arg0, %0[%c3] : memref<13xi2>, vector<4xi2>
695 //
696 // EXAMPLE 2:
697 // Does not require a partial store:
698 // vector.store %arg0, %0[%c4] : memref<13xi2>, vector<4xi2>
699 //
700 // TODO: Take linearizedInfo.linearizedOffset into account. This is
701 // currently not needed/used/exercised as all our tests set offset to 0.
702 bool emulationRequiresPartialStores = *foldedNumFrontPadElems != 0;
703
704 if (!emulationRequiresPartialStores) {
705 // Basic case: storing full bytes.
706 auto numElements = origElements / emulatedPerContainerElem;
707 auto bitCast = vector::BitCastOp::create(
708 rewriter, loc, VectorType::get(numElements, containerElemTy),
709 op.getValueToStore());
710 rewriter.replaceOpWithNewOp<vector::StoreOp>(
711 op, bitCast.getResult(), memrefBase,
712 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices));
713 return success();
714 }
715
716 // Next, handle the case when sub-byte read-modify-write
717 // sequences are needed to emulate a vector store.
718 // Here is an example:
719 //
720 // Vector to store: vector<7xi2>
721 // Value to store: 11 11 11 11 11 11 11 (all ones)
722 //
723 // Destination: memref<12xi2>
724 // Store offset: 2 (i.e. 4 bits into the 1st emulated byte).
725 //
726 // Input MLIR: vector.store %val, %dest[%c2] : memref<12xi2>, vector<7xi2>
727 //
728 // Destination memref before:
729 //
730 // Byte 0 Byte 1 Byte 2
731 // +----------+----------+----------+
732 // | 00000000 | 00000000 | 00000000 |
733 // +----------+----------+----------+
734 //
735 // Destination memref after:
736 //
737 // Byte 0 Byte 1 Byte 2
738 // +----------+----------+----------+
739 // | 00001111 | 11111111 | 11000000 |
740 // +----------+----------+----------+
741 //
742 // Note, stores to Byte 1 are "full-width" and hence don't require RMW (no
743 // need for atomicity). Stores to Bytes 0 and Byte 2 are "partial", hence
744 // requiring RMW access (atomicity is required).
745
746 // The index into the target memref we are storing to.
747 Value currentDestIndex =
748 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices);
749 // The index into the source vector we are currently processing.
750 auto currentSourceIndex = 0;
751
752 // Build a mask used for rmw.
753 auto subWidthStoreMaskType =
754 VectorType::get({emulatedPerContainerElem}, rewriter.getI1Type());
755
756 auto storeFunc = disableAtomicRMW ? nonAtomicRMW : atomicRMW;
757
758 // 1. Partial width store for the leading byte.
759 // When the store address is not aligned to emulated width boundary, deal
760 // with the unaligned part so that the rest elements are aligned to width
761 // boundary.
762 auto frontSubWidthStoreElem =
763 (emulatedPerContainerElem - *foldedNumFrontPadElems) %
764 emulatedPerContainerElem;
765 if (frontSubWidthStoreElem > 0) {
766 SmallVector<bool> frontMaskValues(emulatedPerContainerElem, false);
767 if (*foldedNumFrontPadElems + origElements < emulatedPerContainerElem) {
768 std::fill_n(frontMaskValues.begin() + *foldedNumFrontPadElems,
769 origElements, true);
770 frontSubWidthStoreElem = origElements;
771 } else {
772 std::fill_n(frontMaskValues.end() - frontSubWidthStoreElem,
773 *foldedNumFrontPadElems, true);
774 }
775 auto frontMask = arith::ConstantOp::create(
776 rewriter, loc,
777 DenseElementsAttr::get(subWidthStoreMaskType, frontMaskValues));
778
779 currentSourceIndex = emulatedPerContainerElem - (*foldedNumFrontPadElems);
780 auto value =
781 extractSliceIntoByte(rewriter, loc, valueToStore, 0,
782 frontSubWidthStoreElem, *foldedNumFrontPadElems);
783
784 storeFunc(rewriter, loc, memrefBase, currentDestIndex,
785 cast<VectorValue>(value), frontMask.getResult());
786 }
787
788 if (currentSourceIndex >= origElements) {
789 rewriter.eraseOp(op);
790 return success();
791 }
792
793 // Increment the destination index by 1 to align to the emulated width
794 // boundary.
795 auto constantOne = arith::ConstantIndexOp::create(rewriter, loc, 1);
796 currentDestIndex = arith::AddIOp::create(
797 rewriter, loc, rewriter.getIndexType(), currentDestIndex, constantOne);
798
799 // 2. Full width store for the inner output bytes.
800 // After the previous step, the store address is aligned to the emulated
801 // width boundary.
802 int64_t fullWidthStoreSize =
803 (origElements - currentSourceIndex) / emulatedPerContainerElem;
804 int64_t numNonFullWidthElements =
805 fullWidthStoreSize * emulatedPerContainerElem;
806 if (fullWidthStoreSize > 0) {
807 auto fullWidthStorePart = staticallyExtractSubvector(
808 rewriter, loc, valueToStore, currentSourceIndex,
809 numNonFullWidthElements);
810
811 auto originType = cast<VectorType>(fullWidthStorePart.getType());
812 auto memrefElemType = getElementTypeOrSelf(memrefBase.getType());
813 auto storeType = VectorType::get(
814 {originType.getNumElements() / emulatedPerContainerElem},
815 memrefElemType);
816 auto bitCast = vector::BitCastOp::create(rewriter, loc, storeType,
817 fullWidthStorePart);
818 vector::StoreOp::create(rewriter, loc, bitCast.getResult(), memrefBase,
819 currentDestIndex);
820
821 currentSourceIndex += numNonFullWidthElements;
822 currentDestIndex = arith::AddIOp::create(
823 rewriter, loc, rewriter.getIndexType(), currentDestIndex,
824 arith::ConstantIndexOp::create(rewriter, loc, fullWidthStoreSize));
825 }
826
827 // 3. Partial width store for the trailing output byte.
828 // It is needed when the residual length is smaller than the emulated width,
829 // which is not covered in step 2 above.
830 auto remainingElements = origElements - currentSourceIndex;
831 if (remainingElements != 0) {
832 auto subWidthStorePart =
833 extractSliceIntoByte(rewriter, loc, cast<VectorValue>(valueToStore),
834 currentSourceIndex, remainingElements, 0);
835
836 // Generate back mask.
837 auto maskValues = SmallVector<bool>(emulatedPerContainerElem, false);
838 std::fill_n(maskValues.begin(), remainingElements, 1);
839 auto backMask = arith::ConstantOp::create(
840 rewriter, loc,
841 DenseElementsAttr::get(subWidthStoreMaskType, maskValues));
842
843 storeFunc(rewriter, loc, memrefBase, currentDestIndex,
844 cast<VectorValue>(subWidthStorePart), backMask.getResult());
845 }
846
847 rewriter.eraseOp(op);
848 return success();
849 }
850
851private:
852 const bool disableAtomicRMW;
853 const bool assumeAligned;
854};
855
856//===----------------------------------------------------------------------===//
857// ConvertVectorMaskedStore
858//===----------------------------------------------------------------------===//
859
860/// Converts `vector.maskedstore` operations on narrow element types to work
861/// with wider, byte-aligned container types by adjusting the mask and using
862/// bitcasting.
863///
864/// Example: Storing `vector<6xi4>` is emulated by bitcasting to `vector<3xi8>`
865/// (each `i8` container element holds two `i4` values) and storing with an
866/// adjusted mask .
867struct ConvertVectorMaskedStore final
868 : OpConversionPattern<vector::MaskedStoreOp> {
869 using Base::Base;
870
871 LogicalResult
872 matchAndRewrite(vector::MaskedStoreOp op, OpAdaptor adaptor,
873 ConversionPatternRewriter &rewriter) const override {
874
875 // Prerequisite: memref in the vector.maskedstore op is flattened into 1-D.
876 if (op.getValueToStore().getType().getRank() != 1)
877 return rewriter.notifyMatchFailure(
878 op, "Memref in vector.maskedstore op must be flattened beforehand.");
879
880 auto loc = op.getLoc();
881 auto containerElemTy =
882 cast<MemRefType>(adaptor.getBase().getType()).getElementType();
883 Type emulatedElemTy = op.getValueToStore().getType().getElementType();
884 int emulatedBits = emulatedElemTy.getIntOrFloatBitWidth();
885 int containerBits = containerElemTy.getIntOrFloatBitWidth();
886
887 // Check per-element alignment.
888 if (containerBits % emulatedBits != 0) {
889 return rewriter.notifyMatchFailure(
890 op, "impossible to pack emulated elements into container elements "
891 "(bit-wise misalignment)");
892 }
893
894 int emulatedPerContainerElem = containerBits / emulatedBits;
895 int origElements = op.getValueToStore().getType().getNumElements();
896 if (origElements % emulatedPerContainerElem != 0)
897 return failure();
898
899 auto stridedMetadata =
900 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
901 OpFoldResult linearizedIndicesOfr;
902 memref::LinearizedMemRefInfo linearizedInfo;
903 std::tie(linearizedInfo, linearizedIndicesOfr) =
905 rewriter, loc, emulatedBits, containerBits,
906 stridedMetadata.getConstifiedMixedOffset(),
907 stridedMetadata.getConstifiedMixedSizes(),
908 stridedMetadata.getConstifiedMixedStrides(),
909 getAsOpFoldResult(adaptor.getIndices()));
910 Value linearizedIndices =
911 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndicesOfr);
912
913 // Load the whole data and use arith.select to handle the corner cases.
914 //
915 // As an example, for this masked store of i4 values:
916 //
917 // vector.maskedstore %0[%c0, %c0], %mask, %val_to_store
918 //
919 // and given these input values:
920 //
921 // %mask = [0, 1, 1, 1, 1, 0, 0, 0] (8 * i1)
922 // %0[%c0, %c0] =
923 // [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8] (8 * i4)
924 // %val_to_store =
925 // [0x9, 0xA, 0xB, 0xC, 0xD, 0xE, 0xF, 0x0] (8 * i4)
926 //
927 // we'll have the following i4 output:
928 //
929 // expected output: [0x1, 0xA, 0xB, 0xC, 0xD, 0x6, 0x7, 0x8]
930 //
931 // Emulating the above using i8 will give:
932 //
933 // %compressed_mask = [1, 1, 1, 0] (4 * i1)
934 // %maskedload = [0x12, 0x34, 0x56, 0x00] (4 * i8)
935 // %bitcast = [0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x0, 0x0] (8 * i4)
936 // %select_using_shifted_mask =
937 // [0x1, 0xA, 0xB, 0xC, 0xD, 0x6, 0x0, 0x0] (8 * i4)
938 // %packed_data = [0x1A, 0xBC, 0xD6, 0x00] (4 * i8)
939 //
940 // Using the compressed mask to store %packed_data results in expected
941 // output.
942 //
943 // FIXME: Make an example based on the comment above work (see #115460 for
944 // reproducer).
945 FailureOr<Operation *> newMask = getCompressedMaskOp(
946 rewriter, loc, op.getMask(), origElements, emulatedPerContainerElem);
947 if (failed(newMask))
948 return failure();
949
950 auto numElements = (origElements + emulatedPerContainerElem - 1) /
951 emulatedPerContainerElem;
952 auto newType = VectorType::get(numElements, containerElemTy);
953 auto passThru = arith::ConstantOp::create(rewriter, loc, newType,
954 rewriter.getZeroAttr(newType));
955
956 auto newLoad = vector::MaskedLoadOp::create(
957 rewriter, loc, newType, adaptor.getBase(), linearizedIndices,
958 newMask.value()->getResult(0), passThru);
959
960 auto newBitCastType =
961 VectorType::get(numElements * emulatedPerContainerElem, emulatedElemTy);
962 Value valueToStore =
963 vector::BitCastOp::create(rewriter, loc, newBitCastType, newLoad);
964 valueToStore = arith::SelectOp::create(rewriter, loc, op.getMask(),
965 op.getValueToStore(), valueToStore);
966 valueToStore =
967 vector::BitCastOp::create(rewriter, loc, newType, valueToStore);
968
969 rewriter.replaceOpWithNewOp<vector::MaskedStoreOp>(
970 op, adaptor.getBase(), linearizedIndices, newMask.value()->getResult(0),
971 valueToStore);
972 return success();
973 }
974};
975
976//===----------------------------------------------------------------------===//
977// ConvertVectorLoad
978//===----------------------------------------------------------------------===//
979
980/// Converts `vector.load` on narrow element types to work with
981/// wider, byte-aligned container types by adjusting load sizes and using
982/// bitcasting.
983///
984/// Example: `vector.load` of `vector<4xi4>` from `memref<3x4xi4>` is emulated
985/// by loading `vector<2xi8>` from the linearized `memref<6xi8>` (each `i8`
986/// container holds two `i4` values) and bitcasting back.
987///
988/// There are cases where the number of elements to load is not byte-aligned. In
989/// those cases, loads are converted to byte-aligned, byte-sized loads and the
990/// target vector is extracted from the loaded vector.
991struct ConvertVectorLoad final : OpConversionPattern<vector::LoadOp> {
992 using Base::Base;
993
994 LogicalResult
995 matchAndRewrite(vector::LoadOp op, OpAdaptor adaptor,
996 ConversionPatternRewriter &rewriter) const override {
997 // Prerequisite: memref in the vector.load op is flattened into 1-D.
998 if (op.getVectorType().getRank() != 1)
999 return rewriter.notifyMatchFailure(
1000 op, "Memref in emulated vector ops must be flattened beforehand.");
1001
1002 auto loc = op.getLoc();
1003 auto containerElemTy =
1004 cast<MemRefType>(adaptor.getBase().getType()).getElementType();
1005 Type emulatedElemTy = op.getType().getElementType();
1006 int emulatedBits = emulatedElemTy.getIntOrFloatBitWidth();
1007 int containerBits = containerElemTy.getIntOrFloatBitWidth();
1008
1009 // Check per-element alignment.
1010 if (containerBits % emulatedBits != 0) {
1011 return rewriter.notifyMatchFailure(
1012 op, "impossible to pack emulated elements into container elements "
1013 "(bit-wise misalignment)");
1014 }
1015 int emulatedPerContainerElem = containerBits / emulatedBits;
1016
1017 // Adjust the number of elements to load when emulating narrow types,
1018 // and then cast back to the original type with vector.bitcast op.
1019 // For example, to emulate i4 to i8, the following op:
1020 //
1021 // %1 = vector.load %0[%c0, %c0] : memref<3x4xi4>, vector<4xi4>
1022 //
1023 // can be replaced with
1024 //
1025 // %1 = vector.load %0[%linear_index] : memref<6xi8>, vector<2xi8>
1026 // %2 = vector.bitcast %1 : vector<2xi8> to vector<4xi4>
1027 //
1028 // There are cases where the number of elements to load is not byte-aligned,
1029 // for example:
1030 //
1031 // %1 = vector.load %0[%c1, %c0] : memref<3x3xi2>, vector<3xi2>
1032 //
1033 // we will have to load extra bytes and extract the exact slice in between.
1034 //
1035 // %1 = vector.load %0[%c2] : memref<3xi8>, vector<2xi8>
1036 // %2 = vector.bitcast %1 : vector<2xi8> to vector<8xi2>
1037 // %3 = vector.extract_strided_slice %1 {offsets = [2], sizes = [3], strides
1038 // = [1]}
1039 // : vector<8xi2> to vector<3xi2>
1040 //
1041 // TODO: Currently the extract_strided_slice's attributes must be known at
1042 // compile time as they must be constants.
1043
1044 auto origElements = op.getVectorType().getNumElements();
1045 // Note, per-element-alignment was already verified above.
1046 bool isDivisibleInSize = origElements % emulatedPerContainerElem == 0;
1047
1048 auto stridedMetadata =
1049 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
1050
1051 OpFoldResult linearizedIndices;
1052 memref::LinearizedMemRefInfo linearizedInfo;
1053 std::tie(linearizedInfo, linearizedIndices) =
1055 rewriter, loc, emulatedBits, containerBits,
1056 stridedMetadata.getConstifiedMixedOffset(),
1057 stridedMetadata.getConstifiedMixedSizes(),
1058 stridedMetadata.getConstifiedMixedStrides(),
1059 getAsOpFoldResult(adaptor.getIndices()));
1060
1061 std::optional<int64_t> foldedIntraVectorOffset =
1062 isDivisibleInSize ? 0
1063 : getConstantIntValue(linearizedInfo.intraDataOffset);
1064
1065 // Always load enough elements which can cover the original elements.
1066 int64_t maxintraDataOffset =
1067 foldedIntraVectorOffset.value_or(emulatedPerContainerElem - 1);
1068 auto numElements = llvm::divideCeil(maxintraDataOffset + origElements,
1069 emulatedPerContainerElem);
1070 Value result =
1071 emulatedVectorLoad(rewriter, loc, adaptor.getBase(), linearizedIndices,
1072 numElements, emulatedElemTy, containerElemTy);
1073
1074 if (!foldedIntraVectorOffset) {
1075 auto resultVector = arith::ConstantOp::create(
1076 rewriter, loc, op.getType(), rewriter.getZeroAttr(op.getType()));
1078 rewriter, loc, dyn_cast<TypedValue<VectorType>>(result), resultVector,
1079 linearizedInfo.intraDataOffset, origElements);
1080 } else if (!isDivisibleInSize) {
1082 rewriter, loc, result, *foldedIntraVectorOffset, origElements);
1083 }
1084 rewriter.replaceOp(op, result);
1085 return success();
1086 }
1087};
1088
1089//===----------------------------------------------------------------------===//
1090// ConvertVectorMaskedLoad
1091//===----------------------------------------------------------------------===//
1092
1093/// Converts `vector.maskedload` operations on narrow element types to work with
1094/// wider, byte-aligned container types by adjusting the mask and using
1095/// bitcasting.
1096///
1097/// Example: Loading `vector<6xi4>` is emulated by loading `vector<3xi8>` and
1098/// bitcasting, since each `i8` container element holds two `i4` values.
1099struct ConvertVectorMaskedLoad final
1100 : OpConversionPattern<vector::MaskedLoadOp> {
1101 using Base::Base;
1102
1103 LogicalResult
1104 matchAndRewrite(vector::MaskedLoadOp op, OpAdaptor adaptor,
1105 ConversionPatternRewriter &rewriter) const override {
1106 if (op.getVectorType().getRank() != 1)
1107 return rewriter.notifyMatchFailure(
1108 op, "Memref in emulated vector ops must be flattened beforehand.");
1109
1110 auto loc = op.getLoc();
1111
1112 auto containerElemTy =
1113 cast<MemRefType>(adaptor.getBase().getType()).getElementType();
1114 Type emulatedElemTy = op.getType().getElementType();
1115 int emulatedBits = emulatedElemTy.getIntOrFloatBitWidth();
1116 int containerBits = containerElemTy.getIntOrFloatBitWidth();
1117
1118 // Check per-element alignment.
1119 if (containerBits % emulatedBits != 0) {
1120 return rewriter.notifyMatchFailure(
1121 op, "impossible to pack emulated elements into container elements "
1122 "(bit-wise misalignment)");
1123 }
1124 int emulatedPerContainerElem = containerBits / emulatedBits;
1125
1126 // Adjust the number of elements to load when emulating narrow types,
1127 // and then cast back to the original type with vector.bitcast op.
1128 // For example, to emulate i4 to i8, the following op:
1129 //
1130 // %mask = vector.constant_mask [3] : vector<6xi1>
1131 // %1 = vector.maskedload %0[%c0, %c0], %mask, %pass_thru :
1132 // memref<3x6xi4>, vector<6xi1>, vector<6xi4> into vector<6xi4>
1133 //
1134 // can be replaced with
1135 //
1136 // %new_mask = vector.constant_mask [2] : vector<3xi1>
1137 // %new_pass_thru = vector.bitcast %pass_thru :
1138 // vector<6xi4> to vector<3xi8>
1139 // %1 = vector.maskedload %0[%linear_index], %new_mask, %new_pass_thru :
1140 // memref<9xi8>, vector<3xi1>, vector<3xi8> into vector<3xi8>
1141 // %2 = vector.bitcast %1 : vector<3xi8> to vector<6xi4>
1142 //
1143 // Since we are effectively loading 16 bits (2xi8) from the memref with the
1144 // new mask, while originally we only wanted to effectively load 12 bits
1145 // (3xi4) from the memref, we need to set the second half of the last i8
1146 // that was effectively loaded (i.e. the second i8) to %pass_thru.
1147 //
1148 // %3 = arith.select %mask, %2, %pass_thru : vector<6xi1>, vector<6xi4>
1149 //
1150 // Given these input values:
1151 // %mask = [1, 1, 1, 0, 0, 0]
1152 // %0[%c0, %c0] contains [0x1, 0x2, 0x3, 0x4, 0x5, 0x6]
1153 // %pass_thru = [0x7, 0x8, 0x9, 0xA, 0xB, 0xC]
1154 //
1155 // we'll have:
1156 //
1157 // expected output: [0x1, 0x2, 0x3, 0xA, 0xB, 0xC]
1158 //
1159 // %new_mask = [1, 1, 0]
1160 // %new_pass_thru = [0x78, 0x9A, 0xBC]
1161 // %1 = [0x12, 0x34, 0xBC]
1162 // %2 = [0x1, 0x2, 0x3, 0x4, 0xB, 0xC]
1163 // %3 = [0x1, 0x2, 0x3, 0xA, 0xB, 0xC]
1164 //
1165 // TODO: Currently, only the even number of elements loading is supported.
1166 // To deal with the odd number of elements, one has to extract the
1167 // subvector at the proper offset after bit-casting.
1168 auto origType = op.getVectorType();
1169 auto origElements = origType.getNumElements();
1170 // Note, per-element-alignment was already verified above.
1171 bool isDivisibleInSize = origElements % emulatedPerContainerElem == 0;
1172
1173 auto stridedMetadata =
1174 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
1175 OpFoldResult linearizedIndices;
1176 memref::LinearizedMemRefInfo linearizedInfo;
1177 std::tie(linearizedInfo, linearizedIndices) =
1179 rewriter, loc, emulatedBits, containerBits,
1180 stridedMetadata.getConstifiedMixedOffset(),
1181 stridedMetadata.getConstifiedMixedSizes(),
1182 stridedMetadata.getConstifiedMixedStrides(),
1183 getAsOpFoldResult(adaptor.getIndices()));
1184
1185 std::optional<int64_t> foldedIntraVectorOffset =
1186 isDivisibleInSize ? 0
1187 : getConstantIntValue(linearizedInfo.intraDataOffset);
1188
1189 int64_t maxIntraDataOffset =
1190 foldedIntraVectorOffset.value_or(emulatedPerContainerElem - 1);
1191 FailureOr<Operation *> newMask =
1192 getCompressedMaskOp(rewriter, loc, op.getMask(), origElements,
1193 emulatedPerContainerElem, maxIntraDataOffset);
1194 if (failed(newMask))
1195 return failure();
1196
1197 Value passthru = op.getPassThru();
1198
1199 auto numElements = llvm::divideCeil(maxIntraDataOffset + origElements,
1200 emulatedPerContainerElem);
1201 auto loadType = VectorType::get(numElements, containerElemTy);
1202 auto newBitcastType =
1203 VectorType::get(numElements * emulatedPerContainerElem, emulatedElemTy);
1204
1205 auto emptyVector = arith::ConstantOp::create(
1206 rewriter, loc, newBitcastType, rewriter.getZeroAttr(newBitcastType));
1207 if (!foldedIntraVectorOffset) {
1208 passthru = dynamicallyInsertSubVector(
1209 rewriter, loc, passthru, emptyVector, linearizedInfo.intraDataOffset,
1210 origElements);
1211 } else if (!isDivisibleInSize) {
1212 passthru = staticallyInsertSubvector(rewriter, loc, passthru, emptyVector,
1213 *foldedIntraVectorOffset);
1214 }
1215 auto newPassThru =
1216 vector::BitCastOp::create(rewriter, loc, loadType, passthru);
1217
1218 // Generating the new masked load.
1219 auto newLoad = vector::MaskedLoadOp::create(
1220 rewriter, loc, loadType, adaptor.getBase(),
1221 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices),
1222 newMask.value()->getResult(0), newPassThru);
1223
1224 // Setting the part that originally was not effectively loaded from memory
1225 // to pass through.
1226 auto bitCast =
1227 vector::BitCastOp::create(rewriter, loc, newBitcastType, newLoad);
1228
1229 Value mask = op.getMask();
1230 auto newSelectMaskType = VectorType::get(
1231 numElements * emulatedPerContainerElem, rewriter.getI1Type());
1232 // TODO: try to fold if op's mask is constant
1233 auto emptyMask =
1234 arith::ConstantOp::create(rewriter, loc, newSelectMaskType,
1235 rewriter.getZeroAttr(newSelectMaskType));
1236 if (!foldedIntraVectorOffset) {
1237 mask = dynamicallyInsertSubVector(rewriter, loc, mask, emptyMask,
1238 linearizedInfo.intraDataOffset,
1239 origElements);
1240 } else if (!isDivisibleInSize) {
1241 mask = staticallyInsertSubvector(rewriter, loc, op.getMask(), emptyMask,
1242 *foldedIntraVectorOffset);
1243 }
1244
1245 Value result =
1246 arith::SelectOp::create(rewriter, loc, mask, bitCast, passthru);
1247 if (!foldedIntraVectorOffset) {
1249 rewriter, loc, result, op.getPassThru(),
1250 linearizedInfo.intraDataOffset, origElements);
1251 } else if (!isDivisibleInSize) {
1253 rewriter, loc, result, *foldedIntraVectorOffset, origElements);
1254 }
1255 rewriter.replaceOp(op, result);
1256
1257 return success();
1258 }
1259};
1260
1261/// Check whether `subByteVecTy` fits wthin a vector of `multiByteScalarTy`
1262///
1263/// "Fitting" means that `subByteVecTy` (a vector of sub-byte elements, e.g.
1264/// vector<4xi4>), can fit within N scalar elements of type `multiByteScalarTy`
1265/// (a multi-byte scalar, e.g. i16), where N is some integer.
1266///
1267/// Put differently, this method checks whether this would be valid:
1268///
1269/// vector.bitcast subByteVecTy into vector<N x multiByteScalarTy>
1270///
1271/// EXAMPLES:
1272/// * vector<4xi4> -> i16 - yes (N = 1)
1273/// * vector<4xi4> -> i8 - yes (N = 2)
1274/// * vector<3xi4> -> i8 - no (N would have to be 1.5)
1275/// * vector<3xi2> -> i16 - no (N would have to be 0.5)
1276static bool fitsInMultiByteContainerTy(VectorType subByteVecTy,
1277 Type multiByteScalarTy) {
1278 assert((isa<IntegerType, FloatType>(multiByteScalarTy)) && "Not scalar!");
1279
1280 int subByteBits = subByteVecTy.getElementType().getIntOrFloatBitWidth();
1281 int multiByteBits = multiByteScalarTy.getIntOrFloatBitWidth();
1282
1283 assert(subByteBits < 8 && "Not a sub-byte scalar type!");
1284 assert(multiByteBits % 8 == 0 && "Not a multi-byte scalar type!");
1285 assert(multiByteBits % subByteBits == 0 && "Unalagined element types!");
1286
1287 int elemsPerMultiByte = multiByteBits / subByteBits;
1288
1289 return subByteVecTy.getShape().back() % elemsPerMultiByte == 0;
1290}
1291
1292//===----------------------------------------------------------------------===//
1293// ConvertVectorTransferRead
1294//===----------------------------------------------------------------------===//
1295
1296// TODO: Document-me
1297struct ConvertVectorTransferRead final
1298 : OpConversionPattern<vector::TransferReadOp> {
1299 using Base::Base;
1300
1301 LogicalResult
1302 matchAndRewrite(vector::TransferReadOp op, OpAdaptor adaptor,
1303 ConversionPatternRewriter &rewriter) const override {
1304
1305 // Prerequisites: memref in the vector.transfer_read op is flattened into
1306 // 1-D.
1307 if (op.getVectorType().getRank() != 1)
1308 return rewriter.notifyMatchFailure(
1309 op, "Memref in emulated vector ops must be flattened beforehand.");
1310
1311 auto loc = op.getLoc();
1312 auto containerElemTy =
1313 cast<MemRefType>(adaptor.getBase().getType()).getElementType();
1314 Type emulatedElemTy = op.getType().getElementType();
1315 int emulatedBits = emulatedElemTy.getIntOrFloatBitWidth();
1316 int containerBits = containerElemTy.getIntOrFloatBitWidth();
1317
1318 // Check per-element alignment.
1319 if (containerBits % emulatedBits != 0) {
1320 return rewriter.notifyMatchFailure(
1321 op, "impossible to pack emulated elements into container elements "
1322 "(bit-wise misalignment)");
1323 }
1324 int emulatedPerContainerElem = containerBits / emulatedBits;
1325
1326 auto origElements = op.getVectorType().getNumElements();
1327
1328 // Note, per-element-alignment was already verified above.
1329 bool isDivisibleInSize =
1330 fitsInMultiByteContainerTy(op.getVectorType(), containerElemTy);
1331
1332 // Pad the padding value with 0s on the left. These bits are discarded and
1333 // thus their values don't matter.
1334 Value padding = adaptor.getPadding();
1335 if (!padding.getType().isInteger()) {
1336 padding = arith::BitcastOp::create(
1337 rewriter, loc,
1338 IntegerType::get(rewriter.getContext(),
1339 padding.getType().getIntOrFloatBitWidth()),
1340 padding);
1341 }
1342 auto newPadding =
1343 arith::ExtUIOp::create(rewriter, loc, containerElemTy, padding);
1344
1345 auto stridedMetadata =
1346 memref::ExtractStridedMetadataOp::create(rewriter, loc, op.getBase());
1347
1348 OpFoldResult linearizedIndices;
1349 memref::LinearizedMemRefInfo linearizedInfo;
1350 std::tie(linearizedInfo, linearizedIndices) =
1352 rewriter, loc, emulatedBits, containerBits,
1353 stridedMetadata.getConstifiedMixedOffset(),
1354 stridedMetadata.getConstifiedMixedSizes(),
1355 stridedMetadata.getConstifiedMixedStrides(),
1356 getAsOpFoldResult(adaptor.getIndices()));
1357
1358 std::optional<int64_t> foldedIntraVectorOffset =
1359 isDivisibleInSize ? 0
1360 : getConstantIntValue(linearizedInfo.intraDataOffset);
1361
1362 int64_t maxIntraDataOffset =
1363 foldedIntraVectorOffset.value_or(emulatedPerContainerElem - 1);
1364 auto numElements = llvm::divideCeil(maxIntraDataOffset + origElements,
1365 emulatedPerContainerElem);
1366
1367 auto newRead = vector::TransferReadOp::create(
1368 rewriter, loc, VectorType::get(numElements, containerElemTy),
1369 adaptor.getBase(),
1370 getValueOrCreateConstantIndexOp(rewriter, loc, linearizedIndices),
1371 newPadding);
1372
1373 auto bitCast = vector::BitCastOp::create(
1374 rewriter, loc,
1375 VectorType::get(numElements * emulatedPerContainerElem, emulatedElemTy),
1376 newRead);
1377
1378 Value result = bitCast->getResult(0);
1379 if (!foldedIntraVectorOffset) {
1380 auto zeros = arith::ConstantOp::create(
1381 rewriter, loc, op.getType(), rewriter.getZeroAttr(op.getType()));
1382 result = dynamicallyExtractSubVector(rewriter, loc, bitCast, zeros,
1383 linearizedInfo.intraDataOffset,
1384 origElements);
1385 } else if (!isDivisibleInSize) {
1387 rewriter, loc, result, *foldedIntraVectorOffset, origElements);
1388 }
1389 rewriter.replaceOp(op, result);
1390
1391 return success();
1392 }
1393};
1394} // end anonymous namespace
1395
1396//===----------------------------------------------------------------------===//
1397// RewriteBitCastOfTruncI
1398//===----------------------------------------------------------------------===//
1399
1400namespace {
1401
1402/// Helper struct to keep track of the provenance of a contiguous set of bits
1403/// in a source vector.
1404struct SourceElementRange {
1405 /// The index of the source vector element that contributes bits to *this.
1406 int64_t sourceElementIdx;
1407 /// The range of bits in the source vector element that contribute to *this.
1408 int64_t sourceBitBegin;
1409 int64_t sourceBitEnd;
1410};
1411
1412struct SourceElementRangeList : public SmallVector<SourceElementRange> {
1413 /// Given the index of a SourceElementRange in the SourceElementRangeList,
1414 /// compute the amount of bits that need to be shifted to the left to get the
1415 /// bits in their final location. This shift amount is simply the sum of the
1416 /// bits *before* `shuffleIdx` (i.e. the bits of `shuffleIdx = 0` are always
1417 /// the LSBs, the bits of `shuffleIdx = ` come next, etc).
1418 int64_t computeLeftShiftAmount(int64_t shuffleIdx) const {
1419 int64_t res = 0;
1420 for (int64_t i = 0; i < shuffleIdx; ++i)
1421 res += (*this)[i].sourceBitEnd - (*this)[i].sourceBitBegin;
1422 return res;
1423 }
1424};
1425
1426/// Helper struct to enumerate the source elements and bit ranges that are
1427/// involved in a bitcast operation.
1428/// This allows rewriting a vector.bitcast into shuffles and bitwise ops for
1429/// any 1-D vector shape and any source/target bitwidths.
1430/// This creates and holds a mapping of the form:
1431/// [dstVectorElementJ] ==
1432/// [ {srcVectorElementX, bitRange}, {srcVectorElementY, bitRange}, ... ]
1433/// E.g. `vector.bitcast ... : vector<1xi24> to vector<3xi8>` is decomposed as:
1434/// [0] = {0, [0-8)}
1435/// [1] = {0, [8-16)}
1436/// [2] = {0, [16-24)}
1437/// and `vector.bitcast ... : vector<2xi15> to vector<3xi10>` is decomposed as:
1438/// [0] = {0, [0, 10)}, {1, [0, 5)}
1439/// [1] = {1, [5, 10)}, {2, [0, 10)}
1440struct BitCastBitsEnumerator {
1441 BitCastBitsEnumerator(VectorType sourceVectorType,
1442 VectorType targetVectorType);
1443
1444 int64_t getMaxNumberOfEntries() {
1445 int64_t numVectors = 0;
1446 for (const auto &l : sourceElementRanges)
1447 numVectors = std::max(numVectors, (int64_t)l.size());
1448 return numVectors;
1449 }
1450
1451 VectorType sourceVectorType;
1452 VectorType targetVectorType;
1453 SmallVector<SourceElementRangeList> sourceElementRanges;
1454};
1455
1456/// Rewrite vector.bitcast to a sequence of shuffles and bitwise ops that take
1457/// advantage of high-level information to avoid leaving LLVM to scramble with
1458/// peephole optimizations.
1459/// BitCastBitsEnumerator encodes for each element of the target vector the
1460/// provenance of the bits in the source vector. We can "transpose" this
1461/// information to build a sequence of shuffles and bitwise ops that will
1462/// produce the desired result.
1463//
1464/// Consider the following motivating example:
1465/// ```
1466/// %1 = vector.bitcast %0 : vector<32xi5> to vector<20xi8>
1467/// ```
1468//
1469/// BitCastBitsEnumerator contains the following information:
1470/// ```
1471/// { 0: b@[0..5) lshl: 0}{ 1: b@[0..3) lshl: 5}
1472/// { 1: b@[3..5) lshl: 0}{ 2: b@[0..5) lshl: 2}{ 3: b@[0..1) lshl: 7}
1473/// { 3: b@[1..5) lshl: 0}{ 4: b@[0..4) lshl: 4}
1474/// { 4: b@[4..5) lshl: 0}{ 5: b@[0..5) lshl: 1}{ 6: b@[0..2) lshl: 6}
1475/// { 6: b@[2..5) lshl: 0}{ 7: b@[0..5) lshl: 3}
1476/// { 8: b@[0..5) lshl: 0}{ 9: b@[0..3) lshl: 5}
1477/// { 9: b@[3..5) lshl: 0}{10: b@[0..5) lshl: 2}{11: b@[0..1) lshl: 7}
1478/// {11: b@[1..5) lshl: 0}{12: b@[0..4) lshl: 4}
1479/// {12: b@[4..5) lshl: 0}{13: b@[0..5) lshl: 1}{14: b@[0..2) lshl: 6}
1480/// {14: b@[2..5) lshl: 0}{15: b@[0..5) lshl: 3}
1481/// {16: b@[0..5) lshl: 0}{17: b@[0..3) lshl: 5}
1482/// {17: b@[3..5) lshl: 0}{18: b@[0..5) lshl: 2}{19: b@[0..1) lshl: 7}
1483/// {19: b@[1..5) lshl: 0}{20: b@[0..4) lshl: 4}
1484/// {20: b@[4..5) lshl: 0}{21: b@[0..5) lshl: 1}{22: b@[0..2) lshl: 6}
1485/// {22: b@[2..5) lshl: 0}{23: b@[0..5) lshl: 3}
1486/// {24: b@[0..5) lshl: 0}{25: b@[0..3) lshl: 5}
1487/// {25: b@[3..5) lshl: 0}{26: b@[0..5) lshl: 2}{27: b@[0..1) lshl: 7}
1488/// {27: b@[1..5) lshl: 0}{28: b@[0..4) lshl: 4}
1489/// {28: b@[4..5) lshl: 0}{29: b@[0..5) lshl: 1}{30: b@[0..2) lshl: 6}
1490/// {30: b@[2..5) lshl: 0}{31: b@[0..5) lshl: 3}
1491/// ```
1492///
1493/// In the above, each row represents one target vector element and each
1494/// column represents one bit contribution from a source vector element.
1495/// The algorithm creates vector.shuffle operations (in this case there are 3
1496/// shuffles (i.e. the max number of columns in BitCastBitsEnumerator). The
1497/// algorithm populates the bits as follows:
1498/// ```
1499/// src bits 0 ...
1500/// 1st shuffle |xxxxx |xx |...
1501/// 2nd shuffle | xxx| xxxxx |...
1502/// 3rd shuffle | | x|...
1503/// ```
1504//
1505/// The algorithm proceeds as follows:
1506/// 1. for each vector.shuffle, collect the source vectors that participate in
1507/// this shuffle. One source vector per target element of the resulting
1508/// vector.shuffle. If there is no source element contributing bits for the
1509/// current vector.shuffle, take 0 (i.e. row 0 in the above example has only
1510/// 2 columns).
1511/// 2. represent the bitrange in the source vector as a mask. If there is no
1512/// source element contributing bits for the current vector.shuffle, take 0.
1513/// 3. shift right by the proper amount to align the source bitrange at
1514/// position 0. This is exactly the low end of the bitrange. For instance,
1515/// the first element of row 2 is `{ 1: b@[3..5) lshl: 0}` and one needs to
1516/// shift right by 3 to get the bits contributed by the source element #1
1517/// into position 0.
1518/// 4. shift left by the proper amount to to align to the desired position in
1519/// the result element vector. For instance, the contribution of the second
1520/// source element for the first row needs to be shifted by `5` to form the
1521/// first i8 result element.
1522///
1523/// Eventually, we end up building the sequence
1524/// `(shuffle -> and -> shiftright -> shiftleft -> or)` to iteratively update
1525/// the result vector (i.e. the `shiftright -> shiftleft -> or` part) with the
1526/// bits extracted from the source vector (i.e. the `shuffle -> and` part).
1527struct BitCastRewriter {
1528 /// Helper metadata struct to hold the static quantities for the rewrite.
1529 struct Metadata {
1530 SmallVector<int64_t> shuffles;
1531 SmallVector<Attribute> masks, shiftRightAmounts, shiftLeftAmounts;
1532 };
1533
1534 BitCastRewriter(VectorType sourceVectorType, VectorType targetVectorType);
1535
1536 /// Verify that general preconditions for the rewrite are met.
1537 LogicalResult commonPrecondition(PatternRewriter &rewriter,
1538 VectorType preconditionType, Operation *op);
1539
1540 /// Precompute the metadata for the rewrite.
1541 SmallVector<BitCastRewriter::Metadata>
1542 precomputeMetadata(IntegerType shuffledElementType);
1543
1544 /// Rewrite one step of the sequence:
1545 /// `(shuffle -> and -> shiftright -> shiftleft -> or)`.
1546 Value genericRewriteStep(PatternRewriter &rewriter, Location loc,
1547 Value initialValue, Value runningResult,
1548 const BitCastRewriter::Metadata &metadata);
1549
1550private:
1551 /// Underlying enumerator that encodes the provenance of the bits in the each
1552 /// element of the result vector.
1553 BitCastBitsEnumerator enumerator;
1554};
1555
1556} // namespace
1557
1558[[maybe_unused]] static raw_ostream &
1560 for (const auto &l : vec) {
1561 for (auto it : llvm::enumerate(l)) {
1562 os << "{ " << it.value().sourceElementIdx << ": b@["
1563 << it.value().sourceBitBegin << ".." << it.value().sourceBitEnd
1564 << ") lshl: " << l.computeLeftShiftAmount(it.index()) << " } ";
1565 }
1566 os << "\n";
1567 }
1568 return os;
1569}
1570
1571BitCastBitsEnumerator::BitCastBitsEnumerator(VectorType sourceVectorType,
1572 VectorType targetVectorType)
1573 : sourceVectorType(sourceVectorType), targetVectorType(targetVectorType) {
1574
1575 assert(sourceVectorType.getRank() == 1 && !sourceVectorType.isScalable() &&
1576 "requires -D non-scalable vector type");
1577 assert(targetVectorType.getRank() == 1 && !targetVectorType.isScalable() &&
1578 "requires -D non-scalable vector type");
1579 int64_t sourceBitWidth = sourceVectorType.getElementTypeBitWidth();
1580 int64_t mostMinorSourceDim = sourceVectorType.getShape().back();
1581 LDBG() << "sourceVectorType: " << sourceVectorType;
1582
1583 int64_t targetBitWidth = targetVectorType.getElementTypeBitWidth();
1584 int64_t mostMinorTargetDim = targetVectorType.getShape().back();
1585 LDBG() << "targetVectorType: " << targetVectorType;
1586
1587 int64_t bitwidth = targetBitWidth * mostMinorTargetDim;
1588 (void)mostMinorSourceDim;
1589 assert(bitwidth == sourceBitWidth * mostMinorSourceDim &&
1590 "source and target bitwidths must match");
1591
1592 // Prepopulate one source element range per target element.
1593 sourceElementRanges = SmallVector<SourceElementRangeList>(mostMinorTargetDim);
1594 for (int64_t resultBit = 0; resultBit < bitwidth;) {
1595 int64_t resultElement = resultBit / targetBitWidth;
1596 int64_t resultBitInElement = resultBit % targetBitWidth;
1597 int64_t sourceElementIdx = resultBit / sourceBitWidth;
1598 int64_t sourceBitInElement = resultBit % sourceBitWidth;
1599 int64_t step = std::min(sourceBitWidth - sourceBitInElement,
1600 targetBitWidth - resultBitInElement);
1601 sourceElementRanges[resultElement].push_back(
1602 {sourceElementIdx, sourceBitInElement, sourceBitInElement + step});
1603 resultBit += step;
1604 }
1605}
1606
1607BitCastRewriter::BitCastRewriter(VectorType sourceVectorType,
1608 VectorType targetVectorType)
1609 : enumerator(BitCastBitsEnumerator(sourceVectorType, targetVectorType)) {
1610 LDBG() << "\n" << enumerator.sourceElementRanges;
1611}
1612
1613/// Verify that the precondition type meets the common preconditions for any
1614/// conversion.
1615static LogicalResult commonConversionPrecondition(PatternRewriter &rewriter,
1616 VectorType preconditionType,
1617 Operation *op) {
1618 if (!preconditionType || preconditionType.isScalable())
1619 return rewriter.notifyMatchFailure(op, "scalable vector");
1620
1621 // TODO: consider relaxing this restriction in the future if we find ways
1622 // to really work with subbyte elements across the MLIR/LLVM boundary.
1623 unsigned bitwidth = preconditionType.getElementTypeBitWidth();
1624 if (bitwidth % 8 != 0)
1625 return rewriter.notifyMatchFailure(op, "bitwidth is not k * 8");
1626
1627 return success();
1628}
1629
1630LogicalResult BitCastRewriter::commonPrecondition(PatternRewriter &rewriter,
1631 VectorType preconditionType,
1632 Operation *op) {
1633 if (!enumerator.sourceVectorType || !enumerator.targetVectorType)
1634 return rewriter.notifyMatchFailure(op, "types are not vector");
1635
1636 if (!preconditionType || preconditionType.getRank() != 1)
1637 return rewriter.notifyMatchFailure(op, "unsupported >1-D vector");
1638
1639 return commonConversionPrecondition(rewriter, preconditionType, op);
1640}
1641
1642/// Verify that `subByteVecTy` (vector) and `containerTy` (scalar) are aligned.
1643///
1644/// Alignment means that `subByteVecTy` can be packed into a vector of
1645/// `containerTy` elements. More specifically:
1646/// 1. The bit-width of `containerTy` is a multiple of the
1647/// bit-width of `subByteVecTy` elements. For example, for `i4` and `i16`
1648/// this multiple is 4.
1649/// 2. The multiple from 1. above divides evenly the number of the (trailing)
1650/// elements in `subByteVecTy`.
1651///
1652/// EXAMPLE 1:
1653/// `subByteVecTy = vector<2xi4>`, and
1654/// `containerTy = i16`
1655///
1656/// 2 divides evenly 4 ( = 16 / 4), hence both conditions are _met_.
1657///
1658/// EXAMPLE 2:
1659/// `subByteVecTy = vector<3xi4>`, and
1660/// `containerTy = i16`
1661///
1662/// 3 _does not_ divide evenly 4 (= 16/4), hence the conditions are _not met_.
1663///
1664/// EXAMPLE 3:
1665/// `subByteVecTy = vector<3xi3>`, and
1666/// `containerTy = i16`
1667///
1668/// 16 _is not_ a multiple of 3, hence the conditions are _not met_.
1669///
1670/// NOTE: This method assumes that common conversion preconditions are met. In
1671/// particular, `containerTy` is assumed to be a
1672/// multi-byte scalar type (e.g., i8, i16, i32).
1673static LogicalResult alignedConversionPrecondition(PatternRewriter &rewriter,
1674 VectorType subByteVecTy,
1675 Type containerTy,
1676 Operation *op) {
1677 assert(containerTy.isIntOrFloat() &&
1678 "container element type is not a scalar");
1679
1680 // TODO: This is validating the inputs rather than checking the conditions
1681 // documented above. Replace with an assert.
1682 if (!subByteVecTy)
1683 return rewriter.notifyMatchFailure(op, "not a vector!");
1684
1685 unsigned subByteBits = subByteVecTy.getElementTypeBitWidth();
1686 unsigned containerBits = containerTy.getIntOrFloatBitWidth();
1687
1688 // Enforced by the common pre-conditions.
1689 assert(containerBits % 8 == 0 && "Not a multi-byte scalar type!");
1690
1691 // TODO: Add support other widths (when/if needed)
1692 if (subByteBits != 2 && subByteBits != 4)
1693 return rewriter.notifyMatchFailure(
1694 op, "only 2-bit and 4-bit sub-byte type is supported at this moment");
1695
1696 // Condition 1 ("per-element" alignment)
1697 if (containerBits % subByteBits != 0)
1698 return rewriter.notifyMatchFailure(op, "unalagined element types");
1699
1700 // Condition 2 ("full" alignment)
1701 if (!fitsInMultiByteContainerTy(subByteVecTy, containerTy))
1702 return rewriter.notifyMatchFailure(
1703 op, "not possible to fit this sub-byte vector type into a vector of "
1704 "the given multi-byte type");
1705
1706 return success();
1707}
1708
1709SmallVector<BitCastRewriter::Metadata>
1710BitCastRewriter::precomputeMetadata(IntegerType shuffledElementType) {
1711 SmallVector<BitCastRewriter::Metadata> result;
1712 for (int64_t shuffleIdx = 0, e = enumerator.getMaxNumberOfEntries();
1713 shuffleIdx < e; ++shuffleIdx) {
1714 SmallVector<int64_t> shuffles;
1715 SmallVector<Attribute> masks, shiftRightAmounts, shiftLeftAmounts;
1716
1717 // Create the attribute quantities for the shuffle / mask / shift ops.
1718 for (auto &srcEltRangeList : enumerator.sourceElementRanges) {
1719 int64_t sourceElement = (shuffleIdx < (int64_t)srcEltRangeList.size())
1720 ? srcEltRangeList[shuffleIdx].sourceElementIdx
1721 : 0;
1722 shuffles.push_back(sourceElement);
1723
1724 int64_t bitLo = (shuffleIdx < (int64_t)srcEltRangeList.size())
1725 ? srcEltRangeList[shuffleIdx].sourceBitBegin
1726 : 0;
1727 int64_t bitHi = (shuffleIdx < (int64_t)srcEltRangeList.size())
1728 ? srcEltRangeList[shuffleIdx].sourceBitEnd
1729 : 0;
1730 IntegerAttr mask = IntegerAttr::get(
1731 shuffledElementType,
1732 llvm::APInt::getBitsSet(shuffledElementType.getIntOrFloatBitWidth(),
1733 bitLo, bitHi));
1734 masks.push_back(mask);
1735
1736 int64_t shiftRight = bitLo;
1737 shiftRightAmounts.push_back(
1738 IntegerAttr::get(shuffledElementType, shiftRight));
1739
1740 int64_t shiftLeft = srcEltRangeList.computeLeftShiftAmount(shuffleIdx);
1741 shiftLeftAmounts.push_back(
1742 IntegerAttr::get(shuffledElementType, shiftLeft));
1743 }
1744
1745 result.push_back({shuffles, masks, shiftRightAmounts, shiftLeftAmounts});
1746 }
1747 return result;
1748}
1749
1750Value BitCastRewriter::genericRewriteStep(
1751 PatternRewriter &rewriter, Location loc, Value initialValue,
1752 Value runningResult, const BitCastRewriter::Metadata &metadata) {
1753 // Create vector.shuffle from the metadata.
1754 auto shuffleOp = vector::ShuffleOp::create(rewriter, loc, initialValue,
1755 initialValue, metadata.shuffles);
1756
1757 // Intersect with the mask.
1758 VectorType shuffledVectorType = shuffleOp.getResultVectorType();
1759 auto constOp = arith::ConstantOp::create(
1760 rewriter, loc,
1761 DenseElementsAttr::get(shuffledVectorType, metadata.masks));
1762 Value andValue = arith::AndIOp::create(rewriter, loc, shuffleOp, constOp);
1763
1764 // Align right on 0.
1765 auto shiftRightConstantOp = arith::ConstantOp::create(
1766 rewriter, loc,
1767 DenseElementsAttr::get(shuffledVectorType, metadata.shiftRightAmounts));
1768 Value shiftedRight =
1769 arith::ShRUIOp::create(rewriter, loc, andValue, shiftRightConstantOp);
1770
1771 // Shift bits left into their final position.
1772 auto shiftLeftConstantOp = arith::ConstantOp::create(
1773 rewriter, loc,
1774 DenseElementsAttr::get(shuffledVectorType, metadata.shiftLeftAmounts));
1775 Value shiftedLeft =
1776 arith::ShLIOp::create(rewriter, loc, shiftedRight, shiftLeftConstantOp);
1777
1778 runningResult =
1779 runningResult
1780 ? arith::OrIOp::create(rewriter, loc, runningResult, shiftedLeft)
1781 : shiftedLeft;
1782
1783 return runningResult;
1784}
1785
1786/// Bitcasts the aligned `subByteVec` vector to a vector of i8.
1787/// Where aligned means it satisfies the alignedConversionPreconditions.
1788///
1789/// Example:
1790/// vector<16x16xi2> -> vector<16x4xi8>
1791/// vector<16x16xi4> -> vector<16x8xi8>
1793 Value subByteVec) {
1794 auto srcVecType = cast<VectorType>(subByteVec.getType());
1795 int64_t srcBitwidth = srcVecType.getElementType().getIntOrFloatBitWidth();
1796 assert(8 % srcBitwidth == 0 &&
1797 "Unsupported sub-byte type (not a divisor of i8)");
1798 int64_t numSrcElemsPerByte = 8 / srcBitwidth;
1799 SmallVector<int64_t> vecShape(srcVecType.getShape());
1800 // Adjust last dimension of the vector, so the total size remains the same.
1801 vecShape.back() = vecShape.back() / numSrcElemsPerByte;
1802 auto i8VecType = VectorType::get(vecShape, rewriter.getI8Type());
1803 return vector::BitCastOp::create(rewriter, loc, i8VecType, subByteVec);
1804}
1805
1806/// Extracts a signed N-bit sequence from each element of a vector of bytes,
1807/// starting at the specified bit index.
1808/// The `bitIdx` starts at 0 from the LSB and moves to the left.
1809///
1810/// Example for a single element:
1811/// Extract numBits=2 starting at bitIdx=2
1812/// src = [0 | 1 | 0 | 1 | 1 | 1 | 1 | 0]
1813/// indices = [7 | 6 | 5 | 4 | 3 | 2 | 1 | 0]
1814/// target = [. . . . ^ ^ . .]
1815///
1816/// The target sequence is [11](decimal=-1) as signed 2-bit integer.
1817/// So the result should be [11 11 11 11](decimal=-1) as signed 8-bit integer.
1818///
1819/// src = [01 01 11 10]
1820/// shl = arith.shl(src, 4) -> [11 10 00 00]
1821/// result = arith.shrsi(shl, 6) -> [11 11 11 11]
1823 Location loc, Value src,
1824 int bitIdx, int numBits) {
1825 auto srcType = cast<VectorType>(src.getType());
1826 Value shl = src;
1827 int8_t bitsToShiftLeft = 8 - numBits - bitIdx;
1828 assert(bitIdx >= 0 && bitsToShiftLeft >= 0 && numBits > 0 && numBits <= 8 &&
1829 "Invalid bitIdx range");
1830 if (bitsToShiftLeft != 0) {
1831 Value shiftLeftValues = arith::ConstantOp::create(
1832 rewriter, loc, DenseElementsAttr::get(srcType, bitsToShiftLeft));
1833 shl = arith::ShLIOp::create(rewriter, loc, src, shiftLeftValues);
1834 }
1835
1836 int8_t bitsToShiftRight = 8 - numBits;
1837 Value shiftRightValues = arith::ConstantOp::create(
1838 rewriter, loc, DenseElementsAttr::get(srcType, bitsToShiftRight));
1839 Value shr = arith::ShRSIOp::create(rewriter, loc, shl, shiftRightValues);
1840 return shr;
1841}
1842
1843/// Extracts an unsigned N-bit sequence from each element of a vector of bytes,
1844/// starting at the specified bit index.
1845/// The `bitIdx` starts at 0 from the LSB and moves to the left.
1846///
1847/// Example for a single element:
1848/// Extract numBits=2 starting at bitIdx=2
1849/// src = [0 | 1 | 0 | 1 | 1 | 0 | 1 | 0]
1850/// indices = [7 | 6 | 5 | 4 | 3 | 2 | 1 | 0]
1851/// target = [. . . . ^ ^ . .]
1852///
1853/// The target sequence is [10](decimal=2) as unsigned 2-bit integer.
1854/// So the result should be [00 00 00 10](decimal=2) as unsigned 8-bit integer.
1855///
1856/// src = [01 01 10 10]
1857/// mask = [00 00 00 11]
1858/// shr = arith.shrui(src, 2) = [00 01 01 10]
1859/// result = arith.andi(shr, mask) = [00 00 00 10]
1860/// NOTE: Similarly to extractNBitsPerByteAndSignExtendToI8, this could be
1861/// achieved by using arith::ShLIOp + arith::ShRUIOp instead of the masking.
1862/// However, by using arith::ShRUIOp + arith::AndIOp, we are eliminating shift
1863/// left when the index is 0.
1865 Location loc, Value src,
1866 int bitIdx, int numBits) {
1867 assert(bitIdx >= 0 && bitIdx <= 8 - numBits && numBits > 0 && numBits <= 8 &&
1868 "Invalid bitIdx range");
1869 auto srcType = cast<VectorType>(src.getType());
1870 int8_t bitsToShiftRight = bitIdx;
1871 Value shr = src;
1872 if (bitsToShiftRight != 0) {
1873 Value shiftRightValues = arith::ConstantOp::create(
1874 rewriter, loc, DenseElementsAttr::get(srcType, bitsToShiftRight));
1875 shr = arith::ShRUIOp::create(rewriter, loc, src, shiftRightValues);
1876 }
1877 if (bitIdx + numBits == 8) {
1878 return shr;
1879 }
1880 uint8_t lowBitsMask = (1 << numBits) - 1;
1881 Value lowBitsMaskValues = arith::ConstantOp::create(
1882 rewriter, loc, DenseElementsAttr::get(srcType, lowBitsMask));
1883 return arith::AndIOp::create(rewriter, loc, shr, lowBitsMaskValues);
1884}
1885
1887 std::function<Value(PatternRewriter &, Location, Value, int, int)>;
1888
1889/// Rewrite the i4 -> i8 extension into a sequence of shuffles and
1890/// bitwise ops to avoid leaving LLVM to scramble with peephole optimizations.
1892 Value srcValue, const ExtractNBitsFn &extFn) {
1893 [[maybe_unused]] auto srcVecType = cast<VectorType>(srcValue.getType());
1894 assert(srcVecType.getElementType().isSignlessInteger(4) &&
1895 "Expected i4 type");
1896
1897 // 1. Generate a bitcast vector<Xxi4> -> vector<X/2xi8>.
1898 Value i8Vector = bitcastSubByteVectorToI8(rewriter, loc, srcValue);
1899
1900 // 2. Extend i4 elements to i8 elements. Low i4 elemens of each
1901 // byte are place in one vector and the high i4 elements in another vector.
1902 Value low = extFn(rewriter, loc, i8Vector, 0, 4);
1903 Value high = extFn(rewriter, loc, i8Vector, 4, 4);
1904
1905 // 3. Interleave low and high i8 elements.
1906 return vector::InterleaveOp::create(rewriter, loc, low, high);
1907}
1908
1909/// Rewrite the i2 -> i8 extension into a sequence of shuffles and
1910/// bitwise ops to avoid leaving LLVM to scramble with peephole optimizations.
1912 Value srcValue, const ExtractNBitsFn &extFn) {
1913 [[maybe_unused]] VectorType srcVecType = cast<VectorType>(srcValue.getType());
1914 assert(srcVecType.getElementType().isSignlessInteger(2) &&
1915 "Expected i2 type");
1916
1917 // 1. Generate a bitcast vector<Xxi2> -> vector<X/2xi8>.
1918 Value i8Vector = bitcastSubByteVectorToI8(rewriter, loc, srcValue);
1919
1920 // 2. Extract each i2 element
1921 // Positon 0 (bits 0-1)
1922 Value vec0 = extFn(rewriter, loc, i8Vector, 0, 2);
1923 // Position 1 (bits 2-3)
1924 Value vec1 = extFn(rewriter, loc, i8Vector, 2, 2);
1925 // Position 2 (bits 4-5)
1926 Value vec2 = extFn(rewriter, loc, i8Vector, 4, 2);
1927 // Position 3 (bits 6-7)
1928 Value vec3 = extFn(rewriter, loc, i8Vector, 6, 2);
1929
1930 // 3. Interleave all 4 elements by first interleaving
1931 // even elements and then odd
1932 // vec0 = [0,0,0,0],...
1933 // vec1 = [1,1,1,1],...
1934 // vec2 = [2,2,2,2],...
1935 // vec3 = [3,3,3,3],...
1936 // 02 = [0,2,0,2,0,2,0,2],...
1937 // 13 = [1,3,1,3,1,3,1,3],...
1938 // 0213 = [0,1,2,3,...],...
1939 Value interleave02 = vector::InterleaveOp::create(rewriter, loc, vec0, vec2);
1940 Value interleave13 = vector::InterleaveOp::create(rewriter, loc, vec1, vec3);
1941 return vector::InterleaveOp::create(rewriter, loc, interleave02,
1942 interleave13);
1943}
1944
1945/// Rewrite the i8 -> i4 truncation into a deinterleave and series of bitwise
1946/// ops to avoid leaving LLVM to scramble with peephole optimizations.
1948 Value srcValue) {
1949 VectorType srcVecType = cast<VectorType>(srcValue.getType());
1950 assert(srcVecType.getElementType().isSignlessInteger(8) &&
1951 "Expected i8 type");
1952
1953 // 1. De-interleave low and high i8 elements.
1954 auto deinterleaveOp = vector::DeinterleaveOp::create(rewriter, loc, srcValue);
1955
1956 // 2. Zero out the upper side of each low i8 element.
1957 constexpr int8_t i8LowBitMask = 0x0F;
1958 VectorType deinterI8VecType = deinterleaveOp.getResultVectorType();
1959 Value zeroOutMask = arith::ConstantOp::create(
1960 rewriter, loc, DenseElementsAttr::get(deinterI8VecType, i8LowBitMask));
1961 Value zeroOutLow = arith::AndIOp::create(
1962 rewriter, loc, deinterleaveOp.getRes1(), zeroOutMask);
1963
1964 // 3. Move high i4 values to upper side of the byte.
1965 constexpr int8_t bitsToShift = 4;
1966 auto shiftValues = arith::ConstantOp::create(
1967 rewriter, loc, DenseElementsAttr::get(deinterI8VecType, bitsToShift));
1968 Value shlHigh = arith::ShLIOp::create(rewriter, loc, deinterleaveOp.getRes2(),
1969 shiftValues);
1970
1971 // 4. Merge high and low i4 values.
1972 auto mergedHiLowOp = arith::OrIOp::create(rewriter, loc, zeroOutLow, shlHigh);
1973
1974 // 5. Generate a bitcast vector<Xxi8> -> vector<2Xxi4>.
1975 auto i4VecType = srcVecType.cloneWith(std::nullopt, rewriter.getI4Type());
1976 return vector::BitCastOp::create(rewriter, loc, i4VecType, mergedHiLowOp);
1977}
1978
1979namespace {
1980/// Rewrite bitcast(trunci) to a sequence of shuffles and bitwise ops that take
1981/// advantage of high-level information to avoid leaving LLVM to scramble with
1982/// peephole optimizations.
1983struct RewriteBitCastOfTruncI : OpRewritePattern<vector::BitCastOp> {
1984 using Base::Base;
1985
1986 LogicalResult matchAndRewrite(vector::BitCastOp bitCastOp,
1987 PatternRewriter &rewriter) const override {
1988 // The source must be a trunc op.
1989 auto truncOp =
1990 bitCastOp.getSource().template getDefiningOp<arith::TruncIOp>();
1991 if (!truncOp)
1992 return rewriter.notifyMatchFailure(bitCastOp, "not a trunci source");
1993
1994 // Set up the BitCastRewriter and verify the precondition.
1995 VectorType sourceVectorType = bitCastOp.getSourceVectorType();
1996 VectorType targetVectorType = bitCastOp.getResultVectorType();
1997 BitCastRewriter bcr(sourceVectorType, targetVectorType);
1998 if (failed(bcr.commonPrecondition(rewriter, targetVectorType, bitCastOp)))
1999 return failure();
2000
2001 // Perform the rewrite.
2002 Value truncValue = truncOp.getIn();
2003 auto shuffledElementType =
2004 cast<IntegerType>(getElementTypeOrSelf(truncValue.getType()));
2005 Value runningResult;
2006 for (const BitCastRewriter ::Metadata &metadata :
2007 bcr.precomputeMetadata(shuffledElementType)) {
2008 runningResult = bcr.genericRewriteStep(
2009 rewriter, bitCastOp->getLoc(), truncValue, runningResult, metadata);
2010 }
2011
2012 // Finalize the rewrite.
2013 bool narrowing = targetVectorType.getElementTypeBitWidth() <=
2014 shuffledElementType.getIntOrFloatBitWidth();
2015 if (narrowing) {
2016 if (runningResult.getType() == bitCastOp.getResultVectorType()) {
2017 rewriter.replaceOp(bitCastOp, runningResult);
2018 } else {
2019 rewriter.replaceOpWithNewOp<arith::TruncIOp>(
2020 bitCastOp, bitCastOp.getResultVectorType(), runningResult);
2021 }
2022 } else {
2023 if (runningResult.getType() == bitCastOp.getResultVectorType()) {
2024 rewriter.replaceOp(bitCastOp, runningResult);
2025 } else {
2026 rewriter.replaceOpWithNewOp<arith::ExtUIOp>(
2027 bitCastOp, bitCastOp.getResultVectorType(), runningResult);
2028 }
2029 }
2030
2031 return success();
2032 }
2033};
2034} // namespace
2035
2036//===----------------------------------------------------------------------===//
2037// RewriteExtOfBitCast
2038//===----------------------------------------------------------------------===//
2039
2040namespace {
2041/// Rewrite ext{s,u}i(bitcast) to a sequence of shuffles and bitwise ops that
2042/// take advantage of high-level information to avoid leaving LLVM to scramble
2043/// with peephole optimizations.
2044template <typename ExtOpType>
2045struct RewriteExtOfBitCast : OpRewritePattern<ExtOpType> {
2046 using OpRewritePattern<ExtOpType>::OpRewritePattern;
2047
2048 RewriteExtOfBitCast(MLIRContext *context, PatternBenefit benefit)
2049 : OpRewritePattern<ExtOpType>(context, benefit) {}
2050
2051 LogicalResult matchAndRewrite(ExtOpType extOp,
2052 PatternRewriter &rewriter) const override {
2053 // The source must be a bitcast op.
2054 auto bitCastOp = extOp.getIn().template getDefiningOp<vector::BitCastOp>();
2055 if (!bitCastOp)
2056 return rewriter.notifyMatchFailure(extOp, "not a bitcast source");
2057
2058 // Set up the BitCastRewriter and verify the precondition.
2059 VectorType sourceVectorType = bitCastOp.getSourceVectorType();
2060 VectorType targetVectorType = bitCastOp.getResultVectorType();
2061 BitCastRewriter bcr(sourceVectorType, targetVectorType);
2062 if (failed(bcr.commonPrecondition(
2063 rewriter, cast<VectorType>(extOp.getOut().getType()), bitCastOp)))
2064 return failure();
2065
2066 // Perform the rewrite.
2067 Value runningResult;
2068 Value sourceValue = bitCastOp.getSource();
2069 auto shuffledElementType =
2070 cast<IntegerType>(getElementTypeOrSelf(sourceValue.getType()));
2071 for (const BitCastRewriter::Metadata &metadata :
2072 bcr.precomputeMetadata(shuffledElementType)) {
2073 runningResult = bcr.genericRewriteStep(
2074 rewriter, bitCastOp->getLoc(), sourceValue, runningResult, metadata);
2075 }
2076
2077 // Finalize the rewrite.
2078 bool narrowing =
2079 cast<VectorType>(extOp.getOut().getType()).getElementTypeBitWidth() <=
2080 shuffledElementType.getIntOrFloatBitWidth();
2081 if (narrowing) {
2082 rewriter.replaceOpWithNewOp<arith::TruncIOp>(
2083 extOp, cast<VectorType>(extOp.getOut().getType()), runningResult);
2084 } else {
2085 rewriter.replaceOpWithNewOp<ExtOpType>(
2086 extOp, cast<VectorType>(extOp.getOut().getType()), runningResult);
2087 }
2088
2089 return success();
2090 }
2091};
2092
2093/// Rewrite the i4 -> i8 part of any conversion into a sequence of shuffles and
2094/// bitwise ops that take advantage of high-level information to avoid leaving
2095/// LLVM to scramble with peephole optimizations. Templated to choose between
2096/// signed and unsigned conversions.
2097///
2098/// EXAMPLE 1 (signed):
2099/// arith.extsi %in : vector<8xi4> to vector<8xi32>
2100/// is rewriten as:
2101/// %0 = vector.bitcast %in : vector<8xi4> to vector<4xi8>
2102/// %1 = arith.shli %0, 4 : vector<4xi8>
2103/// %2 = arith.shrsi %1, 4 : vector<4xi8>
2104/// %3 = arith.shrsi %0, 4 : vector<4xi8>
2105/// %4 = vector.interleave %2, %3 : vector<4xi8> -> vector<8xi8>
2106/// %5 = arith.extsi %4 : vector<8xi8> to vector<8xi32>
2107///
2108/// EXAMPLE 2 (fp):
2109/// arith.sitofp %in : vector<8xi4> to vector<8xf32>
2110/// is rewriten as:
2111/// %0 = vector.bitcast %in : vector<8xi4> to vector<4xi8>
2112/// %1 = arith.shli %0, 4 : vector<4xi8>
2113/// %2 = arith.shrsi %1, 4 : vector<4xi8>
2114/// %3 = arith.shrsi %0, 4 : vector<4xi8>
2115/// %4 = vector.interleave %2, %3 : vector<4xi8> -> vector<8xi8>
2116/// %5 = arith.sitofp %4 : vector<8xi8> to vector<8xf32>
2117///
2118/// EXAMPLE 3 (unsigned):
2119/// arith.extui %in : vector<8xi4> to vector<8xi32>
2120/// is rewritten as:
2121/// %0 = vector.bitcast %in : vector<8xi4> to vector<4xi8>
2122/// %1 = arith.andi %0, 15 : vector<4xi8>
2123/// %2 = arith.shrui %0, 4 : vector<4xi8>
2124/// %3 = vector.interleave %1, %2 : vector<4xi8> -> vector<8xi8>
2125/// %4 = arith.extui %3 : vector<8xi8> to vector<8xi32>
2126///
2127template <typename ConversionOpType, bool isSigned>
2128struct RewriteAlignedSubByteIntExt : OpRewritePattern<ConversionOpType> {
2129 using OpRewritePattern<ConversionOpType>::OpRewritePattern;
2130
2131 LogicalResult matchAndRewrite(ConversionOpType conversionOp,
2132 PatternRewriter &rewriter) const override {
2133 // Verify the preconditions.
2134 Value srcValue = conversionOp.getIn();
2135 VectorType srcVecType = dyn_cast<VectorType>(srcValue.getType());
2136 VectorType dstVecType = dyn_cast<VectorType>(conversionOp.getType());
2137
2138 if (failed(
2139 commonConversionPrecondition(rewriter, dstVecType, conversionOp)))
2140 return failure();
2141
2142 // Check general alignment preconditions.
2144 rewriter, srcVecType,
2145 /*containerTy=*/rewriter.getI8Type(), conversionOp)))
2146 return failure();
2147
2148 // Perform the rewrite.
2149 Location loc = conversionOp.getLoc();
2150 const auto &extFn = isSigned ? extractNBitsPerByteAndSignExtendToI8
2152 Value subByteExt;
2153 switch (srcVecType.getElementType().getIntOrFloatBitWidth()) {
2154 case 2:
2155 subByteExt = rewriteI2ToI8Ext(rewriter, loc, srcValue, extFn);
2156 break;
2157 case 4:
2158 subByteExt = rewriteI4ToI8Ext(rewriter, loc, srcValue, extFn);
2159 break;
2160 default:
2161 return failure();
2162 }
2163
2164 // Finalize the rewrite. If subByteExt already has the destination type
2165 // (e.g. extsi i4->i8 where the container is i8), replace directly without
2166 // creating a new conversion op that would have identical src and dst types.
2167 if (subByteExt.getType() == conversionOp.getType())
2168 rewriter.replaceOp(conversionOp, subByteExt);
2169 else
2170 rewriter.replaceOpWithNewOp<ConversionOpType>(
2171 conversionOp, conversionOp.getType(), subByteExt);
2172 return success();
2173 }
2174};
2175
2176/// Rewrite the i8 -> i4 part of any truncation into a deinterleave and
2177/// bitwise ops that take advantage of high-level information to avoid leaving
2178/// LLVM to scramble with peephole optimizations.
2179///
2180/// For example:
2181/// arith.trunci %in : vector<8xi32> to vector<8xi4>
2182///
2183/// is rewriten as:
2184///
2185/// %cst = arith.constant dense<15> : vector<4xi8>
2186/// %cst_0 = arith.constant dense<4> : vector<4xi8>
2187/// %0, %1 = vector.deinterleave %in : vector<8xi8>, vector<8xi8>
2188/// %2 = arith.andi %0, %cst : vector<4xi8>
2189/// %3 = arith.shli %1, %cst_0 : vector<4xi8>
2190/// %4 = arith.ori %2, %3 : vector<4xi8>
2191/// %5 = vector.bitcast %4 : vector<4xi8> to vector<8xi4>
2192///
2193struct RewriteAlignedSubByteIntTrunc : OpRewritePattern<arith::TruncIOp> {
2194 using Base::Base;
2195
2196 LogicalResult matchAndRewrite(arith::TruncIOp truncOp,
2197 PatternRewriter &rewriter) const override {
2198 // Verify the preconditions.
2199 Value srcValue = truncOp.getIn();
2200 auto srcVecType = dyn_cast<VectorType>(srcValue.getType());
2201 auto dstVecType = dyn_cast<VectorType>(truncOp.getType());
2202 if (!srcVecType || !dstVecType)
2203 return failure();
2204
2205 if (failed(commonConversionPrecondition(rewriter, srcVecType, truncOp)))
2206 return failure();
2207
2208 // TODO: Add support for truncating to i2.
2209 if (dstVecType.getElementType().getIntOrFloatBitWidth() == 2)
2210 return failure();
2211
2212 // Check general alignment preconditions. We invert the src/dst type order
2213 // to reuse the existing precondition logic.
2215 rewriter, dstVecType,
2216 /*containerTy=*/rewriter.getI8Type(), truncOp)))
2217 return failure();
2218
2219 // Create a new iX -> i8 truncation op, unless the source is already i8.
2220 Location loc = truncOp.getLoc();
2221 auto i8VecType = srcVecType.cloneWith(std::nullopt, rewriter.getI8Type());
2222 Value i8TruncVal =
2223 srcVecType == i8VecType
2224 ? srcValue
2225 : arith::TruncIOp::create(rewriter, loc, i8VecType, srcValue);
2226
2227 // Rewrite the i8 -> i4 truncation part.
2228 Value subByteTrunc = rewriteI8ToI4Trunc(rewriter, loc, i8TruncVal);
2229
2230 // Finalize the rewrite.
2231 rewriter.replaceOp(truncOp, subByteTrunc);
2232 return success();
2233 }
2234};
2235
2236/// Rewrite a sub-byte vector transpose into a sequence of instructions that
2237/// perform the transpose on wider (byte) element types.
2238///
2239/// EXAMPLE:
2240/// %0 = vector.transpose %a, [1, 0] : vector<8x16xi4> to vector<16x8xi4>
2241///
2242/// is rewritten as:
2243///
2244/// %0 = arith.extsi %arg0 : vector<8x16xi4> to vector<8x16xi8>
2245/// %1 = vector.transpose %0, [1, 0] : vector<8x16xi8> to vector<16x8xi8>
2246/// %2 = arith.trunci %1 : vector<16x8xi8> to vector<16x8xi4>
2247///
2248struct RewriteVectorTranspose : OpRewritePattern<vector::TransposeOp> {
2249 using Base::Base;
2250
2251 RewriteVectorTranspose(MLIRContext *context, PatternBenefit benefit)
2252 : OpRewritePattern<vector::TransposeOp>(context, benefit) {}
2253
2254 LogicalResult matchAndRewrite(vector::TransposeOp transposeOp,
2255 PatternRewriter &rewriter) const override {
2256 // Precondition: sub-byte integer transpose.
2257 constexpr unsigned minNativeBitwidth = 8;
2258 VectorType srcSubByteVecType = transposeOp.getSourceVectorType();
2259 if (!srcSubByteVecType.getElementType().isSignlessInteger() ||
2260 srcSubByteVecType.getElementTypeBitWidth() >= minNativeBitwidth) {
2261 return rewriter.notifyMatchFailure(transposeOp,
2262 "not a sub-byte transpose");
2263 }
2264
2265 // Perform the rewrite.
2266 Location loc = transposeOp.getLoc();
2267 // Signed/unsigned interpretation shouldn't matter here as we are just
2268 // transposing the elements and truncating them back to the original size.
2269 // TODO: Use unsigned extension (more efficient) when emulation or backend
2270 // support is available.
2271 auto srcNativeVecType = srcSubByteVecType.cloneWith(
2272 std::nullopt, rewriter.getIntegerType(minNativeBitwidth));
2273 Value extOp = arith::ExtSIOp::create(rewriter, loc, srcNativeVecType,
2274 transposeOp.getVector());
2275 Value newTranspose = vector::TransposeOp::create(
2276 rewriter, loc, extOp, transposeOp.getPermutation());
2277 VectorType dstSubByteVecType = transposeOp.getResultVectorType();
2278 rewriter.replaceOpWithNewOp<arith::TruncIOp>(transposeOp, dstSubByteVecType,
2279 newTranspose);
2280 return success();
2281 }
2282};
2283
2284} // namespace
2285
2286//===----------------------------------------------------------------------===//
2287// Public Interface Definition
2288//===----------------------------------------------------------------------===//
2289
2290// The emulated type is inferred from the converted memref type.
2291void vector::populateVectorNarrowTypeEmulationPatterns(
2292 const arith::NarrowTypeEmulationConverter &typeConverter,
2293 RewritePatternSet &patterns, bool disableAtomicRMW, bool assumeAligned) {
2294 // Populate `vector.*` conversion patterns.
2295 // TODO: #119553 support atomicity
2296 patterns.add<ConvertVectorLoad, ConvertVectorMaskedLoad,
2297 ConvertVectorMaskedStore, ConvertVectorTransferRead>(
2298 typeConverter, patterns.getContext());
2299
2300 // Populate `vector.*` store conversion patterns. The caller can choose
2301 // to avoid emitting atomic operations and reduce it to read-modify-write
2302 // sequence for stores if it is known there are no thread contentions.
2303 patterns.insert<ConvertVectorStore>(patterns.getContext(), disableAtomicRMW,
2304 assumeAligned);
2305}
2306
2307void vector::populateVectorNarrowTypeRewritePatterns(
2308 RewritePatternSet &patterns, PatternBenefit benefit) {
2309 // TODO: Document what the emulated type is.
2310 patterns.add<RewriteBitCastOfTruncI, RewriteExtOfBitCast<arith::ExtUIOp>,
2311 RewriteExtOfBitCast<arith::ExtSIOp>>(patterns.getContext(),
2312 benefit);
2313
2314 // Patterns for aligned cases. We set higher priority as they are expected to
2315 // generate better performance for aligned cases.
2316 // The container type is always i8.
2317 patterns.add<RewriteAlignedSubByteIntExt<arith::ExtSIOp, /*isSigned=*/true>,
2318 RewriteAlignedSubByteIntExt<arith::SIToFPOp, /*isSigned=*/true>,
2319 RewriteAlignedSubByteIntTrunc>(patterns.getContext(),
2320 benefit.getBenefit() + 1);
2321 // The container type is always i8.
2322 patterns
2323 .add<RewriteAlignedSubByteIntExt<arith::ExtUIOp, /*isSigned=*/false>,
2324 RewriteAlignedSubByteIntExt<arith::UIToFPOp, /*isSigned=*/false>>(
2325 patterns.getContext(), benefit.getBenefit() + 1);
2326}
2327
2328// The container type is always i8.
2329void vector::populateVectorTransposeNarrowTypeRewritePatterns(
2330 RewritePatternSet &patterns, PatternBenefit benefit) {
2331 patterns.add<RewriteVectorTranspose>(patterns.getContext(), benefit);
2332}
2333
2334void vector::populateMemRefFlattenAndVectorNarrowTypeEmulationPatterns(
2335 arith::NarrowTypeEmulationConverter &typeConverter,
2336 RewritePatternSet &patterns) {
2338 vector::populateVectorNarrowTypeEmulationPatterns(typeConverter, patterns);
2339}
return success()
static Type getElementType(Type type)
Determine the element type of type.
static Value extractSliceIntoByte(ConversionPatternRewriter &rewriter, Location loc, VectorValue vector, int64_t extractOffset, int64_t sliceNumElements, int64_t insertOffset)
Extract sliceNumElements from source vector at extractOffset, and insert it into an empty vector at i...
static Value rewriteI8ToI4Trunc(PatternRewriter &rewriter, Location loc, Value srcValue)
Rewrite the i8 -> i4 truncation into a deinterleave and series of bitwise ops to avoid leaving LLVM t...
std::function< Value(PatternRewriter &, Location, Value, int, int)> ExtractNBitsFn
TypedValue< MemRefType > MemRefValue
static VectorValue emulatedVectorLoad(OpBuilder &rewriter, Location loc, Value base, OpFoldResult linearizedIndices, int64_t numContainerElemsToLoad, Type emulatedElemTy, Type containerElemTy)
Emulate a vector load for emulatedElemTy using containerElemTy
TypedValue< VectorType > VectorValue
static FailureOr< Operation * > getCompressedMaskOp(OpBuilder &rewriter, Location loc, Value mask, int numSrcElems, int numSrcElemsPerDest, int numFrontPadElems=0)
Returns a compressed mask for the emulated vector.
static Value downcastSelectAndUpcast(OpBuilder &builder, Location loc, VectorType downcastType, VectorType upcastType, Value mask, Value trueValue, Value falseValue)
Downcast two values to downcastType, then select values based on mask, and casts the result to upcast...
static Value rewriteI4ToI8Ext(PatternRewriter &rewriter, Location loc, Value srcValue, const ExtractNBitsFn &extFn)
Rewrite the i4 -> i8 extension into a sequence of shuffles and bitwise ops to avoid leaving LLVM to s...
static Value dynamicallyInsertSubVector(RewriterBase &rewriter, Location loc, Value src, Value dest, OpFoldResult offset, int64_t numElemsToInsert)
Inserts 1-D subvector into a 1-D vector.
static Value staticallyInsertSubvector(OpBuilder &rewriter, Location loc, Value src, Value dest, int64_t offset)
Inserts 1-D subvector into a 1-D vector.
static void atomicRMW(OpBuilder &builder, Location loc, MemRefValue linearizedMemref, Value storeIdx, VectorValue valueToStore, Value mask)
Emits memref.generic_atomic_rmw op to store a subbyte-sized value to a byte in linearizedMemref,...
static Value staticallyExtractSubvector(OpBuilder &rewriter, Location loc, Value src, int64_t offset, int64_t numElemsToExtract)
Extracts 1-D subvector from a 1-D vector.
static LogicalResult commonConversionPrecondition(PatternRewriter &rewriter, VectorType preconditionType, Operation *op)
Verify that the precondition type meets the common preconditions for any conversion.
static Value dynamicallyExtractSubVector(OpBuilder &rewriter, Location loc, Value src, Value dest, OpFoldResult offset, int64_t numElemsToExtract)
Extracts 1-D subvector from a 1-D vector.
static LogicalResult alignedConversionPrecondition(PatternRewriter &rewriter, VectorType subByteVecTy, Type containerTy, Operation *op)
Verify that subByteVecTy (vector) and containerTy (scalar) are aligned.
static void nonAtomicRMW(OpBuilder &builder, Location loc, MemRefValue linearizedMemref, Value linearizedIndex, VectorValue valueToStore, Value mask)
Generate a non-atomic read-modify-write sequence for storing to the emulated type.
static Value bitcastSubByteVectorToI8(PatternRewriter &rewriter, Location loc, Value subByteVec)
Bitcasts the aligned subByteVec vector to a vector of i8.
static Value extractNBitsPerByteAndExtendToI8(PatternRewriter &rewriter, Location loc, Value src, int bitIdx, int numBits)
Extracts an unsigned N-bit sequence from each element of a vector of bytes, starting at the specified...
static Value rewriteI2ToI8Ext(PatternRewriter &rewriter, Location loc, Value srcValue, const ExtractNBitsFn &extFn)
Rewrite the i2 -> i8 extension into a sequence of shuffles and bitwise ops to avoid leaving LLVM to s...
static Value extractNBitsPerByteAndSignExtendToI8(PatternRewriter &rewriter, Location loc, Value src, int bitIdx, int numBits)
Extracts a signed N-bit sequence from each element of a vector of bytes, starting at the specified bi...
Base type for affine expression.
Definition AffineExpr.h:68
IntegerType getIntegerType(unsigned width)
Definition Builders.cpp:71
IntegerType getI4Type()
Definition Builders.cpp:61
IntegerType getI1Type()
Definition Builders.cpp:57
MLIRContext * getContext() const
Definition Builders.h:56
ArrayAttr getI64ArrayAttr(ArrayRef< int64_t > values)
Definition Builders.cpp:286
IndexType getIndexType()
Definition Builders.cpp:55
IntegerType getI8Type()
Definition Builders.cpp:63
static DenseElementsAttr get(ShapedType type, ArrayRef< Attribute > values)
Constructs a dense elements attribute from an array of element values.
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
RAII guard to reset the insertion point of the builder when destroyed.
Definition Builders.h:350
This class helps build Operations.
Definition Builders.h:209
void setInsertionPointToStart(Block *block)
Sets the insertion point to the start of the specified block.
Definition Builders.h:433
This class represents a single result from folding an operation.
This class implements the operand iterators for the Operation class.
Definition ValueRange.h:44
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
result_type_range getResultTypes()
Definition Operation.h:453
unsigned short getBenefit() const
If the corresponding pattern can match, return its benefit. If the.
A special type of RewriterBase that coordinates the application of a rewrite pattern on the current I...
RewritePatternSet & insert(ConstructorArg &&arg, ConstructorArgs &&...args)
Add an instance of each of the pattern types 'Ts' to the pattern list with the given arguments.
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.
This class coordinates the application of a rewrite on a set of IR, providing a way for clients to tr...
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
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,...
OpTy replaceOpWithNewOp(Operation *op, Args &&...args)
Replace the results of the given (original) op with a new op that is created without verification (re...
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
bool isInteger() const
Return true if this is an integer type (with the specified width).
Definition Types.cpp:58
bool isIntOrFloat() const
Return true if this is an integer (of any signedness) or a float type.
Definition Types.cpp:118
unsigned getIntOrFloatBitWidth() const
Return the bit width of an integer or a float type, assert failure on other types.
Definition Types.cpp:124
This class provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
Type getType() const
Return the type of this value.
Definition Value.h:105
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
Definition ArithOps.cpp:384
OpFoldResult makeComposedFoldedAffineApply(OpBuilder &b, Location loc, AffineMap map, ArrayRef< OpFoldResult > operands, bool composeAffineMin=false)
Constructs an AffineApplyOp that applies map to operands after composing the map with the maps of any...
std::pair< LinearizedMemRefInfo, OpFoldResult > getLinearizedMemRefOffsetAndSize(OpBuilder &builder, Location loc, int srcBits, int dstBits, OpFoldResult offset, ArrayRef< OpFoldResult > sizes, ArrayRef< OpFoldResult > strides, ArrayRef< OpFoldResult > indices={}, LinearizedDivKind sizeDivKind=LinearizedDivKind::Floor)
void populateFlattenMemrefsPatterns(RewritePatternSet &patterns)
Patterns for flattening all supported multi-dimensional memref operations into one-dimensional memref...
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:717
Value constantOne(OpBuilder &builder, Location loc, Type tp)
Generates a 1-valued constant of the given type.
Include the generated interface declarations.
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
raw_ostream & operator<<(raw_ostream &os, const AliasResult &result)
Type getElementTypeOrSelf(Type type)
Return the element type or return the type itself.
std::conditional_t< std::is_same_v< Ty, mlir::Type >, mlir::Value, detail::TypedValue< Ty > > TypedValue
If Ty is mlir::Type this will select Value instead of having a wrapper around it.
Definition Value.h:494
void bindSymbols(MLIRContext *ctx, AffineExprTy &...exprs)
Bind a list of AffineExpr references to SymbolExpr at positions: [0 .
Definition AffineExpr.h:325
llvm::TypeSwitch< T, ResultT > TypeSwitch
Definition LLVM.h:139
Value getValueOrCreateConstantIndexOp(OpBuilder &b, Location loc, OpFoldResult ofr)
Converts an OpFoldResult to a Value.
Definition Utils.cpp:114
OpFoldResult getAsOpFoldResult(Value val)
Given a value, try to extract a constant Attribute.
Eliminates variable at the specified position using Fourier-Motzkin variable elimination.