Skip to content

[Feature][Spec] Support disabling trailing prefix-cache block dropping - #53388

Merged
ZJY0516 merged 6 commits into
vllm-project:mainfrom
ZeldaHuang:support_disable_drop_eagle_block
Sep 1, 2026
Merged

ZJY0516 merged 6 commits into
vllm-project:mainfrom
ZeldaHuang:support_disable_drop_eagle_block

Conversation

@ZeldaHuang

@ZeldaHuang ZeldaHuang commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Purpose

Add an opt-in disable_eagle_block_drop speculative-decoding option for
EAGLE-family methods, including dSpark. When enabled, vLLM keeps the trailing
prefix-cache block instead of conservatively dropping it after speculative-model
prefill.

This does not bypass target-model verification. The option can change which
draft tokens are proposed and therefore may affect speculative-token acceptance
rates, but accepted output tokens are still verified by the target model.

Test Plan

Run an A/B prefix-cache benchmark with the following configuration:

  • 2 nodes x 4 NVIDIA GB300 GPUs, tensor parallel size 8
  • moonshotai/Kimi-K3 MXFP4 weights
  • Inferact/Kimi-K3-DSpark
  • 7 speculative tokens, greedy draft sampling, and FLASHINFER_MLA
  • 12,288-token shared prefix, 256-token suffix, 160 requests, and concurrency 16

Run the following command on both nodes. Set NODE_RANK=0 on the API node and
NODE_RANK=1 on the worker node; only the worker uses --headless.

NODE_RANK=${NODE_RANK:?set NODE_RANK to 0 or 1}
MASTER_ADDR=${MASTER_ADDR:?set MASTER_ADDR to the rank-0 host}
HEADLESS=()
(( NODE_RANK > 0 )) && HEADLESS=(--headless)

vllm serve moonshotai/Kimi-K3 \
  --trust-remote-code \
  --tensor-parallel-size 8 \
  --nnodes 2 \
  --node-rank "$NODE_RANK" \
  --master-addr "$MASTER_ADDR" \
  --served-model-name moonshotai/Kimi-K3 \
  --gpu-memory-utilization 0.92 \
  --max-model-len 32768 \
  --max-num-seqs 16 \
  --max-num-batched-tokens 16384 \
  --moe-backend auto \
  --quantization-config.moe.activation mxfp8 \
  --kv-cache-dtype fp8 \
  --attention-config '{"use_prefill_query_quantization":true,"mla_prefill_backend":"FLASHINFER"}' \
  --enable-prefix-caching \
  --speculative-config '{"method":"dspark","model":"Inferact/Kimi-K3-DSpark","num_speculative_tokens":7,"attention_backend":"FLASHINFER_MLA","draft_sample_method":"greedy","disable_eagle_block_drop":true}' \
  --enforce-eager \
  "${HEADLESS[@]}"

For the default baseline, use the same command but omit
"disable_eagle_block_drop": true.

After resetting the prefix cache, send one untimed request to populate the
12,288-token shared prefix, capture the Prometheus counters, and then run the
timed benchmark:

vllm bench serve \
  --backend openai \
  --base-url http://${MASTER_ADDR}:8000 \
  --model moonshotai/Kimi-K3 \
  --tokenizer moonshotai/Kimi-K3 \
  --trust-remote-code \
  --dataset-name prefix_repetition \
  --num-prompts 160 \
  --prefix-repetition-prefix-len 12288 \
  --prefix-repetition-suffix-len 256 \
  --prefix-repetition-num-prefixes 1 \
  --prefix-repetition-output-len 1 \
  --disable-shuffle \
  --request-rate inf \
  --max-concurrency 16 \
  --temperature 0 \
  --ignore-eos \
  --percentile-metrics ttft,e2el \
  --metric-percentiles 50,90,99

The shared prefix is aligned to Kimi-K3's 1,536-token Mamba state interval.
With the default behavior, the final aligned block is dropped, so each timed
request re-computes 1,536 additional prompt tokens.

For acceptance rate, run a second A/B on the complete GSM8K test split. The
official openai/gsm8k test parquet was exported one-to-one to ShareGPT JSON
because the runtime image does not include the optional Hugging Face datasets
dependency. Both variants use all 1,319 questions in the same order, a maximum
output length of 256, concurrency 16, temperature 0, and EOS handling enabled.
Run one untimed request before capturing the Prometheus counters, then run:

