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