Skip to content

[BugFix] Fix race in c128 prefill plan kernel on ragged extend - #32467

Merged
BBuf merged 5 commits into
sgl-project:mainfrom
EanWang211123:fix/fix-dspark-c128-ragged-prefill-plan-race
Aug 12, 2026
Merged

BBuf merged 5 commits into
sgl-project:mainfrom
EanWang211123:fix/fix-dspark-c128-ragged-prefill-plan-race

Conversation

@EanWang211123

@EanWang211123 EanWang211123 commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Motivation

In DSpark compact ragged-verify mode, CUDA graph capture could fail with an illegal memory access in c128_v2.cuh:506 when max-running-requests is large (e.g. 96). The failure was nondeterministic: one TP rank could build a correct plan while another emitted out-of-bounds ragged_id values.

Root cause: in plan_compress_prefill_kernel0, warp 0 initializes all warp_min/warp_max scratch slots while each warp later writes its own slot in Stage B. Without a barrier between init and reduction, warp 0 can clobber another warp's min result. That makes ragged extend_lens (e.g. [4]×72 + [3]×24) look uniform (s_min == s_max), wrongly selecting the MTP fast path and generating ragged_id up to batch_size × E - 1, past the actual ragged input rows. The write kernel has no bounds check on CUDA, so this surfaces as an IMA.

This is a general correctness bug for any non-uniform prefill extend; compact ragged capture just makes it easy to hit.

Modifications

  • Add __syncthreads() in c_plan.cuh between warp-scratch initialization and per-warp min/max reduction in plan_compress_prefill_kernel0.
  • Ensures ragged extend correctly takes Path 2 (general prefill) instead of being misclassified as MTP-uniform.

Accuracy Tests

Speed Tests and Profiling

Setup: DeepSeek-V4-Flash-DSpark, DSPARK block-size 5, TP=2, Marlin MoE, max-running-requests=96, radix cache disabled. Metric: mean_output_throughput.

batch compact (SGLANG_RAGGED_VERIFY_MODE=compact) default
1 336.46 333.14
32 48.12 48.49
64 32.78 33.21
96 24.75 25.73

compact command:

CUDA_VISIBLE_DEVICES=6,7 \
SGLANG_RAGGED_VERIFY_MODE=compact \
sglang serve \
--trust-remote-code \
--model-path /models/deepseek-ai/DeepSeek-V4-Flash-DSpark/ \
--speculative-algorithm DSPARK \
--speculative-dspark-block-size 5 \
--tp 2 \
--moe-a2a-backend none \
--mem-fraction-static 0.75 \
--moe-runner-backend marlin \
--reasoning-parser deepseek-v4 \
--max-running-requests 96 \
--disable-radix-cache \
--host 0.0.0.0 \
--port 8000

default command:

CUDA_VISIBLE_DEVICES=6,7 \
sglang serve \
--trust-remote-code \
--model-path /models/deepseek-ai/DeepSeek-V4-Flash-DSpark/ \
--speculative-algorithm DSPARK \
--speculative-dspark-block-size 5 \
--tp 2 \
--moe-a2a-backend none \
--mem-fraction-static 0.75 \
--moe-runner-backend marlin \
--reasoning-parser deepseek-v4 \
--max-running-requests 96 \
--disable-radix-cache \
--host 0.0.0.0 \
--port 8000

Before this fix, compact mode failed during CUDA graph capture at large batch tiers; after the fix, capture completes and serving is stable at bs=96.

Checklist

Review and Merge Process

  1. Ping Merge Oncalls to start the process. See the PR Merge Process.
  2. Get approvals from CODEOWNERS and other reviewers.
  3. Trigger CI tests with comments or contact authorized users to do so.
    • Common commands include /tag-and-rerun-ci, /tag-run-ci-label, /rerun-failed-ci
  4. After green CI and required approvals, ask Merge Oncalls or people with Write permission to merge the PR.

CI States

Latest PR Test (Base): ❌ Run #31582596399
Latest PR Test (Extra): ❌ Run #31582595666

Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>
@EanWang211123
EanWang211123 marked this pull request as ready for review July 27, 2026 03:15
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

1 similar comment
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Caution

The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased.

@lmyybh

lmyybh commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

@yhyang201 I see that the plan_compress_prefill_kernel0 kernel was implemented by you. Could you help take a look if this fix can be merged? Thanks!

@hassellof

Copy link
Copy Markdown
Contributor

Independent hardware validation for this fix, in case it helps the review @yhyang201 @lmyybh.

On 4× RTX PRO 6000 Blackwell (sm_120), TP=4 + DP attention + EP, DeepSeek-V4-Flash-0731, sglang v0.5.16 with this PR backported (paths rewritten for the pre-refactor tree): it fixes a deterministic decode-graph capture IMA under SGLANG_RAGGED_VERIFY_MODE=compact.

Before the backport we had mapped an 8-row shape/allocator matrix where the fault threshold moved with the CUDA-graph batch-size ladder and with PYTORCH_CUDA_ALLOC_CONF (without expandable segments the boundary sat between ladder ≤192 healthy and ≤240 faulting; with expandable_segments:True it dropped to ≤96) — the signature of an out-of-bounds landing in mapped vs unmapped VA, which is exactly the ragged_id overrun this PR's missing barrier produces. With this patch alone, every row in that matrix captures cleanly, including the worst one (ctx 1M, mem-fraction 0.90, cap 64, expandable segments on, full ladder). Details and the matrix are in #33412.

Two notes that may be useful:

  • Our fault localizes to the c4 compressor store (fused_norm_rope_v2.cuh) rather than the c128 store cited in the description, which is consistent with a single bad plan being consumed by whichever store reaches it first.
  • The write kernel having no bounds check is why this surfaces as an IMA rather than a clean error; a device-side guard would make the failure mode much easier to diagnose, independent of this fix.

We are also running the 4-arm matrix proposed in #33356 (this PR × #33795's capture-ordering fix) on the same hardware and will report the result there.

CI has not run on this PR — if a maintainer can add the run-ci label it would at least unblock the automated checks. Happy to run further validation on SM120 hardware on request; the repro takes about 30 minutes per arm.

Copy link
Copy Markdown

Independent B300/TP8 validation: PASS

I tested the __syncthreads() fix from this PR on the original B300/TP8 reproducer reported in #33356.

Environment

GPU: 8 x NVIDIA B30Z/B300-class
Tensor parallelism: TP8
Model: DeepSeek-V4-Pro-DSpark
SGLang: official v0.5.16-cu130 image
PyTorch: 2.11.0+cu130
Triton: 3.6.0
CUDA: 13.0
Verify mode: compact
MoE runner: flashinfer_mxfp4
Attention backend: dsv4
cuda-graph-max-bs-decode: 128 (35 capture shapes)
mem-fraction-static: 0.82
chunked-prefill-size: 4096
SPS: disabled
Client workload: none (startup capture gate only)

The v0.5.16 source path is python/sglang/jit_kernel/csrc/deepseek_v4/c_plan.cuh; I applied the same one-line semantic change from this PR between warp scratch initialization and Stage B:

__syncthreads();

I cleared the TVM-FFI JIT cache before the first fixed run so the modified header could not reuse the old compiled artifact.

Four-arm result

Arm #32467 #33795 post-warmup sync/barrier Result
A OFF OFF 0/7 reached full capture; failures drifted across bs=60/96/112 and Triton/TRT-LLM/DeepGEMM/host SIGSEGV surfacing paths
C OFF ON 1/6 READY; 5/6 still failed at bs=60/96/112
B ON OFF 3/3 READY, 35/35 shapes captured in every run, no target failure signature
D ON ON 3/3 READY, 35/35 shapes captured in every run, no target failure signature

Per-run startup times:

#32467 only:       331s, 297s, 291s
#32467 + #33795:   298s, 296s, 296s

Across all six runs with #32467 enabled, log scans found no:

illegal memory access
CUDA_ERROR_ILLEGAL_ADDRESS
Segmentation fault
Capture cuda graph failed
scheduler died during initialization

Interpretation

On this B300/TP8 configuration, the missing block synchronization fixed by #32467 is the main cause of the startup-time compact ragged CUDA-Graph capture IMA tracked in #33356. The post-warmup sync/barrier from #33795 is compatible, but was neither sufficient by itself nor required once this kernel race was fixed.

This result is scoped to startup capture (no SPS, no client workload); it does not by itself resolve the historical sustained replay issue in #31023.

Tested-by: @Phoenix3334

Copy link
Copy Markdown

Independent B300 / TP8 validation

I validated this fix on the original B300 reproducer from #33356 using a controlled #32467 × #33795 four-arm matrix.

Environment

Image: official sglang:v0.5.16-cu130
SGLang: 0.5.16
PyTorch: 2.11.0+cu130
Triton: 3.6.0
CUDA: 13.0
GPU: 8 × NVIDIA B30Z (275 GB)
TP: 8
Model: DeepSeek-V4-Pro-DSpark
Verify mode: compact
MoE runner: flashinfer_mxfp4
cuda-graph-max-bs-decode: 128 (35 shapes)
mem-fraction-static: 0.82
chunked-prefill-size: 4096
SPS: disabled
Client workload: none — startup capture only

The v0.5.16 source tree uses the equivalent file at:

python/sglang/jit_kernel/csrc/deepseek_v4/c_plan.cuh

I applied the same semantic change from this PR: a single __syncthreads() between warp scratch initialization and Stage B min/max reduction.

Four-arm result

Arm #32467 #33795 Result
A OFF OFF 0/7 reached full capture; failures drifted across bs=60/96/112
C OFF ON 1/6 READY; 5 failures at bs=96/112/60/96/60
B ON OFF 3/3 READY, 35/35 capture each run
D ON ON 3/3 READY, 35/35 capture each run

Arm B elapsed times were 331/297/291 s; Arm D was 298/296/296 s. All six fixed-kernel runs completed capture and reached HTTP ready. Scans for illegal memory access, CUDA_ERROR_ILLEGAL_ADDRESS, Segmentation fault, Capture cuda graph failed, and scheduler initialization failures were empty.

Controls

  • Every run used a fresh server in the same container/task and on the same machine.
  • The c_plan.cuh source hash was identical in Arms B and D.
  • Fix(DSpark): sync device before CUDA graph capture to avoid JIT race #33795 was explicitly absent in Arm B and present in Arm D; runtime source markers and hashes were checked before each arm.
  • /root/.cache/tvm-ffi was cleared after changing the .cuh, preventing reuse of the pre-fix compiled kernel.

Conclusion

For this B300×8 / TP8 reproducer, #32467 is the primary fix for the startup capture IMA. #33795 alone was insufficient, while #32467 alone was sufficient in 3/3 fresh runs; combining both fixes was also stable in 3/3 runs.

This matches the proposed mechanism: the missing block barrier can misclassify non-uniform ragged input as uniform and generate out-of-bounds ragged_id; the kernel that later reports the IMA is only the surfacing point, which explains the observed shape/rank/kernel drift.

Tested-by: @Phoenix3334

@Leoyzen

Leoyzen commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Third-hardware datapoint: TP4 H200×4 validation (arm D, both patches)

Adding to @hassellof's SM120 and @Phoenix3334's B300 evidence, here's H200×4 TP4 with DeepSeek-V4-Flash-0731 (image dsv4-fix-9303e26-sync-c128, both #32467 + #33795 applied):

Stage Description Result
Capture smoke CUDA_LAUNCH_BLOCKING=1, bs [1..128] incl. 30/60/128 Zero CUDA errors, clean capture
Full SPS profile 60 rounds, 15 bs × 12 M grid All match_fraction=1.00 (zero retraction)
Replay canary 48 requests × 2 replays hash(text + output_ids) identical — no silent corruption
Sustained load 30 min mixed, 354 requests 0 errors
#31023 stress 128 concurrent, 1280 requests, compact+graph+overlap ON All completed, zero IMA

The match_fraction=1.00 across 60 profiling rounds (bs 1–128, M 0–768) indicates the c128 plan kernel produces correct ragged_id mappings under all tested shapes — no retraction means no garbage indices reaching the compress store.

The patch is deployed in our production DSV4 serving cluster (TP4 H200×4).

Note: this is arm D (both patches). We have not yet isolated #32467 alone on H200 (arm B) — that test would determine whether P2 is reachable on Hopper or is SM120/B300-specific. @hassellof has proposed a specific set of rows for that cross-check; we plan to run them.

Copy link
Copy Markdown

Follow-up B300/TP8 validation: I extended #32467 testing from startup capture to sustained replay, with #33795 kept OFF.

Environment: 8x B30Z, TP8, DeepSeek-V4-Pro-DSpark, official v0.5.16-cu130, compact verify, flashinfer_mxfp4.

Startup remains clean with #32467 alone: 3/3 fresh servers reached READY and captured all 35/35 decode-graph shapes.

Runtime Gate 1 (no SPS, max graph bs=32, max-running-requests=32, c32/n320, 60k/1k): 320/320 completed, 0 timeouts, health 200 before/after, no GPU error signatures, post-run smoke request passed. Runtime sampling showed CUDA Graph replay only (True=134, False=0).

Runtime Gate 2' (compact + old SPS, max graph bs=128, c256/n512, 60k/1k, request-timeout=7200s): 512/512 completed, 0 timeouts, health 200 before/after, no illegal-memory / CUDA_ERROR_ILLEGAL_ADDRESS / scheduler-exception / NCCL-watchdog signatures, and the post-run smoke request passed.

A second fresh Gate 2' run was manually stopped at 293/512 to release the machine; it had no GPU error signature up to the stop point, so I am not counting it as either pass or failure.

Updated conclusion for this B300x8/TP8 reproducer: #32467 alone now has clean evidence for both startup CUDA-Graph capture and sustained replay on the historical strong path. This further supports #32467 as the primary fix for this B300 failure class; #33795 is compatible but was not required for these successful runs.

Copy link
Copy Markdown

Direct kernel-level regression: unpatched produces OOB ragged_id, #32467 eliminates it

I added a small single-GPU regression harness that calls the real GPU-input plan_prefill(...) path down to plan_compress_prefill_kernel0, without loading the model. This gives a much more direct check of the race than the full-server startup/replay tests I posted earlier.

Environment:

GPU: 1 x NVIDIA B30Z
SGLang: official v0.5.16-cu130
PyTorch: 2.11.0+cu130
CUDA: 13.0
path in this pre-refactor tree: python/sglang/jit_kernel/csrc/deepseek_v4/c_plan.cuh
compress_ratio=4, ring_size=4, swa_page_size=64, num_q_tokens=4096

The harness intentionally oversizes req_to_token / full_to_state so a bad plan is observed as data corruption instead of crashing the process. The invariant checked is:

max(ragged_id) < sum(extend_lens)

Results

input B s_min/s_max actual ragged rows unpatched with #32467
[4] x 96 96 4/4 384 0 / 2000 bad 0 / 10000 bad
[4] x 72 + [3] x 24 96 3/4 360 594 / 2000 bad (29.7%) 0 / 10000 bad
[3] x 104 + [2] x 24 128 2/3 360 1998 / 2000 bad (99.9%) 0 / 10000 bad

Aggregate:

unpatched: 2592 bad plans / 6000 calls
patched:      0 bad plans / 30000 calls

I cleared /root/.cache/tvm-ffi before each arm so both versions were re-JITed from the corresponding c_plan.cuh.

The strongest diagnostic detail is the bad value itself. For both ragged cases the maximum generated ragged_id is exactly:

rmax = 383
     = 96 * 4 - 1
     = 128 * 3 - 1
     = B * s_max - 1

while the real ragged row count is only 360. That is exactly the maximum ID produced by the MTP-uniform formula batch_id * E + j with E=s_max, showing that a genuinely ragged batch was planned as uniform. The uniform control [4] x 96 never produces a bad ID.

So on this B300 reproducer the causal chain is now observable directly at the plan output:

missing __syncthreads()
  -> warp scratch init/reduction race
  -> ragged input can collapse to the uniform classification
  -> uniform-formula ragged_id generation
  -> OOB ragged_id

This matches the full-model behavior where the eventual IMA surfaced through different downstream kernels/ranks: those kernels were consumers of an already-invalid plan, not necessarily the producer of the bad address.

