Conversation
|
Per MAINTAINER.md, CODEOWNERS has requested the Speculative decoding owners. Flagging the KV Cache oncalls as well — @ispobock @xiezhq-hermann — since the change is in Two points that may help review:
Non-hybrid-SWA models are unaffected: the whole body is behind |
…SH decode path `spec_prepare_for_decode` routes the dflash family (which includes DSPARK) to `batch.spec_info.prepare_for_decode`, and that handler omits two things the EAGLE path does: `maybe_evict_swa()` and advancing `decode_batch_idx`. Both are needed -- the eviction gate inside `maybe_evict_swa` is `swa_maintenance_step and req.decode_batch_idx >= 1`, so adding the call alone would still never fire. The effect on a hybrid-SWA model is that SWA KV is retained for every generated token and never reclaimed: measured on DeepSeek-V4-Flash-0731 (tp8 + CP8 + DSPARK, A100), a single request at 33,792 tokens of context held 32,300 SWA tokens instead of a bounded window. The pool then fills at `swa_full_tokens_ratio * max_total_num_tokens` and the scheduler starts retracting, which re-prefills `prompt + everything generated so far`. Pool sizing already assumes this eviction runs (pool_configurator computes `trailing_tokens = window + eviction_interval * draft_tokens + page_size`). Measured at in=1024 / out=32768, ratio 0.1 unless noted, no retraction in any row: before conc=11 1113.6 tok/s before conc=14, ratio 0.3 1331.9 tok/s swa peak 0.91 <- best reachable after conc=32 2220.1 tok/s swa peak 0.14 after conc=48 2292.8 tok/s swa peak 0.20 1.72x, and per-request SWA at full context drops 32,300 -> 1,024. Because SWA stops being the scarce pool, `--swa-full-tokens-ratio` no longer needs raising: 0.1 measures the same as 0.3 (2220 vs 2186 tok/s, inside run-to-run noise), so the default is right again and the memory goes back to the full pool. Upstream PR is still open; applies clean on this branch.
…EE_OUT_OF_WINDOW_SLOTS on Frees out-of-window SWA slots when a chunked-prefill request is stashed into the unified radix tree (`UnifiedRadixCache.cache_unfinished_req` -> `SWAComponent.free_out_of_window_slots`). We do run UnifiedRadixCache (`impl=UnifiedRadixCache`, components FULL+SWA), so the flag is live for us. Measured no effect on our workload, in either direction. A/B on DeepSeek-V4-Flash-0731 with in=13,513 (2 chunks at chunked_prefill_size=8192) / out=2048 / conc=32, with sgl-project#33805 also applied: on 1138.3 tok/s prefill-phase swa peak 0.07 off 1147.1 tok/s prefill-phase swa peak 0.07 The chunking did happen (`#new-token: 8192` then `1792` per sequence), so this is a real null result, not a missed code path. The reason is that `max_prefill_tokens=16384` admits only one or two sequences per prefill batch, so the out-of-window KV this frees is a per-request transient that never accumulates across the batch -- and with sgl-project#33805 in place the decode side is already reclaiming for every request that has moved on. It should matter with a much wider prefill batch or much longer inputs. Carried anyway to stay close to upstream default. Note the known issue behind upstream sgl-project#34823: under EAGLE the new-leaf path computes `split_pos = swa_evicted_seqlen - result.prefix_len`, mixing raw-token lengths with bigram key units, and split_pos goes negative. That PR gates the option off for EAGLE only and explicitly keeps it for DSpark/DFlash, which is what we run -- but anyone switching this branch to EAGLE should set SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS=0 until sgl-project#34823 lands.
spec_prepare_for_decode routes dflash-family algorithms (DFLASH, DSPARK) to their own prepare_for_decode, which never calls maybe_evict_swa and never advances decode_batch_idx; eagle_prepare_for_decode does both. On hybrid-SWA models this retains sliding-window KV for every generated token, so a single request exhausts the SWA pool after swa_full_tokens_ratio * max_total_num_tokens generated tokens and is retracted by the scheduler. Pool sizing already assumes the eviction runs (pool_configurator: trailing_tokens = sliding_window + eviction_interval * draft_tokens + page_size). Mirror eagle_prepare_for_decode: evict before advancing decode_batch_idx so the overlap-scheduler gate (decode_batch_idx >= 1) keeps protecting the first decode iteration. Reproduced on DeepSeek-V4-Flash-0731 (SM120, TP=2, DSPARK block 5, ratio 0.1, pool 778,496): SWA occupancy tracked generated tokens 1:1 and the request was retracted at exactly 77,824 tokens. With the fix, the same request generated 100,000 tokens with SWA occupancy holding at ~512 tokens.
26a2a39 to
40034c4
Compare
…SH decode path `spec_prepare_for_decode` routes the dflash family (which includes DSPARK) to `batch.spec_info.prepare_for_decode`, and that handler omits two things the EAGLE path does: `maybe_evict_swa()` and advancing `decode_batch_idx`. Both are needed -- the eviction gate inside `maybe_evict_swa` is `swa_maintenance_step and req.decode_batch_idx >= 1`, so adding the call alone would still never fire. The effect on a hybrid-SWA model is that SWA KV is retained for every generated token and never reclaimed: measured on DeepSeek-V4-Flash-0731 (tp8 + CP8 + DSPARK, A100), a single request at 33,792 tokens of context held 32,300 SWA tokens instead of a bounded window. The pool then fills at `swa_full_tokens_ratio * max_total_num_tokens` and the scheduler starts retracting, which re-prefills `prompt + everything generated so far`. Pool sizing already assumes this eviction runs (pool_configurator computes `trailing_tokens = window + eviction_interval * draft_tokens + page_size`). Measured at in=1024 / out=32768, ratio 0.1 unless noted, no retraction in any row: before conc=11 1113.6 tok/s before conc=14, ratio 0.3 1331.9 tok/s swa peak 0.91 <- best reachable after conc=32 2220.1 tok/s swa peak 0.14 after conc=48 2292.8 tok/s swa peak 0.20 1.72x, and per-request SWA at full context drops 32,300 -> 1,024. Because SWA stops being the scarce pool, `--swa-full-tokens-ratio` no longer needs raising: 0.1 measures the same as 0.3 (2220 vs 2186 tok/s, inside run-to-run noise), so the default is right again and the memory goes back to the full pool. Upstream PR is still open; applies clean on this branch. (cherry picked from commit 3226bc5)
Motivation
spec_prepare_for_decoderoutes dflash-family algorithms (DFLASH, DSPARK) toDFlashDraftInputV2.prepare_for_decode, which never callsmaybe_evict_swaand never advancesdecode_batch_idx— which the eviction gate requires to be>= 1.eagle_prepare_for_decodedoes both. On hybrid-SWA models the sliding-window eviction is therefore unreachable under DFLASH/DSPARK: SWA KV is retained for every generated token, so a single request exhausts the SWA pool after roughlyswa_full_tokens_ratio * max_total_num_tokensgenerated tokens and is retracted by the scheduler mid-generation.Reproduced on DeepSeek-V4-Flash-0731 (2x RTX PRO 6000, SM120, TP=2, DSPARK block size 5,
swa_full_tokens_ratio=0.1,max_total_num_tokens=778496).#swa tokentracks generated tokens 1:1, and the request is retracted at exactly 77,824 tokens —align_page_size(int(778496 * 0.1)), the full SWA pool — while the full pool is only 10% used:Long prompts are unaffected — a 131k-token prefill holds ~8.5K SWA tokens. Only generated tokens accumulate.
The same missing call also skips the DSV4 compress-state drain (
maybe_evict_dsv4_state, documented "called every decode step from ScheduleBatch"), so the NPU + DSV4 + DFLASH combination leaks state-pool slots for the same reason.Modifications
speculative/spec_utils.py: in the dflash-family branch, mirroreagle_prepare_for_decode— callbatch.maybe_evict_swa()and advancedecode_batch_idx. Eviction runs before the tick so the overlap-scheduler gate (decode_batch_idx >= 1) still skips the first decode iteration, exactly as on the eagle path.test/registered/unit/spec/test_decode_bookkeeping_ownership.py: record the new bookkeeping owner in_OWNER_SITES, as that guard requires for a genuinely new owner.test/registered/unit/speculative/test_spec_prepare_swa_eviction.py(new): asserts the dflash branch evicts, ticks, evicts before ticking, keeps advancing across iterations (so the counter stays a clock rather than a flag), and that the eagle path is unchanged.Accuracy Tests
Eviction frees only SWA slots behind the window —
max(sliding_window, page_size)back from the sequence on the radix path, page-flooredpre_len - sliding_windowon the chunk-cache path. SWA layers attend within the window, and both the eagle and non-spec paths already call this same code every decode step (eagle_utils.py,mem_cache/allocation.py). No output change observed.Speed Tests and Profiling
The
ignore_eosrequest that previously died at 77,809 tokens completed 100,000 tokens with the fix, at a sustained ~159 tok/s and zero retractions.#swa tokensampled repeatedly across the run stayed between 512 and 1,024 (~1% of the SWA pool), against 77,824 at the point of failure before the fix.Checklist
Prepared with AI assistance.
CI States
Latest PR Test (Base): ❌ Run #31830154702
Latest PR Test (Extra): ❌ Run #31830154341