vllm bench serve \
  --backend openai \
  --base-url http://${MASTER_ADDR}:8000 \
  --model moonshotai/Kimi-K3 \
  --tokenizer moonshotai/Kimi-K3 \
  --trust-remote-code \
  --dataset-name sharegpt \
  --dataset-path gsm8k-test-sharegpt.json \
  --sharegpt-output-len 256 \
  --num-prompts 1319 \
  --no-oversample \
  --disable-shuffle \
  --request-rate inf \
  --max-concurrency 16 \
  --temperature 0 \
  --percentile-metrics ttft,tpot,e2el \
  --metric-percentiles 50,90,99

Test Result

All timed prefix-cache requests succeeded (160/160) in both variants.

Metric Default drop disable_eagle_block_drop=true Delta
Cached tokens per request 10,752 12,288 +1,536
Locally computed tokens per request 1,792 256 -1,536
Total cached tokens 1,720,320 1,966,080 +245,760
Total locally computed tokens 286,720 40,960 -245,760
Request throughput 12.720 req/s 25.556 req/s +100.91%
Mean TTFT 1,222.00 ms 604.88 ms -50.50%
P50 TTFT 1,225.40 ms 566.73 ms -53.75%
P90 TTFT 1,227.04 ms 746.72 ms -39.14%

All GSM8K requests also succeeded (1,319/1,319) in both variants. Timed-pass
Prometheus counter deltas matched the benchmark summary:

GSM8K metric Default drop disable_eagle_block_drop=true Delta
Draft steps 84,278 84,436 +158
Draft tokens 589,946 591,052 +1,106
Accepted tokens 239,307 240,658 +1,351
Acceptance rate 40.564% 40.717% +0.153 pp (+0.376% relative)
Mean acceptance length 3.84 3.85 +0.01

No acceptance-rate regression was observed on the complete GSM8K test split;
the no-drop variant increased acceptance by 0.153 percentage points. GSM8K's
independent prompts produced a 0% prefix-cache hit rate, so this result checks
for a general acceptance regression rather than the long-prefix cache-hit path.


Essential Elements of an Effective PR Description Checklist
  • The purpose of the PR, such as "Fix some issue (link existing issues this PR will resolve)".
  • The test plan, such as providing test command.
  • The test results, such as pasting the results comparison before and after, or e2e results
  • (Optional) The necessary documentation update, such as updating supported_models.md and examples for a new model.

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@mergify

mergify Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

This pull request has merge conflicts that must be resolved before it can be
merged. Please rebase the PR, @ZeldaHuang.

https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/syncing-a-fork

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@ivanium ivanium left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. cc @WoosukKwon @njhill

@ZJY0516 ZJY0516 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's merge this first to unblock perf

Co-authored-by: OpenAI Codex <codex@openai.com>
Signed-off-by: Ziming Huang <zelda.huanghuang@gmail.com>
@ZJY0516 ZJY0516 added the ready ONLY add when PR is ready to merge/full CI is needed label Sep 1, 2026
@ZJY0516

ZJY0516 commented Sep 1, 2026

Copy link
Copy Markdown
Member

/ci run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86514 for commit 39b9a8596d5a.

Signed-off-by: Ziming Huang <48115868+ZeldaHuang@users.noreply.github.com>
@ZeldaHuang

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86534 for commit 2f5aefeb0b5c.

Signed-off-by: Ziming Huang <48115868+ZeldaHuang@users.noreply.github.com>
@ZeldaHuang

Copy link
Copy Markdown
Contributor Author

/ci run

@github-actions

github-actions Bot commented Sep 1, 2026

Copy link
Copy Markdown

✅ Triggered Buildkite CI #86546 for commit 412e47943c5d.

@ZJY0516
ZJY0516 merged commit 481839a into vllm-project:main Sep 1, 2026
111 checks passed
am-cohere pushed a commit to am-cohere/vllm that referenced this pull request Sep 1, 2026
vllm-project#53388)

Signed-off-by: Ziming Huang <zelda.huanghuang@gmail.com>
Signed-off-by: Ziming Huang <48115868+ZeldaHuang@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
Ledgero added a commit to Ledgero/vllm that referenced this pull request Sep 1, 2026
…che block

SpeculativeConfig.use_eagle() is a stand-in for "spec decode that reads
target hidden states" and returns True for dflash/dspark too. vllm-project#53388's
use_eagle_block_drop() inherited that: DFlash/DSpark still got the
trailing-block drop unless the new disable flag was set.

Redefine use_eagle_block_drop() to compose with the precise capability bit:
only eagle-family drafters (eagle/eagle3/mtp) share (and pollute) the
target's full-attention KV cache groups; DFlash/DSpark draft from their own
KV cache and never write target blocks.

