llvm / llvm/llvm-project

[SPIR-V] SPIRVStructurizer emits multiple merge instructions into one block, producing invalid SPIR-V for irreducible CFGs

Open
#222,555 0 comments 0 reactions 0 assignees View on GitHub
backend:SPIR-V
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary

For an irreducible CFG resolved by LLVM's own `fix-irreducible` pass, the SPIR-V backend emits a block containing **both** `OpLoopMerge` and `OpSelectionMerge`. This places `OpLoopMerge` third-to-last, violating the SPIR-V requirement that it be the second-to-last instruction in its block, immediately preceding the branch. `spirv-val` rejects the module.

Reproduced identically on **LLVM 20.1.2** and **LLVM 21.1.8**.

cc @michalpaszkowski @VyacheslavLevytskyy

### Reproducer

`irreducible.ll` — blocks `A` and `B` form a two-entry SCC (both reachable from `entry`), with `acc` and `i` live across the edges structurization must transform:

```llvm
target triple = "spirv-unknown-vulkan1.3"

@buf = internal addrspace(10) global [64 x i32] zeroinitializer
@n = internal addrspace(10) global i32 0
@seed = internal addrspace(10) global i32 0
@out = internal addrspace(10) global i32 0

define void @main() #0 {
entry:
%n = load i32, ptr addrspace(10) @n
%sd = load i32, ptr addrspace(10) @seed
%p = and i32 %sd, 1
%c0 = icmp eq i32 %p, 0
br i1 %c0, label %A, label %B
A:
%iA = phi i32 [ 0, %entry ], [ %iB2, %Bbody ]
%accA = phi i32 [ 0, %entry ], [ %accB2, %Bbody ]
%ca = icmp sge i32 %iA, %n
br i1 %ca, label %done, label %Abody
Abody:
%ga = getelementptr [64 x i32], ptr addrspace(10) @buf, i32 0, i32 %iA
%va = load i32, ptr addrspace(10) %ga
%m = mul i32 %va, 7
%accA2 = add i32 %accA, %m
%iA2 = add i32 %iA, 1
br label %B
B:
%iB = phi i32 [ 0, %entry ], [ %iA2, %Abody ]
%accB = phi i32 [ 0, %entry ], [ %accA2, %Abody ]
%cb = icmp sge i32 %iB, %n
br i1 %cb, label %done, label %Bbody
Bbody:
%gb = getelementptr [64 x i32], ptr addrspace(10) @buf, i32 0, i32 %iB
%vb = load i32, ptr addrspace(10) %gb
%s = add i32 %vb, %accB
%accB2 = xor i32 %accB, %s
%iB2 = add i32 %iB, 1
br label %A
done:
%r = phi i32 [ %accA, %A ], [ %accB, %B ]
store i32 %r, ptr addrspace(10) @out
ret void
}
attributes #0 = { "hlsl.shader"="compute" "hlsl.numthreads"="1,1,1" }
```

```
opt -passes=fix-irreducible -S irreducible.ll -o armA.ll
llc -mtriple=spirv-unknown-vulkan1.3 -filetype=obj armA.ll -o armA.spv
spirv-val --target-env vulkan1.3 armA.spv
```

### Actual

```
error: line 85: OpLoopMerge must immediately precede either an OpBranch or
OpBranchConditional instruction. OpLoopMerge must be the second-to-last
instruction in its block.
OpLoopMerge %new_exit %Abody None
```

The emitted block:

```
%irr_guard = OpLabel
... 6 x OpPhi ...
OpLoopMerge %new_exit %Abody None
OpSelectionMerge %new_header_new_exit None
OpBranchConditional %Guard_B_reg2mem_0 %B %new_header_new_exit
```

### Expected

A `spirv-val`-clean module, or a diagnostic — not silently invalid SPIR-V.

### Analysis — one root cause, two symptoms

`SPIRVStructurizer` emits **more than one merge declaration into a single block** when a block plays two structural roles, instead of splitting the block so each construct owns one.

**Symptom 1 — `fix-irreducible` (dispatch) resolution.** The `irr.guard` block is simultaneously a loop header and a selection point, and receives both `OpLoopMerge` and `OpSelectionMerge`. Diagnosed as *"OpLoopMerge must be the second-to-last instruction."*

**Symptom 2 — duplication (node-splitting) resolution.** A hand-written node-split form of the same SCC — genuinely reducible, `fix-irreducible` inert on it — puts **two `OpSelectionMerge`** into `%entry`, with an unrelated instruction between them, and the two constructs then collide on the same merge target. Diagnosed as *"Block '%done' is already a merge block for another header."*

Both forms show exactly **one block with more than one merge declaration**.

### The CFG is expressible — only the emitter is wrong

Splitting the composite header by hand makes the module valid:

```
%irr_guard = OpLabel
... OpPhi ...
OpLoopMerge %new_exit %Abody None
OpBranch %sel_hdr ; <-- inserted
%sel_hdr = OpLabel ; <-- inserted
OpSelectionMerge %new_header_new_exit None
OpBranchConditional %Guard_B... %B %new_header_new_exit
```

with the affected `OpPhi` incoming labels rewired from `%irr_guard` to `%sel_hdr`. `spirv-val --target-env vulkan1.3` then **passes**.

**Suggested fix:** when a block would receive a second merge declaration, split it — keep the outer construct's merge with an `OpBranch` to a fresh block that carries the inner construct's merge and the original terminator — and update `OpPhi` predecessors accordingly.

### Environment

| | |
|---|---|
| LLVM | 20.1.2 (Ubuntu 24.04) and 21.1.8 (upstream release tarball) — identical output |
| SPIRV-Tools | v2025.1 and v2025.5 — identical diagnosis |
| Target | `spirv-unknown-vulkan1.3` |
| Host | x86_64 Linux |

Emitted SPIR-V is structurally identical between 20.1.2 and 21.1.8: 10 `OpLabel`, 2 `OpSelectionMerge`, 1 `OpLoopMerge`, 13 `OpPhi`, 127 instructions, 2256 bytes.

### Additional notes

- Without `fix-irreducible`, `llc` aborts on the same input with `LLVM ERROR: No valid candidate in the queue. Is the graph reducible?` — expected, since the structurizer requires reducible input.
- **21 IR-level preparation recipes** were tried between the resolution pass and `llc` (`unify-loop-exits`, `simplifycfg`, `loop-simplify`, `break-crit-edges`, `reg2mem`, `instcombine`, `lcssa`, `loop-rotate`, `sroa`, `jump-threading`, `adce`, and combinations) across both resolution forms. Every one that reaches `llc` fails at the same instruction. Full logs available on request.
- **Control:** an ordinary reducible multi-exit compute shader lowers and validates cleanly on the same toolchain, so the backend is sound on unremarkable CFGs.
- Differential execution of a host-form counterpart shows `fix-irreducible` preserving semantics on this kernel, so the input to the backend is well-formed.

### Note on the target triple

`spirv64-unknown-vulkan1.3` emits `OpCapability Kernel` / `OpMemoryModel Physical64 OpenCL`, which `spirv-val --target-env vulkan1.3` rejects outright; only `spirv-unknown-vulkan1.3` produced Shader/Logical output on these builds. That may be expected behaviour, but it was not obvious from the documentation and is noted here in case it is a separate issue.

Contributor guide

Open the contributing guide

Research direction

Start with SPIRVStructurizer and reproduce the failure using irreducible.ll, opt, llc, and spirv-val. Inspect the emitted %irr_guard block and the node-split form for multiple merge declarations, then validate that the resulting module is accepted by spirv-val or that an appropriate diagnostic is produced.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.