llvm / llvm/llvm-project

[MLIR][Affine] affine-loop-fusion miscompiles when an external func.call uses a memref.cast alias

Open
#211,599 3 comments 0 reactions 0 assignees View on GitHub
miscompilation mlir
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

`affine-loop-fusion` moves an external call that uses a cast alias from between the producer and consumer loops to before the producer, and then fuses the loops. This changes the program semantics.

Public reproducer: https://www.compiler-explorer.com/z/K3MErMWKc

Tested locally at `llvm/llvm-project` commit `450cf0aafeab9fed6666b112b018d3913a36527c` (`LLVM 24.0.0git`).

## Reproducer

```mlir
module {
func.func private @escape(memref)
func.func @control(%in: memref<32xf64>, %comm: memref<32xf64>, %out: memref<32xf64>) {
affine.for %i = 0 to 16 {
%a = affine.load %in[%i] : memref<32xf64>
%b = arith.addf %a, %a : f64
affine.store %b, %comm[%i] : memref<32xf64>
}
%view = memref.cast %comm : memref<32xf64> to memref
func.call @escape(%view) : (memref) -> ()
affine.for %j = 0 to 16 {
%c = affine.load %comm[%j] : memref<32xf64>
%d = arith.addf %c, %c : f64
affine.store %d, %out[%j] : memref<32xf64>
}
return
}
}
```

## Command

```sh
mlir-opt dc8.mlir \
--pass-pipeline='builtin.module(func.func(affine-loop-fusion{mode=producer maximal}))'
```

## Result

The pass emits:

```mlir
module {
func.func private @escape(memref)
func.func @control(%arg0: memref<32xf64>, %arg1: memref<32xf64>, %arg2: memref<32xf64>) {
%cast = memref.cast %arg1 : memref<32xf64> to memref
call @escape(%cast) : (memref) -> ()
affine.for %arg3 = 0 to 16 {
%0 = affine.load %arg0[%arg3] : memref<32xf64>
%1 = arith.addf %0, %0 : f64
affine.store %1, %arg1[%arg3] : memref<32xf64>
%2 = affine.load %arg1[%arg3] : memref<32xf64>
%3 = arith.addf %2, %2 : f64
affine.store %3, %arg2[%arg3] : memref<32xf64>
}
return
}
}
```

In the source, the producer fills `%comm`, then `@escape` modifies it, and only then does the consumer read it. In the transformed program, `@escape` runs before the producer, whose stores overwrite the call's results before the consumer reads them.

For a legal synchronous implementation that writes `comm[k] = 8192 + k` for `k = 0..15`, the source produces `out[0] = 16384`, while the transformed program produces `out[0] = 4`.

Both the source and transformed IR verify. The concrete execution uses initialized, distinct buffers, in-bounds accesses, and a synchronous non-retaining callee.

Contributor guide

Open the contributing guide

Research direction

Start by running the supplied MLIR reproducer with mlir-opt and the affine-loop-fusion pass pipeline, then inspect how the producer loop, memref.cast alias, external func.call, and consumer loop are ordered. Done means the pass no longer produces a transformation that moves the call before the producer and changes the program's observable result.

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
Mostly clear
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.