The spurious back-off made every prompt shorter than two mamba blocks skip
the final block-aligned chunk, so the mamba recurrent state never
materialized on a block boundary and the next turn's prefix-cache lookup
converged to 0 -> the whole context was recomputed on every reply.

use_eagle() keeps its existing semantics (encoder shift, lookahead budget,
num_prefill_lookahead); only the prefix-cache last-block drop is scoped
precisely, and vllm-project#53388's disable_eagle_block_drop flag still applies to the
eagle family. The scheduler warning for a disabled drop is now gated on
eagle-family drafters.

Tests:
- pytest tests/v1/core/test_mamba_align_chunk_split.py -q
- pytest tests/v1/core/test_scheduler.py -q -k mamba_align

Co-authored-by: OpenAI Codex <noreply@openai.com>
Signed-off-by: ouzq <ouzq@seu.edu.cn>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 1, 2026
PR vllm-project#53388 added SpeculativeConfig.disable_eagle_block_drop and routed
every EAGLE trailing prefix-cache block-drop site through a single
use_eagle_block_drop() predicate. Adopt that mechanism to make the safe
default drafter-method aware: the flag becomes `bool | None = None`, and
when unset, use_eagle_block_drop() resolves from the method --
eagle/eagle3/mtp keep the drop (behavior unchanged), dflash/dspark
disable it. An explicit user setting always wins, and the experimental
warning now fires only on an explicit opt-out.

The drop exists because EAGLE-family drafters combine the
prefill-lookahead token (one past a chunked-prefill boundary) with the
chunk's final hidden state and write the result into the drafter KV
cache, so the last block of a prefix-cache hit may hold KV polluted by a
continuation the matching request does not share. dflash/dspark drafters
structurally cannot cache lookahead-polluted KV: their context KV is
projected from target hidden states and positions only
(precompute_and_store_context_kv), and the lookahead (anchor) token
writes KV only at positions past the chunk end, in a block that is
overwritten with clean context KV before it can be completed and hashed.
The drop therefore protects nothing for them, while costing one full
scheduler block of recompute on every prefix-cache hit. On hybrid mamba
models in align mode both gates -- the FullAttn hit drop and the
chunk-split last_cache_position backoff -- ride the same predicate, so
they move together and the recovered block is actually usable.

Behavior matches the previously measured explicit exemption: with a
dspark drafter on a hybrid mamba target (32K-token shared prefix, 2K
unique suffix, 256 output, temp 0, scheduler block 2192), steady-state
cache-hit cached_tokens rise 28,496 -> 30,688 (hit recompute 6,336 ->
4,144 tokens) in every repeat on two GPU generations, cache-hit TTFT
improves ~20-22%, decode throughput is flat within run-to-run noise, and
acceptance length stays pinned at 3.00 with identical per-position
acceptance rates. Hit-vs-miss logit deltas on the reused block sit below
the within-hit noise floor and at the same order as the unpatched
control's, with the greedy argmax stable.

A None default resolves fail-closed: new eagle-family methods keep the
drop until their drafter KV provenance is audited.

Signed-off-by: Rishi Puri <riship@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 1, 2026
PR vllm-project#53388 added SpeculativeConfig.disable_eagle_block_drop and routed
every EAGLE trailing prefix-cache block-drop site through a single
use_eagle_block_drop() predicate. Adopt that mechanism to make the safe
default drafter-method aware: the flag becomes `bool | None = None`, and
when unset, use_eagle_block_drop() resolves from the method --
eagle/eagle3/mtp keep the drop (behavior unchanged), dflash/dspark
disable it. An explicit user setting always wins, and the experimental
warning now fires only on an explicit opt-out.

The drop exists because EAGLE-family drafters combine the
prefill-lookahead token (one past a chunked-prefill boundary) with the
chunk's final hidden state and write the result into the drafter KV
cache, so the last block of a prefix-cache hit may hold KV polluted by a
continuation the matching request does not share. dflash/dspark drafters
structurally cannot cache lookahead-polluted KV: their context KV is
projected from target hidden states and positions only
(precompute_and_store_context_kv), and the lookahead (anchor) token
writes KV only at positions past the chunk end, in a block that is
overwritten with clean context KV before it can be completed and hashed.
The drop therefore protects nothing for them, while costing one full
scheduler block of recompute on every prefix-cache hit. On hybrid mamba
models in align mode both gates -- the FullAttn hit drop and the
chunk-split last_cache_position backoff -- ride the same predicate, so
they move together and the recovered block is actually usable.

