MLIR 24.0.0git
ACCCGToGPU.cpp
Go to the documentation of this file.
1//===- ACCCGToGPU.cpp - Lower acc.compute_region to gpu.launch ------------===//
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 `acc.compute_region` to the GPU dialect. For host-side
10// kernels it wraps the region in `gpu.launch`; for specialized acc routines
11// already inside a `gpu.func`, the body is lowered in place without emitting
12// a launch.
13//
14// Overview:
15// ---------
16// `acc.compute_region` is the compute-body representation produced after
17// OpenACC compute constructs are decomposed and parallelism has been assigned.
18// This pass is the final ACC-to-GPU lowering step for that body: it converts
19// nested `scf.parallel` / `scf.for` loops marked with `acc.par_dims` into GPU
20// block and thread parallelism, materializes privatization and reductions for
21// the device, inserts synchronization where shared state is observed across
22// threads, and erases the ACC scaffolding (`acc.compute_region`,
23// `acc.par_width`).
24//
25// Transformations:
26// ----------------
27// 1. Launch creation: outside a `gpu.func`, each `acc.compute_region` becomes a
28// `gpu.launch` whose grid and block sizes come from `acc.par_width` launch
29// operands (defaulting to 1). Kernel/module name attributes are preserved.
30// Inside a `gpu.func` (specialized acc routine), no launch is emitted.
31//
32// 2. Parallel loops: `scf.parallel` with a single `acc.par_dims` entry is
33// mapped to the corresponding GPU dimension (`block_*` or `thread_*`).
34// Sequential dimensions remain as `scf.parallel`/`scf.for` loops in the
35// generated kernel body.
36//
37// 3. Privatization: `acc.privatize` / `acc.private_local` storage is
38// materialized as one of: a per-thread `memref.alloca` (thread-private
39// arrays within the stack budget), an `acc.gpu_shared_memory` buffer
40// (gang-/worker-private arrays that fit the shared-memory budget), or a
41// `memref.alloc` whose pointer is broadcast to the block through a small
42// shared-memory slot (the data lives in global memory; shared memory only
43// holds the broadcast pointer).
44//
45// 4. Predication: `acc.predicate_region` becomes `scf.if` guarded by active
46// thread/block indices derived from `acc.par_dims` and launch dimensions.
47//
48// 5. Reductions: `acc.reduction_*` ops are lowered to GPU reduction and
49// synchronization primitives according to each reduction's parallel
50// dimensions and accumulator storage class.
51//
52// Example:
53// --------
54// Before:
55// %c128 = arith.constant 128 : index
56// %tx = acc.par_width %c128 par_dim(#acc.par_dim<thread_x>)
57// acc.compute_region launch(%arg0 = %tx) {
58// %c0 = arith.constant 0 : index
59// %c1 = arith.constant 1 : index
60// scf.parallel (%iv) = (%c0) to (%c128) step (%c1) {
61// ...
62// scf.reduce
63// } {acc.par_dims = #acc<par_dims[thread_x]>}
64// acc.yield
65// } <{origin = "acc.parallel"}>
66//
67// After:
68// gpu.launch blocks(%bidx, %bidy, %bidz) in (%gdimx = %c1, ...)
69// threads(%tidx, %tidy, %tidz) in (%bdimx = %c128, ...) {
70// ...
71// }
72//
73// Requirements:
74// -------------
75// - Must run on a GPU device type (`device-type` option); host and multicore
76// targets are rejected.
77// - Input must already be in the `acc.compute_region` form: nested SCF loops
78// carry `acc.par_dims`, privatization is expressed via `acc.privatize` /
79// `acc.private_local`, and reductions use the `acc.reduction_*` ops.
80// - Each `scf.parallel` processed by this pass is expected to have exactly
81// one parallel dimension and one induction variable.
82// - For acc routines, the `acc.compute_region` must live inside a `gpu.func`
83// in the GPU module.
84// - Uses `acc::OpenACCSupport` for NYI reporting and compiler remarks.
85// - Pass options: `max-workgroup-shared-memory`, `max-thread-private-stack`,
86// and `subgroup-size` (used for reductions and block-dimension alignment).
87//
88//===----------------------------------------------------------------------===//
89
91
108#include "mlir/IR/Block.h"
110#include "mlir/IR/BuiltinTypes.h"
111#include "mlir/IR/Diagnostics.h"
112#include "mlir/IR/Dominance.h"
113#include "mlir/IR/IRMapping.h"
114#include "mlir/IR/OpDefinition.h"
115#include "mlir/IR/PatternMatch.h"
116#include "mlir/IR/SymbolTable.h"
117#include "mlir/IR/Value.h"
121#include "mlir/Support/LLVM.h"
123#include "llvm/ADT/ArrayRef.h"
124#include "llvm/ADT/DenseMap.h"
125#include "llvm/ADT/STLExtras.h"
126#include "llvm/ADT/StringExtras.h"
127#include "llvm/ADT/Twine.h"
128#include "llvm/Support/Debug.h"
129#include <algorithm>
130#include <optional>
131#include <utility>
132
133namespace mlir {
134namespace acc {
135#define GEN_PASS_DEF_ACCCGTOGPU
136#include "mlir/Dialect/OpenACC/Transforms/Passes.h.inc"
137} // namespace acc
138} // namespace mlir
139
140#define DEBUG_TYPE "acc-cg-to-gpu"
141
142namespace {
143using namespace mlir;
144using namespace mlir::acc;
145
146enum class PrivateMemScope { Thread, Worker, Gang, None };
147
148/// Device label used in compiler remarks (e.g. "NVIDIA GPU").
149static std::string getDeviceRemarkQualifier(DeviceType deviceType) {
150 switch (deviceType) {
151 case DeviceType::None:
152 case DeviceType::Star:
153 case DeviceType::Default:
154 return "GPU";
155 default: {
156 std::string name;
157 llvm::StringRef deviceName = stringifyDeviceType(deviceType);
158 name.reserve(deviceName.size());
159 for (char c : deviceName)
160 name.push_back(llvm::toUpper(c));
161 return name + " GPU";
162 }
166/// True when \p op is inside a specialized acc routine function.
168 FunctionOpInterface funcOp = op->getParentOfType<FunctionOpInterface>();
169 return funcOp && acc::isSpecializedAccRoutine(funcOp);
170}
172/// Maps an acc.routine's parallelism clauses to a GPU parallel dimension.
173static GPUParallelDimAttr
174getAccRoutineParDim(RoutineOp routineOp, MLIRContext *ctx,
175 const ACCToGPUMappingPolicy &policy) {
176 if (routineOp.getGangDimValue() ||
177 routineOp.getGangDimValue(DeviceType::Nvidia)) {
178 int64_t gangDimValue = routineOp.getGangDimValue(DeviceType::Nvidia)
179 ? *routineOp.getGangDimValue(DeviceType::Nvidia)
180 : *routineOp.getGangDimValue();
181 ParLevel gangLevel = getGangParLevel(gangDimValue);
182 return policy.gangDim(ctx, gangLevel);
183 }
184 if (routineOp.hasGang() || routineOp.hasGang(DeviceType::Nvidia))
185 return policy.gangDim(ctx, ParLevel::gang_dim1);
186 if (routineOp.hasWorker() || routineOp.hasWorker(DeviceType::Nvidia))
187 return policy.workerDim(ctx);
188 if (routineOp.hasVector() || routineOp.hasVector(DeviceType::Nvidia))
189 return policy.vectorDim(ctx);
190 return policy.seqDim(ctx);
191}
192
193/// Looks up the acc.routine symbol associated with \p funcOp.
194static RoutineOp getRoutineOpForAccRoutineFunction(FunctionOpInterface funcOp,
195 const SymbolTable &symTab) {
196 if (isSpecializedAccRoutine(funcOp)) {
197 SpecializedRoutineAttr attr =
198 funcOp->getDiscardableAttrOfType<SpecializedRoutineAttr>(
200 return symTab.lookup<RoutineOp>(attr.getRoutine().getLeafReference());
201 }
202 RoutineInfoAttr routineInfo =
203 funcOp->getDiscardableAttrOfType<RoutineInfoAttr>(
205 if (!routineInfo || routineInfo.getAccRoutines().empty())
206 return nullptr;
207 return symTab.lookup<RoutineOp>(
208 routineInfo.getAccRoutines().front().getLeafReference());
210
211/// Returns the parallelism level of a specialized acc routine function.
212static GPUParallelDimAttr
213getSpecializedRoutineDim(FunctionOpInterface funcOp,
214 const ACCToGPUMappingPolicy &policy) {
215 SpecializedRoutineAttr specAttr =
216 funcOp->getDiscardableAttrOfType<SpecializedRoutineAttr>(
218 assert(specAttr && "expected specialized routine attribute");
219 return policy.map(funcOp->getContext(), specAttr.getLevel().getValue());
220}
221
222/// Returns the parallelism dimension of a callee acc routine, if any.
223static GPUParallelDimAttr
224getAccRoutineCallParDim(CallOpInterface callOp,
225 const ACCToGPUMappingPolicy &policy) {
226 std::optional<CallInterfaceCallable> callee = callOp.getCallableForCallee();
227 if (!callee)
228 return nullptr;
229 SymbolRefAttr calleeSymbolRef = dyn_cast<SymbolRefAttr>(*callee);
230 if (!calleeSymbolRef)
231 return nullptr;
232 ModuleOp moduleOp = callOp->getParentOfType<ModuleOp>();
233 if (!moduleOp)
234 return nullptr;
235
236 SymbolTable symTab(moduleOp);
237 FunctionOpInterface funcOp =
238 symTab.lookup<FunctionOpInterface>(calleeSymbolRef.getLeafReference());
239 if (!funcOp)
240 return nullptr;
241
243 return getSpecializedRoutineDim(funcOp, policy);
244 if (RoutineOp routineOp = getRoutineOpForAccRoutineFunction(funcOp, symTab))
245 return getAccRoutineParDim(routineOp, funcOp.getContext(), policy);
246 return nullptr;
247}
248
249/// Collects parallel dimensions from enclosing loops and the compute region.
250static SmallVector<GPUParallelDimAttr> getAncestorParDims(Operation *op) {
252 ComputeRegionOp computeRegion = op->getParentOfType<ComputeRegionOp>();
253 assert(computeRegion && "missing enclosing acc.compute_region");
254 scf::ParallelOp parentLoop = op->getParentOfType<scf::ParallelOp>();
255 // True while parentLoop is the innermost parallel ancestor of op.
256 bool isInnermostParallelParent = true;
257 while (parentLoop) {
258 bool hasNonSeqParDim = false;
259 if (GPUParallelDimsAttr parDimsAttr = getParDimsAttr(parentLoop)) {
260 for (GPUParallelDimAttr parDim : parDimsAttr.getArray()) {
261 insertParDim(parDimsArray, parDim);
262 if (!parDim.isSeq())
263 hasNonSeqParDim = true;
264 }
265 }
266 // Include launch dims for a block-redundant ancestor when:
267 // - it is itself worksharing (e.g. vector + gpu_block_redundant), or
268 // - it is the innermost parallel parent (sequential remnant after
269 // partition-kernel-loops), so the body is not predicated on blockIdx.
270 // Do not include them for an outer sequential block-redundant wrapper
271 // around nested gang/worker/vector worksharing: that widens gang-private
272 // storage to per-thread.
273 if (hasGPUBlockRedundantAttr(parentLoop) &&
274 (hasNonSeqParDim || isInnermostParallelParent))
275 for (GPUParallelDimAttr parDim : computeRegion.getLaunchParDims())
276 insertParDim(parDimsArray, parDim);
277 isInnermostParallelParent = false;
278 parentLoop = parentLoop->getParentOfType<scf::ParallelOp>();
279 }
280
281 if (GPUParallelDimsAttr parDimsAttr = getParDimsAttr(computeRegion))
282 for (GPUParallelDimAttr parDim : parDimsAttr.getArray())
283 insertParDim(parDimsArray, parDim);
284 return parDimsArray;
285}
286
287/// Strips index casts to reach the underlying defining value.
288static Value stripIndexCastsFromValue(Value x) {
289 Operation *op = x.getDefiningOp();
290 if (!op)
291 return x;
292 while (arith::IndexCastOp castOp = dyn_cast<arith::IndexCastOp>(op)) {
293 op = castOp->getOperand(0).getDefiningOp();
294 if (!op)
295 return x;
296 }
297 return op->getResult(0);
298}
299
300/// Extracts a compile-time integer constant from \p x, when known.
301static FailureOr<int64_t> extractIntConst(Value x,
302 bool stripIndexCasts = false) {
303 if (stripIndexCasts)
304 x = stripIndexCastsFromValue(x);
305 Operation *op = x.getDefiningOp();
306 if (op) {
307 if (arith::ConstantIntOp constOp = dyn_cast<arith::ConstantIntOp>(op)) {
308 assert(constOp.getType().getIntOrFloatBitWidth() <= 64);
309 return constOp.value();
310 }
311 if (arith::ConstantIndexOp constOp = dyn_cast<arith::ConstantIndexOp>(op))
312 return constOp.value();
313 }
314 return failure();
315}
316
317/// True when \p x is a constant equal to \p y (modulo index casts).
318static bool sameEffectiveValue(Value x, int64_t y) {
319 x = stripIndexCastsFromValue(x);
320 FailureOr<int64_t> conX = extractIntConst(x);
321 if (failed(conX))
322 return false;
323 return *conX == y;
324}
325
326/// Continues tracking a memref through view-like and partial-access ops.
327static bool getPassThroughResults(Operation *userOp, Value trackedOperand,
328 SmallVectorImpl<Value> &passThroughResults) {
329 if (ViewLikeOpInterface viewLikeOp = dyn_cast<ViewLikeOpInterface>(userOp)) {
330 if (viewLikeOp.getViewSource() == trackedOperand) {
331 passThroughResults.push_back(viewLikeOp.getViewDest());
332 return true;
333 }
334 return false;
335 }
336
337 // Partial-entity accesses (e.g. array element or field access) forward the
338 // base entity through to their results, so treat them as pass-through when
339 // the base entity is the value being tracked.
340 if (acc::PartialEntityAccessOpInterface partialAccess =
341 dyn_cast<acc::PartialEntityAccessOpInterface>(userOp)) {
342 if (partialAccess.getBaseEntity() == trackedOperand) {
343 passThroughResults.append(userOp->result_begin(), userOp->result_end());
344 return true;
345 }
346 return false;
347 }
348 return false;
349}
350
351/// Skips memref view/cast chains to reach the underlying buffer.
352static Value unwrapMemRefConversion(Value v) {
353 while (Operation *op = v.getDefiningOp()) {
354 if (ViewLikeOpInterface viewLike = dyn_cast<ViewLikeOpInterface>(op)) {
355 if (isa<MemRefType>(viewLike.getViewSource().getType()) ||
356 isa<MemRefType>(viewLike.getViewDest().getType())) {
357 v = viewLike.getViewSource();
358 continue;
359 }
360 }
361 break;
362 }
363 return v;
364}
365
366/// Walks back from a memref use to its defining `acc.private_local`, if any,
367/// looking through view/cast ops.
368static acc::PrivateLocalOp getPrivateLocalForMemref(Value memref);
369
370/// Returns the dimensions that own \p privateLocal.
371static GPUParallelDimsAttr
372getPrivateParDims(acc::PrivateLocalOp privateLocal,
373 acc::ComputeRegionOp computeRegion);
374
375/// True when the storage backing \p privateLocal is thread_x-private. An
376/// unknown scope conservatively counts as per-thread.
377static bool storageHasThreadX(acc::PrivateLocalOp privateLocal,
378 acc::ComputeRegionOp computeRegion) {
379 GPUParallelDimsAttr dims = getPrivateParDims(privateLocal, computeRegion);
380 return !dims || llvm::any_of(dims.getArray(), [](GPUParallelDimAttr d) {
381 return d.isThreadX();
382 });
383}
384
385/// Returns the sole user of \p v, or null if it has zero or multiple uses.
386static Operation *getOnlyUser(Value v) {
387 if (!v.hasOneUse())
388 return nullptr;
389 return *v.user_begin();
390}
391
392/// True when \p privatize is privatized at thread_x parallelism.
393static bool isThreadXPrivatize(PrivatizeOp privatize) {
394 if (GPUParallelDimsAttr parDimsAttr = privatize.getParDimsAttr())
395 return llvm::any_of(parDimsAttr.getArray(),
396 [](GPUParallelDimAttr d) { return d.isThreadX(); });
397 return false;
398}
399
400/// True when \p block is nested in a region that only part of the workgroup
401/// enters.
402static bool isInsideConditional(Block *block) {
403 if (!block)
404 return false;
405 for (Operation *parent = block->getParentOp();
406 parent && !isa<gpu::LaunchOp, FunctionOpInterface>(parent);
407 parent = parent->getParentOp())
408 if (isa<RegionBranchOpInterface>(parent) &&
409 !isa<LoopLikeOpInterface, scf::ExecuteRegionOp>(parent))
410 return true;
411 return false;
412}
413
414/// Emits a workgroup-wide GPU barrier.
415static void emitGPUBarrierWorkgroup(OpBuilder &builder, Location loc) {
416 gpu::BarrierOp::create(builder, loc);
417}
418
419/// Emits a subgroup-scoped GPU barrier.
420static void emitGPUBarrierSubgroup(OpBuilder &builder, Location loc) {
421 gpu::BarrierOp::create(builder, loc, /*address_spaces=*/ArrayAttr{},
422 /*named_barrier=*/Value{},
423 gpu::BarrierScope::Subgroup);
424}
425
426/// Emits a barrier over the lanes of this thread's ThreadY row. \p rowWidth
427/// (blockDim.x) must divide \p subgroupSize so the rows tile a subgroup. A
428/// subgroup-wide barrier would also tie together the other rows sharing it.
429static void emitGPUBarrierRow(OpBuilder &builder, Location loc,
430 int64_t rowWidth, int64_t subgroupSize) {
431 assert(rowWidth > 0 && subgroupSize % rowWidth == 0 &&
432 "row must tile the subgroup");
433 Type i32Ty = builder.getI32Type();
434 int64_t rowsPerSubgroup = subgroupSize / rowWidth;
435
436 // laneBase = (tid.y % rowsPerSubgroup) * rowWidth. Both operands are powers
437 // of two here, since rowWidth divides the subgroup size.
438 Value threadY = gpu::ThreadIdOp::create(builder, loc, builder.getIndexType(),
439 gpu::Dimension::y);
440 Value threadY32 = arith::IndexCastOp::create(builder, loc, i32Ty, threadY);
441 Value rowMask =
442 arith::ConstantIntOp::create(builder, loc, i32Ty, rowsPerSubgroup - 1);
443 Value rowIndex = arith::AndIOp::create(builder, loc, threadY32, rowMask);
444 Value width = arith::ConstantIntOp::create(builder, loc, i32Ty, rowWidth);
445 Value laneBase = arith::MulIOp::create(builder, loc, rowIndex, width);
446
447 Value laneBits = arith::ConstantIntOp::create(builder, loc, i32Ty,
448 (int64_t{1} << rowWidth) - 1);
449 Value mask = arith::ShLIOp::create(builder, loc, laneBits, laneBase);
450 NVVM::SyncWarpOp::create(builder, loc, mask);
451}
452
453/// Lowers a single `acc.compute_region` to GPU dialect IR.
454class ACCCGToGPULowering {
455public:
456 explicit ACCCGToGPULowering(acc::ComputeRegionOp computeRegion,
457 RewriterBase &rewriter,
458 acc::OpenACCSupport &accSupport,
459 const ACCCGToGPUOptions &options)
460 : rewriter(rewriter), computeRegion(computeRegion),
461 accSupport(accSupport), options(options),
462 sharedMemBudget(
463 options.maxWorkgroupSharedMemory,
464 sumExistingSharedMemoryBytes(computeRegion.getRegion())) {}
465
466 /// Main entry point: emit launch (if needed) and lower the region body.
467 LogicalResult rewrite();
468
469 gpu::LaunchOp getLaunch() const { return launch; }
470
471 bool hasFailed = false;
472 bool insideAccumulateGridStride = false;
473 Value reductionSharedBuf;
474 // Reduction-accumulator slot (memref) -> the block-reduced value stored into
475 // it; lets a block combine use the register instead of reloading.
476 llvm::DenseMap<Value, Value> reductionAccumValue;
477 // Combine reloads recorded before accumulates are lowered, patched up after.
478 llvm::SmallVector<std::pair<Value, memref::LoadOp>> pendingCombineReloads;
479
480private:
481 /// Lower a parallel loop to the GPU dimension given by its `acc.par_dims`.
482 void processParallelOp(scf::ParallelOp parallelOp);
483 /// Lower a sequential loop, including any required post-loop barriers.
484 template <typename LoopOp>
485 void processSeqLoop(LoopOp loopOp);
486 /// Lower an `acc.predicate_region` to a predicated `scf.if`.
487 void processPredicateRegion(acc::PredicateRegionOp interOp);
488 /// Materialize storage for an `acc.private_local`.
489 void
490 processPrivateLocal(acc::PrivateLocalOp privateLocal,
491 std::optional<int64_t> sharedMemCopies = std::nullopt);
492 /// Lower an `acc.privatize` to device storage.
493 Value processPrivatize(acc::PrivatizeOp privatize);
494 /// Heap-allocate privatize storage and insert a matching dealloc.
495 Value allocatePrivatizeHeapStorage(acc::PrivatizeOp privatize,
496 MemRefType baseTy,
497 ValueRange mappedDynamicSizes);
498 /// Clone and lower an `scf.execute_region`.
499 void processExecuteRegion(scf::ExecuteRegionOp op);
500 /// Lower `acc.reduction_accumulate`.
501 void processAccumulateOp(acc::ReductionAccumulateOp op);
502 /// Lower `acc.reduction_accumulate_array`.
503 void processAccumulateArrayOp(acc::ReductionAccumulateArrayOp op);
504 /// Lower `acc.reduction_init`.
505 void processReductionOp(acc::ReductionInitOp op);
506 /// Lower `acc.reduction_combine`.
507 void processReductionCombineOp(acc::ReductionCombineOp op);
508 /// Lower `acc.reduction_combine_region`.
509 void processCombineRegionOp(acc::ReductionCombineRegionOp op);
510 /// Clone a leaf operation into the lowered region.
511 void processGenericOp(Operation *op);
512 /// Clone and recursively lower an operation with nested regions.
513 void processGenericOpWithRegions(Operation *op);
514 /// Dispatch lowering for one operation in the compute-region body.
515 void processOp(Operation *op);
516
517 /// Emit an atomic reduction update to \p memref.
518 void constructAtomicAccumulation(Location loc, Value memref,
519 ValueRange indices, Value input,
520 arith::AtomicRMWKind kind);
521
522 /// Map an ACC reduction operator to an atomic RMW kind.
523 FailureOr<arith::AtomicRMWKind> getReductionKind(acc::ReductionOperator redOp,
524 Type type, Location loc);
525
526 /// Split launch dimensions into those that execute \p op and those that do
527 /// not, for predication and barrier placement.
528 std::pair<SmallVector<mlir::acc::GPUParallelDimAttr>,
529 SmallVector<mlir::acc::GPUParallelDimAttr>>
530 computeActiveAndInactiveParDims(Operation *op, Block *block);
531
532 /// Build a predicate that is true only on inactive parallel dimensions.
533 Value
534 emitPredicate(Location loc,
535 SmallVector<mlir::acc::GPUParallelDimAttr> &inactiveParDims);
536
537 /// True when \p privateLocal may be placed in shared memory; returns the
538 /// number of copies needed, or nullopt if ineligible.
539 std::optional<int64_t>
540 isEligibleForSharedMemory(acc::PrivateLocalOp privateLocal,
541 MemRefType baseTy);
542
543 /// Reserve \p bytes from the shared-memory budget.
544 bool tryAllocateSharedMemory(int64_t bytes);
545
546 /// Record \p privateLocal as given per-thread storage, for the closing
547 /// remark. Whether that storage lands in registers or in local memory is
548 /// decided later, so the remark reports the allocation, not the placement.
549 /// Privates with no recoverable name or compiler temps are skipped.
550 void recordThreadPrivate(acc::PrivateLocalOp privateLocal);
551
552 /// Element size in bytes for \p elementType .
553 int64_t getElementSizeInBytes(Location loc, Type elementType) const;
554
555 /// True when a static privatization fits in the per-thread stack budget.
556 bool canUseStackAlloca(MemRefType baseTy, Location loc,
557 int64_t maxThreadPrivateStack) const;
558
559 /// Emit a barrier scoped to the parallel dimensions in \p parDimsAttr.
560 void createBarrier(Location loc, mlir::acc::GPUParallelDimsAttr parDimsAttr);
561
562 /// Emit a per-row (per-worker) barrier.
563 /// Runtime branch on blockDim.y == 1 (workgroup-wide); compile-time choice
564 /// between gpu.barrier scope<subgroup> (staticBlockDimX <= subgroupSize)
565 /// and a named gpu.barrier (staticBlockDimX > subgroupSize) with tid.y+1.
566 void createPerRowBarrier(Location loc);
567
568 /// Insert barriers after a sequential loop when shared private state must be
569 /// visible to later loops.
570 void createBarrierAfterSeqLoop(Operation *loopOp);
571
572 /// Flush any deferred post-loop barriers that precede \p beforeOp.
573 void flushDeferredBarriersBefore(Operation *beforeOp);
574
575 /// True when \p loopOp may write shared memory read by a later sibling loop.
576 bool mayWriteSharedMemory(Operation *loopOp);
577
578 /// Parallelism scope (thread, worker, or gang) of a privatized variable.
579 PrivateMemScope getPrivateMemScope(acc::PrivatizeOp privatizeOp);
580
581 /// Parallelism scope of the private buffer backing \p memref.
582 PrivateMemScope getPrivateScopeForMemref(Value memref);
583
584 /// `acc.privatize` that materialized the private buffer for \p memref.
585 acc::PrivatizeOp getPrivatizeForMemref(Value memref);
586
587 /// Collect privatize ops of the shared private buffers stored to under \p
588 /// root, keeping the gang- and worker-private ones apart: the two scopes need
589 /// different barriers, so a store at one scope must not stand in for the
590 /// other.
591 void collectSharedPrivateStores(
592 Operation *root, llvm::SmallPtrSetImpl<Operation *> &gangPrivatizes,
593 llvm::SmallPtrSetImpl<Operation *> &workerPrivatizes);
594
595 /// True when \p memref is backed by one of \p storePrivatizes at \p
596 /// storeScope.
597 bool isMatchingPrivateMemref(
598 Value memref, PrivateMemScope storeScope,
599 const llvm::SmallPtrSetImpl<Operation *> &storePrivatizes);
600
601 /// True when a parallel region under \p seqLoopOp, outside \p skipOp, uses a
602 /// private buffer at \p storeScope backed by one of \p storePrivatizes.
603 bool hasParallelPrivateReuse(
604 LoopLikeOpInterface seqLoopOp, Operation *skipOp,
605 PrivateMemScope storeScope,
606 const llvm::SmallPtrSetImpl<Operation *> &storePrivatizes);
607
608 /// Whether a predicate region needs a barrier before stores that will be read
609 /// by a later parallel loop over the same private memory.
610 PrivateMemScope needsPreStoreReuseBarrier(acc::PredicateRegionOp interOp);
611
612 /// Whether \p loopOp needs a barrier at the top of its body because a
613 /// gang-private slot it writes is reused by the next iteration.
614 bool needsInLoopReuseBarrier(Operation *loopOp);
615
616 /// Emit `gpu.all_reduce` for a reduction partial.
617 void createGPUAllReduceOp(Location loc, Value input, Value memref,
618 arith::AtomicRMWKind kind,
619 mlir::acc::GPUParallelDimsAttr parDimsAttr,
620 ValueRange indices = {},
621 bool isPerThreadPrivateTarget = false);
622
623 /// Finish lowering a deferred `acc.reduction_accumulate`.
624 void postprocessAccumulateOp(acc::ReductionAccumulateOp op);
625
626 /// Finish lowering reductions attached to a parallel loop.
627 void postprocessLoopReduction(scf::ParallelOp parLoop);
628
629 /// Populate block/thread id and grid/block dimension maps for device
630 /// routines.
631 static void
632 createForAllDimensions(RewriterBase &rewriter, Location loc,
633 llvm::DenseMap<gpu::Processor, Value> &ids,
634 llvm::DenseMap<gpu::Processor, Value> &dims) {
635 ids[gpu::Processor::BlockX] = gpu::BlockIdOp::create(
636 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
637 ids[gpu::Processor::BlockY] = gpu::BlockIdOp::create(
638 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
639 ids[gpu::Processor::BlockZ] = gpu::BlockIdOp::create(
640 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::z);
641 ids[gpu::Processor::ThreadX] = gpu::ThreadIdOp::create(
642 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
643 ids[gpu::Processor::ThreadY] = gpu::ThreadIdOp::create(
644 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
645 ids[gpu::Processor::ThreadZ] = gpu::ThreadIdOp::create(
646 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::z);
647 dims[gpu::Processor::BlockX] = gpu::GridDimOp::create(
648 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
649 dims[gpu::Processor::BlockY] = gpu::GridDimOp::create(
650 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
651 dims[gpu::Processor::BlockZ] = gpu::GridDimOp::create(
652 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::z);
653 dims[gpu::Processor::ThreadX] = gpu::BlockDimOp::create(
654 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
655 dims[gpu::Processor::ThreadY] = gpu::BlockDimOp::create(
656 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
657 dims[gpu::Processor::ThreadZ] = gpu::BlockDimOp::create(
658 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::z);
659 }
660
661 /// Return the compute-region block argument for \p outside, adding an `ins`
662 /// operand when needed.
663 BlockArgument getOrAppendInsBlockArg(Value outside) {
664 if (std::optional<BlockArgument> blockArg =
665 computeRegion.getBlockArg(outside)) {
666 return *blockArg;
667 }
668 return computeRegion.appendInputArg(outside);
669 }
670
671 /// Wire dynamic privatization extents into the compute region as `ins` args.
672 void preparePrivatizeExtentInsOperands() {
673 computeRegion.walk([&](acc::PrivateLocalOp privateLocal) {
674 acc::PrivatizeOp privatizeOp =
675 getPrivatizeOp(privateLocal, computeRegion);
676 if (privatizeOp->getParentOfType<acc::ComputeRegionOp>() == computeRegion)
677 return;
678 for (Value extent : privatizeOp.getDynamicSizes())
679 getOrAppendInsBlockArg(extent);
680 });
681 }
682
683 /// Resolve dynamic size operands for a privatized array.
684 SmallVector<Value>
685 resolvePrivateLocalDynamicExtents(acc::PrivateLocalOp privateLocal) {
686 acc::PrivatizeOp privatizeOp = getPrivatizeOp(privateLocal, computeRegion);
687 SmallVector<Value> extents;
688 for (Value extent : privatizeOp.getDynamicSizes()) {
689 if (std::optional<BlockArgument> blockArg =
690 computeRegion.getBlockArg(extent)) {
691 extents.push_back(mapping.lookupOrDefault(*blockArg));
692 continue;
693 }
694 extents.push_back(mapping.lookupOrDefault(extent));
695 }
696 return extents;
697 }
698
699 RewriterBase &rewriter;
700 acc::ComputeRegionOp computeRegion;
701
702 acc::OpenACCSupport &accSupport;
703 const ACCCGToGPUOptions &options;
704 gpu::LaunchOp launch;
705 IRMapping mapping;
706 llvm::SmallVector<scf::ParallelOp> loopReductions;
707 llvm::DenseMap<gpu::Processor, Value> threadIdMap;
708 llvm::DenseMap<gpu::Processor, Value> dimensionMap;
709 // True if ThreadY reduction exists, which triggers subgroup alignment
710 bool hasThreadYReduction = false;
711 // True if a ThreadX reduction exists, whose shuffles stay inside a row
712 bool hasThreadXReduction = false;
713 // True if an array reduction spans any thread dimension
714 bool hasThreadLevelArrayReduction = false;
715 // True if any ThreadX routine call exists in the kernel
716 bool hasThreadLevelRoutineCall = false;
717 // True when a per-row ThreadY barrier is emitted
718 bool hasThreadYBarrier = false;
719
720 // Reusable privatize broadcast slots per type; disabled for kernels.
721 llvm::DenseMap<Type, Value> privatizeBroadcastCache;
722
723 int64_t staticBlockDimX = 1024;
724 int64_t staticBlockDimY = 1024;
725 int64_t staticBlockDimZ = 1024;
726
727 /// True when the launch is (1, N, 1) with 1 < N <= subgroupSize, so every
728 /// worker owns one thread. N is capped by the worker-indexed shared buffers,
729 /// which hold subgroupSize entries.
730 bool isSingleThreadWorkerLaunch() const {
731 return staticBlockDimX == 1 && staticBlockDimZ == 1 &&
732 staticBlockDimY > 1 && staticBlockDimY <= options.subgroupSize;
733 }
734
735 /// True when only worker reductions would need the rows aligned. They combine
736 /// their partials in the lowest N threads of the block, not within a row, so
737 /// a row may sit anywhere in a subgroup. Array accumulates use atomics
738 /// instead and still need alignment.
739 bool isWorkerOnlyShuffleLaunch() const {
740 return hasThreadYReduction && !hasThreadXReduction &&
741 !hasThreadLevelArrayReduction && staticBlockDimZ == 1 &&
742 staticBlockDimY > 1 && staticBlockDimY <= options.subgroupSize &&
743 options.subgroupSize % staticBlockDimX == 0;
744 }
745
746 acc::DefaultACCToGPUMappingPolicy defaultPolicy;
747 SharedMemoryBudget sharedMemBudget;
748 SmallVector<std::string> sharedMemPrivateVarNames;
749 SmallVector<std::string> threadPrivateVarNames;
750 llvm::SmallVector<Operation *, 4> deferredBarrierSeqLoops;
751
752 Value getThreadId(Location loc, gpu::Dimension dim) {
753 return gpu::ThreadIdOp::create(rewriter, loc, rewriter.getIndexType(), dim);
754 }
755
756 Value getBlockDim(Location loc, gpu::Dimension dim) {
757 return gpu::BlockDimOp::create(rewriter, loc, rewriter.getIndexType(), dim);
758 }
759
760 /// Thread id for \p proc, from the launch op or the routine context map.
761 Value getGPUThreadIdFor(gpu::Processor proc) {
762 return getGPUThreadId(proc, getLaunch(), threadIdMap);
763 }
764
765 /// Grid/block dimension for \p proc, from the launch op or routine map.
766 Value getGPUSizeFor(gpu::Processor proc) {
767 return getGPUSize(proc, getLaunch(), dimensionMap);
768 }
769};
770
771int64_t ACCCGToGPULowering::getElementSizeInBytes(Location loc,
772 Type elementType) const {
773 ModuleOp module = computeRegion->getParentOfType<ModuleOp>();
774 if (std::optional<acc::TypeSizeAndAlignment> sizeAndAlignment =
775 accSupport.getTypeSizeAndAlignment(elementType, module)) {
776 return sizeAndAlignment->first.getFixedValue();
777 }
778 std::string msg;
779 llvm::raw_string_ostream os(msg);
780 os << "element size computation for unsupported type: " << elementType;
781 (void)accSupport.emitNYI(loc, os.str());
782 return 0;
783}
784
785bool ACCCGToGPULowering::canUseStackAlloca(
786 MemRefType baseTy, Location loc, int64_t maxThreadPrivateStack) const {
787 for (int64_t dim : baseTy.getShape())
788 if (dim == ShapedType::kDynamic)
789 return false;
790 int64_t elementSize = getElementSizeInBytes(loc, baseTy.getElementType());
791 int64_t numElements = 1;
792 for (int64_t dim : baseTy.getShape()) {
793 if (numElements > maxThreadPrivateStack / std::max<int64_t>(dim, 1))
794 return false;
795 numElements *= dim;
796 }
797 return elementSize * numElements < maxThreadPrivateStack;
798}
799
800/// True if the accumulate spans a block dim or is nested in a block-mapped
801/// loop, i.e. each block owns the elements it reduces across threads. A
802/// thread-only accumulate with no block context grid-strides its element loop
803/// onto blocks, so per-thread partials would be dropped; such reductions must
804/// stay shared.
805static bool reductionHasBlockContext(acc::ReductionAccumulateArrayOp accArr) {
806 auto hasBlock = [](mlir::acc::GPUParallelDimsAttr parDims) {
807 return parDims && llvm::any_of(parDims.getArray(),
808 [](auto pd) { return pd.isAnyBlock(); });
809 };
810 if (hasBlock(accArr.getParDimsAttr()))
811 return true;
812 for (scf::ParallelOp loop = accArr->getParentOfType<scf::ParallelOp>(); loop;
813 loop = loop->getParentOfType<scf::ParallelOp>()) {
814 if (hasBlock(mlir::acc::getParDimsAttr(loop)))
815 return true;
816 }
817 return false;
818}
819
820/// Returns the array reduction accumulate (through cast/view ops) that \p v
821/// feeds if it needs per-thread storage: its par_dims include a thread dim
822/// and it has block context so the cross-thread all_reduce is well defined.
823static acc::ReductionAccumulateArrayOp perThreadArrayReductionAccum(Value v) {
824 SmallVector<Value> worklist{v};
825 DenseSet<Value> seen;
826 while (!worklist.empty()) {
827 Value cur = worklist.pop_back_val();
828 if (!seen.insert(cur).second)
829 continue;
830 for (Operation *user : cur.getUsers()) {
831 if (acc::ReductionAccumulateArrayOp accArr =
832 dyn_cast<acc::ReductionAccumulateArrayOp>(user)) {
833 bool hasThread = false;
834 for (auto pd : accArr.getParDims().getArray())
835 hasThread |= pd.isAnyThread();
836 if (hasThread && reductionHasBlockContext(accArr))
837 return accArr;
838 continue;
839 }
840 SmallVector<Value> through;
841 if (getPassThroughResults(user, cur, through))
842 worklist.append(through.begin(), through.end());
843 else if (isa<ViewLikeOpInterface>(user))
844 worklist.append(user->result_begin(), user->result_end());
845 }
846 }
847 return nullptr;
848}
849
850/// Store the reduction identity to every element of a freshly allocated
851/// per-thread array accumulator so all lanes start from identity (the original
852/// init loop may only run on one lane).
853static void initPerThreadArrayAccum(OpBuilder &b, Location loc, Value alloca,
854 MemRefType baseTy,
855 arith::AtomicRMWKind kind) {
856 assert(baseTy.getRank() > 0 && baseTy.hasStaticShape() &&
857 "per-thread array reduction accumulator must be static ranked");
858 Value ident = createIdentityValue(b, loc, baseTy.getElementType(), kind,
859 /*useOnlyFiniteValue=*/true);
860 Value lb = arith::ConstantIndexOp::create(b, loc, 0);
861 Value step = arith::ConstantIndexOp::create(b, loc, 1);
862 SmallVector<Value> indices;
863 auto buildLoopNest = [&](auto &&self, unsigned dim) -> void {
864 if (dim == baseTy.getRank()) {
865 memref::StoreOp::create(b, loc, ident, alloca, indices);
866 return;
867 }
868
869 Value ub = arith::ConstantIndexOp::create(b, loc, baseTy.getShape()[dim]);
870 auto forOp = scf::ForOp::create(b, loc, lb, ub, step);
871 OpBuilder::InsertionGuard g(b);
872 b.setInsertionPoint(forOp.getBody()->getTerminator());
873 indices.push_back(forOp.getInductionVar());
874 self(self, dim + 1);
875 indices.pop_back();
876 };
877 buildLoopNest(buildLoopNest, 0);
878}
879
880std::optional<int64_t>
881ACCCGToGPULowering::isEligibleForSharedMemory(acc::PrivateLocalOp privateLocal,
882 MemRefType baseTy) {
883 // Cross-thread array reduction accumulators must stay per-thread when their
884 // storage scope includes thread_x. Gang-/worker-scoped array temps remain
885 // eligible for shared memory.
886 if (perThreadArrayReductionAccum(privateLocal.getResult()) &&
887 storageHasThreadX(privateLocal, computeRegion))
888 return std::nullopt;
889 ModuleOp module = computeRegion->getParentOfType<ModuleOp>();
890 FailureOr<bool> isCandidate = isPrivateLocalSharedMemoryCandidate(
891 privateLocal, computeRegion, module, defaultPolicy, &accSupport);
892 if (failed(isCandidate)) {
893 hasFailed = true;
894 return std::nullopt;
895 }
896 if (!isCandidate.value())
897 return std::nullopt;
898 std::optional<int64_t> upperBound =
899 getPrivateLocalSharedMemoryUpperBoundBytes(privateLocal, computeRegion,
900 module, defaultPolicy);
901 assert(upperBound && "candidate private_local must have an upper bound");
902 int64_t elementSize =
903 getElementSizeInBytes(privateLocal.getLoc(), baseTy.getElementType());
904 int64_t numElements = 1;
905 for (int64_t dim : baseTy.getShape())
906 numElements *= dim;
907 return *upperBound / (elementSize * numElements);
908}
909
910bool ACCCGToGPULowering::tryAllocateSharedMemory(int64_t bytes) {
911 return sharedMemBudget.tryAllocate(bytes);
912}
913
914void ACCCGToGPULowering::recordThreadPrivate(acc::PrivateLocalOp privateLocal) {
915 std::string varName = accSupport.getVariableName(privateLocal.getResult());
916 // A name that could not be recovered, or a compiler temp are skipped.
917 if (varName.empty())
918 return;
919 // The same variable reaches this point once per acc.private_local it has.
920 if (!llvm::is_contained(threadPrivateVarNames, varName))
921 threadPrivateVarNames.push_back(varName);
922}
923
924FailureOr<arith::AtomicRMWKind>
925ACCCGToGPULowering::getReductionKind(acc::ReductionOperator redOp, Type type,
926 Location loc) {
927 if (std::optional<arith::AtomicRMWKind> kind =
929 return *kind;
930
931 std::string msg;
932 llvm::raw_string_ostream os(msg);
933 os << "reduction operator (" << redOp << ") for type " << type;
934 (void)accSupport.emitNYI(loc, os.str());
935 return failure();
936}
937
938LogicalResult ACCCGToGPULowering::rewrite() {
939
940 // Pre-compute if thread-level reductions exist. ThreadY reduction generates
941 // shuffles which require subgroup alignment (blockDim.x = subgroupSize),
942 // meaning ThreadX lanes exist even without explicit ThreadX parallelism.
943 // ThreadX is tracked too, to tell worker-only reductions from row shuffles.
944 computeRegion->walk([&](acc::ReductionAccumulateOp op) {
945 for (auto parDim : op.getParDimsAttr().getArray()) {
946 hasThreadXReduction |= parDim.isThreadX();
947 hasThreadYReduction |= parDim.isThreadY();
948 }
949 });
950 computeRegion->walk([&](acc::ReductionAccumulateArrayOp op) {
951 for (auto parDim : op.getParDimsAttr().getArray()) {
952 if (!parDim.isAnyBlock())
953 hasThreadLevelArrayReduction = true;
954 }
955 });
956
957 // Pre-compute if any thread-level (vector or worker) routine call exists.
958 // Such routines partition work across ThreadX/ThreadY and emit workgroup-wide
959 // barriers internally (e.g. for shared memory alloca synchronization), so all
960 // workgroup threads must reach the call site for those barriers to converge.
961 computeRegion->walk([&](CallOpInterface callOp) -> WalkResult {
962 if (mlir::acc::GPUParallelDimAttr parDim =
963 getAccRoutineCallParDim(callOp, defaultPolicy)) {
964 if (parDim.isThreadX() || parDim.isThreadY()) {
965 hasThreadLevelRoutineCall = true;
966 return WalkResult::interrupt();
967 }
968 }
969 return WalkResult::advance();
970 });
971
972 Location loc = computeRegion->getLoc();
973 Value constantOne = arith::ConstantIndexOp::create(rewriter, loc, 1);
974
975 auto launchArgument = [&](gpu::Processor processor) -> Value {
976 mlir::acc::GPUParallelDimAttr parDim = mlir::acc::GPUParallelDimAttr::get(
977 computeRegion->getContext(), processor);
978 std::optional<Value> maybeLaunchArg =
979 computeRegion.getKnownLaunchArg(parDim);
980 LLVM_DEBUG(llvm::dbgs() << "ACCCGToGPU: launch-arg: "
981 << " parDim: " << parDim << " gpu: " << processor
982 << " widthValue: "
983 << maybeLaunchArg.value_or(constantOne) << "\n");
984
986 rewriter, loc, rewriter.getIndexType(),
987 maybeLaunchArg.value_or(constantOne));
988 };
989 LLVM_DEBUG(llvm::dbgs() << "ACCCGToGPU: creating gpu launch op: \n");
990
991 // acc.compute_region keeps launch argument as block argument, for rewriting
992 // we now replace these with gpu.launch dimensions.
993 auto mapLaunchArguments = [&](gpu::Processor processor, Value launchArg) {
994 mlir::acc::GPUParallelDimAttr parDim = mlir::acc::GPUParallelDimAttr::get(
995 computeRegion->getContext(), processor);
996 std::optional<Value> kernelArg = computeRegion.getLaunchArg(parDim);
997 if (kernelArg)
998 mapping.map(computeRegion.gpuParWidth(processor), launchArg);
999 };
1000
1001 llvm::StringRef blockDimXName = "blockDim.x";
1002 llvm::StringRef blockDimYName = "blockDim.y";
1003 std::string deviceLabel = getDeviceRemarkQualifier(options.deviceType);
1004
1005 if (!computeRegion->getParentOfType<gpu::GPUFuncOp>()) {
1006 Value blockDimX = launchArgument(gpu::Processor::ThreadX);
1007 APInt bdxVal;
1008 if (matchPattern(blockDimX, m_ConstantInt(&bdxVal)))
1009 staticBlockDimX = bdxVal.getSExtValue();
1010 Value blockDimY = launchArgument(gpu::Processor::ThreadY);
1011 APInt bdyVal;
1012 if (matchPattern(blockDimY, m_ConstantInt(&bdyVal)))
1013 staticBlockDimY = bdyVal.getSExtValue();
1014 Value blockDimZ = launchArgument(gpu::Processor::ThreadZ);
1015 APInt bdzVal;
1016 if (matchPattern(blockDimZ, m_ConstantInt(&bdzVal)))
1017 staticBlockDimZ = bdzVal.getSExtValue();
1018 Value gridDimX = launchArgument(gpu::Processor::BlockX);
1019 Value gridDimY = launchArgument(gpu::Processor::BlockY);
1020 Value gridDimZ = launchArgument(gpu::Processor::BlockZ);
1021
1022 // The format of the message is:
1023 // Generating [serial] {deviceLabel} code with gridDim=32x1x1
1024 // blockDim=256x1x1
1025 accSupport.emitRemark(computeRegion, [&]() {
1026 auto getName = [&](Value val) -> std::string {
1027 std::string name = accSupport.getVariableName(val);
1028 return name.empty() ? "(*)" : name;
1029 };
1030 bool isEffectivelySerial =
1031 sameEffectiveValue(blockDimX, 1) &&
1032 sameEffectiveValue(blockDimY, 1) &&
1033 sameEffectiveValue(blockDimZ, 1) && sameEffectiveValue(gridDimX, 1) &&
1034 sameEffectiveValue(gridDimY, 1) && sameEffectiveValue(gridDimZ, 1);
1035 return (llvm::Twine("Generating ") +
1036 llvm::Twine(isEffectivelySerial ? "serial " : "") + deviceLabel +
1037 " code with gridDim=" + getName(gridDimX) + "x" +
1038 getName(gridDimY) + "x" + getName(gridDimZ) +
1039 " blockDim=" + getName(blockDimX) + "x" + getName(blockDimY) +
1040 "x" + getName(blockDimZ))
1041 .str();
1042 });
1043
1044 // Check if kernel has a stream operand for async execution
1045 if (mlir::Value streamValue = computeRegion.getStream()) {
1046 LLVM_DEBUG(llvm::dbgs()
1047 << "\nDEBUG: Creating async gpu.launch with stream: "
1048 << streamValue << "\n");
1049 launch = gpu::LaunchOp::create(
1050 rewriter, loc, gridDimX, gridDimY, gridDimZ, blockDimX, blockDimY,
1051 blockDimZ,
1052 /*dynamicSharedMemorySize=*/mlir::Value{},
1053 /*asyncTokenType=*/
1055 // Add the stream as an async dependency
1056 launch.getAsyncDependenciesMutable().append(streamValue);
1057 } else {
1058 LLVM_DEBUG(llvm::dbgs()
1059 << "\nDEBUG: No stream, creating sync gpu.launch\n");
1060 launch = gpu::LaunchOp::create(rewriter, loc, gridDimX, gridDimY,
1061 gridDimZ, blockDimX, blockDimY, blockDimZ);
1062 }
1063
1064 // Transfer kernel function name and module name from acc.compute_region to
1065 // gpu.launch if present
1066 if (auto kernelFuncName = computeRegion.getKernelFuncNameAttr())
1067 launch.setFunctionAttr(kernelFuncName);
1068 if (auto kernelModuleName = computeRegion.getKernelModuleNameAttr())
1069 launch.setModuleAttr(kernelModuleName);
1070
1071 rewriter.setInsertionPointToEnd(&launch.getBody().front());
1072 gpu::TerminatorOp::create(rewriter, loc);
1073 rewriter.setInsertionPointToStart(&launch.getBody().front());
1074 mapLaunchArguments(gpu::Processor::BlockX,
1075 gpu::GridDimOp::create(rewriter, loc,
1076 rewriter.getIndexType(),
1077 gpu::Dimension::x));
1078 mapLaunchArguments(gpu::Processor::BlockY,
1079 gpu::GridDimOp::create(rewriter, loc,
1080 rewriter.getIndexType(),
1081 gpu::Dimension::y));
1082 mapLaunchArguments(gpu::Processor::BlockZ,
1083 gpu::GridDimOp::create(rewriter, loc,
1084 rewriter.getIndexType(),
1085 gpu::Dimension::z));
1086 mapLaunchArguments(gpu::Processor::ThreadX,
1087 gpu::BlockDimOp::create(rewriter, loc,
1088 rewriter.getIndexType(),
1089 gpu::Dimension::x));
1090 mapLaunchArguments(gpu::Processor::ThreadY,
1091 gpu::BlockDimOp::create(rewriter, loc,
1092 rewriter.getIndexType(),
1093 gpu::Dimension::y));
1094 mapLaunchArguments(gpu::Processor::ThreadZ,
1095 gpu::BlockDimOp::create(rewriter, loc,
1096 rewriter.getIndexType(),
1097 gpu::Dimension::z));
1098 } else {
1099 // Do not create gpu.launch for acc routine and map
1100 // to block/thread index and block/grid size instead
1101 // of launch arguments, using created maps.
1102 OpBuilder::InsertionGuard guard(rewriter);
1103 rewriter.setInsertionPointToStart(computeRegion->getBlock());
1104 createForAllDimensions(rewriter, loc, threadIdMap, dimensionMap);
1105 mapLaunchArguments(gpu::Processor::BlockX,
1106 dimensionMap[gpu::Processor::BlockX]);
1107 mapLaunchArguments(gpu::Processor::BlockY,
1108 dimensionMap[gpu::Processor::BlockY]);
1109 mapLaunchArguments(gpu::Processor::BlockZ,
1110 dimensionMap[gpu::Processor::BlockZ]);
1111 mapLaunchArguments(gpu::Processor::ThreadX,
1112 dimensionMap[gpu::Processor::ThreadX]);
1113 mapLaunchArguments(gpu::Processor::ThreadY,
1114 dimensionMap[gpu::Processor::ThreadY]);
1115 mapLaunchArguments(gpu::Processor::ThreadZ,
1116 dimensionMap[gpu::Processor::ThreadZ]);
1117 }
1118
1119 // Map input arguments for compute region; we go from an IsolatedFromAbove
1120 // operation to gpu.launch which is not IsolatedFromAbove.
1121 preparePrivatizeExtentInsOperands();
1122 Block *body = computeRegion.getBody();
1123 unsigned numLaunchArgs = computeRegion.getLaunchArgs().size();
1124 ValueRange inputArgs = computeRegion.getInputArgs();
1125 for (unsigned i = numLaunchArgs; i < body->getNumArguments(); ++i)
1126 mapping.map(body->getArgument(i), inputArgs[i - numLaunchArgs]);
1127
1128 assert(computeRegion.getRegion().hasOneBlock() &&
1129 "compute region only supports one block region for now");
1130 // process all operations inside kernel region
1131 for (auto &op : computeRegion.getRegion().getBlocks().front().getOperations())
1132 processOp(&op);
1133
1134 for (auto &parLoop : loopReductions)
1135 postprocessLoopReduction(parLoop);
1136
1137 // Replace combine reloads of a reduction slot with the block-reduced value.
1138 // Only when it dominates the reload; otherwise keep the reload.
1139 if (!pendingCombineReloads.empty() && launch) {
1140 DominanceInfo domInfo(launch);
1141 for (auto &[slot, loadOp] : pendingCombineReloads) {
1142 llvm::DenseMap<Value, Value>::iterator it =
1143 reductionAccumValue.find(slot);
1144 if (it == reductionAccumValue.end())
1145 continue;
1146 if (!domInfo.dominates(it->second, loadOp.getOperation()))
1147 continue;
1148 rewriter.replaceOp(loadOp, ValueRange{it->second});
1149 }
1150 }
1151
1152 if (launch) {
1153 const int64_t subgroupSize = options.subgroupSize;
1154 const int64_t subgroupAlignMask = subgroupSize - 1;
1155
1156 // Adjust blockDim.x to be a multiple of subgroupSize. This is required
1157 // because:
1158 // - Subgroup reductions (gpu.all_reduce) require full subgroups
1159 // - Per-row workgroup barriers require blockDim.x aligned to subgroupSize
1160 // Tracked apart because only a ThreadX reduction shuffles within the row,
1161 // so only it needs the row to start at a subgroup boundary.
1162 bool needsThreadXAlign = false;
1163 bool needsThreadYAlign = false;
1164 bool alignThreadXReduction =
1165 getConstantIntValue(launch.getBlockSizeY()) != 1 ||
1166 getConstantIntValue(launch.getBlockSizeZ()) != 1;
1167
1168 // A reduction over both dimensions counts as a ThreadX one: it still
1169 // shuffles ThreadX lanes within the row.
1170 auto classifyAllReduce = [&](gpu::AllReduceOp allReduce) {
1171 bool hasThreadX = false;
1172 bool hasThreadY = false;
1173 for (auto parDim : mlir::acc::getParDimsAttr(allReduce).getArray()) {
1174 hasThreadX |= parDim.isThreadX();
1175 hasThreadY |= parDim.isThreadY();
1176 }
1177 if (hasThreadX && alignThreadXReduction)
1178 needsThreadXAlign = true;
1179 else if (hasThreadY)
1180 needsThreadYAlign = true;
1181 };
1182
1183 launch.walk(classifyAllReduce);
1184 // Also check if called routines have thread-level reductions.
1185 launch.walk([&](func::CallOp callOp) {
1186 if (gpu::GPUFuncOp callee =
1187 callOp->getParentOfType<ModuleOp>().lookupSymbol<gpu::GPUFuncOp>(
1188 callOp.getCallee()))
1189 callee.walk(classifyAllReduce);
1190 });
1191
1192 bool isShuffleEnabled = needsThreadXAlign || needsThreadYAlign;
1193
1194 std::optional<int64_t> constBlockDimX =
1195 getConstantIntValue(launch.getBlockSizeX());
1196 std::optional<int64_t> constBlockDimY =
1197 getConstantIntValue(launch.getBlockSizeY());
1198 std::optional<int64_t> constBlockDimZ =
1199 getConstantIntValue(launch.getBlockSizeZ());
1200
1201 // Skip subgroup alignment only when the total thread count is already
1202 // below a subgroup (constant blockDim.x in 2..subgroupSize-1 and
1203 // constant blockDim.y/z == 1). If blockDim.y/z > 1 or is unknown,
1204 // padding blockDim.x to a subgroup is still required so subgroups don't
1205 // cross row boundaries for row-local shuffle/ThreadY-barrier reductions.
1206 bool skipAlign = constBlockDimX && constBlockDimY && constBlockDimZ &&
1207 *constBlockDimX > 1 && *constBlockDimX < subgroupSize &&
1208 *constBlockDimY == 1 && *constBlockDimZ == 1;
1209
1210 // Padding a worker-only launch divides blockDim.y by the same factor and
1211 // leaves the folded-away workers as ThreadX lanes redoing each other's
1212 // work, so keep its shape instead.
1213 if (isSingleThreadWorkerLaunch())
1214 skipAlign = true;
1215 if (needsThreadYAlign && !needsThreadXAlign && !hasThreadYBarrier &&
1216 isWorkerOnlyShuffleLaunch())
1217 skipAlign = true;
1218
1219 if ((isShuffleEnabled || hasThreadYBarrier) && !skipAlign) {
1220 rewriter.setInsertionPoint(launch);
1221
1222 Value curBlockDimX = launch.getBlockSizeX();
1223 Value curBlockDimY = launch.getBlockSizeY();
1224 Value curBlockDimZ = launch.getBlockSizeZ();
1225
1226 // Emit a report on changing parallelism.
1227 accSupport.emitRemark(computeRegion, [&]() {
1228 auto getName = [&](Value val) -> std::string {
1229 std::string name = accSupport.getVariableName(val);
1230 return name.empty() ? "(*)" : name;
1231 };
1232 std::string blockDimXValStr = getName(curBlockDimX);
1233 std::string blockDimYValStr = getName(curBlockDimY);
1234 llvm::StringRef kind =
1235 isShuffleEnabled ? "Shuffle reduction" : "ThreadY barrier";
1236 return (llvm::Twine(kind) +
1237 " is generated while adjusting the number of threads into "
1238 "groups of " +
1239 llvm::Twine(subgroupSize) + ".\n\t" + blockDimXName + ": `" +
1240 blockDimXValStr + "` to `((" + blockDimXValStr + " + " +
1241 llvm::Twine(subgroupAlignMask) + ") / " +
1242 llvm::Twine(subgroupSize) + ") * " + llvm::Twine(subgroupSize) +
1243 "`\n" + "\t" + blockDimYName + ": `" + blockDimYValStr +
1244 "` to `max(1, (new-" + blockDimXName + " * " + blockDimYValStr +
1245 ") / new-" + blockDimXName + ")`")
1246 .str();
1247 });
1248
1249 // Update the ThreadX length and the numbers of ThreadY and ThreadZ.
1250 // When the original block dimensions are compile-time
1251 // constants, compute the adjusted dimensions as constants directly so
1252 // that the GpuKernelOutliningPass can set `known_block_size` on the
1253 // outlined gpu.func.
1254 Value newBlockDimX, newBlockDimY, newBlockDimZ;
1255 if (constBlockDimX && constBlockDimY && constBlockDimZ) {
1256 int64_t bdx = *constBlockDimX;
1257 int64_t bdy = *constBlockDimY;
1258 int64_t bdz = *constBlockDimZ;
1259 int64_t alignedBdx =
1260 ((bdx + subgroupAlignMask) / subgroupSize) * subgroupSize;
1261 int64_t numXYThreads = bdx * bdy;
1262 int64_t numThreads = numXYThreads * bdz;
1263 int64_t newBdy = std::max<int64_t>(1, numXYThreads / alignedBdx);
1264 int64_t newBdz =
1265 std::max<int64_t>(1, numThreads / (alignedBdx * newBdy));
1266 newBlockDimX =
1267 arith::ConstantIndexOp::create(rewriter, loc, alignedBdx);
1268 newBlockDimY = arith::ConstantIndexOp::create(rewriter, loc, newBdy);
1269 newBlockDimZ = arith::ConstantIndexOp::create(rewriter, loc, newBdz);
1270 } else {
1271 // numXYThreads = blockDim.x * blockDim.y
1272 Value numXYThreads =
1273 arith::MulIOp::create(rewriter, loc, curBlockDimX, curBlockDimY);
1274 Value numThreads =
1275 arith::MulIOp::create(rewriter, loc, numXYThreads, curBlockDimZ);
1276 // blockDim.x = ((blockDim.x + mask) / subgroupSize) * subgroupSize
1277 Value cstMask =
1278 arith::ConstantIndexOp::create(rewriter, loc, subgroupAlignMask);
1279 Value cstSubgroupSize =
1280 arith::ConstantIndexOp::create(rewriter, loc, subgroupSize);
1281 Value padded =
1282 arith::AddIOp::create(rewriter, loc, curBlockDimX, cstMask);
1283 Value subgroupsRequired =
1284 arith::DivUIOp::create(rewriter, loc, padded, cstSubgroupSize);
1285 newBlockDimX = arith::MulIOp::create(rewriter, loc, subgroupsRequired,
1286 cstSubgroupSize);
1287 // blockDim.y = max(1, numXYThreads / blockDim.x)
1288 Value quotient =
1289 arith::DivUIOp::create(rewriter, loc, numXYThreads, newBlockDimX);
1290 Value cst1 = arith::ConstantIndexOp::create(rewriter, loc, 1);
1291 newBlockDimY = arith::MaxUIOp::create(rewriter, loc, cst1, quotient);
1292 // blockDim.z = max(1, numThreads / (blockDim.x * blockDim.y))
1293 Value newNumXYThreads =
1294 arith::MulIOp::create(rewriter, loc, newBlockDimX, newBlockDimY);
1295 quotient =
1296 arith::DivUIOp::create(rewriter, loc, numThreads, newNumXYThreads);
1297 newBlockDimZ = arith::MaxUIOp::create(rewriter, loc, cst1, quotient);
1298 }
1299
1300 launch.getBlockSizeXMutable().assign(newBlockDimX);
1301 launch.getBlockSizeYMutable().assign(newBlockDimY);
1302 launch.getBlockSizeZMutable().assign(newBlockDimZ);
1303 }
1304 }
1305
1306 if (hasFailed)
1307 return failure();
1308
1309 if (!threadPrivateVarNames.empty()) {
1310 accSupport.emitRemark(computeRegion, [&]() {
1311 return (llvm::Twine("Local memory or registers used for ") +
1312 llvm::join(threadPrivateVarNames, ","))
1313 .str();
1314 });
1315 }
1316
1317 if (!sharedMemPrivateVarNames.empty()) {
1318 accSupport.emitRemark(computeRegion, [&]() {
1319 return (llvm::Twine("GPU shared memory used for ") +
1320 llvm::join(sharedMemPrivateVarNames, ","))
1321 .str();
1322 });
1323 }
1324
1325 rewriter.eraseOp(computeRegion);
1326 return success();
1327}
1328
1329/// True when this accumulate is redundant in a nested reduction chain: the
1330/// value is a load of the destination memref and a sibling
1331/// acc.reduction_combine with block par_dims has already reduced %M across
1332/// threads in the block.
1333///
1334/// %v = memref.load %M[]
1335/// acc.reduction_accumulate %v to %M ...
1336/// acc.reduction_combine %M into %parent ... {block par_dims}
1337///
1338/// Lowering the accumulate again would double-count. Detection is structural;
1339/// nested reductions into per-thread privates do not match because their
1340/// combines are not block-scoped.
1341static bool isRedundantChainAccumulate(acc::ReductionAccumulateOp op) {
1342 Value memref = op.getMemref();
1343 memref::LoadOp loadOp = op.getValue().getDefiningOp<memref::LoadOp>();
1344 if (!loadOp || loadOp.getMemRef() != memref)
1345 return false;
1346 for (Operation *user : memref.getUsers()) {
1347 acc::ReductionCombineOp combineOp = dyn_cast<acc::ReductionCombineOp>(user);
1348 if (!combineOp || combineOp.getDestMemref() != memref)
1349 continue;
1350 SmallVector<mlir::acc::GPUParallelDimAttr> parDims =
1351 getReductionCombineParDims(combineOp);
1352 if (llvm::any_of(parDims, [](mlir::acc::GPUParallelDimAttr d) {
1353 return d.isAnyBlock();
1354 })) {
1355 return true;
1356 }
1357 }
1358 return false;
1359}
1360
1361static GPUParallelDimsAttr
1362getPrivateParDims(acc::PrivateLocalOp privateLocal,
1363 acc::ComputeRegionOp computeRegion) {
1364 if (GPUParallelDimsAttr parDims = acc::getParDimsAttr(privateLocal))
1365 return parDims;
1366 if (acc::PrivatizeOp privatize = getPrivatizeOp(privateLocal, computeRegion))
1367 return privatize.getParDimsAttr();
1368 return {};
1369}
1370
1371/// True when \p privateLocal has one private slot per ThreadY row.
1372static bool isThreadYPrivate(acc::PrivateLocalOp privateLocal, bool allowBlock,
1373 acc::ComputeRegionOp computeRegion) {
1374 if (!privateLocal)
1375 return false;
1376 GPUParallelDimsAttr parDims = getPrivateParDims(privateLocal, computeRegion);
1377 if (!parDims)
1378 return false;
1379 return llvm::any_of(parDims.getArray(),
1380 [](auto dim) { return dim.isThreadY(); }) &&
1381 llvm::all_of(parDims.getArray(), [=](auto dim) {
1382 return dim.isThreadY() || (allowBlock && dim.isAnyBlock());
1383 });
1384}
1385
1386struct ThreadYBroadeningInfo {
1387 bool hasActiveWorkerCombine = false;
1388 bool hasExplicitInactiveCombine = false;
1389 bool hasBroadeningConflict = false;
1390 Operation *diagnosticOp = nullptr;
1391
1392 void merge(const ThreadYBroadeningInfo &other) {
1393 hasActiveWorkerCombine |= other.hasActiveWorkerCombine;
1394 hasExplicitInactiveCombine |= other.hasExplicitInactiveCombine;
1395 hasBroadeningConflict |= other.hasBroadeningConflict;
1396 if (!diagnosticOp)
1397 diagnosticOp = other.diagnosticOp;
1398 }
1399};
1400
1401/// True when executing \p op on additional ThreadY rows may change behavior.
1402static bool hasUnsafeEffectsWhenBroadening(Operation *op) {
1403 if (auto effectOp = dyn_cast<MemoryEffectOpInterface>(op)) {
1404 SmallVector<MemoryEffects::EffectInstance> effects;
1405 effectOp.getEffects(effects);
1406 return llvm::any_of(effects, [](const auto &effect) {
1407 return !isa<MemoryEffects::Read>(effect.getEffect());
1408 });
1409 }
1410 return !op->hasTrait<OpTrait::HasRecursiveMemoryEffects>();
1411}
1412
1413/// True when \p accumulator is the destination of a separate block-scoped
1414/// combine, so it holds a distinct per-worker partial rather than a broadcast.
1415static bool isFedByInnerBlockCombine(acc::PrivateLocalOp accumulator,
1416 Operation *selfCombine) {
1417 if (!accumulator)
1418 return false;
1419 for (Operation *user : accumulator.getResult().getUsers()) {
1420 if (user == selfCombine)
1421 continue;
1422 auto combineOp = dyn_cast<acc::ReductionCombineOp>(user);
1423 if (!combineOp ||
1424 unwrapMemRefConversion(combineOp.getDestMemref()).getDefiningOp() !=
1425 accumulator.getOperation())
1426 continue;
1427 SmallVector<mlir::acc::GPUParallelDimAttr> parDims =
1428 getReductionCombineParDims(combineOp);
1429 if (llvm::any_of(parDims, [](mlir::acc::GPUParallelDimAttr d) {
1430 return d.isAnyBlock();
1431 }))
1432 return true;
1433 }
1434 return false;
1435}
1436
1437/// Records whether \p combineOp requires ThreadY to remain active.
1438static void classifyThreadYCombine(ThreadYBroadeningInfo &info,
1439 Operation *combineOp, Value src, Value dest,
1440 ArrayRef<GPUParallelDimAttr> parDims,
1441 acc::ComputeRegionOp computeRegion) {
1442 bool hasThreadY = llvm::any_of(
1443 parDims, [](GPUParallelDimAttr parDim) { return parDim.isThreadY(); });
1444 bool hasBlock = llvm::any_of(
1445 parDims, [](GPUParallelDimAttr parDim) { return parDim.isAnyBlock(); });
1446 acc::PrivateLocalOp srcPrivate =
1447 unwrapMemRefConversion(src).getDefiningOp<acc::PrivateLocalOp>();
1448 acc::PrivateLocalOp destPrivate =
1449 unwrapMemRefConversion(dest).getDefiningOp<acc::PrivateLocalOp>();
1450 bool hasPrivateDest = isa<acc::ReductionCombineOp>(combineOp) && srcPrivate &&
1451 destPrivate &&
1452 getPrivatizeOp(srcPrivate, computeRegion) !=
1453 getPrivatizeOp(destPrivate, computeRegion);
1454 if (hasThreadY && hasBlock &&
1455 isThreadYPrivate(srcPrivate, hasPrivateDest, computeRegion)) {
1456 info.hasActiveWorkerCombine = true;
1457 return;
1458 }
1459
1460 // A block_y+thread_y accumulator fed by an inner block-scoped combine holds
1461 // a distinct partial per worker row, so its combine runs on every row; a
1462 // plain worker accumulate broadcasts via all_reduce and stays row-zero.
1463 if (hasThreadY && hasBlock &&
1464 isThreadYPrivate(srcPrivate, /*allowBlock=*/true, computeRegion) &&
1465 isFedByInnerBlockCombine(srcPrivate, combineOp)) {
1466 info.hasActiveWorkerCombine = true;
1467 return;
1468 }
1469
1470 info.hasExplicitInactiveCombine = true;
1471 if (!info.diagnosticOp)
1472 info.diagnosticOp = combineOp;
1473}
1474
1475/// ThreadY must remain active for hierarchical combines over worker rows.
1476/// Broadening is rejected when siblings require inactive ThreadY or have side
1477/// effects; nested predicates enforce their own safety.
1478static ThreadYBroadeningInfo
1479analyzeThreadYBroadening(Block &predicateBlock,
1480 acc::ComputeRegionOp computeRegion) {
1481 ThreadYBroadeningInfo info;
1482 for (Operation &nestedOp : predicateBlock) {
1483 if (acc::PredicateRegionOp nestedPredicate =
1484 dyn_cast<acc::PredicateRegionOp>(nestedOp)) {
1485 ThreadYBroadeningInfo nestedInfo = analyzeThreadYBroadening(
1486 nestedPredicate.getRegion().front(), computeRegion);
1487 info.hasActiveWorkerCombine |= nestedInfo.hasActiveWorkerCombine;
1488 continue;
1489 }
1490 if (acc::ReductionCombineOp combineOp =
1491 dyn_cast<acc::ReductionCombineOp>(nestedOp)) {
1492 classifyThreadYCombine(
1493 info, combineOp, combineOp.getSrcMemref(), combineOp.getDestMemref(),
1494 getReductionCombineParDims(combineOp), computeRegion);
1495 continue;
1496 }
1497 if (acc::ReductionCombineRegionOp combineRegionOp =
1498 dyn_cast<acc::ReductionCombineRegionOp>(nestedOp)) {
1499 classifyThreadYCombine(info, combineRegionOp, combineRegionOp.getSrcVar(),
1500 combineRegionOp.getDestVar(),
1501 getReductionCombineParDims(combineRegionOp),
1502 computeRegion);
1503 continue;
1504 }
1505 if (nestedOp.getNumRegions() != 0) {
1506 if (hasUnsafeEffectsWhenBroadening(&nestedOp)) {
1507 info.hasBroadeningConflict = true;
1508 if (!info.diagnosticOp)
1509 info.diagnosticOp = &nestedOp;
1510 }
1511 for (Region &region : nestedOp.getRegions())
1512 for (Block &nestedBlock : region)
1513 info.merge(analyzeThreadYBroadening(nestedBlock, computeRegion));
1514 continue;
1515 }
1516 if (hasUnsafeEffectsWhenBroadening(&nestedOp)) {
1517 info.hasBroadeningConflict = true;
1518 if (!info.diagnosticOp)
1519 info.diagnosticOp = &nestedOp;
1520 }
1521 }
1522 return info;
1523}
1524
1525std::pair<SmallVector<mlir::acc::GPUParallelDimAttr>,
1526 SmallVector<mlir::acc::GPUParallelDimAttr>>
1527ACCCGToGPULowering::computeActiveAndInactiveParDims(Operation *op,
1528 Block *block) {
1529 MLIRContext *ctx = computeRegion->getContext();
1530
1531 mlir::acc::GPUParallelDimAttr routineParDim;
1532 if (isInsideACCSpecializedRoutine(computeRegion)) {
1533 FunctionOpInterface funcOp =
1534 computeRegion->getParentOfType<FunctionOpInterface>();
1535 routineParDim = getSpecializedRoutineDim(funcOp, defaultPolicy);
1536 }
1537
1538 // The active set is precomputed separately from the ownership par_dims for
1539 // privatizations, and a predicate region may likewise state which dims run
1540 // it unpredicated; prefer that attribute when present. The inactive dims are
1541 // the launch dims which are not active.
1542 if (isa<acc::PrivateLocalOp, acc::PrivatizeOp, acc::PredicateRegionOp>(op)) {
1543 if (mlir::acc::ActiveParDimsAttr precomputedActiveParDims =
1545 mlir::acc::GPUParallelDimAttr lowestParDim =
1546 mlir::acc::GPUParallelDimAttr::threadXDim(ctx);
1547
1548 SmallVector<mlir::acc::GPUParallelDimAttr> launchParDims;
1549 if (routineParDim) {
1550 for (mlir::acc::GPUParallelDimAttr parDim = routineParDim;
1551 parDim.getOrder() >= lowestParDim.getOrder();
1552 parDim = parDim.getOneLower()) {
1553 mlir::acc::insertParDim(launchParDims, parDim);
1554 }
1555 } else {
1556 launchParDims = computeRegion.getLaunchParDims();
1557 }
1558
1559 SmallVector<mlir::acc::GPUParallelDimAttr> activeParDims(
1560 precomputedActiveParDims.getArray());
1561 SmallVector<mlir::acc::GPUParallelDimAttr> inactiveParDims;
1562 for (mlir::acc::GPUParallelDimAttr launchParDim : launchParDims) {
1563 if (launchParDim.getOrder() < lowestParDim.getOrder())
1564 break;
1565 if (!llvm::is_contained(activeParDims, launchParDim))
1566 inactiveParDims.push_back(launchParDim);
1567 }
1568 return std::pair{activeParDims, inactiveParDims};
1569 }
1570 }
1571
1572 SmallVector<mlir::acc::GPUParallelDimAttr> ancestorParDims =
1573 getAncestorParDims(op);
1574 // Preserve whether there were any structural ancestor par-dims before
1575 // we start augmenting them based on inner uses (e.g. private_local).
1576 // This is needed for gang redundancy check - stores to worker-indexed
1577 // private_local should not disable redundant gang execution.
1578 bool noStructuralAncestorParDims =
1579 llvm::none_of(ancestorParDims, [](auto pd) { return !pd.isSeq(); });
1580
1581 if (routineParDim) {
1582 if (routineParDim.isThreadX()) {
1583 mlir::acc::insertParDim(ancestorParDims,
1584 mlir::acc::GPUParallelDimAttr::threadYDim(ctx));
1585 }
1586 mlir::acc::insertParDim(ancestorParDims,
1587 mlir::acc::GPUParallelDimAttr::blockXDim(ctx));
1588 }
1589
1590 // acc.private_local should use the same par_dims as acc.reduction_accumulate.
1591 if (acc::PrivateLocalOp privateLocalOp = dyn_cast<acc::PrivateLocalOp>(op)) {
1592 for (Operation *user : privateLocalOp.getResult().getUsers()) {
1593 if (acc::ReductionAccumulateOp accumulateOp =
1594 dyn_cast<acc::ReductionAccumulateOp>(user)) {
1595 if (accumulateOp.getMemref() == privateLocalOp.getResult()) {
1596 for (mlir::acc::GPUParallelDimAttr parDim :
1597 accumulateOp.getParDims().getArray()) {
1598 mlir::acc::insertParDim(ancestorParDims, parDim);
1599 }
1600 }
1601 }
1602 // For decomposed complex reductions, the private_local is consumed
1603 // by an acc.reduction_combine{,_region} (no acc.reduction_accumulate
1604 // user). Mirror the par_dims so this private_local is treated as the
1605 // accumulator at the same parallelism level as a scalar reduction
1606 // would be (per-thread, not block-shared).
1607 if (acc::ReductionCombineOp combineOp =
1608 dyn_cast<acc::ReductionCombineOp>(user)) {
1609 if (combineOp.getSrcMemref() == privateLocalOp.getResult()) {
1610 for (mlir::acc::GPUParallelDimAttr parDim :
1611 getReductionCombineParDims(combineOp)) {
1612 mlir::acc::insertParDim(ancestorParDims, parDim);
1613 }
1614 }
1615 }
1616 if (auto combineRegionOp =
1617 dyn_cast<acc::ReductionCombineRegionOp>(user)) {
1618 if (combineRegionOp.getSrcVar() == privateLocalOp.getResult()) {
1619 for (mlir::acc::GPUParallelDimAttr parDim :
1620 getReductionCombineParDims(combineRegionOp)) {
1621 mlir::acc::insertParDim(ancestorParDims, parDim);
1622 }
1623 }
1624 }
1625 }
1626 // A dynamically-shaped privatization is materialized as a strided view
1627 // whose type does not reveal its parallel scope. Keep its own par_dims
1628 // active so the view is materialized (and predicated) per owning
1629 // thread/block. Statically-shaped privatizations are left to the stack-fit
1630 // decision so only do this for dynamic shapes.
1631 acc::PrivateType privTy =
1632 cast<acc::PrivateType>(privateLocalOp.getPrivatized().getType());
1633 MemRefType baseTy = getPrivateBaseMemRefType(
1634 privTy.getBaseTy(), computeRegion->getParentOfType<ModuleOp>());
1635 if (!baseTy.hasStaticShape()) {
1636 GPUParallelDimsAttr ownParDims =
1637 getPrivateParDims(privateLocalOp, computeRegion);
1638 if (ownParDims)
1639 for (GPUParallelDimAttr parDim : ownParDims.getArray())
1640 mlir::acc::insertParDim(ancestorParDims, parDim);
1641 }
1642 }
1643
1644 bool hasBlock = false;
1645 for (mlir::acc::GPUParallelDimAttr parDim : ancestorParDims)
1646 if (parDim.isAnyBlock())
1647 hasBlock = true;
1648
1649 mlir::acc::GPUParallelDimAttr lowestParDim =
1650 mlir::acc::GPUParallelDimAttr::threadXDim(ctx);
1651 if (block) {
1652 ThreadYBroadeningInfo threadYInfo =
1653 analyzeThreadYBroadening(*block, computeRegion);
1654
1655 auto applyCombineParDims =
1656 [&](ArrayRef<mlir::acc::GPUParallelDimAttr> combineParDims) {
1657 for (mlir::acc::GPUParallelDimAttr parDim : combineParDims)
1658 mlir::acc::removeParDim(ancestorParDims, parDim);
1659 return success();
1660 };
1661 block->walk([&](Operation *op) -> WalkResult {
1662 // Writes to acc.private_local must keep the privatization's par_dims
1663 // active so the write runs on all owning threads instead of being
1664 // predicated to a single lane.
1665 auto addPrivateStoreParDims = [&](Value target) {
1666 if (auto privateLocalOp = getPrivateLocalForMemref(target)) {
1667 GPUParallelDimsAttr parDimsAttr =
1668 getPrivateParDims(privateLocalOp, computeRegion);
1669 if (parDimsAttr)
1670 for (auto parDim : parDimsAttr.getArray())
1671 mlir::acc::insertParDim(ancestorParDims, parDim);
1672 }
1673 };
1674 if (auto memEffects = dyn_cast<MemoryEffectOpInterface>(op)) {
1675 SmallVector<MemoryEffects::EffectInstance> effects;
1676 memEffects.getEffects(effects);
1677 for (const MemoryEffects::EffectInstance &effect : effects) {
1678 if (isa<MemoryEffects::Write>(effect.getEffect()) &&
1679 effect.getValue())
1680 addPrivateStoreParDims(effect.getValue());
1681 }
1682 }
1683 // Consider ACC routine calls; routine calls should be predicated up to
1684 // one level above the parallel dimension of the callee.
1685 if (CallOpInterface callOp = dyn_cast<CallOpInterface>(op)) {
1686 if (mlir::acc::GPUParallelDimAttr parDim =
1687 getAccRoutineCallParDim(callOp, defaultPolicy)) {
1688 if (parDim.isBlockZ())
1689 lowestParDim = parDim;
1690 else
1691 lowestParDim = parDim.getOneHigher();
1692 }
1693 }
1694 // acc.reduction_combine_region should be predicated with the par_dims of
1695 // acc.reduction_accumulate. This is required when using combine between
1696 // kernel and loop in combined constructs.
1697 if (acc::ReductionCombineOp reductionCombineOp =
1698 dyn_cast<acc::ReductionCombineOp>(op)) {
1699 if (failed(applyCombineParDims(
1700 getReductionCombineParDims(reductionCombineOp))))
1701 return WalkResult::interrupt();
1702 }
1703 if (acc::ReductionCombineRegionOp combineRegionOp =
1704 dyn_cast<acc::ReductionCombineRegionOp>(op)) {
1705 if (failed(applyCombineParDims(
1706 getReductionCombineParDims(combineRegionOp))))
1707 return WalkResult::interrupt();
1708 }
1709 // An array accumulate reduces across its par_dims via gpu.all_reduce, so
1710 // all those threads must execute it - treat them as active (unlike the
1711 // scalar accumulate, which is active through its enclosing scf.parallel).
1712 if (acc::ReductionAccumulateArrayOp accArrayOp =
1713 dyn_cast<acc::ReductionAccumulateArrayOp>(op)) {
1714 for (mlir::acc::GPUParallelDimAttr parDim :
1715 accArrayOp.getParDims().getArray()) {
1716 mlir::acc::insertParDim(ancestorParDims, parDim);
1717 }
1718 }
1719 return WalkResult::advance();
1720 });
1721 mlir::acc::GPUParallelDimAttr threadY =
1722 mlir::acc::GPUParallelDimAttr::threadYDim(ctx);
1723 bool baselineThreadYActive = llvm::is_contained(ancestorParDims, threadY);
1724 if (threadYInfo.hasActiveWorkerCombine && !baselineThreadYActive) {
1725 if (threadYInfo.hasExplicitInactiveCombine ||
1726 threadYInfo.hasBroadeningConflict) {
1727 Operation *diagnosticOp =
1728 threadYInfo.diagnosticOp ? threadYInfo.diagnosticOp : op;
1729 (void)accSupport.emitNYI(
1730 diagnosticOp->getLoc(),
1731 "operations in the same predicate region require incompatible "
1732 "ThreadY predication");
1733 hasFailed = true;
1734 return {};
1735 }
1736 mlir::acc::insertParDim(ancestorParDims, threadY);
1737 }
1738 }
1739
1740 // Obtain launch dimensions
1741 SmallVector<mlir::acc::GPUParallelDimAttr> launchParDims;
1742 if (routineParDim) {
1743 for (mlir::acc::GPUParallelDimAttr parDim = routineParDim;
1744 parDim.getOrder() >= lowestParDim.getOrder();
1745 parDim = parDim.getOneLower()) {
1746 mlir::acc::insertParDim(launchParDims, parDim);
1747 }
1748 } else {
1749 launchParDims = computeRegion.getLaunchParDims();
1750 }
1751
1752 // Compute dimensions that execute op
1753 SmallVector<mlir::acc::GPUParallelDimAttr> activeParDims, inactiveParDims;
1754 for (mlir::acc::GPUParallelDimAttr launchParDim : launchParDims) {
1755 if (launchParDim.getOrder() < lowestParDim.getOrder())
1756 break;
1757 if (llvm::find(ancestorParDims, launchParDim) != ancestorParDims.end() ||
1758 (launchParDim.isAnyBlock() &&
1759 (noStructuralAncestorParDims || hasBlock))) {
1760 activeParDims.push_back(launchParDim);
1761 } else {
1762 inactiveParDims.push_back(launchParDim);
1763 }
1764 }
1765
1766 return std::pair{activeParDims, inactiveParDims};
1767}
1768
1769Value ACCCGToGPULowering::emitPredicate(
1770 Location loc, SmallVector<mlir::acc::GPUParallelDimAttr> &inactiveParDims) {
1771 Value predicate;
1772 for (mlir::acc::GPUParallelDimAttr inactiveParDim : inactiveParDims) {
1773 Value threadId = getGPUThreadIdFor(inactiveParDim.getProcessor());
1774 TypedAttr zeroAttr = rewriter.getZeroAttr(threadId.getType());
1775 Value zero = arith::ConstantOp::create(rewriter, loc, zeroAttr);
1776 Value cmp = arith::CmpIOp::create(rewriter, loc, arith::CmpIPredicate::eq,
1777 threadId, zero);
1778 if (predicate)
1779 predicate = arith::AndIOp::create(rewriter, loc, cmp, predicate);
1780 else
1781 predicate = cmp;
1782 }
1783 return predicate;
1784}
1785
1786void ACCCGToGPULowering::createBarrier(
1787 Location loc, mlir::acc::GPUParallelDimsAttr parDimsAttr) {
1788 bool hasAnyBlock = false, hasThreadY = false, hasThreadX = false;
1789 for (auto parDim : parDimsAttr.getArray()) {
1790 if (parDim.isAnyBlock())
1791 hasAnyBlock = true;
1792 if (parDim.isThreadY())
1793 hasThreadY = true;
1794 if (parDim.isThreadX())
1795 hasThreadX = true;
1796 }
1797
1798 if (hasAnyBlock || hasThreadY)
1799 emitGPUBarrierWorkgroup(rewriter, loc);
1800 else if (hasThreadX)
1801 createPerRowBarrier(loc);
1802}
1803
1804void ACCCGToGPULowering::createPerRowBarrier(Location loc) {
1805 // A row of a (1, N, 1) launch is a single thread; a subgroup barrier would
1806 // instead synchronize N workers.
1807 if (isSingleThreadWorkerLaunch())
1808 return;
1809
1810 // A worker-only row narrower than a subgroup covers only part of one, so
1811 // synchronize just its lanes. Both paths leave hasThreadYBarrier unset, so
1812 // the launch is not padded on account of this barrier.
1813 if (isWorkerOnlyShuffleLaunch() && staticBlockDimX < options.subgroupSize) {
1814 emitGPUBarrierRow(rewriter, loc, staticBlockDimX, options.subgroupSize);
1815 return;
1816 }
1817
1818 hasThreadYBarrier = true;
1819
1820 if (staticBlockDimX <= options.subgroupSize) {
1821 emitGPUBarrierSubgroup(rewriter, loc);
1822 return;
1823 }
1824
1825 if (options.deviceType != mlir::acc::DeviceType::Nvidia) {
1826 (void)accSupport.emitNYI(
1827 loc,
1828 "per-row barrier to support worker parallelism on non-NVIDIA device");
1829 }
1830
1831 // Per-row barrier with fully runtime branching.
1832 // Three mutually exclusive paths:
1833 // blockDim.y == 1 -> gpu.barrier (workgroup-wide, only one worker)
1834 // blockDim.x <= subgroupSize -> gpu.barrier scope<subgroup>
1835 // blockDim.x > subgroupSize -> nvvm.barrier (tid.y + 1), blockDim.x
1836 // (named)
1837 //
1838 // Per-row barriers use tid.y+1 so IDs start at 1, avoiding clash with
1839 // barrier 0. When blockDim.y >= 16, worker 15's ID (16) wraps to
1840 // physical barrier 0; this is safe because named barriers are reusable
1841 // resources - workgroup-wide and per-row barriers on the same physical
1842 // barrier execute at different program points and never overlap.
1843 Value blockDimX = gpu::BlockDimOp::create(
1844 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
1845 Value blockDimY = gpu::BlockDimOp::create(
1846 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
1847 Value cst1 = arith::ConstantIndexOp::create(rewriter, loc, 1);
1848 Value isSingleWorker = arith::CmpIOp::create(
1849 rewriter, loc, arith::CmpIPredicate::eq, blockDimY, cst1);
1850
1851 auto outerIf = scf::IfOp::create(rewriter, loc, isSingleWorker,
1852 /*withElseRegion=*/true);
1853
1854 // Then: blockDim.y == 1 -> workgroup-wide barrier (safe, only one worker)
1855 rewriter.setInsertionPointToStart(&outerIf.getThenRegion().front());
1856 emitGPUBarrierWorkgroup(rewriter, loc);
1857
1858 // Else: blockDim.y > 1 - choose between subgroup sync and named barrier
1859 rewriter.setInsertionPointToStart(&outerIf.getElseRegion().front());
1860 Value cstSubgroupSize =
1861 arith::ConstantIndexOp::create(rewriter, loc, options.subgroupSize);
1862 Value isSubgroupSized = arith::CmpIOp::create(
1863 rewriter, loc, arith::CmpIPredicate::ule, blockDimX, cstSubgroupSize);
1864
1865 auto innerIf = scf::IfOp::create(rewriter, loc, isSubgroupSized,
1866 /*withElseRegion=*/true);
1867
1868 // Then: blockDim.x <= subgroupSize -> subgroup barrier (one worker per
1869 // subgroup)
1870 rewriter.setInsertionPointToStart(&innerIf.getThenRegion().front());
1871 emitGPUBarrierSubgroup(rewriter, loc);
1872
1873 // Else: blockDim.x > subgroupSize -> per-row named barrier with tid.y + 1.
1874 // The 1024-thread-per-block hardware limit with subgroup-aligned blockDim.x
1875 // (>= 64 here) guarantees blockDim.y <= 16, so IDs span at most 16
1876 // physical barriers (0-15) with no aliasing across workers.
1877 rewriter.setInsertionPointToStart(&innerIf.getElseRegion().front());
1878 Value threadYId = gpu::ThreadIdOp::create(
1879 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::y);
1880 Value barrierId = arith::AddIOp::create(rewriter, loc, threadYId, cst1);
1881 Type i32Ty = rewriter.getI32Type();
1882 Value barrierId32 =
1883 arith::IndexCastOp::create(rewriter, loc, i32Ty, barrierId);
1884 Value numberOfThreads32 =
1885 arith::IndexCastOp::create(rewriter, loc, i32Ty, blockDimX);
1886
1887 // GPU dialect named barriers do not have a means to create a custom barrier
1888 // id. Thus use nvvm directly.
1889 assert(options.deviceType == mlir::acc::DeviceType::Nvidia);
1890 NVVM::BarrierOp::create(rewriter, loc, barrierId32, numberOfThreads32);
1891
1892 rewriter.setInsertionPointAfter(outerIf);
1893}
1894
1895/// Whether any later sibling of \p loopOp (or a loop nested inside one) is a
1896/// loop, i.e. whether some subsequent loop in the same region may read what
1897/// \p loopOp wrote. Used to skip a barrier after the last loop, where nothing
1898/// reads the data afterward.
1899static bool hasSubsequentLoopSibling(Operation *loopOp) {
1900 for (Operation *next = loopOp->getNextNode(); next;
1901 next = next->getNextNode()) {
1902 if (isa<scf::ParallelOp, scf::ForOp>(next))
1903 return true;
1904 bool nested = false;
1905 next->walk([&](Operation *op) {
1906 if (isa<scf::ParallelOp, scf::ForOp>(op)) {
1907 nested = true;
1908 return WalkResult::interrupt();
1909 }
1910 return WalkResult::advance();
1911 });
1912 if (nested)
1913 return true;
1914 }
1915 return false;
1916}
1917
1918/// Nearest enclosing sequential loop ancestor of \p op.
1919static LoopLikeOpInterface findFirstSequentialLoop(Operation *op) {
1920 auto isAllSequentialParDims = [](scf::ParallelOp par) -> bool {
1921 mlir::acc::GPUParallelDimsAttr pd = mlir::acc::getParDimsAttr(par);
1922 if (!pd || pd.getArray().empty())
1923 return false;
1924 return llvm::all_of(pd.getArray(), [](mlir::acc::GPUParallelDimAttr d) {
1925 return d.isSeq();
1926 });
1927 };
1928
1929 for (Operation *p = op->getParentOp(); p; p = p->getParentOp()) {
1930 // Do not need to check scf.for op's parents
1931 if (isa<scf::ForOp>(p))
1932 return cast<LoopLikeOpInterface>(p);
1933 if (scf::ParallelOp parOp = dyn_cast<scf::ParallelOp>(p)) {
1934 if (isAllSequentialParDims(parOp))
1935 return cast<LoopLikeOpInterface>(p);
1936 }
1937 }
1938 return nullptr;
1939}
1940
1941// A sequential loop that uses gang-private shared memory needs a
1942// workgroup-wide barrier afterward so every thread in the block observes the
1943// same state. When more work still lies between the loop and the next
1944// thread-reconvergence point in the same block, that barrier must follow that
1945// work; not sit immediately after the loop.
1946//
1947// The helpers below mark loop-body closure and other reconvergence points
1948// where any postponed barrier must be inserted.
1949
1950/// Marks the end of a loop body's iteration in the current block.
1951static bool isLoopBodyClosureOp(Operation *op) {
1952 return isa<scf::ReduceOp, scf::YieldOp, acc::YieldOp>(op);
1953}
1954
1955/// Thread-reconvergence point where any postponed post-loop barrier for earlier
1956/// loops in this block must be inserted before proceeding.
1957static bool isDeferredBarrierFlushPoint(Operation *op) {
1958 if (isLoopBodyClosureOp(op))
1959 return true;
1960 // The next sequential loop may consume shared state produced by the prior
1961 // one.
1962 if (isa<scf::ForOp>(op))
1963 return true;
1964 if (scf::ParallelOp parallelOp = dyn_cast<scf::ParallelOp>(op)) {
1965 if (mlir::acc::hasParDimsAttr(parallelOp)) {
1966 if (mlir::acc::GPUParallelDimsAttr parDims =
1967 mlir::acc::getParDimsAttr(parallelOp)) {
1968 if (parDims.getArray().size() == 1 &&
1969 parDims.getArray().front().isSeq()) {
1970 return true;
1971 }
1972 }
1973 }
1974 }
1975 return false;
1976}
1977
1978/// True when a loop is followed by other work in the same block before the
1979/// loop body closes; the post-loop barrier must wait for that reconvergence
1980/// point instead of being placed right after the loop.
1981static bool hasTrailingSideEffectSiblings(Operation *loopOp) {
1982 for (Operation *next = loopOp->getNextNode(); next;
1983 next = next->getNextNode()) {
1984 return !isLoopBodyClosureOp(next);
1985 }
1986 return false;
1987}
1988
1989// Insert a barrier after a sequential loop when the surrounding parallel
1990// structure requires it, so that block threads observe the loop's writes to
1991// gang-/block-private shared memory before a later loop reads them. The barrier
1992// must land at an enclosing collective (block- or worker-level) point where all
1993// threads converge, so it cannot deadlock.
1994//
1995// The function walks the parent hierarchy of the loop to decide where (if at
1996// all) to place the barrier:
1997// - No enclosing parallel loop (top-level worksharing loop): emit a
1998// workgroup barrier when the loop writes shared memory and a later sibling
1999// loop may read it.
2000// - Nearest scf.parallel parent is itself sequential: a barrier *after* it
2001// would be unsafe when threads have varying iteration counts. However, when
2002// the loop has thread-level sub-loops collaborating on block-private memory
2003// (all threads participate every iteration), walk up to the block-level
2004// ancestor and insert the barrier there.
2005// - Nearest scf.parallel parent is a non-sequential parallel loop: walk up to
2006// the block-level (or worker/thread-y) ancestor and insert the barrier
2007// there, handling gang-redundant init loops and grid-stride remainders.
2008void ACCCGToGPULowering::createBarrierAfterSeqLoop(Operation *loopOp) {
2009 // Host loops enclosing the compute region are not GPU execution scopes.
2010 auto getParentParallel = [&](Operation *op) -> scf::ParallelOp {
2011 scf::ParallelOp parent = op->getParentOfType<scf::ParallelOp>();
2012 if (parent && computeRegion->isAncestor(parent))
2013 return parent;
2014 return {};
2015 };
2016 scf::ParallelOp wsLoop = getParentParallel(loopOp);
2017 if (!wsLoop) {
2018 // loopOp is a worksharing loop at the kernel-body top level (no enclosing
2019 // parallel loop). When it writes gang-private shared memory
2020 // that a later sibling loop reads, a workgroup barrier is needed between
2021 // them. The point just after a top-level loop is uniform (all threads reach
2022 // it), so a workgroup-wide barrier here cannot deadlock. Loops writing only
2023 // global/thread-private memory, or with no subsequent reader, get none.
2024 if (mayWriteSharedMemory(loopOp) && hasSubsequentLoopSibling(loopOp))
2025 emitGPUBarrierWorkgroup(rewriter, loopOp->getLoc());
2026 return;
2027 }
2028
2029 mlir::acc::GPUParallelDimsAttr wsParDims = mlir::acc::getParDimsAttr(wsLoop);
2030 bool parentIsSeq =
2031 wsParDims.getArray().size() == 1 && wsParDims.getArray().front().isSeq();
2032
2033 if (parentIsSeq) {
2034 // loopOp is nested inside a sequential parent loop.
2035 // When the loop contains thread-level sub-loops,
2036 // multiple threads collaborate on shared (block-private) memory
2037 // within each iteration. Insert a block-level barrier.
2038 bool hasThreadSubLoop = false;
2039 loopOp->walk([&](scf::ParallelOp innerPar) -> WalkResult {
2040 if (innerPar.getOperation() == loopOp)
2041 return WalkResult::advance();
2042 mlir::acc::GPUParallelDimsAttr dims = mlir::acc::getParDimsAttr(innerPar);
2043 for (auto d : dims.getArray()) {
2044 if (d.isThreadX() || d.isThreadY()) {
2045 hasThreadSubLoop = true;
2046 return WalkResult::interrupt();
2047 }
2048 }
2049 return WalkResult::advance();
2050 });
2051 if (!hasThreadSubLoop)
2052 return;
2053 scf::ParallelOp threadLoop = getParentParallel(wsLoop);
2054 if (!threadLoop)
2055 return;
2056 scf::ParallelOp blockLoop = getParentParallel(threadLoop);
2057 if (!blockLoop)
2058 return;
2059 mlir::acc::GPUParallelDimsAttr parDimsAttr =
2060 mlir::acc::getParDimsAttr(blockLoop);
2061 if (parDimsAttr.hasOnlyBlockLevel())
2062 createBarrier(loopOp->getLoc(), parDimsAttr);
2063 return;
2064 }
2065
2066 // Parent is a non-sequential parallel loop. Walk up to find the block-level
2067 // ancestor and insert a barrier there.
2068 scf::ParallelOp seqLoop = getParentParallel(wsLoop);
2069 if (!seqLoop) {
2070 // wsLoop is a worksharing loop directly under the compute region with no
2071 // gang ancestor: a gang-redundant init loop (e.g. a thread-level loop that
2072 // every gang runs to fill its own copy of gang-private shared memory). If
2073 // it writes gang-private (block-only) memory that a later sibling loop
2074 // reads, insert a workgroup barrier between them. The barrier sits just
2075 // after the top-level loop where all threads converge, so it cannot
2076 // deadlock.
2077 if (mayWriteSharedMemory(loopOp) && hasSubsequentLoopSibling(wsLoop))
2078 emitGPUBarrierWorkgroup(rewriter, loopOp->getLoc());
2079 return;
2080 }
2081 if (scf::ParallelOp outerParLoop = getParentParallel(seqLoop)) {
2082 mlir::acc::GPUParallelDimsAttr parDimsAttr =
2083 mlir::acc::getParDimsAttr(outerParLoop);
2084 if (parDimsAttr.hasOnlyBlockLevel()) {
2085 createBarrier(loopOp->getLoc(), parDimsAttr);
2086 } else if (parDimsAttr.hasOnlyThreadYLevel()) {
2087 createPerRowBarrier(loopOp->getLoc());
2088 } else if (parDimsAttr && parDimsAttr.isSeq()) {
2089 // outerParLoop is a sequential grid-stride remainder of a partitioned
2090 // gang loop, not the gang. Walk past the remainder(s) to the block-level
2091 // gang and barrier there.
2092 for (scf::ParallelOp gangLoop = getParentParallel(outerParLoop); gangLoop;
2093 gangLoop = getParentParallel(gangLoop)) {
2094 mlir::acc::GPUParallelDimsAttr gangDims =
2095 mlir::acc::getParDimsAttr(gangLoop);
2096 if (gangDims.hasOnlyBlockLevel()) {
2097 createBarrier(loopOp->getLoc(), gangDims);
2098 break;
2099 }
2100 if (!gangDims.isSeq())
2101 break;
2102 }
2103 }
2104 return;
2105 }
2106
2107 // No block-level ancestor above seqLoop: the gang loop is seqLoop itself, a
2108 // gang(+vector) loop directly under the compute region. The fixed-depth
2109 // lookup above misses it (issue: a gang+vector init loop that writes
2110 // gang-private shared memory must be followed by a barrier before another
2111 // loop reads it). Only needed when the loop writes such shared memory; loops
2112 // writing global/thread-private memory (e.g. a plain gang-vector saxpy) do
2113 // not need one here.
2114 mlir::acc::GPUParallelDimsAttr parDimsAttr =
2116 if (parDimsAttr.hasOnlyBlockLevel() && mayWriteSharedMemory(loopOp)) {
2117 createBarrier(loopOp->getLoc(), parDimsAttr);
2118 }
2119}
2120
2121bool ACCCGToGPULowering::mayWriteSharedMemory(Operation *loopOp) {
2122 bool found = false;
2123 loopOp->walk([&](memref::StoreOp storeOp) {
2124 // Trace the store target back to its backing acc.private_local through
2125 // view/cast/box ops, then check the privatization is gang-level.
2126 llvm::SmallVector<Value, 8> worklist{storeOp.getMemref()};
2127 llvm::SmallPtrSet<Value, 8> seen;
2128 while (!worklist.empty()) {
2129 Value v = worklist.pop_back_val();
2130 if (!seen.insert(v).second)
2131 continue;
2132 Operation *def = v.getDefiningOp();
2133 if (!def)
2134 continue;
2135 if (acc::PrivateLocalOp privateLocal =
2136 dyn_cast<acc::PrivateLocalOp>(def)) {
2137 acc::PrivatizeOp privatizeOp =
2138 getPrivatizeOp(privateLocal, computeRegion);
2139 // Only gang-level (block, no thread) private memory is a single copy
2140 // shared across the workgroup's threads, so a write needs a workgroup
2141 // barrier before another thread reads it. A [block_x, thread_x]
2142 // privatization is thread-private (one copy per thread); barriering on
2143 // it would deadlock when threads take different grid-stride iteration
2144 // counts.
2145 if (mlir::acc::GPUParallelDimsAttr parDims =
2146 privatizeOp.getParDimsAttr()) {
2147 bool hasBlock = false, hasThread = false;
2148 for (mlir::acc::GPUParallelDimAttr d : parDims.getArray()) {
2149 if (d.isAnyBlock())
2150 hasBlock = true;
2151 if (d.isThreadX() || d.isThreadY())
2152 hasThread = true;
2153 }
2154 if (hasBlock && !hasThread) {
2155 found = true;
2156 return WalkResult::interrupt();
2157 }
2158 }
2159 continue;
2160 }
2161 worklist.append(def->getOperands().begin(), def->getOperands().end());
2162 }
2163 return WalkResult::advance();
2164 });
2165 return found;
2166}
2167
2168PrivateMemScope
2169ACCCGToGPULowering::getPrivateMemScope(acc::PrivatizeOp privatizeOp) {
2170 bool hasBlock = false;
2171 bool hasThreadX = false;
2172 bool hasThreadY = false;
2173 if (mlir::acc::GPUParallelDimsAttr parDims = privatizeOp.getParDimsAttr()) {
2174 for (mlir::acc::GPUParallelDimAttr d : parDims.getArray()) {
2175 if (d.isAnyBlock())
2176 hasBlock = true;
2177 if (d.isThreadX())
2178 hasThreadX = true;
2179 if (d.isThreadY())
2180 hasThreadY = true;
2181 }
2182 } else {
2183 for (mlir::acc::GPUParallelDimAttr d : computeRegion.getLaunchParDims())
2184 if (d.isAnyBlock())
2185 hasBlock = true;
2186 if (hasBlock)
2187 return PrivateMemScope::Gang;
2188 return PrivateMemScope::Thread;
2189 }
2190 if (hasThreadX)
2191 return PrivateMemScope::Thread;
2192 if (hasBlock && hasThreadY)
2193 return PrivateMemScope::Worker;
2194 if (hasBlock)
2195 return PrivateMemScope::Gang;
2196 return PrivateMemScope::Thread;
2197}
2198
2199/// Walks back from a memref use to its defining `acc.private_local`, if any.
2200static acc::PrivateLocalOp getPrivateLocalForMemref(Value memref) {
2201 llvm::SmallVector<Value, 8> worklist{memref};
2202 llvm::SmallPtrSet<Value, 8> seen;
2203 while (!worklist.empty()) {
2204 Value v = worklist.pop_back_val();
2205 if (!seen.insert(v).second)
2206 continue;
2207 Operation *def = v.getDefiningOp();
2208 if (!def)
2209 continue;
2210 if (acc::PrivateLocalOp privateLocal = dyn_cast<acc::PrivateLocalOp>(def))
2211 return privateLocal;
2212 worklist.append(def->getOperands().begin(), def->getOperands().end());
2213 }
2214 return nullptr;
2215}
2216
2217PrivateMemScope ACCCGToGPULowering::getPrivateScopeForMemref(Value memref) {
2218 if (auto privateLocal = getPrivateLocalForMemref(memref))
2219 return getPrivateMemScope(getPrivatizeOp(privateLocal, computeRegion));
2220 return PrivateMemScope::None;
2221}
2222
2223acc::PrivatizeOp ACCCGToGPULowering::getPrivatizeForMemref(Value memref) {
2224 if (auto privateLocal = getPrivateLocalForMemref(memref))
2225 return getPrivatizeOp(privateLocal, computeRegion);
2226 return acc::PrivatizeOp();
2227}
2228
2229void ACCCGToGPULowering::collectSharedPrivateStores(
2230 Operation *root, llvm::SmallPtrSetImpl<Operation *> &gangPrivatizes,
2231 llvm::SmallPtrSetImpl<Operation *> &workerPrivatizes) {
2232 root->walk([&](memref::StoreOp storeOp) {
2233 PrivateMemScope scope = getPrivateScopeForMemref(storeOp.getMemref());
2234 if (scope != PrivateMemScope::Gang && scope != PrivateMemScope::Worker)
2235 return;
2236 acc::PrivatizeOp privatize = getPrivatizeForMemref(storeOp.getMemref());
2237 if (!privatize)
2238 return;
2239 if (scope == PrivateMemScope::Gang)
2240 gangPrivatizes.insert(privatize.getOperation());
2241 else
2242 workerPrivatizes.insert(privatize.getOperation());
2243 });
2244}
2245
2246bool ACCCGToGPULowering::isMatchingPrivateMemref(
2247 Value memref, PrivateMemScope storeScope,
2248 const llvm::SmallPtrSetImpl<Operation *> &storePrivatizes) {
2249 if (getPrivateScopeForMemref(memref) != storeScope)
2250 return false;
2251 acc::PrivatizeOp usePrivatize = getPrivatizeForMemref(memref);
2252 return usePrivatize && storePrivatizes.contains(usePrivatize.getOperation());
2253}
2254
2255bool ACCCGToGPULowering::hasParallelPrivateReuse(
2256 LoopLikeOpInterface seqLoopOp, Operation *skipOp,
2257 PrivateMemScope storeScope,
2258 const llvm::SmallPtrSetImpl<Operation *> &storePrivatizes) {
2259 if (storePrivatizes.empty())
2260 return false;
2261
2262 bool hasParallelPrivateUse = false;
2263 seqLoopOp.getOperation()->walk([&](Operation *op) {
2264 // Ignore loads inside the predicate.
2265 if (skipOp->isAncestor(op))
2266 return WalkResult::advance();
2267
2268 Value memref;
2269 if (memref::LoadOp loadOp = dyn_cast<memref::LoadOp>(op))
2270 memref = loadOp.getMemref();
2271 else if (memref::StoreOp storeOp = dyn_cast<memref::StoreOp>(op))
2272 memref = storeOp.getMemref();
2273 else
2274 return WalkResult::advance();
2275
2276 if (!isMatchingPrivateMemref(memref, storeScope, storePrivatizes))
2277 return WalkResult::advance();
2278
2279 bool insideNestedParallel = false;
2280 for (Operation *p = op->getParentOp(); p && p != seqLoopOp.getOperation();
2281 p = p->getParentOp()) {
2282 if (scf::ParallelOp par = dyn_cast<scf::ParallelOp>(p)) {
2283 if (mlir::acc::GPUParallelDimsAttr pd =
2285 if (llvm::any_of(pd.getArray(), [](mlir::acc::GPUParallelDimAttr d) {
2286 return !d.isSeq();
2287 })) {
2288 insideNestedParallel = true;
2289 break;
2290 }
2291 }
2292 }
2293 }
2294 if (!insideNestedParallel)
2295 return WalkResult::advance();
2296 // An inner parallel region uses the same private memory.
2297 hasParallelPrivateUse = true;
2298 return WalkResult::interrupt();
2299 });
2300
2301 return hasParallelPrivateUse;
2302}
2303
2304PrivateMemScope
2305ACCCGToGPULowering::needsPreStoreReuseBarrier(acc::PredicateRegionOp interOp) {
2306
2307 // Check if we need a pre-predicate barrier first.
2308 // Next, check if the barrier should be gang- or worker-level.
2309 LoopLikeOpInterface seqLoopOp = findFirstSequentialLoop(interOp);
2310 if (!seqLoopOp)
2311 return PrivateMemScope::None;
2312
2313 // Check if any op in the predicate region stores to gang- or worker-private
2314 // memory. If not, no barrier is needed.
2315 llvm::SmallPtrSet<Operation *, 4> gangPrivatizes, workerPrivatizes;
2316 collectSharedPrivateStores(interOp, gangPrivatizes, workerPrivatizes);
2317
2318 // A reused gang slot needs the workgroup barrier, which also reconverges the
2319 // worker rows, so look for that hazard first. A gang store whose slot is
2320 // never read back does not stand in for a worker one: fall through and check
2321 // the worker slots on their own, otherwise their per-row barrier is lost.
2322 if (hasParallelPrivateReuse(seqLoopOp, interOp, PrivateMemScope::Gang,
2323 gangPrivatizes))
2324 return PrivateMemScope::Gang;
2325 if (hasParallelPrivateReuse(seqLoopOp, interOp, PrivateMemScope::Worker,
2326 workerPrivatizes))
2327 return PrivateMemScope::Worker;
2328 return PrivateMemScope::None;
2329}
2330
2331bool ACCCGToGPULowering::needsInLoopReuseBarrier(Operation *loopOp) {
2332 // A sequential scf.parallel is the remainder of a partitioned gang loop: the
2333 // gang iterations this workgroup still runs as a grid stride. Its scalar
2334 // stores stay predicated and already reconverge between iterations, so the
2335 // reuse hole is the nested scf.for coming from `acc loop seq`.
2336 if (scf::ParallelOp par = dyn_cast<scf::ParallelOp>(loopOp))
2337 return false;
2338
2339 // The hazard needs a worker- or vector-level routine call in the body: every
2340 // workgroup thread reaches such a call and the callee synchronizes
2341 // internally, so threads leave it in no particular order and can then be
2342 // running different iterations of this loop at the same time.
2343 if (!hasThreadLevelRoutineCall)
2344 return false;
2345 bool callsThreadLevelRoutine = false;
2346 loopOp->walk([&](CallOpInterface callOp) -> WalkResult {
2347 if (mlir::acc::GPUParallelDimAttr parDim =
2348 getAccRoutineCallParDim(callOp, defaultPolicy)) {
2349 if (parDim.isThreadX() || parDim.isThreadY()) {
2350 callsThreadLevelRoutine = true;
2351 return WalkResult::interrupt();
2352 }
2353 }
2354 return WalkResult::advance();
2355 });
2356 if (!callsThreadLevelRoutine)
2357 return false;
2358
2359 // Only gang-level privatizations are a single copy shared by the whole
2360 // workgroup and reused by every iteration; worker- and thread-private copies
2361 // cannot be clobbered by another thread.
2362 llvm::SmallPtrSet<Operation *, 4> storedPrivatizes, workerPrivatizes;
2363 collectSharedPrivateStores(loopOp, storedPrivatizes, workerPrivatizes);
2364 if (storedPrivatizes.empty())
2365 return false;
2366
2367 // Storing alone is harmless: the next iteration only clobbers a value someone
2368 // still needs when the slot is read back in the body, either directly or by a
2369 // callee it is handed to.
2370 bool reusesGangPrivate = false;
2371 loopOp->walk([&](Operation *op) -> WalkResult {
2372 if (memref::LoadOp loadOp = dyn_cast<memref::LoadOp>(op))
2373 reusesGangPrivate = isMatchingPrivateMemref(
2374 loadOp.getMemref(), PrivateMemScope::Gang, storedPrivatizes);
2375 else if (CallOpInterface callOp = dyn_cast<CallOpInterface>(op))
2376 reusesGangPrivate = llvm::any_of(callOp.getArgOperands(), [&](Value arg) {
2377 return isMatchingPrivateMemref(arg, PrivateMemScope::Gang,
2378 storedPrivatizes);
2379 });
2380 return reusesGangPrivate ? WalkResult::interrupt() : WalkResult::advance();
2381 });
2382 if (!reusesGangPrivate)
2383 return false;
2384
2385 // A barrier inside the body is only safe when the whole workgroup runs every
2386 // iteration, so bail out under any thread-level worksharing loop, whose trip
2387 // count varies per thread.
2388 for (Operation *p = loopOp->getParentOp();
2389 p && p != computeRegion.getOperation(); p = p->getParentOp()) {
2390 scf::ParallelOp par = dyn_cast<scf::ParallelOp>(p);
2391 if (!par)
2392 continue;
2393 if (mlir::acc::GPUParallelDimsAttr parDims = mlir::acc::getParDimsAttr(par))
2394 for (mlir::acc::GPUParallelDimAttr d : parDims.getArray())
2395 if (d.isThreadX() || d.isThreadY())
2396 return false;
2397 }
2398 return true;
2399}
2400
2401void ACCCGToGPULowering::processPredicateRegion(
2402 acc::PredicateRegionOp interOp) {
2403 LLVM_DEBUG(llvm::dbgs() << "processing predicate region: ";
2404 interOp->print(llvm::dbgs()); llvm::dbgs() << "\n");
2405 Location loc = interOp->getLoc();
2406
2407 std::pair<SmallVector<mlir::acc::GPUParallelDimAttr>,
2408 SmallVector<mlir::acc::GPUParallelDimAttr>>
2409 parDimsPair = computeActiveAndInactiveParDims(
2410 interOp, &interOp.getRegion().front());
2411 if (hasFailed)
2412 return;
2413
2414 // If ThreadY reduction exists, subgroup alignment is applied
2415 // (blockDim.x = subgroupSize), so ThreadX lanes exist even without explicit
2416 // ThreadX parallelism. Add ThreadX to inactiveParDims if not already present.
2417 // Exception: if this region contains a thread-level (vector or worker)
2418 // routine call, all ThreadX threads must reach the call so the routine's
2419 // workgroup-wide barriers (e.g. shared memory alloca sync) converge.
2420 // A single-thread worker launch is left unaligned, so it has no such lanes.
2421 if (hasThreadYReduction && !isSingleThreadWorkerLaunch()) {
2422 MLIRContext *ctx = computeRegion->getContext();
2423 mlir::acc::GPUParallelDimAttr threadXParDim =
2424 mlir::acc::GPUParallelDimAttr::threadXDim(ctx);
2425 bool hasThreadXInActive =
2426 llvm::any_of(parDimsPair.first, [](mlir::acc::GPUParallelDimAttr pd) {
2427 return pd.isThreadX();
2428 });
2429 bool hasThreadXInInactive =
2430 llvm::any_of(parDimsPair.second, [](mlir::acc::GPUParallelDimAttr pd) {
2431 return pd.isThreadX();
2432 });
2433
2434 // Check if THIS predicate region contains a thread-level routine call.
2435 // We use the pre-computed hasThreadLevelRoutineCall as an early-out
2436 // optimization.
2437 bool regionHasThreadLevelRoutineCall = false;
2438 if (hasThreadLevelRoutineCall) {
2439 interOp.getRegion().walk([&](CallOpInterface callOp) {
2440 if (mlir::acc::GPUParallelDimAttr parDim =
2441 getAccRoutineCallParDim(callOp, defaultPolicy)) {
2442 if (parDim.isThreadX() || parDim.isThreadY()) {
2443 regionHasThreadLevelRoutineCall = true;
2444 return WalkResult::interrupt();
2445 }
2446 }
2447 return WalkResult::advance();
2448 });
2449 }
2450
2451 if (!hasThreadXInActive && !hasThreadXInInactive &&
2452 !regionHasThreadLevelRoutineCall) {
2453 parDimsPair.second.push_back(threadXParDim);
2454 }
2455 }
2456
2457 Value predicate = emitPredicate(loc, parDimsPair.second);
2458 // With one thread per block nothing is predicated, but a block-level
2459 // reduction store below must still become a cross-block atomic.
2460 if (!predicate && llvm::all_of(computeRegion.getLaunchParDims(),
2461 [](mlir::acc::GPUParallelDimAttr pd) {
2462 return pd.isAnyBlock();
2463 }))
2464 predicate = arith::ConstantIntOp::create(rewriter, loc, 1, 1);
2465 if (predicate) {
2466 LLVM_DEBUG(llvm::dbgs() << "predicate: " << predicate << "\n");
2467 bool isInsideThreadXLoop = false;
2468 bool isInsideThreadYLoop = false;
2469 for (auto parDim : parDimsPair.first) {
2470 if (parDim.isThreadX())
2471 isInsideThreadXLoop = true;
2472 if (parDim.isThreadY())
2473 isInsideThreadYLoop = true;
2474 }
2475 // Emits the reconvergence barrier matching this region's predication level,
2476 // at the current insertion point. Called for both before and after the
2477 // predicated store. Below is the pre-predicate barrier; the post-predicate
2478 // barrier is emitted after the ifOp.
2479 auto emitReconvergenceBarrier = [&]() {
2480 if (isInsideThreadXLoop) {
2481 // Inside ThreadX loop - skip barrier
2482 } else if (isInsideThreadYLoop) {
2483 // Inside ThreadY loop
2484 if (!isInsideACCSpecializedRoutine(computeRegion)) {
2485 // Add barrier if ThreadX is predicated (lane 0 writes must be
2486 // visible to all lanes before they read).
2487 bool predicatesThreadX = llvm::any_of(
2488 parDimsPair.second,
2489 [](mlir::acc::GPUParallelDimAttr pd) { return pd.isThreadX(); });
2490 if (predicatesThreadX)
2491 createPerRowBarrier(loc);
2492 }
2493 // For acc routine ThreadY routines, skip barrier
2494 } else if (!parDimsPair.first.empty()) {
2495 // Inside block loop
2496 createBarrier(loc, mlir::acc::GPUParallelDimsAttr::get(
2497 interOp->getContext(), parDimsPair.first));
2498 } else {
2499 // Top level
2500 createBarrier(loc, mlir::acc::GPUParallelDimsAttr::get(
2501 interOp->getContext(), parDimsPair.second));
2502 }
2503 };
2504
2505 // A gang-level (block, no thread) shared slot written here and reused on
2506 // the next iteration of an enclosing sequential loop must not be
2507 // overwritten before all threads read the current value. Emit the
2508 // reconvergence barrier before the store too (the post-store barrier below
2509 // only orders this iteration's store->read). Restricted to the block-level
2510 // case: such loops are run uniformly by every workgroup thread, so the
2511 // workgroup barrier cannot deadlock; worker/vector (thread_y/thread_x)
2512 // loops may have divergent trip counts.
2513 PrivateMemScope scope = needsPreStoreReuseBarrier(interOp);
2514 if (scope == PrivateMemScope::Gang)
2515 emitGPUBarrierWorkgroup(rewriter, loc);
2516 else if (scope == PrivateMemScope::Worker)
2517 createPerRowBarrier(loc);
2518
2519 auto ifOp = scf::IfOp::create(rewriter, loc, predicate,
2520 /*withElseRegion=*/false);
2521 Region &thenRegion = ifOp.getThenRegion();
2522 Block &thenBlock = thenRegion.back();
2523 rewriter.setInsertionPoint(thenBlock.getTerminator());
2524 // Ops in a predicate region may need to be further processed, recurse
2525 for (auto &bodyOp : interOp.getRegion().front().getOperations()) {
2526 // If the store's value loads from a block-level reduction
2527 // memref, convert to atomic for cross-block correctness.
2528 // Only at kernel top level (no active thread dims).
2529 if (memref::StoreOp storeOp = dyn_cast<memref::StoreOp>(&bodyOp)) {
2530 std::optional<arith::AtomicRMWKind> blockReduceKind;
2531 bool failedReductionKind = false;
2532 Value storeVal = storeOp.getValueToStore();
2533 if (storeVal.getDefiningOp()) {
2534 // Walk the epilogue def-chain within the enclosing block to find
2535 // the block-level accumulate load feeding this store. Epilogue ops
2536 // (type conversions, arithmetic, etc.) are traversed transparently.
2537 // Non-acc loads and values defined outside the block are treated as
2538 // loop-invariant and return nullopt, bounding the search naturally.
2539 Block *epilogueBlock = interOp->getBlock();
2540 auto findBlockAccLoad =
2541 [&](auto &self,
2542 Value val) -> std::optional<arith::AtomicRMWKind> {
2543 Operation *def = val.getDefiningOp();
2544 if (!def || def->getBlock() != epilogueBlock)
2545 return std::nullopt;
2546 if (memref::LoadOp loadOp = dyn_cast<memref::LoadOp>(def)) {
2547 for (auto *user : loadOp.getMemRef().getUsers()) {
2548 if (acc::ReductionAccumulateOp accOp =
2549 dyn_cast<acc::ReductionAccumulateOp>(user)) {
2550 if (llvm::any_of(accOp.getParDims().getArray(),
2551 [](mlir::acc::GPUParallelDimAttr pd) {
2552 return pd.isAnyBlock();
2553 })) {
2554 FailureOr<arith::AtomicRMWKind> kind = getReductionKind(
2555 accOp.getReductionOperator(),
2556 accOp.getValue().getType(), accOp.getLoc());
2557 if (failed(kind)) {
2558 failedReductionKind = true;
2559 return std::nullopt;
2560 }
2561 return *kind;
2562 }
2563 }
2564 }
2565 return std::nullopt;
2566 }
2567 for (Value operand : def->getOperands())
2568 if (auto kind = self(self, operand))
2569 return kind;
2570 return std::nullopt;
2571 };
2572 blockReduceKind = findBlockAccLoad(findBlockAccLoad, storeVal);
2573 }
2574 if (failedReductionKind)
2575 return;
2576 if (blockReduceKind) {
2577 Value input = mapping.lookupOrDefault(storeOp.getValueToStore());
2578 Value memref = mapping.lookupOrDefault(storeOp.getMemref());
2579 bool threadIsActive = llvm::any_of(
2580 parDimsPair.first, [](mlir::acc::GPUParallelDimAttr pd) {
2581 return !pd.isAnyBlock();
2582 });
2583 if (!threadIsActive &&
2584 !isa_and_nonnull<memref::AllocaOp>(
2585 unwrapMemRefConversion(memref).getDefiningOp())) {
2586 // Initialize the destination to the reduction identity before
2587 // cross-block atomics so that the final result reflects pure
2588 // assignment semantics (e.g. r = sum(a)), not accumulation
2589 // on top of the pre-kernel value.
2590 if (launch) {
2591 MemRefType memrefTy = cast<MemRefType>(memref.getType());
2592 // Map the store indices for ranked memrefs.
2593 SmallVector<Value> initIndices;
2594 for (Value idx : storeOp.getIndices())
2595 initIndices.push_back(mapping.lookupOrDefault(idx));
2596
2597 OpBuilder::InsertionGuard guard(rewriter);
2598 Block &launchBody = launch.getBody().front();
2599 Operation *insertBefore = nullptr;
2600
2601 launchBody.walk([&](scf::ParallelOp parOp) -> WalkResult {
2602 for (Operation *parent = parOp->getParentOp(); parent;
2603 parent = parent->getParentOp()) {
2604 if (parent == launch.getOperation())
2605 break;
2606 if (isa<scf::ParallelOp>(parent))
2607 return WalkResult::advance();
2608 }
2609 insertBefore = parOp.getOperation();
2610 return WalkResult::interrupt();
2611 });
2612 // The identity must be ordered before the atomics of every
2613 // block, so store it from a launch ahead of this one when the
2614 // address does not depend on the launch.
2615 std::function<bool(Value)> fromLaunch = [&](Value v) -> bool {
2616 if (auto arg = dyn_cast<BlockArgument>(v))
2617 return launch->isAncestor(arg.getOwner()->getParentOp());
2618 Operation *def = v.getDefiningOp();
2619 if (!launch->isAncestor(def) ||
2620 def->hasTrait<OpTrait::ConstantLike>())
2621 return false;
2622 // Ids and dims such as gpu.grid_dim take no operands.
2623 return def->getNumOperands() == 0 ||
2624 llvm::any_of(def->getOperands(), fromLaunch);
2625 };
2626 if (!fromLaunch(memref) &&
2627 llvm::none_of(initIndices, fromLaunch)) {
2628 rewriter.setInsertionPoint(launch);
2629 Value one = arith::ConstantIndexOp::create(rewriter, loc, 1);
2630 Value token = launch.getAsyncToken();
2631 auto initLaunch = gpu::LaunchOp::create(
2632 rewriter, loc, one, one, one, one, one, one,
2633 /*dynamicSharedMemorySize=*/nullptr,
2634 token ? token.getType() : Type(),
2635 launch.getAsyncDependencies());
2636 rewriter.setInsertionPointToStart(
2637 &initLaunch.getBody().front());
2638 rewriter.setInsertionPoint(
2639 gpu::TerminatorOp::create(rewriter, loc));
2640 } else if (insertBefore)
2641 rewriter.setInsertionPoint(insertBefore);
2642 else
2643 rewriter.setInsertionPointToStart(&launchBody);
2644 // Recursively re-materialize operations whose definitions
2645 // do not dominate the insertion point. A single-level clone
2646 // is insufficient when the value is produced by a chain of
2647 // operations (e.g. reinterpret_cast depending on box_dims,
2648 // divsi, convert, etc.) that are all defined after the
2649 // insertion point.
2650 DominanceInfo domInfo(launch);
2651 IRMapping initMapping;
2652 std::function<Value(Value)> materialize =
2653 [&](Value val) -> Value {
2654 Operation *defOp = val.getDefiningOp();
2655 if (!defOp)
2656 return val;
2657 if (domInfo.dominates(defOp, &*rewriter.getInsertionPoint()))
2658 return val;
2659 if (auto mapped = initMapping.lookupOrNull(val))
2660 return mapped;
2661 // Recurse on operands; the recursive call seeds
2662 // initMapping for any operand it clones, which the
2663 // subsequent rewriter.clone(..., initMapping) picks up.
2664 for (Value operand : defOp->getOperands())
2665 materialize(operand);
2666 Operation *cloned = rewriter.clone(*defOp, initMapping);
2667 for (auto [orig, clonedRes] :
2668 llvm::zip(defOp->getResults(), cloned->getResults())) {
2669 initMapping.map(orig, clonedRes);
2670 }
2671 return initMapping.lookup(val);
2672 };
2673 Value initMemref = materialize(memref);
2674 for (auto &idx : initIndices)
2675 idx = materialize(idx);
2676 Value identityVal = createIdentityValue(
2677 rewriter, loc, memrefTy.getElementType(), *blockReduceKind,
2678 /*useOnlyFiniteValue=*/true);
2679 Value blockId = gpu::BlockIdOp::create(
2680 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
2681 Value threadId = gpu::ThreadIdOp::create(
2682 rewriter, loc, rewriter.getIndexType(), gpu::Dimension::x);
2683 Value zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
2684 Value isBlock0 = arith::CmpIOp::create(
2685 rewriter, loc, arith::CmpIPredicate::eq, blockId, zero);
2686 Value isThread0 = arith::CmpIOp::create(
2687 rewriter, loc, arith::CmpIPredicate::eq, threadId, zero);
2688 Value isFirstThread =
2689 arith::AndIOp::create(rewriter, loc, isBlock0, isThread0);
2690 auto initIf = scf::IfOp::create(rewriter, loc, isFirstThread,
2691 /*withElseRegion=*/false);
2692 rewriter.setInsertionPoint(
2693 initIf.getThenRegion().back().getTerminator());
2694 memref::StoreOp::create(rewriter, loc, identityVal, initMemref,
2695 initIndices);
2696 rewriter.setInsertionPointAfter(initIf);
2697 gpu::BarrierOp::create(rewriter, loc);
2698 }
2699 SmallVector<Value> atomicIndices;
2700 for (Value idx : storeOp.getIndices())
2701 atomicIndices.push_back(mapping.lookupOrDefault(idx));
2702 constructAtomicAccumulation(loc, memref, atomicIndices, input,
2703 *blockReduceKind);
2704 continue;
2705 }
2706 }
2707 }
2708 processOp(&bodyOp);
2709 }
2710 rewriter.setInsertionPointAfter(ifOp);
2711 emitReconvergenceBarrier();
2712 } else {
2713 // Ops in a predicate region may need to be further processed, recurse
2714 for (auto &bodyOp : interOp.getRegion().front().getOperations())
2715 processOp(&bodyOp);
2716 }
2717}
2718
2719// clang-format off
2720//
2721// Allocates private storage and broadcasts its pointer through a shared
2722// memref-of-memref slot, using the same predication utility as predicate
2723// regions.
2724//
2725// %0 = arith.cmpi eq, %thread_id_y, %c0 : index
2726// %1 = arith.cmpi eq, %thread_id_x, %c0 : index
2727// %2 = arith.andi %1, %0 : i1
2728// scf.if %2 {
2729// %alloc = memref.alloc() : memref<10xi32>
2730// memref.store %alloc, %arg1[] : memref<memref<10xi32>, #gpu.address_space<workgroup>>
2731// }
2732// gpu.barrier scope<subgroup>
2733// %4 = memref.load %arg1[] : memref<memref<10xi32>, #gpu.address_space<workgroup>>
2734//
2735// clang-format on
2736
2737Value ACCCGToGPULowering::allocatePrivatizeHeapStorage(
2738 acc::PrivatizeOp privatize, MemRefType baseTy,
2739 ValueRange mappedDynamicSizes) {
2740 Location loc = privatize.getLoc();
2741 Value mem =
2742 memref::AllocOp::create(rewriter, loc, baseTy, mappedDynamicSizes);
2743 OpBuilder::InsertionGuard guard(rewriter);
2744 Block *allocBlock = mem.getDefiningOp()->getBlock();
2745 if (allocBlock->mightHaveTerminator()) {
2746 rewriter.setInsertionPoint(allocBlock->getTerminator());
2747 memref::DeallocOp::create(rewriter, loc, mem);
2748 }
2749 mapping.map(privatize.getResult(), mem);
2750 // Ops inside the kernel are rewritten from scratch. If privatize is
2751 // outside the kernel, it must be replaced with the allocated storage.
2752 if (!privatize->getParentOfType<acc::ComputeRegionOp>())
2753 rewriter.replaceOp(privatize, mem);
2754 return mem;
2755}
2756
2757Value ACCCGToGPULowering::processPrivatize(acc::PrivatizeOp privatize) {
2758 LLVM_DEBUG(llvm::dbgs() << "processing privatize: ";
2759 privatize->print(llvm::dbgs()); llvm::dbgs() << "\n");
2760 Value tracked = privatize.getResult();
2761 if (acc::ComputeRegionOp insUser =
2762 dyn_cast<acc::ComputeRegionOp>(getOnlyUser(tracked))) {
2763 assert(privatize->hasOneUse() &&
2764 "expected acc.privatize op to have one use");
2765 tracked = insUser.getBody()->getArgument(
2766 privatize->use_begin()->getOperandNumber());
2767 }
2768 Operation *privatizeUser = getOnlyUser(tracked);
2769 assert(privatizeUser && "expected PrivateLocalOp user for privatize");
2770 acc::PrivateLocalOp privateLocalUser =
2771 dyn_cast<acc::PrivateLocalOp>(privatizeUser);
2772
2773 std::pair<SmallVector<mlir::acc::GPUParallelDimAttr>,
2774 SmallVector<mlir::acc::GPUParallelDimAttr>>
2775 parDimsPair = computeActiveAndInactiveParDims(privatizeUser, nullptr);
2776 // Set `par_dims` only when this `acc.privatize` does not already carry it.
2777 if (!privatize.getParDimsAttr()) {
2778 privatize.setParDimsAttr(mlir::acc::GPUParallelDimsAttr::get(
2779 rewriter.getContext(), parDimsPair.first));
2780 }
2781
2782 Location loc = privatize->getLoc();
2783 acc::PrivateType privTy = cast<acc::PrivateType>(privatize.getType());
2784 ModuleOp module = computeRegion->getParentOfType<ModuleOp>();
2785 MemRefType baseTy = getPrivateBaseMemRefType(privTy.getBaseTy(), module);
2786
2787 gpu::GPUFuncOp gpuFuncOp = computeRegion->getParentOfType<gpu::GPUFuncOp>();
2788 // acc.privatize is outside this compute_region (e.g. passed via ins).
2789 // Leave the op unchanged here; processPrivateLocal materializes
2790 // storage when acc.private_local is lowered.
2791 if (!gpuFuncOp &&
2792 privatize->getParentOfType<acc::ComputeRegionOp>() != computeRegion) {
2793 return privatize.getResult();
2794 }
2795
2796 bool threadXActive =
2797 llvm::any_of(parDimsPair.first, [](mlir::acc::GPUParallelDimAttr parDim) {
2798 return parDim.isThreadX();
2799 });
2800 if (threadXActive &&
2801 canUseStackAlloca(baseTy, loc, options.maxThreadPrivateStack)) {
2802 auto alloca = memref::AllocaOp::create(rewriter, loc, baseTy);
2803 recordThreadPrivate(privateLocalUser);
2804 mapping.map(privatize.getResult(), alloca.getResult());
2805 return alloca.getResult();
2806 }
2807
2808 if (!gpuFuncOp)
2809 return privatize.getResult();
2810
2811 // When ThreadY is active, shared memory must be indexed by ThreadY ID to
2812 // avoid races between ThreadY threads.
2813 // For ThreadX acc routines, assume ThreadY may be active since the routine
2814 // can be called from a ThreadY loop at runtime.
2815 // For ThreadY/block acc routines, do not force ThreadY indexing - variables
2816 // outside the ThreadY loop should be shared across ThreadY threads.
2817 bool threadYIsActive =
2818 llvm::any_of(parDimsPair.first, [](mlir::acc::GPUParallelDimAttr parDim) {
2819 return parDim.isThreadY();
2820 });
2821 // For routines with multiple ThreadY threads, need a workgroup barrier
2822 // instead of a subgroup barrier.
2823 // - ThreadX routines: called independently by different ThreadY threads, need
2824 // per-ThreadY slots (override threadYIsActive)
2825 // - ThreadY routines: workgroup barrier, but respect parDimsPair for
2826 // threadYIsActive
2827 // (variables before ThreadY loop are shared, inside are per-ThreadY)
2828 // - Block routines: single call, ThreadY threads cooperate within the
2829 // routine, so
2830 // variables at routine level are shared across ThreadY threads (single
2831 // slot)
2832 bool needsWorkgroupBarrier = false;
2833 if (isInsideACCSpecializedRoutine(computeRegion)) {
2834 FunctionOpInterface funcOp =
2835 computeRegion->getParentOfType<FunctionOpInterface>();
2836 mlir::acc::GPUParallelDimAttr routineParDim =
2837 getSpecializedRoutineDim(funcOp, defaultPolicy);
2838 if (routineParDim.isThreadX()) {
2839 // ThreadX routine: always per-ThreadY slots since called from ThreadY
2840 // loops
2841 threadYIsActive = true;
2842 } else if (routineParDim.isThreadY()) {
2843 // ThreadY routine: workgroup barrier, but keep original threadYIsActive
2844 // (variables before ThreadY loop shared, inside per-ThreadY)
2845 needsWorkgroupBarrier = true;
2846 } else if (routineParDim.isAnyBlock()) {
2847 needsWorkgroupBarrier = true;
2848 }
2849 }
2850
2851 llvm::SmallVector<Value> mappedDynamicSizes;
2852 for (auto dynamicSize : privatize.getDynamicSizes()) {
2853 Value mappedDynamicSize = mapping.lookupOrDefault(dynamicSize);
2854 mappedDynamicSizes.push_back(mappedDynamicSize);
2855 }
2856 if (isInsideACCSpecializedRoutine(computeRegion) &&
2857 computeRegion.isEffectivelySerial()) {
2858 if (mappedDynamicSizes.empty()) {
2859 // Static sizes: use alloca (stack allocation)
2860 auto alloca =
2861 memref::AllocaOp::create(rewriter, privatize->getLoc(), baseTy);
2862 recordThreadPrivate(privateLocalUser);
2863 mapping.map(privatize.getResult(), alloca.getResult());
2864 return alloca.getResult();
2865 }
2866 // Dynamic sizes: use alloc (heap allocation) with dealloc
2867 return allocatePrivatizeHeapStorage(privatize, baseTy, mappedDynamicSizes);
2868 }
2869
2870 if (threadXActive) {
2871 // Reached only after canUseStackAlloca failed: either the static size is
2872 // over the per-thread stack budget, or the shape is dynamic so the size
2873 // is unknown. Heap-allocate in both cases so a large runtime extent
2874 // cannot overflow the stack. Each lane still owns its own copy.
2875 return allocatePrivatizeHeapStorage(privatize, baseTy, mappedDynamicSizes);
2876 }
2877
2878 // Predication - when threadYIsActive, don't predicate on ThreadY dimension
2879 // since each ThreadY needs to execute the allocation for its own slot
2880 SmallVector<mlir::acc::GPUParallelDimAttr> predicateDims;
2881 for (auto parDim : parDimsPair.second) {
2882 // Skip ThreadY if threadYIsActive - each ThreadY needs to allocate
2883 if (threadYIsActive && parDim.isThreadY())
2884 continue;
2885 predicateDims.push_back(parDim);
2886 }
2887 Value predicate = emitPredicate(loc, predicateDims);
2888 if (!predicate) {
2889 predicate = arith::ConstantOp::create(
2890 rewriter, loc, rewriter.getIntegerAttr(rewriter.getI1Type(), 1));
2891 }
2892 auto ifOp = scf::IfOp::create(rewriter, loc, predicate,
2893 /*withElseRegion=*/false);
2894 Region &thenRegion = ifOp.getThenRegion();
2895 Block &thenBlock = thenRegion.back();
2896 rewriter.setInsertionPoint(thenBlock.getTerminator());
2897 auto mem = memref::AllocOp::create(rewriter, privatize->getLoc(), baseTy,
2898 mappedDynamicSizes);
2899 // Shared memory allocation
2900 gpu::AddressSpaceAttr sharedMemoryAddressSpace = gpu::AddressSpaceAttr::get(
2901 computeRegion->getContext(), gpu::GPUDialect::getWorkgroupAddressSpace());
2902 // When ThreadY is active, create a shared memory array indexed by ThreadY ID.
2903 // Each ThreadY stores to its own slot; ThreadX lanes within a ThreadY share
2904 // it.
2905 constexpr int64_t kMaxThreadY = 32;
2906 MemRefType sharedMemTy =
2907 threadYIsActive
2908 ? MemRefType::get({kMaxThreadY}, baseTy, MemRefLayoutAttrInterface{},
2909 sharedMemoryAddressSpace)
2910 : MemRefType::get({}, baseTy, MemRefLayoutAttrInterface{},
2911 sharedMemoryAddressSpace);
2912 // The slot only transiently broadcasts the storage pointer, so reuse one per
2913 // type across privatizes, barriering (before the predicated store) on reuse.
2914 bool reuseBroadcast = !gpuFuncOp.isKernel();
2915 Value alloca;
2916 llvm::DenseMap<Type, Value>::iterator cachedSlot =
2917 reuseBroadcast ? privatizeBroadcastCache.find(sharedMemTy)
2918 : privatizeBroadcastCache.end();
2919 if (reuseBroadcast && cachedSlot != privatizeBroadcastCache.end()) {
2920 alloca = cachedSlot->second;
2921 OpBuilder::InsertionGuard guard(rewriter);
2922 rewriter.setInsertionPoint(ifOp);
2923 mlir::acc::GPUParallelDimAttr dim =
2924 needsWorkgroupBarrier
2925 ? mlir::acc::GPUParallelDimAttr::threadYDim(rewriter.getContext())
2926 : mlir::acc::GPUParallelDimAttr::threadXDim(rewriter.getContext());
2927 createBarrier(
2928 loc, mlir::acc::GPUParallelDimsAttr::get(rewriter.getContext(), {dim}));
2929 } else {
2930 alloca = gpuFuncOp.addWorkgroupAttribution(sharedMemTy,
2931 rewriter.getUnknownLoc());
2932 // Setting the alignment to 16 because of a bug in the gpu toolchain.
2933 // The default alignment is 8, but optimizations create a packed store of 16
2934 // bytes which cause a misalignment error at runtime.
2935 unsigned index = gpuFuncOp.getNumWorkgroupAttributions() - 1;
2936 gpuFuncOp.setWorkgroupAttributionAttr(index,
2937 LLVM::LLVMDialect::getAlignAttrName(),
2938 rewriter.getI32IntegerAttr(16));
2939 if (reuseBroadcast)
2940 privatizeBroadcastCache[sharedMemTy] = alloca;
2941 }
2942 // Store to shared memory, indexed by ThreadY ID when ThreadY is active.
2943 if (threadYIsActive) {
2944 Value threadYId = getThreadId(loc, gpu::Dimension::y);
2945 memref::StoreOp::create(rewriter, privatize->getLoc(), mem, alloca,
2946 ValueRange{threadYId});
2947 } else {
2948 memref::StoreOp::create(rewriter, privatize->getLoc(), mem, alloca);
2949 }
2950
2951 // Sync and load - use workgroup barrier for Block/ThreadY routines,
2952 // ThreadX barrier for ThreadX-only routines
2953 rewriter.setInsertionPointAfter(ifOp);
2954 if (needsWorkgroupBarrier) {
2955 mlir::acc::GPUParallelDimsAttr threadYDimsAttr =
2956 mlir::acc::GPUParallelDimsAttr::get(
2957 rewriter.getContext(),
2958 {mlir::acc::GPUParallelDimAttr::threadYDim(rewriter.getContext())});
2959 createBarrier(loc, threadYDimsAttr);
2960 } else {
2961 // ThreadX-only: use per-ThreadY barrier
2962 mlir::acc::GPUParallelDimsAttr threadXDimsAttr =
2963 mlir::acc::GPUParallelDimsAttr::get(
2964 rewriter.getContext(),
2965 {mlir::acc::GPUParallelDimAttr::threadXDim(rewriter.getContext())});
2966 createBarrier(loc, threadXDimsAttr);
2967 }
2968 // Load from shared memory, indexed by ThreadY ID when ThreadY is active.
2969 Value load;
2970 if (threadYIsActive) {
2971 Value threadYId = getThreadId(loc, gpu::Dimension::y);
2972 load = memref::LoadOp::create(rewriter, privatize->getLoc(), alloca,
2973 ValueRange{threadYId});
2974 } else {
2975 load = memref::LoadOp::create(rewriter, privatize->getLoc(), alloca,
2976 ValueRange{});
2977 }
2978 rewriter.setInsertionPointAfter(load.getDefiningOp());
2979 mapping.map(privatize.getResult(), load);
2980
2981 // Operations inside the kernel are all rewritten from scratch.
2982 // But if the privatize op is outside the kernel, it needs to be replaced.
2983 if (!privatize->getParentOfType<acc::ComputeRegionOp>())
2984 rewriter.replaceOp(privatize, load);
2985 // Deallocate
2986 rewriter.setInsertionPoint(ifOp->getBlock()->getTerminator());
2987 if (needsWorkgroupBarrier) {
2988 mlir::acc::GPUParallelDimsAttr workerDimsAttr =
2989 mlir::acc::GPUParallelDimsAttr::get(
2990 rewriter.getContext(),
2991 {mlir::acc::GPUParallelDimAttr::threadYDim(rewriter.getContext())});
2992 createBarrier(loc, workerDimsAttr);
2993 } else {
2994 mlir::acc::GPUParallelDimsAttr vectorDimsAttr =
2995 mlir::acc::GPUParallelDimsAttr::get(
2996 rewriter.getContext(),
2997 {mlir::acc::GPUParallelDimAttr::threadXDim(rewriter.getContext())});
2998 createBarrier(loc, vectorDimsAttr);
2999 }
3000 auto ifOp2 = scf::IfOp::create(rewriter, loc, predicate,
3001 /*withElseRegion=*/false);
3002 Region &thenRegion2 = ifOp2.getThenRegion();
3003 Block &thenBlock2 = thenRegion2.back();
3004 rewriter.setInsertionPoint(thenBlock2.getTerminator());
3005 memref::DeallocOp::create(rewriter, privatize->getLoc(), load);
3006
3007 // Return the private memory
3008 rewriter.setInsertionPointAfter(load.getDefiningOp());
3009
3010 return load;
3011}
3012
3013// Materialize acc.private_local storage from acc.privatize: per-thread alloca
3014// when possible, otherwise a shared broadcast slot or acc.gpu_shared_memory.
3015
3016void ACCCGToGPULowering::processPrivateLocal(
3017 acc::PrivateLocalOp privateLocal, std::optional<int64_t> sharedMemCopies) {
3018 LLVM_DEBUG(llvm::dbgs() << "processing private local: ";
3019 privateLocal->print(llvm::dbgs()); llvm::dbgs() << "\n");
3020 Location loc = privateLocal.getLoc();
3021 acc::PrivateType privTy =
3022 cast<acc::PrivateType>(privateLocal.getPrivatized().getType());
3023 ModuleOp module = computeRegion->getParentOfType<ModuleOp>();
3024 MemRefType baseTy = getPrivateBaseMemRefType(privTy.getBaseTy(), module);
3025 MemRefType byteMemrefTy =
3026 MemRefType::get({ShapedType::kDynamic}, rewriter.getI8Type());
3027
3028 acc::PrivatizeOp privatizeOp = getPrivatizeOp(privateLocal, computeRegion);
3029 Value inputMem;
3030 if (privatizeOp->getParentOfType<acc::ComputeRegionOp>() == computeRegion) {
3031 inputMem = mapping.lookupOrNull(privatizeOp);
3032 if (inputMem) {
3033 Value result = castPointerLikeTypeIfNeeded(rewriter, loc, inputMem,
3034 privateLocal.getType());
3035 mapping.map(privateLocal.getResult(), result);
3036 return;
3037 }
3038 } else {
3039 // Hoisted acc.privatize: allocate per-thread stack storage in the launch
3040 // body. Cross-thread array reduction accumulators are per-thread too, so
3041 // the accumulate can reduce each element across threads. Skip when storage
3042 // par_dims lack thread_x (gang-/worker-scoped array temp).
3043 acc::ReductionAccumulateArrayOp arrayAccum =
3044 perThreadArrayReductionAccum(privateLocal.getResult());
3045 if ((isThreadXPrivatize(privatizeOp) ||
3046 (arrayAccum && storageHasThreadX(privateLocal, computeRegion))) &&
3047 canUseStackAlloca(baseTy, loc, options.maxThreadPrivateStack)) {
3048 Value alloca = memref::AllocaOp::create(rewriter, loc, baseTy);
3049 recordThreadPrivate(privateLocal);
3050 if (arrayAccum) {
3051 FailureOr<arith::AtomicRMWKind> kind = getReductionKind(
3052 arrayAccum.getReductionOperator(), baseTy.getElementType(), loc);
3053 if (failed(kind))
3054 return;
3055 initPerThreadArrayAccum(rewriter, loc, alloca, baseTy, *kind);
3056 }
3057 Value mem = castPointerLikeTypeIfNeeded(rewriter, loc, alloca,
3058 privateLocal.getType());
3059 mapping.map(privateLocal.getResult(), mem);
3060 return;
3061 }
3062
3063 // If acc.privatize is outside the kernel, it needs to be converted
3064 // explicitly.
3065 std::optional<int64_t> copies =
3066 sharedMemCopies ? sharedMemCopies
3067 : isEligibleForSharedMemory(privateLocal, baseTy);
3068 if (copies) {
3069 int64_t numCopies = *copies;
3070 int64_t elementSize = getElementSizeInBytes(loc, baseTy.getElementType());
3071 int64_t numElements = 1;
3072 for (int64_t dim : baseTy.getShape())
3073 numElements *= dim;
3074 int64_t upperBound = elementSize * numElements * numCopies;
3075
3076 if (tryAllocateSharedMemory(upperBound)) {
3077 std::string varName =
3078 accSupport.getVariableName(privateLocal.getResult());
3079 sharedMemPrivateVarNames.push_back(varName.empty() ? "(*)" : varName);
3080
3081 gpu::AddressSpaceAttr workgroupAS = gpu::AddressSpaceAttr::get(
3082 computeRegion->getContext(),
3083 gpu::GPUDialect::getWorkgroupAddressSpace());
3084 MemRefType sharedMemTy =
3085 MemRefType::get(baseTy.getShape(), baseTy.getElementType(),
3086 MemRefLayoutAttrInterface{}, workgroupAS);
3087 Value sharedMem = acc::GPUSharedMemoryOp::create(
3088 rewriter, loc, sharedMemTy, rewriter.getI64IntegerAttr(numCopies),
3089 rewriter.getI64IntegerAttr(upperBound), ValueRange{}, IntegerAttr{},
3090 IntegerAttr{});
3091
3092 Value mem =
3093 castPointerLikeTypeIfNeeded(rewriter, loc, sharedMem, baseTy);
3094 Value result = castPointerLikeTypeIfNeeded(rewriter, loc, mem,
3095 privateLocal.getType());
3096
3097 mapping.map(privateLocal.getResult(), result);
3098 return;
3099 }
3100 }
3101
3102 OpBuilder::InsertionGuard guard(rewriter);
3103 rewriter.setInsertionPoint(privatizeOp);
3104 inputMem = processPrivatize(privatizeOp);
3105 }
3106
3107 if (isInsideACCSpecializedRoutine(computeRegion)) {
3108 assert(inputMem && "expected input mem to be mapped");
3109 Value result = castPointerLikeTypeIfNeeded(rewriter, loc, inputMem,
3110 privateLocal.getType());
3111 mapping.map(privateLocal.getResult(), result);
3112 return;
3113 }
3114
3115 // The private element shape augmented with a dimension for each level of
3116 // parallelism
3117 SmallVector<int64_t> viewShape;
3118 // The dynamic sizes of the view (num_gangs, num_workers, vector_length)
3119 SmallVector<Value> viewDynSizes;
3120 // The offset of the subview from gpu.block_id/gpu.thread_id dimensions.
3121 SmallVector<OpFoldResult> subviewOffset;
3122 // The sizes of the subview where the dimensionality is brought back to the
3123 // private element. That is size 1 for each active block/thread dimension.
3124 SmallVector<OpFoldResult> subviewSizes;
3125 // The strides of the subview
3126 SmallVector<int64_t> subviewStrides;
3127 // The shape of the subview
3128 SmallVector<int64_t> subviewShape;
3129
3130 std::pair<SmallVector<mlir::acc::GPUParallelDimAttr>,
3131 SmallVector<mlir::acc::GPUParallelDimAttr>>
3132 parDimsPair = computeActiveAndInactiveParDims(privateLocal, nullptr);
3133 acc::ReductionAccumulateArrayOp arrayAccum =
3134 perThreadArrayReductionAccum(privateLocal.getResult());
3135 for (mlir::acc::GPUParallelDimAttr parDim : parDimsPair.first) {
3136 if ((parDim.isThreadX() ||
3137 (arrayAccum && storageHasThreadX(privateLocal, computeRegion))) &&
3138 canUseStackAlloca(baseTy, loc, options.maxThreadPrivateStack)) {
3139 Value alloca = memref::AllocaOp::create(rewriter, loc, baseTy);
3140 recordThreadPrivate(privateLocal);
3141 if (arrayAccum) {
3142 FailureOr<arith::AtomicRMWKind> kind = getReductionKind(
3143 arrayAccum.getReductionOperator(), baseTy.getElementType(), loc);
3144 if (failed(kind))
3145 return;
3146 initPerThreadArrayAccum(rewriter, loc, alloca, baseTy, *kind);
3147 }
3148 Value mem = castPointerLikeTypeIfNeeded(rewriter, loc, alloca,
3149 privateLocal.getType());
3150 mapping.map(privateLocal.getResult(), mem);
3151 return;
3152 }
3153 }
3154 if (parDimsPair.first.empty()) {
3155 // No parallelism is found above. It's single block execution.
3157 parDimsPair.first,
3158 mlir::acc::GPUParallelDimAttr::blockXDim(privateLocal.getContext()));
3159 }
3160 for (mlir::acc::GPUParallelDimAttr parDim : parDimsPair.first) {
3161 gpu::Processor gpuProc = parDim.getProcessor();
3162 Value gpuSize = getGPUSizeFor(gpuProc);
3163 viewDynSizes.push_back(gpuSize);
3164 viewShape.push_back(ShapedType::kDynamic);
3165 subviewOffset.push_back(getGPUThreadIdFor(gpuProc));
3166 subviewSizes.push_back(rewriter.getIndexAttr(1));
3167 }
3168
3169 SmallVector<Value> innerDynSizes =
3170 resolvePrivateLocalDynamicExtents(privateLocal);
3171
3172 unsigned dynIdx = 0;
3173 for (auto innerDim : baseTy.getShape()) {
3174 subviewOffset.push_back(rewriter.getIndexAttr(0));
3175 viewShape.push_back(innerDim);
3176 subviewShape.push_back(innerDim);
3177 if (innerDim == ShapedType::kDynamic) {
3178 assert(dynIdx < innerDynSizes.size() &&
3179 "not enough dynamic sizes for inner dimensions");
3180 viewDynSizes.push_back(innerDynSizes[dynIdx]);
3181 subviewSizes.push_back(innerDynSizes[dynIdx]);
3182 ++dynIdx;
3183 } else {
3184 subviewSizes.push_back(rewriter.getIndexAttr(innerDim));
3185 }
3186 }
3187
3188 // Do the strides in reverse order.
3189 int64_t stride = 1;
3190 for (auto innerDimIt = baseTy.getShape().rbegin();
3191 innerDimIt != baseTy.getShape().rend(); ++innerDimIt) {
3192 int64_t innerDim = *innerDimIt;
3193 subviewStrides.insert(subviewStrides.begin(), stride);
3194 if (innerDim == ShapedType::kDynamic)
3195 stride = ShapedType::kDynamic;
3196 if (stride != ShapedType::kDynamic)
3197 stride *= innerDim;
3198 }
3199
3200 Value memBuffer =
3201 castPointerLikeTypeIfNeeded(rewriter, loc, inputMem, byteMemrefTy);
3202 auto c0 = arith::ConstantIndexOp::create(rewriter, loc, 0);
3203 MemRefType viewType = MemRefType::get(viewShape, baseTy.getElementType());
3204 auto view = memref::ViewOp::create(rewriter, loc, viewType, memBuffer,
3205 c0.getResult(), viewDynSizes);
3206
3207 // memref.subview
3208 StridedLayoutAttr stridedLayout = StridedLayoutAttr::get(
3209 computeRegion->getContext(), ShapedType::kDynamic, subviewStrides);
3210 MemRefType subviewType =
3211 MemRefType::get(subviewShape, baseTy.getElementType(), stridedLayout);
3212 SmallVector<OpFoldResult> ones(viewType.getRank(), rewriter.getIndexAttr(1));
3213 Value subview = memref::SubViewOp::create(rewriter, loc, subviewType, view,
3214 subviewOffset, subviewSizes, ones);
3215
3216 // Pointer-like casts cannot carry memref offsets. Shift the aligned pointer
3217 // so later zero-offset views retain this thread's private slice.
3218 auto metadata =
3219 memref::ExtractStridedMetadataOp::create(rewriter, loc, subview);
3220 Value elementBytes = arith::ConstantIndexOp::create(
3221 rewriter, loc, getElementSizeInBytes(loc, baseTy.getElementType()));
3222 Value byteOffset =
3223 arith::MulIOp::create(rewriter, loc, metadata.getOffset(), elementBytes);
3224 Value privateView = memref::ViewOp::create(rewriter, loc, baseTy, memBuffer,
3225 byteOffset, innerDynSizes);
3226 Value result = castPointerLikeTypeIfNeeded(rewriter, loc, privateView,
3227 privateLocal.getType());
3228 mapping.map(privateLocal.getResult(), result);
3229}
3230
3231// Could be scf::for or scf::parallel
3232template <typename LoopOp>
3233void ACCCGToGPULowering::processSeqLoop(LoopOp loopOp) {
3234 // Pre-process shared-memory-eligible private_local ops. Only direct-child
3235 // ops are considered; nested private_local ops (e.g. inside predicate_region)
3236 // are handled by recursive body processing.
3237 LLVM_DEBUG(llvm::dbgs() << "processing seq loop: ";
3238 loopOp->print(llvm::dbgs()); llvm::dbgs() << "\n");
3239 llvm::SmallPtrSet<Operation *, 4> preProcessedPrivateLocals;
3240 ModuleOp module = computeRegion->getParentOfType<ModuleOp>();
3241 for (auto &bodyOp : loopOp.getBody()->getOperations()) {
3242 if (acc::PrivateLocalOp privateLocal =
3243 dyn_cast<acc::PrivateLocalOp>(&bodyOp)) {
3244 acc::PrivateType privTy =
3245 cast<acc::PrivateType>(privateLocal.getPrivatized().getType());
3246 MemRefType baseTy = getPrivateBaseMemRefType(privTy.getBaseTy(), module);
3247 if (auto copies = isEligibleForSharedMemory(privateLocal, baseTy)) {
3248 processPrivateLocal(privateLocal, copies);
3249 preProcessedPrivateLocals.insert(privateLocal.getOperation());
3250 }
3251 }
3252 }
3253
3254 LoopOp newLoop =
3255 dyn_cast<LoopOp>(rewriter.cloneWithoutRegions(*loopOp, mapping));
3256 rewriter.createBlock(
3257 &newLoop.getRegion(), newLoop.getRegion().begin(),
3258 loopOp.getBody()->getArgumentTypes(),
3259 SmallVector<Location>(loopOp.getBody()->getArgumentTypes().size(),
3260 loopOp->getLoc()));
3261 rewriter.setInsertionPointToStart(&newLoop.getRegion().front());
3262
3263 // Need to clone all block arguments
3264 Block::BlockArgListType blockArgs = loopOp.getBody()->getArguments();
3265 assert(blockArgs.size() && "expected block arguments for loop");
3266 mapping.map(blockArgs, newLoop.getBody()->getArguments());
3267
3268 // A gang-private slot written here is one copy per workgroup, reused by every
3269 // iteration, and a thread that has already started the next iteration would
3270 // overwrite the value another thread has not read yet. Separate the
3271 // iterations with a barrier at the top of the body.
3272 if (needsInLoopReuseBarrier(loopOp.getOperation()) &&
3273 !isInsideConditional(rewriter.getInsertionBlock()))
3274 emitGPUBarrierWorkgroup(rewriter, loopOp->getLoc());
3275
3276 for (auto &bodyOp : loopOp.getBody()->getOperations()) {
3277 if (preProcessedPrivateLocals.contains(&bodyOp))
3278 continue;
3279 processOp(&bodyOp);
3280 }
3281
3282 mapping.map(loopOp.getResults(), newLoop.getResults());
3283 rewriter.setInsertionPointAfter(newLoop);
3284
3285 // Postpone the barrier when trailing work in this block still separates the
3286 // loop from the next reconvergence point.
3287 if (hasTrailingSideEffectSiblings(loopOp.getOperation()))
3288 deferredBarrierSeqLoops.push_back(loopOp.getOperation());
3289 else
3290 createBarrierAfterSeqLoop(loopOp.getOperation());
3291}
3292
3293void ACCCGToGPULowering::flushDeferredBarriersBefore(Operation *beforeOp) {
3294 Block *block = beforeOp->getBlock();
3295 SmallVector<Operation *, 4> toFlush;
3296 for (Operation *loopOp : deferredBarrierSeqLoops)
3297 if (loopOp->getBlock() == block && loopOp->isBeforeInBlock(beforeOp))
3298 toFlush.push_back(loopOp);
3299 if (toFlush.empty())
3300 return;
3301 llvm::sort(toFlush,
3302 [](Operation *a, Operation *b) { return a->isBeforeInBlock(b); });
3303 for (Operation *loopOp : toFlush)
3304 createBarrierAfterSeqLoop(loopOp);
3305 deferredBarrierSeqLoops.erase(
3306 std::remove_if(deferredBarrierSeqLoops.begin(),
3307 deferredBarrierSeqLoops.end(),
3308 [&](Operation *loopOp) {
3309 return loopOp->getBlock() == block &&
3310 loopOp->isBeforeInBlock(beforeOp);
3311 }),
3312 deferredBarrierSeqLoops.end());
3313}
3314
3315// try to process op as a loop mapped to a gpu parallelism
3316// failure signifies not a loop and needs different processing
3317void ACCCGToGPULowering::processParallelOp(scf::ParallelOp parallelOp) {
3318 LLVM_DEBUG(llvm::dbgs() << "processing par loop: ";
3319 parallelOp->print(llvm::dbgs()); llvm::dbgs() << "\n");
3320 assert(mlir::acc::hasParDimsAttr(parallelOp) &&
3321 "requires parallel dimensions attribute");
3322 mlir::acc::GPUParallelDimsAttr pDimsAttr =
3323 mlir::acc::getParDimsAttr(parallelOp);
3324 // both of these should be dealt with before compiler reaches ACCCGToGPU
3325 // Invalid parallel-loop structure should be rejected before acc-cg-to-gpu.
3326 assert(pDimsAttr.getArray().size() == 1 &&
3327 "expected a single par dim in acc-cg-to-gpu");
3328 assert(parallelOp.getInductionVars().size() == 1 &&
3329 "expected a single induction variable in acc-cg-to-gpu");
3330
3331 mlir::acc::GPUParallelDimAttr parDim = pDimsAttr.getArray().front();
3332
3333 bool savedGridStrideFlag = insideAccumulateGridStride;
3334 Value savedReductionBuf = reductionSharedBuf;
3335 if (parDim.isThreadX()) {
3336 bool found = false;
3337 parallelOp.getBody()->walk([&](acc::ReductionAccumulateOp accOp) {
3338 bool hasBlockDim = false;
3339 bool hasThreadDim = false;
3340 for (auto d : accOp.getParDims().getArray()) {
3341 if (d.isAnyBlock())
3342 hasBlockDim = true;
3343 if (d.isThreadX() || d.isThreadY())
3344 hasThreadDim = true;
3345 }
3346 if (hasThreadDim && !hasBlockDim) {
3347 found = true;
3348 return WalkResult::interrupt();
3349 }
3350 return WalkResult::advance();
3351 });
3352 if (found)
3353 insideAccumulateGridStride = true;
3354 }
3355
3356 // common loop body processing for both par0 and 1+
3357 auto processLoopBody = [&]() {
3358 // process inner ops recursively
3359 for (auto &bodyOp : parallelOp.getBody()->getOperations()) {
3360 if (bodyOp.hasTrait<OpTrait::IsTerminator>()) {
3361 // Non-cloned parallel loops reconverge at their terminator.
3362 flushDeferredBarriersBefore(&bodyOp);
3363 continue;
3364 }
3365 // Non-terminator body ops are processed recursively.
3366 processOp(&bodyOp);
3367 }
3368 };
3369
3370 if (parDim.isSeq()) {
3371 LLVM_DEBUG(llvm::dbgs() << "loop: parDim: " << parDim << " as gpu seq\n");
3372 // Sequential loops here are remainder loops from partitioned parallel
3373 // loops - clone them as-is but process the body as a parallel region.
3374 // When blockDim.x >= subgroupSize and the loop contains a thread-level
3375 // accumulate, inactive grid-stride threads cannot participate in subgroup
3376 // reductions, so we use atomic-to-shared-memory reduction instead.
3377 bool needsAtomicReduction = false;
3378 bool hasAccumulateSibling = false;
3379 if (scf::ParallelOp parentPar =
3380 parallelOp->getParentOfType<scf::ParallelOp>()) {
3381 if (mlir::acc::GPUParallelDimsAttr parentDims =
3382 mlir::acc::getParDimsAttr(parentPar);
3383 parentDims && llvm::any_of(parentDims.getArray(),
3384 [](auto d) { return d.isThreadX(); })) {
3385 for (auto &op : parentPar.getBody()->getOperations()) {
3386 if (acc::ReductionAccumulateOp acc =
3387 dyn_cast<acc::ReductionAccumulateOp>(op)) {
3388 bool hasBlockDim = false;
3389 bool hasThreadDim = false;
3390 for (auto d : acc.getParDims().getArray()) {
3391 if (d.isAnyBlock())
3392 hasBlockDim = true;
3393 if (d.isThreadX() || d.isThreadY())
3394 hasThreadDim = true;
3395 }
3396 if (hasThreadDim && !hasBlockDim)
3397 hasAccumulateSibling = true;
3398 }
3399 }
3400 }
3401 }
3402 if (insideAccumulateGridStride || hasAccumulateSibling) {
3403 for (auto launchArg : computeRegion.getLaunchArgs()) {
3404 if (acc::ParWidthOp pw = launchArg.getDefiningOp<acc::ParWidthOp>()) {
3405 if (pw.getParDim().isThreadX()) {
3406 if (auto cval = getConstantIntValue(pw.getLaunchArg()))
3407 needsAtomicReduction = (*cval >= options.subgroupSize);
3408 else
3409 needsAtomicReduction = true;
3410 break;
3411 }
3412 }
3413 }
3414 }
3415 if (needsAtomicReduction && !reductionSharedBuf) {
3416 Type elemTy;
3417 parallelOp.getBody()->walk([&](acc::ReductionAccumulateOp accOp) {
3418 Type t = accOp.getValue().getType();
3419 if (isa<FloatType, IntegerType>(t))
3420 elemTy = t;
3421 return elemTy ? WalkResult::interrupt() : WalkResult::advance();
3422 });
3423 if (!elemTy)
3424 needsAtomicReduction = false;
3425 }
3426 if (needsAtomicReduction && !reductionSharedBuf) {
3427 Location seqLoc = parallelOp->getLoc();
3428 gpu::AddressSpaceAttr workgroupAS = gpu::AddressSpaceAttr::get(
3429 computeRegion->getContext(),
3430 gpu::GPUDialect::getWorkgroupAddressSpace());
3431 Type elemTy;
3432 parallelOp.getBody()->walk([&](acc::ReductionAccumulateOp accOp) {
3433 Type t = accOp.getValue().getType();
3434 if (isa<FloatType, IntegerType>(t))
3435 elemTy = t;
3436 return elemTy ? WalkResult::interrupt() : WalkResult::advance();
3437 });
3438 assert(elemTy && "expected scalar reduction element type");
3439 unsigned elemBytes = elemTy.getIntOrFloatBitWidth() / 8;
3440 MemRefType bufTy = MemRefType::get({options.subgroupSize}, elemTy,
3441 AffineMap{}, workgroupAS);
3442 reductionSharedBuf = acc::GPUSharedMemoryOp::create(
3443 rewriter, seqLoc, bufTy, rewriter.getI64IntegerAttr(1),
3444 rewriter.getI64IntegerAttr(options.subgroupSize * elemBytes),
3445 ValueRange{}, IntegerAttr{}, IntegerAttr{});
3446 Value tidY = getThreadId(seqLoc, gpu::Dimension::y);
3447 Value identity;
3448 if (isa<FloatType>(elemTy)) {
3449 identity = arith::ConstantOp::create(
3450 rewriter, seqLoc, elemTy, rewriter.getFloatAttr(elemTy, 0.0));
3451 } else {
3452 identity = arith::ConstantIntOp::create(rewriter, seqLoc, elemTy, 0);
3453 }
3454 memref::StoreOp::create(rewriter, seqLoc, identity, reductionSharedBuf,
3455 tidY);
3456 createPerRowBarrier(seqLoc);
3457 }
3458 processSeqLoop(parallelOp);
3459 loopReductions.push_back(parallelOp);
3460 } else {
3461 LLVM_DEBUG(llvm::dbgs()
3462 << "processing loop: parDim: " << parDim << " as gpu par\n");
3463 // actual parallel loops get their iv mapped to gpu hierarchy
3464 // and the loop construct is not cloned to gpu kernel only the
3465 // ops are cloned with mapping of gpu id for original loop iv
3466 Value gpuThreadId = getGPUThreadIdFor(parDim.getProcessor());
3467 mapping.map(parallelOp.getInductionVars()[0], gpuThreadId);
3468
3469 processLoopBody();
3470
3471 // Since the loop is not copied over, create dummy mappings for_each
3472 // of the loop results. These will ultimately by replaced with a
3473 // reduction
3474 llvm::for_each(parallelOp.getResults(), [&](Value v) {
3475 Type valTy = v.getType();
3476 TypedAttr zeroAttr = rewriter.getZeroAttr(valTy);
3477 auto zero = arith::ConstantOp::create(rewriter, parallelOp->getLoc(),
3478 valTy, zeroAttr);
3479 mapping.map(v, zero);
3480 });
3481 loopReductions.push_back(parallelOp);
3482 }
3483 insideAccumulateGridStride = savedGridStrideFlag;
3484 if (!insideAccumulateGridStride && !savedReductionBuf)
3485 reductionSharedBuf = Value();
3486}
3487
3488/// Map an atomic RMW kind to the corresponding `gpu.all_reduce` operation.
3489static gpu::AllReduceOperation
3490getAllReduceOperation(arith::AtomicRMWKind kind) {
3491 switch (kind) {
3492 case arith::AtomicRMWKind::addf:
3493 case arith::AtomicRMWKind::addi:
3494 return gpu::AllReduceOperation::ADD;
3495 case arith::AtomicRMWKind::mulf:
3496 case arith::AtomicRMWKind::muli:
3497 return gpu::AllReduceOperation::MUL;
3498 case arith::AtomicRMWKind::minu:
3499 return gpu::AllReduceOperation::MINUI;
3500 case arith::AtomicRMWKind::mins:
3501 return gpu::AllReduceOperation::MINSI;
3502 case arith::AtomicRMWKind::minnumf:
3503 return gpu::AllReduceOperation::MINNUMF;
3504 case arith::AtomicRMWKind::maxu:
3505 return gpu::AllReduceOperation::MAXUI;
3506 case arith::AtomicRMWKind::maxs:
3507 return gpu::AllReduceOperation::MAXSI;
3508 case arith::AtomicRMWKind::maxnumf:
3509 return gpu::AllReduceOperation::MAXNUMF;
3510 case arith::AtomicRMWKind::ori:
3511 return gpu::AllReduceOperation::OR;
3512 case arith::AtomicRMWKind::andi:
3513 return gpu::AllReduceOperation::AND;
3514 case arith::AtomicRMWKind::xori:
3515 return gpu::AllReduceOperation::XOR;
3516 case arith::AtomicRMWKind::minimumf:
3517 return gpu::AllReduceOperation::MINIMUMF;
3518 case arith::AtomicRMWKind::maximumf:
3519 return gpu::AllReduceOperation::MAXIMUMF;
3520 case arith::AtomicRMWKind::assign:
3521 break;
3522 }
3523 llvm_unreachable("unsupported atomic kind");
3524}
3525
3526void ACCCGToGPULowering::constructAtomicAccumulation(
3527 Location loc, Value memref, ValueRange indices, Value input,
3528 arith::AtomicRMWKind kind) {
3529 assert(!memref.getDefiningOp<memref::AllocaOp>() &&
3530 "cannot lower atomic accumulation on an stack variable");
3531
3532 // acc.atomic.update derives the element address from the memref descriptor's
3533 // base pointer and offset field; it has no subscript operand. When the store
3534 // being lowered targets a specific array element (e.g. result(idx) =
3535 // max(...)), fold the indices into the descriptor offset with a subview so
3536 // the atomic updates the intended element. Otherwise the atomic always hits
3537 // element 0, so any reduction whose destination index is non-zero is
3538 // miscompiled (the result lands in element 0 while the intended element keeps
3539 // its identity-init value).
3540 Value target = memref;
3541 if (!indices.empty()) {
3542 MemRefType memrefTy = cast<MemRefType>(memref.getType());
3543 unsigned rank = memrefTy.getRank();
3544 assert(indices.size() == rank && "expected one index per memref dimension");
3545 SmallVector<OpFoldResult> offsets(indices.begin(), indices.end());
3546 SmallVector<OpFoldResult> sizes(rank, rewriter.getIndexAttr(1));
3547 SmallVector<OpFoldResult> strides(rank, rewriter.getIndexAttr(1));
3548 target = memref::SubViewOp::create(rewriter, loc, memref, offsets, sizes,
3549 strides);
3550 }
3551
3552 auto atomicUpdateOp =
3553 acc::AtomicUpdateOp::create(rewriter, loc, target, /*ifCond=*/Value());
3554 Region &region = atomicUpdateOp->getRegion(0);
3555 Block *block =
3556 rewriter.createBlock(&region, region.begin(), {input.getType()}, {loc});
3557 rewriter.setInsertionPointToStart(block);
3558 Value reductionExpr =
3559 generateReductionOp(rewriter, loc, input, block->getArgument(0), kind);
3560 acc::YieldOp::create(rewriter, loc, reductionExpr);
3561 rewriter.setInsertionPointAfter(atomicUpdateOp);
3562}
3563
3564void ACCCGToGPULowering::createGPUAllReduceOp(
3565 Location loc, Value input, Value memref, arith::AtomicRMWKind kind,
3566 mlir::acc::GPUParallelDimsAttr parDimsAttr, ValueRange indices,
3567 bool isPerThreadPrivateTarget) {
3568 gpu::AllReduceOperationAttr attr = gpu::AllReduceOperationAttr::get(
3569 computeRegion->getContext(), getAllReduceOperation(kind));
3570 auto allReduceOp = gpu::AllReduceOp::create(rewriter, loc, input, attr, true);
3571 mlir::acc::setParDimsAttr(allReduceOp, parDimsAttr);
3572 // Predicate the store on the thread-level dimensions being reduced so that
3573 // only one thread per reduced group writes the result. Only dimensions in
3574 // parDimsAttr are included; sweeping over all dimensions between the highest
3575 // par_dim and thread_x would incorrectly add unrelated dimensions (e.g.
3576 // thread_y for a thread_x-only reduction), preventing other rows from
3577 // storing their independent results.
3578 SmallVector<mlir::acc::GPUParallelDimAttr> inactiveParDims;
3579 MLIRContext *ctx = computeRegion->getContext();
3580 bool hasThreadX = false;
3581 for (auto parDim : parDimsAttr.getArray()) {
3582 if (parDim.isAnyBlock())
3583 continue;
3584 if (parDim.isThreadX())
3585 hasThreadX = true;
3586 if (computeRegion.getLaunchArg(parDim) ||
3587 isInsideACCSpecializedRoutine(computeRegion)) {
3588 inactiveParDims.push_back(parDim);
3589 }
3590 }
3591 // Subgroup alignment may introduce extra ThreadX lanes even when ThreadX is
3592 // not part of the reduction. Predicate on ThreadX so only one lane stores.
3593 // A single-thread worker launch is left unaligned, so it has no such lanes.
3594 if (!hasThreadX && !isSingleThreadWorkerLaunch())
3595 inactiveParDims.push_back(mlir::acc::GPUParallelDimAttr::threadXDim(ctx));
3596 Value predicate = emitPredicate(loc, inactiveParDims);
3597 // Predication is only needed when the store target is visible to
3598 // multiple threads (shared/global memory). Per-thread targets like
3599 // memref.alloca are thread-private: gpu.all_reduce returns the same
3600 // value on all threads, so each can safely store to its own copy.
3601 // Detect per-thread storage by walking through conversion ops to
3602 // find the underlying allocation.
3603 bool isPerThreadPrivate = isPerThreadPrivateTarget ||
3604 isa_and_nonnull<memref::AllocaOp>(
3605 unwrapMemRefConversion(memref).getDefiningOp());
3606 // combine
3607 scf::IfOp ifOp;
3608 if (predicate && !isPerThreadPrivate) {
3609 ifOp =
3610 scf::IfOp::create(rewriter, loc, predicate, /*withElseRegion=*/false);
3611 Region &thenRegion = ifOp.getThenRegion();
3612 Block &thenBlock = thenRegion.back();
3613 rewriter.setInsertionPoint(thenBlock.getTerminator());
3614 }
3615 memref::StoreOp::create(rewriter, loc, allReduceOp, memref, indices);
3616 if (predicate && !isPerThreadPrivate)
3617 rewriter.setInsertionPointAfter(ifOp);
3618 // A later block combine reuses this instead of reloading the slot. This only
3619 // applies to scalar accumulators; array elements are indexed individually.
3620 if (indices.empty())
3621 reductionAccumValue[memref] = allReduceOp;
3622}
3623
3624void ACCCGToGPULowering::postprocessAccumulateOp(
3625 acc::ReductionAccumulateOp op) {
3626 Location loc = op->getLoc();
3627
3628 rewriter.setInsertionPoint(op);
3629
3630 // Check whether this accumulate has only block-level par dims (no thread
3631 // dims). gpu.all_reduce reduces across threads within a block, which is
3632 // wrong for block-only reductions - the loop result is already per-thread
3633 // and only needs a predicated store to the memref.
3634 bool hasThreadDim = false;
3635 SmallVector<mlir::acc::GPUParallelDimAttr> threadParDims;
3636 for (auto parDim : op.getParDims().getArray()) {
3637 if (!parDim.isAnyBlock()) {
3638 hasThreadDim = true;
3639 threadParDims.push_back(parDim);
3640 }
3641 }
3642
3643 std::optional<arith::AtomicRMWKind> kind;
3644 if (hasThreadDim) {
3645 FailureOr<arith::AtomicRMWKind> kindOr = getReductionKind(
3646 op.getReductionOperator(), op.getValue().getType(), loc);
3647 if (failed(kindOr))
3648 return;
3649 kind = *kindOr;
3650 }
3651
3652 if (hasThreadDim && reductionSharedBuf &&
3653 op.getValue().getType() ==
3654 cast<MemRefType>(reductionSharedBuf.getType()).getElementType()) {
3655 Value val = op.getValue();
3656 Value mem = op.getMemref();
3657 Value tidY = getThreadId(loc, gpu::Dimension::y);
3658 memref::AtomicRMWOp::create(rewriter, loc, *kind, val, reductionSharedBuf,
3659 ValueRange{tidY});
3660 createPerRowBarrier(loc);
3661 Value result =
3662 memref::LoadOp::create(rewriter, loc, reductionSharedBuf, tidY);
3663 memref::StoreOp::create(rewriter, loc, result, mem);
3664 reductionAccumValue[mem] = result;
3665 } else if (hasThreadDim) {
3666 createGPUAllReduceOp(loc, op.getValue(), op.getMemref(), *kind,
3667 op.getParDims());
3668 } else {
3669 // Block-only: no gpu.all_reduce needed (all threads have the same
3670 // value after broadcast). Just store the value to the memref.
3671 // Predicate on all thread dims when the target is shared memory.
3672 Value val = mapping.lookupOrDefault(op.getValue());
3673 Value mem = mapping.lookupOrDefault(op.getMemref());
3674 bool isPerThreadPrivate = isa_and_nonnull<memref::AllocaOp>(
3675 unwrapMemRefConversion(mem).getDefiningOp());
3676 if (!isPerThreadPrivate) {
3677 SmallVector<mlir::acc::GPUParallelDimAttr> predDims;
3678 for (auto parDim : computeRegion.getLaunchParDims())
3679 if (!parDim.isAnyBlock())
3680 predDims.push_back(parDim);
3681 if (predDims.empty()) {
3682 predDims.push_back(mlir::acc::GPUParallelDimAttr::threadXDim(
3683 computeRegion->getContext()));
3684 }
3685 Value predicate = emitPredicate(loc, predDims);
3686 auto ifOp =
3687 scf::IfOp::create(rewriter, loc, predicate, /*withElseRegion=*/false);
3688 rewriter.setInsertionPoint(ifOp.getThenRegion().back().getTerminator());
3689 memref::StoreOp::create(rewriter, loc, val, mem);
3690 rewriter.setInsertionPointAfter(ifOp);
3691 } else {
3692 memref::StoreOp::create(rewriter, loc, val, mem);
3693 }
3694 }
3695
3696 // erase acc.reduction_accumulate
3697 rewriter.eraseOp(op);
3698}
3699
3700void ACCCGToGPULowering::postprocessLoopReduction(scf::ParallelOp parLoop) {
3701 if (parLoop.getNumReductions() == 0)
3702 return;
3703
3704 for (unsigned i = 0; i < parLoop.getNumResults(); ++i) {
3705 for (Operation *user :
3706 mapping.lookupOrDefault(parLoop.getResult(i)).getUsers()) {
3707 if (acc::ReductionAccumulateOp accumulateOp =
3708 dyn_cast<acc::ReductionAccumulateOp>(user)) {
3709 postprocessAccumulateOp(accumulateOp);
3710 }
3711 }
3712 }
3713}
3714
3715void ACCCGToGPULowering::processExecuteRegion(scf::ExecuteRegionOp op) {
3716 LLVM_DEBUG(llvm::dbgs() << "processing execute region op: ";
3717 op->print(llvm::dbgs()); llvm::dbgs() << "\n");
3718 Location loc = op->getLoc();
3719 auto types = op.getResultTypes();
3720 Region &oldRegion = op.getRegion();
3721 // create the executeRegion op inside gpu launch
3722 auto executeRegionOp = scf::ExecuteRegionOp::create(rewriter, loc, types);
3723 Region &region = executeRegionOp.getRegion();
3724 rewriter.createBlock(&region);
3725 rewriter.setInsertionPointToEnd(&region.front());
3726
3727 llvm::DenseMap<Block *, Block *> blockMap;
3728 blockMap[&oldRegion.front()] = &region.front();
3729
3730 // Create blocks in the new operation corresponding to all blocks in the
3731 // original op
3732 for (auto &oldBlock : llvm::drop_begin(oldRegion.getBlocks())) {
3733 TypeRange argTypes = oldBlock.getArgumentTypes();
3734 size_t numArgs = argTypes.size();
3735 // Create new block with same argument types
3736 Block *newBlock = rewriter.createBlock(&region, region.end(), argTypes,
3737 SmallVector<Location>(numArgs, loc));
3738 blockMap[&oldBlock] = newBlock;
3739 // Map block arguments
3740 mapping.map(oldBlock.getArguments(), newBlock->getArguments());
3741 }
3742
3743 // Iterate over all blocks of oldRegion and all operations inside them
3744 // process all the ops except the terminator
3745 for (auto [oldBlock, newBlock] :
3746 llvm::zip(oldRegion.getBlocks(), region.getBlocks())) {
3747 OpBuilder::InsertionGuard blockGuard(rewriter);
3748 rewriter.setInsertionPointToStart(&newBlock);
3749 for (auto &bodyOp : oldBlock.getOperations()) {
3750 // Skip terminators during normal iteration - handle them separately
3751 if (bodyOp.hasTrait<OpTrait::IsTerminator>())
3752 continue;
3753 processOp(&bodyOp);
3754 }
3755
3756 // Copy the terminator from old block to new block
3757 Operation *oldTerminator = oldBlock.getTerminator();
3758 rewriter.setInsertionPointToEnd(&newBlock);
3759 Operation *newTerminator = rewriter.clone(*oldTerminator, mapping);
3760
3761 // Replace successors with mapped blocks
3762 for (unsigned i = 0; i < oldTerminator->getNumSuccessors(); ++i) {
3763 Block *oldDest = oldTerminator->getSuccessor(i);
3764 Block *newDest = blockMap.lookup(oldDest);
3765 assert(newDest && "Successor block must be in blockMap");
3766 newTerminator->setSuccessor(newDest, i);
3767 }
3768 }
3769 mapping.map(op->getResults(), executeRegionOp->getResults());
3770 rewriter.setInsertionPointAfter(executeRegionOp);
3771}
3772
3773void ACCCGToGPULowering::processAccumulateOp(acc::ReductionAccumulateOp op) {
3774 LLVM_DEBUG(llvm::dbgs() << "processing accumulate op: " << *op << "\n");
3775 Value accumulateValue = op.getValue();
3776 if (reductionSharedBuf &&
3777 mapping.lookupOrDefault(accumulateValue).getType() ==
3778 cast<MemRefType>(reductionSharedBuf.getType()).getElementType()) {
3779 Location loc = op->getLoc();
3780 FailureOr<arith::AtomicRMWKind> kind = getReductionKind(
3781 op.getReductionOperator(), accumulateValue.getType(), loc);
3782 if (failed(kind))
3783 return;
3784 Value mappedValue = mapping.lookupOrDefault(accumulateValue);
3785 Value memref = mapping.lookupOrDefault(op.getMemref());
3786 Value tidY = getThreadId(loc, gpu::Dimension::y);
3787 memref::AtomicRMWOp::create(rewriter, loc, *kind, mappedValue,
3788 reductionSharedBuf, ValueRange{tidY});
3789 createPerRowBarrier(loc);
3790 Value result =
3791 memref::LoadOp::create(rewriter, loc, reductionSharedBuf, tidY);
3792 memref::StoreOp::create(rewriter, loc, result, memref);
3793 reductionAccumValue[memref] = result;
3794 return;
3795 }
3796 if (accumulateValue.getDefiningOp<scf::ParallelOp>()) {
3797 Operation *newOp = rewriter.clone(*op, mapping);
3798 mapping.map(op->getResults(), newOp->getResults());
3799 } else if (isRedundantChainAccumulate(op)) {
3800 // The destination memref already holds the correctly aggregated value
3801 // (atomically reduced by a preceding acc.reduction_combine with a block
3802 // par_dim). Skip the redundant gpu.all_reduce + atomic.update; emit a
3803 // Workgroup-wide barrier so downstream readers see all prior atomic
3804 // updates.
3805 LLVM_DEBUG(llvm::dbgs() << " skipped: redundant chain accumulate\n");
3806 gpu::BarrierOp::create(rewriter, op->getLoc());
3807 } else {
3808 Value mappedValue = mapping.lookupOrDefault(accumulateValue);
3809 Value memref = mapping.lookupOrDefault(op.getMemref());
3810 FailureOr<arith::AtomicRMWKind> kind = getReductionKind(
3811 op.getReductionOperator(), accumulateValue.getType(), op.getLoc());
3812 if (failed(kind))
3813 return;
3814 createGPUAllReduceOp(op->getLoc(), mappedValue, memref, *kind,
3815 op.getParDims());
3816 }
3817}
3818
3819/// True when \p v takes a distinct value per thread: it derives from a thread
3820/// id, either directly or through the bounds of an enclosing loop. By this
3821/// point a thread-mapped loop carries its mapping in its bounds rather than in
3822/// `acc.par_dims`, so the bounds are what must be inspected.
3823static bool isThreadVarying(Value v, ArrayRef<Value> threadIds,
3824 DenseSet<Value> &visited) {
3825 if (!v || !visited.insert(v).second)
3826 return false;
3827 if (llvm::is_contained(threadIds, v))
3828 return true;
3829 if (auto arg = dyn_cast<BlockArgument>(v)) {
3830 Operation *owner = arg.getOwner()->getParentOp();
3831 unsigned dim = arg.getArgNumber();
3832 if (auto loop = dyn_cast<scf::ParallelOp>(owner)) {
3833 if (dim >= loop.getLowerBound().size())
3834 return false;
3835 return isThreadVarying(loop.getLowerBound()[dim], threadIds, visited) ||
3836 isThreadVarying(loop.getStep()[dim], threadIds, visited);
3837 }
3838 if (auto loop = dyn_cast<scf::ForOp>(owner))
3839 return dim == 0 &&
3840 (isThreadVarying(loop.getLowerBound(), threadIds, visited) ||
3841 isThreadVarying(loop.getStep(), threadIds, visited));
3842 return false;
3843 }
3844 Operation *def = v.getDefiningOp();
3845 if (!def)
3846 return false;
3847 if (isa<gpu::ThreadIdOp, gpu::LaneIdOp>(def))
3848 return true;
3849 return llvm::any_of(def->getOperands(), [&](Value o) {
3850 return isThreadVarying(o, threadIds, visited);
3851 });
3852}
3853
3854/// Strips view and memory-space-cast chains to the underlying buffer.
3855static Value accumulatorRoot(Value v) {
3856 while (Operation *op = v.getDefiningOp()) {
3857 if (auto cast = dyn_cast<memref::MemorySpaceCastOp>(op)) {
3858 v = cast.getSource();
3859 continue;
3860 }
3861 if (auto viewLike = dyn_cast<ViewLikeOpInterface>(op)) {
3862 if (isa<MemRefType>(viewLike.getViewSource().getType())) {
3863 v = viewLike.getViewSource();
3864 continue;
3865 }
3866 }
3867 break;
3868 }
3869 return v;
3870}
3871
3872/// Matches `%l = load %m[%i]` / `%c = combine(%l, %x)` / `store %c, %m[%i]` on
3873/// the accumulator \p accum and returns the contributed value `%x`.
3874static Value matchAccumulatorUpdate(memref::StoreOp store, Value accum) {
3875 if (accumulatorRoot(store.getMemRef()) != accum)
3876 return {};
3877 Operation *combine = store.getValueToStore().getDefiningOp();
3878 if (!combine || combine->getNumOperands() != 2)
3879 return {};
3880 for (unsigned i = 0; i != 2; ++i) {
3881 auto load = combine->getOperand(i).getDefiningOp<memref::LoadOp>();
3882 if (!load || accumulatorRoot(load.getMemRef()) != accum)
3883 continue;
3884 if (!llvm::equal(load.getIndices(), store.getIndices()))
3885 continue;
3886 return combine->getOperand(1 - i);
3887 }
3888 return {};
3889}
3890
3891/// A block-shared accumulator is updated in place by the loop body, so several
3892/// threads may hit the same element. Make those updates atomic unless the
3893/// element index provably varies across the participating threads.
3894static void atomicizeSharedAccumulatorUpdates(Value accum,
3895 arith::AtomicRMWKind kind,
3896 ArrayRef<Value> threadIds,
3897 RewriterBase &rewriter) {
3898 OpBuilder::InsertionGuard guard(rewriter);
3899 SmallVector<memref::StoreOp> stores;
3900 SmallVector<Value> worklist{accum};
3901 DenseSet<Value> seen;
3902 while (!worklist.empty()) {
3903 Value cur = worklist.pop_back_val();
3904 if (!seen.insert(cur).second)
3905 continue;
3906 for (Operation *user : cur.getUsers()) {
3907 if (auto store = dyn_cast<memref::StoreOp>(user))
3908 stores.push_back(store);
3909 else if (isa<ViewLikeOpInterface, memref::MemorySpaceCastOp>(user))
3910 llvm::append_range(worklist, user->getResults());
3911 }
3912 }
3913
3914 for (memref::StoreOp store : stores) {
3915 Value contribution = matchAccumulatorUpdate(store, accum);
3916 if (!contribution)
3917 continue;
3918 // A thread-varying index means each thread owns its element, so the
3919 // existing plain update is already race-free.
3920 if (llvm::any_of(store.getIndices(), [&](Value idx) {
3921 DenseSet<Value> visited;
3922 return isThreadVarying(idx, threadIds, visited);
3923 }))
3924 continue;
3925 Operation *combine = store.getValueToStore().getDefiningOp();
3926 rewriter.setInsertionPoint(store);
3927 memref::AtomicRMWOp::create(rewriter, store.getLoc(), kind, contribution,
3928 store.getMemRef(), store.getIndices());
3929 rewriter.eraseOp(store);
3930 if (combine && combine->use_empty())
3931 rewriter.eraseOp(combine);
3932 }
3933}
3934
3935void ACCCGToGPULowering::processAccumulateArrayOp(
3936 acc::ReductionAccumulateArrayOp op) {
3937 LLVM_DEBUG(llvm::dbgs() << "processing accumulate array op: " << *op << "\n");
3938 Location loc = op.getLoc();
3939
3940 Value memref = mapping.lookupOrDefault(op.getMemref());
3941 MemRefType memrefTy = dyn_cast<MemRefType>(memref.getType());
3942 if (!memrefTy) {
3943 (void)accSupport.emitNYI(loc, "reduction: non-MemRefTy accumulate array");
3944 return;
3945 }
3946 FailureOr<arith::AtomicRMWKind> kindOr = getReductionKind(
3947 op.getReductionOperator(), memrefTy.getElementType(), loc);
3948 if (failed(kindOr))
3949 return;
3950 arith::AtomicRMWKind kind = *kindOr;
3951
3952 // The (already mapped/cloned) acc.bounds op describes the element range; it
3953 // is dead after lowering since we read its operands directly.
3954 acc::DataBoundsOp boundsOp = mapping.lookupOrDefault(op.getBounds())
3955 .getDefiningOp<acc::DataBoundsOp>();
3956 assert(boundsOp && "expected acc.bounds defining op for array accumulate");
3957 auto eraseDeadBounds = [&] {
3958 if (boundsOp->use_empty())
3959 rewriter.eraseOp(boundsOp);
3960 };
3961
3962 bool hasThreadDim = false;
3963 bool hasBlockDim = false;
3964 for (auto pd : op.getParDims().getArray()) {
3965 hasThreadDim |= pd.isAnyThread();
3966 hasBlockDim |= pd.isAnyBlock();
3967 }
3968
3969 // Block-only (gang) reduction: each element is produced by one gang, so the
3970 // per-gang copy already holds the result and the combine does the rest.
3971 if (hasBlockDim && !hasThreadDim) {
3972 eraseDeadBounds();
3973 return;
3974 }
3975
3976 // A thread-only accumulate merges with a within-block all_reduce, which is
3977 // complete only in one block: block context, or a launch with no block dim.
3978 // Multi-block thread-only still grid-strides across blocks, so stays NYI.
3979 bool regionLaunchesBlocks = llvm::any_of(
3980 computeRegion.getLaunchParDims(),
3981 [](mlir::acc::GPUParallelDimAttr d) { return d.isAnyBlock(); });
3982 if (!reductionHasBlockContext(op) && regionLaunchesBlocks) {
3983 (void)accSupport.emitNYI(
3984 loc, "reduction: thread-only array reduction accumulate");
3985 return;
3986 }
3987
3988 // Per-element gpu.all_reduce is only correct when each thread owns its own
3989 // accumulator copy. For a statically-shaped accumulator, classify from the
3990 // operand: an explicit shared/heap allocation is block-shared regardless of
3991 // size, and a stack alloca (or a view over one) is per-thread when it fits
3992 // the per-thread stack budget. Storage `acc.par_dims` without thread_x means
3993 // gang-/worker-scoped privacy (shared among vector lanes) even if the type
3994 // would fit on the stack. For a dynamically-shaped accumulator the type
3995 // conveys no size, so classify from storage/accumulate thread_x dims.
3996 auto storageIsThreadXPrivate = [&](Value v) -> bool {
3997 acc::PrivateLocalOp privateLocal = getPrivateLocalForMemref(v);
3998 GPUParallelDimsAttr dims =
3999 privateLocal ? getPrivateParDims(privateLocal, computeRegion)
4000 : GPUParallelDimsAttr();
4001 if (!dims) {
4002 if (Operation *root = unwrapMemRefConversion(v).getDefiningOp())
4003 dims = getParDimsAttr(root);
4004 }
4005 return !dims ||
4006 llvm::any_of(dims.getArray(), [](auto d) { return d.isThreadX(); });
4007 };
4008 Operation *rootOp = unwrapMemRefConversion(memref).getDefiningOp();
4009 bool isSharedStorage = isa_and_nonnull<memref::AllocOp>(rootOp) ||
4010 isa_and_nonnull<acc::GPUSharedMemoryOp>(rootOp);
4011 if (auto addrSpace = dyn_cast_if_present<gpu::AddressSpaceAttr>(
4012 memrefTy.getMemorySpace())) {
4013 isSharedStorage |=
4014 addrSpace.getValue() == gpu::GPUDialect::getWorkgroupAddressSpace();
4015 }
4016 bool isPerThreadPrivate =
4017 !isSharedStorage && storageIsThreadXPrivate(op.getMemref()) &&
4018 (memrefTy.hasStaticShape()
4019 ? canUseStackAlloca(memrefTy, loc, options.maxThreadPrivateStack)
4020 : llvm::any_of(op.getParDims().getArray(),
4021 [](mlir::acc::GPUParallelDimAttr d) {
4022 return d.isThreadX();
4023 }));
4024 if (!isPerThreadPrivate) {
4025 // Block-shared accumulator: the body already updated it in place, so the
4026 // block partial is complete and the atomic combine finishes across blocks.
4027 // Threads that share an element must not race, so their in-place updates
4028 // become atomic.
4029 SmallVector<Value> threadIds;
4030 if (Value xId = getGPUThreadIdFor(gpu::Processor::ThreadX))
4031 threadIds.push_back(xId);
4032 if (Value yId = getGPUThreadIdFor(gpu::Processor::ThreadY))
4033 threadIds.push_back(yId);
4034 if (Value zId = getGPUThreadIdFor(gpu::Processor::ThreadZ))
4035 threadIds.push_back(zId);
4036 atomicizeSharedAccumulatorUpdates(accumulatorRoot(memref), kind, threadIds,
4037 rewriter);
4038 eraseDeadBounds();
4039 return;
4040 }
4041
4042 // Bounds are normalized to be zero-based.
4043 auto toIndex = [&](Value v) -> Value {
4044 if (v.getType().isIndex())
4045 return v;
4046 return arith::IndexCastOp::create(rewriter, loc, rewriter.getIndexType(),
4047 v);
4048 };
4049
4050 Value zero = arith::ConstantIndexOp::create(rewriter, loc, 0);
4051 Value one = arith::ConstantIndexOp::create(rewriter, loc, 1);
4052 Value lb =
4053 boundsOp.getLowerbound() ? toIndex(boundsOp.getLowerbound()) : zero;
4054 Value step = boundsOp.getStride() ? toIndex(boundsOp.getStride()) : one;
4055 // Exclusive upper bound. `extent` counts elements, so the span is
4056 // `extent * step` (for the common unit-stride case step is 1); fall back to
4057 // the inclusive upperbound when no extent is given.
4058 Value ub;
4059 if (boundsOp.getExtent()) {
4060 Value span = arith::MulIOp::create(rewriter, loc,
4061 toIndex(boundsOp.getExtent()), step);
4062 ub = arith::AddIOp::create(rewriter, loc, lb, span);
4063 } else {
4064 assert(boundsOp.getUpperbound() &&
4065 "acc.bounds must specify an extent or upperbound");
4066 ub = arith::AddIOp::create(rewriter, loc, toIndex(boundsOp.getUpperbound()),
4067 one);
4068 }
4069
4070 // Reduce each array element across the requested parallel dimensions.
4071 auto forOp = scf::ForOp::create(rewriter, loc, lb, ub, step);
4072 {
4073 OpBuilder::InsertionGuard guard(rewriter);
4074 rewriter.setInsertionPoint(forOp.getBody()->getTerminator());
4075 Value iv = forOp.getInductionVar();
4076 SmallVector<Value> indices{iv};
4077 if (memrefTy.getRank() > 1) {
4078 indices.resize(memrefTy.getRank());
4079 Value linearIndex = iv;
4080 for (int64_t dim = memrefTy.getRank() - 1; dim >= 0; --dim) {
4081 Value dimSize =
4082 memrefTy.isDynamicDim(dim)
4083 ? memref::DimOp::create(rewriter, loc, memref, dim).getResult()
4084 : arith::ConstantIndexOp::create(rewriter, loc,
4085 memrefTy.getDimSize(dim))
4086 .getResult();
4087 indices[dim] =
4088 arith::RemUIOp::create(rewriter, loc, linearIndex, dimSize);
4089 if (dim != 0)
4090 linearIndex =
4091 arith::DivUIOp::create(rewriter, loc, linearIndex, dimSize);
4092 }
4093 }
4094 Value elem = memref::LoadOp::create(rewriter, loc, memref, indices);
4095 createGPUAllReduceOp(loc, elem, memref, kind, op.getParDims(), indices,
4096 /*isPerThreadPrivateTarget=*/true);
4097 }
4098
4099 eraseDeadBounds();
4100}
4101
4102void ACCCGToGPULowering::processReductionOp(acc::ReductionInitOp op) {
4103 // Clone the inner ops of the reduction op only
4104 op.getRegion().walk<WalkOrder::PreOrder>([&](Operation *innerOp) {
4105 if (acc::YieldOp yieldOp = dyn_cast<acc::YieldOp>(innerOp)) {
4106 op.getResult().replaceAllUsesWith(mapping.lookup(yieldOp.getOperand(0)));
4107 return WalkResult::interrupt();
4108 }
4109 if (innerOp->getNumRegions() > 0) {
4110 processOp(innerOp);
4111 return WalkResult::skip();
4112 }
4113 rewriter.clone(*innerOp, mapping);
4114 return WalkResult::advance();
4115 });
4116}
4117
4118void ACCCGToGPULowering::processReductionCombineOp(acc::ReductionCombineOp op) {
4119 LLVM_DEBUG(llvm::dbgs() << "processing reduction combine op: ";
4120 op->print(llvm::dbgs()); llvm::dbgs() << "\n");
4121 Location loc = op.getLoc();
4122 MemRefType memrefType = dyn_cast<MemRefType>(op.getSrcMemref().getType());
4123 assert(memrefType && "expected memref type for reduction combine op");
4124 assert(memrefType.getRank() == 0 &&
4125 "expected scalar memref type for reduction combine op");
4126 Type elTy = memrefType.getElementType();
4127 FailureOr<arith::AtomicRMWKind> kindOr =
4128 getReductionKind(op.getReductionOperator(), elTy, loc);
4129 if (failed(kindOr))
4130 return;
4131 arith::AtomicRMWKind kind = *kindOr;
4132
4133 Value srcMemref = mapping.lookupOrDefault(op.getSrcMemref());
4134 Value destMemref = mapping.lookupOrDefault(op.getDestMemref());
4135
4136 // A block par_dim normally means the accumulator is shared across blocks and
4137 // must be updated atomically. But when the destination resolves to a
4138 // thread-private stack alloca (e.g. an inner-loop reduction combining one
4139 // private accumulator into another private accumulator for the same thread),
4140 // the update is not visible to other threads and must not be atomic. Treat
4141 // such destinations as a plain load/combine/store below.
4142 bool destIsPerThreadPrivate = isa_and_nonnull<memref::AllocaOp>(
4143 unwrapMemRefConversion(destMemref).getDefiningOp());
4144
4145 SmallVector<mlir::acc::GPUParallelDimAttr> parDims =
4147 for (auto parDim : parDims) {
4148 if (parDim.isAnyBlock() && !destIsPerThreadPrivate) {
4149 // Block reduction directly stores to the accumulator using atomic.
4150 // The predication (tid.x == 0 when subgroup-aligned) is already handled
4151 // by the parent predicate_region processing.
4152 // Reloading a grid-shared slot races with other blocks; record
4153 // it and replace with the block-reduced register value in the fixup.
4154 auto srcLoad =
4155 memref::LoadOp::create(rewriter, loc, srcMemref, ValueRange{});
4156 pendingCombineReloads.push_back({srcMemref, srcLoad});
4157 constructAtomicAccumulation(loc, destMemref, /*indices=*/{}, srcLoad,
4158 kind);
4159 return;
4160 }
4161 }
4162
4163 // Atomic construction is not needed; lower this operation to typical
4164 // reduction update operations. E.g. dest = dest <kind> src
4165 auto srcLoad = memref::LoadOp::create(rewriter, loc, srcMemref, ValueRange{});
4166 auto destLoad =
4167 memref::LoadOp::create(rewriter, loc, destMemref, ValueRange{});
4168 Value combine = generateReductionOp(rewriter, loc, srcLoad, destLoad, kind);
4169 memref::StoreOp::create(rewriter, loc, combine, destMemref, ValueRange{});
4170}
4171
4172void ACCCGToGPULowering::processCombineRegionOp(
4173 acc::ReductionCombineRegionOp op) {
4174 LLVM_DEBUG(llvm::dbgs() << "processing combine region op: ";
4175 op->print(llvm::dbgs()); llvm::dbgs() << "\n");
4176 // A block par_dim on a combine into a thread-private stack alloca is not a
4177 // real cross-block accumulation (the alloca is not shared across blocks), so
4178 // it must use a plain load/combine/store rather than an atomic update.
4179 bool destIsPerThreadPrivate = isa_and_nonnull<memref::AllocaOp>(
4180 unwrapMemRefConversion(mapping.lookupOrDefault(op.getDestVar()))
4181 .getDefiningOp());
4182 SmallVector<mlir::acc::GPUParallelDimAttr> parDims =
4184 for (auto parDim : parDims) {
4185 if (parDim.isAnyBlock() && !destIsPerThreadPrivate) {
4186 // Block reduction directly stores to the accumulator using atomic.
4187 // The predication (tid.x == 0 when subgroup-aligned) is already handled
4188 // by the parent predicate_region processing.
4189 for (Operation *user : op.getSrcVar().getUsers()) {
4190 if (acc::ReductionAccumulateOp accumulateOp =
4191 dyn_cast<acc::ReductionAccumulateOp>(user)) {
4192 Location loc = accumulateOp.getLoc();
4193 FailureOr<arith::AtomicRMWKind> kind =
4194 getReductionKind(accumulateOp.getReductionOperator(),
4195 accumulateOp.getValue().getType(), loc);
4196 if (failed(kind))
4197 return;
4198 Value srcMemref = mapping.lookupOrDefault(accumulateOp.getMemref());
4199 // Recorded and patched in the fixup to avoid the reload race.
4200 auto reductionLoad =
4201 memref::LoadOp::create(rewriter, loc, srcMemref, ValueRange{});
4202 pendingCombineReloads.push_back({srcMemref, reductionLoad});
4203 constructAtomicAccumulation(loc,
4204 mapping.lookupOrDefault(op.getDestVar()),
4205 /*indices=*/{}, reductionLoad, *kind);
4206 return;
4207 }
4208 }
4209 // For decomposed complex reductions, the AccumulateOp was replaced
4210 // with real/imag AccumulateOps. Load from the private memref which
4211 // holds the reconstructed complex value.
4212 Value privateMemref = mapping.lookupOrDefault(op.getSrcVar());
4213 MemRefType memrefTy = cast<MemRefType>(privateMemref.getType());
4214 if (isa<ComplexType>(memrefTy.getElementType())) {
4215 Location loc = op.getLoc();
4216 Value reductionResult =
4217 memref::LoadOp::create(rewriter, loc, privateMemref, ValueRange{});
4218 arith::AtomicRMWKind kind = arith::AtomicRMWKind::addf;
4219 op.getRegion().walk([&](Operation *innerOp) {
4220 if (isa<complex::MulOp>(innerOp))
4221 kind = arith::AtomicRMWKind::mulf;
4222 });
4223 constructAtomicAccumulation(loc,
4224 mapping.lookupOrDefault(op.getDestVar()),
4225 /*indices=*/{}, reductionResult, kind);
4226 return;
4227 }
4228 }
4229 }
4230 op.getRegion().walk<WalkOrder::PreOrder>([&](Operation *innerOp) {
4231 if (acc::YieldOp yieldOp = dyn_cast<acc::YieldOp>(innerOp))
4232 return WalkResult::interrupt();
4233 if (innerOp->getNumRegions() > 0) {
4234 processOp(innerOp);
4235 return WalkResult::skip();
4236 }
4237 rewriter.clone(*innerOp, mapping);
4238 return WalkResult::advance();
4239 });
4240}
4241
4242void ACCCGToGPULowering::processGenericOp(Operation *op) {
4243 // Operations with no regions or operations for which we know
4244 // no recursive processing is needed can be fully cloned.
4245 LLVM_DEBUG(llvm::dbgs() << "processing generic op, cloning: ";
4246 op->print(llvm::dbgs()); llvm::dbgs() << "\n");
4247 Operation *newOp = rewriter.clone(*op, mapping);
4248 // update mapping as cloning creates different result values
4249 mapping.map(op->getResults(), newOp->getResults());
4250}
4251
4252void ACCCGToGPULowering::processGenericOpWithRegions(Operation *op) {
4253 // Generic handling for operations with regions
4254 LLVM_DEBUG(llvm::dbgs() << "processing generic op with regions: ";
4255 op->print(llvm::dbgs()); llvm::dbgs() << "\n");
4256
4257 // Clone the operation structure without its regions
4258 Operation *newOp = rewriter.cloneWithoutRegions(*op, mapping);
4259
4260 // Process each region recursively
4261 for (auto [oldRegion, newRegion] :
4262 llvm::zip(op->getRegions(), newOp->getRegions())) {
4263 // Create blocks in the new region corresponding to old region blocks
4264 for (auto &oldBlock : oldRegion.getBlocks()) {
4265 TypeRange argTypes = oldBlock.getArgumentTypes();
4266 size_t numArgs = argTypes.size();
4267 // Create new block with same argument types
4268 Block *newBlock =
4269 rewriter.createBlock(&newRegion, newRegion.end(), argTypes,
4270 SmallVector<Location>(numArgs, op->getLoc()));
4271
4272 // Map block arguments
4273 mapping.map(oldBlock.getArguments(), newBlock->getArguments());
4274
4275 // Process each operation in the block
4276 for (auto &innerOp : oldBlock.getOperations()) {
4277 OpBuilder::InsertionGuard guard(rewriter);
4278 rewriter.setInsertionPointToEnd(newBlock);
4279 processOp(&innerOp);
4280 }
4281 }
4282 }
4283 rewriter.setInsertionPointAfter(newOp);
4284
4285 // Update mapping for results
4286 mapping.map(op->getResults(), newOp->getResults());
4287}
4288
4289// thread through par dim to verify redundant/0 execution modes
4290void ACCCGToGPULowering::processOp(Operation *op) {
4291 if (isDeferredBarrierFlushPoint(op))
4292 flushDeferredBarriersBefore(op);
4293 if (mlir::acc::hasParDimsAttr(op) && isa<scf::ParallelOp>(op)) {
4294 // parallel loops require special processing based on parallel dimension
4295 // this is mutually recursive with processOp
4296 scf::ParallelOp parallelOp = cast<scf::ParallelOp>(op);
4297 processParallelOp(parallelOp);
4298 } else if (scf::ForOp seqLoop = dyn_cast<scf::ForOp>(op)) {
4299 processSeqLoop(seqLoop);
4300 } else if (acc::PrivatizeOp privatize = dyn_cast<acc::PrivatizeOp>(op)) {
4301 processPrivatize(privatize);
4302 } else if (acc::PrivateLocalOp privateLocal =
4303 dyn_cast<acc::PrivateLocalOp>(op)) {
4304 processPrivateLocal(privateLocal);
4305 } else if (acc::PredicateRegionOp predicateRegionOp =
4306 dyn_cast<acc::PredicateRegionOp>(op)) {
4307 processPredicateRegion(predicateRegionOp);
4308 } else if (acc::ReductionAccumulateOp accumulateOp =
4309 dyn_cast<acc::ReductionAccumulateOp>(op)) {
4310 processAccumulateOp(accumulateOp);
4311 } else if (auto accumulateArrayOp =
4312 dyn_cast<acc::ReductionAccumulateArrayOp>(op)) {
4313 processAccumulateArrayOp(accumulateArrayOp);
4314 } else if (acc::ReductionInitOp reductionInitOp =
4315 dyn_cast<acc::ReductionInitOp>(op)) {
4316 processReductionOp(reductionInitOp);
4317 } else if (acc::ReductionCombineOp reductionCombineOp =
4318 dyn_cast<acc::ReductionCombineOp>(op)) {
4319 processReductionCombineOp(reductionCombineOp);
4320 } else if (auto combineRegionOp =
4321 dyn_cast<acc::ReductionCombineRegionOp>(op)) {
4322 processCombineRegionOp(combineRegionOp);
4323 } else if (acc::ReductionOp accReductionOp = dyn_cast<acc::ReductionOp>(op)) {
4324 mapping.map(accReductionOp->getResult(0), accReductionOp.getVarPtr());
4325 } else if (mapping.contains(op)) {
4326 // do nothing, operation in mapping signals it is already taken care of
4327 LLVM_DEBUG(llvm::dbgs() << "skipping mapped op: " << *op << "\n");
4328 } else if (isa<acc::YieldOp>(op)) {
4329 for (auto [operand, result] :
4330 llvm::zip(op->getOperands(), op->getParentOp()->getResults())) {
4331 result.replaceAllUsesWith(mapping.lookup(operand));
4332 }
4333 } else if (isa<scf::ExecuteRegionOp>(op)) {
4334 processExecuteRegion(cast<scf::ExecuteRegionOp>(op));
4335 } else if (op->getNumRegions() == 0 ||
4336 isa<acc::OpenACCDialect>(op->getDialect())) {
4337 processGenericOp(op);
4338 } else {
4339 processGenericOpWithRegions(op);
4340 }
4341}
4342
4343/// Fold `acc.par_width` to its launch operand or constant one.
4344class RemoveParWidth : public OpRewritePattern<acc::ParWidthOp> {
4345 using OpRewritePattern<acc::ParWidthOp>::OpRewritePattern;
4346 LogicalResult matchAndRewrite(acc::ParWidthOp op,
4347 PatternRewriter &rewriter) const override {
4348 if (Value launchArg = op.getLaunchArg()) {
4349 rewriter.replaceOp(op, launchArg);
4350 } else {
4351 Value one = arith::ConstantIndexOp::create(rewriter, op.getLoc(), 1);
4352 rewriter.replaceOp(op, one);
4353 }
4354 return success();
4355 }
4356};
4357
4358/// Rewrite pattern that lowers `acc.compute_region` via ACCCGToGPULowering.
4359class ACCComputeRegionToGPUPattern
4360 : public OpRewritePattern<acc::ComputeRegionOp> {
4361public:
4362 ACCComputeRegionToGPUPattern(MLIRContext *context,
4363 acc::OpenACCSupport &accSupport,
4364 const ACCCGToGPUOptions &options)
4365 : OpRewritePattern<acc::ComputeRegionOp>(context), accSupport(accSupport),
4366 options(options) {}
4367
4368 LogicalResult matchAndRewrite(acc::ComputeRegionOp op,
4369 PatternRewriter &rewriter) const override {
4370 ACCCGToGPULowering kernelOpRewriter(op, rewriter, accSupport, options);
4371 return kernelOpRewriter.rewrite();
4372 }
4373
4374private:
4375 acc::OpenACCSupport &accSupport;
4376 const ACCCGToGPUOptions &options;
4377};
4378
4379class ACCCGToGPU : public acc::impl::ACCCGToGPUBase<ACCCGToGPU> {
4380public:
4381 using acc::impl::ACCCGToGPUBase<ACCCGToGPU>::ACCCGToGPUBase;
4382
4383 void runOnOperation() override {
4384 FunctionOpInterface funcOp = getOperation();
4385 MLIRContext *context = funcOp->getContext();
4386
4387 assert(deviceType != mlir::acc::DeviceType::Host &&
4388 deviceType != mlir::acc::DeviceType::Multicore &&
4389 "ACCCGToGPU only supports GPU device types");
4390
4391 // Validate parallel-loop mappings before any compute region is rewritten.
4392 // Loops outside compute regions do not require GPU mappings.
4393 WalkResult result = funcOp->walk([](scf::ParallelOp loop) {
4394 if (!loop->getParentOfType<acc::ComputeRegionOp>() ||
4396 return WalkResult::advance();
4397 loop.emitOpError("requires an 'acc.par_dims' attribute");
4398 return WalkResult::interrupt();
4399 });
4400 if (result.wasInterrupted()) {
4401 signalPassFailure();
4402 return;
4403 }
4404
4405 ACCCGToGPUOptions options;
4406 options.deviceType = deviceType;
4407 options.maxWorkgroupSharedMemory = maxWorkgroupSharedMemory;
4408 options.maxThreadPrivateStack = maxThreadPrivateStack;
4409 options.subgroupSize = subgroupSize;
4410
4411 // Try to get cached parent analysis first, fall back to local analysis.
4412 std::optional<std::reference_wrapper<acc::OpenACCSupport>> cachedAnalysis =
4413 getCachedParentAnalysis<acc::OpenACCSupport>(funcOp->getParentOp());
4414 acc::OpenACCSupport &accSupport = cachedAnalysis
4415 ? cachedAnalysis->get()
4416 : getAnalysis<acc::OpenACCSupport>();
4417
4418 RewritePatternSet patterns(context);
4419 patterns.insert<ACCComputeRegionToGPUPattern>(context, accSupport, options);
4420 patterns.insert<RemoveParWidth>(context);
4421 ConversionTarget target(getContext());
4422 target.markUnknownOpDynamicallyLegal([](Operation *) { return true; });
4423 target.addIllegalOp<acc::ComputeRegionOp, acc::ParWidthOp>();
4424 if (failed(applyPartialConversion(getOperation(), target,
4425 std::move(patterns)))) {
4426 signalPassFailure();
4427 }
4428 }
4429};
4430
4431} // namespace
return success()
static void createForAllDimensions(OpBuilder &builder, Location loc, SmallVectorImpl< Value > &values)
b
Return true if permutation is a valid permutation of the outer_dims_perm (case OuterOrInnerPerm::Oute...
ArrayAttr()
b getContext())
*if copies could not be generated due to yet unimplemented cases *copyInPlacementStart and copyOutPlacementStart in copyPlacementBlock *specify the insertion points where the incoming copies and outgoing copies
Creates a buffer in the faster memory space for the specified memref region (memref has to be non-zer...
auto load
@ None
static llvm::ManagedStatic< PassManagerOptions > options
static void rewrite(DataFlowSolver &solver, MLIRContext *context, MutableArrayRef< Region > initialRegions)
Rewrite the given regions using the computing analysis.
Definition SCCP.cpp:67
Block represents an ordered list of Operations.
Definition Block.h:34
MutableArrayRef< BlockArgument > BlockArgListType
Definition Block.h:110
BlockArgument getArgument(unsigned i)
Definition Block.h:154
unsigned getNumArguments()
Definition Block.h:153
RetT walk(FnT &&callback)
Walk all nested operations, blocks (including this block) or regions, depending on the type of callba...
Definition Block.h:318
Operation * getTerminator()
Get the terminator operation of this block.
Definition Block.cpp:249
bool mightHaveTerminator()
Return "true" if this block might have a terminator.
Definition Block.cpp:255
BlockArgListType getArguments()
Definition Block.h:112
Operation * getParentOp()
Returns the closest surrounding operation that contains this block.
Definition Block.cpp:31
IntegerAttr getIndexAttr(int64_t value)
Definition Builders.cpp:116
IntegerAttr getI32IntegerAttr(int32_t value)
Definition Builders.cpp:208
IntegerAttr getIntegerAttr(Type type, int64_t value)
Definition Builders.cpp:237
FloatAttr getFloatAttr(Type type, double value)
Definition Builders.cpp:263
IntegerType getI32Type()
Definition Builders.cpp:71
IntegerAttr getI64IntegerAttr(int64_t value)
Definition Builders.cpp:120
TypedAttr getZeroAttr(Type type)
Definition Builders.cpp:333
IntegerType getI1Type()
Definition Builders.cpp:61
Location getUnknownLoc()
Definition Builders.cpp:25
MLIRContext * getContext() const
Definition Builders.h:56
IndexType getIndexType()
Definition Builders.cpp:59
IntegerType getI8Type()
Definition Builders.cpp:67
auto lookupOrDefault(T from) const
Lookup a mapped value within the map.
Definition IRMapping.h:65
auto lookup(T from) const
Lookup a mapped value within the map.
Definition IRMapping.h:72
void map(Value from, Value to)
Inserts a new mapping for 'from' to 'to'.
Definition IRMapping.h:30
bool contains(T from) const
Checks to see if a mapping for 'from' exists.
Definition IRMapping.h:51
auto lookupOrNull(T from) const
Lookup a mapped value within the map.
Definition IRMapping.h:58
This class defines the main interface for locations in MLIR and acts as a non-nullable wrapper around...
Definition Location.h:76
MLIRContext is the top-level object for a collection of MLIR operations.
Definition MLIRContext.h:64
RAII guard to reset the insertion point of the builder when destroyed.
Definition Builders.h:351
This class helps build Operations.
Definition Builders.h:210
Block::iterator getInsertionPoint() const
Returns the current insertion point of the builder.
Definition Builders.h:448
Block * createBlock(Region *parent, Region::iterator insertPt={}, TypeRange argTypes={}, ArrayRef< Location > locs={})
Add new block with 'argTypes' arguments and set the insertion point to the end of it.
Definition Builders.cpp:439
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:581
void setInsertionPointToStart(Block *block)
Sets the insertion point to the start of the specified block.
Definition Builders.h:434
void setInsertionPoint(Block *block, Block::iterator insertPoint)
Set the insertion point to the specified location.
Definition Builders.h:401
void setInsertionPointToEnd(Block *block)
Sets the insertion point to the end of the specified block.
Definition Builders.h:439
Block * getInsertionBlock() const
Return the block the current insertion point belongs to.
Definition Builders.h:445
void setInsertionPointAfter(Operation *op)
Sets the insertion point to the node after the specified operation, which will cause subsequent inser...
Definition Builders.h:415
Operation * cloneWithoutRegions(Operation &op, IRMapping &mapper)
Creates a deep copy of this operation but keep the operation regions empty.
Definition Builders.h:596
Operation is the basic unit of execution within MLIR.
Definition Operation.h:87
Dialect * getDialect()
Return the dialect this operation is associated with, or nullptr if the associated dialect is not loa...
Definition Operation.h:237
Value getOperand(unsigned idx)
Definition Operation.h:375
bool hasTrait()
Returns true if the operation was registered with a particular trait, e.g.
Definition Operation.h:801
unsigned getNumSuccessors()
Definition Operation.h:758
bool isBeforeInBlock(Operation *other)
Given an operation 'other' that is within the same parent block, return whether the current operation...
result_iterator result_begin()
Definition Operation.h:438
Block * getBlock()
Returns the operation block that contains this operation.
Definition Operation.h:230
OpResult getResult(unsigned idx)
Get the 'idx'th result of this operation.
Definition Operation.h:432
unsigned getNumRegions()
Returns the number of regions held by this operation.
Definition Operation.h:726
Location getLoc()
The source location the operation was defined or derived from.
Definition Operation.h:240
Operation * getParentOp()
Returns the closest surrounding operation that contains this operation or nullptr if this is a top-le...
Definition Operation.h:251
unsigned getNumOperands()
Definition Operation.h:371
OpTy getParentOfType()
Return the closest surrounding parent operation that is of type 'OpTy'.
Definition Operation.h:255
void print(raw_ostream &os, const OpPrintingFlags &flags={})
MutableArrayRef< Region > getRegions()
Returns the regions held by this operation.
Definition Operation.h:729
result_iterator result_end()
Definition Operation.h:439
operand_range getOperands()
Returns an iterator on the underlying Value's.
Definition Operation.h:403
void setSuccessor(Block *block, unsigned index)
bool isAncestor(Operation *other)
Return true if this operation is an ancestor of the other operation.
Definition Operation.h:288
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
Block * getSuccessor(unsigned index)
Definition Operation.h:760
result_range getResults()
Definition Operation.h:440
This class contains a list of basic blocks and a link to the parent operation it is attached to.
Definition Region.h:26
Block & front()
Definition Region.h:65
Block & back()
Definition Region.h:64
iterator end()
Definition Region.h:56
iterator begin()
Definition Region.h:55
BlockListType & getBlocks()
Definition Region.h:45
virtual void replaceOp(Operation *op, ValueRange newValues)
Replace the results of the given (original) operation with the specified list of values (replacements...
virtual void eraseOp(Operation *op)
This method erases an operation that is known to have no uses.
This class allows for representing and managing the symbol table used by operations with the 'SymbolT...
Definition SymbolTable.h:25
Operation * lookup(StringRef name) const
Look up a symbol with the specified name, returning null if no such name exists.
Instances of the Type class are uniqued, have an immutable identifier and an optional mutable compone...
Definition Types.h:74
bool isIndex() const
Definition Types.cpp:56
auto walk(WalkFns &&...walkFns)
Walk this type and all attibutes/types nested within using the provided walk functions.
Definition Types.h:218
unsigned getIntOrFloatBitWidth() const
Return the bit width of an integer or a float type, assert failure on other types.
Definition Types.cpp:124
This class provides an abstraction over the different types of ranges over Values.
Definition ValueRange.h:389
This class represents an instance of an SSA value in the MLIR system, representing a computable value...
Definition Value.h:96
Type getType() const
Return the type of this value.
Definition Value.h:105
user_iterator user_begin() const
Definition Value.h:216
user_range getUsers() const
Definition Value.h:218
bool hasOneUse() const
Returns true if this value has exactly one use.
Definition Value.h:197
Operation * getDefiningOp() const
If this value is the result of an operation, return the operation that defines it.
Definition Value.cpp:18
static WalkResult skip()
Definition WalkResult.h:48
static WalkResult advance()
Definition WalkResult.h:47
static WalkResult interrupt()
Definition WalkResult.h:46
ParDimAttrT seqDim(MLIRContext *ctx) const
virtual ParDimAttrT map(MLIRContext *ctx, ParLevel level) const =0
Map an OpenACC parallelism level to target dimension.
ParDimAttrT vectorDim(MLIRContext *ctx) const
ParDimAttrT workerDim(MLIRContext *ctx) const
ParDimAttrT gangDim(MLIRContext *ctx, ParLevel level) const
Convenience methods for specific parallelism levels.
remark::detail::InFlightRemark emitRemark(Operation *op, std::function< std::string()> messageFn, llvm::StringRef category="openacc")
Emit an OpenACC remark with lazy message generation.
std::string getVariableName(Value v, VariableNameConfig config={})
Get the variable name for a given value.
InFlightDiagnostic emitNYI(Location loc, const Twine &message)
Report a case that is not yet supported by the implementation.
std::optional< TypeSizeAndAlignment > getTypeSizeAndAlignment(Type ty, ModuleOp module)
Returns the size and ABI alignment in bytes for ty.
bool tryAllocate(int64_t bytes, int64_t alignment=kDefaultAlignmentBytes)
Reserve bytes, rounding the current offset up to alignment first.
Specialization of arith.constant op that returns an integer of index type.
Definition Arith.h:93
static ConstantIndexOp create(OpBuilder &builder, Location location, int64_t value)
Definition ArithOps.cpp:398
Specialization of arith.constant op that returns an integer value.
Definition Arith.h:34
static ConstantIntOp create(OpBuilder &builder, Location location, int64_t value, unsigned width)
Definition ArithOps.cpp:297
SideEffects::EffectInstance< Effect > EffectInstance
Value getGPUSize(gpu::Processor processor, gpu::LaunchOp launch, const llvm::DenseMap< gpu::Processor, Value > &dimensionOps)
Return the launch dimension for processor from launch, or from dimensionOps when launch is null.
ParLevel getGangParLevel(int64_t gangDimValue)
Convert a gang dimension value (1, 2, or 3) to the corresponding ParLevel.
GPUParallelDimsAttr getParDimsAttr(Operation *op)
Obtain the parallel dimensions carried by op, if any.
MemRefType getPrivateBaseMemRefType(Type baseTy, ModuleOp module)
Returns the ranked MemRef type used to allocate privatized storage.
SmallVector< GPUParallelDimAttr > getReductionCombineParDims(ReductionCombineOp op)
Returns the parallel dimensions that participate in op's combine step.
void insertParDim(llvm::SmallVector< GPUParallelDimAttr > &parDims, GPUParallelDimAttr parDim)
Insert parDim into parDims while preserving dimension ordering.
static constexpr StringLiteral getSpecializedRoutineAttrName()
Definition OpenACC.h:208
bool hasParDimsAttr(Operation *op)
Return whether op carries parallel dimensions.
std::optional< arith::AtomicRMWKind > translateACCReductionOperator(ReductionOperator redOp, Type type)
Maps an acc reduction operator to the arith atomic RMW kind for type.
bool isSpecializedAccRoutine(mlir::Operation *op)
Used to check whether this is a specialized accelerator version of acc routine function.
Definition OpenACC.h:220
static bool isInsideACCSpecializedRoutine(Operation *op)
Value createIdentityValue(OpBuilder &b, Location loc, Type type, arith::AtomicRMWKind kind, bool useOnlyFiniteValue=true)
Creates the identity (neutral) value for a reduction of type and kind.
FailureOr< bool > isPrivateLocalSharedMemoryCandidate(PrivateLocalOp privateLocal, ComputeRegionOp computeRegion, ModuleOp module, const ACCToGPUMappingPolicy &policy, OpenACCSupport *support=nullptr)
True when privateLocal may be placed in shared memory.
static constexpr StringLiteral getRoutineInfoAttrName()
Definition OpenACC.h:204
Value castPointerLikeTypeIfNeeded(OpBuilder &builder, Location loc, Value value, Type resultType)
Cast value to resultType via PointerLikeType::genCast when needed.
int64_t sumExistingSharedMemoryBytes(Region &region)
Sum aligned static_upper_bound_bytes for all acc.gpu_shared_memory in region.
Value getGPUThreadId(gpu::Processor processor, gpu::LaunchOp launch, const llvm::DenseMap< gpu::Processor, Value > &indexOps)
Return the thread/block index for processor from launch, or from indexOps when launch is null.
PrivatizeOp getPrivatizeOp(PrivateLocalOp privateLocal, ComputeRegionOp computeRegion)
Resolve the acc.privatize operation associated with a private local.
bool hasGPUBlockRedundantAttr(Operation *op)
Return whether op is marked with the acc.gpu_block_redundant attribute, i.e.
Value generateReductionOp(OpBuilder &b, Location loc, Value lhs, Value rhs, arith::AtomicRMWKind kind)
Combines two reduction partial values using the operator for kind.
void removeParDim(llvm::SmallVector< GPUParallelDimAttr > &parDims, GPUParallelDimAttr parDim)
Remove parDim from parDims if present.
void setParDimsAttr(Operation *op, GPUParallelDimsAttr attr)
Set parallel dimensions on op.
ActiveParDimsAttr getActiveParDimsAttr(Operation *op)
Obtain the active parallel dimensions carried by op, if any.
std::optional< int64_t > getPrivateLocalSharedMemoryUpperBoundBytes(PrivateLocalOp privateLocal, ComputeRegionOp computeRegion, ModuleOp module, const ACCToGPUMappingPolicy &policy, OpenACCSupport *support=nullptr)
Upper-bound byte size for a shared-memory private_local candidate, or std::nullopt when not eligible ...
static bool isThreadXPrivatize(PrivatizeOp privatize)
ACCParMappingPolicy< mlir::acc::GPUParallelDimAttr > ACCToGPUMappingPolicy
Type alias for the GPU-specific mapping policy.
InFlightDiagnostic & next(InFlightDiagnostic &diag)
Starts a new message part in an in-flight diagnostic.
detail::InFlightRemark failed(Location loc, RemarkOpts opts)
Report an optimization remark that failed.
Definition Remarks.h:734
LoopNest buildLoopNest(OpBuilder &builder, Location loc, ValueRange lbs, ValueRange ubs, ValueRange steps, ValueRange iterArgs, function_ref< ValueVector(OpBuilder &, Location, ValueRange, ValueRange)> bodyBuilder=nullptr)
Creates a perfect nest of "for" loops, i.e.
Definition SCF.cpp:823
Value constantOne(OpBuilder &builder, Location loc, Type tp)
Generates a 1-valued constant of the given type.
OwningOpRef< spirv::ModuleOp > combine(ArrayRef< spirv::ModuleOp > inputModules, OpBuilder &combinedModuleBuilder, SymbolRenameListener symRenameListener)
Combines a list of SPIR-V inputModules into one.
Include the generated interface declarations.
bool matchPattern(Value value, const Pattern &pattern)
Entry point for matching a pattern over a Value.
Definition Matchers.h:490
detail::constant_int_value_binder m_ConstantInt(IntegerAttr::ValueType *bind_value)
Matches a constant holding a scalar/vector/tensor integer (splat) and writes the integer value to bin...
Definition Matchers.h:527
std::optional< int64_t > getConstantIntValue(OpFoldResult ofr)
If ofr is a constant integer or an IntegerAttr, return the integer.
llvm::DenseSet< ValueT, ValueInfoT > DenseSet
Definition LLVM.h:125
Value getValueOrCreateCastToIndexLike(OpBuilder &b, Location loc, Type targetType, Value value)
Create a cast from an index-like value (index or integer) to another index-like value.
Definition Utils.cpp:122
auto get(MLIRContext *context, Ts &&...params)
Helper method that injects context only if needed, this helps unify some of the attribute constructio...