Repository navigation
[Core][Spec Decode] Lookahead block hashes for EAGLE prefix caching - #61589
Merged
Merged
Conversation
Hash the token after each block so EAGLE-family drafters can reuse the last matched prefix-cache block instead of dropping it. KV connectors keep the legacy hashes; disable_lookahead_block_hashes opts out. Split from vllm-project#50897. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> # Conflicts: # vllm/v1/core/kv_cache_utils.py # vllm/v1/engine/core.py
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Contributor
|
This pull request has merge conflicts that must be resolved before it can be |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
ZJY0516
marked this pull request as ready for review
October 14, 2026 09:14
ZJY0516
requested review from
AndreasKaratzas,
ApostaC,
DarkLight1337,
WoosukKwon,
alexm-redhat,
heheda12345,
houseroad,
ivanium,
mgoin,
njhill,
orozery,
robertgshaw2-redhat,
tlrmchlsmth,
wzhao18,
youkaichao and
ywang96
as code owners
October 14, 2026 09:14
Member
Author
|
/ci run --allow-stale |
|
✅ Triggered Buildkite CI #97197 for commit
|
edit: allow explicit speculative configs in scheduler tests Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Dakai An <dakaian108@gmail.com>
Contributor
|
/ci run |
|
❌ @andakai, Only reviewers with write access can use CI commands before CI is delegated to the PR author. |
Contributor
|
/ci run |
|
✅ Triggered Buildkite CI #97218 for commit |
ZJY0516
enabled auto-merge (squash)
October 26, 2026 05:40
Lucas-Fernandes-Martins
added a commit
to Lucas-Fernandes-Martins/vllm
that referenced
this pull request
Oct 26, 2026
Resolve conflicts with vllm-project#61589 by generating lookahead tokens and multimodal extra keys for each published partial-tail hash. Retain upstream lookahead test configuration and extend regressions across lookahead lengths 0, 1, and 2 and DCP world sizes 1, 2, and 4. Assisted-by: OpenAI Codex Signed-off-by: Lucas-Fernandes-Martins <lucasfmartins16@gmail.com>
2 tasks done
vMaroon
added a commit
to vMaroon/vllm
that referenced
this pull request
Oct 26, 2026
vllm-project#61589 adds `lookahead_token_ids` to `BlockStored` as a block hash input. Snapshot stores dropped it, so a consumer that hashes tokens would key those blocks differently, and a hash restated with other lookahead tokens went unnoticed. The recorder now keeps them per block, compares them once stated (offload stores omit them), and replays them. Co-authored-by: Claude <noreply@anthropic.com> Signed-off-by: Maroon Ayoub <mayoub@redhat.com>
Micador
pushed a commit
to Micador/vllm
that referenced
this pull request
Oct 27, 2026
…llm-project#61589) Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Signed-off-by: Dakai An <dakaian108@gmail.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dakai An <dakaian108@gmail.com> Co-authored-by: Dakai An <77474977+andakai@users.noreply.github.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Micador
pushed a commit
to Micador/vllm
that referenced
this pull request
Oct 27, 2026
…llm-project#61589) Signed-off-by: zjy0516 <riverclouds.zhu@qq.com> Signed-off-by: Dakai An <dakaian108@gmail.com> Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Dakai An <dakaian108@gmail.com> Co-authored-by: Dakai An <77474977+andakai@users.noreply.github.com> Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai> Co-authored-by: Codex <noreply@openai.com> Signed-off-by: Christopher Mackenzie <christopher.mackenzie.software@gmail.com>
Peuqui
added a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Oct 28, 2026
…589) The EAGLE/MTP draft KV at a block's last position depends on the token after the block, so every prefix hit dropped the last matched block. On hybrid models the attention block is unified to the Mamba page, so that block is 800 tokens on the Qwen3.8 27B and 1,616 on Flash-Next: a 23,527 token resend hit 22,400 instead of 23,200 tokens and recomputed 1,127. Backport of vllm-project/vllm#61589 in its revision of 2026-10-14 (upstream merged a revised version on 2026-10-26): each block hash also covers the token after the block, emitted once that token exists, and the trailing-block drop is gone. A block is reused only when its lookahead token matches, so a hit reads the same draft KV as a cold run. The block before the last sampled token is published when the request stops. Fitted to this tree: the coordinator keeps the annotated EAGLE groups but takes drop decisions from drop_eagle_group_ids, which is empty without the drop (DeepSeek-V4 groups are annotated independently of use_eagle); the retention backport's replay boundaries, the Mamba lookbehind and the scheduler's aligned chunk split follow the drop, while the encoder shift and the P/D lookahead limit keep EAGLE semantics (use_eagle vs use_eagle_block_drop, as upstream). Enabled with prefix caching for use_eagle_kv_cache() methods (DFlash keeps its own exemption), not with a KV connector or multi-module MTP, and switchable off with speculative_config.disable_lookahead_block_hashes; scheduler-side only, the compile hash is unchanged. cache_blocks counts only hashed blocks. The streaming-session reset also truncates block hashes that covered the discarded token (upstream already does this without lookahead). Tests: tests/v1/core/test_lookahead_block_hashes.py (24, adapted from the PR; the hybrid case checks 1,088 with lookahead and without speculation against 544 with the drop). Fakes in the Mamba chunk-split and retention tests and the abort test take the new fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaG8VjXn5eN3Aq7eZrA3dp Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
Peuqui
added a commit
to Peuqui/1Cat-vLLM
that referenced
this pull request
Oct 28, 2026
…589) The EAGLE/MTP draft KV at a block's last position depends on the token after the block, so every prefix hit dropped the last matched block. On hybrid models the attention block is unified to the Mamba page, so that block is 800 tokens on the Qwen3.8 27B and 1,616 on Flash-Next: a 23,527 token resend hit 22,400 instead of 23,200 tokens and recomputed 1,127. Backport of vllm-project/vllm#61589 in its revision of 2026-10-14 (upstream merged a revised version on 2026-10-26): each block hash also covers the token after the block, emitted once that token exists, and the trailing-block drop is gone. A block is reused only when its lookahead token matches, so a hit reads the same draft KV as a cold run. The block before the last sampled token is published when the request stops. Fitted to this tree: the coordinator keeps the annotated EAGLE groups but takes drop decisions from drop_eagle_group_ids, which is empty without the drop (DeepSeek-V4 groups are annotated independently of use_eagle); the retention backport's replay boundaries, the Mamba lookbehind and the scheduler's aligned chunk split follow the drop, while the encoder shift and the P/D lookahead limit keep EAGLE semantics (use_eagle vs use_eagle_block_drop, as upstream). Enabled with prefix caching for use_eagle_kv_cache() methods (DFlash keeps its own exemption), not with a KV connector or multi-module MTP, and switchable off with speculative_config.disable_lookahead_block_hashes; scheduler-side only, the compile hash is unchanged. cache_blocks counts only hashed blocks. The streaming-session reset also truncates block hashes that covered the discarded token (upstream already does this without lookahead). Tests: tests/v1/core/test_lookahead_block_hashes.py (24, adapted from the PR; the hybrid case checks 1,088 with lookahead and without speculation against 544 with the drop). Fakes in the Mamba chunk-split and retention tests and the abort test take the new fields. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YaG8VjXn5eN3Aq7eZrA3dp Signed-off-by: Peuqui <43776522+Peuqui@users.noreply.github.com>
1 task done
5 of 6 tasks
5 tasks done
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Overview
Part 1 of 2 split from #50897 (implements #50438): lookahead block hashes for the local prefix cache. EAGLE, EAGLE3 and MTP drafters write the draft KV at a block's last position from the token after the block, so
maindrops the last matched block on every hit. This PR puts that token into the block hash and removes the drop when no KV connector is configured. KV connector support is in the stacked PR (part 2).Claims
floor((N - 1) / B) * B, the same as without speculative decoding, instead of one cache unit less (544–1600 tokens on hybrid attention + Mamba/GDN models). Multi-module MTP with k modules reachesfloor((N - k) / B) * B.SpeculativeConfig.disable_lookahead_block_hashesrestoresmain's behavior. With any KV connector configured, this PR keeps the legacy hashes.BlockStored.lookahead_token_idsfield, so consumers can reproduce the hashes.Validation
Unit tests on this branch (one GB200 node):
python -m pytest tests/v1/core tests/v1/streaming_input/test_scheduler_streaming.py \ tests/v1/kv_connector/unit/test_nixl_connector_hma.py tests/models/kimi_k3/test_kda_metadata.py \ tests/distributed/test_kv_cache_events.py \ --ignore tests/v1/core/prefix_cache/test_hybrid_prefix_cache_hit_rate.py -k "not ray"Result: 1,256 passed.
test_hybrid_prefix_cache_hit_rate.pyis excluded because it launches thevllmCLI (see the TODO below).Hash format CPU cost: one
request_block_hashercall over a whole prompt, SHA-256, one pinned GB200 Grace core, median of 15 alternating-order rounds of 5 calls. Token IDs are random, and the prompt length is aligned + 1 so every format emits the same hashes; MM rows add a 512-token image every 1,024 tokens. Each variant is the real hasher with only its lookahead branch replaced; an unmodified copy run the same way stays within 0.2% of the real one. Percentages are versusmain.H((parent, tokens[s:e], (E(s, e), tokens[e], E(e, e + 1)))), the previous revision of this PRH((parent, tokens[s:e + 1], E(s, e + 1)))H((parent, tokens[s:e], (E(s, e + 1), tokens[e])))One
Ecall instead of two saves 7–17% at B ≤ 128 versus the nested form. The expanded window is cheapest, but its hash input equals a legacy hash with block sizeB + 1; keepingtokens[e]in the extra keys costs 3–5% more than it at B = 16 and at most 2.5% at B ≥ 128. With one lookahead token the hasher indexestokens[e]instead of slicing, which builds the same tuple; the slice cost about 7 more points at B = 16.End-to-end and draft-KV results from the combined PR (#50897, earlier revision; not yet re-run on this split):
prefix_repetitionmainHit length, Qwen3.5-2B + MTP k=1, unit 544, 1,367-token prompt sent twice:
mainwithout spec decode 1088,mainwith MTP 544, lookahead hashes 1088.Draft-KV check (KV of the hit blocks vs a cold run, bf16 max abs diff): Qwen3-8B + EAGLE3 0.09–0.14 (negative control with a stale lookahead token: 7.55); Qwen3.5-2B MTP 0.19 (control: 14.53).
TODO before review: re-run
tests/v1/e2e/general/test_mamba_prefix_cache.py,tests/v1/core/prefix_cache/test_hybrid_prefix_cache_hit_rate.pyand the benchmark on GPU.Details
Enablement.
get_num_hash_lookahead_tokens(v1/core/kv_cache_utils.py) returnsL = num_prefill_lookahead_tokens(1, or k for multi-module MTP with k modules) whenuse_eagle_block_drop(),not disable_lookahead_block_hashes, prefix caching is on and there is no KV transfer config; otherwise 0, which keeps the legacy hashes and the block drop. DFlash and DSpark build draft KV from target hidden states alone; #54163 removes them fromuse_eagle_block_drop(), after which they keep the legacy hashes with no drop. Until then they get lookahead hashes too, which is correct but unnecessary.num_hash_lookahead_tokensreplaces the former boolean flag through the KV cache manager, coordinator, block pool and scheduler. Part 2 replaces the last condition with a per-connector capability.Hashing.
get_request_block_hasher(..., num_hash_lookahead_tokens=L)hashes a block[s, e)only once tokens up toe + L - 1are inall_token_ids, asH((parent, tokens[s:e], (E(s, e + L), *tokens[e:e + L]))); forL = 1that is(E(s, e + 1), tokens[e]).Eisgenerate_block_hash_extra_keysover the block and its lookahead tokens, called once per block, so MM inputs or prompt embeddings there are covered too; the MM cursor then steps back to the first input the next block overlaps. The hashed tokens stay the block's tokens, and the(E, token)shape keeps these hashes apart from legacy ones; hashingtokens[s:e + L]instead would equal a legacy hash with block sizeB + L. DifferentLgive different tuple lengths. All KV cache groups share these hashes.KV events.
BlockStored.lookahead_token_idsholdstokens[e:e + L]per block, andextra_keyskeeps its untagged per-block shape but covers[s, e + L). A block hash is reproducible fromtoken_ids,extra_keysandlookahead_token_ids(test_kv_events_reconstruct_lookahead_block_hashes). Consumers that ignore the new field still decode events, but blocks with the same tokens at the same position can now carry different hashes, so routers that index one hash per position need to key on the lookahead tokens. The extra field widens the msgpack map header of everyBlockStored(fixmap to map16); decoded contents are unchanged.examples/features/kv_events/kv_events_subscriber.pyincludes the field.Caching. Blocks are cached at allocation, as without lookahead hashes;
SingleTypeKVCacheManager.cache_blocksstops at the last hashed block. At request finish,update_from_outputcaches the block whose lookahead the last sampled token completes.Drop removal. The scheduler clears
use_eagle_block_drop.KVCacheCoordinator.drop_eagle_group_idsis empty in this mode and drives the per-group drop andreplay_boundaries, so the last-block drop, the Mamba checkpoint back-off and the replay-boundary shift are all skipped from one place.eagle_group_idsstays, sincemamba_shared_prefix_checkpointstill applies.Docs. A new "Prefix caching" section in
docs/features/speculative_decoding/README.mdcovers the default,disable_lookahead_block_hashesandBlockStored.lookahead_token_ids.Why it is correct. The drafter writes the draft KV at
e - 1(input(h_{e-1}, token e)) in the step that prefills or samples tokene. The hash exists only once tokeneis inall_token_ids, which never holds draft or in-flight tokens, so a cached block's draft KV is written by the end of that step. With k-module MTP, module j writes its KV atpfrom tokenp + j, so the hash waits for tokens up toe + k - 1.MultiModuleMTPSpeculatorre-prefills a slot written from a rejected draft in the step that samples its replacement, and the coordinator already keeps the lastk - 1computed tokens out of the cache.Limitations
HiSparseConnector, so like other connectors it keeps the legacy hashes and the block drop. Its host tier publishes pages once their host copies finish; whether those copies include the last position's draft KV needs GPU validation before part 2 letsHiSparseConnectoropt in.main); see [Core][Spec Decode] Add lookahead-aware prefix cache hashing for EAGLE-style draft models #50897 for the reproducer.Duplicate-work check
Split from #50897 by the same author. No other open PR adds the successor token to the prefix-cache key (#44986, #45614, #48375 and #39171 change when the drop applies, not the key).
AI assistance (Claude Code) was used to split, implement, test and describe this change. The submitting human reviews every changed line and is responsible for it end to end.
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.
🤖 Generated with Claude Code