Skip to content

Conversation

@jeanPerier
Copy link
Contributor

"!$acc loop" directive may be placed above loops with early exits.

Currently flang lowers loop with early exits to explicit control flow (this may be revisited when MLIR allows early exits in structured region). The acc loop directive cannot simply be ignored in such case in lowering because it may hold data clauses that should be applied when reaching that point.

This patch adds an "unstructured" attribute to acc.loop to support that case.
An acc.loop with such attributes may hold data operands but must have no controls. It is expected that the loop logic is implemented in its body in a way that the acc dialect may not understand.

Such acc.loop is just a container and the loop with early exit will be executed sequentially.

@llvmbot
Copy link
Member

llvmbot commented Oct 24, 2025

@llvm/pr-subscribers-openacc

@llvm/pr-subscribers-mlir-openacc

Author: None (jeanPerier)

Changes

"!$acc loop" directive may be placed above loops with early exits.

Currently flang lowers loop with early exits to explicit control flow (this may be revisited when MLIR allows early exits in structured region). The acc loop directive cannot simply be ignored in such case in lowering because it may hold data clauses that should be applied when reaching that point.

This patch adds an "unstructured" attribute to acc.loop to support that case.
An acc.loop with such attributes may hold data operands but must have no controls. It is expected that the loop logic is implemented in its body in a way that the acc dialect may not understand.

Such acc.loop is just a container and the loop with early exit will be executed sequentially.


Full diff: https://github.com/llvm/llvm-project/pull/164990.diff

4 Files Affected:

  • (modified) mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td (+8-1)
  • (modified) mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp (+6-2)
  • (modified) mlir/test/Dialect/OpenACC/invalid.mlir (+9)
  • (modified) mlir/test/Dialect/OpenACC/ops.mlir (+14)
diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td index 2f87975ebaa04..0a2b3f9e5e601 100644 --- a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td +++ b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td @@ -2487,6 +2487,12 @@ def OpenACC_LoopOp : OpenACC_Op<"loop", device-type-aware getter methods. When modifying these operands, the corresponding `device_type` attributes must be updated to maintain consistency between operands and their target device types. + + The `unstructured` attribute indicates that the loops inside the OpenACC + construct contain early exits and cannot be lowered to structured MLIR + operations. When this flag is set, the acc.loop should have no induction + variables and the loop must be implemented via explicit control flow + inside its body. }]; let arguments = (ins @@ -2520,7 +2526,8 @@ def OpenACC_LoopOp : OpenACC_Op<"loop", OptionalAttr<SymbolRefArrayAttr>:$firstprivatizationRecipes, Variadic<AnyType>:$reductionOperands, OptionalAttr<SymbolRefArrayAttr>:$reductionRecipes, - OptionalAttr<OpenACC_CombinedConstructsAttr>:$combined + OptionalAttr<OpenACC_CombinedConstructsAttr>:$combined, + UnitAttr:$unstructured ); let results = (outs Variadic<AnyType>:$results); diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp index ca46629919dba..ceb6a92de3772 100644 --- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp +++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp @@ -3065,8 +3065,12 @@ LogicalResult acc::LoopOp::verify() { if (getRegion().empty()) return emitError("expected non-empty body."); - // When it is container-like - it is expected to hold a loop-like operation. - if (isContainerLike()) { + if (getUnstructured()) { + if (!isContainerLike()) + return emitError( + "unstructured acc.loop must not have induction variables"); + } else if (isContainerLike()) { + // When it is container-like - it is expected to hold a loop-like operation. // Obtain the maximum collapse count - we use this to check that there // are enough loops contained. uint64_t collapseCount = getCollapseValue().value_or(1); diff --git a/mlir/test/Dialect/OpenACC/invalid.mlir b/mlir/test/Dialect/OpenACC/invalid.mlir index 26b63fbe182ea..0e75894eaeceb 100644 --- a/mlir/test/Dialect/OpenACC/invalid.mlir +++ b/mlir/test/Dialect/OpenACC/invalid.mlir @@ -492,6 +492,15 @@ func.func @fct1(%0 : !llvm.ptr) -> () { // ----- +%i1 = arith.constant 1 : i32 +%i2 = arith.constant 10 : i32 +// expected-error@+1 {{unstructured acc.loop must not have induction variables}} +acc.loop control(%iv : i32) = (%i1 : i32) to (%i2 : i32) step (%i1 : i32) { + acc.yield +} attributes {independent = [#acc.device_type<none>], unstructured} + +// ----- + // expected-error@+1 {{expect at least one of num, dim or static values}} acc.loop gang({}) { "test.openacc_dummy_op"() : () -> () diff --git a/mlir/test/Dialect/OpenACC/ops.mlir b/mlir/test/Dialect/OpenACC/ops.mlir index 77d18da49276a..6e63efc59fe57 100644 --- a/mlir/test/Dialect/OpenACC/ops.mlir +++ b/mlir/test/Dialect/OpenACC/ops.mlir @@ -2143,6 +2143,20 @@ func.func @acc_loop_container() { // ----- +func.func @acc_unstructured_loop() { + acc.loop { + acc.yield + } attributes {independent = [#acc.device_type<none>], unstructured} + return +} + +// CHECK-LABEL: func.func @acc_unstructured_loop +// CHECK: acc.loop +// CHECK: acc.yield +// CHECK: } attributes {independent = [#acc.device_type<none>], unstructured} + +// ----- + // Test private recipe with data bounds for array slicing acc.private.recipe @privatization_memref_slice : memref<10x10xf32> init { ^bb0(%arg0: memref<10x10xf32>, %bounds0: !acc.data_bounds_ty, %bounds1: !acc.data_bounds_ty): 
@llvmbot
Copy link
Member

llvmbot commented Oct 24, 2025

@llvm/pr-subscribers-mlir

Author: None (jeanPerier)

Changes

"!$acc loop" directive may be placed above loops with early exits.

Currently flang lowers loop with early exits to explicit control flow (this may be revisited when MLIR allows early exits in structured region). The acc loop directive cannot simply be ignored in such case in lowering because it may hold data clauses that should be applied when reaching that point.

This patch adds an "unstructured" attribute to acc.loop to support that case.
An acc.loop with such attributes may hold data operands but must have no controls. It is expected that the loop logic is implemented in its body in a way that the acc dialect may not understand.

Such acc.loop is just a container and the loop with early exit will be executed sequentially.


Full diff: https://github.com/llvm/llvm-project/pull/164990.diff

4 Files Affected:

  • (modified) mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td (+8-1)
  • (modified) mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp (+6-2)
  • (modified) mlir/test/Dialect/OpenACC/invalid.mlir (+9)
  • (modified) mlir/test/Dialect/OpenACC/ops.mlir (+14)
diff --git a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td index 2f87975ebaa04..0a2b3f9e5e601 100644 --- a/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td +++ b/mlir/include/mlir/Dialect/OpenACC/OpenACCOps.td @@ -2487,6 +2487,12 @@ def OpenACC_LoopOp : OpenACC_Op<"loop", device-type-aware getter methods. When modifying these operands, the corresponding `device_type` attributes must be updated to maintain consistency between operands and their target device types. + + The `unstructured` attribute indicates that the loops inside the OpenACC + construct contain early exits and cannot be lowered to structured MLIR + operations. When this flag is set, the acc.loop should have no induction + variables and the loop must be implemented via explicit control flow + inside its body. }]; let arguments = (ins @@ -2520,7 +2526,8 @@ def OpenACC_LoopOp : OpenACC_Op<"loop", OptionalAttr<SymbolRefArrayAttr>:$firstprivatizationRecipes, Variadic<AnyType>:$reductionOperands, OptionalAttr<SymbolRefArrayAttr>:$reductionRecipes, - OptionalAttr<OpenACC_CombinedConstructsAttr>:$combined + OptionalAttr<OpenACC_CombinedConstructsAttr>:$combined, + UnitAttr:$unstructured ); let results = (outs Variadic<AnyType>:$results); diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp index ca46629919dba..ceb6a92de3772 100644 --- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp +++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp @@ -3065,8 +3065,12 @@ LogicalResult acc::LoopOp::verify() { if (getRegion().empty()) return emitError("expected non-empty body."); - // When it is container-like - it is expected to hold a loop-like operation. - if (isContainerLike()) { + if (getUnstructured()) { + if (!isContainerLike()) + return emitError( + "unstructured acc.loop must not have induction variables"); + } else if (isContainerLike()) { + // When it is container-like - it is expected to hold a loop-like operation. // Obtain the maximum collapse count - we use this to check that there // are enough loops contained. uint64_t collapseCount = getCollapseValue().value_or(1); diff --git a/mlir/test/Dialect/OpenACC/invalid.mlir b/mlir/test/Dialect/OpenACC/invalid.mlir index 26b63fbe182ea..0e75894eaeceb 100644 --- a/mlir/test/Dialect/OpenACC/invalid.mlir +++ b/mlir/test/Dialect/OpenACC/invalid.mlir @@ -492,6 +492,15 @@ func.func @fct1(%0 : !llvm.ptr) -> () { // ----- +%i1 = arith.constant 1 : i32 +%i2 = arith.constant 10 : i32 +// expected-error@+1 {{unstructured acc.loop must not have induction variables}} +acc.loop control(%iv : i32) = (%i1 : i32) to (%i2 : i32) step (%i1 : i32) { + acc.yield +} attributes {independent = [#acc.device_type<none>], unstructured} + +// ----- + // expected-error@+1 {{expect at least one of num, dim or static values}} acc.loop gang({}) { "test.openacc_dummy_op"() : () -> () diff --git a/mlir/test/Dialect/OpenACC/ops.mlir b/mlir/test/Dialect/OpenACC/ops.mlir index 77d18da49276a..6e63efc59fe57 100644 --- a/mlir/test/Dialect/OpenACC/ops.mlir +++ b/mlir/test/Dialect/OpenACC/ops.mlir @@ -2143,6 +2143,20 @@ func.func @acc_loop_container() { // ----- +func.func @acc_unstructured_loop() { + acc.loop { + acc.yield + } attributes {independent = [#acc.device_type<none>], unstructured} + return +} + +// CHECK-LABEL: func.func @acc_unstructured_loop +// CHECK: acc.loop +// CHECK: acc.yield +// CHECK: } attributes {independent = [#acc.device_type<none>], unstructured} + +// ----- + // Test private recipe with data bounds for array slicing acc.private.recipe @privatization_memref_slice : memref<10x10xf32> init { ^bb0(%arg0: memref<10x10xf32>, %bounds0: !acc.data_bounds_ty, %bounds1: !acc.data_bounds_ty): 
Copy link
Contributor

@clementval clementval left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@razvanlupusoru
Copy link
Contributor

Hi Jean! Thank you for this work! Can you please make sure that CIR build still works? I am guessing that with LoopOp constructor changes there will be a few spots needing to be fixed.

@erichkeane
Copy link
Collaborator

Hi Jean! Thank you for this work! Can you please make sure that CIR build still works? I am guessing that with LoopOp constructor changes there will be a few spots needing to be fixed.

I just did a pull of this and it compiles/runs my tests. For LoopOp I'm using the retType/operands-collection constructor, so this doesn't change it for me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment