Skip to content

Conversation

@linuxlonelyeagle
Copy link
Member

@linuxlonelyeagle linuxlonelyeagle commented Oct 1, 2025

Fix #158760. Introduce the deleteDeadFunction function, which is used to delete dead functions.

@llvmbot llvmbot added mlir:core MLIR Core Infrastructure mlir labels Oct 1, 2025
@llvmbot
Copy link
Member

llvmbot commented Oct 1, 2025

@llvm/pr-subscribers-mlir

@llvm/pr-subscribers-mlir-core

Author: lonely eagle (linuxlonelyeagle)

Changes

Fix #158760. If a private functionOpInterface has no users and there is no callOpInterface in its region, it is a useless function, and the remove-dead-values pass will delete this function.


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

2 Files Affected:

  • (modified) mlir/lib/Transforms/RemoveDeadValues.cpp (+15-2)
  • (modified) mlir/test/Transforms/remove-dead-values.mlir (+15)
diff --git a/mlir/lib/Transforms/RemoveDeadValues.cpp b/mlir/lib/Transforms/RemoveDeadValues.cpp
index e0c65b0e09774..18c6006f04788 100644
--- a/mlir/lib/Transforms/RemoveDeadValues.cpp
+++ b/mlir/lib/Transforms/RemoveDeadValues.cpp
@@ -297,6 +297,17 @@ static void processFuncOp(FunctionOpInterface funcOp, Operation *module,
     return;
   }
 
+  // If a private function has neither users and function calls, it is a useless
+  // function.
+  SymbolTable::UseRange uses = *funcOp.getSymbolUses(module);
+  auto callSites = funcOp.getFunctionBody().getOps<CallOpInterface>();
+  if (uses.empty() && callSites.empty()) {
+    LDBG() << "Delete function op: "
+           << OpWithFlags(funcOp, OpPrintingFlags().skipRegions());
+    funcOp.erase();
+    return;
+  }
+
   // Get the list of unnecessary (non-live) arguments in `nonLiveArgs`.
   SmallVector<Value> arguments(funcOp.getArguments());
   BitVector nonLiveArgs = markLives(arguments, nonLiveSet, la);
