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