Skip to content

[WIP][DSV4] Breakable CUDA graph for mixed-chunk prefill - #30420

Draft
yhyang201 wants to merge 2 commits into
sgl-project:mainfrom
yhyang201:dsv4-bcg-mixed-prefill
Draft

yhyang201 wants to merge 2 commits into
sgl-project:mainfrom
yhyang201:dsv4-bcg-mixed-prefill

Conversation

@yhyang201

@yhyang201 yhyang201 commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Status: WIP / experimental draft — not ready to merge. Opened to share the approach and measurements; see the open-problems list below.

Motivation

With mixed-chunk enabled, the steady state is 100% mixed steps running fully eager: launching a ~180 ms step costs ~170 ms of Python launch time (measured), so the GPU queue never builds backlog and every scheduler pause surfaces as a GPU bubble (~7 ms/step). BCG cuts launch CPU ~6x (2156 kernel launches -> 62 graph replays + eager attention breaks), but naively enabling it made throughput worse. Three integration issues were found and fixed.

What this PR does

  1. WAR read-done event for BCG prefill replays — the fast path (Scheduler._apply_war_barrier) was only fed by the decode graph; BCG replays fell back to wait_stream(forward), turning batch-assembly D2H syncs into a ~90 ms/step wall. The DSV4 backend now pre-builds the sparse-prefill chunk cache from the live batch in the replay-prepare hook (sync-free: total_swa computed CPU-side), moving all scheduler-shared reads to the snapshot, and the prefill runner publishes the event when the backend opts in (war_reads_done_at_snapshot).
  2. Mixed decode/prefill split under replay — the [DSV4] Split mixed-chunk attention: route decode tokens to the fp8 paged MLA kernel #30338 split gate relied on forward_mode.is_mixed(), which is normalized to EXTEND at replay; the boundary now flows through the refreshed metadata.
  3. Live logprob counts for the eager logits tail — the static batch carries capture-time bucket-padded global_num_tokens_for_logprob_*, making the DP-attention hidden gather + vocab all-gather + reorder copies run over every padded token (e.g. 16384 rows) instead of the sampling positions (~1.8k rows); the runner now passes the live counts.

Enablement is gated behind SGLANG_EXPERIMENTAL_FORCE_BCG=1, which bypasses the four breakable-CG incompatibility rules (MLA / DSV4 capture-pool pressure / MoE A2A bucket cap / DP-attention) pending proper graduation.

Results

8xB300, DeepSeek-V4-Pro FP4, 8k1k, conc 2048 (megamoe + dp-attention + mixed-chunk), radix cache with forced misses, num_prompts = 6x conc, warmup = 2x conc; additionally --cuda-graph-bs-prefill 512 1024 1536 2048 2560 2816 and SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK=2816:

Throughput (tok/s) Median TPOT GSM8K (n=200)
eager, no split 8983 196.3 ms
eager + mixed split (#30338) 9828 177.2 ms 0.970
BCG, no integration fixes 8471 208.6 ms 0.975
BCG + WAR event + split-under-replay 9486 183.6 ms 0.980
BCG + all fixes (this PR) 10076 172.5 ms 0.985
BCG + all fixes + --enable-dp-lm-head 10105 172.0 ms

All runs 12288/12288 successful, zero scheduler exceptions. Steady-state traces confirm: launch CPU 171 -> 29 ms/step, graphs replayed on >66k mixed steps under DP attention.

--enable-dp-lm-head is neutral (+0.3%, within run variance) once the logits-gather sizing is fixed — before the fix it would mask the oversized gather entirely (the DP gather path is skipped), at the cost of replicating the LM head (~1.85 GB/GPU). We keep it off by default.

Why this is a draft — open problems

  1. The four incompatibility rules are bypassed, not graduated. They need to become conditional: MoE A2A is safe when capture buckets ≤ NUM_MAX_TOKENS_PER_RANK; DSV4 capture-pool pressure is bounded by a small bucket list (the old default ran to 16384); the MLA rule should exclude the DSV4 backend (its whole attention is an eager break); DP-attention is validated here for one topology only.
  2. Prefill capture buckets are manual (--cuda-graph-bs-prefill); needs defaults derived from chunked_prefill_size + the mixed budget (steady mixed steps land exactly on chunk-size tokens by construction).
  3. The WAR snapshot contract is backend-declared (war_reads_done_at_snapshot); needs review that eviction/allocator interactions cannot invalidate it, plus a debug assertion story.
  4. Residual replay overhead remains (~4% vs the GPU-bound floor): per-layer attention-break output copies (~30 MB x 61/step) could write directly into the static slots; the multimem all-gather capacity (recommended_max_tokens(include_prefill=False)) needs prefill-aware sizing.
  5. Coverage: validated only on 8xB300 DSV4-Pro at conc 2048; no CI tests for BCG x DSV4 x DP-attention capture/replay or accuracy.

Depends on #30338.


CI States

Latest PR Test (Base): ❌ Run #28889917824
Latest PR Test (Extra): ❌ Run #28889917584

yhyang201 added 2 commits July 7, 2026 15:20
…ged MLA kernel

Mixed-chunk batches currently send all tokens (prefill chunk + one decode
token per running request) through the bf16 sparse-prefill workspace path,
which dequantizes the fp8 KV cache every layer. Split the batch at the
prefill/decode boundary instead: the decode tail uses the fp8-native
flash_mla_with_kvcache kernel (no dequant), and the sparse-prefill
workspace only covers the prefill requests' windows.

Gated behind SGLANG_OPT_MIXED_SPLIT_DECODE_ATTN (default off).

8xB300 DSV4-Pro 8k1k conc2048: 9064 -> 9796 tok/s (+8.1%), TPOT 192.6 -> 174.5 ms.
GSM8K under concurrency: 0.970 (on) vs 0.980 (off), within noise.
Mixed-chunk steady state runs fully eager today: launching a ~180 ms step
costs ~170 ms of Python launch time, so the GPU queue never builds backlog
and every scheduler pause becomes a GPU bubble. Breakable CUDA graph (BCG)
cuts the launch cost ~6x, but three integration issues erase the gain:

1. The WAR barrier fast-path event is only published by the decode graph;
   BCG prefill replays fall back to waiting for the whole in-flight forward,
   turning batch-assembly D2H syncs into a ~90 ms per-step wall.
2. The mixed-chunk decode/prefill attention split is disabled under replay
   (MIXED normalizes to EXTEND and the metadata refresh drops the boundary).
3. The eager logits tail sizes its DP-attention gathers from the static
   batch's capture-time padded token counts, so the vocab all-gather and
   hidden dp_gather run over every padded token (e.g. 8x2048 rows) instead
   of the sampling positions.

This PR: pre-builds the sparse-prefill chunk cache (sync-free via CPU-side
total_swa) and the split boundary in the replay-prepare hook, publishes the
WAR read-done event from the prefill runner when the backend opts in, and
passes the live logprob token counts to the eager logits tail. BCG for
DSV4+megamoe+DP-attention is enabled via SGLANG_EXPERIMENTAL_FORCE_BCG=1,
which bypasses the four incompatibility rules pending proper graduation.

8xB300 DSV4-Pro FP4, 8k1k conc 2048, radix forced-miss, num_prompts 6x conc:
9828 -> 10076 tok/s (+2.5% over the eager mixed-split baseline), TPOT
177.2 -> 172.5 ms, GSM8K 0.985 vs 0.980 eager control.
@gemini-code-assist

Copy link
Copy Markdown
Contributor

Warning

You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant