MLIR 24.0.0git
MarkDeclareTarget.cpp
Go to the documentation of this file.
1//===- MarkDeclareTarget.cpp ----------------------------------------------===//
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// Mark functions called from explicit target code as implicitly declare target.
10//
11//===----------------------------------------------------------------------===//
12
14#include "mlir/IR/Operation.h"
15#include "mlir/IR/SymbolTable.h"
17#include "mlir/Pass/Pass.h"
18#include "mlir/Support/LLVM.h"
19#include "llvm/ADT/StringMap.h"
20#include "llvm/ADT/StringSet.h"
21#include "llvm/ADT/TypeSwitch.h"
22
23namespace mlir {
24namespace omp {
25
26#define GEN_PASS_DEF_MARKDECLARETARGETPASS
27#include "mlir/Dialect/OpenMP/Transforms/Passes.h.inc"
28
29} // namespace omp
30} // namespace mlir
31
32using namespace mlir;
33
34/// Check whether the given operation is located inside of an \c omp.target.
35static bool isInTargetRegion(Operation &op) {
36 // TODO: Detection of callees inside of a target region might need an update
37 // once reverse offloading is implemented.
38 // Reverse offload target regions would then have to propagate the "host"
39 // device type.
40 return op.getParentOfType<omp::TargetOp>();
41}
42
43/// Add to \c callees all names of the functions called from regions owned by
44/// \c op. If \c targetCallees is provided, split non-target and target uses
45/// between these two output sets.
47 llvm::StringSet<> *targetCallees = nullptr) {
48 op.walk([&](CallOpInterface callOp) {
49 CallInterfaceCallable callable = callOp.getCallableForCallee();
50 if (auto callableSymRef = dyn_cast<SymbolRefAttr>(callable)) {
51 StringRef callee = callableSymRef.getLeafReference();
52 if (targetCallees && isInTargetRegion(*callOp))
53 targetCallees->insert(callee);
54 else
55 callees.insert(callee);
56 }
57 });
58}
59
60/// Extract from \c arrayAttr and into \c syms the list of symbol names stored
61/// in the attribute.
62static void gatherSymsFromAttr(ArrayAttr arrayAttr, llvm::StringSet<> &syms) {
63 if (!arrayAttr)
64 return;
65
66 for (Attribute attr : arrayAttr)
67 if (auto symbolRefAttr = dyn_cast<SymbolRefAttr>(attr))
68 syms.insert(symbolRefAttr.getLeafReference());
69}
70
71/// Go through all OpenMP dialect operations located in regions owned by \c op
72/// looking for symbol references to \c accomp::RecipeInterface or
73/// \c FunctionOpInterface operations and, based on whether they are located
74/// within a nested \c omp.target region, add them to the corresponding output
75/// \c StringSet.
77 llvm::StringSet<> &nestedRecipeUses,
78 llvm::StringSet<> &targetRecipeUses,
79 llvm::StringSet<> &nestedFunctionUses,
80 llvm::StringSet<> &targetFunctionUses) {
81 op.walk([&](Operation *op) {
82 bool inTarget = isInTargetRegion(*op);
83 llvm::StringSet<> &recipeUses =
84 inTarget ? targetRecipeUses : nestedRecipeUses;
85 llvm::StringSet<> &functionUses =
86 inTarget ? targetFunctionUses : nestedFunctionUses;
87
88 // Handle each op holding clauses linked to a recipe op separately. This
89 // must be kept in sync with dialect changes.
91 .Case([&](omp::DistributeOp op) {
92 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
93 })
94 .Case([&](omp::LoopOp op) {
95 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
96 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
97 })
98 .Case([&](omp::MapInfoOp op) {
99 if (FlatSymbolRefAttr mapperAttr = op.getMapperIdAttr())
100 recipeUses.insert(mapperAttr.getValue());
101 })
102 .Case([&](omp::ParallelOp op) {
103 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
104 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
105 })
106 .Case([&](omp::ScopeOp op) {
107 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
108 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
109 })
110 .Case([&](omp::SectionsOp op) {
111 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
112 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
113 })
114 .Case([&](omp::SimdOp op) {
115 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
116 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
117 })
118 .Case([&](omp::SingleOp op) {
119 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
120 // This goes directly to the called functions, as it's pointing to a
121 // function, not a recipe op.
122 gatherSymsFromAttr(op.getCopyprivateSymsAttr(), functionUses);
123 })
124 .Case([&](omp::TargetOp op) {
125 // omp.private is inlined inside of the target region, hence we need
126 // to add it with the target uses rather than base it on context.
127 // TODO: The reverse-offload case would require adding it to
128 // nestedRecipeUses.
129 gatherSymsFromAttr(op.getPrivateSymsAttr(), targetRecipeUses);
130 gatherSymsFromAttr(op.getInReductionSymsAttr(), recipeUses);
131 })
132 .Case([&](omp::TaskgroupOp op) {
133 gatherSymsFromAttr(op.getTaskReductionSymsAttr(), recipeUses);
134 })
135 .Case([&](omp::TaskloopContextOp op) {
136 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
137 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
138 gatherSymsFromAttr(op.getInReductionSymsAttr(), recipeUses);
139 })
140 .Case([&](omp::TaskOp op) {
141 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
142 gatherSymsFromAttr(op.getInReductionSymsAttr(), recipeUses);
143 })
144 .Case([&](omp::TeamsOp op) {
145 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
146 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
147 })
148 .Case([&](omp::WsloopOp op) {
149 gatherSymsFromAttr(op.getPrivateSymsAttr(), recipeUses);
150 gatherSymsFromAttr(op.getReductionSymsAttr(), recipeUses);
151 });
152 });
153}
154
155namespace {
156
157class MarkDeclareTargetPass
158 : public omp::impl::MarkDeclareTargetPassBase<MarkDeclareTargetPass> {
159
160 // This pass executes on mlir::ModuleOp, marking functions contained within
161 // as implicitly declare target if they are called from within an explicitly
162 // marked declare target function or a target region (TargetOp), or
163 // transitively through recipe ops (e.g. omp.declare_reduction, omp.private)
164 // or other function calls.
165 void runOnOperation() override {
166 // Illegal as an MLIR symbol name to avoid collisions. Used to gather all
167 // calls from within omp.target regions as a single "function".
168 constexpr const static ::llvm::StringLiteral kTargetRegionsSymName =
169 "omp targets";
170
171 ModuleOp modOp = getOperation();
172
173 // Gather and store the set of called functions by each recipe.
174 // TODO: This doesn't currently support recipe ops holding references to
175 // other recipe ops.
176 llvm::StringMap<llvm::StringSet<>> calls;
177 for (auto recipeOp : modOp.getOps<accomp::RecipeInterface>()) {
178 StringAttr recipeSymName;
179 if (auto symOp = dyn_cast<SymbolOpInterface>(*recipeOp))
180 recipeSymName = symOp.getNameAttr();
181 else if (auto privateOp = dyn_cast<omp::PrivateClauseOp>(*recipeOp))
182 recipeSymName = privateOp.getSymNameAttr();
184 if (recipeSymName) {
185 llvm::StringSet<> recipeCalls;
186 gatherNestedCallees(*recipeOp, recipeCalls);
187 calls[recipeSymName] = recipeCalls;
189 }
190
191 // Gather and store the set of called functions by each function.
192 for (auto funcOp : modOp.getOps<FunctionOpInterface>()) {
193 llvm::StringSet<> functionCalls, targetCalls;
194 gatherNestedCallees(*funcOp, functionCalls, &targetCalls);
195
196 // Transitively include functions called from recipe op users, as if
197 // inlined.
198 llvm::StringSet<> recipeUses, targetRecipeUses;
199 gatherNestedSymbolUses(*funcOp, recipeUses, targetRecipeUses,
200 functionCalls, targetCalls);
201 for (auto &recipe : recipeUses) {
202 const llvm::StringSet<> &recipeCalls = calls.at(recipe.getKey());
203 functionCalls.insert_range(recipeCalls);
204 }
205 for (auto &recipe : targetRecipeUses) {
206 const llvm::StringSet<> &recipeCalls = calls.at(recipe.getKey());
207 targetCalls.insert_range(recipeCalls);
208 }
209
210 calls[funcOp.getName()] = functionCalls;
211 calls[kTargetRegionsSymName].insert_range(targetCalls);
212 }
213
214 // Create worklist with all functions that are directly reachable from
215 // declare_target functions or target regions.
217 worklist;
218 for (auto funcOp : getOperation().getOps<FunctionOpInterface>()) {
219 auto declareTargetOp =
220 llvm::dyn_cast<omp::DeclareTargetInterface>(funcOp.getOperation());
221
222 if (!declareTargetOp || !declareTargetOp.isDeclareTarget())
223 continue;
224
225 // Add to the worklist all called functions with the declare_target
226 // information of this one, so it gets propagated.
227 for (auto &callee : calls[funcOp.getName()])
228 worklist.push_back(
229 {callee.getKey(), declareTargetOp.getDeclareTargetDeviceType()});
230 }
231
232 // Add to the worklist all functions reached from target regions.
233 for (auto &callee : calls[kTargetRegionsSymName])
234 worklist.push_back(
235 {callee.getKey(), omp::DeclareTargetDeviceType::nohost});
236
237 // Process the work list by propagating changes to other non-explicit
238 // declare_target functions based on the call graph, until no updates are
239 // left.
240 while (!worklist.empty()) {
241 std::pair<StringRef, omp::DeclareTargetDeviceType> workItem =
242 worklist.pop_back_val();
243 auto funcOp = modOp.lookupSymbol<FunctionOpInterface>(workItem.first);
244 assert(funcOp && "a work item must point to an existing function");
245
246 // Skip if the function is explicitly marked as declare_target or if it
247 // doesn't support the interface. We only want to propagate implicit
248 // declare_target information to functions for which the user hasn't
249 // specified an explicit behavior.
250 auto declareTargetOp = dyn_cast<omp::DeclareTargetInterface>(*funcOp);
251 if (!declareTargetOp || (declareTargetOp.isDeclareTarget() &&
252 !declareTargetOp.isImplicitDeclareTarget()))
253 continue;
254
255 omp::DeclareTargetDeviceType changedDeviceType;
256 if (declareTargetOp.isDeclareTarget()) {
257 // Implicit declare_target update.
258 omp::DeclareTargetDeviceType currentDeviceType =
259 declareTargetOp.getDeclareTargetDeviceType();
260
261 // Skip the update (and adding callees to the worklist) if the added
262 // info doesn't change anything.
263 if (currentDeviceType == omp::DeclareTargetDeviceType::any ||
264 currentDeviceType == workItem.second) {
265 continue;
266 }
267
268 // Update intermediate information about this function. By the previous
269 // check, we know it's host + nohost = any.
270 changedDeviceType = omp::DeclareTargetDeviceType::any;
271 } else {
272 // No declare_target information present.
273
274 // Prevent public and external functions from being restricted to a
275 // device. We don't have visibility over all their uses.
276 if (funcOp.isPublic() || funcOp.isExternal())
277 changedDeviceType = omp::DeclareTargetDeviceType::any;
278 else
279 changedDeviceType = workItem.second;
280 }
281
282 // Update the operation and add callees to the worklist to propagate it.
283 declareTargetOp.setDeclareTarget(changedDeviceType,
284 omp::DeclareTargetCaptureClause::to,
285 /*automap=*/false, /*implicit=*/true);
286
287 for (auto &callee : calls[workItem.first])
288 worklist.push_back({callee.getKey(), changedDeviceType});
289 }
290 }
291};
292} // namespace
ArrayAttr()
static void gatherNestedCallees(Operation &op, llvm::StringSet<> &callees, llvm::StringSet<> *targetCallees=nullptr)
Add to callees all names of the functions called from regions owned by op.
static void gatherNestedSymbolUses(Operation &op, llvm::StringSet<> &nestedRecipeUses, llvm::StringSet<> &targetRecipeUses, llvm::StringSet<> &nestedFunctionUses, llvm::StringSet<> &targetFunctionUses)
Go through all OpenMP dialect operations located in regions owned by op looking for symbol references...
static bool isInTargetRegion(Operation &op)
Check whether the given operation is located inside of an omp.target.
static void gatherSymsFromAttr(ArrayAttr arrayAttr, llvm::StringSet<> &syms)
Extract from arrayAttr and into syms the list of symbol names stored in the attribute.
Attributes are known-constant values of operations.
Definition Attributes.h:25
A symbol reference with a reference path containing a single element.
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
OpTy getParentOfType()
Return the closest surrounding parent operation that is of type 'OpTy'.
Definition Operation.h:255
std::enable_if_t< llvm::function_traits< std::decay_t< FnT > >::num_args==1, RetT > walk(FnT &&callback)
Walk the operation by calling the callback for each nested operation (including this one),...
Definition Operation.h:842
Include the generated interface declarations.
A callable is either a symbol, or an SSA value, that is referenced by a call-like operation.