[MLIR][Affine] affine-loop-fusion crashes in Operation::moveBefore after replacing the destination loop
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
## Summary
Running `affine-loop-fusion` on the verifier-valid program below crashes `mlir-opt`.
During sibling fusion, promotion of a cloned single-iteration reduction replaces the destination parent loop. The fusion code subsequently attempts to relocate the old, detached destination operation using `Operation::moveBefore`.
With assertions enabled, MLIR reports that the operation being moved is not contained in a block. With assertions disabled, `mlir-opt` segfaults inside `Operation::moveBefore`.
Public reproducer:
https://godbolt.org/z/Ex8qfMdn8
## 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() -> i32 {
%c7_i64 = arith.constant 7 : i64
%c3_i64 = arith.constant 3 : i64
%c0_i64 = arith.constant 0 : i64
%c97_i64 = arith.constant 97 : i64
%c1_i64 = arith.constant 1 : i64
%alloc = memref.alloc() : memref<8xi64>
%alloc_0 = memref.alloc() : memref<8xi64>
%alloc_1 = memref.alloc() : memref<8xi64>
affine.for %arg0 = 0 to 8 {
%4 = arith.index_cast %arg0 : index to i64
%5 = arith.addi %4, %c1_i64 : i64
%6 = arith.remsi %5, %c97_i64 : i64
affine.store %6, %alloc[%arg0] : memref<8xi64>
}
affine.for %arg0 = 0 to 8 {
affine.store %c0_i64, %alloc_0[%arg0] : memref<8xi64>
}
affine.for %arg0 = 0 to 8 {
affine.store %c0_i64, %alloc_1[%arg0] : memref<8xi64>
}
affine.for %arg0 = 0 to 8 {
%4 = affine.load %alloc[%arg0] : memref<8xi64>
%5 = arith.muli %4, %c3_i64 : i64
affine.store %5, %alloc_0[%arg0] : memref<8xi64>
}
affine.for %arg0 = 0 to 8 {
%4 = affine.load %alloc_0[%arg0] : memref<8xi64>
%5 = arith.addi %4, %c7_i64 : i64
affine.store %5, %alloc_1[%arg0] : memref<8xi64>
}
%0 = affine.for %arg0 = 0 to 8
iter_args(%arg1 = %c0_i64) -> (i64) {
%4 = affine.load %alloc_0[%arg0] : memref<8xi64>
%5 = arith.addi %arg1, %4 : i64
affine.yield %5 : i64
}
%1 = affine.for %arg0 = 0 to 8
iter_args(%arg1 = %c0_i64) -> (i64) {
%4 = affine.load %alloc_1[%arg0] : memref<8xi64>
%5 = arith.addi %arg1, %4 : i64
affine.yield %5 : i64
}
%2 = arith.addi %0, %1 : i64
%3 = arith.trunci %2 : i64 to i32
memref.dealloc %alloc : memref<8xi64>
memref.dealloc %alloc_0 : memref<8xi64>
memref.dealloc %alloc_1 : memref<8xi64>
return %3 : i32
}
}
```
## Commands
The input verifies successfully:
```sh
mlir-opt input.mlir \
-o /dev/null
```
Running Affine loop fusion crashes:
```sh
mlir-opt input.mlir \
--affine-loop-fusion \
-o /dev/null
```
## Actual behavior
### Assertions enabled
`mlir-opt` exits with status 134 (`SIGABRT`) and reports:
```text
Operation.cpp:555: Operation::moveBefore(Block*, iterator):
Assertion `getBlock() &&
"cannot move an operation that isn't contained in a block"' failed.
```
Relevant stack:
```text
mlir::Operation::moveBefore(mlir::Operation*)
(anonymous namespace)::GreedyFusion::fuseWithSiblingNodes(...)
(anonymous namespace)::GreedyFusion::fuseSiblingNodes()
(anonymous namespace)::LoopFusion::runOnBlock(...)
(anonymous namespace)::LoopFusion::runOnOperation()
mlir::detail::OpToOpPassAdaptor::run(...)
mlir::PassManager::runPasses(...)
```
### Assertions disabled
Compiler Explorer reports:
```text
Program terminated with signal: SIGSEGV
Compiler returned: 139
```
Relevant public backtrace:
```text
mlir::Operation::moveBefore(mlir::Operation*)
(anonymous namespace)::GreedyFusion::fuseWithSiblingNodes(...)
(anonymous namespace)::GreedyFusion::fuseSiblingNodes()
(anonymous namespace)::LoopFusion::runOnBlock(...)
(anonymous namespace)::LoopFusion::runOnOperation()
mlir::detail::OpToOpPassAdaptor::run(...)
mlir::detail::OpToOpPassAdaptor::runPipeline(...)
mlir::PassManager::run(...)
```
The selected sibling fusion clones the reduction that reads `%alloc_0` into the loop that reads `%alloc_0` and writes `%alloc_1`.
Promotion of the cloned single-iteration reduction replaces the destination parent loop. However, the destination node retained by the fusion logic still refers to the old destination operation.
Immediately before the failing `moveBefore` call:
```text
operation being moved: old destination affine.for
moved operation block: null
insertion-point block: non-null
```
The old detached destination is then used as:
```text
dstForInst->moveBefore(insertPointInst)
```
The assertions build detects that the moved operation is no longer contained in a block. The assertions-disabled build proceeds into the intrusive-list relocation code and segfaults.
No valid transformed target is emitted in either configuration.
## Expected behavior
After fusion replaces a destination loop, every retained reference to that destination should be updated to refer to the attached replacement operation before any relocation or subsequent graph processing.
Alternatively, the relocation should be skipped or recomputed using the replacement loop.
`affine-loop-fusion` must not pass a detached operation to `Operation::moveBefore` or crash while processing verifier-valid input.
## Validity
The original input passes MLIR verification.
All loop bounds, memory accesses, reduction results, and external SSA uses are well formed. The input contains three allocated `memref<8xi64>` buffers, and all indexed accesses use values in the range 0 through 7.
The failure occurs during `affine-loop-fusion`; no valid transformed target is emitted.
The assertions-enabled and assertions-disabled failures are two consequences of the same invalid compiler state: relocation is attempted using an old destination loop that is no longer attached to a block.
Contributor guide
Research direction
Start at GreedyFusion::fuseWithSiblingNodes and trace destination replacement through LoopFusion::runOnBlock, focusing on the retained destination before Operation::moveBefore. Reproduce with input.mlir using mlir-opt --affine-loop-fusion; done means verifier-valid input completes without passing a detached operation to moveBefore or crashing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 52/100