Repository navigation
[Bugfix][Core] Cover the first spec decode step in the full-sequence admission gate - #63288
rebel-jinhwan wants to merge 4 commits into
Conversation
The full-sequence admission gate counts request.num_tokens alone, but the chunk that completes the sequence also allocates num_lookahead_tokens slots. A prompt whose last block has fewer free slots than the lookahead then passes the gate and fails to allocate that final chunk, which preempts it after its prefill ran. Count the lookahead in num_tokens and keep num_tokens_main_model at the sequence, as the allocation itself does. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: rebel-jinhwan <jinhwan.suk@rebellions.ai>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
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>
The decode step right after the prefill schedules the sampled token and the draft tokens on top of the lookahead, so a prompt that fits with the lookahead alone can still be preempted on that step after its prefill ran. The scheduler passes those tokens to the gate as num_decode_step_tokens, the same 1 + num_spec_tokens margin _request_remaining_blocks already uses. Async loads keep reserving that step through reserved_blocks instead. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: rebel-jinhwan <jinhwan.suk@rebellions.ai>
num_decode_step_tokens depends only on num_spec_tokens, so set it in __init__ next to num_lookahead_tokens. The waiting loop still zeroes it for async loads, whose margin stays in reserved_blocks. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: rebel-jinhwan <jinhwan.suk@rebellions.ai>
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>
An async load's decode step margin sits in reserved_blocks, and the gate counts it once the load finishes. Say so where the waiting loop zeroes num_decode_step_tokens. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: rebel-jinhwan <jinhwan.suk@rebellions.ai>
Overview
The full-sequence admission gate (
allocate_slots(..., full_sequence_must_fit=True), which the scheduler uses wheneverscheduler_reserve_full_islis on, the default) checks the prompt alone. With speculative decoding, the request's last prefill chunk also allocates the lookahead slots, and the decode step right after the prefill adds the sampled token and the draft tokens. This PR makes the gate cover that step.Claims
num_tokens + 1 + num_spec_tokens + num_lookahead_tokens, capped atmax_model_len. Before, a prompt that fit only without those slots passed the gate, ran its prefill, and was then preempted on its last chunk (the lookahead) or on its first decode step (the sampled and draft tokens). FCFS preemption pops the newest running request, which is that request, so the prefill is thrown away; in the scheduler test below this happens three times in a row.reserved_blocks(see [Bugfix][Core] Reserve only the blocks a spec decode step adds when admitting async KV loads #63136): the scheduler passes neither the lookahead nor the decode step to the gate there.Validation
tests/v1/core/test_prefix_caching.py::test_full_sequence_admission_counts_the_first_decode_step: four usable 16-token blocks; prompt / decode-step tokens / lookahead of 64/0/0, 62/0/3, 57/4/3 and 58/4/3.tests/v1/core/test_scheduler.py::test_full_sequence_admission_covers_the_first_decode_step: realScheduler, ngram k=3. A 30-token prompt arrives while two decodes hold all but two 16-token blocks; its prefill fits, its first decode step (30 + 1 + 3 slots) needs a third block.tests/v1/core/test_prefix_caching.py,test_kv_cache_utils.py,test_scheduler.py,test_priority_preemption_bug.py: 511 passed. Two tests intest_scheduler.pyfail the same way without this change, for local reasons: the run used--noconftest(missingcaplog_vllmfixture) and the host has no Hugging Face Hub access.Environment: CPU-only host, this branch's sources on top of a vllm 0.30.0+cpu environment.
End to end on 1x A10 (driver 570, CUDA 13 forward-compatibility libraries), vllm
0.32.1rc1.dev368+g32551991b(the nightly wheel of this PR's base commit) with main's or this PR'skv_cache_manager.pyandscheduler.py. Qwen2.5-1.5B-Instruct with Qwen2.5-0.5B-Instruct as adraft_modeldrafter (num_speculative_tokens=3, lookahead 3),block_size=64, a 7-block pool,max_num_batched_tokens=32, no prefix caching, greedy. Two decodes hold four blocks;varrives with two blocks (128 slots) free.vd0/d1outputs are identical in every run, and the 125-token runs were repeated twice with the same result. With the fixvwaits for a decode to free a block instead, so it finishes 1-3 engine steps later in this pool.v's own output follows prefill chunking: its first token is a tie (tokens 12 and 220 at -4.871 for the 125-token prompt with 32-token chunks).Reproducer (
MODE=decode_steporMODE=lookahead)Details
Root cause. The gate passed
request.num_tokensboth asnum_tokens, which should include the lookahead, and asnum_tokens_main_model, unlike the allocation path. It now passes the sequence through the first decode step (num_decode_step_tokens = 1 + num_spec_tokens, the margin_request_remaining_blocksalready adds) and the lookahead on top, both capped atmax_model_len.Tradeoff. A request whose sequence ends within
1 + num_spec_tokens + num_lookahead_tokensof a block boundary needs one more free block to be admitted; with the pool full it would be preempted on that step anyway. Without speculative decoding the gate is unchanged, so a block-aligned prompt's first decode token stays unreserved as before.How it was found. In vllm-rbln, whose scheduler never mixes prefill and decode, this mismatch is a livelock: the self-preempted prefill evicts the decode batch on every re-admission, so no block is ever freed (RBLN-SW/vllm-rbln#1330). On upstream's scheduler it shows as recomputed prefills instead. vllm-rbln patches the gate today; this fix lets it drop the patch.
Not a duplicate. #57104, #59880 and #63136 handle the spec decode margin for async KV loads (
reserved_blocks); this PR covers every other admission. #57658 aligns Mamba admission with dynamic K; #52520 / #52530 concern Mambaalignpool sizing.Pull Request Checklist
I used vLLM's
/pr-checklistskill. (Mandatory for agents, optional for humans).AI assistance was used during the creation of this PR.
Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.
Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.
Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.
Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.