MLIR 24.0.0git
ACCIfClauseLowering.cpp
Go to the documentation of this file.
1//===- ACCIfClauseLowering.cpp - Lower ACC compute construct if clauses --===//
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 pass lowers OpenACC compute constructs (parallel, kernels, serial) with
10// `if` clauses using region specialization. It creates two execution paths:
11// device execution when the condition is true, host execution when false.
12//
13// Overview:
14// ---------
15// When an ACC compute construct has an `if` clause, the construct should only
16// execute on the device when the condition is true. If the condition is false,
17// the code should execute on the host instead. This pass transforms:
18//
19// acc.parallel if(%cond) { ... }
20//
21// Into:
22//
23// scf.if %cond {
24// // Device path: clone data ops, compute construct without if, exit ops
25// acc.parallel { ... }
26// } else {
27// // Host path: original region body with ACC ops converted to host
28// }
29//
30// Transformations:
31// ----------------
32// For each compute construct with an `if` clause:
33//
34// 1. Device Path (true branch):
35// - Clone data entry operations (acc.copyin, acc.create, etc.)
36// - Clone the compute construct without the `if` clause
37// - Clone data exit operations (acc.copyout, acc.delete, etc.)
38//
39// 2. Host Path (false branch):
40// - Move the original region body to the else branch
41// - Apply host fallback patterns to convert ACC ops to host equivalents
42//
43// 3. Cleanup:
44// - Erase the original compute construct and data operations
45// - Replace uses of ACC variables with host variables in the else branch
46//
47// Requirements:
48// -------------
49// To use this pass in a pipeline, the following requirements exist:
50//
51// 1. Analysis Registration (Optional): If custom behavior is needed for
52// emitting not-yet-implemented messages for unsupported cases, the pipeline
53// should pre-register the `acc::OpenACCSupport` analysis.
54//
55//===----------------------------------------------------------------------===//
56
58
64#include "mlir/IR/Builders.h"
65#include "mlir/IR/IRMapping.h"
68#include "llvm/ADT/STLExtras.h"
69#include "llvm/ADT/SetVector.h"
70#include "llvm/Support/Debug.h"
71
72namespace mlir {
73namespace acc {
74#define GEN_PASS_DEF_ACCIFCLAUSELOWERING
75#include "mlir/Dialect/OpenACC/Transforms/Passes.h.inc"
76} // namespace acc
77} // namespace mlir
78
79#define DEBUG_TYPE "acc-if-clause-lowering"
80
81using namespace mlir;
82using namespace mlir::acc;
83
84namespace {
85
86class ACCIfClauseLowering
87 : public acc::impl::ACCIfClauseLoweringBase<ACCIfClauseLowering> {
88 using ACCIfClauseLoweringBase<ACCIfClauseLowering>::ACCIfClauseLoweringBase;
89
90private:
91 OpenACCSupport *accSupport = nullptr;
92
93 void convertHostRegion(Operation *computeOp, Region &region);
94
95 template <typename OpTy>
96 void
97 lowerIfClauseForComputeConstruct(OpTy computeConstructOp,
99 llvm::SetVector<Operation *> &condEraseOps);
100
101public:
102 void runOnOperation() override;
103};
104
105void ACCIfClauseLowering::convertHostRegion(Operation *computeOp,
106 Region &region) {
107 // Only collect ACC dialect operations - other ops don't need conversion
109 region.walk<WalkOrder::PreOrder>([&](Operation *op) {
110 if (isa<acc::OpenACCDialect>(op->getDialect()))
111 hostOps.push_back(op);
112 });
113
114 RewritePatternSet patterns(computeOp->getContext());
115 populateACCHostFallbackPatterns(patterns, *accSupport);
116
117 GreedyRewriteConfig config;
118 config.setUseTopDownTraversal(true);
120 if (failed(applyOpPatternsGreedily(hostOps, std::move(patterns), config)))
121 accSupport->emitNYI(computeOp->getLoc(), "failed to convert host region");
122}
123
124// Template function to handle if condition conversion for ACC compute
125// constructs
126template <typename OpTy>
127void ACCIfClauseLowering::lowerIfClauseForComputeConstruct(
128 OpTy computeConstructOp, llvm::SetVector<Operation *> &eraseOps,
129 llvm::SetVector<Operation *> &condEraseOps) {
130 Value ifCond = computeConstructOp.getIfCond();
131 if (!ifCond)
132 return;
133
134 IRRewriter rewriter(computeConstructOp);
135
136 LLVM_DEBUG(llvm::dbgs() << "Converting " << computeConstructOp->getName()
137 << " with if condition: " << computeConstructOp
138 << "\n");
139
140 // Collect data clause operations that need to be recreated in the if
141 // condition
142 llvm::SetVector<Operation *> dataEntryOps;
144 SmallVector<Operation *> firstprivateOps;
145 SmallVector<Operation *> privateOps;
146 SmallVector<Operation *> reductionOps;
147
148 // Entries used outside this construct belong to the surrounding scope.
149 // Their entry and exit operations must remain outside the conditional.
150 auto isExternallyOwned = [&](Operation *dataOp) {
151 for (Operation *user : dataOp->getUsers())
152 if (!isa<ACC_DATA_EXIT_OPS>(user) && user != computeConstructOp &&
153 !computeConstructOp->isAncestor(user))
154 return true;
155 return false;
156 };
157
158 // Collect data entry operations
159 for (Value operand : computeConstructOp.getDataClauseOperands())
160 if (Operation *defOp = operand.getDefiningOp())
161 if (isa<ACC_DATA_ENTRY_OPS>(defOp))
162 dataEntryOps.insert(defOp);
163
164 // Find corresponding exit operations for each local entry operation. Exit ops
165 // of externally owned entry ops belong to the enclosing construct.
166 // Iterate backwards through entry ops since exit ops appear in reverse order.
167 for (Operation *dataEntryOp : llvm::reverse(dataEntryOps))
168 if (!isExternallyOwned(dataEntryOp))
169 for (Operation *user : dataEntryOp->getUsers())
170 if (isa<ACC_DATA_EXIT_OPS>(user))
171 dataExitOps.insert(user);
172
173 // Collect firstprivate, private, and reduction operations
174 auto collectOps = [&](SmallVector<Operation *> &ops, OperandRange operands) {
175 for (Value operand : operands)
176 if (Operation *defOp = operand.getDefiningOp())
177 ops.push_back(defOp);
178 };
179 collectOps(firstprivateOps, computeConstructOp.getFirstprivateOperands());
180 collectOps(privateOps, computeConstructOp.getPrivateOperands());
181 collectOps(reductionOps, computeConstructOp.getReductionOperands());
182
183 // Create scf.if with device and host execution paths
184 auto ifOp = scf::IfOp::create(rewriter, computeConstructOp.getLoc(),
185 TypeRange{}, ifCond, /*withElseRegion=*/true);
186
187 LLVM_DEBUG(llvm::dbgs() << "Cloning " << dataEntryOps.size()
188 << " data entry operations for device path\n");
189
190 // Device execution path (true branch)
191 Block &thenBlock = ifOp.getThenRegion().front();
192 rewriter.setInsertionPointToStart(&thenBlock);
193
194 // Clone data entry operations
195 SmallVector<Value> deviceDataOperands;
196 SmallVector<Value> firstprivateOperands;
197 SmallVector<Value> privateOperands;
198 SmallVector<Value> reductionOperands;
199
200 // Map the data entry and firstprivate ops for the cloned region
201 IRMapping deviceMapping;
202 auto cloneAndMapOps = [&](SmallVector<Operation *> &ops,
203 SmallVector<Value> &operands) {
204 for (Operation *op : ops) {
205 Operation *clonedOp = rewriter.clone(*op, deviceMapping);
206 operands.push_back(clonedOp->getResult(0));
207 deviceMapping.map(op->getResult(0), clonedOp->getResult(0));
208 }
209 };
210 // Clone each local data entry op once. Externally owned ops (mapped by a
211 // surrounding scope) are referenced directly.
212 for (Operation *op : dataEntryOps) {
213 if (isExternallyOwned(op))
214 continue;
215 Operation *clonedOp = rewriter.clone(*op, deviceMapping);
216 deviceMapping.map(op->getResult(0), clonedOp->getResult(0));
217 }
218 // Preserve the original operand order and multiplicity while using the
219 // cloned value for local entries and the original value for external ones.
220 for (Value operand : computeConstructOp.getDataClauseOperands())
221 deviceDataOperands.push_back(deviceMapping.lookupOrDefault(operand));
222 cloneAndMapOps(firstprivateOps, firstprivateOperands);
223 cloneAndMapOps(privateOps, privateOperands);
224 cloneAndMapOps(reductionOps, reductionOperands);
225
226 // Create new compute op without if condition for device execution by
227 // cloning
228 OpTy newComputeOp = cast<OpTy>(
229 rewriter.clone(*computeConstructOp.getOperation(), deviceMapping));
230 newComputeOp.getIfCondMutable().clear();
231 newComputeOp.getDataClauseOperandsMutable().assign(deviceDataOperands);
232 newComputeOp.getFirstprivateOperandsMutable().assign(firstprivateOperands);
233 newComputeOp.getPrivateOperandsMutable().assign(privateOperands);
234 newComputeOp.getReductionOperandsMutable().assign(reductionOperands);
235
236 // Clone data exit operations
237 rewriter.setInsertionPointAfter(newComputeOp);
238 for (Operation *dataOp : dataExitOps)
239 rewriter.clone(*dataOp, deviceMapping);
240
241 rewriter.setInsertionPointToEnd(&thenBlock);
242 if (!thenBlock.getTerminator())
243 scf::YieldOp::create(rewriter, computeConstructOp.getLoc());
244
245 // Host execution path (false branch)
246 Region &hostRegion = computeConstructOp.getRegion();
247 if (hostRegion.hasOneBlock()) {
248 // Don't need to clone original ops, just take them and legalize for host.
249 ifOp.getElseRegion().takeBody(hostRegion);
250
251 // Swap acc yield for scf yield.
252 Block &elseBlock = ifOp.getElseRegion().front();
253 elseBlock.getTerminator()->erase();
254 rewriter.setInsertionPointToEnd(&elseBlock);
255 scf::YieldOp::create(rewriter, computeConstructOp.getLoc());
256
257 convertHostRegion(computeConstructOp, ifOp.getElseRegion());
258 } else {
259 // scf.if regions must stay single-block. Wrap the original multi-block ACC
260 // body in scf.execute_region so it can be hosted in the else branch.
261 Block &elseBlock = ifOp.getElseRegion().front();
262 rewriter.setInsertionPoint(elseBlock.getTerminator());
263 IRMapping hostMapping;
264 auto hostExecuteRegion = wrapMultiBlockRegionWithSCFExecuteRegion(
265 hostRegion, hostMapping, computeConstructOp.getLoc(), rewriter);
266 convertHostRegion(computeConstructOp, hostExecuteRegion.getRegion());
267 }
268
269 // The original op is now empty and can be erased
270 eraseOps.insert(computeConstructOp);
271
272 // TODO: Can probably 'move' the data ops instead of cloning them
273 // which would eliminate need to explicitly erase
274 for (Operation *dataOp : dataExitOps)
275 eraseOps.insert(dataOp);
276
277 // Redirect host-side uses while preserving uses outside this construct.
278 // Shared operations are erased only after their users are gone.
279 Region &elseRegion = ifOp.getElseRegion();
280 auto replaceHostUsesAndScheduleErase = [&](auto &ops) {
281 for (Operation *op : ops) {
282 getAccVar(op).replaceUsesWithIf(getVar(op), [&](OpOperand &use) {
283 return elseRegion.isAncestor(use.getOwner()->getParentRegion());
284 });
285 condEraseOps.insert(op);
286 }
287 };
288 replaceHostUsesAndScheduleErase(dataEntryOps);
289 replaceHostUsesAndScheduleErase(firstprivateOps);
290 replaceHostUsesAndScheduleErase(privateOps);
291 replaceHostUsesAndScheduleErase(reductionOps);
292}
293
294void ACCIfClauseLowering::runOnOperation() {
295 func::FuncOp funcOp = getOperation();
296 accSupport = &getAnalysis<OpenACCSupport>();
297
299 llvm::SetVector<Operation *> condEraseOps;
300 funcOp.walk([&](Operation *op) {
301 if (auto parallelOp = dyn_cast<acc::ParallelOp>(op))
302 lowerIfClauseForComputeConstruct(parallelOp, eraseOps, condEraseOps);
303 else if (auto kernelsOp = dyn_cast<acc::KernelsOp>(op))
304 lowerIfClauseForComputeConstruct(kernelsOp, eraseOps, condEraseOps);
305 else if (auto serialOp = dyn_cast<acc::SerialOp>(op))
306 lowerIfClauseForComputeConstruct(serialOp, eraseOps, condEraseOps);
307 });
308
309 for (Operation *op : llvm::reverse(eraseOps))
310 op->erase();
311 // Shared entry/private/reduction ops can become dead in stages.
312 // Revisit deferred producers after their consumers are erased.
313 SmallVector<Operation *> pendingEraseOps(condEraseOps.begin(),
314 condEraseOps.end());
315 bool erased;
316 do {
317 erased = false;
318 for (size_t i = pendingEraseOps.size(); i > 0; --i) {
319 Operation *op = pendingEraseOps[i - 1];
320 if (!op->use_empty())
321 continue;
322 pendingEraseOps.erase(pendingEraseOps.begin() + i - 1);
323 op->erase();
324 erased = true;
325 }
326 } while (erased);
327}
328
329} // namespace
Block represents an ordered list of Operations.
Definition Block.h:33
Operation & front()
Definition Block.h:177
Operation * getTerminator()
Get the terminator operation of this block.
Definition Block.cpp:249
This class allows control over how the GreedyPatternRewriteDriver works.
GreedyRewriteConfig & setUseTopDownTraversal(bool use=true)
GreedyRewriteConfig & setStrictness(GreedyRewriteStrictness mode)
This is a utility class for mapping one set of IR entities to another.
Definition IRMapping.h:26
auto lookupOrDefault(T from) const
Lookup a mapped value within the map.
Definition IRMapping.h:65
void map(Value from, Value to)
Inserts a new mapping for 'from' to 'to'.
Definition IRMapping.h:30
This class coordinates rewriting a piece of IR outside of a pattern rewrite, providing a way to keep ...
Operation * clone(Operation &op, IRMapping &mapper)
Creates a deep copy of the specified operation, remapping any operands that use values outside of the...
Definition Builders.cpp:567
void setInsertionPointToStart(Block *block)
Sets the insertion point to the start of the specified block.
Definition Builders.h:433
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
Definition Builders.h:400
void setInsertionPointToEnd(Block *block)
Sets the insertion point to the end of the specified block.
Definition Builders.h:438
void setInsertionPointAfter(Operation *op)
Sets the insertion point to the node after the specified operation, which will cause subsequent inser...
Definition Builders.h:414
This class represents an operand of an operation.
Definition Value.h:254
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
bool use_empty()
Returns true if this operation has no uses.
Definition Operation.h:877
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
Location getLoc()
The source location the operation was defined or derived from.
Definition Operation.h:240
user_range getUsers()
Returns a range of all users.
Definition Operation.h:898
Region * getParentRegion()
Returns the region to which the instruction belongs.
Definition Operation.h:247
MLIRContext * getContext()
Return the context this operation is associated with.
Definition Operation.h:233
void erase()
Remove this operation from its parent block and delete it.
This class contains a list of basic blocks and a link to the parent operation it is attached to.
Definition Region.h:26
bool isAncestor(Region *other)
Return true if this region is ancestor of the other region.
Definition Region.h:246
bool hasOneBlock()
Return true if this region has exactly one block.
Definition Region.h:68
RetT walk(FnT &&callback)
Walk all nested operations, blocks or regions (including this region), depending on the type of callb...
Definition Region.h:309
This class provides an abstraction over the various different ranges of value types.
Definition TypeRange.h:40
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
void replaceUsesWithIf(Value newValue, function_ref< bool(OpOperand &)> shouldReplace)
Replace all uses of 'this' value with 'newValue' if the given callback returns true.
Definition Value.cpp:91
InFlightDiagnostic emitNYI(Location loc, const Twine &message)
Report a case that is not yet supported by the implementation.
Operation * getOwner() const
Return the owner of this operand.
Definition UseDefLists.h:38
mlir::Value getAccVar(mlir::Operation *accDataClauseOp)
Used to obtain the accVar from a data clause operation.
Definition OpenACC.cpp:5256
mlir::Value getVar(mlir::Operation *accDataClauseOp)
Used to obtain the var from a data clause operation.
Definition OpenACC.cpp:5225
scf::ExecuteRegionOp wrapMultiBlockRegionWithSCFExecuteRegion(Region &region, IRMapping &mapping, Location loc, RewriterBase &rewriter)
Wrap a multi-block region in an scf.execute_region.
void populateACCHostFallbackPatterns(RewritePatternSet &patterns, OpenACCSupport &accSupport, bool enableLoopConversion=true)
Populates all patterns for host fallback path (when if clause evaluates to false).
Include the generated interface declarations.
LogicalResult applyOpPatternsGreedily(ArrayRef< Operation * > ops, const FrozenRewritePatternSet &patterns, GreedyRewriteConfig config=GreedyRewriteConfig(), bool *changed=nullptr, bool *allErased=nullptr)
Rewrite the specified ops by repeatedly applying the highest benefit patterns in a greedy worklist dr...
@ ExistingOps
Only pre-existing ops are processed.