Skip to content

[Spec] Add DSpark: confidence-scheduled speculative decoding - #30261

Merged
hnyls2002 merged 114 commits into
mainfrom
sglang-dspark
Jul 12, 2026
Merged

[Spec] Add DSpark: confidence-scheduled speculative decoding#30261
hnyls2002 merged 114 commits into
mainfrom
sglang-dspark

Conversation

@sglang-bot

@sglang-bot sglang-bot commented Jul 6, 2026

Copy link
Copy Markdown
Member

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:

  • The num_tokens_per_bs family (runners / graph runners / spec helpers) is misnamed — the value is per request, not per batch. Renaming it to num_tokens_per_req is mechanical (~240 internal occurrences).
  • The fa3 backend rewrites speculative_num_draft_tokens in place for draft workers; it should keep the server-args value verbatim and read a derived num_tokens_per_req field instead.
  • The per-algorithm hooks in speculative_hook.py duplicate the shared num_steps/topk forcing and spec batching defaults across algorithms.
  • The DSpark Triton kernels live outside the sglang.kernels ops layout; migrating them waits on untangling their ScheduleBatch coupling.
  • The dsv4 backend carries algorithm-keyed instance state: the draft-block attention support and the needs_cpu_seq_lens opt-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.
  • The decode cuda-graph runner drives ragged verify as a parallel token-bucket-keyed mode beside the bs-keyed graphs, with its own capture/admission paths. Unifying the two keyings — uniform verify windows as the special case of token keying — would remove the duplicated capture machinery, but re-keys every algorithm's replay path and needs its own performance validation.
  • The final shape of the shared draft-worker construction layer (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.
  • Hand-built DSpark draft batches leave can_run_dp_cuda_graph unset, 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.
  • The confidence-relay staging (device-to-host confidence transfer for the scheduler) lives in the shared overlap-utils module while its only producer and consumer are DSpark components, and the planner imports its constants back from there; it also allocates its CUDA buffers lazily on first use. Relocating it into the DSpark component package (with construction-time allocation) would shrink the shared-file footprint.
  • The dsv4 attention-metadata builders (causal expansion, page-table/positions) were rewritten as triton kernels serving every dsv4 path, speculative or not. Triton-vs-torch parity is CI-covered; a dedicated performance validation of the non-speculative dsv4 paths on the new builders is still to be done.
  • The 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 sibling OPT_* 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

…ssive drafting

Co-Authored-By: Claude Code <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
fzyzcjy added a commit that referenced this pull request Jul 13, 2026
…: 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.
fzyzcjy added a commit that referenced this pull request Jul 13, 2026
…: 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.
fzyzcjy added a commit that referenced this pull request Jul 13, 2026
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.
Jialin added a commit to Jialin/sglang that referenced this pull request Jul 13, 2026
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.
Jialin added a commit to Jialin/sglang that referenced this pull request Jul 13, 2026
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.
Jialin added a commit to Jialin/sglang that referenced this pull request Jul 13, 2026
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.
weireweire added a commit to weireweire/sglang that referenced this pull request Jul 14, 2026
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.
weireweire added a commit to weireweire/sglang that referenced this pull request Jul 14, 2026
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.
AliceChenyy added a commit to AliceChenyy/sglang that referenced this pull request Jul 14, 2026
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>

Copy link
Copy Markdown

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 distinction

The official Figure 5 command uses:

SGLANG_RAGGED_VERIFY_MODE=compact
no SPS table
bs=1, input/output=256/256
cuda-graph-max-bs=4

So this is the compact verify-all uniform-ragged path, with default overlap compared against --disable-overlap-schedule.

The investigation in #31023 has two relevant reproducer families on an earlier development snapshot:

A. compact/no-SPS, bs=64, repeated and fresh-service CUDA-Graph replay
B. compact+SPS, c256, 60k/1k, 2560 requests, variable verify lengths and sustained multi-tier replay

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 out_loc, SITE4 resource structure, or result-copy handoff. The exact in-Graph faulting kernel/resource remains unknown.

Version caveat

The positive evidence was collected in a v0.5.14-series base environment while importing source at A1 22cfd54c, later with local default-off diagnostics. This is not yet a claim that clean v0.5.16 is affected.

A clean v0.5.16 regression matrix is now being prepared:

  1. the official-scale compact/no-SPS happy path;
  2. compact/no-SPS bs=64 stress replay;
  3. compact+SPS c256 60k/1k;
  4. Graph-OFF / overlap-OFF / static controls.

If clean v0.5.16 does not reproduce repeatedly, the next action will be a bisect from 22cfd54c to v0.5.16 rather than carrying the historical diagnosis forward.

Upstream question

Is there already an internal or public stress/regression suite covering both of the following on TP8/B300?

compact verify-all/no-SPS + sustained CUDA-Graph replay
compact+SPS variable verify + high-concurrency long-context replay

Any known post-22cfd54c change expected to harden these paths would also help narrow the release regression.

Copy link
Copy Markdown

Follow-up from #31023: clean v0.5.16 shape-specific decode Graph capture failure at bs=60

A clean downstream regression on DeepSeek-V4-Pro-DSpark / TP8 has isolated a deterministic startup-time failure outside the blog's Figure 5 shape range.

What passes

The exact Figure 5 startup scale reaches readiness:

SGLANG_RAGGED_VERIFY_MODE=compact
no SPS table
CUDA Graph ON
overlap ON
--cuda-graph-max-bs 4

So this is not a report that the official batch-1 Figure 5 happy path is broken.

What fails

Using the same compact/no-SPS mechanism family but expanding the decode Graph capture set:

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:

  1. Is bs=60 included in an existing DeepSeek-V4-Pro-DSpark TP8 CUDA-Graph capture regression on the pinned dev-dspark/692c5f7d environment or current main?
  2. 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.

Copy link
Copy Markdown

Final downstream regression update: large-shape capture failure is non-deterministic, not one bad shape

A clean v0.5.16 downstream regression on DeepSeek-V4-Pro-DSpark / TP8 has now completed the key controls.

Passing paths

Official Figure 5-scale path:
  compact / no SPS / max-bs=4 / bs1 / 256-256 -> PASS

Low-tier path:
  decode capture bs<=32 -> PASS
  compact+SPS sustained replay with max-running-requests=32:
    32/32 + 320/320 requests
    100% cuda graph: True
    zero illegal-memory / scheduler exceptions

Failing larger-shape capture path

Across otherwise equivalent clean launches, the first illegal-memory exposure moved:

bs=60  / TP7 / Triton surfacing point
bs=96  / TP4 / TRT-LLM fused-MoE routing surfacing point
bs=112 / TP6 / DeepGEMM surfacing point

An explicit --cuda-graph-bs-decode list that skipped bs=60 still failed at bs=96; bs=96 had captured successfully in another run. Approximately 42 GB remained free per GPU at failure.

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:

large-shape startup capture failure: confirmed
low-tier capture/replay: stable in tested matrix
historical runtime Bug 2 fixed: not established
shared root cause: plausible, not proven

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.

Smallfu666 added a commit to Smallfu666/sglang that referenced this pull request Aug 4, 2026
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>
Smallfu666 added a commit to Smallfu666/sglang that referenced this pull request Aug 5, 2026
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>
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