llvm / llvm/llvm-project

[AMDGPU] Long-branch reserved SGPR pair is the return address s[30:31] in a callable function; s_getpc_b64/s_setpc_b64 clobber it and the function returns into itself

Open
#224,205 2 comments 0 reactions 0 assignees View on GitHub
backend:AMDGPU miscompilation
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

### Summary

In a callable (non-kernel) AMDGPU function whose branches exceed the 16-bit `s_cbranch` range, branch relaxation expands them to `s_getpc_b64` / `s_add_u32` / `s_addc_u32` / `s_setpc_b64` using the SGPR pair reserved by `GCNPreRALongBranchReg`. After RA that reservation is shifted down to the lowest "unused" SGPR pair, and in this function that pair is **`s[30:31]`, the return address**. Nothing saves or restores it, so the first long branch overwrites the return address and the function's `s_setpc_b64 s[30:31]` epilogue jumps back into the long-branch target instead of returning. On hardware the kernel spins forever at 100% GPU.

### Environment

- AMD clang version 22.0.0git (`/srcdest/rocm-llvm f58b06dce1f9c15707c5f808fd002e18c2accf7e`), ROCm 7.x toolchain on Arch Linux
- `-target amdgcn-amd-amdhsa -mcpu=gfx1101 -O3` (Radeon RX 7700 XT)
- The affected function is `contraction_forward_body`, `internal`, `noinline nounwind`, no calls, a leaf. It compiles to ~258 KB of code (0x2200..0x41210), well past the ±128 KB branch range.

### Reproducer

Full LLVM IR module (3.8 MB, generated by an ML runtime; a small synthetic function did not reach the reserved-register path because block placement turned the long branch conditional, so the real module is attached): https://gist.github.com/nm-z/b6177637549d5ed84367c67d9e664d5a (`recipe-amd-noinline.ll`, plus `callee-tail.s` with the disassembly excerpt).

```
B=/opt/rocm/amdgcn/bitcode
clang -target amdgcn-amd-amdhsa -mcpu=gfx1101 -O3 -nogpulib \
-Xclang -mlink-builtin-bitcode -Xclang $B/ocml.bc \
-Xclang -mlink-builtin-bitcode -Xclang $B/ockl.bc \
-Xclang -mlink-builtin-bitcode -Xclang $B/oclc_abi_version_600.bc \
-Xclang -mlink-builtin-bitcode -Xclang $B/oclc_finite_only_off.bc \
-Xclang -mlink-builtin-bitcode -Xclang $B/oclc_unsafe_math_off.bc \
-Xclang -mlink-builtin-bitcode -Xclang $B/oclc_isa_version_1101.bc \
-x ir recipe-amd-noinline.ll -o out.hsaco
llvm-objdump -d out.hsaco | awk '/:/{on=1} on&&/:/{on=0} on' | grep -n 's_getpc_b64'
```

### Observed

Five long branches inside `contraction_forward_body` use the return-address pair (the other three use `s[2:3]` / `s[10:11]`):

```
67: s_getpc_b64 s[2:3]
15323: s_getpc_b64 s[30:31]
16126: s_getpc_b64 s[10:11]
17027: s_getpc_b64 s[30:31]
17062: s_getpc_b64 s[30:31]
17117: s_getpc_b64 s[30:31]
30969: s_getpc_b64 s[2:3]
45141: s_getpc_b64 s[30:31]
```

There is no `v_writelane_b32 vN, s30/s31`, `v_readlane_b32 s30/s31`, or any other save/restore of `s30`/`s31` anywhere in the function. The function's tail:

```
s_and_not1_saveexec_b32 s8, s13
s_cbranch_execz 6
s_getpc_b64 s[30:31]
s_add_u32 s30, s30, 0xfffd6744
s_addc_u32 s31, s31, -1
s_setpc_b64 s[30:31] ; long branch, return address gone
s_or_b32 exec_lo, exec_lo, s8
s_waitcnt lgkmcnt(0)
s_setpc_b64 s[30:31] ; "return" -> jumps to the long-branch target
```

and one of the sites in the body (0x17920 -> 0x41204, which is the `s_or_b32 exec_lo, exec_lo, s8` right before the epilogue above):

```
s_and_not1_saveexec_b32 s8, s13
s_cbranch_execnz 6
s_getpc_b64 s[30:31]
s_add_u32 s30, s30, 0x298e4
s_addc_u32 s31, s31, 0
s_setpc_b64 s[30:31]
```

Runtime symptom: the calling kernel never gets past the call. Instrumenting the callee with per-lane atomic counters showed every lane reaches the callee's final block, yet no wave ever arrives at the caller's next grid barrier; all waves spin at 100% GPU utilisation. With the callee force-inlined (`alwaysinline`) the same module runs correctly.

### Analysis

- `GCNPreRALongBranchReg` reserves the highest free SGPR pair when it estimates a long branch.
- `SIFrameLowering.cpp` then does: `// We initally reserved the highest available SGPR pair for long branches now, after RA, we shift down to a lower unused one if one exists` and calls `TRI->findUnusedRegister(MRI, &AMDGPU::SGPR_64RegClass, MF)`.
- `findUnusedRegister` only checks `MRI.isAllocatable(Reg) && !MRI.isPhysRegUsed(Reg) && !isVCC(Reg)`. In a non-entry function `SGPR30_SGPR31` has no operands at that point (the return is still `SI_RETURN`; the `S_SETPC_B64_return $sgpr30_sgpr31` is only materialised in the epilogue), so it is reported unused. With `s0..s29` occupied by arguments and uniform values, it is the first pair the ascending scan finds.
- `SIInstrInfo::insertIndirectBranch` then uses the reserved pair unconditionally (`if (LongBranchReservedReg) { ... Scav = LongBranchReservedReg; }`), skipping the liveness-aware scavenger and the spill path. BranchRelaxation runs after PEI, so nothing ever saves `s[30:31]`.

### Confirmation

Compiling the same module with `-mllvm -amdgpu-long-branch-factor=0` (no pre-RA reservation, scavenger path) produces the same five sites using `s[0:1]`, which is dead at those points, and leaves `s[30:31]` untouched:

```
15323: s_getpc_b64 s[0:1]
17027: s_getpc_b64 s[0:1]
17062: s_getpc_b64 s[0:1]
17117: s_getpc_b64 s[0:1]
45141: s_getpc_b64 s[0:1]
```

### Expected

The post-RA shift-down should never pick a register that is implicitly live throughout a callable function, at minimum the return address `SGPR30_SGPR31` (and it should probably respect callee-saved registers the same way `determinePrologEpilogSGPRSaves` does by pre-marking CSRs as live), or the reserved pair should be spilled around the long branch like the scavenger fallback does.

Contributor guide

Open the contributing guide

Research direction

Read SIFrameLowering.cpp around the post-RA findUnusedRegister shift, and SIInstrInfo::insertIndirectBranch, using GCNPreRALongBranchReg and determinePrologEpilogSGPRSaves as entry points. Build the attached IR with the shown gfx1101 command and inspect long-branch registers with llvm-objdump. Done when callable functions preserve s[30:31] for returns or use a safe fallback.

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
Active
Clarity
Clearly specified
Newbie friendliness
45/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.