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