GPU hang (Xid 109 CTX SWITCH TIMEOUT) from a data-dependent half4 store over a large compute dispatch — Vulkan cooperative-vector path, RTX 5070 Ti, driver 610.43.02
Summary
A compute shader that performs a data-dependent (branched) store of a 16-bit
half4 into a storage buffer, dispatched over a large grid (~16.7 million
elements), reliably hangs the GPU. The kernel becomes non-preemptible; the
driver kills the channel with NVRM: Xid 109 ... CTX SWITCH TIMEOUT, and
every subsequent CPU-side fence wait on that queue blocks forever (the
application appears to “load endlessly”).
Making the identical store unconditional (so the address is always
written, the branch is removed) completely eliminates the hang with no other
change. The data, dispatch dimensions, buffer, and surrounding code are
otherwise identical.
This looks like a driver/compiler bug in handling a divergent buffer store on
the cooperative-vector code path, not an application error: the shader is valid
SPIR-V, the access is in-bounds, and the only difference between “hangs” and
“works” is whether the store is guarded by a runtime if.
Environment
| GPU | NVIDIA GeForce RTX 5070 Ti (Blackwell) |
| Driver | 610.43.02 |
| Kernel modules | NVIDIA open kernel modules (Dual MIT/GPL) |
| OS / kernel | Arch Linux, kernel 7.0.11-arch1-1 (x86_64) |
| Graphics API | Vulkan, apiVersion 1.4.341 |
| Relevant extension | VK_NV_cooperative_vector (extension revision 4) |
| Shader toolchain | Slang 2025.23 → SPIR-V (capability spvCooperativeVectorNV) |
| Workload | Tiny-MLP training (cooperative-vector matmul) + Adam optimizer over a hash-grid parameter table |
Symptom (from dmesg)
NVRM: Xid (PCI:...): 109, pid=<...>, Ch 0x5, ... CTX SWITCH TIMEOUT
NVRM: Xid (PCI:...): 109, pid=<...>, Ch 0x8, ... CTX SWITCH TIMEOUT
The channel running the offending compute dispatch fails to context-switch
within the timeout, so the driver tears it down. Any CPU thread blocked on a
fence/semaphore for that submission never wakes — from the application side it
manifests as a permanent hang during what should be a sub-millisecond compute
pass.
Minimal reproducer
The hang is in the Adam optimizer kernel that updates a large learnable
parameter table (a hash-grid encoding: 16 levels × 2²⁰ entries × 2 features ≈
16.78 M elements). Each thread strides over the table in half4 (4×fp16)
groups and writes the updated parameters back.
Dispatch: 1-D grid, 256 threads/group, ~524 288 threads, each looping over the
16.78 M-element table in steps of threadCount * 4.
Hangs (data-dependent store)
// RWByteAddressBuffer gEncodingParams; // ~33.5 MB, fp16
[numthreads(256, 1, 1)]
void optimizeMain(uint3 tid : SV_DispatchThreadID)
{
for (uint base = tid.x * 4u; base < kElementCount; base += gThreadCount * 4u)
{
half4 p = loadHalf4(gEncodingParams, base);
float4 g = loadGrad(base);
// ... Adam moment update producing the new params `p` and a bool ...
bool updated = adamStep(p, /*moments*/, g);
if (updated) // <-- runtime, divergent
gEncodingParams.StoreAligned<half4>(base * 2u, p); // <-- HANGS
}
}
updated is dot(step, step) > epsilon, i.e. it is true for most threads on
most iterations but false for some — a genuinely divergent, data-dependent
predicate guarding a 64-bit (half4) store. With this guard present the
dispatch triggers Xid 109.
Works (unconditional store)
// identical Adam update ...
adamStep(p, /*moments*/, g);
gEncodingParams.StoreAligned<half4>(base * 2u, p); // <-- no hang
Removing only the if (updated) guard — writing the (possibly unchanged) value
unconditionally — makes the same dispatch complete normally every time. No
change to dispatch size, buffer, data, or any other code.
Notes that may help narrow it down
- The store width matters to the trigger: it is a
half4/ 8-byte aligned
store (StoreAligned<half4>, SPIR-VOpStoreof a 4×16-bit vector). We did
not reproduce with a scalarfloatstore in the same structure, though we did
not exhaustively bisect store widths. - The shader also uses
VK_NV_cooperative_vector(cooperative-vector matmul) in
the same module (the surrounding MLP forward/backward). The optimizer loop
itself is plain buffer load/store + arithmetic, but the module is compiled
with thespvCooperativeVectorNVcapability enabled. - The grid is large (16.78 M elements / ~65 536 iterations per thread at this
thread count). We have not checked whether a much smaller table still hangs. - 100% reproducible on this machine with the guarded store; 0% with the
unconditional store.
Impact
Because the hang is an uninterruptible context-switch timeout on the open
kernel modules, the only recovery is killing the process (and sometimes a GPU
reset). It is easy to hit from ordinary, valid shader code (a conditional
write-back is a completely natural optimizer micro-optimization), and there is
no diagnostic from the API layer — the submission simply never completes.
What we expect
A divergent, in-bounds half4 store guarded by a runtime predicate should
either execute or be skipped per lane; it should not stall context switching and
trip Xid 109. Either the compiler should not emit code that wedges the
scheduler, or the driver should preempt/recover the channel cleanly.
We are happy to provide the full SPIR-V, a standalone Vulkan repro, or an
nvidia-bug-report.log on request.