Repository navigation
fix(scheduler): cover the first spec decode step in the admission gate - #1330
rebel-jinhwan wants to merge 4 commits into
Conversation
78683c1 to
c755406
Compare
|
Reworked: the gate now counts the lookahead through a registry patch ( 🤖 Generated with Claude Code |
The scheduler raises num_lookahead_tokens to num_spec_tokens even for ngram, and the final prefill chunk allocates those slots, but the full-sequence admission gate counts request.num_tokens alone. A prompt ending within the lookahead of a block boundary then needs one block more on its final chunk than it was admitted with. With the rest of the pool held by decodes, that chunk preempts the prefill itself, its resume evicts the decode batch again (no mixed batching), and no decode ever runs to free a block. The gate is patched through the registry: upstream has no hook for the token count it passes on. The test also covers sub-block caching off, which sub_block_size=None does not mean. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The gate counted the last chunk's lookahead but not the decode step after the prefill, which adds the sampled and draft tokens. A prompt that fits with the lookahead but not with that step was admitted and preempted itself on its first decode step. The scheduler passes the step's tokens as num_decode_step_tokens, mirroring vllm-project/vllm#63288; the patch takes the argument upstream does not have yet. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Matches vllm-project/vllm#63288, which now sets num_decode_step_tokens in the Scheduler's __init__; this copy goes with the patch. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
7e35f52 to
29db5d5
Compare
|
@rebel-cjlee with high kv cache pressure and speculative decoding, livelock can be occurred (admit -> prefill -> try decode -> preempt itself -> admit -> prefill -> try decode -> ...). This PR fixes admission gate to take account not only prompt tokens but also lookahead and decode step tokens. Could you take a look when you have a time? |
|
@rebel-jinhwan How often this show up in practice? |
|
@rebel-cjlee thanks, it appears on high concurrency & spec dec where every block is used. It is not very often but it makes livelock, so i think that it is the issue that must be fixed |
🚀 Summary of Changes
Fixes a livelock in which a rank stops generating once the KV cache is full: the scheduler keeps cycling on one prefill and no request ever finishes. This is the cause of the stalls seen in long-context benchmarks.
The scheduler raises
num_lookahead_tokenstonum_spec_tokenseven for ngram (#1128), and the final prefill chunk allocates those slots. The full-sequence admission gate (full_sequence_must_fit) countsrequest.num_tokensalone. A prompt whose last block has fewer free slots than the lookahead (in particular, any block-aligned prompt) therefore needs one block more on its final chunk than it was admitted with. When the rest of the pool is held by decodes:No decode ever runs, so no block is freed.
The same happens one step later, which review of the upstream fix (vllm-project/vllm#63288) pointed out: the first decode step after the prefill schedules the sampled token and the draft tokens on top of the lookahead,
num_tokens + 1 + num_spec_tokens + num_lookahead_tokensslots. A prompt that fits with the lookahead alone is admitted and preempts itself on that step after its prefill ran.The gate now covers the sequence through that step. The scheduler passes the step's main-model tokens,
1 + num_spec_tokens, toallocate_slotsasnum_decode_step_tokens(RBLNKVCacheManagerforwards it), and a registry patch,patches/admission_lookahead.py, does the counting on two upstream methods:KVCacheManager.allocate_slotstakesnum_decode_step_tokens, which vllm 0.31 does not have, and records it withnum_lookahead_tokensin aContextVarfor the duration of a call made withfull_sequence_must_fit=True.KVCacheCoordinator.get_num_blocks_to_allocate, on the gate's call only (apply_admission_cap=True), adds the decode step tonum_tokens_main_modeland the lookahead on top of it tonum_tokens, both capped atmax_model_len. That restores the coordinator's own contract, in whichnum_tokensincludes the lookahead andnum_tokens_main_modelexcludes it; the gate breaks it by passing the prompt for both. Upstream's block arithmetic still does the counting.Outside a gated call the variable is unset, so upstream's other admission-cap caller (
Scheduler._request_remaining_blocks) counts what it did. The waiting loop already passeseffective_lookahead_tokens, so the async P/D + EAGLE case that zeroes the lookahead keeps zero. Unlike upstream, the decode step is counted for async KV loads too: this scheduler has no separate spec decode reservation for them. The upstream fix is vllm-project/vllm#63288, which adds the samenum_decode_step_tokensargument; once the vllm pin includes it, the patch goes and the scheduler's call stays.Split out of #1315. #1315 now carries only the sub-block match fix and is stacked on this PR. This one has to land first: the retry in #1315 removes an accidental escape that
devhas from this livelock, so #1315 alone would turn a configuration that finishes today into one that stalls.Model path: vllm model path only. The optimum model path uses
optimum_scheduler.py, which does not have this admission path.📌 Related Issues / Tickets
✅ Type of Change
release)feature)model)core)fix)perf)refactor)docs)other): please describe🧪 How to Test
uv run --no-sync pytest "tests/vllm/v1/core/test_rbln_scheduler.py::TestAdmissionCoversStepAfterPrefill"Expected: a prompt admitted with two free blocks, while two drafting decodes hold the rest of the pool, finishes without being preempted. Two prompts, both two chunks long: 32 tokens, whose last chunk's lookahead needs a third block, and 26 tokens, which fits with the lookahead but whose first decode step needs 26 + 1 + 3 + 3 slots. Each runs with sub-block caching off, at sub-block size 4 and at 8, with and without a KV connector.
On
devall 12 cases fail; with only the lookahead counted, the six 26-token cases still fail.📸 Screenshots / Logs (if applicable)
This host cannot install the locked environment (glibc 2.35;
devpins vllm 0.31.0+cpu), so this was checked on vllm 0.30.0+cpu with the same changes applied on top of44c3ece, CPU only:Please let CI confirm on 0.31.0.
📋 Checklist
💬 Notes
coordinator.get_num_blocks_to_allocatewith keywords and marking that call withapply_admission_cap=True, and on callers passingfull_sequence_must_fitby keyword. A positional argument would raise; a renamed flag would silently skip the margin, whichTestAdmissionCoversStepAfterPrefillcatches. The alternative was duplicating upstream's admission arithmetic (retained-hit trimming,prefill_end, watermark) in the scheduler.allocate_slotsconsumesnum_decode_step_tokens; the patch applies in every vllm-model-path process._request_remaining_blocksdoes, and amax_tokens == 1request reserves it too although it never decodes.allocate_slotscall, then as the registry patch counting the lookahead only. The decode step came from review of the upstream PR.sub_block_size=Nonedoes not turn sub-block caching off increate_rbln_scheduler: it takes the prefill chunk. The test therefore setsenable_sub_block_cacheexplicitly, and sub-block caching off is the case without the accidental escape.vllm:num_preemptions_total, which is why these stalls showed only one preemption in metrics. Both are worth separate changes.🤖 Generated with Claude Code