Harden DSpark and add SM120 PCIe serving support - #88
Conversation
The explicit add + plain rms_norm deviates from the fused-add residual idiom, misses the fused kernel, and leaves the last MoE all-reduce unmatched by the sequence-parallelism patterns: with enable_sp the sharded residual meets a full-shape tensor and compilation fails with a size mismatch (s72 vs s72//4). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
SM120 is PCIe-only, where allreduce is far more expensive relative to compute than on NVLink platforms. Measured on 4x RTX PRO 6000 at hidden 4096: fused matmul+RS / AG+matmul crosses over plain mm+allreduce between 512 and 1024 tokens (bf16) and reaches 1.35-1.66x at 1024-8192 tokens. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
The SP pass only rewrites compile ranges at or above sp_min_token_num, but the tp-divisibility shape constraints applied unconditionally: cudagraph capture sizes were filtered/rounded to tp multiples, the runner padded every step's token count, and spec-decode + SP refused to start whenever max(num_speculative_tokens + 1, tp) was not divisible by both, even when no captured size can reach the threshold. Sub-threshold decode batches paid for the padding - on a 192-expert MoE at bs1 the padded expert gather cost +46% ITL. Scope all six sites (capture filter, spec-decode rounding and its startup guard, runner padding, dispatch assert, residual-scattered check) to sizes at or above the threshold, and reword the two misleading disable warnings. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
…lashInfer native path FlashInfer's tensor-core decode wrapper can now plan uniform multi-token queries cudagraph-safely (flashinfer-ai/flashinfer specdec-uniform-plan). Detect that capability from the fast_decode_plan signature and, when the generic (non-trtllm) decode path is active: - treat spec-decode verify batches as decode (reorder_batch_threshold 1 + num_speculative_tokens), matching the trtllm-gen behavior - declare AttentionCGSupport.UNIFORM_BATCH so uniform verify batches capture FULL decode cudagraphs instead of forcing piecewise - key the decode wrapper and its kv metadata by request count and pass q_len_per_req through fast_plan_decode (both the wrapper plan and the flashinfer fast_decode_plan paths) Single-token decode behavior is unchanged: q_len_per_req == 1 plans are bit-identical and request count equals token count. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
Add capability-12.0 entries to the symm-mem all-reduce tables (64 MiB cap, always two-shot: no multicast hardware), plus a 128 KiB floor below which the input is handed back to the next backend — measured on 4x RTX PRO 6000, two-shot beats the NCCL ring 1.2-2x from its ~160 KB crossover through 64 MB. Scope the multicast_ptr check to the multimem kernels; the two-shot all-reduce runs on plain P2P mappings. Only takes effect on platforms that pass the native P2P atomics gate added in this PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
The one/two-shot all-reduce kernels synchronize the group through per-slot signal-pad exchanges with no notion of op instances, so their device execution order must match the group issue order. vLLM issues all-reduces from different streams across startup phases (profile, compile warmup, cudagraph capture), which CUDA does not order across streams; a second-scale rank skew then interleaves two barrier instances and the group wedges permanently (pytorch/pytorch#189228). Serialize every op of the communicator on a dedicated internal stream, fenced with events against the caller's stream. Caller-transparent and graph-capturable; validated on the previously-deadlocking multi-stream repro (300 rounds clean, bitwise-correct). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
torch's symmetric-memory signal-pad protocol (put_signal/wait_signal in the one/two-shot all-reduce kernels) issues system-scope CAS directly on peer-mapped memory. P2P read/write access does not imply atomic support: on platforms without native P2P atomics (cudaDevP2PAttrNativeAtomicSupported=0, e.g. PCIe-only multi-GPU boxes) the CAS is not atomic, barrier tokens are lost or duplicated under PCIe load, and the whole group wedges permanently. torch's rendezvous performs no such capability check (pytorch/pytorch#189228). Add CudaPlatform.has_native_p2p_atomics() (pairwise NVML_P2P_CAPS_INDEX_ATOMICS, mirroring is_fully_connected) and refuse to enable SymmMemCommunicator when any device pair in the group lacks native atomics, falling back to NCCL. Forensics on 4xRTX PRO 6000 (PCIe, no NVLink): completed two_shot counts stay exactly aligned across ranks while a single orphaned signal-pad token (one block, one rank pair, location wandering across runs) wedges every subsequent barrier; per-call ring buffers show byte-identical issue sequences on all ranks, ruling out ordering and grid mismatches; nvidia-smi topo -p2p n reports NS for every pair. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
The custom allreduce sync protocol is atomics-free (peer plain writes
plus local polling with per-block monotonic counters), so it is sound
on platforms without native P2P atomics — exactly where the torch
symm-mem CAS protocol is not. The >2-GPU full-NVLink requirement is a
performance heuristic, not a correctness gate.
Add VLLM_ALLOW_CUSTOM_ALLREDUCE_PCIE to opt in on such topologies:
relax the init and dispatch gates, default VLLM_CUSTOM_ALLREDUCE_ALGO
to 2stage (the C++ dispatch otherwise launches no kernel for
non-fully-connected world sizes > 2 — a proper default policy there is
left as a follow-up), and raise the size ceiling to 256 MiB so
chunked-prefill all-reduces are covered (16k tokens x 4k hidden x bf16
= 128 MiB). The env is read once at init; should_custom_ar is on the
per-all-reduce dispatch path.
Measured on 4x RTX PRO 6000 (SM120, PCIe-only, two switches,
NativeAtomicSupported=0 on every pair), tp4 bf16 vs NCCL:
2-stage: 1.8-2.4x from 8 KB to 1 MB, 1.2-1.6x from 2 MB to 64 MB,
1.15x at 128-256 MB — no upper crossover
1-stage: 1.6-2.1x below the 1 MB crossover, loses above
Correctness: 1200 mixed-size rounds bitwise-matching NCCL goldens
under concurrent H2D/D2H + P2P copy storms with rank-skew stalls (the
traffic profile that wedges the symm-mem CAS protocol within ~25
rounds on this platform); dummy-weight engine runs over TP-only,
SP-without-fusion, and MTP k=1 spec-decode configs all capture FULL
cudagraphs and produce token sequences identical to the NCCL path.
End-to-end qualify (Hy3-FP8 tp4, 8k1k serving bench): +1-8% output
throughput and -4..-8% mean ITL at every concurrency vs NCCL, with
gsm8k parity.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
The pipelined fused GEMM+comm ops (fused_matmul_reduce_scatter, fused_all_gather_matmul and scaled variants) synchronize exclusively through _SymmetricMemory.barrier(channel), whose CAS exchanges on peer-mapped signal pads are not atomic on platforms without native P2P atomics — the group wedges permanently under PCIe load, which is what gated sequence parallelism off on SM120 PCIe boxes. Add an opt-in (VLLM_SYMM_MEM_PCIE_SAFE_BARRIER) that swaps the barrier for a stream-memops protocol: cuStreamWriteValue32 of a monotonic sequence number into the receiver's pad (plain posted P2P write with release semantics) plus cuStreamWaitValue32(GEQ) on the sender's own pad (local polling). No remote read-modify-write; self-pairing sequence numbers also remove the multi-stream instance-interleaving hazard of the CAS design (pytorch/pytorch#189228). The patched barrier refuses CUDA graph capture: its baked sequence number would satisfy its own wait on replay without synchronizing (the stock CAS barrier replays correctly). The fused ops only run at sequence-parallel sizes above the cudagraph capture ceiling, so the path is structurally unreachable today; the guard keeps a future change from silently desynchronizing. Sequence state is keyed by the handle's signal-pad address; reset_pcie_barrier_state() covers the free-and-remap corner (see module docstring). Validated on 4x RTX PRO 6000 (NativeAtomicSupported=0 everywhere): both fused ops match unfused goldens (rel_err ~6e-3, bf16 reduction-order noise); 300 mixed rounds clean under H2D/D2H storms with rank-skew stalls (the stock barrier wedges within ~25 rounds on this profile); fused speedups intact at 8192x4096x4096 (mm+RS 1.55x, AG+mm 1.80x vs unfused NCCL). End-to-end qualify with SP+fused on the same box: first run hang-free, mean TTFT 0.94 s at concurrency 1 (restoring the async-TP level), gsm8k parity. CPU-only unit tests cover the atomics platform query, slot addressing, sequence monotonicity, write-before-wait ordering, and the capture guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
…-allocation torch's get_symm_mem_workspace() re-allocates and re-rendezvous's the fused-op workspace whenever a caller requests more than the current size, dropping the old workspace tensor with no synchronization of any kind. Device-side work still targeting the old workspace — parked barrier waits on its signal pads, in-flight P2P chunk copies from peers — then polls freed/recyclable memory and can park its stream forever. The growth decision is also per-rank local, so one rank can free and re-map while a peer still has device work against the old remote mapping. Wrap the workspace getter (only when the PCIe-safe barrier is installed) with two defenses: * floor the first allocation (VLLM_SYMM_MEM_WORKSPACE_FLOOR_MB, default 256) so steady-state growth never happens; * if growth does happen, log it, drain the device, and retire the old tensor to a keep-alive list (bounded leak, rare) so parked device work keeps polling stable memory. On a 4x SM120 PCIe box this converted a 100%-reproducible sustained- load wedge (8/8 runs) into 8/8 clean for this mechanism. An upstream torch report is being filed separately; this guard is correct (and inert at steady state) regardless of when that lands. Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
…ring torch's pipelined fused ops (_pipelined_produce_and_all2all, _pipelined_multi_all_gather_and_consume) enqueue peer-memory operations — signal-pad memops and P2P copies — concurrently from two streams per process, and secure the kernel scheduling order their cross-rank obligations depend on with a sleep-kernel nudge the source itself only calls 'almost guarantee[d]'. When the PCIe-safe barrier is installed, replace both pipelines with comm-stream variants: every peer-memory operation is issued on ONE stream per process, while producers/consumers (pure local kernels) ride a second stream ordered by explicit CUDA events in both directions (producer done -> comm may start; peers done reading -> buffer may be overwritten). Compute/comm overlap is retained; the only overlap given up is between P2P copies on different streams, which torch's own comments note cannot overlap anyway. Measured on 4x RTX PRO 6000 Blackwell (SM120, PCIe P2P), 8192x4096 @ 4096x4096 bf16 vs unfused NCCL: matmul-reduce-scatter 1.78x (stock dual-stream: 1.56x — explicit ordering also removes the stock path's scheduling-miss degradation), all-gather-matmul 1.69x (stock: 1.78x); end-to-end serving within noise of the stock pipelines at c1 and c32. Motivation beyond hygiene: on this platform family we observe a host-side wedge (all ranks parked inside libcuda enqueue calls) under sustained load with the stock dual-stream shape; an A/B with only the pipeline shape changed passes the same trigger with comm-stream (details in the driver escalation; under investigation with the NVIDIA driver team). Confining peer enqueues to one stream both sidesteps that exposure and removes the undocumented scheduling-order dependency. Signed-off-by: Zihua Wu <13583761+lucifer1004@users.noreply.github.com>
# Conflicts: # tests/compile/test_config.py # vllm/distributed/device_communicators/custom_all_reduce.py
…ptance/recovery noise With draft_sample_method="probabilistic", the draft token for position P was sampled with Gumbel noise keyed by Philox offset P -- the same offset that keys the acceptance uniform (u == float of the very Philox draw used as the draft's Gumbel key) and the recovery Gumbel noise in the rejection sampler. On rejection, the recovery draw therefore reused the exact noise vector that selected the rejected draft token, violating the independence assumption of rejection sampling and biasing the output marginal toward draft-favored tokens (measured TV distance from the target 0.0125 vs a 0.0028 noise floor; 0.0014 after the fix). Acceptance rate is unchanged. Salt the draft-side Philox offsets into a range disjoint from the target-side streams (positions are bounded by max_model_len << 2**30). The default greedy draft mode consumes no Gumbel noise and is unaffected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
DSparkSpeculator's sequential Markov sampling calls gumbel_sample directly with key Q-1 (the verification key), bypassing sample_draft. Route it through draft_gumbel_pos so its probabilistic drafts get the same disjoint stream. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Signed-off-by: EanWang211123 <wangyiheng@sangfor.com.cn>
DSpark shares the target model's lm_head (has_own_lm_head=False) and pays a full [hidden, vocab] bf16 GEMM per draft step just to propose tokens. On bandwidth-bound GPUs that weight read dominates the draft loop. When VLLM_DSPARK_FP8_DRAFT_HEAD=1, quantize the (vocab-sharded) local lm_head shard once to rowwise fp8-e4m3 at load time and compute draft-proposal base logits via dynamic per-token activation quant + torch._scaled_mm, halving lm_head weight traffic. Draft-time only: the verify pass never sees the fp8 weights, so accepted outputs are unchanged; a rare draft argmax flip only costs a rejected draft token. The fp8 copy is materialized eagerly in load_dspark_model (after lm_head aliasing, before CUDA graph capture) because the whole DSpark draft step, including compute_draft_logits, runs inside a FULL captured graph. Measured on DGX Spark (GB10, ~235 GB/s), DeepSeek-V4-Flash-DSpark: draft head GEMM 2.73ms -> 1.45ms, +3-5% end-to-end single-stream decode, argmax-identical draft proposals on our eval set, per-position acceptance unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: bird <6666242+bird@users.noreply.github.com>
…context DFlash/DSpark build the draft's context KV from target aux hidden states, which only exist for tokens that flow through a target forward pass. Tokens restored from the prefix cache (or a KV connector) at request (re)admission never do, so their draft KV slots are never written — yet the draft attends over the full sequence. With automatic prefix caching and a long shared prefix, the draft reads thousands of uninitialized slots and acceptance collapses to ~0.3% (position-0 only); the same workload with unique prompts reaches ~20%. MTP is unaffected (no context KV), which hid the interaction. Fix: track per request-slot how many tokens were restored at the last (re)admission (RequestState.num_cached_tokens) and hide the restored whole blocks from the draft's attention — the prep kernel shortens the draft seq_lens and a new kernel left-shifts the draft block-table rows in place (safe: input_block_tables are regathered every step, and the shift runs after slot mappings are computed from the unshifted table). Draft KV stores post-RoPE keys at absolute positions, so no position rewriting is needed. Requests without cache hits and dense DFlash/DSpark setups are unaffected (shift 0). Up to block_size - 1 restored slots stay visible when the restored count is not block-aligned (e.g. full-prompt hits). The draft loses the cached prefix from its context (bounded by its training window anyway) in exchange for prefix caching and speculative decoding composing at all. A durable alternative — letting the draft KV cache group participate in prefix-cache block reuse — is left for a follow-up RFC. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: giorgiopiatti-dfinity <giorgio.piatti@dfinity.org>
Signed-off-by: mgoin <mgoin64@gmail.com>
…tention Non-causal draft attention (DFlash/DSpark) skips trtllm-gen and runs the FlashInfer prefill wrapper, whose run() is not replay-safe once plan() changes; replaying a full CUDA graph then returns wrong output or an illegal memory access. Only claim UNIFORM_BATCH cudagraph support for causal attention, build draft attention metadata under the draft's attention config, and fall back to eager draft attention when full graphs are unsupported. Signed-off-by: mgoin <mgoin64@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The scheduler already suppresses speculative lookahead allocation for WAITING-path edge cases, but the RUNNING path still always passed `self.num_lookahead_tokens` into `allocate_slots`. For in-flight chunked prefill requests this over-reserves KV cache blocks, even though prefill chunks do not schedule speculative tokens. This patch uses zero lookahead tokens for `request.is_prefill_chunk` in the RUNNING path, matching the existing prefill-chunk scheduling semantics. Also add a regression test that asserts running chunked prefill requests call `allocate_slots(..., num_lookahead_tokens=0)`. Signed-off-by: tianyu.jiang <tianyu.jiang@enflame-tech.com>
Follow-up to vllm-project#47716. The fp8_ds_mla reshape fix relies on quantized KV cache specs carrying kv_quant_mode so the reshape path selects the 584-byte-per-token layout instead of the semantic head-size shape. Two spec-conversion paths dropped kv_quant_mode: - SlidingWindowMLASpec.merge() did not copy kv_quant_mode into the merged spec (SinkFullAttentionSpec.merge() already does). - unify_hybrid_kv_cache_specs() lost kv_quant_mode when converting SlidingWindowMLASpec -> MLAAttentionSpec (the sibling SlidingWindowSpec -> FullAttentionSpec branch already passes it). These don't break the default DeepSeek-V4 path today (it groups via UniformTypeKVCacheSpecs and preserves per-layer specs), but they are latent correctness gaps when specs are merged or the hybrid manager is disabled. Add regression tests that fail without the fix. Co-authored-by: Claude Signed-off-by: mgoin <mike.goin12@gmail.com> Signed-off-by: mgoin <mgoin64@gmail.com>
…cation
Implements DSpark (arXiv 2607.05147) confidence-scheduled verification with
two capacity enforcement modes and full CUDA graph support:
- Capacity manager with `mask` (pad pruned verify rows; defaults
VLLM_MOE_SKIP_PADDING=1 so MoE kernels skip the pruned rows) and
`varlen` (compact the verifier batch) modes; varlen replays FULL CUDA graphs for
both the target verify step and the DSpark draft query step.
- Paper-faithful Algorithm 1 allocator: capacities are the greedy admission
counts (sum(capacities) == spent budget, hard cap; zero-survival tokens
are never candidates), fixing a threshold-recount tie escape that
disabled the budget under saturated confidence logits.
- Hardware-aware prefix scheduler: `dspark_sps_curve` (profiled
steps-per-second vs verification batch tokens) drives the
theta = tau * SPS(B) argmax stopping rule; `dspark_budget_frac` remains
as an admission upper bound. `dspark_sps_curve="auto"` profiles the
curve at engine init: uniform-decode dummy runs (the DP idle-step path,
which replays the captured verify graph AND the full draft step) are
timed per power-of-two request count up to max_num_seqs after graph
capture, and rank 0's measurements are broadcast so every TP rank builds
the identical table; the allocator captures a flat placeholder table
whose contents are refreshed in place. `dspark_sps_overhead_ms` adds
host/scheduler time the dummy path cannot see.
benchmarks/profile_dspark_sps_curve.py remains for offline measurement.
- Online Sequential Temperature Scaling (`dspark_online_sts`, on by
default with capacity modes): per-position temperatures fitted online by
an ECE grid search over binned rejection-sampler outcomes
(order-preserving, per the paper Sec 3.2.1; identity until observations
accumulate; deterministic one-hot reductions so TP ranks stay bitwise
identical).
- Varlen full-CG correctness fixes: per-request token bound in cudagraph
dispatch, capture/replay buffer-address consistency in the DSA indexer
varlen decode path (forced flatten + persistent indices buffer),
padded-row sizing in the indexer build, TP-deterministic capacity
flushes, and correct handling of the scheduler's -1 draft placeholder
ids in capacity accounting.
- TP-rank determinism fixes for padded draft FULL-graph replays: padding
rows of sample_idx_mapping now carry a -1 inert-row sentinel so replays
never scatter draft logits through stale req-state slot ids
(duplicate-index scatters have undefined write order and silently
diverge per-rank state; with varlen capacity this became a
collective-size-mismatch deadlock), and the online-STS proposal staging
moved out of the captured graph. Worker slot recycling now iterates
finished_req_ids in sorted order and fallback per-request sampling
seeds come from a dedicated RNG stream, removing two more per-rank
divergence hazards. Debug guard: VLLM_DSPARK_TP_CHECK={1,2}
(capacity.py::check_dspark_tp_consistency) cross-checks request-keyed
capacity/STS state across TP ranks each step and fails fast with
per-rank state dumps.
Co-authored-by: OpenAI Codex <codex@openai.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Lucas Wilkinson <lwilkins@redhat.com>
|
Same-hardware comparison against current upstream main, using independently tuned profiles for the same C1-C8 / near-1M workload constraint. Hardware: 2x RTX PRO 6000 Max-Q, 300 W/card, PCIe 4.0 x16, TP2, DeepSeek-V4-Flash-DSpark, fixed probabilistic K=5. Benchmark: temperature 0, context 0, 30 s/cell, 1,024 output tokens.
C1 triplicate medians: current main PyNCCL 184.84 tok/s; this PR PyNCCL 188.62; this PR B12X 193.96. B12X adds 2.83% within this PR and accounts for approximately 60% of its total measured C1 advantage over current main. Current main is 2.00% behind when both profiles use PyNCCL. Exact cold prefill, current / this-PR PyNCCL / this-PR B12X: 8k 8,274 / 8,437 / 8,635; 64k 8,238 / 8,587 / 8,673; 128k 7,507 / 7,834 / 7,880 tok/s. Cold 960,164-token prompt, zero cached tokens: current main 284.221 s TTFT (3,378 prompt tok/s); this PR 273.377 s (3,512 prompt tok/s). Current main was 3.82% lower in prompt throughput. Reported KV pools: current main 1,399,974 tokens at max context 1,048,576; this PR 1,052,723 at max context 1,032,192. No measured cell had a request error, capacity timeout, or inference-time JIT warning. Benchmark SHA256: |
|
Follow-up Dry-run checks produce TP2 Published image: Manifest: |
|
#88 is now fully superseded by reviewable PRs against the current Canonical replacements:
Related canonical quantization work is #104, replacing closed #92. The following #88 content is intentionally not carried into core FF:
The combined #97-#107 tree has been merged locally in dependency order without conflicts. Closing #88 removes the conflicting mixed branch as a possible merge source while retaining its investigation history here. |
Final release follow-up (2026-07-12,
adf15cadb)The synchronized release sweep completed all 40 TP2/TP4 x backend x MTP/DSpark cases with valid decode, coding, and 8k/64k/128k prefill artifacts. Standard MTP0 remained at or above the v9 baseline across the latency-sensitive C1 cells.
The final follow-up changes only the environment launcher: TP4
lucifer-cutlass + dsparkcould leave 777 MiB free while the first real FlashInfer MoE prefill requested a 764 MiB transient workspace. Its defaultGPU_MEMORY_UTILIZATIONis now 0.94 instead of 0.9465. The exact final image profiled 1,523,880 KV tokens and completed C1/C64 plus 128k prefill without OOM.Final image:
voipmonitor/vllm:fathomless-firmament-ds4-v10-vllmadf15ca-b12x90172a5-fi2cba2f7-cu132-20260712Digest:
sha256:4f07aefe2aa15f66d5fd90580eaa2553926aea0c3fa46bc227610e90c668c9f0Final clean-cache correction (2026-07-12,
5dd080a70)This section supersedes the earlier event-lifetime conclusion below while retaining the exploration history.
Fresh, isolated compilation caches showed that the apparent event/stream fixes were warm-cache false positives:
07f877827image completed C1 and then hung at C64;CapacityBasedVerificationManager._flush_draft_token_capacity_copy()while their GPUs remained at 100% utilization;The failure was the non-canonical TP control input for compact verification. Each TP rank independently read back its draft-capacity tensor and selected a physical verifier graph. Any rank-local difference could therefore select different token shapes and eventually deadlock a shape-sensitive TP collective.
The production fix broadcasts the small active capacity vector from TP rank 0 before the existing asynchronous D2H copy. Every rank now trims the same requests and selects the same physical graph. This keeps all current pre/post-GEMM auxiliary streams, FULL CUDA graphs, Lucifer/B12X backends, and dynamic K behavior enabled; it adds no global CUDA synchronization and does not serialize attention.
Clean-cache validation on TP=2 Lucifer CUTLASS + B12X indexer/all-reduce:
The soak ended with
running=0,waiting=0, anderror=0; capacity telemetry showed compact batches continuing oncg=FULL. The complete capacity test module passed in the pinned runtime image (22 passed), and Ruff format/check plusgit diff --checkpassed.The communication investigation remains unchanged: NCCL kernels account for roughly 16% of the observed C64 GPU interval before overlap. Pair-local P2P reached 56.4 GB/s, NCCL 39.47 GB/s, and B12X DMA 48.67 GB/s, but the end-to-end B12X-DMA gain was only about 1.4% in the three-run C128 comparison. Communication tuning is therefore secondary to the now-fixed TP graph-shape correctness issue.
Summary
This PR consolidates the DeepSeek-V4-Flash-DSpark work developed and validated on RTX 6000 Pro Blackwell (SM120), rebased onto
dev/fathomless-firmament. It combines correctness hardening, optional DSpark experiments, the SM120 PCIe serving stack from upstream vLLM PR vllm-project#47979, and an environment-driven launcher that makes the tested configurations reproducible.The production default remains conservative: DSpark uses probabilistic fixed-depth K=5 verification. Capacity-aware verification, dynamic physical draft depth, the rowwise-FP8 draft head, alternative all-reduce paths, and SP/async-TP are opt-in.
Latest validated follow-up (2026-07-12)
Commits e24d8ae through 07f8778 complete the profiler-driven DSpark performance and correctness pass. This section supersedes the earlier exploratory capacity and performance numbers below.
What changed
The TP=2 auto-profiled saturation budget was 160 draft tokens, corresponding to an activation knee of 32 requests at K=5. At C64 the controller selects physical K=3 and typically reduces 320 possible draft rows to roughly 95-105 retained rows plus 64 bonus rows.
Final TP=2 ctx0 decode
Two additional 30-second C64 runs measured 2719.1 and 2738.0 tok/s. Three-run mean is 2725.1 tok/s with sample SD 11.2. Five coding runs measured median 323.0, mean 327.5, and max 359.2 tok/s, with zero CJK runs.
PCIe/NCCL conclusion
Pair-local P2P measured 56.4 GB/s. On the exact 6 MiB serving tensor, NCCL measured 39.47 GB/s and B12X DMA 48.67 GB/s. Despite the 23.3% isolated collective gain, B12X DMA improved end-to-end C128 by only 0.72%. Forcing 16 NCCL channels improved C64 by 1.71% while costing about 2.7% KV capacity. Torch traces identify target MoE verification, not NCCL, as the dominant cost, so both remain optional diagnostics.
Additional validation
What changed
DSpark correctness and determinism
kv_quant_modethrough merge/unify.These changes address the classes of failures seen during long-context and full-KV-cache testing: stale draft slots, nondeterministic padded-capacity state, races between the B12X auxiliary stream and the verifier, and graph outputs whose lifetime ended before downstream consumers completed. Review follow-up also makes capacity compaction out-of-place, fails closed for block-unaligned restored prefixes, validates that confidence-head weights really loaded, and hardens finite-value/configuration checks.
Capacity-aware and variable-length verification
Masked capacity verification remains non-default because CUTLASS still executes the padded target computation. Compact varlen verification now has a validated load-aware mode: exact fixed-width K5 graphs are used below the activation knee, while C32+ uses physically compact target batches.
SM120 post-GEMM stream safety
The first correctness pass serialized FlashInfer SM120 post-GEMM indexer and
compressor work. That diagnostic removed long-prefill illegal accesses and
proved that the original implementation had a missing producer/consumer edge,
but it also cost roughly 13% at C1 and several percent at C64.
The final implementation keeps the overlap. GEMM fan-out and post-GEMM work use
separate event sets. Three progressively stricter A/B tests were required:
eagerly between CUDA graph segments.
one warm AOT artifact, but a fresh-cache C1 -> C64 run reproduced the hang
(253.9 tok/s followed by 0.0).
failed (251.4 followed by 0.0). Completion therefore does not imply that an
event handle is no longer owned by a captured graph artifact.
A controlled same-cache run with non-recycled event handles completed at
239.2/2745.2 tok/s, proving that permanent event-handle reuse was the trigger.
The first production attempt retained events only when CUDA reported the current
stream as capturing and released other eager wrappers after enqueue. That passed
as an overlay but a clean immutable image changed the AOT schedule and reproduced
the C64 hang at 254.9/0.0 tok/s. Adding scopes only around the explicit
torch.cuda.graphblocks also failed at 250.9/0.0.The missing lifetime begins earlier:
CudaGraphManager.capture()runs descriptorprewarm forwards before each capture, and multi-stream custom ops may create event
handles there that later graph artifacts retain. The production fix therefore
marks the entire manager capture phase, including prewarms, with a context-local
vLLM capture scope. Standard, breakable, and direct FULL capture blocks also carry
the scope for correctness outside the normal manager. Captured/prewarm event
wrappers are retained with their owning modules. Runtime eager wrappers remain
private to one invocation and are released after enqueue; pending
cudaEventDestroyis asynchronous, matchingtorch.cuda.Stream.wait_stream().A retain-everything diagnostic passed but grew RSS from 16.19 to 16.24 GiB during
a 120-second C64 run, so it was rejected. The bounded outer-scope implementation
completed C1 -> C64 -> C1 at 248.1/2727.5/245.3 tok/s and sustained C64 for
120 seconds at 2743.4 tok/s. The server remained healthy and its log contained no
CUDA/NCCL errors. Runtime code cannot append to the retained capture-event list
after the outer scope exits.
Global synchronization remains rejected. The fix adds no steady-state barrier
and does not disable parallel attention. B12X large-tensor DMA also remains
explicit opt-in: its standalone collective is faster, but the end-to-end gain
was below 1% and an earlier integrated long-prefill path produced an illegal
memory access.
Standard-path CUDA-graph padding and v15 parity
During final v9/v15 parity validation, standard MTP-off serving exposed a separate correctness bug in the capacity-integration plumbing. CUDA-graph capture initializes the persistent
is_paddingbuffer as padding for the full capture shape. The standard non-speculative path did not clear active rows whenVLLM_MOE_SKIP_PADDING=0, yet slot mapping always consumed the buffer. Active tokens consequently receivedPAD_SLOT_ID, KV writes were skipped, and long generation could become CJK/garbled while short-context throughput was distorted.The fix now passes an input-padding mask to slot mapping only when speculative/capacity trimming or MoE padding actually consumes it. Those paths refresh the live rows; ordinary decode ignores the stale persistent buffer and avoids an extra fill kernel. In the same final audit:
After every A/B/C server had fully loaded before any client started, three isolated TP=2 A8 C1 runs measured 141.3, 141.6, and 141.5 tok/s; the v15 control was 141.7 tok/s. The current implementation produced normal English/code output and passed six focused padding, indexer, and stream-policy regression tests inside the pinned image. Earlier 132-133 tok/s observations were invalid concurrent-load measurements, not a code regression.
Causal MTP parity follow-up
A synchronized v9 comparison found a standard-MTP regression that MTP-off canaries could not expose: TP=2 B12X A8 MTP2 measured 196.0 tok/s versus the published v9 217.4 tok/s, while acceptance was unchanged (0.6548 versus 0.6559). Re-running the new image through the exact old v9 launcher produced 198.6 tok/s, proving that the loss was in the code path rather than the helper or arguments.
Two safe optimizations were restored without weakening the DSpark guards:
b12x_mhc_postwhen the B12X mHC path is active, while retaining the newer final-state reuse that avoids duplicate reconstruction.The same TP=2 A8 canary progressed from 196.0 to 205.8 and then 208.9 tok/s; coding median reached 220.6 tok/s. Disabling the new B12X fused all-reduce + RMSNorm path reduced C1 to 201.2 tok/s, so that fusion is beneficial and remains enabled. The remaining small delta is shared with the Fathomless MTP orchestration path and is evaluated by the immutable-image TP=2/TP=4 release sweep rather than hidden in launcher defaults.
Optional rowwise-FP8 draft head
lm_headimplementation and loader integration.VLLM_DSPARK_FP8_DRAFT_HEAD=1.This was retained because it can help bandwidth-bound systems, but on RTX 6000 Pro it was approximately neutral at C1 and only about 1.2% faster at C64. It is therefore not the default.
SM120 PCIe serving stack
This branch includes the rebased changes from upstream vLLM PR vllm-project#47979:
Sequence parallelism and async TP are useful for V1 paths, but DSpark currently requires the V2 model runner and this vLLM revision explicitly rejects SP under V2. The launcher therefore refuses that unsupported combination instead of silently claiming it is active.
The validated Lucifer image also needs FlashInfer PR vllm-project#3871 plus the canonical SM120 DSV4
topk=256fixes: decode PR vllm-project#3817 and prefill PR vllm-project#3896. DSpark TP=2 exercises both shapes during warmup and serving, so the release image pins all three FlashInfer PRs outside this vLLM diff.Reproducible DS4 launcher
serve-ds4-flash.shis now an environment-driven launcher suitable for installation in an image as/usr/local/bin/serve-ds4-flash.sh.It provides:
MODE=mtp0|mtp2|mtp3|dsparkBACKEND=b12x-a16|b12x-a8|b12x-a8-dglin|lucifer-default|lucifer-cutlassALLREDUCE_MODE=b12x|vllm-custom|vllm-custom-2stage|ncclINDEXER_BACKEND=auto|b12x|nativeB12X_PCIE_DMA=0|1, default off after the integrated long-prefill failureDSPARK_DRAFT_ATTENTION_BACKEND=auto|B12X_MLA_SPARSE|FLASHINFER_MLA_SPARSE_DSV4|FLASHMLA_SPARSE_DSV4for explicit draft-only experimentsMAX_NUM_SEQS, capped so generated capture sizes never exceed the declared graph maximumDRY_RUN=1for exact command inspection without starting a serverThe launcher does not hard-code GPU IDs. Container orchestration owns GPU placement.
Development history and rejected approaches
The following branches were implemented or measured and are intentionally not production defaults:
VLLM_DSPARK_FP8_DRAFT_HEAD, opt-in) #73): retained as the opt-in implementation in this branch, but not made default because the RTX gain was negligible at low concurrency.[40, 41, 42].Performance context
On the current TP=2 Lucifer/CUTLASS fixed-K5 path, one synchronized validation sweep measured:
The final Docker/wiki sweep is intentionally kept outside this PR description so published numbers are tied to an immutable image digest and synchronized launch procedure.
Validation
git diff --check origin/dev/fathomless-firmament...HEADbash -n serve-ds4-flash.shDRY_RUN=1for all 20 combinations of four modes and five backend profilesgit diff --checktests/v1/spec_decode/test_dspark_fp8_draft_head.pytests/v1/spec_decode/test_dflash_cudagraph_lifetime.pytests/v1/attention/test_deepseek_v4_dspark_metadata.pyThe broad
test_arg_utils.pyinvocation was also attempted in a container without GPU access. Its platform-autodetection cases fail because CUDA is unavailable in that test container; this is an environment limitation rather than a branch regression.Related work
VLLM_DSPARK_FP8_DRAFT_HEAD, opt-in) #73; those historical PRs can remain closed.Summary by CodeRabbit
New Features
--dspark-capacity-verification-mode, plus SPS-curve profiling, dynamic draft depth, and online temperature calibration.max_req_tokens), plus extended PCIe-safe comm controls.Bug Fixes