[flang-commits] [flang] [flang] Support scoped LICM and OpenACC capture provenance (PR #225401)

via flang-commits flang-commits at lists.llvm.org
Tue Sep 22 06:52:16 PDT 2026


https://github.com/jeanPerier created https://github.com/llvm/llvm-project/pull/225401

Follow compute-region capture operands when checking whether scalar and scalar-descriptor loads are safe to speculate. Preserve the existing optional, array-element, and loop-modification safety checks.

Add an optional only-inside operation-name selector while retaining function-scoped alias analysis. Resolve the name once per function and compare interned operation names during ancestor traversal. The default continues to select all loops. An explicit name selects loops with a matching ancestor within the function, including the function itself.

Cover capture safety, host exclusion, non-OpenACC and nested scopes, loop boundaries, unmatched names, and the function boundary.

The motivation for this is to allow running LICM only on device relevant loop at O0.

>From eec6f63be9b60f46bbb51afb63aba3a3172ec4fc Mon Sep 17 00:00:00 2001
From: Jean Perier <jperier at nvidia.com>
Date: Tue, 22 Sep 2026 05:17:07 -0700
Subject: [PATCH] [flang] Support scoped LICM and OpenACC capture provenance

Follow compute-region capture operands when checking whether scalar and
scalar-descriptor loads are safe to speculate. Preserve the existing
optional, array-element, and loop-modification safety checks.

Add an optional only-inside operation-name selector while retaining
function-scoped alias analysis. Resolve the name once per function and
compare interned operation names during ancestor traversal. The default
continues to select all loops. An explicit name selects loops with a
matching ancestor within the function, including the function itself.

Cover capture safety, host exclusion, non-OpenACC and nested scopes,
loop boundaries, unmatched names, and the function boundary.
---
 .../flang/Optimizer/Transforms/Passes.td      |  12 +-
 .../Transforms/LoopInvariantCodeMotion.cpp    |  23 ++-
 .../test/Transforms/licm-acc-captured-ref.fir | 137 ++++++++++++++++++
 .../test/Transforms/licm-acc-compute-only.fir |  44 ++++++
 flang/test/Transforms/licm-scope.fir          |  39 +++++
 5 files changed, 253 insertions(+), 2 deletions(-)
 create mode 100644 flang/test/Transforms/licm-acc-captured-ref.fir
 create mode 100644 flang/test/Transforms/licm-acc-compute-only.fir
 create mode 100644 flang/test/Transforms/licm-scope.fir

diff --git a/flang/include/flang/Optimizer/Transforms/Passes.td b/flang/include/flang/Optimizer/Transforms/Passes.td
index 653f490715707..a308eb6ad05fa 100644
--- a/flang/include/flang/Optimizer/Transforms/Passes.td
+++ b/flang/include/flang/Optimizer/Transforms/Passes.td
@@ -811,6 +811,11 @@ def LoopInvariantCodeMotion : Pass<"flang-licm", "::mlir::func::FuncOp"> {
     fir.declare) and interfaces such as FortranObjectViewOpInterface.
     The pass only moves existing operations, so there are no dependent
     dialects.
+
+    With only-inside set, a loop must have an ancestor with the given operation
+    name, within the current function (including the function itself). A loop
+    matching the name does not qualify on that basis alone. No matching ancestor
+    means no hoisting. The pass and its analyses remain rooted at the function.
   }];
   let options = [Option<"hoistFromNestedRegions", "hoist-from-nested-regions",
                         "::fir::LICMNestedHoistingMode",
@@ -826,7 +831,12 @@ def LoopInvariantCodeMotion : Pass<"flang-licm", "::mlir::func::FuncOp"> {
                                 "loads"),
             clEnumValN(::fir::LICMNestedHoistingMode::Aggressive,
                        "aggressive", "Hoist all safe invariant ops")
-           )}]>];
+           )}]>,
+    Option<"onlyInside", "only-inside", "std::string",
+           /*default=*/"std::string{}",
+           "Only hoist from loops nested in an operation with this name "
+           "(empty means all loops)">
+  ];
 }
 
 #endif // FLANG_OPTIMIZER_TRANSFORMS_PASSES
