fix(dsv4): avoid fused TopK v2 on Hopper - #33835
Conversation
…3.1+
`TopKCluster::forward` selected its phase-3 scatter destination up front:
const auto cur_out = is_primary ? problem.out : smem->tmp_out;
`problem.out` can be a `shared::cluster` (DSMEM) alias of the elected rank's
buffer, while `tmp_out` is `shared::cta`. Merging both into one pointer variable
makes cicc 13.1+ mis-lower the block-local arm for sm_90a and silently drop
every non-primary rank's staged output: `tmp_out` stays zero, and phase 3.5 then
faithfully copies zeros to perfectly correct DSMEM addresses.
The result is a top-k row where only the primary's slots and the `handle_tie`
tail hold valid indices. Slot-diffing one row against a CUDA 12.9 build shows
exactly one contiguous bad run -- [61, 443] -- with slots 0-60 (the primary's own
scatter) and 444-511 (handle_tie) correct. Downstream sparse attention then
dereferences the garbage slots, which is the illegal memory access reported in
sgl-project#33835.
Fix: keep the two destinations in separate code paths so neither pointer ever
carries two address spaces, and note it so nobody merges them back.
Scope: affects any fused small-batch cluster shape (`batch <= 30` and
`seq_len > cluster_floor`), not just rows near the 32K small-batch floor -- the
floor only makes the path reachable at `batch <= 15`. The persistent-pool path
was never affected because it stages output in global memory.
Toolchain matrix (H200, sm_90a, 157-row suite over the report's shapes plus a
boundary sweep and register/streaming/persistent controls):
nvcc before after
12.9 0 bad 0 bad
13.0 0 bad 0 bad
13.1 157 bad 0 bad
13.2 157 bad 0 bad
13.3 157 bad 0 bad
CI builds cu130, which is a clean cell -- that is why this stayed hidden and why
the report came from a CUDA 13.1 deployment.
Also drops the `peer_problem` copy from sgl-project#32910 and states the
block-local pointer at the read-back site instead. That `__builtin_assume` is
load-bearing: removing it reproduces the sgl-project#32830 cicc segfault on 13.1/13.2/13.3.
Validation: 0 bad rows on 12.9/13.0/13.1/13.2/13.3; 1500-iteration randomized
stress and 500 CUDA-graph replays clean; builds for sm_90a and sm_100a on every
toolchain. No performance regression -- worst case +0.1% (noise), and the fused
cluster shapes get 1.3-2.9% faster. sm_100a is compile-verified only; no
Blackwell was available to run on.
B200 (sm_100a) runtime result: the corruption does not reproduce on Blackwell, so keeping the fused path enabled on SM100 — as this PR does — does not ship a silently-wrong Blackwell. This also closes the "sm_100a is compile-verified only" gap left open in #34167. Since the trigger is the CUDA toolkit that builds the JIT module rather than the GPU (see #34167), every affected nvcc minor was measured separately, each with a fresh
All three toolkits are cells that are 157/157 bad on H200, so this is an architecture split, not an out-of-range toolchain. Three checks so this negative is not a false negative:
Consistent with codegen: compiling the same unfixed source with one nvcc (13.2) for both targets,
|
…3.1+
`TopKCluster::forward` selected its phase-3 scatter destination up front:
const auto cur_out = is_primary ? problem.out : smem->tmp_out;
`problem.out` can be a `shared::cluster` (DSMEM) alias of the elected rank's
buffer, while `tmp_out` is `shared::cta`. Merging both into one pointer variable
makes cicc 13.1+ mis-lower the block-local arm for sm_90a and silently drop
every non-primary rank's staged output: `tmp_out` stays zero, and phase 3.5 then
faithfully copies zeros to perfectly correct DSMEM addresses.
The result is a top-k row where only the primary's slots and the `handle_tie`
tail hold valid indices. Slot-diffing one row against a CUDA 12.9 build shows
exactly one contiguous bad run -- [61, 443] -- with slots 0-60 (the primary's own
scatter) and 444-511 (handle_tie) correct. Downstream sparse attention then
dereferences the garbage slots, which is the illegal memory access reported in
sgl-project#33835.
Fix: keep the two destinations in separate code paths so neither pointer ever
carries two address spaces, and note it so nobody merges them back.
Scope: affects any fused small-batch cluster shape (`batch <= 30` and
`seq_len > cluster_floor`), not just rows near the 32K small-batch floor -- the
floor only makes the path reachable at `batch <= 15`. The persistent-pool path
was never affected because it stages output in global memory.
Toolchain matrix (H200, sm_90a, 157-row suite over the report's shapes plus a
boundary sweep and register/streaming/persistent controls):
nvcc before after
12.9 0 bad 0 bad
13.0 0 bad 0 bad
13.1 157 bad 0 bad
13.2 157 bad 0 bad
13.3 157 bad 0 bad
CI builds cu130, which is a clean cell -- that is why this stayed hidden and why
the report came from a CUDA 13.1 deployment.
Also drops the `peer_problem` copy from sgl-project#32910 and states the
block-local pointer at the read-back site instead. That `__builtin_assume` is
load-bearing: removing it reproduces the sgl-project#32830 cicc segfault on 13.1/13.2/13.3.
Validation: 0 bad rows on 12.9/13.0/13.1/13.2/13.3; 1500-iteration randomized
stress and 500 CUDA-graph replays clean; builds for sm_90a and sm_100a on every
toolchain. No performance regression -- worst case +0.1% (noise), and the fused
cluster shapes get 1.3-2.9% faster. sm_100a is compile-verified only; no
Blackwell was available to run on.
…root cause for sgl-project#33835) (sgl-project#34167) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…root cause for sgl-project#33835) (sgl-project#34167) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…root cause for sgl-project#33835) (sgl-project#34167) Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Thanks for the PR. This should already be fixed by #34167. Feel free to reopen if that PR does not work in your case. |
Motivation
DeepSeek-V4 decode can produce incomplete DSA TopK v2 output on Hopper when a small mixed batch crosses the 32K C4 cluster floor. In an 8x H200 TP8 deployment, this surfaced as a reproducible downstream CUDA illegal-memory access when one long request crossed raw token 131072 while seven short requests remained active.
The failure is in the current fused small-batch DSMEM path, not in Marlin, Mooncake, FP8 KV cache, or the general persistent-cluster implementation.
Root cause
Calling
topk_transform_512_v2directly on H200 with an identity page table gave the following result:For
batch_size <= 15, the small-batch cluster floor is 32768. The two rows just above that floor enterCluster::forwardintopk_small_batch_kerneland return incomplete output on SM90. Register and streaming rows in the same launch are correct.Additional isolation:
seq_lensread did not repair the two failing rows.batch=31, seq_len=65537returned 512 unique valid indices for all 31 rows.The direct kernel corruption is deterministic. The link from stale/duplicate TopK slots to the downstream sparse-attention illegal access is supported by the full-model boundary A/B.
Changes
Blackwell dispatch is unchanged.
Validation
Tested environment:
Results with the Hopper fallback:
1 x 130800 -> 10000plus7 x 2000 -> 10000): all requests completed; 80,000 output tokens; no CUDA error.40 x 200000 -> 40000, concurrency 40): 40/40 requests completed; 8,000,000 input and 1,600,000 output tokens; 1566.01 output tok/s; HTTP health remained 200.1 x 298933 -> 10000plus7 x 9731 -> 10000): all requests completed, including the C4> 65536row.The deployed validation branch also contained a PDL-wait placement cleanup inside the fused kernel. A PDL-only A/B did not fix the corruption, and the fused kernel is not launched on SM90 after this dispatch change. This PR intentionally excludes that unrelated cleanup and contains only the minimal Hopper fallback.
The tested
topk_v2.cuhand regression-test base blobs are unchanged on currentmainas of 2026-08-06.Related work
seq_lens, a different failure class.Checklist
git diff --checkpasses.CI States
Latest PR Test (Base): ❌ Run #31081928822
Latest PR Test (Extra): ❌ Run #31081928039