Repository navigation
[Perf] [NVIDIA] dspark: Move the host work between DSv4.1 decode graphs into the graphs for the megakernel path - #45450
Conversation
469d319 to
6ded689
Compare
|
@Oasis-Git Two more commits are now appended to this PR (7036147, 02d5caf):
AgentX c1, GB300 TP4, SGLang main
The two commits add +4.6% P50 / +7.8% P90 on top of the rest of the PR. CPU tests and GPU kernel tests pass on both commits (GPU: 168 / 168). Note: on a second cluster, long-context requests showed an extra 45-105 ms stall before the last streamed token; it follows that cluster's NCCL cuMem / MNNVL settings, not these commits, and the numbers above are from the cluster without it. |
02d5caf to
2fc44e4
Compare
…is captured A captured metadata glue graph reads runner slots and backend buffers by address, so a replay whose key it has captured needs neither the replay ForwardBatch view nor the out_cache_loc re-point; building the view costs host time on every step, in the target verify and the draft runner alike, between the draft and the verify. SGLANG_OPT_SKIP_GLUED_REPLAY_VIEW (default off): the decode graph runner's replay metadata prep, now _init_replay_attn_metadata, replays SGLang's own captured glue graph without the view and builds the view only for the glue's warm-ups and capture or the eager prep. A staged early verify metadata step only reinstalls. The routing guards and keys are unchanged; a plugin-supplied glue still receives run(view, key).
…launch SGLANG_OPT_DSV41_DSPARK_SIM_DRAW_AHEAD (default off; inert without SGLANG_SIMULATE_ACC_LEN): the step's simulated correct length and, under the simulate-fold bonus, its temperature / seed staging run before the draft graph launch instead of between the draft and the target verify. At small batches the host is the critical path in that window, while before the draft it waits on the previous verify graph anyway. Neither write reads anything the draft produces: the draw uses the host RNG, the bonus staging the batch size and request temperatures; the draft path draws no host randomness, so fixed seeds give the same values in either order. The sim bonus buffers are read only by a folded verify accept, so a step that turns out not fold-eligible ignores them. Real acceptance (the ACC gate) is unchanged.
… verify accept A sampled DSpark verify step softmaxed the full draft and target logits and ran the single-CTA classic rejection kernel. The block-wise sampler reads per-vocab- block max / sum-exp stats instead, accepts with one warp per request, and resamples the rejected row's residual with a block-parallel Gumbel-max. It draws (accept length, bonus) from the exact chain speculative-sampling law; only the random stream differs. Its draws are counter-based: a function of the server seed, a device step counter, row and token. Every TP rank holds the same seed, counter and logits, so the ranks agree without an accept broadcast, and a CUDA graph replay advances the counter like an eager call. Scope: CUDA, no top-k / top-p, no block verification; anything else keeps the classic kernel. Behind SGLANG_OPT_DSV41_BLOCKWISE_REJECTION (default off).
…re-sampling accept The static verify graph folds only the greedy accept. On an all-sampled step the rest ran eagerly after the replay: draft and target softmax, the rejection kernel, the bonus gather, the accept-site TP broadcast, the engram history commit, the future relay and the target-hidden KV commit. With the megakernel verify shorter than that host enqueue, the GPU idled on the host after every verify. SGLANG_OPT_DSV41_SAMPLING_ACCEPT_GRAPH (default off) captures that region once per verify-graph batch size and replays it right after the verify graph. It reads the verify graph's static inputs and outputs (StaticVerifyIO) and the draft sampler's static buffers, so a replay stages nothing, and its outputs land in the epilogue's accept buffers like the greedy fold. The region and the eager accept share one RejectionNoise, so a replay draws what the eager block-wise accept draws at the same counter. Greedy steps, top-k / top-p, logit adjustments, grammar and a simulated sampled bonus keep the eager accept.
88f6731 to
6410850
Compare
nvpohanh
left a comment
There was a problem hiding this comment.
[by Claude Code] This review is done by agents. Please feel free to disagree if they do not make sense.
[by Claude Code] Severity: style | Confidence: High
Issue
The Accuracy Tests and Speed Tests sections report results but give no commands, so others cannot rerun them. "All flags above on" does not list the env vars and values used, and the DSpark speculative arguments and megakernel plugin settings are not shown. The isolated A/B and DEP2 comments have the same gap.
Fix
For each table, add the exact server launch command (model path, TP, DSpark draft and speculative arguments, every env var with its value, and the plugin setup), plus the GSM8K, needle, and AgentX benchmark commands with their settings.
| self._skips_prefilled_inputs = self._resolve_skips_prefilled_inputs() | ||
| if self._draft_metadata_glue or self._target_metadata_glue: | ||
| self._metadata_glue = MetadataGlueGraph(self.device) | ||
| atexit.register(self._log_draft_glue, " at shutdown") |
There was a problem hiding this comment.
[by Claude Code] Severity: functional | Confidence: Medium
Issue
atexit.register(self._log_draft_glue, ...) holds a strong reference to this runner until the process exits. The draft glue is on by default for the DSpark draft runner, so that runner, its CUDA graphs and its buffers can never be freed.
Example
A weight update with recapture_cuda_graph=True calls ModelRunner.recapture_decode_cuda_graph, which drops the old runner and captures a new one. The old runner stays alive through the atexit entry, so each recapture keeps another set of graphs in GPU memory and adds another shutdown log.
Fix
Remove the atexit hook; _maybe_log_draft_glue already logs the counts every 1000 steps. If the shutdown line is needed, register through a weakref.WeakMethod.
| return False | ||
| self.discard() | ||
| matches = staged.key == key and all( | ||
| getattr(forward_batch, name) is not None |
There was a problem hiding this comment.
[by Claude Code] Severity: style | Confidence: High
Issue
take reads the staged inputs with getattr(forward_batch, name). SGLang does not allow getattr here. The field names come from the STAGED_SLOTS strings, so type checks and grep cannot see which ForwardBatch fields this compares.
Fix
Store the four sources as named fields, for example a msgspec.Struct with positions, out_cache_loc, req_pool_indices and seq_lens, and compare forward_batch.positions and the others directly. The new tests repeat the pattern (test_dspark_accept_tail.py:196, test_dspark_sampling_accept_graph.py:186 and :208); msgspec.structs.asdict(...) can replace those calls.
| self.out_tokens_buf = torch.zeros( | ||
| (self.max_bs, self.stride), dtype=torch.int64, device=device | ||
| ) | ||
| # SGLANG_DSPARK_FUSED_ACCEPT_TAIL. With the accept-site broadcast selected, only |
There was a problem hiding this comment.
[by Claude Code] Severity: style | Confidence: Medium
Issue
Many new comments and docstrings run three to six dense lines and are hard to read. This one packs two conditions and a reason into one sentence. Other examples: the run_static docstring (dspark_verify.py:745), stage_verify_metadata_early (decode_cuda_graph_runner.py:1857) and _install_early_verify_metadata (deepseek_v4_backend.py:2628). The repo's .claude/rules/comment-style.md asks for one or two lines.
Fix
Keep one short line of rationale, for example # The fused tail cannot run the greedy accept's TP broadcast; use it only without the broadcast or under simulate_fold. Put longer contracts in the module docstrings, which already describe them.
…aging - Drop the glue-count atexit hook, which kept the draft runner and its graphs alive across a CUDA graph recapture; the periodic debug line stays. - Stage the early verify inputs as a named VerifyInputSources struct instead of reading ForwardBatch fields by name with getattr; tests compare structs via msgspec.structs.asdict. - Shorten the long comments and docstrings added by this PR to one or two lines.
|
CI pass with three known issues: https://github.com/sgl-project/sglang/actions/runs/39830030045/job/120003967874 |
|
[Codex]: As requested, run only the three previously failing tests on the updated PR head b69cdf6: DSpark decode/prefill logprob consistency, dynamic LoRA update consistency, and UNO CUDA graph parity. This branch now includes the upstream lm_head precision fix. Use /rerun-test with the three specific test methods; this is targeted validation, not a full CI rerun. |
|
/rerun-test test/registered/core/test_basic_sanity_dspark.py::TestBasicSanityDSpark.test_logprob_decode_match_prefill test/registered/lora/test_lora_update.py::TestLoRADynamicUpdate.test_dynamic_lora_update_server test/registered/spec/uno/test_uno.py::TestUnoCudaGraph.test_ar_parity_and_nontrivial_tpf |
|
🚀 |
|
[Codex]: Targeted CI passed on b69cdf6: all three requested H100 tests executed and passed—DSpark decode/prefill logprob consistency, dynamic LoRA update consistency, and UNO CUDA graph parity. DSpark maximum logprob differences were 0.077889 and 0.059973. This validates only these three tests, not full CI. |
|
Failed CI Pass |
…ck too sgl-project#45450 added block_accept_lens_buf, written by the static accept tail and handed to GenerationBatchResult as a view of the graph buffer; the overlap readback copies it on copy_stream after the next replay may have rewritten it, the same hazard snapshot_accept covers for the other accept outputs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Motivation
DeepSeek-V4.1-Flash low-latency decode on GB300 (TP4, DSpark speculative decoding). With the NVIDIA megakernel plugin enabled (
--enable-nvidia-megakernel), the GPU part of each decode step gets short enough that SGLang's host-side work between graphs (input preparation, verify metadata, accept bookkeeping, KV commits) becomes a large share of the step. This PR moves that work into the existing CUDA graphs, into fewer launches, or off the critical path.Most changes are behind environment flags that default to off. Two DSpark-only changes are on by default: the draft sampling-param restaging (1) and the draft runner's metadata prep through the glue graph where it applies (2). The two changes that reach beyond DSpark are opt-in: the MoE output-shape fix (3,
SGLANG_OPT_MXFP4_DEFERRED_FINALIZED_SHAPE) and the CUDA graph fill fast paths (7,SGLANG_OPT_CUDA_GRAPH_FILL_FAST_PATH).Modifications
One commit per change:
SGLANG_OPT_MXFP4_DEFERRED_FINALIZED_SHAPE): pass the finalized output shape on deferred-finalize calls above 32 rows, so decode batches hit the tuned tactic instead of the default tile. Off keeps main's 97-384-row window.SGLANG_DSPARK_FUSED_ACCEPT_TAIL): the static greedy accept and its bookkeeping in one kernel.SGLANG_OPT_DSV41_DSPARK_PREPARE_INPUTS_ONCE,SGLANG_OPT_DSV41_DSPARK_MID_FOLD): the step inputs in one launch; the verify ids come from the draft graph's tail.SGLANG_DSPARK_DEFER_CONTEXT_KV): the static verify's draft context-KV commit moves to the next draft graph.SGLANG_OPT_CUDA_GRAPH_FILL_FAST_PATH): skip the padding reset of an unpadded fill; copy one dense integer dtype pair with one foreach launch.SGLANG_OPT_DSV41_TARGET_VERIFY_METADATA_GLUE).SGLANG_OPT_DSV41_POST_FORWARD_CUDA_GRAPH).SGLANG_OPT_DSV41_DSPARK_DRAFT_PREP_FORK).SGLANG_OPT_DSV41_DSPARK_POST_FOLD).SGLANG_OPT_DSV41_DSPARK_ONE_LAUNCH_COMMIT_STAGE).SGLANG_OPT_DSV41_TARGET_METADATA_EARLY; enabled only when the runner's conditions hold, including the metadata glue graph).SGLANG_OPT_DSV41_DSPARK_SKIP_PREFILLED_INPUTS).SGLANG_OPT_SKIP_GLUED_REPLAY_VIEW).SGLANG_OPT_DSV41_DSPARK_SIM_DRAW_AHEAD). Simulated acceptance only.SGLANG_OPT_DSV41_BLOCKWISE_REJECTION).SGLANG_OPT_DSV41_SAMPLING_ACCEPT_GRAPH): the sampling accept, its TP broadcast and the KV / engram commits replay as one graph launch.Accuracy Tests
GB300, TP4, DeepSeek-V4.1-Flash, megakernel plugin on, all flags above on, real acceptance (commands under Reproduce below):
Both commits were confirmed active in the first run (block-wise rejection on sampled verifies, the sampling-accept region captured, 4 / 4 ranks). The needle test varies between 22 and 27 / 27 from run to run on identical code.
Speed Tests and Profiling
GB300, TP4, DeepSeek-V4.1-Flash. AgentX (InferenceX agentic traces), concurrency 1, 3,600 s, synthetic acceptance 3.51 with the stock accept sampling (the simulated-acceptance shortcut is reverted in every arm). All arms run on SGLang main
f0b65907227with the same image and settings, and are compared on the same 220 requests. TPS/User = 1 / ITL.Rows with this PR were measured with the changes listed above enabled, including
SGLANG_OPT_MXFP4_DEFERRED_FINALIZED_SHAPE=1andSGLANG_OPT_CUDA_GRAPH_FILL_FAST_PATH=1(on by default at measurement time). With the plugin held fixed, this PR adds +30.6% P50 / +35.6% P90 (rows 3 and 5). Rows 4 and 5 also enable three opt-in plugin slots. Without the plugin, the host work this PR removes is hidden behind the longer GPU step, so the PR alone moves little. Rows 1-3 repeat on a second cluster within 0.3% at P50.Known interaction: with
NCCL_CUMEM_ENABLE=1andNCCL_MNNVL_ENABLE=1set in the environment, the deferred context-KV commit (SGLANG_DSPARK_DEFER_CONTEXT_KV) adds a 45-105 ms stall before the last streamed token of requests above 256K tokens (87 / 209 such requests in one AgentX run). The stall grows with context length. With both NCCL variables at 0, no request stalls (0 / 209) and P90 rises 4.5%. The table above was measured without these variables.Reproduce
Code and setup. One GB300 node (4 GPUs), image
lmsysorg/sglang:nightly-dev-cu13-20261020-ab580b2e, modeldeepseek-ai/DeepSeek-V4.1-Flash(revisiondf42c109f1d). Every tree is onmainatf0b65907227with #45421 reverted (git revert 7d9bc104932): underSGLANG_SIMULATE_ACC_LENthat commit skips the accept sampling on CUDA.f0b65907227+ the revert98891329ced)227d2a4934e)The current head carries the same commits on a newer main plus review fixes. On it, also set
SGLANG_OPT_MXFP4_DEFERRED_FINALIZED_SHAPE=1andSGLANG_OPT_CUDA_GRAPH_FILL_FAST_PATH=1(commits 3 and 7 had no switch when measured). Plugin for Speed 3-5 and Accuracy: sgl-project/sgl-nvidia-mega547bf4486e9, installed over the image's copy withpip install --no-deps -e .; it stays inactive without--enable-nvidia-megakernel.Server (Speed Tests).
AgentX client (SemiAnalysisAI/InferenceX
ea0db0384,benchmarks/srt_agentic.sh, run withCONC=1 DURATION=3600):TPS/User P50 / P90 = 1 / p50 and 1 / p90 of the per-request ITL in
profile_export.jsonl(profiling phase, failed requests dropped). Every row is read on the same 220 requests, matched by conversation and turn; over each whole run (391-492 requests) the values are within 0.8% of the table. Decode step p50 (> 256K) comes from TP0'sDecode batchlines with one running request: accept length / gen throughput, lines above 262,144 tokens.Accuracy Tests. Tree
227d2a4934e, the plugin with the 42 slots above, real acceptance (noSGLANG_SIMULATE_ACC_*), the server args above with--max-running-requests 256. The two columns differ only inSGLANG_OPT_DSV41_BLOCKWISE_REJECTIONandSGLANG_OPT_DSV41_SAMPLING_ACCEPT_GRAPH(1 vs 0). These runs used imagenightly-dev-cu13-20261012-e6072935with FlashInfer 0.7.2 and sglang-kernel 0.4.10, the versions in the image above.openai/gsm8k(main) test questions, 5-shot from the first five train examples, DeepSeek-V4.1 chat format with thinking off, sent to/generatewithmax_new_tokens512 and 8 requests in flight, temperature 0.0 or 1.0. An answer is correct when its last number equals the reference's. Accept length = Δsglang:generation_tokens_total/ Δsglang:spec_verify_calls_totalover the pass./v1/completionswithmax_tokens16 after a cache flush.Checklist
CI States
Latest PR Test (Base): 🚫 Run #39872814449
Latest PR Test (Extra): ❌ Run #39872814456
Latest PR Test (AMD ROCm 10): ⏳ Run #39872814641