[Spec] Add DSpark: confidence-scheduled speculative decoding - #30261
Conversation
…ssive drafting Co-Authored-By: Claude Code <noreply@anthropic.com> Co-Authored-By: Codex <noreply@openai.com>
…: ParallelState through DSparkWorkerV2 Upstream #30261 (DSpark) added DSparkWorkerV2 with the legacy flat draft-rank __init__ (tp_rank/dp_rank/moe_ep_rank/attn_cp_rank/moe_dp_rank). This chain's single-parallel-state refactor replaced those flat args with a single ps: ParallelState across every spec draft worker and build_draft_tp_worker, so the scheduler constructs draft workers with ps=self.ps. DSparkWorkerV2 was upstream-new and never migrated, so DSparkWorkerV2.__init__() got an unexpected keyword argument 'ps' (test_basic_sanity_dspark.py). Migrate it to match the sibling DFlashWorkerV2: take ps, store self.ps, pass ps=replace(ps, pp_rank=0) to build_draft_tp_worker, and read self.ps.tp_rank.
…: ParallelState through DSparkWorkerV2 Upstream #30261 (DSpark) added DSparkWorkerV2 with the legacy flat draft-rank __init__ (tp_rank/dp_rank/moe_ep_rank/attn_cp_rank/moe_dp_rank). This chain's single-parallel-state refactor replaced those flat args with a single ps: ParallelState across every spec draft worker and build_draft_tp_worker, so the scheduler constructs draft workers with ps=self.ps. DSparkWorkerV2 was upstream-new and never migrated, so DSparkWorkerV2.__init__() got an unexpected keyword argument 'ps' (test_basic_sanity_dspark.py). Migrate it to match the sibling DFlashWorkerV2: take ps, store self.ps, pass ps=replace(ps, pp_rank=0) to build_draft_tp_worker, and read self.ps.tp_rank.
DeepseekV4AttnBackend.__init__ reads model_runner.spec_algorithm (added by upstream DSpark #30261 in deepseek_v4_backend.py) but MockDSV4ModelRunner never set it, so test_deepseek_v4.py's dsv4-backend construction raised AttributeError: 'MockDSV4ModelRunner' object has no attribute 'spec_algorithm'. This is a pre-existing upstream gap (#30261 updated the backend but not the mock; upstream/main is red on it too), surfaced on our b200 CUDA lane. Set spec_algorithm=SpeculativeAlgorithm.NONE so is_none()/is_dspark() work, matching the non-spec DSV4 attention test. Mirrors how this mock already tracks is_draft_worker / ps to stay in sync with the real ModelRunner interface.
Cherry-pick of jialino/fix-dsv4-mock-spec-algorithm (standalone PR) to unblock this PR's CI: DSpark (sgl-project#30261) made DeepseekV4AttnBackend.__init__ read model_runner.spec_algorithm, which the attention-unittest mock lacks. Drop this commit (revert or rebase) once the standalone fix lands on main.
Cherry-pick of jialino/fix-dsv4-mock-spec-algorithm (standalone PR) to unblock this PR's CI: DSpark (sgl-project#30261) made DeepseekV4AttnBackend.__init__ read model_runner.spec_algorithm, which the attention-unittest mock lacks. Drop this commit (revert or rebase) once the standalone fix lands on main.
Cherry-pick of jialino/fix-dsv4-mock-spec-algorithm (standalone PR) to unblock this PR's CI: DSpark (sgl-project#30261) made DeepseekV4AttnBackend.__init__ read model_runner.spec_algorithm, which the attention-unittest mock lacks. Drop this commit (revert or rebase) once the standalone fix lands on main.
Enable CUDA draft-extend graph capture for DSV4 while handling the two correctness constraints at their source. Fixed-width graph padding uses seq_len=1 even when the query width is larger. Clamp expanded DSV4 causal lengths to 1 so padding positions are all zero and real requests remain unchanged. DSV4 also rereads the live full-to-SWA mapping during replay, so publish the WAR read-done event only after replay through the existing backend capability. Prior art: DSA fixed the same DP-padded length underflow by clamping expanded lengths in seqlens_expand_kernel and _fused_dsa_draft_extend_metadata_kernel (sgl-project#30378, bbc5370). DecodeCudaGraphRunner likewise records read-done after replay when captured breakable metadata rereads shared state (sgl-project#30261, 6cc9352). Validation: targeted production draft-extend capture/replay coverage checks both zero padding positions and replay-before-event ordering. A 30-minute no-profiler A/B of the replay-after event path completed with zero errors and changed average ITL from 7.996 ms to 7.930 ms and p99 from 11.257 ms to 10.977 ms.
Enable CUDA draft-extend graph capture for DSV4 while handling the two correctness constraints at their source. Fixed-width graph padding uses seq_len=1 even when the query width is larger. Clamp expanded DSV4 causal lengths to 1 so padding positions are all zero and real requests remain unchanged. DSV4 also rereads the live full-to-SWA mapping during replay, so publish the WAR read-done event only after replay. Declare defer_war_read_done_until_after_replay as an AttentionBackend capability with a default of false, and opt in only DSV4. This keeps the WAR timing contract typed and independent from the breakable-CUDA-graph captured-metadata contract. Prior art: DSA fixed the same DP-padded length underflow by clamping expanded lengths in seqlens_expand_kernel and _fused_dsa_draft_extend_metadata_kernel (sgl-project#30378, bbc5370). DecodeCudaGraphRunner likewise records read-done after replay when captured breakable metadata rereads shared state (sgl-project#30261, 6cc9352). Validation: targeted production draft-extend capture/replay coverage checks both zero padding positions and replay-before-event ordering. A 30-minute no-profiler A/B of the replay-after event path completed with zero errors and changed average ITL from 7.996 ms to 7.930 ms and p99 from 11.257 ms to 10.977 ms.
Conflicts resolved: - decode_cuda_graph_runner.py: kept the PP-proxy input-buffer refresh, adopted upstream's is_dflash_family()/is_ragged condition (sgl-project#30261), and renamed num_tokens_per_bs -> num_tokens_per_req in the PP proxy output slice (sgl-project#30977). - eagle_worker_v2.py: dropped the local _get_plan_stream (extracted to a shared get_plan_stream upstream, sgl-project#31008/sgl-project#30857) and re-applied the pp_proxy_tensors parameter on the deduplicated forward_batch_generation signature. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Cross-reference: compact CUDA-Graph stress investigation (#31023 / #31455)Cross-referencing a downstream DSpark stability investigation because it exercises paths beyond the blog's Figure 5 happy path. Mode distinctionThe official Figure 5 command uses: So this is the compact verify-all uniform-ragged path, with default overlap compared against The investigation in #31023 has two relevant reproducer families on an earlier development snapshot: The residual failure is an asynchronous CUDA illegal-memory access in target verification after the earlier cross-TP planning divergence was fixed by #31195. A strong Graph-OFF control survived, while completed default-off diagnostics did not find an actionable error in the covered host-visible metadata, explicit covered index bounds, translated Version caveatThe positive evidence was collected in a v0.5.14-series base environment while importing source at A1 A clean v0.5.16 regression matrix is now being prepared:
If clean v0.5.16 does not reproduce repeatedly, the next action will be a bisect from Upstream questionIs there already an internal or public stress/regression suite covering both of the following on TP8/B300? Any known post- |
Follow-up from #31023: clean v0.5.16 shape-specific decode Graph capture failure at
|
cuda-graph-max-bs |
Result |
|---|---|
| 4 / 8 / 16 / 32 | READY |
| 64 | bs=64 captures, then illegal memory at bs=60 |
| 128 | larger shapes through bs=64 capture, then illegal memory at bs=60 |
Tested environment:
image: iregistry.baidu-int.com/nccl/sglang:v0.5.16-cu130
sglang: 0.5.16
CUDA: 13.0
PyTorch: 2.11.0+cu130
model: DeepSeek-V4-Pro-DSpark
TP: 8
mem-fraction-static: 0.82
chunked-prefill-size: 4096
Each arm used a fresh server; GPU memory returned to 0 MiB afterward. The exception is surfaced from decode CUDA-Graph capture, but the exact faulting kernel is not yet known.
The detailed evidence and interpretation are tracked in #31023. The current classification is an expanded-shape startup regression, separate from the historical runtime compact+SPS Bug 2 unless a shared cause is later demonstrated.
Two upstream questions:
- Is
bs=60included in an existing DeepSeek-V4-Pro-DSpark TP8 CUDA-Graph capture regression on the pinneddev-dspark/692c5f7denvironment or current main? - Are there known backend-specific shape constraints around this capture tier that should be encoded in the capture-shape generator or test suite?
A reproduction on the official pinned image/source would help distinguish a core SGLang/DSpark issue from the specific v0.5.16-cu130 runtime stack.
Final downstream regression update: large-shape capture failure is non-deterministic, not one bad shapeA clean v0.5.16 downstream regression on DeepSeek-V4-Pro-DSpark / TP8 has now completed the key controls. Passing pathsFailing larger-shape capture pathAcross otherwise equivalent clean launches, the first illegal-memory exposure moved: An explicit This rejects the hypothesis of one uniquely corrupt shape. It instead indicates a non-deterministic illegal-memory fault in the larger DSpark decode CUDA-Graph capture path. The reported kernels are asynchronous surfacing locations, not confirmed faulting kernels. The historical compact+SPS c256 runtime Bug 2 remains inconclusive on v0.5.16 because the required larger Graph range fails during startup, while limiting capture to 32 forces the c256 workload predominantly onto eager decode. Current classification: A useful upstream regression would cover repeated TP8 capture of the larger decode shape set in addition to the published small-batch Figure 5 path. |
Two defects keep additive sampling penalties (e.g. the min_new_tokens stop-token suppression) from working under --speculative-algo dflash / dspark. They have to be fixed together: fixing only the first makes the second user-visible. 1. Stale field name. PR sgl-project#21258 renamed SamplingBatchInfo.acc_linear_penalties to acc_additive_penalties (and split out acc_scaling_penalties). Two speculative readers added after that rename were written against the old name and fetch it with getattr(..., None), so the stale key has silently resolved to None since the day each landed: dflash_utils .apply_dflash_verify_logits_adjustments (sgl-project#23000) and dspark_verify .verify_logits_adjustments_are_noop (sgl-project#30261). In overlap scheduling copy_for_forward() folds the penalizer into acc_additive_penalties and strips penalizer_orchestrator, so the dense fallback in DFlash verify does not trigger either. Additive penalties were therefore dropped from the verify logits, and the DSpark noop gate wrongly reported noop and allowed the folded greedy fast path. 2. Output tokens are never accumulated. cumulate_penalty_output_tokens() had exactly two callers: the non-spec decode path in ScheduleBatch.prepare_for_decode, and eagle_prepare_for_decode. The dflash family returns early into spec_prepare_for_decode and reaches neither, so BatchedMinNewTokensPenalizer.len_output_tokens stays 0 and the stop-token -inf mask never lifts. With (1) fixed that mask now does reach the verify logits, so a min_new_tokens > 0 request would have its stop tokens suppressed for the whole generation. Mirror EAGLE's accumulation hook on the dflash-family branch. Fixes sgl-project#33493. Signed-off-by: Han-Yin Chang <nick20350@gmail.com>
Two defects keep the min_new_tokens stop-token suppression from working under --speculative-algo dflash / dspark. They have to be fixed together: fixing only the first makes the second user-visible. 1. Stale field name. PR sgl-project#21258 renamed SamplingBatchInfo.acc_linear_penalties to acc_additive_penalties (and split out acc_scaling_penalties). Two speculative readers added after that rename were written against the old name and fetch it with getattr(..., None), so the stale key has silently resolved to None since the day each landed: dflash_utils .apply_dflash_verify_logits_adjustments (sgl-project#23000) and dspark_verify .verify_logits_adjustments_are_noop (sgl-project#30261). In overlap scheduling copy_for_forward() folds the penalizer into acc_additive_penalties and strips penalizer_orchestrator, so the dense fallback in DFlash verify does not trigger either. Additive penalties were therefore dropped from the verify logits, and the DSpark noop gate wrongly reported noop and allowed the folded greedy fast path. 2. The min_new_tokens seen-token counter never advances. The dflash family returns from ScheduleBatch.prepare_for_decode into spec_prepare_for_decode before the seen-token accounting runs, so BatchedMinNewTokensPenalizer.len_output_tokens stays 0 and the stop-token -inf mask never lifts. With (1) fixed that mask now does reach the verify logits, so a min_new_tokens > 0 request would have its stop tokens suppressed for the whole generation. Add an idempotent synchronization from the committed Req.output_ids. Speculative decoding commits a variable-length accepted run per step, so one-per-step accounting would undercount; req.output_ids is where the run _resolve_spec_v2_tokens settled is appended (already grammar-truncated, and left untouched for a retracted or already-finished request), so the counts are read from there. Assignment rather than accumulation makes replaying the sync a no-op. The hook is deliberately narrow: it reaches only the min_new_tokens penalizer, not the repetition / frequency / presence ledgers, which need the accepted token ids. It also self-gates on that penalizer being prepared, before building the host tensor, so requests using only the other penalizers pay nothing on the decode hot path. The remaining main-line repetition/scaling gaps are tracked by sgl-project#28180. PR sgl-project#28200 targets release/v0.5.13 and is not reused here. Fixes sgl-project#33493. Signed-off-by: Han-Yin Chang <nick20350@gmail.com>
DSpark speculative decoding — semi-autoregressive block drafting + confidence-scheduled, variable-length verify.
Follow-up work (separate PRs)
Kept out of this PR to limit its footprint; planned as follow-ups after it lands:
num_tokens_per_bsfamily (runners / graph runners / spec helpers) is misnamed — the value is per request, not per batch. Renaming it tonum_tokens_per_reqis mechanical (~240 internal occurrences).speculative_num_draft_tokensin place for draft workers; it should keep the server-args value verbatim and read a derivednum_tokens_per_reqfield instead.speculative_hook.pyduplicate the shared num_steps/topk forcing and spec batching defaults across algorithms.sglang.kernelsops layout; migrating them waits on untangling theirScheduleBatchcoupling.needs_cpu_seq_lensopt-out are keyed on flags fixed at construction. These are the only instance-keyed modes in the attention layer, where every other speculative path dispatches on per-batch data (spec input types, ragged layouts) or capability methods.draft_worker_common, shared with DFLASH) is undecided: a stateless builder module as today, or a shared block-draft worker base class owning the common identity/lifecycle code; the module name should follow whichever outcome.can_run_dp_cuda_graphunset, and the dp-attention sync gate ANDs it into graph admission, so the dense-draft block never replays cuda graphs under dp attention. Whether that conservatism is required (the draft forward is attention-TP-local) or a missed graph path needs GPU validation.SGLANG_DSPARK_*environment variables sit inside the generic test-and-debug block (plus one under spec config) instead of a dedicated section, and three boolean perf toggles (FAST_KERNEL,FAST_SAMPLING,FP32_LM_HEAD) carry no verb category token, unlike the siblingOPT_*entries. Renaming becomes an alias-deprecation exercise once released, so the regrouping and renames should happen soon after landing.CI States
Latest PR Test (Base): ✅ Run #29208178181
Latest PR Test (Extra): ❌ Run #29208178114