Skip to content

[DSpark] Support pipeline-parallel targets in disaggregated and aggregated serving (+ padded graph batch safety) - #53577

Closed
lucifer1004 wants to merge 25 commits into
vllm-project:mainfrom
lucifer1004:pr/dspark-pd-pp-graph-v2
Closed

lucifer1004 wants to merge 25 commits into
vllm-project:mainfrom
lucifer1004:pr/dspark-pd-pp-graph-v2

Conversation

@lucifer1004

@lucifer1004 lucifer1004 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

[DSpark] Support pipeline-parallel prefill in disaggregated serving (+ padded graph batch safety)

Two stacked commits:

  1. feat(dspark): support PP prefill in disaggregated serving
  2. fix(dspark): make padded graph batches safe

Purpose

PD-disaggregated serving with DSpark previously forced the prefill (producer)
side to run without speculation: the DFlash/DSpark drafter does not support
pipeline parallelism, so a PP>1 producer could not warm the draft context, and
the decode-side drafter started cold (measured per-token acceptance dropped
from ~45% to ~35% in PD vs aggregated serving).

This PR materializes the DSpark draft's context KV on PP prefill workers
without running the decode-only draft path:

  • SpeculativeConfig.is_dspark_prefill_only() detects the producer role
    (dspark + PP>1 + kv_producer, fed by the new target_kv_transfer_config).
  • On such producers the draft model is built in a context_kv_only mode: only
    the context-KV projection (main_proj/main_norm + per-layer fused wq_a/wkv,
    kv_norm, rotary, SWA cache layer) is constructed and loaded; embedding,
    lm_head, Markov/confidence heads are skipped; drafting tokens raises.
  • DFlashSpeculator.propose() is split so materialize_context_kv() runs the
    context-KV projection without token generation; the prefill model runner
    calls only this path and skips the rejection sampler, draft CUDA graphs,
    EPLB registration, and adaptive verification. The draft parallel config is
    forced to PP=1, and all auxiliary hidden states must live on the last
    pipeline stage (explicitly validated).
  • Connector requirements: the transfer of the materialized draft KV relies on
    connectors that register and align named per-layer KV regions (verified with
    MooncakeConnector, which aligns producer/consumer regions by layer name and
    pulls from every producer PP stage). The NIXL connector does not yet describe
    packed KV regions per producer stage, so it raises a loud
    NotImplementedError for this configuration instead of silently
    mis-transferring. NIXL packed-region support is left to the [KVConnector][NIXL] Support attention-HMA layouts in pipeline-parallel push prefill #50494/[KVConnector][NIXL] Support packed MLA KV layouts in pipeline-parallel push prefill #50499
    line of work.

The second commit makes padded CUDA-graph batches safe by tracking and masking
graph-padding rows through DFlash input preparation and DSV4 routing,
preserving compact request-slot mappings, and allowing an optional fixed DSpark
graph batch size.

Relationship to existing work

Test plan

  • pytest tests/config/test_dspark_prefill_only.py tests/models/test_deepseek_v4_mega_moe.py tests/v1/spec_decode/test_dflash_prepare_inputs.py tests/kernels/moe/test_topk_softplus_sqrt.py — all pass.
  • pre-commit run --from-ref upstream/main --to-ref HEAD — all hooks pass.
  • E2E on 2x4 RTX 6000 Pro (SM120), DeepSeek-V4-Flash-0731 NVFP4, PD with
    PP2xTP2 prefill (node A) + TP4 decode (node B) over MooncakeConnector,
    DSpark draft-context materialization enabled: both roles reach readiness,
    CUDA-graph capture succeeds, and an 8K/1K random benchmark across six
    concurrency levels completes with zero failed requests and active draft
    acceptance (36% accepted/drafted tokens this run). Note: current main
    additionally needs [Bugfix][SM120] DSv4: pass contiguous C128A decode topk indices on SM120 #53574 (one-line C128A contiguity fix) to boot on SM120
    at all; the E2E above was run on this PR branch stacked with that fix.
  • Acceptance-recovery measurement (34.6% → 47.0% draft acceptance in PD vs
    aggregated parity at 41-48% under an 18K/3K workload) was taken on our full
    integration tree, which carries the same two commits plus unrelated
    SM120-enablement work; the mechanism under test is identical.

Notes

AI assistance (Kimi Code / OpenAI Codex) was used in preparing this change;
every line was reviewed and tested by the submitting human.


Update 2026-09-03: extended to aggregated (IFB) serving + PP

The branch now carries a fresh merge with main (ee17d0d8) plus three new
commits:

  1. feat(dspark): support pipeline-parallel targets in aggregated serving
  2. fix(pp): make warmup deadlock-free under pipeline parallelism
  3. fix(pp): complete the sampled-token broadcast contract for spec decoding

What changed:

  • SpeculativeConfig.use_dspark_last_stage_drafter() replaces the
    kv_producer-only check: a DSpark drafter under a PP>1 target always runs
    with a draft-local PP=1 config, in aggregated serving as well as in PD.
    The PD prefill-only paths are unchanged (all new logic is gated on
    num_speculative_steps > 0, which is 0 for that mode).
  • Two PP broadcast deadlocks fixed, both with the same mechanism — a
    spinning, unmatched NCCL kernel blocking the host-side CUDA module load of
    a first-time triton compile:
    • the sampled-token broadcast is disabled during warmup (its outputs are
      discarded there anyway), and the deferred post-update kernel is
      explicitly pre-warmed on non-last ranks;
    • the wire shape is now constant: plain-sampler [N, 1] payloads are
      padded to [N, max_sample_len], and the (2, N) counts buffer is padded
      so both views stay 16-byte aligned (triton specializes on pointer
      alignment; a misaligned view compiled a second kernel variant at serving
      time).
  • Draft tokens are broadcast to non-last PP stages after propose() and
    adopted into their req_states.draft_tokens. Previously those stages
    embedded token id 0 for every draft slot, which would have produced garbage
    verification logits in aggregated serving (PD prefill-only never needs
    drafts on the producer).

Validation (8x RTX 6000 Pro SM120, DeepSeek-V4-Flash-0731 NVFP4, aggregated
IFB PP2xTP2 + DSpark, b12x MoE backend):

  • pytest tests/v1/worker/test_pp_utils.py tests/config/test_dspark_prefill_only.py tests/models/test_deepseek_v4_mega_moe.py tests/v1/spec_decode/test_dflash_prepare_inputs.py — 45 pass;
    pre-commit run — all hooks pass.
  • GSM8K strict-match (full 1319): dspark pp2tp2 = 0.9507, dspark tp4 = 0.9484,
    target-only tp4 = 0.9469 — within noise, distribution preserved.
  • Draft acceptance: 65-69% (PP) vs 67.4% (non-PP) — parity.
  • Standardized 8K/1K corpus sweep (real text, cold cache per level, C1-C64,
    zero failed requests): PP costs ~22-35% at C1-C16, reaches parity at C32,
    and leads at C64 (508 vs 416 tok/s). As expected, PP in aggregated serving
    is a capacity play, not a decode-throughput one.

AI assistance (Kimi Code) was used for this update as well; all changes were
reviewed and tested by the submitting human.

Review follow-ups (2026-09-04)

Addressing review findings with 0f851e2c5d:

  • NIXL detection for the prefill-only guard now uses
    KVTransferConfig.has_connector(), catching NixlConnector nested in a
    MultiConnector.
  • The scheduler's use_eagle_block_drop now follows the narrowed
    use_eagle, so a DSpark prefill-only producer keeps its trailing
    prefix-cache block.
  • DFlashSpeculator.propose rebuilds the reused DP-sync token count when
    the fixed DSpark graph batch re-pins the dispatch shape (dp_size > 1
    assertion path; dp_size = 1 behavior unchanged and re-covered by the IFB
    PP2xTP2 smoke run).

Update 2026-09-04 (later): Kimi-K3 cross-stage aux taps

New commit 6. feat(dspark): support Kimi-K3 targets with cross-stage aux hidden taps

At the previous head, K3 + PP + DSpark crashed at startup: the aux-tap check
in load_dspark_model read dspark_target_layer_ids directly, which
speculators-format draft configs (K3) do not define, and it required every
tap on the last stage while K3's five taps ([24, 48, 72, 88, 92]) span the
whole model. The check now resolves taps via the shared
get_eagle3_aux_layers_from_config fallback chain (value-identical for
DeepSeek-V4's dspark_target_layer_ids), and targets may opt into
cross-stage transport with supports_pp_aux_hidden_state_transport;
KimiLinearModel packs earlier stages' taps into IntermediateTensors and
unpacks them downstream (stage-entry capture stays first-stage-only so
boundary taps are not captured twice).

Validation (8x RTX 6000 Pro SM120):

  • pytest tests/config/test_dspark_prefill_only.py tests/models/kimi_k3/test_eagle3.py — 21 pass; prek run on changed files — all hooks pass.
  • DeepSeek-V4-Flash-0731 DSpark PP2xTP2 GSM8K strict-match (full 1319): 0.9484, vs 0.9462 pre-change — no regression.
  • Kimi-K3-pruned75 + RedHatAI/Kimi-K3-speculator.dspark, GSM8K (1319q, 5-shot, temp 0, official tests/evals/gsm8k shape): DSpark PP2xTP4 = 0.2570 vs TP8 = 0.2684 (McNemar z = 1.08, ns); target-only PP2xTP4 = 0.3336 vs TP8 = 0.3351 (z = 0.13, ns). Draft acceptance length 5.3-6.3 (gate 4.8). PP has no measurable quality impact on K3 DSpark.

Two honest notes: (1) on this pruned K3 checkpoint DSpark scores ~7pp below
target-only at temp 0 under both TP8 and PP2xTP4 — speculator-side, not PP;
flagged separately. (2) K3 PD with PP (KDA state transfer) is not covered by
this change. AI assistance (Kimi Code); all changes reviewed and tested by
the submitting human.

Update 2026-09-06: rebased on main (52358e6) + four fixes

Upstream #50514 landed a generic EAGLE3-over-PP aux-hidden-state relay, so the
merge adopts that mechanism wholesale and drops this branch's earlier custom
transport. Both DSv4 and K3 opt in via supports_aux_hidden_states_over_pp.
Four fixes on top of the merge:

  • fix(dspark): let PP drafters load their own embedding when the target's is stranded (4728759) — under PP the target's embedding table lives on the
    first stage, so a drafter on a later stage cannot alias it. K3/DSv4 DSpark
    drafters now load the checkpoint's own embed copy
    (loads_own_embed_under_pp) instead; without this, DSv4 DSpark + PP raises
    at load time on stock main.
  • fix(dspark): address review findings on topk uint32 dispatch and context-KV construction (1bb49fb) — uint32+padding is excluded from
    the dsv4_topk fast path (its -1 sentinel requires signed indices);
    context-KV-only draft layers build the attention submodule directly instead
    of a whole decoder layer, removing a transient MoE allocation that could
    OOM on memory-tight GPUs.
  • fix(pp): do not double-post draft broadcasts when a speculator ran
    (5b572da) — the merge kept two broadcast_drafts call sites on the last
    PP rank (one in the propose() path, one next to set_draft_tokens() from
    [Core][MRV2] Support eagle3 spec decode with pipeline parallel #50514). With a speculator present both fired per step while earlier stages
    post one recv: the extra send shifted the pp_broadcast FIFO, the next
    step's sampled-token recv paired with a leftover draft broadcast of a
    different size, and the mismatched NCCL collective spun on-device, blocking
    any later context-level CUDA call and deadlocking spec-decode + PP
    deterministically. Reproduced on DSv4 pp2tp2 and K3 pp2tp4 (non-spec PP
    unaffected); fixed by broadcasting from the handler path only when there
    is no speculator (e.g. diffusion-style drafts).
  • fix(k3): keep the DSpark draft's marker from flagging the target's KV group (0a2f859) — the K3-native DSpark draft's
    non_causal_multi_token_decode MLA layers merged with the target's
    identical-geometry MLA layers (OR-merge since [Bugfix][MLA] Restore DSpark cache-group capability under optimized Python #55234), flagging the
    target group and misrouting its short prefills and causal verification
    blocks into TritonMLA's single-row decode path: NaN logits from the
    first token, acceptance 0, and an IMA under graph capture. The draft's
    spec now carries a distinct model_version so the groups stay separate.
    Verified on Kimi-K3-pruned75 + Inferact/Kimi-K3-DSpark (SM120): tp8
    eager and pp2tp4 graphs both sane, draft per-position acceptance 0.85.

Validation on this branch (RTX PRO 6000 Blackwell, SM120):

  • pytest tests/v1/worker/test_pp_utils.py tests/v1/worker/test_spec_decode_embed_sharing_pp.py tests/v1/worker/test_mixed_warmup_gate.py tests/models/kimi_k3/test_eagle3.py tests/models/kimi_k3/test_dspark_mla.py — all pass (incl. new mapper and
    embed-under-PP tests); tests/kernels/moe/test_topk_softplus_sqrt.py incl.
    the new uint32+padding fallback case passes on GPU; pre-commit run clean.
  • E2E DeepSeek-V4-Flash-0731 DSpark in-flight-batching PP2xTP2, GSM8K:
    exact_match (strict) = 0.9568.
  • E2E Kimi-K3-pruned75 DSpark in-flight-batching PP2xTP4, GSM8K 5-shot
    (1319 questions): accuracy 0.2798, invalid 0.0015 — in line with the
    pre-merge 0.2570 reference on this weak pruned checkpoint.
  • Note: with the native (unpruned) Inferact/Kimi-K3-DSpark drafter, the
    target's triton_mla decode kernel hits an illegal memory access on SM120
    during profiling, also reproducible with TP8 alone, independent of PP and
    of this PR's changes; tracked separately.

AI assistance (Kimi Code) was used for this update as well; every line was
reviewed and the validation above run by the submitting human's setup.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify mergify Bot added the mrv2 Model Runner V2 specific label Aug 24, 2026
@mergify

mergify Bot commented Sep 12, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @lucifer1004.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 12, 2026
Keep the None guard around get_mtp_target_hidden_states() alongside the
new GPUWatermarkSampler preparation from upstream.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
@mergify mergify Bot removed the needs-rebase label Sep 12, 2026
@mergify

mergify Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @lucifer1004.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 13, 2026
Adapt to the DFlash JIT-warmup migration (vllm-project#56323): prepare_dflash_inputs
now returns a DispatchSpec, with our out_is_padding_ptr output added to
the launch dict and warmup inputs. Sampler init moves under the
jit_warmup_registry block, keeping the dspark_prefill_only gate on the
rejection sampler.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
@mergify mergify Bot removed the needs-rebase label Sep 13, 2026
@mergify

mergify Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @lucifer1004.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 13, 2026
Follow the revert of the Triton JIT warmup migration (vllm-project#56654):
prepare_dflash_inputs launches the kernel directly again, with our
out_is_padding_ptr argument kept; sampler init dedents back out of the
jit_warmup_registry block, keeping the dspark_prefill_only gate.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
@mergify mergify Bot removed the needs-rebase label Sep 13, 2026
@mergify

mergify Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @lucifer1004.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@mergify mergify Bot added the needs-rebase label Sep 15, 2026
Take upstream's use_confidence_head attribute declaration in
DSparkSpeculator.__init__ alongside our confidence/adaptive-verification
and fixed-graph-batch setup.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
@mergify mergify Bot removed the needs-rebase label Sep 15, 2026
@zyongye

zyongye commented Sep 15, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

❌ This PR is 4 commits behind upstream main. Your branch must contain every commit currently on upstream main. No new CI build was started. Merge or rebase onto the latest main, then rerun /ci run. To test this branch at your own risk, use /ci run --allow-stale.

@zyongye

zyongye commented Sep 15, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #88978 for commit 6dceb4f72134.

- warmup: short-circuit on is_last_pp_rank before reading pp_handler so
  SimpleNamespace runner stubs without the attribute keep working.
- test_gpu_model_runner_v2: the QSA runner stub bypasses __init__, so set
  dspark_prefill_only explicitly.

Co-authored-by: Kimi Code <noreply@moonshot.cn>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>

@GirasoleY GirasoleY left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The changes feel a bit wider than expected for supporting PP + Dspark. Could we maybe break it down to a few isolated PRs for easy review? High level it seems like we can have

  • DSpark + PP aggregated serving
  • DSpark + PP prefill + KV transfer
  • K3 cache fix

GirasoleY

This comment was marked as duplicate.

@lucifer1004

Copy link
Copy Markdown
Contributor Author

Per @GirasoleY's review, this PR is now split into three isolated pieces:

Each carries its own tests and e2e evidence (GSM8K 0.9545 on the IFB PP arm). Closing this one in favor of the series; thanks for the review!

@github-project-automation github-project-automation Bot moved this from Backlog to Done in Sprint - DFlash Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

4 participants