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