llvm / llvm/llvm-project

[NVPTX] Since the default-SM bump (#176021), SimplifyCFG builds lookup tables of .shared addresses — llc silently emits PTX that ptxas rejects

Open
#219,793 1 comment 0 reactions 1 assignee Claimed by @AlexMaclean View on GitHub
backend:NVPTX llvm:transforms
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

This was discovered while moving our Rust to PTX compiler (cuda-oxide) from LLVM 22 → 23 and this one is a correctness bug rather than perf: a Blackwell GEMM example started failing at module load with `CUDA_ERROR_INVALID_PTX` (218). Identical IR in; the only change was which `opt` ran the `default` pipeline.

The kernel has a classic staged-pipeline shape: `stage = i & 3`, then a 4-arm match handing back pointers to per-stage shared-memory buffers. LLVM 23's `SimplifyCFG` now converts that switch into a constant lookup table:

```llvm
@switch.table.stage_select = private unnamed_addr constant [4 x ptr addrspace(3)]
[ptr addrspace(3) @buf0, ptr addrspace(3) @buf1,
ptr addrspace(3) @buf2, ptr addrspace(3) @buf3], align 4
```

and llc prints it without complaint i.e. exit 0, empty stderr:

```
.visible .shared .align 16 .b8 buf0[4096];
...
.global .align 4 .u64 switch_$_table_$_stage_select[4] = {buf0, buf1, buf2, buf3};
```

ptxas simply rejects it, correctly i.e. a `.shared` address is a per-CTA runtime value, not a link-time constant:

```
ptxas out.ptx, line 14; fatal : Variable used as initial value not in .global or .const state space
```

(With generic pointers in the phi the initializers come out as `generic(buf0)` and fail the same way; with internal-linkage shared symbols the message is `Invalid initial value symbol 'buf0'`.) If the PTX is JIT-compiled at runtime instead, none of this surfaces at build time — the driver just hands back 218 at `cuModuleLoad`, which is where we started.

The 22 → 23 change is #176021 (default SM sm_30 → sm_75). That raises the default PTX floor past 6.0, so `hasBrx()` goes true, `BR_JT` becomes Legal (from #102550), and `BasicTTIImpl::shouldBuildLookupTables()` flips from false to true for any module optimized without `-mcpu`. From there nothing stops the table: `validLookupTableConstant` accepts any `GlobalValue` with no address-space check. The bug was already latent, the default just changed:

```
opt -passes='default' on the repro below switch.table globals
LLVM 22, no -mcpu 0
LLVM 23, no -mcpu 2
LLVM 22, -mcpu=sm_90 2 <- latent since brx.idx (LLVM 20)
```

Full repro, 33 lines (tested with the rust-bundled 23.1.0, but nothing rust-specific is involved — plain `opt`/`llc` on this file):

```llvm
target datalayout = "e-p3:32:32-p4:32:32-p5:32:32-p6:32:32-p7:32:32-i64:64-i128:128-v16:16-v32:32-n16:32:64"
target triple = "nvptx64-nvidia-cuda"

@buf0 = addrspace(3) global [4096 x i8] undef, align 16
@buf1 = addrspace(3) global [4096 x i8] undef, align 16
@buf2 = addrspace(3) global [4096 x i8] undef, align 16
@buf3 = addrspace(3) global [4096 x i8] undef, align 16

define ptx_kernel void @stage_select(ptr %out, i32 %i) {
entry:
%stage = and i32 %i, 3
switch i32 %stage, label %bb0 [
i32 1, label %bb1
i32 2, label %bb2
i32 3, label %bb3
]

bb0:
br label %done
bb1:
br label %done
bb2:
br label %done
bb3:
br label %done

done:
%buf = phi ptr addrspace(3) [ @buf0, %bb0 ], [ @buf1, %bb1 ], [ @buf2, %bb2 ], [ @buf3, %bb3 ]
%gen = addrspacecast ptr addrspace(3) %buf to ptr
store volatile i8 1, ptr %gen, align 1
store ptr %gen, ptr %out, align 8
ret void
}
```

```
opt -passes='default' stage_select.ll -S -o out.ll # @switch.table.* of addrspace(3) appears
llc -march=nvptx64 -mcpu=sm_90 out.ll -o out.ptx # exit 0, no diagnostic
ptxas -arch=sm_90 out.ptx -o /dev/null # fatal, line 14
```

I looked around a bit for prior art:

* #159748 already disables *relative* lookup tables on NVPTX (`shouldBuildRelLookupTables()` = false) for essentially this class of reason, but absolute tables have no equivalent guard. That seems like the natural shape of a fix: an NVPTX override of `shouldBuildLookupTablesForConstant` rejecting constants that reference non-`global`/`const` address spaces (or an address-space check in `validLookupTableConstant` itself).
* #65806 is the only other upstream record of this ptxas message - different mechanism (clang, dynamic shared memory), same underlying constraint.
* rustc's `nvptx64-nvidia-cuda` target pins `"cpu": "sm_70"`, so rustc-compiled kernels have been in the latent-exposed camp since brx.idx landed. We suspect nobody hit it because the triggering shape (a dense ≥4-arm switch where every arm feeds a distinct shared global into one phi) mostly shows up in staged-pipeline code.

**Our workaround:** we pass `-switch-to-lookup=false` to `opt`. That's actually fine for us, the switches lower to `brx.idx` over `.branchtargets` code labels, which is legal PTX, and we'd rather not have a dependent `.global` load on a hot path anyway, but it's module-wide and also turns off perfectly good scalar tables.

**Two things seem worth fixing upstream:** SimplifyCFG shouldn't build a table the target can't materialize as a data initializer, and independently, the NVPTX asm printer emitting a `.global` initializer that references a `.shared` symbol with exit 0 turned a compile-time bug into a runtime JIT failure i.e. a hard error there would probably be better.

cc @Artem-B, and @justinfargnoli since #176021 surfaced it.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.