[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
- 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
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