-
Notifications
You must be signed in to change notification settings - Fork 15.3k
[mlir][Transforms] Fix crash in -remove-dead-values on private functions
#169269
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
|
@llvm/pr-subscribers-mlir-ub @llvm/pr-subscribers-mlir-core Author: Matthias Springer (matthias-springer) ChangesThis commit fixes a crash in the With this commit, a "simple" operation is erased when it has a dead operand. If the operation were not dead, the liveness analysis would not have marked one of its operands as "dead". Full diff: https://github.com/llvm/llvm-project/pull/169269.diff 2 Files Affected:
diff --git a/mlir/lib/Transforms/RemoveDeadValues.cpp b/mlir/lib/Transforms/RemoveDeadValues.cpp
index 989c614ef6617..9d4d24c39c116 100644
--- a/mlir/lib/Transforms/RemoveDeadValues.cpp
+++ b/mlir/lib/Transforms/RemoveDeadValues.cpp
@@ -141,6 +141,33 @@ static bool hasLive(ValueRange values, const DenseSet<Value> &nonLiveSet,
return false;
}
+/// Return true iff at least one value in `values` is dead, given the liveness
+/// information in `la`.
+static bool hasDead(ValueRange values, const DenseSet<Value> &nonLiveSet,
+ RunLivenessAnalysis &la) {
+ for (Value value : values) {
+ if (nonLiveSet.contains(value)) {
+ LDBG() << "Value " << value << " is already marked non-live (dead)";
+ return true;
+ }
+
+ const Liveness *liveness = la.getLiveness(value);
+ if (!liveness) {
+ LDBG() << "Value " << value
+ << " has no liveness info, conservatively considered live";
+ continue;
+ }
+ if (liveness->isLive) {
+ LDBG() << "Value " << value << " is live according to liveness analysis";
+ continue;
+ } else {
+ LDBG() << "Value " << value << " is dead according to liveness analysis";
+ return true;
+ }
+ }
+ return false;
+}
+
/// Return a BitVector of size `values.size()` where its i-th bit is 1 iff the
/// i-th value in `values` is live, given the liveness information in `la`.
static BitVector markLives(ValueRange values, const DenseSet<Value> &nonLiveSet,
@@ -260,6 +287,17 @@ static SmallVector<OpOperand *> operandsToOpOperands(OperandRange operands) {
static void processSimpleOp(Operation *op, RunLivenessAnalysis &la,
DenseSet<Value> &nonLiveSet,
RDVFinalCleanupList &cl) {
+ if (hasDead(op->getOperands(), nonLiveSet, la)) {
+ LDBG() << "Simple op has dead operands, so the op must be dead: "
+ << OpWithFlags(op, OpPrintingFlags().skipRegions());
+ assert(!hasLive(op->getResults(), nonLiveSet, la) &&
+ "expected the op to have no live results");
+ cl.operations.push_back(op);
+ collectNonLiveValues(nonLiveSet, op->getResults(),
+ BitVector(op->getNumResults(), true));
+ return;
+ }
+
if (!isMemoryEffectFree(op) || hasLive(op->getResults(), nonLiveSet, la)) {
LDBG() << "Simple op is not memory effect free or has live results, "
"preserving it: "
diff --git a/mlir/test/Transforms/remove-dead-values.mlir b/mlir/test/Transforms/remove-dead-values.mlir
index 4bae85dcf4f7d..af157fc8bc5b0 100644
--- a/mlir/test/Transforms/remove-dead-values.mlir
+++ b/mlir/test/Transforms/remove-dead-values.mlir
@@ -118,6 +118,17 @@ func.func @main(%arg0 : i32) {
// -----
+// CHECK-LABEL: func.func private @clean_func_op_remove_side_effecting_op() {
+// CHECK-NEXT: return
+// CHECK-NEXT: }
+func.func private @clean_func_op_remove_side_effecting_op(%arg0: i32) -> (i32) {
+ // vector.print has a side effect but the op is dead.
+ vector.print %arg0 : i32
+ return %arg0 : i32
+}
+
+// -----
+
// %arg0 is not live because it is never used. %arg1 is not live because its
// user `arith.addi` doesn't have any uses and the value that it is forwarded to
// (%non_live_0) also doesn't have any uses.
|
|
@llvm/pr-subscribers-mlir Author: Matthias Springer (matthias-springer) ChangesThis commit fixes a crash in the With this commit, a "simple" operation is erased when it has a dead operand. If the operation were not dead, the liveness analysis would not have marked one of its operands as "dead". Full diff: https://github.com/llvm/llvm-project/pull/169269.diff 2 Files Affected:
diff --git a/mlir/lib/Transforms/RemoveDeadValues.cpp b/mlir/lib/Transforms/RemoveDeadValues.cpp
index 989c614ef6617..9d4d24c39c116 100644
--- a/mlir/lib/Transforms/RemoveDeadValues.cpp
+++ b/mlir/lib/Transforms/RemoveDeadValues.cpp
@@ -141,6 +141,33 @@ static bool hasLive(ValueRange values, const DenseSet<Value> &nonLiveSet,
return false;
}
+/// Return true iff at least one value in `values` is dead, given the liveness
+/// information in `la`.
+static bool hasDead(ValueRange values, const DenseSet<Value> &nonLiveSet,
+ RunLivenessAnalysis &la) {
+ for (Value value : values) {
+ if (nonLiveSet.contains(value)) {
+ LDBG() << "Value " << value << " is already marked non-live (dead)";
+ return true;
+ }
+
+ const Liveness *liveness = la.getLiveness(value);
+ if (!liveness) {
+ LDBG() << "Value " << value
+ << " has no liveness info, conservatively considered live";
+ continue;
+ }
+ if (liveness->isLive) {
+ LDBG() << "Value " << value << " is live according to liveness analysis";
+ continue;
+ } else {
+ LDBG() << "Value " << value << " is dead according to liveness analysis";
+ return true;
+ }
+ }
+ return false;
+}
+
/// Return a BitVector of size `values.size()` where its i-th bit is 1 iff the
/// i-th value in `values` is live, given the liveness information in `la`.
static BitVector markLives(ValueRange values, const DenseSet<Value> &nonLiveSet,
@@ -260,6 +287,17 @@ static SmallVector<OpOperand *> operandsToOpOperands(OperandRange operands) {
static void processSimpleOp(Operation *op, RunLivenessAnalysis &la,
DenseSet<Value> &nonLiveSet,
RDVFinalCleanupList &cl) {
+ if (hasDead(op->getOperands(), nonLiveSet, la)) {
+ LDBG() << "Simple op has dead operands, so the op must be dead: "
+ << OpWithFlags(op, OpPrintingFlags().skipRegions());
+ assert(!hasLive(op->getResults(), nonLiveSet, la) &&
+ "expected the op to have no live results");
+ cl.operations.push_back(op);
+ collectNonLiveValues(nonLiveSet, op->getResults(),
+ BitVector(op->getNumResults(), true));
+ return;
+ }
+
if (!isMemoryEffectFree(op) || hasLive(op->getResults(), nonLiveSet, la)) {
LDBG() << "Simple op is not memory effect free or has live results, "
"preserving it: "
diff --git a/mlir/test/Transforms/remove-dead-values.mlir b/mlir/test/Transforms/remove-dead-values.mlir
index 4bae85dcf4f7d..af157fc8bc5b0 100644
--- a/mlir/test/Transforms/remove-dead-values.mlir
+++ b/mlir/test/Transforms/remove-dead-values.mlir
@@ -118,6 +118,17 @@ func.func @main(%arg0 : i32) {
// -----
+// CHECK-LABEL: func.func private @clean_func_op_remove_side_effecting_op() {
+// CHECK-NEXT: return
+// CHECK-NEXT: }
+func.func private @clean_func_op_remove_side_effecting_op(%arg0: i32) -> (i32) {
+ // vector.print has a side effect but the op is dead.
+ vector.print %arg0 : i32
+ return %arg0 : i32
+}
+
+// -----
+
// %arg0 is not live because it is never used. %arg1 is not live because its
// user `arith.addi` doesn't have any uses and the value that it is forwarded to
// (%non_live_0) also doesn't have any uses.
|
|
I'm thinking that in this case, shouldn't we save the print? I believe we should save the print |
Why would you save the |
The function added in the test file is the print function parameter, so we should save vector.print.
Yes. I just had a thought: since the data flow analysis framework relies on dead-code analysis, a private function that is not called by any other public function will not be fully traversed(Since dead code analysis marks it as dead code, the data flow analysis framework will not reach it.). Here it will cause the function's parameter to be dead. You can see following example is run well. More example. #161471 .Previously, I suggested that the fix should involve directly removing the redundant private function, wrap the symbol-dce functionality in a function-based pass, enabling us to use it in other passes—including the remove-dead-code pass. But @ftynse felt it was not appropriate. Unfortunately, this bug still hasn't been fixed today. |
ftynse
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Indeed, I think we shouldn't just remove side-effecting operations, this changes the observable behavior of the program. Maybe in some separate -remove-otherwise-dead-side-effecting-operations-i'm-sure-what-i'm-doing pass if really needed. Prints may be innocuous, but this could also remove cf.assert %false or a write to invalid memory location, rendering an incorrect program correct.
|
Why does it change the observable behavior when dead operations are removed? |
|
@ftynse @joker-eph @linuxlonelyeagle What are the next steps here? Are you convinced that this fix is correct? If not, can you think of a case where this fix is incorrect? Summary:
[1] [2] |
|
Thank you for inviting me to participate in the discussion.
#158760, The problem you are solving should be similar to this one.
I have always suggested deleting useless private functions directly and introducing this feature in remove-dead-values. |
|
I took a look at #158760. It is indeed similar. It's the same problem that I am fixing here, but for branch ops. My fix from this PR here won't help because the op is not a "simple" op. I see 5 options to fix issues such as #158760 and the one from this PR:
I am not very happy with option 1 because it conceptually does not fit in well with As for option 2, the problem in #158760 is that the condition value of a Which brings me to option 3 |
Seems to me that this is a case where we would insert a "unreachable" terminator instead? |
78783ae to
d2349ca
Compare
I like this approach. This allows us to fix the I updated the pull request accordingly. |
d2349ca to
a013bc2
Compare
| if (op->hasTrait<OpTrait::IsTerminator>()) { | ||
| // When erasing a terminator, insert an unreachable op in its place. | ||
| OpBuilder b(op); | ||
| ub::UnreachableOp::create(b, op->getLoc()); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
| if (op->hasTrait<OpTrait::IsTerminator>()) { | |
| // When erasing a terminator, insert an unreachable op in its place. | |
| OpBuilder b(op); | |
| ub::UnreachableOp::create(b, op->getLoc()); | |
| } | |
| // When erasing a terminator, insert an unreachable op in its place. | |
| if (op->hasTrait<OpTrait::IsTerminator>()) | |
| ub::UnreachableOp::create(OpBuilder{op}, op->getLoc()); |
Nit: simplifying a bit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is giving me a compilation error:
llvm-project/mlir/lib/Transforms/RemoveDeadValues.cpp:832:7: error: no matching function for call to 'create'
832 | ub::UnreachableOp::create(OpBuilder{op}, op->getLoc());
| ^~~~~~~~~~~~~~~~~~~~~~~~~
llvm-project/build/tools/mlir/include/mlir/Dialect/UB/IR/UBOps.h.inc:377:24: note: candidate function not viable: expects an lvalue for 1st argument
377 | static UnreachableOp create(::mlir::OpBuilder &builder, ::mlir::Location location);
| ^ ~~~~~~~~~~~~~~~~~~~~~~~~~~
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's annoying, we should make it so that it is possible.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(not a blocker here)
linuxlonelyeagle
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I also think it's good to do this.
a013bc2 to
c08ef69
Compare
|
@ftynse Can I merge this? |
This commit fixes two crashes in the
-remove-dead-valuespass related to private functions.Private functions are considered entirely "dead" by the liveness analysis, which drives the
-remove-dead-valuespass.The
-remove-dead-valuespass removes dead block arguments from private functions. Private functions are entirely dead, so all of their block arguments are removed. However, the pass did not correctly update all users of these dropped block arguments.cf.cond_br.) Whenever a terminator is removed, aub.unrechableoperation is inserted. This fixes [MLIR] RemoveDeadValues pass errors when run onprivatefunction #158760.Depends on #169872.