MLIR-AIE 1.4.5.dev
CopyRemoval.cpp
Go to the documentation of this file.
1// Copyright (C) 2023-2025 Advanced Micro Devices, Inc.
2// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
3
5
6#include "mlir/Dialect/MemRef/IR/MemRef.h"
7#include "mlir/Interfaces/SideEffectInterfaces.h"
8#include "mlir/Pass/Pass.h"
9
10using namespace mlir;
11using namespace MemoryEffects;
12
13namespace {
14
15//===----------------------------------------------------------------------===//
16// TODO: this pass once existed in the upstream MLIR and was removed at
17// https://reviews.llvm.org/D99172. This CopyRemoval pass was removed due to the
18// introduction of memref.clone (now bufferization.clone). bufferization.clone
19// is essentially memref.alloc+copy ops and is mostly created in
20// buffer-deallocation pass. It is introduced for better memory deallocation and
21// optimization. The canonicalization pass can optimize out some of the
22// bufferization.clone+memref.dealloc ops. If the bufferization.clone still
23// exists in the end of bufferization pass, we should call
24// "-convert-bufferization-to-memref" pass to convert it back to
25// memref.alloc+memref.copy. Hence, the original CopyRemoval pass (which
26// removes memref.alloc/copy/dealloc) is replaced by the canonicalization of
27// bufferization.clone+memref.dealloc op. However, in our case, memref.alloc()
28// is from the tensor.empty() after bufferization, memref.copy() is added in
29// buffer-results-to-out-params pass, and memref.dealloc() is added in
30// buffer-deallocation pass, which creates a pattern that the existing
31// canonicalizer cannot support. As a result, we recover this CopyRemoval pass
32// back into the canonicalization pass for Vector before lowering to AIEVec and
33// help eliminate unnecessary memory allocation after the bufferization. Note
34// that we can also try to remove copy op before the bufferization. This will
35// require the function to be converted to destination-passing style code at
36// tensor level, which isn't fully supported in the existing MLIR environment
37// yet. Once the upstream MLIR has a mature support on this, removing copy op
38// before bufferization can be another direction to pursue.
39//===----------------------------------------------------------------------===//
40
41//===----------------------------------------------------------------------===//
42// CopyRemovalPass
43//===----------------------------------------------------------------------===//
44
45/// This pass removes the redundant Copy operations. Additionally, it
46/// removes the leftover definition and deallocation operations by erasing the
47/// copy operation.
48class CopyRemovalPass : public PassWrapper<CopyRemovalPass, OperationPass<>> {
49public:
50 MLIR_DEFINE_EXPLICIT_INTERNAL_INLINE_TYPE_ID(CopyRemovalPass)
51
52 void runOnOperation() override {
53 getOperation()->walk([&](memref::CopyOp copyOp) {
54 reuseCopySourceAsTarget(copyOp);
55 reuseCopyTargetAsSource(copyOp);
56 });
57 for (std::pair<Value, Value> &pair : replaceList)
58 pair.first.replaceAllUsesWith(pair.second);
59 for (Operation *op : eraseList)
60 op->erase();
61 }
62
63private:
64 /// List of operations that need to be removed.
65 llvm::SmallPtrSet<Operation *, 4> eraseList;
66
67 /// List of values that need to be replaced with their counterparts.
68 llvm::SmallDenseSet<std::pair<Value, Value>, 4> replaceList;
69
70 /// Returns the allocation operation for `value` in `block` if it exists.
71 /// nullptr otherwise.
72 Operation *getAllocationOpInBlock(Value value, Block *block) {
73 assert(block && "Block cannot be null");
74 Operation *op = value.getDefiningOp();
75 if (op && op->getBlock() == block) {
76 auto effects = dyn_cast<MemoryEffectOpInterface>(op);
77 if (effects && effects.hasEffect<Allocate>())
78 return op;
79 }
80 return nullptr;
81 }
82
83 /// Returns the deallocation operation for `value` in `block` if it exists.
84 /// nullptr otherwise.
85 Operation *getDeallocationOpInBlock(Value value, Block *block) {
86 assert(block && "Block cannot be null");
87 auto valueUsers = value.getUsers();
88 auto it = llvm::find_if(valueUsers, [&](Operation *op) {
89 auto effects = dyn_cast<MemoryEffectOpInterface>(op);
90 return effects && op->getBlock() == block && effects.hasEffect<Free>();
91 });
92 return (it == valueUsers.end() ? nullptr : *it);
93 }
94
95 /// Returns true if an operation between start and end operations has memory
96 /// effect.
97 bool hasMemoryEffectOpBetween(Operation *start, Operation *end) {
98 assert((start || end) && "Start and end operations cannot be null");
99 assert(start->getBlock() == end->getBlock() &&
100 "Start and end operations should be in the same block.");
101 Operation *op = start->getNextNode();
102 while (op->isBeforeInBlock(end)) {
103 if (isa<MemoryEffectOpInterface>(op))
104 return true;
105 op = op->getNextNode();
106 }
107 return false;
108 };
109
110 /// Returns true if `val` value has at least a user between `start` and
111 /// `end` operations.
112 bool hasUsersBetween(Value val, Operation *start, Operation *end) {
113 assert((start || end) && "Start and end operations cannot be null");
114 Block *block = start->getBlock();
115 assert(block == end->getBlock() &&
116 "Start and end operations should be in the same block.");
117 return llvm::any_of(val.getUsers(), [&](Operation *op) {
118 return op->getBlock() == block && start->isBeforeInBlock(op) &&
119 op->isBeforeInBlock(end);
120 });
121 };
122
123 bool areOpsInTheSameBlock(ArrayRef<Operation *> operations) {
124 assert(!operations.empty() &&
125 "The operations list should contain at least a single operation");
126 Block *block = operations.front()->getBlock();
127 return llvm::none_of(
128 operations, [&](Operation *op) { return block != op->getBlock(); });
129 }
130
131 /// Input:
132 /// func(){
133 /// %from = alloc()
134 /// write_to(%from)
135 /// %to = alloc()
136 /// copy(%from,%to)
137 /// dealloc(%from)
138 /// return %to
139 /// }
140 ///
141 /// Output:
142 /// func(){
143 /// %from = alloc()
144 /// write_to(%from)
145 /// return %from
146 /// }
147 /// Constraints:
148 /// 1) %to, copy and dealloc must all be defined and lie in the same block.
149 /// 2) This transformation cannot be applied if there is a single user/alias
150 /// of `to` value between the defining operation of `to` and the copy
151 /// operation.
152 /// 3) This transformation cannot be applied if there is a single user/alias
153 /// of `from` value between the copy operation and the deallocation of `from`.
154 /// TODO: Alias analysis is not available at the moment. Currently, we check
155 /// if there are any operations with memory effects between copy and
156 /// deallocation operations.
157 void reuseCopySourceAsTarget(memref::CopyOp copyOp) {
158 if (eraseList.count(copyOp))
159 return;
160
161 Value from = copyOp.getSource();
162 Value to = copyOp.getTarget();
163
164 Operation *copy = copyOp.getOperation();
165 Block *copyBlock = copy->getBlock();
166 Operation *fromDefiningOp = from.getDefiningOp();
167 Operation *fromFreeingOp = getDeallocationOpInBlock(from, copyBlock);
168 Operation *toDefiningOp = getAllocationOpInBlock(to, copyBlock);
169 if (!fromDefiningOp || !fromFreeingOp || !toDefiningOp ||
170 !areOpsInTheSameBlock({fromFreeingOp, toDefiningOp, copy}) ||
171 hasUsersBetween(to, toDefiningOp, copy) ||
172 hasUsersBetween(from, copy, fromFreeingOp) ||
173 hasMemoryEffectOpBetween(copy, fromFreeingOp))
174 return;
175
176 replaceList.insert({to, from});
177 eraseList.insert(copy);
178 eraseList.insert(toDefiningOp);
179 eraseList.insert(fromFreeingOp);
180 }
181
182 /// Input:
183 /// func(){
184 /// %to = alloc()
185 /// %from = alloc()
186 /// write_to(%from)
187 /// copy(%from,%to)
188 /// dealloc(%from)
189 /// return %to
190 /// }
191 ///
192 /// Output:
193 /// func(){
194 /// %to = alloc()
195 /// write_to(%to)
196 /// return %to
197 /// }
198 /// Constraints:
199 /// 1) %from, copy and dealloc must all be defined and lie in the same block.
200 /// 2) This transformation cannot be applied if there is a single user/alias
201 /// of `to` value between the defining operation of `from` and the copy
202 /// operation.
203 /// 3) This transformation cannot be applied if there is a single user/alias
204 /// of `from` value between the copy operation and the deallocation of `from`.
205 /// TODO: Alias analysis is not available at the moment. Currently, we check
206 /// if there are any operations with memory effects between copy and
207 /// deallocation operations.
208 void reuseCopyTargetAsSource(memref::CopyOp copyOp) {
209 if (eraseList.count(copyOp))
210 return;
211
212 Value from = copyOp.getSource();
213 Value to = copyOp.getTarget();
214
215 Operation *copy = copyOp.getOperation();
216 Block *copyBlock = copy->getBlock();
217 Operation *fromDefiningOp = getAllocationOpInBlock(from, copyBlock);
218 Operation *fromFreeingOp = getDeallocationOpInBlock(from, copyBlock);
219 if (!fromDefiningOp || !fromFreeingOp ||
220 !areOpsInTheSameBlock({fromFreeingOp, fromDefiningOp, copy}) ||
221 hasUsersBetween(to, fromDefiningOp, copy) ||
222 hasUsersBetween(from, copy, fromFreeingOp) ||
223 hasMemoryEffectOpBetween(copy, fromFreeingOp))
224 return;
225
226 replaceList.insert({from, to});
227 eraseList.insert(copy);
228 eraseList.insert(fromDefiningOp);
229 eraseList.insert(fromFreeingOp);
230 }
231};
232
233} // end anonymous namespace
234
235//===----------------------------------------------------------------------===//
236// CopyRemovalPass construction
237//===----------------------------------------------------------------------===//
238
239std::unique_ptr<::mlir::Pass> xilinx::aievec::createCopyRemovalPass() {
240 return std::make_unique<CopyRemovalPass>();
241}
std::unique_ptr<::mlir::Pass > createCopyRemovalPass()
Create a pass that removes unnecessary Copy operations.