MLIR 23.0.0git
ElideReinterpretCast.cpp
Go to the documentation of this file.
1//===-ElideReinterpretCast.cpp - Expansion patterns for MemRef operations-===//
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
14#include "mlir/IR/Matchers.h"
17#include "llvm/ADT/Repeated.h"
18#include <cassert>
19#include <optional>
20
21namespace mlir {
22namespace memref {
23#define GEN_PASS_DEF_ELIDEREINTERPRETCASTPASS
24#include "mlir/Dialect/MemRef/Transforms/Passes.h.inc"
25} // namespace memref
26} // namespace mlir
27
28using namespace mlir;
30namespace {
31
32/// Returns true if `rc` represents a scalar view (all sizes == 1)
33/// into a memref that has exactly one non-unit dimension located at
34/// either the first or last position (i.e. a "row" or "column").
35///
36/// Examples that return true:
37///
38/// // Row-major slice (last dim is non-unit)
39/// memref.reinterpret_cast %buff to offset: [%off],
40/// sizes: [1, 1, 1], strides: [1, 1, 1]
41/// : memref<1x1x8xi32> to memref<1x1x1xi32>
42///
43/// // Column-major slice (first dim is non-unit)
44/// memref.reinterpret_cast %buff to offset: [%off],
45/// sizes: [1, 1], strides: [1, 1]
46/// : memref<2x1xf32> to memref<1x1xf32>
47///
48/// // Random strides
49/// memref.reinterpret_cast %buff to offset: [%off],
50/// sizes: [1, 1], strides: [10, 100]
51/// : memref<2x1xf32, strided<[10, 100]>>
52/// to memref<1x1xf32>
53///
54/// // Rank-1 case
55/// memref.reinterpret_cast %buf to offset: [%off],
56/// sizes: [1], strides: [1]
57/// : memref<8xi32> to memref<1xi32>
58///
59/// Examples that return false:
60///
61/// // More non-unit dims
62/// memref.reinterpret_cast %buff to offset: [%off],
63/// sizes: [1, 1, 1], strides: [1, 1, 1]
64/// : memref<1x2x8xi32> to memref<1x1x1xi32>
65///
66/// // View is not scalar (size != 1)
67/// memref.reinterpret_cast %buff to offset: [%off],
68/// sizes: [2, 1], strides: [1, 1]
69/// : memref<1x2xf32> to memref<2x1xf32>
70///
71/// // Base has non-identity layout
72/// %buff = memref.alloc() : memref<1x2xf32, strided<[1, 3]>>
73/// memref.reinterpret_cast %buff to offset: [%off],
74/// sizes: [1, 1], strides: [1, 1]
75/// : memref<1x2xf32, strided<[1, 3]>> to memref<1x1xf32>
76static bool isScalarSlice(memref::ReinterpretCastOp rc) {
77 auto rcInputTy = dyn_cast<MemRefType>(rc.getSource().getType());
78 auto rcOutputTy = dyn_cast<MemRefType>(rc.getType());
79
80 // Reject strided base - logic for computing linear idx is TODO
81 if (!rcInputTy.getLayout().isIdentity())
82 return false;
84 // Reject non-matching ranks
85 unsigned srcRank = rcInputTy.getRank();
86 if (srcRank != rcOutputTy.getRank())
87 return false;
88
89 ArrayRef<int64_t> sizes = rc.getStaticSizes();
90
91 // View must be scalar: memref<1x...x1>
92 if (!llvm::all_of(rcOutputTy.getShape(),
93 [](int64_t dim) { return dim == 1; }))
94 return false;
95
96 // Sizes must all be statically 1
97 if (!llvm::all_of(sizes, [](int64_t size) {
98 return !ShapedType::isDynamic(size) && size == 1;
99 }))
100 return false;
101
102 // Rank-1 special case
103 if (srcRank == 1) {
104 // Reject non-scalar output
105 if (rcOutputTy.getDimSize(0) > 1)
106 return false;
107 }
108
109 int nonUnitCount =
110 std::count_if(rcInputTy.getShape().begin(), rcInputTy.getShape().end(),
111 [](int dim) { return dim != 1; });
112 return nonUnitCount == 1;
113}
114
115/// Rewrites `memref.copy` of a 1-element MemRef as a scalar load-store pair
116///
117/// The pattern matches a reinterpret_cast that creates a scalar view
118/// (`sizes = [1, ..., 1]`) into a memref with a single non-unit dimension.
119/// Since the view contains only one element, the accessed address is
120/// determined solely by the base pointer and the offset.
121///
122/// Two layouts are supported:
123/// * row-major slice (stride pattern [N, ..., 1])
124/// * column-major slice (stride pattern [1, ..., N])
125///
126/// BEFORE (row-major slice)
127/// %view = memref.reinterpret_cast %base
128/// to offset: [%off], sizes: [1, ..., 1], strides: [N, ..., 1]
129/// : memref<1x...xNxf32>
130/// to memref<1x...x1xf32, strided<[N, ..., 1], offset: ?>>
131/// memref.copy %src, %view
132/// : memref<1x...x1xf32>
133/// to memref<1x...x1xf32, strided<[N, ..., 1], offset: ?>>
134///
135/// AFTER
136/// %c0 = arith.constant 0 : index
137/// %v = memref.load %src[%c0, ..., %c0] : memref<1x...x1xf32>
138/// memref.store %v, %base[%c0, ..., %off] : memref<1x...xNxf32>
139///
140/// BEFORE (column-major slice)
141/// %view = memref.reinterpret_cast %base
142/// to offset: [%off], sizes: [1, ..., 1], strides: [1, ..., N]
143/// : memref<Nx...x1xf32>
144/// to memref<1x...x1xf32, strided<[1, ..., N], offset: ?>>
145/// memref.copy %src, %view
146/// : memref<1x...x1xf32>
147/// to memref<1x...x1xf32, strided<[1, ..., N], offset: ?>>
148///
149/// AFTER
150/// %c0 = arith.constant 0 : index
151/// %v = memref.load %src[%c0, ..., %c0] : memref<1x...x1xf32>
152/// memref.store %v, %base[%off, ..., %c0] : memref<Nx...x1xf32>
153struct CopyToScalarLoadAndStore : public OpRewritePattern<memref::CopyOp> {
154public:
156
157 LogicalResult matchAndRewrite(memref::CopyOp op,
158 PatternRewriter &rewriter) const final {
159 Value rcOutput = op.getTarget();
160 auto rc = rcOutput.getDefiningOp<memref::ReinterpretCastOp>();
161 if (!rc)
162 return rewriter.notifyMatchFailure(
163 op, "target is not a memref.reinterpret_cast");
164
165 if (!isScalarSlice(rc))
166 return rewriter.notifyMatchFailure(
167 op, "reinterpret_cast does not match scalar slice");
168
169 Location loc = op.getLoc();
170
171 Value src = op.getSource();
172 Value dst = rc.getSource();
173
174 auto dstType = cast<MemRefType>(dst.getType());
175 unsigned dstRank = dstType.getRank();
176
177 Value zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
178
179 auto srcType = cast<MemRefType>(src.getType());
180 Repeated<Value> loadIndices(srcType.getRank(), zero);
181 auto offsets = rc.getMixedOffsets();
182 assert(offsets.size() == 1 && "Expecting single offset");
183 OpFoldResult offset = offsets[0];
184 Value storeOffset = getValueOrCreateConstantIndexOp(rewriter, loc, offset);
185 unsigned offsetDim = dstType.getDimSize(0) == 1 ? dstRank - 1 : 0;
186 SmallVector<Value> storeIndices(dstRank, zero);
187 storeIndices[offsetDim] = storeOffset;
188 // If the only user of `rc` is the current Op (which is about to be erased),
189 // we can safely erase it.
190 if (rcOutput.hasOneUse())
191 rewriter.eraseOp(rc);
192
193 Value val = memref::LoadOp::create(rewriter, loc, src, loadIndices);
194 memref::StoreOp::create(rewriter, loc, val, dst, storeIndices);
195
196 rewriter.eraseOp(op);
197 return success();
198 }
199};
200
201//===----------------------------------------------------------------------===//
202// Load Rewrite Helpers
203//===----------------------------------------------------------------------===//
204
205static bool hasStaticZeroOffset(memref::ReinterpretCastOp rc) {
206 ArrayRef<int64_t> offsets = rc.getStaticOffsets();
207 // FIXME: Despite what `getStaticOffsets` implies, `reinterpret_cast` takes
208 // only a single offset. That should be fixed at the op definition level.
209 assert(offsets.size() == 1 && "Expecting single offset");
210 return !ShapedType::isDynamic(offsets[0]) && offsets[0] == 0;
211}
212
213static std::optional<int64_t> getConstantIndex(Value v) {
214 if (auto cst = v.getDefiningOp<arith::ConstantIndexOp>())
215 return cst.value();
216 // Non-constant and dynamic indices
217 return std::nullopt;
218}
219
220/// Return true if input index is in bounds, i.e. `0 <= idx < upperBound`.
221/// Fully dynamic index values (i.e. non-constant) that cannot be analysed are
222/// treated as in-bounds.
223static bool isConstantIndexExplicitlyOutOfBounds(Value idx,
224 int64_t upperBound) {
225 // Only statically known `arith.constant` indices are checked here.
226 std::optional<int64_t> idxVal = getConstantIndex(idx);
227 return idxVal && (*idxVal < 0 || *idxVal >= upperBound);
228}
229
230using NonUnitDimMapping = SmallVector<std::pair<int64_t, int64_t>>;
231
232/// Shape restriction accepting only unit-dim insertion/removal
233/// reinterpret_casts.
234///
235/// Examples accepted:
236/// memref<1x1x1x108xf32> <-> memref<1x108xf32>
237/// memref<100x1xf32> <-> memref<100x1x1xf32>
238/// memref<1x33x40xf32> <-> memref<33x1x1x40xf32>
239/// memref<1> <-> memref<1x1x1>
240///
241/// Returns the mapping of non-unit dimensions from the source
242/// to the result MemRef if the reinterpret_cast preserved sizes and order (no
243/// transposition) of these dimensions.
244static std::optional<NonUnitDimMapping>
245getNonUnitDimMapping(memref::ReinterpretCastOp rc) {
246 auto inputTy = cast<MemRefType>(rc.getSource().getType());
247 auto outputTy = cast<MemRefType>(rc.getResult().getType());
248
249 // Only zero, statically known offsets are accepted. Non-zero or dynamic
250 // offsets would require reasoning about storage shifts in the underlying
251 // reinterpret_cast, which this helper does not model.
252 if (!hasStaticZeroOffset(rc))
253 return std::nullopt;
254
255 // Dynamic sizes/strides prevent precise reasoning about the underlying
256 // reinterpret_cast, so only fully static shape metadata is accepted.
257 if (llvm::any_of(rc.getStaticSizes(), ShapedType::isDynamic) ||
258 llvm::any_of(rc.getStaticStrides(), ShapedType::isDynamic))
259 return std::nullopt;
260
261 ArrayRef<int64_t> inputShape = inputTy.getShape();
262 ArrayRef<int64_t> outputShape = outputTy.getShape();
263 int64_t inputDim = 0;
264 int64_t outputDim = 0;
265 int64_t inputRank = inputTy.getRank();
266 int64_t outputRank = outputTy.getRank();
267 NonUnitDimMapping mapping;
268
269 // The preserved non-unit dimensions must have the same static sizes and
270 // appear in the same order.
271 while (inputDim < inputRank || outputDim < outputRank) {
272 if (inputDim < inputRank && inputShape[inputDim] == 1) {
273 ++inputDim;
274 continue;
275 }
276 if (outputDim < outputRank && outputShape[outputDim] == 1) {
277 ++outputDim;
278 continue;
279 }
280
281 if (inputDim == inputRank || outputDim == outputRank)
282 return std::nullopt;
283
284 if (ShapedType::isDynamic(inputShape[inputDim]) ||
285 ShapedType::isDynamic(outputShape[outputDim]) ||
286 inputShape[inputDim] != outputShape[outputDim])
287 return std::nullopt;
288
289 mapping.push_back({inputDim, outputDim});
290 ++inputDim;
291 ++outputDim;
292 }
293 return mapping;
294}
295
296/// Checks statically known and constant indices accessed by a load from a
297/// unit-dim insertion/removal reinterpret_cast to ensure in-bounds only access.
298/// Fully dynamic indices are skipped (there is no way to verify them).
299[[maybe_unused]] static bool areIndicesInBounds(memref::LoadOp load) {
300 auto rc = load.getMemRef().getDefiningOp<memref::ReinterpretCastOp>();
301 auto rcOutputTy = cast<MemRefType>(rc.getResult().getType());
302
303 for (auto [pos, idx] : llvm::enumerate(load.getIndices())) {
304 // FIXME: This should be ensured by the memref.load semantics.
305 // In the long term, this sanity-check may live in the same debug-only
306 // checks as `MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS`. This rejects
307 // only explicit constant OOB indices. Dynamic/non-constant indices are not
308 // filtered here.
309 if (isConstantIndexExplicitlyOutOfBounds(idx, rcOutputTy.getDimSize(pos)))
310 return false;
311 }
312 return true;
313}
314
315/// Rewrites `memref.load` through a reinterpret_cast that only inserts/removes
316/// unit dimensions by mapping the load indices directly onto the source MemRef.
317///
318/// Shape restriction gated by getNonUnitDimMapping().
319///
320/// BEFORE (rank expansion)
321/// %view = memref.reinterpret_cast %src
322/// : memref<1xNxMxf32> to memref<Nx1x1xMxf32>
323/// %v = memref.load %view[%i, %c0, %c0, %j] : memref<Nx1x1xMxf32>
324///
325/// AFTER
326/// %v = memref.load %src[%c0, %i, %j] : memref<1xNxMxf32>
327///
328/// BEFORE (rank collapsing)
329/// %view = memref.reinterpret_cast %src
330/// : memref<Nx1x1xMxf32> to memref<1xNxMxf32>
331/// %v = memref.load %view[%c0, %i, %j] : memref<1xNxMxf32>
332///
333/// AFTER
334/// %v = memref.load %src[%i, %c0, %c0, %j] : memref<Nx1x1xMxf32>
335struct RewriteLoadFromReinterpretCast
336 : public OpRewritePattern<memref::LoadOp> {
337public:
339
340 LogicalResult matchAndRewrite(memref::LoadOp op,
341 PatternRewriter &rewriter) const override {
342 auto rc = op.getMemRef().getDefiningOp<memref::ReinterpretCastOp>();
343 if (!rc)
344 return rewriter.notifyMatchFailure(
345 op, "target is not a memref.reinterpret_cast");
346 std::optional<NonUnitDimMapping> dimMapping = getNonUnitDimMapping(rc);
347 if (!dimMapping)
348 return rewriter.notifyMatchFailure(
349 op, "reinterpret_cast is not a unit-dim insertion/removal preserving "
350 "non-unit dimensions");
351
352 assert(areIndicesInBounds(op) &&
353 "load from reinterpret_cast indexes out of bounds!");
354
355 auto rcInputTy = cast<MemRefType>(rc.getSource().getType());
356
357 int64_t rcInputRank = rcInputTy.getRank();
358
359 SmallVector<Value> oldIdxs(op.getIndices().begin(), op.getIndices().end());
360
361 // Prefer reusing an explicit constant-zero index from the old load.
362 Value zeroIndex;
363 for (Value idx : oldIdxs) {
364 std::optional<int64_t> idxVal = getConstantIndex(idx);
365 if (idxVal && *idxVal == 0) {
366 zeroIndex = idx;
367 break;
368 }
369 }
370 if (!zeroIndex)
371 zeroIndex = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 0);
372
373 // Initialize new load indices to all 0s.
374 SmallVector<Value> rcInputIdxs(rcInputRank, zeroIndex);
375 for (auto [inputDim, outputDim] : *dimMapping)
376 rcInputIdxs[inputDim] = oldIdxs[outputDim];
377
378 auto rcInput = rc.getSource();
379 // If the only user of rc is the current Op (which is about to be erased),
380 // we can safely erase it.
381 if (rc.getResult().hasOneUse())
382 rewriter.eraseOp(rc);
383 rewriter.replaceOpWithNewOp<memref::LoadOp>(op, rcInput, rcInputIdxs);
384 return success();
385 }
386};
387
388struct ElideReinterpretCastPass
390 ElideReinterpretCastPass> {
391 void runOnOperation() override {
392 MLIRContext &ctx = getContext();
393
394 RewritePatternSet patterns(&ctx);
396 ConversionTarget target(ctx);
397 target.addDynamicallyLegalOp<memref::CopyOp>([](memref::CopyOp op) {
398 auto rc = op.getTarget().getDefiningOp<memref::ReinterpretCastOp>();
399 if (!rc)
400 return true;
401 return !isScalarSlice(rc);
402 });
403 target.addDynamicallyLegalOp<memref::LoadOp>([](memref::LoadOp op) {
404 auto rc = op.getMemRef().getDefiningOp<memref::ReinterpretCastOp>();
405 if (!rc)
406 return true;
407 return !getNonUnitDimMapping(rc);
408 });
409 target.addLegalDialect<arith::ArithDialect, memref::MemRefDialect>();
410 if (failed(applyPartialConversion(getOperation(), target,
411 std::move(patterns))))
412 signalPassFailure();
413 }
414};
415
416} // namespace
417
419 RewritePatternSet &patterns) {
420 patterns.add<CopyToScalarLoadAndStore, RewriteLoadFromReinterpretCast>(
421 patterns.getContext());
422}
return success()
b getContext())
auto load
Location getLoc()
The source location the operation was defined or derived from.
Definition Operation.h:240
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.
virtual void eraseOp(Operation *op)
This method erases an operation that is known to have no uses.
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...
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
bool hasOneUse() const
Returns true if this value has exactly one use.
Definition Value.h:197
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
Specialization of arith.constant op that returns an integer of index type.
Definition Arith.h:114
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
Definition ArithOps.cpp:384
void populateElideReinterpretCastPatterns(RewritePatternSet &patterns)
Collects a set of patterns that bypass memref.reinterpet_cast Ops.
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:717
Include the generated interface declarations.
Value getValueOrCreateConstantIndexOp(OpBuilder &b, Location loc, OpFoldResult ofr)
Converts an OpFoldResult to a Value.
Definition Utils.cpp:114
OpRewritePattern is a wrapper around RewritePattern that allows for matching and rewriting against an...
OpRewritePattern(MLIRContext *context, PatternBenefit benefit=1, ArrayRef< StringRef > generatedNames={})
Patterns must specify the root operation name they match against, and can also specify the benefit of...