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/DenseSet.h"
189#include "llvm/ADT/SmallVector.h"
190#include "llvm/ADT/TypeSwitch.h"
191
192namespace mlir {
193namespace acc {
194#define GEN_PASS_DEF_ACCIMPLICITDECLARE
195#include "mlir/Dialect/OpenACC/Transforms/Passes.h.inc"
196} // namespace acc
197} // namespace mlir
198
199#define DEBUG_TYPE "acc-implicit-declare"
200
201using namespace mlir;
202
203namespace {
204
205using GlobalOpSetT = llvm::SmallSetVector<Operation *, 16>;
206
207/// Checks whether a use of the requested `globalOp` should be considered
208/// for hoisting out of acc region due to avoid `acc declare`ing something
209/// that instead should be implicitly mapped.
210static bool isGlobalUseCandidateForHoisting(Operation *globalOp,
211 Operation *user,
212 SymbolRefAttr symbol,
213 acc::OpenACCSupport &accSupport) {
214 // This symbol is valid in GPU region. This means semantics
215 // would change if moved to host - therefore it is not a candidate.
216 if (accSupport.isValidSymbolUse(user, symbol))
217 return false;
218
219 bool isInitializedConstant = false;
220 bool isFunction = false;
221
222 if (auto globalVarOp = dyn_cast<acc::GlobalVariableOpInterface>(globalOp))
223 isInitializedConstant =
224 globalVarOp.isConstant() && globalVarOp.hasInitializer();
225
226 if (isa<FunctionOpInterface>(globalOp))
227 isFunction = true;
228
229 // Initialized constants should be kept in device code so their definitions
230 // can be duplicated in the device image. An initializer-less constant is an
231 // external declaration, so keeping its address-of in device code would
232 // create a device symbol that a separately compiled defining translation
233 // unit may not provide. Hoist it so normal implicit mapping passes its host
234 // definition to the compute region instead.
235 //
236 // Function references should be kept in device code to ensure their device
237 // addresses are computed. Everything else should be hoisted since we already
238 // proved it is not a valid symbol in the GPU region.
239 return !isInitializedConstant && !isFunction;
240}
241
242/// Checks whether it is valid to use acc.declare marking on the global.
243bool isValidForAccDeclare(Operation *globalOp) {
244 // For functions - we use acc.routine marking instead.
245 return !isa<FunctionOpInterface>(globalOp);
246}
247
248/// Collect remaining symbol uses with a single walk. Asking whether each
249/// recipe is referenced via getSymbolUses walks the whole module again, and
250/// recipes are generated per type, so there can be many of them. A recipe
251/// referring only to its own symbol is not a "relevant" use (it does not
252/// mean the recipe is attached to a compute construct).
253static std::optional<llvm::DenseSet<StringAttr>>
254collectUsedSymbolsExcludingRecipeSelfUses(ModuleOp mod) {
255 // The module region, not the module op, is the symbol table scope: asking
256 // for the uses on the op itself would not walk into the body.
257 std::optional<SymbolTable::UseRange> uses =
258 SymbolTable::getSymbolUses(&mod.getBodyRegion());
259 if (!uses)
260 return std::nullopt;
261
262 llvm::DenseSet<StringAttr> usedSymbols;
263 auto isRecipeSelfUse = [](Operation *user, StringAttr name) {
264 if (auto recipe = dyn_cast<acc::PrivateRecipeOp>(user))
265 return recipe.getNameAttr() == name;
266 if (auto recipe = dyn_cast<acc::FirstprivateRecipeOp>(user))
267 return recipe.getNameAttr() == name;
268 if (auto recipe = dyn_cast<acc::ReductionRecipeOp>(user))
269 return recipe.getNameAttr() == name;
270 return false;
271 };
272 for (const SymbolTable::SymbolUse &use : *uses) {
273 StringAttr name = use.getSymbolRef().getLeafReference();
274 if (!isRecipeSelfUse(use.getUser(), name))
275 usedSymbols.insert(name);
276 }
277 return usedSymbols;
278}
279
280/// Checks whether a recipe operation has meaningful use of its symbol that
281/// justifies processing its regions for global references. Returns false if:
282/// 1. The recipe has no symbol uses at all, or
283/// 2. The only symbol use is the recipe's own symbol definition
284template <typename RecipeOpT>
285static bool hasRelevantRecipeUse(
286 RecipeOpT &recipeOp, ModuleOp &mod,
287 const std::optional<llvm::DenseSet<StringAttr>> &usedSymbols) {
288 auto recipeName = recipeOp.getNameAttr();
289 if (usedSymbols)
290 return usedSymbols->contains(recipeName);
291
292 std::optional<SymbolTable::UseRange> symbolUses = recipeOp.getSymbolUses(mod);
293
294 // No recipe symbol uses.
295 if (!symbolUses.has_value() || symbolUses->empty())
296 return false;
297
298 // If more than one use, assume it's used.
299 auto begin = symbolUses->begin();
300 auto end = symbolUses->end();
301 if (begin != end && std::next(begin) != end)
302 return true;
303
304 // If single use, check if the use is the recipe itself.
305 const SymbolTable::SymbolUse &use = *symbolUses->begin();
306 return use.getUser() != recipeOp.getOperation();
307}
308
309// Hoists addr_of operations for globals that cannot be defined in this
310// translation unit out of OpenACC regions. This way they are implicitly mapped
311// instead of being considered for implicit declare.
312template <typename AccConstructT>
313static void hoistNonConstantDirectUses(AccConstructT accOp,
314 acc::OpenACCSupport &accSupport) {
315 accOp.walk([&](acc::AddressOfGlobalOpInterface addrOfOp) {
316 SymbolRefAttr symRef = addrOfOp.getSymbol();
317 if (symRef) {
318 Operation *globalOp =
319 SymbolTable::lookupNearestSymbolFrom(addrOfOp, symRef);
320 if (isGlobalUseCandidateForHoisting(globalOp, addrOfOp, symRef,
321 accSupport)) {
322 auto computeRegionParent =
323 addrOfOp->getParentOfType<acc::ComputeRegionOp>();
324 addrOfOp->moveBefore(accOp);
325 if (computeRegionParent)
326 for (Value v : addrOfOp->getResults())
327 computeRegionParent.wireHoistedValueThroughIns(v);
328 LLVM_DEBUG(
329 llvm::dbgs() << "Hoisted:\n\t" << addrOfOp << "\n\tfrom:\n\t";
330 accOp->print(llvm::dbgs(),
331 OpPrintingFlags{}.skipRegions().enableDebugInfo());
332 llvm::dbgs() << "\n");
333 }
334 }
335 });
336}
337
338// Collects the globals referenced in a device region
339static void collectGlobalsFromDeviceRegion(Region &region,
340 GlobalOpSetT &globals,
341 acc::OpenACCSupport &accSupport,
342 SymbolTable &symTab) {
343 region.walk([&](Operation *op) {
344 // 1) Only consider relevant operations which use symbols
345 auto addrOfOp = dyn_cast<acc::AddressOfGlobalOpInterface>(op);
346 if (addrOfOp) {
347 SymbolRefAttr symRef = addrOfOp.getSymbol();
348 // 2) Found an operation which uses the symbol. Next determine if it
349 // is a candidate for `acc declare`. Some of the criteria considered
350 // is whether this symbol is not already a device one (either because
351 // acc declare is already used or this is a CUF global).
352 Operation *globalOp = nullptr;
353 bool isCandidate = !accSupport.isValidSymbolUse(op, symRef, &globalOp);
354 // 3) Add the candidate to the set of globals to be `acc declare`d.
355 if (isCandidate && globalOp && isValidForAccDeclare(globalOp))
356 globals.insert(globalOp);
357 } else if (auto indirectAccessOp =
358 dyn_cast<acc::IndirectGlobalAccessOpInterface>(op)) {
359 // Process operations that indirectly access globals
361 indirectAccessOp.getReferencedSymbols(symbols, &symTab);
362 for (SymbolRefAttr symRef : symbols)
363 if (Operation *globalOp = symTab.lookup(symRef.getLeafReference()))
364 if (isValidForAccDeclare(globalOp))
365 globals.insert(globalOp);
366 }
367 });
368}
369
370// Adds the declare attribute to the operation `op`.
371static void addDeclareAttr(MLIRContext *context, Operation *op,
372 acc::DataClause clause) {
375 acc::DeclareAttr::get(context,
376 acc::DataClauseAttr::get(context, clause)));
377}
378
379// This pass applies implicit declare actions for globals referenced in
380// OpenACC compute and routine regions.
381class ACCImplicitDeclare
382 : public acc::impl::ACCImplicitDeclareBase<ACCImplicitDeclare> {
383public:
384 using ACCImplicitDeclareBase<ACCImplicitDeclare>::ACCImplicitDeclareBase;
385
386 void runOnOperation() override {
387 ModuleOp mod = getOperation();
388 MLIRContext *context = &getContext();
389 acc::OpenACCSupport &accSupport = getAnalysis<acc::OpenACCSupport>();
390
391 // 1) Start off by hoisting any AddressOf operations out of acc region
392 // for any cases we do not want to `acc declare`. This is because we can
393 // rely on implicit data mapping in majority of cases without uselessly
394 // polluting the device globals.
395 mod.walk([&](Operation *op) {
397 .Case<ACC_COMPUTE_CONSTRUCT_OPS, acc::ComputeRegionOp>(
398 [&](auto accOp) {
399 hoistNonConstantDirectUses(accOp, accSupport);
400 });
401 });
402
403 // 2) Collect global symbols which need to be `acc declare`d. Do it for
404 // compute regions, acc routine, and existing globals with the declare
405 // attribute.
406 SymbolTable symTab(mod);
407 GlobalOpSetT globalsToAccDeclare;
408 std::optional<llvm::DenseSet<StringAttr>> usedSymbols =
409 collectUsedSymbolsExcludingRecipeSelfUses(mod);
410 mod.walk([&](Operation *op) {
412 .Case<ACC_COMPUTE_CONSTRUCT_OPS, acc::ComputeRegionOp>(
413 [&](auto accOp) {
414 collectGlobalsFromDeviceRegion(
415 accOp.getRegion(), globalsToAccDeclare, accSupport, symTab);
416 })
417 .Case([&](FunctionOpInterface func) {
418 if ((acc::isAccRoutine(func) ||
420 !func.isExternal())
421 collectGlobalsFromDeviceRegion(func.getFunctionBody(),
422 globalsToAccDeclare, accSupport,
423 symTab);
424 })
425 .Case([&](acc::GlobalVariableOpInterface globalVarOp) {
426 if (globalVarOp->getDiscardableAttr(acc::getDeclareAttrName()))
427 if (Region *initRegion = globalVarOp.getInitRegion())
428 collectGlobalsFromDeviceRegion(*initRegion, globalsToAccDeclare,
429 accSupport, symTab);
430 })
431 .Case([&](acc::PrivateRecipeOp privateRecipe) {
432 if (hasRelevantRecipeUse(privateRecipe, mod, usedSymbols)) {
433 collectGlobalsFromDeviceRegion(privateRecipe.getInitRegion(),
434 globalsToAccDeclare, accSupport,
435 symTab);
436 collectGlobalsFromDeviceRegion(privateRecipe.getDestroyRegion(),
437 globalsToAccDeclare, accSupport,
438 symTab);
439 }
440 })
441 .Case([&](acc::FirstprivateRecipeOp firstprivateRecipe) {
442 if (hasRelevantRecipeUse(firstprivateRecipe, mod, usedSymbols)) {
443 collectGlobalsFromDeviceRegion(firstprivateRecipe.getInitRegion(),
444 globalsToAccDeclare, accSupport,
445 symTab);
446 collectGlobalsFromDeviceRegion(
447 firstprivateRecipe.getDestroyRegion(), globalsToAccDeclare,
448 accSupport, symTab);
449 collectGlobalsFromDeviceRegion(firstprivateRecipe.getCopyRegion(),
450 globalsToAccDeclare, accSupport,
451 symTab);
452 }
453 })
454 .Case([&](acc::ReductionRecipeOp reductionRecipe) {
455 if (hasRelevantRecipeUse(reductionRecipe, mod, usedSymbols)) {
456 collectGlobalsFromDeviceRegion(reductionRecipe.getInitRegion(),
457 globalsToAccDeclare, accSupport,
458 symTab);
459 collectGlobalsFromDeviceRegion(
460 reductionRecipe.getCombinerRegion(), globalsToAccDeclare,
461 accSupport, symTab);
462 }
463 });
464 });
465
466 // 3) Finally, generate the appropriate declare actions needed to ensure
467 // this is considered for device global.
468 for (Operation *globalOp : globalsToAccDeclare) {
469 LLVM_DEBUG(
470 llvm::dbgs() << "Global is being `acc declare copyin`d: ";
471 globalOp->print(llvm::dbgs(),
472 OpPrintingFlags{}.skipRegions().enableDebugInfo());
473 llvm::dbgs() << "\n");
474
475 // Mark it as declare copyin.
476 addDeclareAttr(context, globalOp, acc::DataClause::acc_copyin);
477
478 // TODO: May need to create the global constructor which does the mapping
479 // action. It is not yet clear if this is needed yet (since the globals
480 // might just end up in the GPU image without requiring mapping via
481 // runtime).
482 }
483 }
484};
485
486} // 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 setDiscardableAttr(StringAttr name, Attribute value)
Set a discardable attribute by name.
Definition Operation.h:512
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:849
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,...
static std::optional< UseRange > getSymbolUses(Operation *from)
Get an iterator range for all of the uses, for any symbol, that are nested within the given operation...
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