[Mlir-commits] [mlir] deedc7b - [Flang][OpenMP] Don't generate code for unreachable target regions. (#178937)
llvmlistbot at llvm.org
llvmlistbot at llvm.org
Mon Feb 16 01:31:46 PST 2026
Author: Abid Qadeer
Date: 2026-02-16T09:31:42Z
New Revision: deedc7bfe3159dfe08d0a74c9822c6d5157c6fe8
URL: https://github.com/llvm/llvm-project/commit/deedc7bfe3159dfe08d0a74c9822c6d5157c6fe8
DIFF: https://github.com/llvm/llvm-project/commit/deedc7bfe3159dfe08d0a74c9822c6d5157c6fe8.diff
LOG: [Flang][OpenMP] Don't generate code for unreachable target regions. (#178937)
When a target region is placed inside a constant false condition (e.g.,
`if (.false.)`), the dead code gets eliminated on the host side,
removing the `omp.target` operation entirely. However, the device-side
compilation pipeline is unaware of this elimination and attempts to
generate kernel code. Since the host never created offload metadata for
the eliminated target, the device-side kernel function lacks the
"kernel" attribute, causing `OpenMPOpt` to fail with an assertion when
it expects all outlined kernels to have this attribute. The problem can
be seen with the following code:
```fortran
program cele
implicit none
real :: V
integer :: i
if (.false.) then
!$omp target teams distribute parallel do
do i = 1, 5
V = V * 2
end do
!$omp end target teams distribute parallel do
end if
end program
```
It currently fails with the following assertion:
```
Assertion `omp::isOpenMPKernel(*Kernel) && "Expected kernel function!"' failed.
llvm/lib/Transforms/IPO/OpenMPOpt.cpp:4291
```
This PR adds `DeleteUnreachableTargetsPass` that identifies `omp.target`
operations in unreachable code blocks and removes them.
Added:
flang/lib/Optimizer/OpenMP/DeleteUnreachableTargets.cpp
flang/test/Lower/OpenMP/target-dead-code.f90
flang/test/Transforms/OpenMP/delete-unreachable-targets.mlir
Modified:
flang/include/flang/Optimizer/OpenMP/Passes.td
flang/lib/Optimizer/OpenMP/CMakeLists.txt
flang/lib/Optimizer/OpenMP/FunctionFiltering.cpp
flang/lib/Optimizer/Passes/Pipelines.cpp
mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp
Removed:
################################################################################
diff --git a/flang/include/flang/Optimizer/OpenMP/Passes.td b/flang/include/flang/Optimizer/OpenMP/Passes.td
index f17b1e3794908..1b7da0da3721b 100644
--- a/flang/include/flang/Optimizer/OpenMP/Passes.td
+++ b/flang/include/flang/Optimizer/OpenMP/Passes.td
@@ -41,6 +41,18 @@ def MarkDeclareTargetPass
let dependentDialects = ["mlir::omp::OpenMPDialect"];
}
+def DeleteUnreachableTargetsPass
+ : Pass<"omp-delete-unreachable-targets", "mlir::ModuleOp"> {
+ let summary = "Deletes OpenMP target operations in unreachable code";
+ let description = [{
+ Identifies and removes OpenMP target operations that reside in unreachable
+ code (e.g., inside if(.false.) blocks). This ensures consistency between
+ host and device compilation by preventing unreachable targets from being
+ processed on the device side.
+ }];
+ let dependentDialects = ["mlir::omp::OpenMPDialect"];
+}
+
def FunctionFilteringPass : Pass<"omp-function-filtering"> {
let summary = "Filters out functions intended for the host when compiling "
"for the target device.";
diff --git a/flang/lib/Optimizer/OpenMP/CMakeLists.txt b/flang/lib/Optimizer/OpenMP/CMakeLists.txt
index 23a7dc8f08399..eb4930fb2f6a7 100644
--- a/flang/lib/Optimizer/OpenMP/CMakeLists.txt
+++ b/flang/lib/Optimizer/OpenMP/CMakeLists.txt
@@ -8,6 +8,7 @@ add_flang_library(FlangOpenMPTransforms
MapsForPrivatizedSymbols.cpp
MapInfoFinalization.cpp
MarkDeclareTarget.cpp
+ DeleteUnreachableTargets.cpp
LowerWorkdistribute.cpp
LowerWorkshare.cpp
LowerNontemporal.cpp
diff --git a/flang/lib/Optimizer/OpenMP/DeleteUnreachableTargets.cpp b/flang/lib/Optimizer/OpenMP/DeleteUnreachableTargets.cpp
new file mode 100644
index 0000000000000..a6544ae4968a8
--- /dev/null
+++ b/flang/lib/Optimizer/OpenMP/DeleteUnreachableTargets.cpp
@@ -0,0 +1,79 @@
+//===- DeleteUnreachableTargets.cpp --------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+//
+// This pass removes OpenMP target operations that are in unreachable code.
+// This ensures host and device compilation have consistent target regions.
+//
+//===----------------------------------------------------------------------===//
+
+#include "flang/Optimizer/Dialect/FIRDialect.h"
+#include "flang/Optimizer/Dialect/FIROps.h"
+#include "flang/Optimizer/OpenMP/Passes.h"
+#include "mlir/Analysis/DataFlow/DeadCodeAnalysis.h"
+#include "mlir/Analysis/DataFlow/Utils.h"
+#include "mlir/Analysis/DataFlowFramework.h"
+#include "mlir/Dialect/Func/IR/FuncOps.h"
+#include "mlir/Dialect/OpenMP/OpenMPDialect.h"
+#include "mlir/IR/BuiltinOps.h"
+#include "mlir/Pass/Pass.h"
+#include "mlir/Support/LLVM.h"
+#include "llvm/ADT/SmallVector.h"
+
+namespace flangomp {
+#define GEN_PASS_DEF_DELETEUNREACHABLETARGETSPASS
+#include "flang/Optimizer/OpenMP/Passes.h.inc"
+} // namespace flangomp
+
+using namespace mlir;
+
+namespace {
+
+/// Check if an operation is unreachable using DeadCodeAnalysis.
+static bool isOperationUnreachable(Operation *op, DataFlowSolver &solver) {
+ Block *block = op->getBlock();
+ if (!block)
+ return false;
+
+ // Query DeadCodeAnalysis to check if the block is live (reachable).
+ ProgramPoint *point = solver.getProgramPointBefore(block);
+ const dataflow::Executable *executable =
+ solver.lookupState<dataflow::Executable>(point);
+
+ return (executable && !executable->isLive());
+}
+
+class DeleteUnreachableTargetsPass
+ : public flangomp::impl::DeleteUnreachableTargetsPassBase<
+ DeleteUnreachableTargetsPass> {
+public:
+ DeleteUnreachableTargetsPass() = default;
+
+ void runOnOperation() override {
+ auto module = getOperation();
+ DataFlowSolver solver;
+ dataflow::loadBaselineAnalyses(solver);
+
+ if (failed(solver.initializeAndRun(module))) {
+ signalPassFailure();
+ return;
+ }
+
+ // Collect unreachable target operations
+ SmallVector<omp::TargetOp> unreachableTargets;
+ module.walk([&](omp::TargetOp targetOp) {
+ if (isOperationUnreachable(targetOp.getOperation(), solver))
+ unreachableTargets.push_back(targetOp);
+ });
+
+ // Delete unreachable target operations
+ for (omp::TargetOp targetOp : unreachableTargets)
+ targetOp->erase();
+ }
+};
+
+} // namespace
diff --git a/flang/lib/Optimizer/OpenMP/FunctionFiltering.cpp b/flang/lib/Optimizer/OpenMP/FunctionFiltering.cpp
index 0acee8991e372..472d6a9f08a6e 100644
--- a/flang/lib/Optimizer/OpenMP/FunctionFiltering.cpp
+++ b/flang/lib/Optimizer/OpenMP/FunctionFiltering.cpp
@@ -122,8 +122,9 @@ class FunctionFilteringPass
// offloading can be supported.
bool hasTargetRegion =
funcOp
- ->walk<WalkOrder::PreOrder>(
- [&](omp::TargetOp) { return WalkResult::interrupt(); })
+ ->walk<WalkOrder::PreOrder>([&](omp::TargetOp targetOp) {
+ return WalkResult::interrupt();
+ })
.wasInterrupted();
omp::DeclareTargetDeviceType declareType =
diff --git a/flang/lib/Optimizer/Passes/Pipelines.cpp b/flang/lib/Optimizer/Passes/Pipelines.cpp
index 18ad22f80ae2b..9b73d587ee7bc 100644
--- a/flang/lib/Optimizer/Passes/Pipelines.cpp
+++ b/flang/lib/Optimizer/Passes/Pipelines.cpp
@@ -348,6 +348,11 @@ void createOpenMPFIRPassPipeline(mlir::PassManager &pm,
pm.addPass(flangomp::createAutomapToTargetDataPass());
pm.addPass(flangomp::createMapInfoFinalizationPass());
pm.addPass(flangomp::createMarkDeclareTargetPass());
+
+ // Delete unreachable target operations before FunctionFilteringPass
+ // extracts them.
+ pm.addPass(flangomp::createDeleteUnreachableTargetsPass());
+
pm.addPass(flangomp::createGenericLoopConversionPass());
if (opts.isTargetDevice)
pm.addPass(flangomp::createFunctionFilteringPass());
diff --git a/flang/test/Lower/OpenMP/target-dead-code.f90 b/flang/test/Lower/OpenMP/target-dead-code.f90
new file mode 100644
index 0000000000000..876618d6d963c
--- /dev/null
+++ b/flang/test/Lower/OpenMP/target-dead-code.f90
@@ -0,0 +1,88 @@
+! RUN: %flang_fc1 -emit-hlfir -fopenmp %s -o - | FileCheck %s --check-prefix=FIR
+
+! Test that OpenMP target regions in dead code are deleted
+
+! Test 1: if (.false.) with target - target should be deleted
+! FIR-LABEL: func.func @_QPtest_dead_simple
+! FIR: %[[FALSE:.*]] = arith.constant false
+! FIR: fir.if %[[FALSE]] {
+! FIR-NOT: omp.target
+subroutine test_dead_simple()
+ real :: v
+ if (.false.) then
+ !$omp target map(tofrom:v)
+ v = 1.0
+ !$omp end target
+ end if
+end subroutine
+
+! Test 2: Live target - should remain
+! FIR-LABEL: func.func @_QPtest_live_simple
+! FIR: omp.target
+subroutine test_live_simple()
+ real :: v
+ !$omp target map(tofrom:v)
+ v = 2.0
+ !$omp end target
+end subroutine
+
+! Test 3: Mixed dead and live
+! FIR-LABEL: func.func @_QPtest_mixed
+subroutine test_mixed()
+ real :: v
+ ! Dead - should be deleted
+ ! FIR: fir.if %{{.*}} {
+ if (.false.) then
+ !$omp target map(tofrom:v)
+ v = 3.0
+ !$omp end target
+ end if
+ ! FIR-NOT: omp.target
+ ! Live - should remain (expect exactly 1 omp.target in function)
+ !$omp target map(tofrom:v)
+ ! FIR: omp.target
+ v = 4.0
+ !$omp end target
+end subroutine
+
+! Test 4: Nested - outer false, target should be deleted
+! FIR-LABEL: func.func @_QPtest_nested_outer_false
+subroutine test_nested_outer_false()
+ real :: v
+ ! FIR: fir.if %{{.*}} {
+ if (.false.) then
+ if (.true.) then
+ !$omp target map(tofrom:v)
+ v = 5.0
+ !$omp end target
+ end if
+ end if
+ ! FIR-NOT: omp.target
+end subroutine
+
+! Test 5: Parameter constant - target should be deleted
+! FIR-LABEL: func.func @_QPtest_parameter
+subroutine test_parameter()
+ real :: v
+ logical, parameter :: DEAD = .false.
+ ! FIR: fir.if %{{.*}} {
+ if (DEAD) then
+ !$omp target map(tofrom:v)
+ v = 6.0
+ !$omp end target
+ end if
+ ! FIR-NOT: omp.target
+end subroutine
+
+! FIR-LABEL: func.func @_QPtest_outer
+subroutine test_outer
+ implicit none
+contains
+ subroutine unused_sub()
+ real :: v
+ !$omp target map(tofrom: v)
+ v = 5.0
+ !$omp end target
+ end subroutine
+ ! FIR-NOT: omp.target
+end subroutine
diff --git a/flang/test/Transforms/OpenMP/delete-unreachable-targets.mlir b/flang/test/Transforms/OpenMP/delete-unreachable-targets.mlir
new file mode 100644
index 0000000000000..55e4bdf5d65de
--- /dev/null
+++ b/flang/test/Transforms/OpenMP/delete-unreachable-targets.mlir
@@ -0,0 +1,322 @@
+// RUN: fir-opt --omp-delete-unreachable-targets %s | FileCheck %s
+
+// This test verifies that OpenMP target operations in unreachable code are
+// deleted.
+
+
+// CHECK-LABEL: func.func @test_if_false_simple
+func.func @test_if_false_simple() {
+ %false = arith.constant false
+ // The target in the dead branch should be removed
+ // CHECK: fir.if %false {
+ // CHECK-NOT: omp.target
+ // CHECK: }
+ fir.if %false {
+ omp.target {
+ omp.terminator
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_if_true_simple
+func.func @test_if_true_simple() {
+ %true = arith.constant true
+ // The target should remain since the branch is reachable
+ // CHECK: omp.target
+ fir.if %true {
+ omp.target {
+ omp.terminator
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_nested_outer_false
+func.func @test_nested_outer_false() {
+ %false = arith.constant false
+ %true = arith.constant true
+ // Outer false makes the whole nested structure unreachable
+ // CHECK: fir.if %false {
+ // CHECK-NOT: omp.target
+ // CHECK: }
+ fir.if %false {
+ fir.if %true {
+ omp.target {
+ omp.terminator
+ }
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_nested_inner_false
+func.func @test_nested_inner_false() {
+ %false = arith.constant false
+ %true = arith.constant true
+ // Outer true, inner false - target should be removed
+ // CHECK: fir.if %true {
+ // CHECK: fir.if %false {
+ // CHECK-NOT: omp.target
+ // CHECK: }
+ fir.if %true {
+ fir.if %false {
+ omp.target {
+ omp.terminator
+ }
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_nested_both_true
+func.func @test_nested_both_true() {
+ %true1 = arith.constant true
+ %true2 = arith.constant true
+ // CHECK: omp.target
+ fir.if %true1 {
+ fir.if %true2 {
+ omp.target {
+ omp.terminator
+ }
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_mixed_targets
+func.func @test_mixed_targets() {
+ %false = arith.constant false
+ %true = arith.constant true
+
+ // Live target - should remain (expect 2 targets total in output)
+ // CHECK: omp.target
+ omp.target {
+ omp.terminator
+ }
+
+ // Another live target in if (true) - should remain
+ // CHECK: omp.target
+ fir.if %true {
+ omp.target {
+ omp.terminator
+ }
+ }
+
+ // Dead target - will be removed
+ // CHECK-NOT: omp.target
+ fir.if %false {
+ omp.target {
+ omp.terminator
+ }
+ }
+
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_multiple_dead_targets
+func.func @test_multiple_dead_targets() {
+ %false = arith.constant false
+
+ // All targets inside dead branch should be removed
+ // CHECK-NOT: omp.target
+ fir.if %false {
+ omp.target {
+ omp.terminator
+ }
+
+ omp.target {
+ omp.terminator
+ }
+
+ omp.target {
+ omp.terminator
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_if_else_false
+func.func @test_if_else_false() {
+ %false = arith.constant false
+
+ // CHECK: fir.if %false {
+ fir.if %false {
+ // Then branch is unreachable, target should be deleted
+ omp.target {
+ omp.terminator
+ }
+ } else {
+ // CHECK-NOT: omp.target
+ // CHECK: } else {
+ // Else branch is reachable, target should remain
+ // CHECK: omp.target
+ omp.target {
+ omp.terminator
+ }
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_runtime_condition
+func.func @test_runtime_condition(%arg0: i1) {
+ // Runtime condition - cannot be optimized, should remain unchanged
+ // CHECK: fir.if %arg0 {
+ fir.if %arg0 {
+ // CHECK: omp.target
+ omp.target {
+ omp.terminator
+ }
+ }
+ return
+}
+
+// -----
+
+// Test that targets nested in structured control flow within unreachable blocks
+// are correctly identified as unreachable
+// CHECK-LABEL: func.func @test_nested_in_unreachable_block
+func.func @test_nested_in_unreachable_block() {
+ cf.br ^bb2
+^bb1:
+ // This entire block is unreachable
+ // Even though the fir.if condition is true, the whole block is dead
+ %true = arith.constant true
+ // CHECK: ^bb1:
+ // CHECK-NOT: omp.target
+ // CHECK: cf.br ^bb2
+ fir.if %true {
+ omp.target {
+ omp.terminator
+ }
+ }
+ cf.br ^bb2
+^bb2:
+ // CHECK: ^bb2:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_unreachable_block_after_branch
+func.func @test_unreachable_block_after_branch() {
+ cf.br ^bb2
+^bb1:
+ // This block is unreachable - no predecessor branches to it
+ // CHECK: ^bb1:
+ // CHECK-NOT: omp.target
+ // CHECK: cf.br ^bb2
+ omp.target {
+ omp.terminator
+ }
+ cf.br ^bb2
+^bb2:
+ // This block is reachable
+ // CHECK: ^bb2:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_multiple_unreachable_blocks
+func.func @test_multiple_unreachable_blocks() {
+ cf.br ^bb3
+^bb1:
+ // Unreachable block - no predecessor branches to it
+ // CHECK: ^bb1:
+ // CHECK-NOT: omp.target
+ // CHECK: cf.br ^bb2
+ omp.target {
+ omp.terminator
+ }
+ cf.br ^bb2
+^bb2:
+ // Also unreachable - only reachable from ^bb1 which is itself unreachable
+ // CHECK: ^bb2:
+ // CHECK-NOT: omp.target
+ // CHECK: return
+ omp.target {
+ omp.terminator
+ }
+ return
+^bb3:
+ // Reachable from entry
+ // CHECK: ^bb3:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_both_branches_reachable
+func.func @test_both_branches_reachable(%arg0: i1) {
+ cf.cond_br %arg0, ^bb1, ^bb2
+^bb1:
+ // CHECK: ^bb1:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ cf.br ^bb3
+^bb2:
+ // CHECK: ^bb2:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ cf.br ^bb3
+^bb3:
+ return
+}
+
+// -----
+
+// CHECK-LABEL: func.func @test_disconnected_block
+func.func @test_disconnected_block() {
+ // Entry goes directly to exit
+ cf.br ^bb2
+^bb1:
+ // This block is completely disconnected - no way to reach it
+ // CHECK: ^bb1:
+ // CHECK-NOT: omp.target
+ // CHECK: cf.br ^bb2
+ omp.target {
+ omp.terminator
+ }
+ cf.br ^bb2
+^bb2:
+ // Reachable from entry
+ // CHECK: ^bb2:
+ // CHECK-NEXT: omp.target
+ omp.target {
+ omp.terminator
+ }
+ return
+}
diff --git a/mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp b/mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp
index 41b1b9c37dead..bbde9f3b9071f 100644
--- a/mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp
+++ b/mlir/lib/Target/LLVMIR/Dialect/OpenMP/OpenMPToLLVMIRTranslation.cpp
@@ -6443,6 +6443,7 @@ static LogicalResult
convertOmpTarget(Operation &opInst, llvm::IRBuilderBase &builder,
LLVM::ModuleTranslation &moduleTranslation) {
auto targetOp = cast<omp::TargetOp>(opInst);
+
// The current debug location already has the DISubprogram for the outlined
// function that will be created for the target op. We save it here so that
// we can set it on the outlined function.
More information about the Mlir-commits
mailing list