[Mlir-commits] [mlir] [flang][acc] Prune the acc.loop zero-trip edge when the body is proven to run (PR #219287)

llvmlistbot at llvm.org llvmlistbot at llvm.org
Thu Aug 27 13:47:38 PDT 2026


llvmorg-github-actions[bot] wrote:


<!--LLVM PR SUMMARY COMMENT-->

@llvm/pr-subscribers-mlir-openacc

Author: Susan Tan (ス-ザン タン) (SusanTan)

<details>
<summary>Changes</summary>

Structured acc.loop used to tell MLIR that control could either enter the body or skip straight past it, always both. The new helper tries to prove which of those actually happens: when the bounds are constants it can often show the body definitely runs, or definitely doesn't, and getSuccessorRegions then reports only the edge that's real. When it can't prove anything — non-constant bounds, or a loop whose iteration space lives inside its region — it reports both edges, exactly as before.

So the op's control flow graph goes from "always conservative" to "as precise as the bounds allow, conservative otherwise." Dataflow analyses running over that graph stop seeing a phantom path around loops that always execute, which is what unblocked the privatization cases.




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


3 Files Affected:

- (modified) mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt (+1) 
- (modified) mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp (+69-1) 
- (modified) mlir/test/Dialect/OpenACC/region-branchop-interface.mlir (+103-4) 


``````````diff
diff --git a/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt b/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
index b04a30b442de0..409155ec18fe6 100644
--- a/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
+++ b/mlir/lib/Dialect/OpenACC/IR/CMakeLists.txt
@@ -15,6 +15,7 @@ add_mlir_dialect_library(MLIROpenACCDialect
 
   LINK_LIBS PUBLIC
   MLIRIR
+  MLIRDialectUtils
   MLIRGPUDialect
   MLIRLLVMDialect
   MLIRMemRefDialect
diff --git a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
index 360eac356cceb..f03bddca19a73 100644
--- a/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
+++ b/mlir/lib/Dialect/OpenACC/IR/OpenACC.cpp
@@ -12,6 +12,7 @@
 #include "mlir/Dialect/LLVMIR/LLVMDialect.h"
 #include "mlir/Dialect/LLVMIR/LLVMTypes.h"
 #include "mlir/Dialect/MemRef/IR/MemRef.h"
+#include "mlir/Dialect/Utils/StaticValueUtils.h"
 #include "mlir/IR/Builders.h"
 #include "mlir/IR/BuiltinAttributes.h"
 #include "mlir/IR/BuiltinOps.h"
@@ -593,6 +594,59 @@ ValueRange HostDataOp::getSuccessorInputs(RegionSuccessor successor) {
   return getSingleRegionSuccessorInputs(getOperation(), successor);
 }
 
+/// Whether the body of a structured `acc.loop` is proven to run.
+enum class BodyExecution {
+  /// The body runs at least once, so control cannot branch past it.
+  Always,
+  /// The body never runs, so control cannot enter it.
+  Never,
+  /// Neither could be proven, so both edges are possible.
+  Maybe
+};
+
+/// Prove whether the body of `loopOp` runs. A counted dimension whose entry
+/// test already fails at its lower bound runs exactly zero times, so a single
+/// comparison decides both `Always` and `Never`. Bounds that are not constant
+/// prove nothing.
+static BodyExecution getBodyExecution(LoopOp loopOp) {
+  // A container-like loop describes its iteration space inside the region.
+  if (loopOp.isContainerLike())
+    return BodyExecution::Maybe;
+
+  ValueRange lbs = loopOp.getLowerbound();
+  ValueRange ubs = loopOp.getUpperbound();
+  ValueRange steps = loopOp.getStep();
+  if (lbs.size() != ubs.size() || lbs.size() != steps.size())
+    return BodyExecution::Maybe;
+
+  std::optional<ArrayRef<bool>> inclusive = loopOp.getInclusiveUpperbound();
+
+  BodyExecution result = BodyExecution::Always;
+  for (unsigned i = 0, e = lbs.size(); i < e; ++i) {
+    std::optional<int64_t> lb = getConstantIntValue(lbs[i]);
+    std::optional<int64_t> ub = getConstantIntValue(ubs[i]);
+    std::optional<int64_t> step = getConstantIntValue(steps[i]);
+    // A zero step either never advances or never runs; the two are
+    // indistinguishable here.
+    if (!lb || !ub || !step || *step == 0) {
+      result = BodyExecution::Maybe;
+      continue;
+    }
+
+    // A descending dimension compares against its bound the other way round.
+    bool closed = inclusive && i < inclusive->size() && (*inclusive)[i];
+    bool runsOnce = *step > 0 ? (closed ? *lb <= *ub : *lb < *ub)
+                              : (closed ? *lb >= *ub : *lb > *ub);
+    // The dimensions are iterated as a nest, so one empty dimension empties
+    // the whole nest whatever the others do, while the body runs only if every
+    // dimension runs.
+    if (!runsOnce)
+      return BodyExecution::Never;
+  }
+
+  return result;
+}
+
 void LoopOp::getSuccessorRegions(RegionBranchPoint point,
                                  SmallVectorImpl<RegionSuccessor> &regions) {
   // Unstructured loops: the body may contain arbitrary CFG and early exits.
@@ -607,7 +661,21 @@ void LoopOp::getSuccessorRegions(RegionBranchPoint point,
     return;
   }
 
-  // Structured loops: model a loop-shaped region graph similar to scf.for.
+  // Structured loops: model a loop-shaped region graph similar to scf.for,
+  // minus the entry edge the loop is proven not to take.
+  if (point.isParent()) {
+    switch (getBodyExecution(*this)) {
+    case BodyExecution::Always:
+      regions.push_back(RegionSuccessor(&getRegion()));
+      return;
+    case BodyExecution::Never:
+      regions.push_back(RegionSuccessor(getOperation()));
+      return;
+    case BodyExecution::Maybe:
+      break;
+    }
+  }
+
   regions.push_back(RegionSuccessor(&getRegion()));
   regions.push_back(RegionSuccessor(getOperation()));
 }
diff --git a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
index 708b48cbdfe5c..da10a76f3af46 100644
--- a/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
+++ b/mlir/test/Dialect/OpenACC/region-branchop-interface.mlir
@@ -119,10 +119,9 @@ func.func @last_mod_openacc_host_data(%arg0: memref<f32>, %mapped: memref<f32>)
 // CHECK-NEXT:   - loop_region
 // CHECK-LABEL: test_tag: acc_loop_after:
 // CHECK:  operand #0
-// CHECK-DAG:   - pre
-// CHECK-DAG:   - loop_region
-// the last writer is either the pre-loop store or
-// the store in the loop depending on the iteration count
+// CHECK-NEXT:   - loop_region
+// these bounds run the body at least once, so the store in the loop is the
+// only possible last writer
 // CHECK-LABEL: test_tag: acc_loop_post:
 // CHECK:  operand #0
 // CHECK-NEXT:   - post_loop
@@ -150,6 +149,106 @@ func.func @last_mod_openacc_loop(%arg0: memref<f32>) -> memref<f32> {
 
 // -----
 
+// structured acc.loop with an unknown upper bound: the body may or may not
+// run, so the edge that branches past it is kept.
+//
+// CHECK-LABEL: test_tag: acc_loop_dynamic_after:
+// CHECK:  operand #0
+// CHECK-DAG:   - pre
+// CHECK-DAG:   - loop_region
+func.func @last_mod_openacc_loop_dynamic(%arg0: memref<f32>, %n: i32) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  acc.loop control(%iv : i32) = (%c1_i32 : i32) to (%n : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_dynamic_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop with matching bounds and an exclusive upper bound: the
+// entry test already fails at the lower bound, so the body never runs and the
+// store before the loop is the last writer.
+//
+// CHECK-LABEL: test_tag: acc_loop_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_empty(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c10_i32 : i32)
+      step (%c1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_
+  memref.load %arg0[] {tag = "acc_loop_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop counting down with an inclusive upper bound: `lb` is
+// above `ub`, which an ascending-only comparison would misread as an empty
+// iteration space. This loop runs 10 times, so the body is guaranteed to run
+// and the store inside it is the only possible last writer.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - loop_region
+func.func @last_mod_openacc_loop_descending(%arg0: memref<f32>) -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c10_i32 : i32) to (%c1_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_descending_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
+// structured acc.loop stepping down away from an inclusive upper bound above
+// it: the body never runs, so the store before the loop is the last writer.
+// Comparing the bounds as if the step were ascending would instead prove the
+// body always runs, which is the opposite conclusion.
+//
+// CHECK-LABEL: test_tag: acc_loop_descending_empty_after:
+// CHECK:  operand #0
+// CHECK-NEXT:   - pre
+func.func @last_mod_openacc_loop_descending_empty(%arg0: memref<f32>)
+    -> memref<f32> {
+  %zero = arith.constant 0.0 : f32
+  %one = arith.constant 1.0 : f32
+  memref.store %zero, %arg0[] {tag_name = "pre"} : memref<f32>
+  %c1_i32 = arith.constant 1 : i32
+  %cm1_i32 = arith.constant -1 : i32
+  %c10_i32 = arith.constant 10 : i32
+  acc.loop control(%iv : i32) = (%c1_i32 : i32) to (%c10_i32 : i32)
+      step (%cm1_i32 : i32) {
+    memref.store %one, %arg0[] {tag_name = "loop_region"} : memref<f32>
+    acc.yield
+  } auto_ inclusiveUpperbound(array<i1: true>)
+  memref.load %arg0[] {tag = "acc_loop_descending_empty_after"} : memref<f32>
+  return %arg0 : memref<f32>
+}
+
+// -----
+
 // Unstructured acc.loop: the RegionBranch is modeled with explicit CFG and early
 // exits, and the RegionBranch graph only exposes a single entry and single
 // exit edge (no region backedge).

``````````

</details>


https://github.com/llvm/llvm-project/pull/219287


More information about the Mlir-commits mailing list