@@ -312,7 +323,6 @@ static void processFuncOp(FunctionOpInterface funcOp, Operation *module,
   // Do (2). (Skip creating generic operand cleanup entries for call ops.
   // Call arguments will be removed in the call-site specific segment-aware
   // cleanup, avoiding generic eraseOperands bitvector mechanics.)
-  SymbolTable::UseRange uses = *funcOp.getSymbolUses(module);
   for (SymbolTable::SymbolUse use : uses) {
     Operation *callOp = use.getUser();
     assert(isa<CallOpInterface>(callOp) && "expected a call-like user");
@@ -881,9 +891,12 @@ void RemoveDeadValues::runOnOperation() {
   // end of this pass.
   RDVFinalCleanupList finalCleanupList;
 
+  module->walk([&](FunctionOpInterface op) {
+    processFuncOp(op, module, la, deadVals, finalCleanupList);
+  });
   module->walk([&](Operation *op) {
     if (auto funcOp = dyn_cast<FunctionOpInterface>(op)) {
-      processFuncOp(funcOp, module, la, deadVals, finalCleanupList);
+      // The FunctionOpInterface has been processed in advance.
     } else if (auto regionBranchOp = dyn_cast<RegionBranchOpInterface>(op)) {
       processRegionBranchOp(regionBranchOp, la, deadVals, finalCleanupList);
     } else if (auto branchOp = dyn_cast<BranchOpInterface>(op)) {
diff --git a/mlir/test/Transforms/remove-dead-values.mlir b/mlir/test/Transforms/remove-dead-values.mlir
index 56449469dc29f..b8ad762c52ddf 100644
--- a/mlir/test/Transforms/remove-dead-values.mlir
+++ b/mlir/test/Transforms/remove-dead-values.mlir
@@ -649,3 +649,18 @@ func.func @callee(%arg0: index, %arg1: index, %arg2: index) -> index {
   %res = call @mutl_parameter(%arg0, %arg1, %arg2) : (index, index, index) -> (index)
   return %res : index
 }
+
+// -----
+
+// CHECK-NOT: func private @single_private_func 
+func.func private @single_private_func(%arg0: i64) -> (i64) {
+    %c0_i64 = arith.constant 0 : i64
+    %2 = arith.cmpi eq, %arg0, %c0_i64 : i64
+    cf.cond_br %2, ^bb1, ^bb2
+  ^bb1:  // pred: ^bb0
+    %c1_i64 = arith.constant 1 : i64
+    return %c1_i64 : i64
+  ^bb2:  // pred: ^bb0
+    %c3_i64 = arith.constant 3 : i64
+    return %c3_i64 : i64
+}
\ No newline at end of file

@linuxlonelyeagle linuxlonelyeagle force-pushed the fix-private-func-remove-dead-values-bug branch from 5bb13ee to cee600a Compare October 1, 2025 01:58
Copy link
Member

@ftynse ftynse left a comment

Choose a reason for hiding this comment

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

I don't think this fixes the bug in question, just removes the function that contained the bug. We also have a dead symbol elimination pass that handles such cases generically.

@linuxlonelyeagle
Copy link
Member Author

linuxlonelyeagle commented Oct 1, 2025

I don't think this fixes the bug in question, just removes the function that contained the bug. We also have a dead symbol elimination pass that handles such cases generically.

https://mlir.llvm.org/docs/Passes/ I didn't see the dead symbol elimination pass.
Beyond that, I believe such a fix is reasonable.In this bug, the key reason is that the dead code analysis pass determined the entire IR to be dead.This led to unexpected results in liveness analysis.The removal of dead values relies on liveness analysis, which is why this bug occurred.

Looking at this from another angle, the private function is actually dead, so I introduced this.

@joker-eph joker-eph changed the title [mlir] Make remove-dead-values can delate useless function [mlir] Enable remove-dead-values to delete unused private function Oct 1, 2025
@joker-eph
Copy link
Collaborator

I believe such a fix is reasonable.In this bug, the key reason is that the dead code analysis pass determined the entire IR to be dead.This led to unexpected results in liveness analysis

Can you describe a bit more the underlying issue?

@linuxlonelyeagle linuxlonelyeagle force-pushed the fix-private-func-remove-dead-values-bug branch from 11eb3f8 to 16af35c Compare October 1, 2025 16:05
@linuxlonelyeagle
Copy link
Member Author

I believe such a fix is reasonable.In this bug, the key reason is that the dead code analysis pass determined the entire IR to be dead.This led to unexpected results in liveness analysis

Can you describe a bit more the underlying issue?

mlir-opt test.mlir -remove-dead-values -allow-unregistered-dialect -debug &> log   
...
[liveness-analysis LivenessAnalysis.cpp:299 1] RunLivenessAnalysis initialized for op: builtin.module check on unreachable code now:
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c0_i64 = arith.constant 0 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %0 = arith.cmpi eq, %arg0, %c0_i64 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c1_i64 = arith.constant 1 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c3_i64 = arith.constant 3 : i64 has no liveness info (unreachable), mark dead

Why did the above result occur in the liveness analysis?
Because they are dead code in dead code analysis.
Why are they dead code?
Because private functions have no caller.
This is the motivation behind why this PR was done in this manner.
cc: @ftynse @joker-eph

@joker-eph
Copy link
Collaborator

That does not really explain the crash though: why wasn't the cond_branch also dead and removed?

@linuxlonelyeagle
Copy link
Member Author

That does not really explain the crash though: why wasn't the cond_branch also dead and removed?

Good question. Give me a moment to look into the code.

@linuxlonelyeagle
Copy link
Member Author

That does not really explain the crash though: why wasn't the cond_branch also dead and removed?

[dataflow SparseAnalysis.cpp:431 1] Visiting operation: cf.cond_br with 1 operands and 0 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: arith.cmpi with 2 operands and 1 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: arith.constant with 0 operands and 1 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: func.return with 1 operands and 0 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: arith.constant with 0 operands and 1 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: func.return with 1 operands and 0 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[dataflow SparseAnalysis.cpp:431 1] Visiting operation: arith.constant with 0 operands and 1 results
[dataflow SparseAnalysis.cpp:439 1] Operation is in dead block, bailing out
[liveness-analysis LivenessAnalysis.cpp:299 1] RunLivenessAnalysis initialized for op: builtin.module check on unreachable code now:
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c0_i64 = arith.constant 0 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %0 = arith.cmpi eq, %arg0, %c0_i64 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c1_i64 = arith.constant 1 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:307 1] Result: 0 of %c3_i64 = arith.constant 3 : i64 has no liveness info (unreachable), mark dead
[liveness-analysis LivenessAnalysis.cpp:317 1] Block argument: 0 of func.func private @test(%arg0: i64) -> i64 {...} has no liveness info, mark dead
[pass-manager Pass.cpp:1102 2] PassManager run completed with result: success
test_tag: cf:
 operand #0: not live
module {
  func.func private @test(%arg0: i64) -> i64 {
    %c0_i64 = arith.constant 0 : i64
    %0 = arith.cmpi eq, %arg0, %c0_i64 : i64
    cf.cond_br %0, ^bb1, ^bb2 {tag = "cf"}
  ^bb1:  // pred: ^bb0
    %c1_i64 = arith.constant 1 : i64
    return %c1_i64 : i64
  ^bb2:  // pred: ^bb0
    %c3_i64 = arith.constant 3 : i64
    return %c3_i64 : i64
  }
}

LDBG() << "Block argument: " << blockArg.index() << " of "

The liveness lattice will by default set the SSA values without a liveness lattice to dead.
Why isn't the br.cond op explicitly set to dead?
Because its operand, which is the result of arith.cmpi, is explicitly set to dead. In fact, it is dead.

@linuxlonelyeagle
Copy link
Member Author

That does not really explain the crash though: why wasn't the cond_branch also dead and removed?

#161117 This PR maybe also need you.Thank you.

@linuxlonelyeagle linuxlonelyeagle force-pushed the fix-private-func-remove-dead-values-bug branch from 16af35c to 8f974c9 Compare October 6, 2025 15:33
@linuxlonelyeagle
Copy link
Member Author

@ftynse @joker-eph I've rewritten the code, and I think this should fix the issue nicely.

if (funcOp.isPublic() || funcOp.isExternal())
return;

SymbolTable::UseRange uses = *funcOp.getSymbolUses(module);
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this super expensive to build?

Copy link
Member Author

Choose a reason for hiding this comment

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

It certainly feels super expensive.I've updated the code so that it now only runs on functions that are likely to be dead functions.

Copy link
Member Author

Choose a reason for hiding this comment

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

I accidentally discovered that the -symbol-dce pass includes the functionality of this PR.

Copy link
Collaborator

Choose a reason for hiding this comment

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

This was mentioned to you earlier actually: #161471 (review)

Copy link
Member Author

Choose a reason for hiding this comment

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

I must admit I wasn't particularly well-versed in many of the MLIR passes, but I've gained a much better understanding now. Ha ha.😘
Actually, I just took another look at the code for this pass. The most immediate issue stems from directly deleting the operands of cond_br. The underlying problem remains the data flow analysis issue mentioned earlier.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's a bit of a pity that -symbol-dce isn't implemented via patterns; otherwise, it might have been possible to incorporate it into remove-dead-values.To be perfectly honest, I know what the best fix is.Indeed, deleting dead functions is not the best approach.

Copy link
Member Author

Choose a reason for hiding this comment

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

I think perhaps we could wrap the logic of symbol-dce in a function and then use it in remove-dead-values.

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

Labels

mlir:core MLIR Core Infrastructure mlir

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[MLIR] RemoveDeadValues pass errors when run on private function

4 participants