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