MLIR 24.0.0git
ACCImplicitDeclare.cpp
Go to the documentation of this file.
1//===- ACCImplicitDeclare.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// This pass applies implicit `acc declare` actions to global variables
10// referenced in OpenACC compute regions and routine functions.
11//
12// Overview:
13// ---------
14// Global references in an acc regions (for globals not marked with `acc
15// declare` by the user) can be handled in one of two ways:
16// - Mapped through data clauses
17// - Implicitly marked as `acc declare` (this pass)
18//
19// Thus, the OpenACC specification focuses solely on implicit data mapping rules
20// whose implementation is captured in `ACCImplicitData` pass.
21//
22// However, it is both advantageous and required for certain cases to
23// use implicit `acc declare` instead:
24// - Any functions that are implicitly marked as `acc routine` through
25// `ACCImplicitRoutine` may reference globals. Since data mapping
26// is only possible for compute regions, such globals can only be
27// made available on device through `acc declare`.
28// - Compiler can generate and use globals for cases needed in IR
29// representation such as type descriptors or various names needed for
30// runtime calls and error reporting - such cases often are introduced
31// after a frontend semantic checking is done since it is related to
32// implementation detail. Thus, such compiler generated globals would
33// not have been visible for a user to mark with `acc declare`.
34// - Constant globals such as filename strings or data initialization values
35// are values that do not get mutated but are still needed for appropriate
36// runtime execution. If a kernel is launched 1000 times, it is not a
37// good idea to map such a global 1000 times. Therefore, such globals
38// benefit from being marked with `acc declare`.
39//
40// This pass automatically
41// marks global variables with the `acc.declare` attribute when they are
42// referenced in OpenACC compute constructs or routine functions and meet
43// the criteria noted above, ensuring
44// they are properly handled for device execution.
45//
46// The pass performs two main optimizations:
47//
48// 1. Hoisting: For non-constant globals and constant globals without a local
49// initializer referenced in compute regions, the pass hoists the address-of
50// operation out of the region when possible, allowing them to be implicitly
51// mapped through normal data clause mechanisms rather than requiring
52// declare marking.
53//
54// 2. Declaration: For globals that must be available on the device (constants,
55// globals in routines, globals in recipe operations), the pass adds the
56// `acc.declare` attribute with the copyin data clause.
57//
58// Requirements:
59// -------------
60// To use this pass in a pipeline, the following requirements must be met:
61//
62// 1. Operation Interface Implementation: Operations that compute addresses
63// of global variables must implement the `acc::AddressOfGlobalOpInterface`
64// and those that represent globals must implement the
65// `acc::GlobalOpInterface`. Additionally, any operations that indirectly
66// access globals must implement the `acc::IndirectGlobalAccessOpInterface`.
67//
68// 2. Analysis Registration (Optional): If custom behavior is needed for
69// determining if a symbol use is valid within GPU regions, the dialect
70// should pre-register the `acc::OpenACCSupport` analysis.
71//
72// Examples:
73// ---------
74//
75// Example 1: Non-constant global in compute region (hoisted)
76//
77// Before:
78// memref.global @g_scalar : memref<f32> = dense<0.0>
79// func.func @test() {
80// acc.serial {
81// %addr = memref.get_global @g_scalar : memref<f32>
82// %val = memref.load %addr[] : memref<f32>
83// acc.yield
84// }
85// }
86//
87// After:
88// memref.global @g_scalar : memref<f32> = dense<0.0>
89// func.func @test() {
90// %addr = memref.get_global @g_scalar : memref<f32>
91// acc.serial {
92// %val = memref.load %addr[] : memref<f32>
93// acc.yield
94// }
95// }
96//
97// Example 2: Constant global in compute region (declared)
98//
99// Before:
100// memref.global constant @g_const : memref<f32> = dense<1.0>
101// func.func @test() {
102// acc.serial {
103// %addr = memref.get_global @g_const : memref<f32>
104// %val = memref.load %addr[] : memref<f32>
105// acc.yield
106// }
107// }
108//
109// After:
110// memref.global constant @g_const : memref<f32> = dense<1.0>
111// {acc.declare = #acc.declare<dataClause = acc_copyin>}
112// func.func @test() {
113// acc.serial {
114// %addr = memref.get_global @g_const : memref<f32>
115// %val = memref.load %addr[] : memref<f32>
116// acc.yield
117// }
118// }
119//
120// Example 3: Global in acc routine (declared)
121//
122// Before:
123// memref.global @g_data : memref<f32> = dense<0.0>
124// acc.routine @routine_0 func(@device_func)
125// func.func @device_func() attributes {acc.routine_info = ...} {
126// %addr = memref.get_global @g_data : memref<f32>
127// %val = memref.load %addr[] : memref<f32>
128// }
129//
130// After:
131// memref.global @g_data : memref<f32> = dense<0.0>
132// {acc.declare = #acc.declare<dataClause = acc_copyin>}
133// acc.routine @routine_0 func(@device_func)
134// func.func @device_func() attributes {acc.routine_info = ...} {
135// %addr = memref.get_global @g_data : memref<f32>
136// %val = memref.load %addr[] : memref<f32>
137// }
138//
139// Example 4: Global in private recipe (declared if recipe is used)
140//
141// Before:
142// memref.global @g_init : memref<f32> = dense<0.0>
143// acc.private.recipe @priv_recipe : memref<f32> init {
144// ^bb0(%arg0: memref<f32>):
145// %alloc = memref.alloc() : memref<f32>
146// %global = memref.get_global @g_init : memref<f32>
147// %val = memref.load %global[] : memref<f32>
148// memref.store %val, %alloc[] : memref<f32>
149// acc.yield %alloc : memref<f32>
150// } destroy { ... }
151// func.func @test() {
152// %var = memref.alloc() : memref<f32>
153// %priv = acc.private varPtr(%var : memref<f32>)
154// recipe(@priv_recipe) -> memref<f32>
155// acc.parallel private(%priv : memref<f32>) { ... }
156// }
157//
158// After:
159// memref.global @g_init : memref<f32> = dense<0.0>
160// {acc.declare = #acc.declare<dataClause = acc_copyin>}
161// acc.private.recipe @priv_recipe : memref<f32> init {
162// ^bb0(%arg0: memref<f32>):
163// %alloc = memref.alloc() : memref<f32>
164// %global = memref.get_global @g_init : memref<f32>
165// %val = memref.load %global[] : memref<f32>
166// memref.store %val, %alloc[] : memref<f32>
167// acc.yield %alloc : memref<f32>
168// } destroy { ... }
169// func.func @test() {
170// %var = memref.alloc() : memref<f32>
171// %priv = acc.private varPtr(%var : memref<f32>)
172// recipe(@priv_recipe) -> memref<f32>
173// acc.parallel private(%priv : memref<f32>) { ... }
174// }
175//
176//===----------------------------------------------------------------------===//
177
179
182#include "mlir/IR/Builders.h"
184#include "mlir/IR/BuiltinOps.h"
185#include "mlir/IR/Operation.h"
186#include "mlir/IR/Value.h"
188#include "llvm/ADT/SmallVector.h"
189#include "llvm/ADT/TypeSwitch.h"
190
191namespace mlir {
192namespace acc {
193#define GEN_PASS_DEF_ACCIMPLICITDECLARE
194#include "mlir/Dialect/OpenACC/Transforms/Passes.h.inc"
195} // namespace acc
196} // namespace mlir
197
198#define DEBUG_TYPE "acc-implicit-declare"
199
200using namespace mlir;
201
202namespace {
203
204using GlobalOpSetT = llvm::SmallSetVector<Operation *, 16>;
205
206/// Checks whether a use of the requested `globalOp` should be considered
207/// for hoisting out of acc region due to avoid `acc declare`ing something
208/// that instead should be implicitly mapped.
209static bool isGlobalUseCandidateForHoisting(Operation *globalOp,
210 Operation *user,
211 SymbolRefAttr symbol,
212 acc::OpenACCSupport &accSupport) {
213 // This symbol is valid in GPU region. This means semantics
214 // would change if moved to host - therefore it is not a candidate.
215 if (accSupport.isValidSymbolUse(user, symbol))
216 return false;
217
218 bool isInitializedConstant = false;
219 bool isFunction = false;
220
221 if (auto globalVarOp = dyn_cast<acc::GlobalVariableOpInterface>(globalOp))
222 isInitializedConstant =
223 globalVarOp.isConstant() && globalVarOp.hasInitializer();
224
225 if (isa<FunctionOpInterface>(globalOp))
226 isFunction = true;
227
228 // Initialized constants should be kept in device code so their definitions
229 // can be duplicated in the device image. An initializer-less constant is an
230 // external declaration, so keeping its address-of in device code would
231 // create a device symbol that a separately compiled defining translation
232 // unit may not provide. Hoist it so normal implicit mapping passes its host
233 // definition to the compute region instead.
234 //
235 // Function references should be kept in device code to ensure their device
236 // addresses are computed. Everything else should be hoisted since we already
237 // proved it is not a valid symbol in the GPU region.
238 return !isInitializedConstant && !isFunction;
239}
240
241/// Checks whether it is valid to use acc.declare marking on the global.
242bool isValidForAccDeclare(Operation *globalOp) {
243 // For functions - we use acc.routine marking instead.
244 return !isa<FunctionOpInterface>(globalOp);
245}
246
247/// Checks whether a recipe operation has meaningful use of its symbol that
248/// justifies processing its regions for global references. Returns false if:
249/// 1. The recipe has no symbol uses at all, or
250/// 2. The only symbol use is the recipe's own symbol definition
251template <typename RecipeOpT>
252static bool hasRelevantRecipeUse(RecipeOpT &recipeOp, ModuleOp &mod) {
253 std::optional<SymbolTable::UseRange> symbolUses = recipeOp.getSymbolUses(mod);
254
255 // No recipe symbol uses.
256 if (!symbolUses.has_value() || symbolUses->empty())
257 return false;
258
259 // If more than one use, assume it's used.
260 auto begin = symbolUses->begin();
261 auto end = symbolUses->end();
262 if (begin != end && std::next(begin) != end)
263 return true;
264
265 // If single use, check if the use is the recipe itself.
266 const SymbolTable::SymbolUse &use = *symbolUses->begin();
267 return use.getUser() != recipeOp.getOperation();
268}
269
270// Hoists addr_of operations for globals that cannot be defined in this
271// translation unit out of OpenACC regions. This way they are implicitly mapped
272// instead of being considered for implicit declare.
273template <typename AccConstructT>
274static void hoistNonConstantDirectUses(AccConstructT accOp,
275 acc::OpenACCSupport &accSupport) {
276 accOp.walk([&](acc::AddressOfGlobalOpInterface addrOfOp) {
277 SymbolRefAttr symRef = addrOfOp.getSymbol();
278 if (symRef) {
279 Operation *globalOp =
280 SymbolTable::lookupNearestSymbolFrom(addrOfOp, symRef);
281 if (isGlobalUseCandidateForHoisting(globalOp, addrOfOp, symRef,
282 accSupport)) {
283 auto computeRegionParent =
284 addrOfOp->getParentOfType<acc::ComputeRegionOp>();
285 addrOfOp->moveBefore(accOp);
286 if (computeRegionParent)
287 for (Value v : addrOfOp->getResults())
288 computeRegionParent.wireHoistedValueThroughIns(v);
289 LLVM_DEBUG(
290 llvm::dbgs() << "Hoisted:\n\t" << addrOfOp << "\n\tfrom:\n\t";
291 accOp->print(llvm::dbgs(),
292 OpPrintingFlags{}.skipRegions().enableDebugInfo());
293 llvm::dbgs() << "\n");
294 }
295 }
296 });
297}
298
299// Collects the globals referenced in a device region
300static void collectGlobalsFromDeviceRegion(Region &region,
301 GlobalOpSetT &globals,
302 acc::OpenACCSupport &accSupport,
303 SymbolTable &symTab) {
304 region.walk([&](Operation *op) {
305 // 1) Only consider relevant operations which use symbols
306 auto addrOfOp = dyn_cast<acc::AddressOfGlobalOpInterface>(op);
307 if (addrOfOp) {
308 SymbolRefAttr symRef = addrOfOp.getSymbol();
309 // 2) Found an operation which uses the symbol. Next determine if it
310 // is a candidate for `acc declare`. Some of the criteria considered
311 // is whether this symbol is not already a device one (either because
312 // acc declare is already used or this is a CUF global).
313 Operation *globalOp = nullptr;
314 bool isCandidate = !accSupport.isValidSymbolUse(op, symRef, &globalOp);
315 // 3) Add the candidate to the set of globals to be `acc declare`d.
316 if (isCandidate && globalOp && isValidForAccDeclare(globalOp))
317 globals.insert(globalOp);
318 } else if (auto indirectAccessOp =
319 dyn_cast<acc::IndirectGlobalAccessOpInterface>(op)) {
320 // Process operations that indirectly access globals
322 indirectAccessOp.getReferencedSymbols(symbols, &symTab);
323 for (SymbolRefAttr symRef : symbols)
324 if (Operation *globalOp = symTab.lookup(symRef.getLeafReference()))
325 if (isValidForAccDeclare(globalOp))
326 globals.insert(globalOp);
327 }
328 });
329}
330
331// Adds the declare attribute to the operation `op`.
332static void addDeclareAttr(MLIRContext *context, Operation *op,
333 acc::DataClause clause) {
335 acc::DeclareAttr::get(context,
336 acc::DataClauseAttr::get(context, clause)));
337}
338
339// This pass applies implicit declare actions for globals referenced in
340// OpenACC compute and routine regions.
341class ACCImplicitDeclare
342 : public acc::impl::ACCImplicitDeclareBase<ACCImplicitDeclare> {
343public:
344 using ACCImplicitDeclareBase<ACCImplicitDeclare>::ACCImplicitDeclareBase;
345
346 void runOnOperation() override {
347 ModuleOp mod = getOperation();
348 MLIRContext *context = &getContext();
349 acc::OpenACCSupport &accSupport = getAnalysis<acc::OpenACCSupport>();
350
351 // 1) Start off by hoisting any AddressOf operations out of acc region
352 // for any cases we do not want to `acc declare`. This is because we can
353 // rely on implicit data mapping in majority of cases without uselessly
354 // polluting the device globals.
355 mod.walk([&](Operation *op) {
357 .Case<ACC_COMPUTE_CONSTRUCT_OPS, acc::ComputeRegionOp>(
358 [&](auto accOp) {
359 hoistNonConstantDirectUses(accOp, accSupport);
360 });
361 });
362
363 // 2) Collect global symbols which need to be `acc declare`d. Do it for
364 // compute regions, acc routine, and existing globals with the declare
365 // attribute.
366 SymbolTable symTab(mod);
367 GlobalOpSetT globalsToAccDeclare;
368 mod.walk([&](Operation *op) {
370 .Case<ACC_COMPUTE_CONSTRUCT_OPS, acc::ComputeRegionOp>(
371 [&](auto accOp) {
372 collectGlobalsFromDeviceRegion(
373 accOp.getRegion(), globalsToAccDeclare, accSupport, symTab);
374 })
375 .Case([&](FunctionOpInterface func) {
376 if ((acc::isAccRoutine(func) ||
378 !func.isExternal())
379 collectGlobalsFromDeviceRegion(func.getFunctionBody(),
380 globalsToAccDeclare, accSupport,
381 symTab);
382 })
383 .Case([&](acc::GlobalVariableOpInterface globalVarOp) {
384 if (globalVarOp->getAttr(acc::getDeclareAttrName()))
385 if (Region *initRegion = globalVarOp.getInitRegion())
386 collectGlobalsFromDeviceRegion(*initRegion, globalsToAccDeclare,
387 accSupport, symTab);
388 })
389 .Case([&](acc::PrivateRecipeOp privateRecipe) {
390 if (hasRelevantRecipeUse(privateRecipe, mod)) {
391 collectGlobalsFromDeviceRegion(privateRecipe.getInitRegion(),
392 globalsToAccDeclare, accSupport,
393 symTab);
394 collectGlobalsFromDeviceRegion(privateRecipe.getDestroyRegion(),
395 globalsToAccDeclare, accSupport,
396 symTab);
397 }
398 })
399 .Case([&](acc::FirstprivateRecipeOp firstprivateRecipe) {
400 if (hasRelevantRecipeUse(firstprivateRecipe, mod)) {
401 collectGlobalsFromDeviceRegion(firstprivateRecipe.getInitRegion(),
402 globalsToAccDeclare, accSupport,
403 symTab);
404 collectGlobalsFromDeviceRegion(
405 firstprivateRecipe.getDestroyRegion(), globalsToAccDeclare,
406 accSupport, symTab);
407 collectGlobalsFromDeviceRegion(firstprivateRecipe.getCopyRegion(),
408 globalsToAccDeclare, accSupport,
409 symTab);
410 }
411 })
412 .Case([&](acc::ReductionRecipeOp reductionRecipe) {
413 if (hasRelevantRecipeUse(reductionRecipe, mod)) {
414 collectGlobalsFromDeviceRegion(reductionRecipe.getInitRegion(),
415 globalsToAccDeclare, accSupport,
416 symTab);
417 collectGlobalsFromDeviceRegion(
418 reductionRecipe.getCombinerRegion(), globalsToAccDeclare,
419 accSupport, symTab);
420 }
421 });
422 });
423
424 // 3) Finally, generate the appropriate declare actions needed to ensure
425 // this is considered for device global.
426 for (Operation *globalOp : globalsToAccDeclare) {
427 LLVM_DEBUG(
428 llvm::dbgs() << "Global is being `acc declare copyin`d: ";
429 globalOp->print(llvm::dbgs(),
430 OpPrintingFlags{}.skipRegions().enableDebugInfo());
431 llvm::dbgs() << "\n");
432
433 // Mark it as declare copyin.
434 addDeclareAttr(context, globalOp, acc::DataClause::acc_copyin);
435
436 // TODO: May need to create the global constructor which does the mapping
437 // action. It is not yet clear if this is needed yet (since the globals
438 // might just end up in the GPU image without requiring mapping via
439 // runtime).
440 }
441 }
442};
443
444} // namespace
b getContext())
MLIRContext is the top-level object for a collection of MLIR operations.
Definition MLIRContext.h:63
Set of flags used to control the behavior of the various IR print methods (e.g.
OpPrintingFlags & skipRegions(bool skip=true)
Skip printing regions.
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
void setAttr(StringAttr name, Attribute value)
If the an attribute exists with the specified name, change it to the new value.
Definition Operation.h:607
void print(raw_ostream &os, const OpPrintingFlags &flags={})
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:822
This class contains a list of basic blocks and a link to the parent operation it is attached to.
Definition Region.h:26
RetT walk(FnT &&callback)
Walk all nested operations, blocks or regions (including this region), depending on the type of callb...
Definition Region.h:312
This class represents a specific symbol use.
Operation * getUser() const
Return the operation user of this symbol reference.
This class allows for representing and managing the symbol table used by operations with the 'SymbolT...
Definition SymbolTable.h:24
Operation * lookup(StringRef name) const
Look up a symbol with the specified name, returning null if no such name exists.
static Operation * lookupNearestSymbolFrom(Operation *from, StringAttr symbol)
Returns the operation registered with the given symbol name within the closest parent operation of,...
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
bool isValidSymbolUse(Operation *user, SymbolRefAttr symbol, Operation **definingOpPtr=nullptr)
Check if a symbol use is valid for use in an OpenACC region.
#define ACC_COMPUTE_CONSTRUCT_OPS
Definition OpenACC.h:63
bool isAccRoutine(mlir::Operation *op)
Used to check whether the current operation is marked with acc routine.
Definition OpenACC.h:195
bool isSpecializedAccRoutine(mlir::Operation *op)
Used to check whether this is a specialized accelerator version of acc routine function.
Definition OpenACC.h:201
static constexpr StringLiteral getDeclareAttrName()
Used to obtain the attribute name for declare.
Definition OpenACC.h:177
Include the generated interface declarations.
llvm::TypeSwitch< T, ResultT > TypeSwitch
Definition LLVM.h:139