Behavior matches the previously measured explicit exemption: with a
dspark drafter on a hybrid mamba target (32K-token shared prefix, 2K
unique suffix, 256 output, temp 0, scheduler block 2192), steady-state
cache-hit cached_tokens rise 28,496 -> 30,688 (hit recompute 6,336 ->
4,144 tokens) in every repeat on two GPU generations, cache-hit TTFT
improves ~20-22%, decode throughput is flat within run-to-run noise, and
acceptance length stays pinned at 3.00 with identical per-position
acceptance rates. Hit-vs-miss logit deltas on the reused block sit below
the within-hit noise floor and at the same order as the unpatched
control's, with the greedy argmax stable.

A None default resolves fail-closed: new eagle-family methods keep the
drop until their drafter KV provenance is audited.

Signed-off-by: Rishi Puri <riship@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
ptorsten added a commit to ptorsten/vllm that referenced this pull request Sep 2, 2026
…hetero tests, xfail vllm-project#53388 boundary test under grid stops, mixed-page fallback, keep packed path as upstream

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
mylibrar pushed a commit to tanyuqian/vllm that referenced this pull request Sep 3, 2026
vllm-project#53388)

Signed-off-by: Ziming Huang <zelda.huanghuang@gmail.com>
Signed-off-by: Ziming Huang <48115868+ZeldaHuang@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
sheralskumar pushed a commit to sheralskumar/vllm that referenced this pull request Sep 8, 2026
vllm-project#53388)

Signed-off-by: Ziming Huang <zelda.huanghuang@gmail.com>
Signed-off-by: Ziming Huang <48115868+ZeldaHuang@users.noreply.github.com>
Co-authored-by: OpenAI Codex <codex@openai.com>
puririshi98 added a commit to puririshi98/vllm that referenced this pull request Sep 9, 2026
PR vllm-project#53388 added SpeculativeConfig.disable_eagle_block_drop and routed
every EAGLE trailing prefix-cache block-drop site through a single
use_eagle_block_drop() predicate. Adopt that mechanism to make the safe
default drafter-method aware: the flag becomes `bool | None = None`, and
when unset, use_eagle_block_drop() resolves from the method --
eagle/eagle3/mtp keep the drop (behavior unchanged), dflash/dspark
disable it. An explicit user setting always wins, and the experimental
warning now fires only on an explicit opt-out.

The drop exists because EAGLE-family drafters combine the
prefill-lookahead token (one past a chunked-prefill boundary) with the
chunk's final hidden state and write the result into the drafter KV
cache, so the last block of a prefix-cache hit may hold KV polluted by a
continuation the matching request does not share. dflash/dspark drafters
structurally cannot cache lookahead-polluted KV: their context KV is
projected from target hidden states and positions only
(precompute_and_store_context_kv), and the lookahead (anchor) token
writes KV only at positions past the chunk end, in a block that is
overwritten with clean context KV before it can be completed and hashed.
The drop therefore protects nothing for them, while costing one full
scheduler block of recompute on every prefix-cache hit. On hybrid mamba
models in align mode both gates -- the FullAttn hit drop and the
chunk-split last_cache_position backoff -- ride the same predicate, so
they move together and the recovered block is actually usable.

Behavior matches the previously measured explicit exemption: with a
dspark drafter on a hybrid mamba target (32K-token shared prefix, 2K
unique suffix, 256 output, temp 0, scheduler block 2192), steady-state
cache-hit cached_tokens rise 28,496 -> 30,688 (hit recompute 6,336 ->
4,144 tokens) in every repeat on two GPU generations, cache-hit TTFT
improves ~20-22%, decode throughput is flat within run-to-run noise, and
acceptance length stays pinned at 3.00 with identical per-position
acceptance rates. Hit-vs-miss logit deltas on the reused block sit below
the within-hit noise floor and at the same order as the unpatched
control's, with the greedy argmax stable.

A None default resolves fail-closed: new eagle-family methods keep the
drop until their drafter KV provenance is audited.

Overlap: open PR vllm-project#54163 (Fixes vllm-project#53477) stops the same dflash/dspark
block drop by redefining use_eagle_block_drop() over a method list
(use_eagle_preserves_target_kv_cache(): eagle, eagle3, mtp) in the same
predicate and the same scheduler warning block, reaching the same
default outcome. This commit keeps the explicit flag and resolves its
None default per method inside use_eagle_block_drop(), so an explicit
user setting still wins for every method. Whichever lands first, the
other reduces to a rebase.

Signed-off-by: Rishi Puri <riship@nvidia.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kv-cache-manager kv-connector needs-rebase ready ONLY add when PR is ready to merge/full CI is needed scheduler

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants