Repository navigation
Conversation
…fers; DeepSeek-V4.1 frees a spilled conversation's host copy right away. DSpark's accept step returned views of fixed buffers that the target-verify CUDA graph writes and its next replay overwrites. Under the overlap scheduler the copy of those results to the host can run after that replay, so each tensor-parallel rank could read a different step's values and commit a different number of tokens for the same request. The accept step now returns copies made right after the replay (sgl-project/sglang#42812, by Divy Vasal, with its unit test). DeepSeek-V4.1's DSpark worker had copied the step lengths itself for the same reason; that copy is gone. DeepSeek-V4.1 keeps one conversation's sparse-attention (CSA2) state on the GPU and writes it to disk when a request starts a different conversation. torch.save leaves what it writes referenced from a reference cycle, so the host copy of each spilled state, about 240 MB per GPU process, stayed in memory until Python's next full garbage collection. The engine freezes its startup objects, which makes those collections rare, so a run that keeps switching conversations grew by gigabytes and pushed the host into swap. The session store now runs a full collection right after each spill; in the running engine one takes about a millisecond. A unit test checks that nothing written outlives the spill. On 58 single-turn requests, each one a conversation switch, the eight GPU processes went from 122.7 to 145.5 GiB before the fix and stayed at 121.1 to 120.9 GiB with it, with no swap growth. The DeepSeek-V4.1 model page and the README said a second conversation always prefills from zero. The serve script keeps the last four conversations on disk, and a later request that continues one of them loads it back. After switching away and back, a 300-token continuation of a 3k-token conversation reached its first token in 3.1 s, against 9.5 s to prefill the 3k tokens cold. The page now has a row for this. Two speculative-decoding unit tests follow upstream changes: test_dflash2 drops the cases for helpers that no longer exist, and the DP spec-prefill coordination test mocks the DSpark target worker. Checked on 8x V100-SXM2. GLM-5.3-Flash: gsm8k 92.8% (all 1319 questions, 5-shot, greedy), 125 output tok/s at two requests in parallel, 3.40 tokens per verify step. DeepSeek-V4.1-Flash: chat, tool calls and image prompts pass; follow-up turns of 40 / 300 / 700 new tokens reach the first token in 0.95 / 2.28 / 3.27 s; decode 11.2 tok/s greedy and 10.5 at temperature 1 on merge_sorted (5.94 / 5.61 tokens accepted per step), unchanged.
|
@hnyls2002 @ch-wan could one of you take a look and add |
…iting the static buffers 0.4.0-rc.2 crashed on the first folded DSpark verify: sgl-project/sglang#42812 makes read_accept return clones, and main's #43790 passes out=self.read_accept(bs) to the in-graph accept, so every replay wrote throwaway copies and commit_lens read back as 0 (a negative correct-draft count in the scheduler). The carry now writes through _accept_buffers and keeps the copies for the post-replay read (internal up/dspark-read-accept-no-alias 621aeac1, new regression test); main's test_dspark_accept_tail.py, which fails on rc.2's tree, joins the CPU tests.
|
Heads-up before this lands: on current main, The PR head still applies against main's layout because it predates the What worked for us: keep a views accessor as the write target and clone only for the reader. def _accept_buffers(self, bs): # views; out= target inside the graph
return AcceptOuts(correct_len=self.correct_len_buf[:bs], ...)
def read_accept(self, bs): # clones, as this PR intends
b = self._accept_buffers(bs)
return AcceptOuts(correct_len=b.correct_len.clone(), ...)and |
…xt verify replay rewrites DsparkVerifyEpilogue.read_accept returned views of fixed buffers (commit_lens, out_tokens, ...) that the folded accept kernels write inside the target-verify CUDA graph and that the next replay overwrites. Under the overlap scheduler the result D2H runs on copy_stream, which forward_stream never waits on, so a copy that runs late reads step N+1's values. This happens per rank, so TP ranks can commit different token counts for the same request (the rank-consensus checker reports cache_finished_req owned_kv_len differing by 1-2 tokens) and HiCache then hangs on the divergent inserts (sgl-project#42465). Return clones made right after the replay; async_d2h's record_stream keeps them alive until the copy has run. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ead_accept's copies Since the build_accept_outs(out=...) refactor, both in-graph accept writers (DsparkVerifyEpilogue._accept and the sampling accept region, sampling_accept_commit_lens) passed out=read_accept(bs). With read_accept returning copies, those writes landed in throwaway tensors: the static accept buffers stayed at their old values, so the folded path read accept_len 0 (num_correct_drafts -1, IndexError in update_spec_correct_drafts_histogram), and the engram history commit and future relay, which read the buffers directly, saw stale lengths. Add accept_buffers(bs), the views, as the out= target of both writers; read_accept clones from it for the readers, keeping the sgl-project#42465 fix. The new unit test writes through accept_buffers and checks read_accept returns those values and keeps them after the buffers are overwritten. It fails when the write target is a copy; the alias-only tests do not. Reported by shanemort1982 in the PR review. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
7a8f749 to
155fa07
Compare
|
@shanemort1982 thanks for catching this, and for the clear writeup. You're right: since the I rebased onto main and added an |
|
Required checks failed on 155fa07; this PR cannot be merged until they pass. PR title (run)
|
|
Hi, we hit the same aliasing from a different direction, in case it helps the review. On 0.5.22 with --speculative-algorithm DSPARK --speculative-dspark-block-size 7 --speculative-dspark-sps-table-path and SGLANG_RAGGED_VERIFY_MODE=compact, the synchronous loop (--disable-overlap-schedule) loses acceptance: 2.42 vs 3.09 under the overlap scheduler and 3.12 eager, on the same 80 MT-Bench prompts (Qwen3-8B-based target, greedy), while the outputs stay byte-identical.Mechanism: the sync scheduler adopts GenerationBatchResult.new_seq_lens (a view of new_seq_lens_buf) as the next batch’s seq_lens; the next replay’s in-graph accept rewrites it right after the verify, so inject_ragged computes the draft-KV injection positions from post-commit lengths (positions [13..20] where [10..17] were correct, slots right, RoPE phase wrong). Static verify isn’t affected because its injection uses the window’s precomputed positions; eager returns fresh tensors; overlap keeps its own FutureMap buffer. Returning copies from the folded accept, as this PR does, brings the sync loop back to 3.09. |
|
Thanks @0xtoward, this is really helpful! It's the same aliasing showing up in a sneakier way: the outputs stay identical, so you'd only notice the drop in acceptance. That lines up with the mechanism too. Anything that keeps a Heads up that I rebased yesterday (now at @hnyls2002 @ch-wan this now covers two issues: the TP-rank deadlock in #42465 and this acceptance loss in the sync loop. Would one of you mind taking a look and adding |
Motivation
Fixes #42465. With DSPARK in greedy mode (folded accept) and the overlap scheduler on, TP ranks can commit different token counts for the same request.
DsparkVerifyEpilogue.read_accept()returns views of fixed buffers:commit_lens_buf,out_tokens_buf,correct_len_buf,bonus_buf,cap_trim_lens_buf,new_seq_lens_buf;Under
event_loop_overlap:accept_lens/next_token_ids) runs oncopy_stream;forward_streamnever waits on that copy;A copy that runs late therefore reads step N+1's values. The race resolves independently on each rank, so ranks disagree on
kv_committed_len.With
SGLANG_ENABLE_RANK_CONSENSUS_CHECKER=1:With HiCache (
write_through), the divergent inserts become divergent backups. The ranks end up one collective apart and the replica hangs, as described in #42465.Why HiCache triggers it: HiCache's D2H write-through traffic delays the small result copy by a different amount on each rank. Under that traffic the race fires within minutes.
Without HiCache: the race is still present, but we did not observe it in ~21M tokens. If it does fire, it can also return the next step's
next_token_idson rank 0.Modifications
read_accept()returns.clone()of each buffer.async_d2h'srecord_streamkeeps them alive until the copy has run.--disable-overlap-schedulealso avoids the bug, but cost us 14-20% per-request decode throughput.Accuracy / validation
Setup: DeepSeek-V4.1-Flash (MXFP4), TP4/EP4 on 4x B300, two replicas per node, nightly
4cbf290fplus this change.Flags:
The checker was on. Workload: greedy (temperature 0) unique ~540K-token prompts,
max_tokens 4; 1 in flight on one replica, 8 on the other.owned_kv_lendivergence above within 8-12 minControls on the same hardware and workload, ~21M tokens each, checker on:
--disable-overlap-scheduleSo the bug needs DSPARK, HiCache and the overlap scheduler together.
New CPU unit test:
test/registered/unit/spec/test_dspark_read_accept_no_alias.py.Lint:
isort/ruff format/ruff check --select=F401,F821,UP037/codespellall clean.Checklist
CI States
Latest PR Test (Base): ❌ Run #40133351715
Latest PR Test (Extra): ❌ Run #40133351725
Latest PR Test (AMD ROCm 10): ❌ Run #40133351776