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