[mlir][OpenACC] Preserve worker rows when combining reductions - #210804
Merged
khaki3 merged 11 commits intoJul 22, 2026
Conversation
|
@llvm/pr-subscribers-openacc @llvm/pr-subscribers-mlir-openacc Author: Matsu (khaki3) ChangesExample: !$cuf kernel do(3) <<< *,(32,8,1) >>> reduce(+:res)
do k = 1, n3
do j = 1, n2
do i = 1, n1
res = res + a(i,j,k)
end do
end do
end doIn this code, each ThreadY worker row produces a partial result, but the final combine was restricted to Fix: keep ThreadY active when combining worker-private reductions and reject mixed scopes requiring incompatible predication. Full diff: https://github.com/llvm/llvm-project/pull/210804.diff 3 Files Affected:
diff --git a/mlir/lib/Dialect/OpenACC/Transforms/ACCCGToGPU.cpp b/mlir/lib/Dialect/OpenACC/Transforms/ACCCGToGPU.cpp
index 24aeed6e23bda..6be79b0dd40e4 100644
--- a/mlir/lib/Dialect/OpenACC/Transforms/ACCCGToGPU.cpp
+++ b/mlir/lib/Dialect/OpenACC/Transforms/ACCCGToGPU.cpp
@@ -1246,7 +1246,30 @@ ACCCGToGPULowering::computeActiveAndInactiveParDims(Operation *op,
mlir::acc::GPUParallelDimAttr lowestParDim =
mlir::acc::GPUParallelDimAttr::threadXDim(ctx);
if (block) {
- block->walk([&](Operation *op) {
+ std::optional<bool> combineThreadYActive;
+ auto applyCombineParDims =
+ [&](Operation *combineOp, Value src,
+ ArrayRef<mlir::acc::GPUParallelDimAttr> combineParDims) {
+ bool isWorkerPrivate =
+ getPrivateScopeForMemref(src) == PrivateMemScope::Worker;
+ for (mlir::acc::GPUParallelDimAttr parDim : combineParDims) {
+ if (parDim.isThreadY()) {
+ if (combineThreadYActive &&
+ *combineThreadYActive != isWorkerPrivate) {
+ combineOp->emitError()
+ << "mixed worker-private and non-worker-private reduction "
+ "combines require incompatible ThreadY predication";
+ hasFailed = true;
+ return failure();
+ }
+ combineThreadYActive = isWorkerPrivate;
+ } else {
+ mlir::acc::removeParDim(ancestorParDims, parDim);
+ }
+ }
+ return success();
+ };
+ block->walk([&](Operation *op) -> WalkResult {
// Check stores to acc.private_local - add the privatize's par_dims
// as active dims so predication is correct for per-worker/gang memory.
if (memref::StoreOp storeOp = dyn_cast<memref::StoreOp>(op)) {
@@ -1277,17 +1300,17 @@ ACCCGToGPULowering::computeActiveAndInactiveParDims(Operation *op,
// kernel and loop in combined constructs.
if (acc::ReductionCombineOp reductionCombineOp =
dyn_cast<acc::ReductionCombineOp>(op)) {
- for (mlir::acc::GPUParallelDimAttr parDim :
- getReductionCombineParDims(reductionCombineOp)) {
- mlir::acc::removeParDim(ancestorParDims, parDim);
- }
+ if (failed(applyCombineParDims(
+ reductionCombineOp, reductionCombineOp.getSrcMemref(),
+ getReductionCombineParDims(reductionCombineOp))))
+ return WalkResult::interrupt();
}
if (acc::ReductionCombineRegionOp combineRegionOp =
dyn_cast<acc::ReductionCombineRegionOp>(op)) {
- for (mlir::acc::GPUParallelDimAttr parDim :
- getReductionCombineParDims(combineRegionOp)) {
- mlir::acc::removeParDim(ancestorParDims, parDim);
- }
+ if (failed(applyCombineParDims(
+ combineRegionOp, combineRegionOp.getSrcVar(),
+ getReductionCombineParDims(combineRegionOp))))
+ return WalkResult::interrupt();
}
// An array accumulate reduces across its par_dims via gpu.all_reduce, so
// all those threads must execute it - treat them as active (unlike the
@@ -1301,6 +1324,14 @@ ACCCGToGPULowering::computeActiveAndInactiveParDims(Operation *op,
}
return WalkResult::advance();
});
+ if (combineThreadYActive) {
+ mlir::acc::GPUParallelDimAttr threadY =
+ mlir::acc::GPUParallelDimAttr::threadYDim(ctx);
+ if (*combineThreadYActive)
+ mlir::acc::insertParDim(ancestorParDims, threadY);
+ else
+ mlir::acc::removeParDim(ancestorParDims, threadY);
+ }
}
// Obtain launch dimensions
@@ -1747,7 +1778,7 @@ ACCCGToGPULowering::getPrivateMemScope(acc::PrivatizeOp privatizeOp) {
}
if (hasThreadX)
return PrivateMemScope::Thread;
- if (hasBlock && hasThreadY)
+ if (hasThreadY)
return PrivateMemScope::Worker;
if (hasBlock)
return PrivateMemScope::Gang;
@@ -1871,6 +1902,8 @@ void ACCCGToGPULowering::processPredicateRegion(
SmallVector<mlir::acc::GPUParallelDimAttr>>
parDimsPair = computeActiveAndInactiveParDims(
interOp, &interOp.getRegion().front());
+ if (hasFailed)
+ return;
// If ThreadY reduction exists, subgroup alignment is applied
// (blockDim.x = subgroupSize), so ThreadX lanes exist even without explicit
diff --git a/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine-mixed-scope.mlir b/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine-mixed-scope.mlir
new file mode 100644
index 0000000000000..a0d381740c034
--- /dev/null
+++ b/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine-mixed-scope.mlir
@@ -0,0 +1,43 @@
+// RUN: mlir-opt %s --pass-pipeline="builtin.module(func.func(acc-cg-to-gpu))" \
+// RUN: -verify-diagnostics
+
+func.func @mixed_scope_worker_reduction_combine(
+ %other: memref<i32>, %result: memref<i32>) {
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c32 = arith.constant 32 : index
+ %block_y = acc.par_width %c1 {par_dim = #acc.par_dim<block_y>}
+ %thread_y = acc.par_width %c4 {par_dim = #acc.par_dim<thread_y>}
+ %thread_x = acc.par_width %c32 {par_dim = #acc.par_dim<thread_x>}
+ acc.kernel_environment {
+ %private = acc.privatize [#acc<par_dims[thread_y]>]
+ : () -> !acc.private_type<memref<i32>>
+ // expected-error@+1 {{failed to legalize operation 'acc.compute_region' that was explicitly marked illegal}}
+ acc.compute_region launch(%by = %block_y, %ty = %thread_y, %tx = %thread_x)
+ ins(%private_arg = %private, %other_arg = %other,
+ %result_arg = %result)
+ : (!acc.private_type<memref<i32>>, memref<i32>, memref<i32>) {
+ %c0 = arith.constant 0 : index
+ %c1_inner = arith.constant 1 : index
+ %c0_i32 = arith.constant 0 : i32
+ scf.parallel (%block_iv) = (%c0) to (%by) step (%c1_inner) {
+ %local = acc.private_local %private_arg
+ : (!acc.private_type<memref<i32>>) -> memref<i32>
+ scf.parallel (%worker_iv) = (%c0) to (%ty) step (%c1_inner) {
+ memref.store %c0_i32, %local[] : memref<i32>
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[thread_y]>}
+ acc.predicate_region {
+ acc.reduction_combine %local into %result_arg <add> : memref<i32>
+ {acc.par_dims = #acc<par_dims[block_y, thread_y]>}
+ // expected-error@+1 {{mixed worker-private and non-worker-private reduction combines require incompatible ThreadY predication}}
+ acc.reduction_combine %other_arg into %result_arg <add> : memref<i32>
+ {acc.par_dims = #acc<par_dims[block_y, thread_y]>}
+ }
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[block_y]>}
+ acc.yield
+ } {origin = "acc.parallel"}
+ }
+ return
+}
diff --git a/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine.mlir b/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine.mlir
new file mode 100644
index 0000000000000..b2fd246d33d1b
--- /dev/null
+++ b/mlir/test/Dialect/OpenACC/acc-cg-to-gpu-worker-reduction-combine.mlir
@@ -0,0 +1,95 @@
+// RUN: mlir-opt %s --pass-pipeline="builtin.module(func.func(acc-cg-to-gpu))" | FileCheck %s
+
+// A worker-private reduction has one shared slot per ThreadY row. The combine
+// must keep ThreadY active and predicate only ThreadX.
+
+// CHECK-LABEL: func.func @worker_reduction_combine
+// CHECK: gpu.launch {{.*}} threads([[TID_X:%[^,]+]], [[TID_Y:%[^,]+]],
+// CHECK-NOT: arith.cmpi eq, [[TID_Y]]
+// CHECK: %[[IS_X_ZERO:.*]] = arith.cmpi eq, [[TID_X]],
+// CHECK-NOT: arith.andi
+// CHECK: scf.if %[[IS_X_ZERO]]
+// CHECK: acc.atomic.update
+
+func.func @worker_reduction_combine(%result: memref<i32>) {
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c32 = arith.constant 32 : index
+ %block_y = acc.par_width %c1 {par_dim = #acc.par_dim<block_y>}
+ %thread_y = acc.par_width %c4 {par_dim = #acc.par_dim<thread_y>}
+ %thread_x = acc.par_width %c32 {par_dim = #acc.par_dim<thread_x>}
+ acc.kernel_environment {
+ %private = acc.privatize [#acc<par_dims[thread_y]>]
+ : () -> !acc.private_type<memref<i32>>
+ acc.compute_region launch(%by = %block_y, %ty = %thread_y, %tx = %thread_x)
+ ins(%private_arg = %private, %result_arg = %result)
+ : (!acc.private_type<memref<i32>>, memref<i32>) {
+ %c0 = arith.constant 0 : index
+ %c1_inner = arith.constant 1 : index
+ %c0_i32 = arith.constant 0 : i32
+ scf.parallel (%block_iv) = (%c0) to (%by) step (%c1_inner) {
+ %local = acc.private_local %private_arg
+ : (!acc.private_type<memref<i32>>) -> memref<i32>
+ scf.parallel (%worker_iv) = (%c0) to (%ty) step (%c1_inner) {
+ memref.store %c0_i32, %local[] : memref<i32>
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[thread_y]>}
+ acc.predicate_region {
+ acc.reduction_combine %local into %result_arg <add> : memref<i32>
+ {acc.par_dims = #acc<par_dims[block_y, thread_y]>}
+ }
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[block_y]>}
+ acc.yield
+ } {origin = "acc.parallel"}
+ }
+ return
+}
+
+// CHECK-LABEL: func.func @worker_reduction_combine_region
+// CHECK: gpu.launch {{.*}} threads([[REGION_TID_X:%[^,]+]], [[REGION_TID_Y:%[^,]+]],
+// CHECK-NOT: arith.cmpi eq, [[REGION_TID_Y]]
+// CHECK: %[[REGION_IS_X_ZERO:.*]] = arith.cmpi eq, [[REGION_TID_X]],
+// CHECK-NOT: arith.andi
+// CHECK: scf.if %[[REGION_IS_X_ZERO]]
+// CHECK: arith.addi
+
+func.func @worker_reduction_combine_region(%result: memref<i32>) {
+ %c1 = arith.constant 1 : index
+ %c4 = arith.constant 4 : index
+ %c32 = arith.constant 32 : index
+ %block_y = acc.par_width %c1 {par_dim = #acc.par_dim<block_y>}
+ %thread_y = acc.par_width %c4 {par_dim = #acc.par_dim<thread_y>}
+ %thread_x = acc.par_width %c32 {par_dim = #acc.par_dim<thread_x>}
+ acc.kernel_environment {
+ %private = acc.privatize [#acc<par_dims[thread_y]>]
+ : () -> !acc.private_type<memref<i32>>
+ acc.compute_region launch(%by = %block_y, %ty = %thread_y, %tx = %thread_x)
+ ins(%private_arg = %private, %result_arg = %result)
+ : (!acc.private_type<memref<i32>>, memref<i32>) {
+ %c0 = arith.constant 0 : index
+ %c1_inner = arith.constant 1 : index
+ %c0_i32 = arith.constant 0 : i32
+ scf.parallel (%block_iv) = (%c0) to (%by) step (%c1_inner) {
+ %local = acc.private_local %private_arg
+ : (!acc.private_type<memref<i32>>) -> memref<i32>
+ scf.parallel (%worker_iv) = (%c0) to (%ty) step (%c1_inner) {
+ memref.store %c0_i32, %local[] : memref<i32>
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[thread_y]>}
+ acc.predicate_region {
+ acc.reduction_combine_region %local into %result_arg : memref<i32> {
+ %lhs = memref.load %result_arg[] : memref<i32>
+ %rhs = memref.load %local[] : memref<i32>
+ %sum = arith.addi %lhs, %rhs : i32
+ memref.store %sum, %result_arg[] : memref<i32>
+ acc.yield
+ } {acc.par_dims = #acc<par_dims[block_y, thread_y]>}
+ }
+ scf.reduce
+ } {acc.par_dims = #acc<par_dims[block_y]>}
+ acc.yield
+ } {origin = "acc.parallel"}
+ }
+ return
+}
|
Keep ThreadY active for worker-private sources so every worker row contributes, and reject mixed scopes that require incompatible predication.
Use the standard OpenACC unsupported-feature diagnostic for incompatible ThreadY predication.
Propagate nested worker requirements without broadening unrelated side effects, and reject incompatible operations sharing one predicate region.
Analyze control-flow regions recursively and recognize aliased worker-private stores so their worker rows remain active.
Preserve all privatized ThreadY dimensions through aliases and reject intrinsically effecting region operations that cannot share worker predication.
Follow only alias-preserving view operations and treat every non-private combine as ThreadY-inactive to avoid broadening ambiguous global effects.
Preserve legacy predicates unless a proven worker combine requires ThreadY, and reject broadening around writes, unknown effects, or incompatible combines.
khaki3
force-pushed
the
kmatsumura/fix-worker-reduction-combine
branch
from
July 22, 2026 19:20
c7c5aaf to
b5e3c09
Compare
midhuncodes7
pushed a commit
to midhuncodes7/llvm-project
that referenced
this pull request
Jul 28, 2026
…210804) Example: ```fortran !$acc parallel loop gang worker reduction(+:sum) do j = 1, n !$acc loop vector reduction(+:sum) do i = 1, n sum = sum + a(i, j) end do end do ``` In this code, each worker has a private partial result, but combine predication previously allowed only ThreadY row zero to contribute. Fix: keep ThreadY active only for proven atomic worker combines, while preserving legacy predication or reporting NYI for unsafe combinations.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Example:
In this code, each worker has a private partial result, but combine predication previously allowed only ThreadY row zero to contribute.
Fix: keep ThreadY active only for proven atomic worker combines, while preserving legacy predication or reporting NYI for unsafe combinations.