diff --git a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
index 4003f1cb30b18..093ddd613cafd 100644
--- a/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
+++ b/flang/lib/Optimizer/Transforms/LoopInvariantCodeMotion.cpp
@@ -19,11 +19,13 @@
 #include "flang/Optimizer/HLFIR/HLFIROps.h"
 #include "flang/Optimizer/Support/Utils.h"
 #include "flang/Optimizer/Transforms/Passes.h"
+#include "mlir/Dialect/OpenACC/OpenACCUtils.h"
 #include "mlir/Interfaces/LoopLikeInterface.h"
 #include "mlir/Pass/Pass.h"
 #include "mlir/Transforms/LoopInvariantCodeMotionUtils.h"
 #include "llvm/ADT/TypeSwitch.h"
 #include "llvm/Support/DebugLog.h"
+#include <optional>
 #include <utility>
 
 namespace fir {
@@ -79,6 +81,12 @@ struct LoopInvariantCodeMotion
 /// on its own.
 static bool isNonOptionalScalar(Value location) {
   while (true) {
+    // A compute-region argument forwards a mapped input. Recover its storage
+    // provenance before checking whether a speculative scalar read is safe.
+    if (Value operand = acc::getACCOperandForBlockArg(location)) {
+      location = operand;
+      continue;
+    }
     LDBG() << "Checking location:\n" << location;
     Type dataType = fir::unwrapRefType(location.getType());
     if (!isa<fir::BaseBoxType>(location.getType()) &&
@@ -374,7 +382,20 @@ void LoopInvariantCodeMotion::runOnOperation() {
                             maybeConditionallyExecuted);
       };
 
-  getOperation()->walk([&](LoopLikeOpInterface loopLike) {
+  // Resolve the name once: ancestor checks compare interned operation names,
+  // not strings. Keep analysis scope independent of the selected loop scope.
+  std::optional<OperationName> scopeOpName;
+  if (!onlyInside.empty())
+    scopeOpName.emplace(onlyInside, &getContext());
+  Operation *function = getOperation();
+  function->walk([&](LoopLikeOpInterface loopLike) {
+    if (scopeOpName) {
+      Operation *scope = loopLike->getParentOp();
+      while (scope != function && scope->getName() != *scopeOpName)
+        scope = scope->getParentOp();
+      if (scope->getName() != *scopeOpName)
+        return;
+    }
     if (!fir::canMoveOutOf(loopLike, nullptr)) {
       LDBG() << "Cannot hoist anything out of loop operation: ";
       LDBG_OS([&](llvm::raw_ostream &os) {
diff --git a/flang/test/Transforms/licm-acc-captured-ref.fir b/flang/test/Transforms/licm-acc-captured-ref.fir
new file mode 100644
index 0000000000000..0ec3be41ab2f7
--- /dev/null
+++ b/flang/test/Transforms/licm-acc-captured-ref.fir
@@ -0,0 +1,137 @@
+// RUN: fir-opt %s --flang-licm | FileCheck %s
+
+// CHECK-LABEL: func.func @captured_descriptor
+// CHECK: acc.compute_region
+// CHECK: fir.load
+// CHECK: scf.for
+// CHECK-NOT: fir.load
+// CHECK: acc.yield
+func.func @captured_descriptor(%x: !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>, %n: index) {
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      scf.for %i = %c0 to %bound step %c1 {
+        %descriptor = fir.load %ref : !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
+
+// CHECK-LABEL: func.func @captured_optional_descriptor
+// CHECK: acc.compute_region
+// CHECK-NOT: fir.load
+// CHECK: scf.for
+// CHECK: fir.load
+// CHECK: acc.yield
+func.func @captured_optional_descriptor(%x: !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>> {fir.optional}, %n: index) {
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      scf.for %i = %c0 to %bound step %c1 {
+        %descriptor = fir.load %ref : !fir.ref<!fir.box<!fir.heap<!fir.array<?xf32>>>>
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
+
+// Capturing a reference must preserve the ability to prove that loading a
+// non-optional scalar is safe even when the loop might execute zero times.
+// CHECK-LABEL: func.func @captured_scalar
+// CHECK: acc.compute_region
+// CHECK: fir.load
+// CHECK: scf.for
+// CHECK-NOT: fir.load
+// CHECK: acc.yield
+func.func @captured_scalar(%x: !fir.ref<i32>, %n: index) {
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<i32>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      %zero = arith.constant 0 : i32
+      %result = scf.for %i = %c0 to %bound step %c1 iter_args(%previous = %zero) -> i32 {
+        %value = fir.load %ref : !fir.ref<i32>
+        scf.yield %value : i32
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
+
+// CHECK-LABEL: func.func @captured_optional
+// CHECK: acc.compute_region
+// CHECK-NOT: fir.load
+// CHECK: scf.for
+// CHECK: fir.load
+// CHECK: acc.yield
+// Capture forwarding must retain OPTIONAL, not treat every input as present.
+func.func @captured_optional(%x: !fir.ref<i32> {fir.optional}, %n: index) {
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<i32>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      %zero = arith.constant 0 : i32
+      %result = scf.for %i = %c0 to %bound step %c1 iter_args(%previous = %zero) -> i32 {
+        %value = fir.load %ref : !fir.ref<i32>
+        scf.yield %value : i32
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
+
+// CHECK-LABEL: func.func @captured_element
+// CHECK: acc.compute_region
+// CHECK-NOT: fir.load
+// CHECK: scf.for
+// CHECK: fir.load
+// CHECK: acc.yield
+// A captured scalar reference can designate an array element whose subscript
+// is not known to be valid. Its type alone does not justify speculation.
+func.func @captured_element(%x: !fir.ref<!fir.array<10xi32>>, %index: index, %n: index) {
+  %element = fir.coordinate_of %x, %index : (!fir.ref<!fir.array<10xi32>>, index) -> !fir.ref<i32>
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %element, %bound = %n) : (!fir.ref<i32>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      %zero = arith.constant 0 : i32
+      %result = scf.for %i = %c0 to %bound step %c1 iter_args(%previous = %zero) -> i32 {
+        %value = fir.load %ref : !fir.ref<i32>
+        scf.yield %value : i32
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
+
+// CHECK-LABEL: func.func @captured_modified
+// CHECK: acc.compute_region
+// CHECK-NOT: fir.load
+// CHECK: scf.for
+// CHECK: fir.load
+// CHECK: acc.yield
+// Speculative-read safety does not replace the loop's mod/ref check.
+func.func @captured_modified(%x: !fir.ref<i32>, %n: index) {
+  acc.kernel_environment {
+    acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<i32>, index) {
+      %c0 = arith.constant 0 : index
+      %c1 = arith.constant 1 : index
+      %zero = arith.constant 0 : i32
+      %result = scf.for %i = %c0 to %bound step %c1 iter_args(%previous = %zero) -> i32 {
+        %value = fir.load %ref : !fir.ref<i32>
+        fir.store %zero to %ref : !fir.ref<i32>
+        scf.yield %value : i32
+      }
+      acc.yield
+    } <{origin = "acc.kernels"}>
+  }
+  return
+}
diff --git a/flang/test/Transforms/licm-acc-compute-only.fir b/flang/test/Transforms/licm-acc-compute-only.fir
new file mode 100644
index 0000000000000..acdb3f811af82
--- /dev/null
+++ b/flang/test/Transforms/licm-acc-compute-only.fir
@@ -0,0 +1,44 @@
+// RUN: fir-opt %s --flang-licm="only-inside=acc.compute_region" | FileCheck %s
+// RUN: fir-opt %s --flang-licm | FileCheck %s --check-prefix=GLOBAL
+
+// Filtering must retain host loads even in a loop enclosing a compute region,
+// and find device loops beneath host conditionals. The default is unchanged.
+// CHECK-LABEL: func.func @mixed
+// CHECK: scf.for
+// CHECK: fir.load
+// CHECK: scf.if
+// CHECK: acc.compute_region
+// CHECK: fir.load
+// CHECK: scf.for
+// CHECK-NOT: fir.load
+// CHECK: acc.yield
+// CHECK: } else {
+// CHECK: scf.for
+// CHECK: fir.load
+// GLOBAL-LABEL: func.func @mixed
+// GLOBAL: fir.load
+// GLOBAL: scf.for
+func.func @mixed(%x: !fir.ref<i32>, %n: index, %condition: i1) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  scf.for %h = %c0 to %n step %c1 {
+    %host = fir.load %x : !fir.ref<i32>
+    scf.if %condition {
+      acc.kernel_environment {
+        acc.compute_region ins(%ref = %x, %bound = %n) : (!fir.ref<i32>, index) {
+          %zero = arith.constant 0 : index
+          %one = arith.constant 1 : index
+          scf.for %i = %zero to %bound step %one {
+            %device = fir.load %ref : !fir.ref<i32>
+          }
+          acc.yield
+        } <{origin = "acc.kernels"}>
+      }
+    } else {
+      scf.for %i = %c0 to %n step %c1 {
+        %fallback = fir.load %x : !fir.ref<i32>
+      }
+    }
+  }
+  return
+}
diff --git a/flang/test/Transforms/licm-scope.fir b/flang/test/Transforms/licm-scope.fir
new file mode 100644
index 0000000000000..84eeda62dc687
--- /dev/null
+++ b/flang/test/Transforms/licm-scope.fir
@@ -0,0 +1,39 @@
+// RUN: fir-opt %s --flang-licm="only-inside=scf.execute_region" | FileCheck %s --check-prefix=SCOPED
+// RUN: fir-opt %s --flang-licm="only-inside=scf.for" | FileCheck %s --check-prefix=SCOPED
+// RUN: fir-opt %s --flang-licm > %t.default
+// RUN: fir-opt %s --flang-licm="only-inside=func.func" > %t.function
+// RUN: diff %t.default %t.function
+// RUN: fir-opt %s > %t.input
+// RUN: fir-opt %s --flang-licm="only-inside=scf.if" > %t.unmatched
+// RUN: diff %t.input %t.unmatched
+// RUN: fir-opt %s --flang-licm="only-inside=builtin.module" > %t.outside-function
+// RUN: diff %t.input %t.outside-function
+
+// A matching region can be nested under host control flow. Nested matching
+// regions are supported; selecting a loop name does not select that loop itself.
+// SCOPED-LABEL: func.func @nested
+// SCOPED: scf.for
+// SCOPED: arith.addi
+// SCOPED: scf.execute_region
+// SCOPED: scf.execute_region
+// SCOPED: arith.muli
+// SCOPED: scf.for
+// SCOPED-NOT: arith.muli
+// SCOPED: scf.yield
+func.func @nested(%n: index) {
+  %c0 = arith.constant 0 : index
+  %c1 = arith.constant 1 : index
+  scf.for %outer = %c0 to %n step %c1 {
+    %host = arith.addi %n, %n : index
+    scf.execute_region {
+      scf.execute_region {
+        scf.for %inner = %c0 to %n step %c1 {
+          %selected = arith.muli %n, %n : index
+        }
+        scf.yield
+      }
+      scf.yield
+    }
+  }
+  return
+}



More information about the flang-commits mailing list