llvm / llvm/llvm-project

[MLIR][Affine] affine-loop-fusion crashes after erasing a result-producing loop with live uses

Open
#212,046 2 comments 0 reactions 0 assignees View on GitHub
crash mlir
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.