[MLIR][Affine] affine-loop-fusion crashes after erasing a result-producing loop with live uses
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Running `affine-loop-tile` followed by `affine-loop-fusion` on the verifier-valid program below crashes `mlir-opt`.
Sibling fusion clones a result-producing `affine.for`, but does not replace the external use of the original loop result. It then erases the original loop while the result is still used by:
```mlir
%sum = arith.addi %s0, %s1 : i64
```
With assertions enabled, MLIR reports that the `affine.for` operation is being destroyed while it still has uses. With assertions disabled, the pass leaves a dangling SSA operand and subsequently segfaults during post-pass verification.
Public reproducer:
https://godbolt.org/z/fxW4GcaTa
## Version
Tested at `llvm/llvm-project` commit: `450cf0aafeab9fed6666b112b018d3913a36527c`
LLVM version:
```text
LLVM 24.0.0git
```
The failure reproduces deterministically in Release builds with assertions both enabled and disabled.
It also reproduces on Compiler Explorer MLIR trunk.
## Reproducer
```mlir
module {
func.func @main() -> i64 {
%A = memref.alloc() : memref<4xi64>
%B = memref.alloc() : memref<4xi64>
%C = memref.alloc() : memref<4xi64>
affine.for %i = 0 to 4 {
%a = arith.constant 3 : i64
affine.store %a, %A[%i] : memref<4xi64>
}
%z0 = arith.constant 0 : i64
affine.for %i = 0 to 4 {
affine.store %z0, %B[%i] : memref<4xi64>
}
%z1 = arith.constant 0 : i64
affine.for %i = 0 to 4 {
affine.store %z1, %C[%i] : memref<4xi64>
}
affine.for %i = 0 to 4 {
%v = affine.load %A[%i] : memref<4xi64>
%k = arith.constant 3 : i64
%p = arith.muli %v, %k : i64
affine.store %p, %B[%i] : memref<4xi64>
}
affine.for %i = 0 to 4 {
%v = affine.load %B[%i] : memref<4xi64>
%k = arith.constant 7 : i64
%q = arith.addi %v, %k : i64
affine.store %q, %C[%i] : memref<4xi64>
}
%za = arith.constant 0 : i64
%s0 = affine.for %i = 0 to 4
iter_args(%a = %za) -> (i64) {
%v = affine.load %B[%i] : memref<4xi64>
%na = arith.addi %a, %v : i64
affine.yield %na : i64
}
%zb = arith.constant 0 : i64
%s1 = affine.for %i = 0 to 4
iter_args(%a = %zb) -> (i64) {
%v = affine.load %C[%i] : memref<4xi64>
%na = arith.addi %a, %v : i64
affine.yield %na : i64
}
%sum = arith.addi %s0, %s1 : i64
memref.dealloc %A : memref<4xi64>
memref.dealloc %B : memref<4xi64>
memref.dealloc %C : memref<4xi64>
return %sum : i64
}
}
```
## Commands
The input verifies successfully:
```sh
mlir-opt input.mlir \
-o /dev/null
```
Tiling alone also succeeds:
```sh
mlir-opt input.mlir \
--affine-loop-tile=tile-size=8 \
-o tiled.mlir
mlir-opt tiled.mlir \
-o /dev/null
```
Running tiling followed by fusion crashes:
```sh
mlir-opt input.mlir \
--affine-loop-tile=tile-size=8 \
--affine-loop-fusion \
-o /dev/null
```
## Actual behavior
### Assertions enabled
`mlir-opt` exits with status 134 (`SIGABRT`) and reports:
```text
'affine.for' op operation destroyed but still has uses
remaining use: %sum = arith.addi %s0, %s1 : i64
LLVM ERROR: operation destroyed but still has uses
```
Relevant stack:
```text
mlir::Operation::~Operation()
llvm::iplist_impl<...>::erase(...)
(anonymous namespace)::GreedyFusion::fuseSiblingNodes()
(anonymous namespace)::LoopFusion::runOnOperation()
mlir::detail::OpToOpPassAdaptor::run(...)
mlir::PassManager::runPasses(...)
```
The `%s0` loop is a result-producing reduction:
```mlir
%s0 = affine.for %i = 0 to 4
iter_args(%a = %za) -> (i64) {
%v = affine.load %B[%i] : memref<4xi64>
%na = arith.addi %a, %v : i64
affine.yield %na : i64
}
```
Its result has the external use:
```mlir
%sum = arith.addi %s0, %s1 : i64
```
Sibling fusion clones the reduction into another loop nest, but the cloned loop result is not used to replace `%s0`. The original `%s0` loop is then erased while `%sum` still uses its result.
### Assertions disabled
`mlir-opt` exits with status 139 (`SIGSEGV`). Compiler Explorer reports:
```text
Program terminated with signal: SIGSEGV
Compiler returned: 139
```
Relevant public backtrace:
```text
mlir::Type::getContext() const
(anonymous namespace)::OperationVerifier::verifyOnEntrance(mlir::Operation&)
(anonymous namespace)::OperationVerifier::verifyOperation(mlir::Operation&)
mlir::verify(mlir::Operation*, bool)
mlir::detail::OpToOpPassAdaptor::run(...)
mlir::detail::OpToOpPassAdaptor::runPipeline(...)
mlir::PassManager::run(mlir::Operation*)
```
Without the assertion, destruction proceeds and leaves the final `arith.addi` with a dangling operand. Post-pass verification then segfaults while inspecting the invalid SSA value.
No valid transformed target is emitted in either configuration.
## Expected behavior
`affine-loop-fusion` must not erase a result-producing source loop while any of its results still have external SSA uses.
The pass should either:
- replace every external use with an equivalent result produced by the fused computation;
- preserve the original result-producing loop; or
- decline the fusion.
The pass must not leave dangling SSA operands or crash the verifier.
## Validity
The original input passes MLIR verification.
Both result-producing loops are valid `affine.for` operations with explicit `iter_args`, yielded `i64` values, and well-formed external uses. The final `arith.addi` legally consumes the two loop results.
Tiling alone succeeds and produces verifier-valid IR. The external uses of `%s0` and `%s1` exist before tiling and remain valid after tiling alone.
The invalid state is introduced only when `affine-loop-fusion` erases the original `%s0` loop without replacing its externally used result.
Contributor guide
Research direction
Start with the GreedyFusion::fuseSiblingNodes() entry point identified in the stack and reproduce the failure using the provided MLIR input with affine-loop-tile followed by affine-loop-fusion. Compare the result-producing loop's external uses before and after fusion. Done means the pass neither destroys a loop with live uses nor leaves dangling operands, and post-pass verification succeeds.
Written by the indexing model from the issue text.
Assessment
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 57/100