Together with the earlier B300 results (#32467-only startup 3/3 READY, sustained replay 320/320 and 512/512 clean), this is much stronger evidence that the barrier in this PR fixes the underlying correctness bug rather than only changing timing.

The harness should also be a good basis for a compact GPU regression test: the two ragged inputs above reproduce quickly without model weights, while the fixed kernel stays clean across 30k calls.

Copy link
Copy Markdown

Direct kernel-level regression on B300: unpatched produces OOB plans; this PR eliminates them

I added a small single-GPU repro that calls the real DSv4 plan_prefill(...) GPU path directly, so it reaches plan_compress_prefill_kernel0 without loading model weights or starting a server.

Environment:

GPU: 1 × NVIDIA B30Z
SGLang: v0.5.16-cu130 backport tree
PyTorch: 2.11.0+cu130
CUDA: 13.0
compress_ratio: 4
ring_size: 4
swa_page_size: 64
num_q_tokens: 4096

The harness intentionally oversizes req_to_token / full_to_state so the race is observed as bad plan data rather than crashing the process. For each generated plan I check whether the emitted ragged_id exceeds the real ragged input rows (sum(extend_lens)).

Results

input B min/max extend real ragged rows unpatched with #32467
uniform [4] × 96 96 4 / 4 384 0 / 2000 bad 0 / 10000 bad
ragged [4] × 72 + [3] × 24 96 3 / 4 360 594 / 2000 bad (29.7%) 0 / 10000 bad
ragged [3] × 104 + [2] × 24 128 2 / 3 360 1998 / 2000 bad (99.9%) 0 / 10000 bad

Aggregate:

unpatched: 2592 / 6000 bad plans
patched:       0 / 30000 bad plans

The first bad ragged plans appear at iteration 0/1, so this is a fast and highly reproducible kernel-level regression rather than a service-level timing probe.

Why this directly matches the proposed root cause

For both ragged cases the bad maximum ragged_id is exactly:

rmax = 383
383 = 96 * 4 - 1
383 = 128 * 3 - 1
     = B * s_max - 1

That is the maximum ID generated by the MTP-uniform formula (ragged_id = batch_id * E + j, with E = s_max). But the real ragged input has only 360 rows, so IDs 360..383 are OOB.

The uniform control never produces a bad plan, while the two non-uniform inputs do. This gives a direct reproduction of the failure chain described in this PR:

missing __syncthreads()
  -> warp_min/warp_max scratch race
  -> non-uniform extend misclassified as uniform
  -> MTP-uniform ragged_id formula used on ragged input
  -> OOB ragged_id

After adding this PR's barrier and clearing the TVM JIT cache so c_plan.cuh is recompiled, the same three cases are clean for 30,000 total calls.

This is consistent with the earlier full-model B300/TP8 result (3/3 startup captures clean with #32467 alone) and the sustained-replay follow-up (320/320 low-tier graph replay + 512/512 on the historical compact/SPS 60k/1k path, with no GPU fault signatures).

I have the standalone harness in a form that can be reduced into a 1-GPU pytest regression. I think adding that coverage would be valuable here or as a small follow-up PR.

Copy link
Copy Markdown

Direct kernel-level regression reproducer for #32467

I now have a much smaller reproducer that exercises the real GPU JIT path for plan_compress_prefill_kernel0 directly, without loading DeepSeek-V4 model weights or starting a server.

Environment:

  • 1× NVIDIA B30Z
  • SGLang v0.5.16-cu130
  • PyTorch 2.11.0+cu130 / CUDA 13
  • GPU-input plan_prefill(...) path, which dispatches the real plan_compress_prefill_kernel0
  • compress_ratio=4, ring_size=4, swa_page_size=64, num_q_tokens=4096
  • req_to_token / full_to_state are intentionally oversized so the bug is observed as bad plan data instead of crashing the process

The check is simple: for a ragged input, max(ragged_id) must stay below sum(extend_lens).

case B s_min/s_max sum_ext before #32467 after #32467
uniform [4]×96 96 4/4 384 0 / 2000 bad 0 / 10000 bad
ragged [4]×72 + [3]×24 96 3/4 360 594 / 2000 bad (29.7%) 0 / 10000 bad
ragged [3]×104 + [2]×24 128 2/3 360 1998 / 2000 bad (99.9%) 0 / 10000 bad

Aggregate:

unpatched: 2592 / 6000 bad plans
patched:       0 / 30000 bad plans

I cleared /root/.cache/tvm-ffi before each arm so the modified .cuh was actually recompiled.

The strongest signature is the bad ragged_id maximum. In both failing ragged cases it is exactly:

rmax = 383
     = 96 × 4 - 1
     = 128 × 3 - 1
     = B × s_max - 1

while the real ragged row count is only 360. That is exactly the maximum ID produced by the uniform-path formula with E=s_max, but it appears for a non-uniform ragged batch. The uniform control stays clean.

So this reproducer directly demonstrates the failure chain behind the full-model IMA:

missing __syncthreads()
  -> warp_min/warp_max scratch race
  -> ragged batch can be treated as uniform
  -> ragged_id generated up to B * s_max - 1
  -> ragged_id >= actual ragged rows
  -> downstream OOB / IMA

This is consistent with the earlier B300/TP8 full-model validation, but is much cheaper and more reproducible: single GPU, tiny inputs, no model weights, and the failure appears within the first few iterations before the fix.

I have a Python harness for this and can turn it into a GPU regression pytest if useful for this PR / a follow-up test PR.

Copy link
Copy Markdown

Deterministic single-GPU kernel regression for the missing barrier

I added a direct regression harness for plan_compress_prefill_kernel0 to validate this PR at the kernel-plan level rather than only through whole-model startup/replay behavior.

Environment:

  • 1× NVIDIA B30Z
  • official SGLang v0.5.16-cu130
  • torch 2.11.0+cu130 / CUDA 13
  • no model weights loaded
  • real GPU JIT path via plan_prefill(...) -> plan_compress_prefill_kernel0
  • compress_ratio=4, ring_size=4, swa_page_size=64, num_q_tokens=4096

The harness deliberately oversizes the backing mapping tensors so the race produces inspectable bad plan data instead of immediately crashing the process. The primary check is:

bad_oob := max(ragged_id) >= sum(extend_lens)

I tested three shapes:

case batch min/max extend true ragged rows unpatched patched
uniform [4] × 96 96 4 / 4 384 0 / 2000 bad 0 / 10000 bad
ragged [4] × 72 + [3] × 24 96 3 / 4 360 594 / 2000 bad (29.7%) 0 / 10000 bad
ragged [3] × 104 + [2] × 24 128 2 / 3 360 1998 / 2000 bad (99.9%) 0 / 10000 bad

Aggregate:

without #32467: 2592 / 6000 bad plans
with    #32467:    0 / 30000 bad plans

For both failing ragged cases, the bad maximum ragged_id is exactly 383:

96 × 4  - 1 = 383
128 × 3 - 1 = 383

while the actual number of ragged rows is only 360.

That is the exact maximum produced by the MTP-uniform indexing formula batch_id * E + j with E = s_max, but it appears on a non-uniform batch. This gives a direct mechanism-level reproduction of the failure chain described in this PR:

missing __syncthreads()
  -> warp scratch init/reduction race
  -> non-uniform extend is misclassified as uniform
  -> uniform ragged_id formula is used
  -> ragged_id reaches 383 for only 360 valid rows
  -> downstream consumer can surface an IMA

The uniform control remained clean before and after the patch, so the detector is not simply flagging the normal uniform path.

For cache hygiene I cleared the TVM-FFI JIT cache before both arms so the modified c_plan.cuh was actually recompiled. The unpatched and patched source hashes were also recorded separately.

One caveat: I did not use the harness's internal took_uniform flag as evidence because that field is rewritten later by kernel 1 in this call path. The OOB condition and the exact 383 = B × s_max - 1 value are the reliable checks.

This is much cheaper than the whole-model repro (single GPU, tiny tensors, no checkpoint load), so I think it is a good candidate for a GPU regression test. I can send a follow-up PR that converts this harness into a small pytest if maintainers prefer the regression coverage separately from this fix.

Copy link
Copy Markdown

Additional producer-level validation following @DarkSharpness's review suggestion to remove the redundant warp-scratch initialization instead of adding another barrier.

I ran a same-session A/B/C kernel experiment on B30Z, 10,000 calls per input shape / 30,000 calls per variant:

variant scratch init extra pre-reduction barrier OOB plans
A: original ON OFF 13111 / 30000
B: current #32467 ON ON 0 / 30000
C: reviewer variant OFF OFF 0 / 30000

Inputs:

[4] * 96
[4] * 72 + [3] * 24
[3] * 104 + [2] * 24

Oracle:

max(valid ragged_id) < sum(extend_lens)

This supports the simpler no-initialization formulation under the current fixed launch geometry: each of the 32 warps overwrites its own scratch slot before the existing post-reduction __syncthreads(), so the racy warp-0 initialization is unnecessary.

One implementation caveat: if the no-init variant is adopted, I think the fixed 1024-thread / 32-warp assumption should be made explicit with a comment or assertion so a future launch-geometry change cannot silently invalidate the reasoning.

Together with the earlier B300 direct harness (2592/6000 bad -> 0/30000) and #32467-only startup/replay validation, this is further producer-level evidence that the plan corruption is real rather than a downstream timing artifact.

Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>

Copy link
Copy Markdown

Independent B300 runtime regression for the final reviewer-selected no-init implementation.

Environment:

  • 8x NVIDIA B30Z 275GB
  • SGLang v0.5.16-cu130 runtime stack
  • DeepSeek-V4-Pro-DSpark
  • TP8 + DP8
  • DP attention + DP LM head
  • moe_a2a_backend=none
  • disaggregation decode + fake bootstrap
  • compact ragged verify
  • CUDA Graph decode max bs = 128

The final #32467 implementation from 58124fb983136b903108c8ef1305c135eb7d376f was source-equivalently backported onto the previously validated v0.5.16 B300 stack.

For plan_compress_prefill_kernel0, the backported function body matches the final upstream no-init implementation modulo whitespace:

  • redundant warp_max / warp_min initialization removed
  • no additional barrier added
  • existing synchronization structure otherwise unchanged

Runtime provenance also confirmed that the JIT module used by Stage 1/2 was the no-init variant: ...compress_plan_0dfd36cb1ccb668a..., distinct from the previous original and barrier variants.

Two unrelated integration prerequisites were required before the full DP8 + disaggregation path could run:

I am keeping these separate from the #32467 result.

Correctness regression:

  • /health 200
  • target/draft CUDA Graph capture completed
  • compact ragged-verify scheduler enabled with a freshly profiled SPS table
  • 0 illegal-memory-access / CUDA illegal-address / device-assert / scheduler-fault signatures

Under the natural 60k/1k C64 workload, verify lengths happened to stay uniform, so the ragged producer was not exercised. Using the existing dspark_force_budget_frac=0.5 control produced a genuinely ragged window:

  • verify_len distribution contained 1..6
  • among fully parsed multi-request scheduling blocks, 39/43 (90.7%) had min(verify_len) != max(verify_len)
  • example: lens=[4,3,3]
  • 0 GPU fault / NaN / verify-budget violation in this ragged window

D-only performance/stability sweep, with debug/assert instrumentation disabled and a fresh engine for each point:

concurrency requests success failed gen tok/s per-rank peak running
64 640 640 0 3638.96 15
256 2560 2560 0 8340.59 46
512 5120 5120 0 13534.73 87
1024 10240 10240 0 24793.49 128

Total: 18,560 / 18,560 successful requests, 0 failures. Post-run health remained 200 for every point, with no preallocation warning or request retraction. cuda graph: True was repeatedly observed during decode, and C1024 reached the captured per-rank bs=128 tier in actual replay.

This should be read as post-fix runtime no-regression evidence for the final no-init implementation on this B300 matrix, not as a new proof of the producer-race root cause. The producer-level A/B/C regression remains the direct causality evidence.

Copy link
Copy Markdown

2026-08-11 follow-up to the earlier final-no-init B300 runtime regression above: I extended the same decode-only topology into an external-concurrency / admission-pressure sweep while keeping max-running-requests=1024 and per-rank CUDA-Graph max batch 128 fixed.

external C requests success fail gen tok/s TTFT (s) TPOT (s) per-rank running peak prealloc peak
1280 5120 5120 0 20430 6.87 0.0332 128 96
1536 6144 6144 0 21877 19.09 0.0322 128 121
2048 6144 6144 0 21275 35.06 0.0320 128 222
2560 7680 7680 0 23882 53.21 0.0306 128 245

All four arms completed with zero request failures, no observed CUDA illegal-address/device-assert/scheduler faults, no retractions/preallocation warnings, Graph replay retained, and /health=200.

This is not a claim of 1280–2560 simultaneously running requests: MRR remained 1024 and the per-rank running peak remained 128. It is useful as additional sustained-replay/admission-pressure evidence for the final no-init kernel, while true higher in-flight capacity requires raising MRR / graph tiers and re-profiling SPS.

The full updated root-cause and runtime/capacity map is in #34297. In particular, the roadmap now records the final #32467 fix as removal of the redundant scratch initialization (not the earlier barrier experiment), and keeps downstream IMA sites classified as consumers/surfacing points.

Copy link
Copy Markdown

One review-facing documentation note: the PR diff/HEAD is now the final no-init implementation, but the current PR description still says “Add __syncthreads() between warp-scratch initialization and reduction.” That text reflects the earlier candidate and is now stale.

Current final state:

HEAD: 58124fb983136b903108c8ef1305c135eb7d376f
change: remove redundant warp_min / warp_max initialization
extra barrier added by final patch: no
existing post-reduction synchronization: retained

So the current root-cause/fix wording should be: redundant scratch initialization creates a write-write race with per-warp reductions; the final patch removes the redundant initialization and therefore removes the race source. The barrier variant remains useful as an A/B control, not the final implementation.

@EanWang211123 if convenient, updating the PR body’s Modifications section to match the final diff would avoid reviewers reading the historical barrier formulation. The same correction is recorded in #32470 and the source-of-truth roadmap #34297.

@DarkSharpness

Copy link
Copy Markdown
Collaborator

/tag-and-rerun-ci

@BBuf
BBuf merged commit 8549cce into sgl-project:main Aug 12, 2026
121 of 160 checks passed
@nvpohanh

Copy link
Copy Markdown
Collaborator

cc @weireweire

Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 14, 2026
…roject#32467)

Adopt official fix: remove the warp_min/warp_max init block + __syncthreads()
barrier. Each warp writes its own slot via warp::reduce_min/max, making the
init redundant and the barrier unnecessary. The existing __syncthreads() after
the reduce-write (before warp 0's final cross-warp reduce) ensures all slots
are written before any read.
Leoyzen added a commit to Leoyzen/sglang that referenced this pull request Aug 14, 2026
saturn-acc pushed a commit to saturn-acc/sglang that referenced this pull request Aug 16, 2026
…roject#32467)

Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 19, 2026
… upstream sgl-project#25529) + c_plan barrier (sgl-project#32467)

EAGLE draft cuda-graph replay padded batch_size/seq_lens/req_pool_indices/
positions to the captured bucket but left forward_batch.out_cache_loc at the
RAW request layout. init_forward_metadata_out_graph then built replay
metadata with a padded bs over a raw cache-location buffer — the layout
mismatch produces invalid page metadata inside the captured graph and
surfaces as an ASYNC illegal memory access at whatever host sync comes
first (observed: process_batch_result_decode copy_done.synchronize,
alloc_for_decode_prealloc; nondeterministic per TP rank; not reproducible
under CUDA_LAUNCH_BLOCKING). This matches the upstream family sgl-project#25512/sgl-project#25529
(same padding signature raw_bs=3 graph_bs=4) and sgl-project#28569 (crash as the
running batch shrinks — our mixed load with waves of completing requests
triggers exactly the padding boundary; --disable-cuda-graph is immune).

Fix:
- execute() now swaps forward_batch.out_cache_loc to the zero-filled padded
  buffers slice (num_tokens * speculative_num_steps) during the padded
  window, so replay metadata sees the same layout the captured graph does;
  padded lanes write to reserved cache slot 0.
- try/finally restores the raw batch view even when metadata init or
  replay raises (upstream sgl-project#25529 contract) — a padded leftover poisoned
  every later consumer of the ForwardBatch.

Also ports sgl-project#32467: __syncthreads() between warp-scratch init and the
per-warp min/max writes in c_plan.cuh plan_compress_prefill_kernel0 —
warp 0 could clobber another warp's slot, misclassifying ragged extend as
MTP-uniform and emitting out-of-bounds ragged_id (B300 non-deterministic
IMA family sgl-project#33356). Dormant on the GLM path (no callers) but the kernel is
shared with DSV4 deployments.
Chronostasys pushed a commit to MindLab-Research/sglang that referenced this pull request Aug 24, 2026
… upstream sgl-project#25529) + c_plan barrier (sgl-project#32467)

EAGLE draft cuda-graph replay padded batch_size/seq_lens/req_pool_indices/
positions to the captured bucket but left forward_batch.out_cache_loc at the
RAW request layout. init_forward_metadata_out_graph then built replay
metadata with a padded bs over a raw cache-location buffer — the layout
mismatch produces invalid page metadata inside the captured graph and
surfaces as an ASYNC illegal memory access at whatever host sync comes
first (observed: process_batch_result_decode copy_done.synchronize,
alloc_for_decode_prealloc; nondeterministic per TP rank; not reproducible
under CUDA_LAUNCH_BLOCKING). This matches the upstream family sgl-project#25512/sgl-project#25529
(same padding signature raw_bs=3 graph_bs=4) and sgl-project#28569 (crash as the
running batch shrinks — our mixed load with waves of completing requests
triggers exactly the padding boundary; --disable-cuda-graph is immune).

Fix:
- execute() now swaps forward_batch.out_cache_loc to the zero-filled padded
  buffers slice (num_tokens * speculative_num_steps) during the padded
  window, so replay metadata sees the same layout the captured graph does;
  padded lanes write to reserved cache slot 0.
- try/finally restores the raw batch view even when metadata init or
  replay raises (upstream sgl-project#25529 contract) — a padded leftover poisoned
  every later consumer of the ForwardBatch.

Also ports sgl-project#32467: __syncthreads() between warp-scratch init and the
per-warp min/max writes in c_plan.cuh plan_compress_prefill_kernel0 —
warp 0 could clobber another warp's slot, misclassifying ragged extend as
MTP-uniform and emitting out-of-bounds ragged_id (B300 non-deterministic
IMA family sgl-project#33356). Dormant on the GLM path (no callers) but the kernel is
shared with DSV4 deployments.
jakki-amd pushed a commit to jakki-amd/sglang that referenced this pull request Sep 9, 2026
…roject#32467)

Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>
Atituiset pushed a commit to Atituiset/sglang that referenced this pull request Sep 10, 2026
…roject#32467)

Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

8 participants