Skip to content

[2/N][Core] support partial prefix cache hit for hybrid model - #46384

Merged
ivanium merged 31 commits into
vllm-project:mainfrom
ZJY0516:feature/partial-prefix-cache-coordinator
Jul 12, 2026
Merged

[2/N][Core] support partial prefix cache hit for hybrid model#46384
ivanium merged 31 commits into
vllm-project:mainfrom
ZJY0516:feature/partial-prefix-cache-coordinator

Conversation

@ZJY0516

@ZJY0516 ZJY0516 commented Jun 22, 2026

Copy link
Copy Markdown
Member

Purpose

This PR builds on the partial prefix-cache primitives from #45939 and completes fine-grained prefix-cache hits for aligned full-attention + Mamba hybrid models. The overall design is described in RFC #45702.

The PR adds:

  • a public prefix_match_unit that selects prefix-match granularity independently from physical KV-cache block sizes;
  • fine-grained lookup in the full-attention and Mamba cache managers;
  • token-accurate convergence in the hybrid coordinator;
  • scheduler boundaries needed to materialize and resume partial Mamba states;
  • copy-on-write for requests that continue from a shared partial block;
  • worker-side block copies with scheduler-side lifetime fencing.

It e2e works now

vllm serve Qwen/Qwen3.5-35B-A3B-FP8 --enable-prefix-caching --mamba-cache-mode align --prefix-match-unit 16

Use 2 sequential request to simulate multi-turn:

  • 10000 in, 1 out
  • 10000 + 200 in, 1 out
w/ partial cache hit
"first_latency_sec": 0.3047793720033951, "second_latency_sec": 0.09551339800236747

w/o partial cache hit
"first_latency_sec": 0.2865299719851464, "second_latency_sec": 0.13373498094733804

Design

Match unit and cache-manager hash views

Request.block_hashes is computed at prefix_match_unit granularity. Each KV-cache group continues to allocate its existing physical block size.

For example:

prefix_match_unit          = 2
full-attention block_size  = 4
Mamba block_size           = 8

request hashes: H2, H4, H6, H8, H10, ...

Managers using physical-block lookup receive a grouped view such as [H4, H8, ...]. Managers supporting fine-grained lookup keep the original hash list and may probe H2 or H6 inside a physical block.

The configured match unit must divide every cache-group block size. If it is not specified, vLLM derives it from the GCD of the resolved group block sizes.

Explicit token-length cache hits

Cache-manager lookup now returns both the physical blocks and the exact number of matched tokens:

(blocks_by_group, hit_length)

The separate length is required because a hit at H6 includes the physical block [4, 8), but only tokens [4, 6) in that block are valid.

FullAttentionManager first matches complete physical blocks from the start of the request, then probes hash boundaries inside the next block from longest to shortest. MambaManager keeps its existing right-to-left state lookup, but can now search at match-unit boundaries and return the physical block containing the matched recurrent state.

Managers without fine-grained support continue to return block-aligned hit lengths through the same API.

Hybrid-coordinator convergence

The hybrid coordinator tracks a token hit length independently from each group's block list. It repeatedly reduces the candidate length until every cache group can cover the same logical prefix.

For example:

full attention: 6-token hit, 2 physical blocks
Mamba:          4-token hit, 1 physical block
------------------------------------------------
coordinator:    4-token hit

Full-attention results are downward-closed, so an existing result can be trimmed without another lookup. Physical block lists are trimmed with ceiling division so the block containing a partial tail is retained.

EAGLE also uses the explicit token length. In fine-grained mode it drops one match unit, rather than one physical block, before the result is returned.

The explicit-length contract is also propagated to the Mooncake store coordinator. Its block-pool adapter exposes hash_block_size so it can reuse the same manager lookup interface.

Creating partial tail entries

Full attention registers the final hash-aligned prompt boundary once the containing physical block is available.

Aligned Mamba requires the recurrent state from exactly that boundary. The scheduler therefore stops once at the final match-unit boundary before the end of a partial prompt block. MambaManager registers that state only when the step lands on this boundary; it does not retain every intermediate state inside the physical block.

When execution resumes from a partial hit, the first chunk stops at the next physical block boundary. Subsequent chunks return to the existing block-aligned scheduling path.

Copy-on-write

A request cannot append to a physical block while the prefix cache exposes an entry for an earlier boundary in that block. This PR uses copy-on-write, with two allocation paths.

For a new request hitting a partial entry, the request is redirected to a private block:

request table: shared source -> private destination
copy:          shared source -> private destination

The shared cached block remains unchanged and the request continues from the private copy.

An already-running aligned-Mamba request may register a partial entry for its current state and then continue in the next step. Its worker block table is append-only, so that request must remain on the original block. Instead, the cache entry is moved to a new block:

cache hashes: original request block -> cache-owned destination
copy:         original request block -> cache-owned destination
request:      continues writing the original block

BlockPool.move_block_hashes() re-points all hashes without emitting a remove/store event. The destination is excluded from same-step lookup until the copy has populated it.

Copy execution and lifetime

Managers queue (source, destination) block pairs. KVCacheManager converts them to block-ID copies on SchedulerOutput, and workers execute them after zeroing newly allocated blocks and before model forward:

zero new blocks -> copy partial blocks -> model forward

The worker copies complete block-major storage pages. Storage aliases across layer views are deduplicated, so each backing allocation is copied once.

Both copy endpoints are retained until the non-empty scheduler step carrying the copy has completed. Their release uses the existing deferred-free queue, preventing either block from being recycled while asynchronous execution is still reading or writing it.


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.

Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
@ZJY0516 ZJY0516 changed the title [1/N][Core] support partial prefix cache hits in hybrid coordinator [2/N][Core] support partial prefix cache hits in hybrid coordinator Jun 22, 2026
@mergify mergify Bot added the v1 label Jun 22, 2026

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 55de9c44e1

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vllm/v1/core/single_type_kv_cache_manager.py
ZJY0516 added 3 commits June 22, 2026 17:21
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
(cherry picked from commit 425b969)
@ZJY0516
ZJY0516 requested a review from yewentao256 as a code owner June 25, 2026 08:00

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7327f58bea

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vllm/v1/worker/utils.py
Comment thread vllm/v1/worker/utils.py
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cb2b1592cd

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vllm/v1/core/single_type_kv_cache_manager.py Outdated
Comment thread vllm/v1/core/single_type_kv_cache_manager.py Outdated
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
@ZJY0516
ZJY0516 requested a review from ivanium as a code owner July 1, 2026 03:10
ZJY0516 added 5 commits July 1, 2026 03:19
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
MengqingCao pushed a commit to vllm-project/vllm-ascend that referenced this pull request Jul 24, 2026
### What this PR does / why we need it?

Adapt vllm-ascend to vLLM main commits up to July 17.

### Changes

| Files | Upstream vLLM change | vllm-ascend adaptation |
|-------|---------------------|------------------------|
|
`vllm_ascend/_310p/ops/fla/idex.py`<br>`vllm_ascend/_310p/ops/fla/l2norm.py`<br>`vllm_ascend/ops/bailing_moe_linear_attn.py`<br>`vllm_ascend/ops/gdn.py`<br>`vllm_ascend/ops/triton/fla/chunk.py`<br>`vllm_ascend/patch/worker/patch_idex_310.py`<br>`vllm_ascend/patch/worker/patch_triton.py`<br>`tests/e2e/nightly/.../test_fused_recurrent_gated_delta_rule.py`<br>`tests/e2e/nightly/.../test_fused_sigmoid_gating_delta_rule.py`<br>`tests/ut/ops/test_gdn_attn_builder.py`
| [vllm#48500](vllm-project/vllm#48500)
relocated flash-linear-attention from
`vllm.third_party.flash_linear_attention` to
`vllm.model_executor.layers.fla` | Version-gated all FLA imports and
monkey-patch sites with `vllm_version_is("0.25.1")` |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/config_data.py`
| [vllm#45939](vllm-project/vllm#45939) replaced
SHA-256 rehashed grouped block hashes with chained fine-grained hashes
(terminal hash identifies complete block) | Removed
`_rehash_block_hash_group` and associated constants. `get_block_hashes`
returns `block_hashes[idx + scale_factor - 1]` instead of computing
compound hash. |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/coordinator.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_scheduler.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) added
`hash_block_size` to block pool API;
[vllm#46384](vllm-project/vllm#46384) renamed
`prefix_match_unit` -> `hash_block_size`, moved hash resolution into
individual managers | `ExternalCachedBlockPool` takes `hash_block_size`.
`_find_longest_cache_hit` return version-gated. `block_hashes_for_spec`
no-op on main. `prefix_match_unit` fallback on main. |
|
`vllm_ascend/core/recompute_scheduler.py`<br>`vllm_ascend/core/scheduler_profiling_chunk.py`<br>`vllm_ascend/core/single_type_kv_cache_manager.py`
| [vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
`get_computed_blocks` 3-tuple -> 2-tuple, `get_num_blocks_to_allocate`
params, `find_longest_cache_hit` return | Version-gated unpacking with
`cast`. `num_tokens_main_model` made optional. `find_longest_cache_hit`
returns blocks-only on v0.25.1 vs `(blocks, hit_length)` on main. |
| `vllm_ascend/patch/platform/patch_kv_cache_coordinator.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782)
restructured return contracts; added partial Mamba hash hit support on
main | Added `enable_partial_hash_hits` (main-only). Added
`_cache_hit_alignment_tokens`. `find_longest_cache_hit` tracks per-group
lengths, uses `cdiv`. `find_longest_cache_hit_per_group` simplified to
single-pass; returns per-group `(blocks, lengths)`. Fixed v0.25.1
`hit_length` over-counting: use physical `spec.block_size` (not
`_get_effective_block_size()` which includes `compress_ratio`) when
multiplying by physical block count — affected both
`find_longest_cache_hit` and `find_longest_cache_hit_per_group`; without
this, MLA models (DS-V2-Lite/V3/V4) got 4x inflated hit length ->
scheduler skipped non-cached tokens -> segfault. |
| `vllm_ascend/patch/platform/patch_mamba_manager.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
method signatures | Version-gated `find_longest_cache_hit` (delegates to
super on main) and `get_num_blocks_to_allocate` (optional params). |
| `vllm_ascend/patch/worker/patch_qwen3_5.py` |
[vllm#47006](vllm-project/vllm#47006) changed
Qwen3-Next sequence-parallel gather contracts | Added
`_ascend_all_gather_hidden_and_residual` (main-only). Gated
`Qwen3_5DecoderLayer.forward` to v0.25.1. |
| `vllm_ascend/ops/vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) changed LM
head apply contract | Added `_apply_head` routing to
`quant_method.apply` on v0.25.1 and `super()._apply_head` on main. |
|
`vllm_ascend/patch/worker/__init__.py`<br>`vllm_ascend/patch/worker/patch_v2/patch_eagle_speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/aclgraph.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dflash/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dspark/speculator.py`
| [vllm#48261](vllm-project/vllm#48261) /
[vllm#48167](vllm-project/vllm#48167) unified
`PrefillSpeculatorCudaGraphManager` + `DecodeSpeculatorCudaGraphManager`
-> `SpeculatorCudaGraphManager`; `capture()` parameterless; `set_attn`
new params; `dflash_causal` -> `_group_causal` | Gated MRV2 spec decode
imports to main-only. Consolidated Eagle managers into
`EagleAclGraphManager`. Simplified DFlash/DSpark `capture()`. Added
`target_input_buffers`/`target_attn_groups` to `set_attn`. Renamed
`dflash_causal` -> `_group_causal` in DSpark
`build_draft_attn_metadatas` (DFlash was already updated; DSpark was
missed). |
|
`tests/ut/ops/test_gdn_layerwise_kv.py`<br>`tests/ut/ops/a2/test_gdn_layerwise_kv.py`
| [vllm#46998](vllm-project/vllm#46998) changed
GDN `forward` from `(hidden_states, output)` -> `(hidden_states) ->
Tensor` | Added `_run_gdn_forward` wrapper handling both calling
conventions. |
| `tests/e2e/conftest.py` |
[vllm#48549](vllm-project/vllm#48549) removed
`swap_space` from `LLM` | Removed from `VllmRunner` and `DPVllmRunner`.
|
| `tests/e2e/pull_request/one_card/model_runner_v2/test_basic.py` |
[vllm#48261](vllm-project/vllm#48261) MRV2 spec
decode main-only | Added `_SKIP_V025_MRV2_SPEC_DECODE` skip marker. |
| `tests/ut/core/test_recompute_scheduler.py` | Upstream main's
`_free_request` accesses `self.ec_connector` (new attribute) | Added
`scheduler.ec_connector = None` to test setup — scheduler is constructed
via `__new__` (bypasses `__init__`), so the attribute must be mocked
explicitly. |
|
`tests/ut/distributed/ascend_store/test_config_data.py`<br>`tests/ut/distributed/ascend_store/test_coordinator.py`<br>`tests/ut/distributed/ascend_store/test_pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) /
[vllm#46384](vllm-project/vllm#46384) | Updated
block hash assertions to terminal hash. Added `hash_block_size`.
Version-gated mock returns. |
| `tests/ut/ops/test_vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) | Replaced
`patch()` with context manager for `set_current_vllm_config`. |
| `tests/ut/patch/platform/test_prefix_cache_cp_patches.py` |
[vllm#47782](vllm-project/vllm#47782) | Removed
obsolete `num_prompt_tokens` kwarg. |
| `tests/ut/patch/worker/test_patch_qwen3_5_mtp.py` |
[vllm#48429](vllm-project/vllm#48429) | Set
`use_attn_reduce_scatter_for_moe = False` on mock layer. |
| `tests/ut/test_compressed_prefix_cache.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) |
Version-gated unpacking. Added `test_find_longest_cache_hit_per_group`.
|
| `vllm_ascend/patch/__init__.py` | - | Updated HunyuanVL comment block.
|

- vLLM version: v0.25.1
- vLLM main:
vllm-project/vllm@54503ec
---------
Signed-off-by: wjunLu <wjunlu217@gmail.com>
Signed-off-by: hfadzxy <starmoon_zhang@163.com>
Co-authored-by: hfadzxy <starmoon_zhang@163.com>
alexbi29 added a commit to alexbi29/vllm that referenced this pull request Jul 26, 2026
`find_longest_cache_hit` returns `(computed_blocks, hit_length)` since vllm-project#46384
("[2/N][Core] support partial prefix cache hit for hybrid model"), where
`computed_blocks` is a tuple with one block list per KV cache group.

`test_mamba_possible_cached_prefix_with_eagle_drop` indexed only `[0]`, so it
asserted against the tuple of groups rather than group 0's block list, and
failed with `assert 1 == 2`. The other tests in this file already use `[0][0]`.

Test-only change; the MambaManager logic is unaffected and its assertions
(null_block at index 0, cached block 11 at index 1) pass once the correct list
is selected.
alexbi29 added a commit to alexbi29/vllm that referenced this pull request Jul 26, 2026
`find_longest_cache_hit` returns `(computed_blocks, hit_length)` since vllm-project#46384
("[2/N][Core] support partial prefix cache hit for hybrid model"), where
`computed_blocks` is a tuple with one block list per KV cache group.

`test_mamba_possible_cached_prefix_with_eagle_drop` indexed only `[0]`, so it
asserted against the tuple of groups rather than group 0's block list, and
failed with `assert 1 == 2`. The other tests in this file already use `[0][0]`.

Test-only change; the MambaManager logic is unaffected and its assertions
(null_block at index 0, cached block 11 at index 1) pass once the correct list
is selected.
alexbi29 added a commit to alexbi29/vllm that referenced this pull request Jul 26, 2026
`find_longest_cache_hit` returns `(computed_blocks, hit_length)` since vllm-project#46384
("[2/N][Core] support partial prefix cache hit for hybrid model"), where
`computed_blocks` is a tuple with one block list per KV cache group.

`test_mamba_possible_cached_prefix_with_eagle_drop` indexed only `[0]`, so it
asserted against the tuple of groups rather than group 0's block list, and
failed with `assert 1 == 2`. The other tests in this file already use `[0][0]`.

Test-only change; the MambaManager logic is unaffected and its assertions
(null_block at index 0, cached block 11 at index 1) pass once the correct list
is selected.
alexbi29 added a commit to alexbi29/vllm that referenced this pull request Jul 26, 2026
`find_longest_cache_hit` returns `(computed_blocks, hit_length)` since vllm-project#46384
("[2/N][Core] support partial prefix cache hit for hybrid model"), where
`computed_blocks` is a tuple with one block list per KV cache group.

`test_mamba_possible_cached_prefix_with_eagle_drop` indexed only `[0]`, so it
asserted against the tuple of groups rather than group 0's block list, and
failed with `assert 1 == 2`. The other tests in this file already use `[0][0]`.

Test-only change; the MambaManager logic is unaffected and its assertions
(null_block at index 0, cached block 11 at index 1) pass once the correct list
is selected.
alexbi29 added a commit to alexbi29/vllm that referenced this pull request Jul 26, 2026
`find_longest_cache_hit` returns `(computed_blocks, hit_length)` since vllm-project#46384
("[2/N][Core] support partial prefix cache hit for hybrid model"), where
`computed_blocks` is a tuple with one block list per KV cache group.

`test_mamba_possible_cached_prefix_with_eagle_drop` indexed only `[0]`, so it
asserted against the tuple of groups rather than group 0's block list, and
failed with `assert 1 == 2`. The other tests in this file already use `[0][0]`.

Test-only change; the MambaManager logic is unaffected and its assertions
(null_block at index 0, cached block 11 at index 1) pass once the correct list
is selected.
aditi-amd pushed a commit to aditi-amd/vllm that referenced this pull request Aug 4, 2026
…roject#46384)

Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
Signed-off-by: root <root@smci355-ccs-aus-m02-09.cs-aus.dcgpu>
@Sunshine-llh

Copy link
Copy Markdown

Purpose

This PR builds on the partial prefix-cache primitives from #45939 and completes fine-grained prefix-cache hits for aligned full-attention + Mamba hybrid models. The overall design is described in RFC #45702.

The PR adds:

  • a public prefix_match_unit that selects prefix-match granularity independently from physical KV-cache block sizes;
  • fine-grained lookup in the full-attention and Mamba cache managers;
  • token-accurate convergence in the hybrid coordinator;
  • scheduler boundaries needed to materialize and resume partial Mamba states;
  • copy-on-write for requests that continue from a shared partial block;
  • worker-side block copies with scheduler-side lifetime fencing.

It e2e works now

vllm serve Qwen/Qwen3.5-35B-A3B-FP8 --enable-prefix-caching --mamba-cache-mode align --prefix-match-unit 16

Use 2 sequential request to simulate multi-turn:

  • 10000 in, 1 out
  • 10000 + 200 in, 1 out
w/ partial cache hit
"first_latency_sec": 0.3047793720033951, "second_latency_sec": 0.09551339800236747

w/o partial cache hit
"first_latency_sec": 0.2865299719851464, "second_latency_sec": 0.13373498094733804

Design

Match unit and cache-manager hash views

Request.block_hashes is computed at prefix_match_unit granularity. Each KV-cache group continues to allocate its existing physical block size.

For example:

prefix_match_unit          = 2
full-attention block_size  = 4
Mamba block_size           = 8

request hashes: H2, H4, H6, H8, H10, ...

Managers using physical-block lookup receive a grouped view such as [H4, H8, ...]. Managers supporting fine-grained lookup keep the original hash list and may probe H2 or H6 inside a physical block.

The configured match unit must divide every cache-group block size. If it is not specified, vLLM derives it from the GCD of the resolved group block sizes.

Explicit token-length cache hits

Cache-manager lookup now returns both the physical blocks and the exact number of matched tokens:

(blocks_by_group, hit_length)

The separate length is required because a hit at H6 includes the physical block [4, 8), but only tokens [4, 6) in that block are valid.

FullAttentionManager first matches complete physical blocks from the start of the request, then probes hash boundaries inside the next block from longest to shortest. MambaManager keeps its existing right-to-left state lookup, but can now search at match-unit boundaries and return the physical block containing the matched recurrent state.

Managers without fine-grained support continue to return block-aligned hit lengths through the same API.

Hybrid-coordinator convergence

The hybrid coordinator tracks a token hit length independently from each group's block list. It repeatedly reduces the candidate length until every cache group can cover the same logical prefix.

For example:

full attention: 6-token hit, 2 physical blocks
Mamba:          4-token hit, 1 physical block
------------------------------------------------
coordinator:    4-token hit

Full-attention results are downward-closed, so an existing result can be trimmed without another lookup. Physical block lists are trimmed with ceiling division so the block containing a partial tail is retained.

EAGLE also uses the explicit token length. In fine-grained mode it drops one match unit, rather than one physical block, before the result is returned.

The explicit-length contract is also propagated to the Mooncake store coordinator. Its block-pool adapter exposes hash_block_size so it can reuse the same manager lookup interface.

Creating partial tail entries

Full attention registers the final hash-aligned prompt boundary once the containing physical block is available.

Aligned Mamba requires the recurrent state from exactly that boundary. The scheduler therefore stops once at the final match-unit boundary before the end of a partial prompt block. MambaManager registers that state only when the step lands on this boundary; it does not retain every intermediate state inside the physical block.

When execution resumes from a partial hit, the first chunk stops at the next physical block boundary. Subsequent chunks return to the existing block-aligned scheduling path.

Copy-on-write

A request cannot append to a physical block while the prefix cache exposes an entry for an earlier boundary in that block. This PR uses copy-on-write, with two allocation paths.

For a new request hitting a partial entry, the request is redirected to a private block:

request table: shared source -> private destination
copy:          shared source -> private destination

The shared cached block remains unchanged and the request continues from the private copy.

An already-running aligned-Mamba request may register a partial entry for its current state and then continue in the next step. Its worker block table is append-only, so that request must remain on the original block. Instead, the cache entry is moved to a new block:

cache hashes: original request block -> cache-owned destination
copy:         original request block -> cache-owned destination
request:      continues writing the original block

BlockPool.move_block_hashes() re-points all hashes without emitting a remove/store event. The destination is excluded from same-step lookup until the copy has populated it.

Copy execution and lifetime

Managers queue (source, destination) block pairs. KVCacheManager converts them to block-ID copies on SchedulerOutput, and workers execute them after zeroing newly allocated blocks and before model forward:

zero new blocks -> copy partial blocks -> model forward

The worker copies complete block-major storage pages. Storage aliases across layer views are deduplicated, so each backing allocation is copied once.

Both copy endpoints are retained until the non-empty scheduler step carrying the copy has completed. Their release uses the existing deferred-free queue, preventing either block from being recycled while asynchronous execution is still reading or writing it.

Essential Elements of an Effective PR Description Checklist

Thanks again for the partial-prefix-cache work. We integrated the approach and confirmed that it enables partial cache reuse for aligned full-attention + Mamba hybrid models.

However, we still observe a relatively low prefix-cache hit rate on our real workload.

Our understanding is that each request retains the final prefix_match_unit-aligned boundary of its prompt, rather than retaining every intermediate Mamba checkpoint inside the
physical Mamba block. For example, with:

  • prefix_match_unit = 16
  • a request prompt length of 1946

the cache can retain a checkpoint around token 1936, but not necessarily checkpoints such as H560.

This is problematic for our workload because many requests share a common system/context prefix near the beginning, then diverge with different user content:

Request A: [shared prefix: 560 tokens] + [unique content A]  -> total 1946 tokens
Request B: [shared prefix: 560 tokens] + [unique content B]

Even though the first 560 tokens are identical, Request B may not be able to reuse the prefix cached by Request A because the cached partial Mamba state is associated with the
final aligned boundary (H1936), not the internal shared boundary (H560).

We understand that materializing a Mamba state at every prefix_match_unit boundary would have significant memory, copy, and metadata overhead. Still, do you see room for further
optimization here? For example:

- retaining multiple selected intermediate checkpoints within a physical Mamba block;
- adaptively materializing checkpoints at frequently reused prefix boundaries;
- allowing applications to mark known reusable boundaries, such as system prompts or document-prefix boundaries;
- using a coarser checkpoint interval than prefix_match_unit, while still providing better coverage than only the final prompt boundary.

Any guidance on the intended next optimization direction would be very helpful. Thank you again for the work on this PR; it already solves an important limitation for partial
prefix reuse in hybrid Mamba models.

puririshi98 pushed a commit to puririshi98/vllm that referenced this pull request Aug 15, 2026
…roject#46384)

Signed-off-by: zjy0516 <riverclouds.zhu@qq.com>
Signed-off-by: Yifan Qiao <yifanqiao@inferact.ai>
Co-authored-by: Yifan Qiao <yifanqiao@inferact.ai>
(cherry picked from commit 481e481)
(cherry picked from commit a0ad76a)
MmMmaru pushed a commit to jiaqi-lee/vllm-ascend that referenced this pull request Aug 19, 2026
### What this PR does / why we need it?

Adapt vllm-ascend to vLLM main commits up to July 17.

### Changes

| Files | Upstream vLLM change | vllm-ascend adaptation |
|-------|---------------------|------------------------|
|
`vllm_ascend/_310p/ops/fla/idex.py`<br>`vllm_ascend/_310p/ops/fla/l2norm.py`<br>`vllm_ascend/ops/bailing_moe_linear_attn.py`<br>`vllm_ascend/ops/gdn.py`<br>`vllm_ascend/ops/triton/fla/chunk.py`<br>`vllm_ascend/patch/worker/patch_idex_310.py`<br>`vllm_ascend/patch/worker/patch_triton.py`<br>`tests/e2e/nightly/.../test_fused_recurrent_gated_delta_rule.py`<br>`tests/e2e/nightly/.../test_fused_sigmoid_gating_delta_rule.py`<br>`tests/ut/ops/test_gdn_attn_builder.py`
| [vllm#48500](vllm-project/vllm#48500)
relocated flash-linear-attention from
`vllm.third_party.flash_linear_attention` to
`vllm.model_executor.layers.fla` | Version-gated all FLA imports and
monkey-patch sites with `vllm_version_is("0.25.1")` |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/config_data.py`
| [vllm#45939](vllm-project/vllm#45939) replaced
SHA-256 rehashed grouped block hashes with chained fine-grained hashes
(terminal hash identifies complete block) | Removed
`_rehash_block_hash_group` and associated constants. `get_block_hashes`
returns `block_hashes[idx + scale_factor - 1]` instead of computing
compound hash. |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/coordinator.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_scheduler.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) added
`hash_block_size` to block pool API;
[vllm#46384](vllm-project/vllm#46384) renamed
`prefix_match_unit` -> `hash_block_size`, moved hash resolution into
individual managers | `ExternalCachedBlockPool` takes `hash_block_size`.
`_find_longest_cache_hit` return version-gated. `block_hashes_for_spec`
no-op on main. `prefix_match_unit` fallback on main. |
|
`vllm_ascend/core/recompute_scheduler.py`<br>`vllm_ascend/core/scheduler_profiling_chunk.py`<br>`vllm_ascend/core/single_type_kv_cache_manager.py`
| [vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
`get_computed_blocks` 3-tuple -> 2-tuple, `get_num_blocks_to_allocate`
params, `find_longest_cache_hit` return | Version-gated unpacking with
`cast`. `num_tokens_main_model` made optional. `find_longest_cache_hit`
returns blocks-only on v0.25.1 vs `(blocks, hit_length)` on main. |
| `vllm_ascend/patch/platform/patch_kv_cache_coordinator.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782)
restructured return contracts; added partial Mamba hash hit support on
main | Added `enable_partial_hash_hits` (main-only). Added
`_cache_hit_alignment_tokens`. `find_longest_cache_hit` tracks per-group
lengths, uses `cdiv`. `find_longest_cache_hit_per_group` simplified to
single-pass; returns per-group `(blocks, lengths)`. Fixed v0.25.1
`hit_length` over-counting: use physical `spec.block_size` (not
`_get_effective_block_size()` which includes `compress_ratio`) when
multiplying by physical block count — affected both
`find_longest_cache_hit` and `find_longest_cache_hit_per_group`; without
this, MLA models (DS-V2-Lite/V3/V4) got 4x inflated hit length ->
scheduler skipped non-cached tokens -> segfault. |
| `vllm_ascend/patch/platform/patch_mamba_manager.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
method signatures | Version-gated `find_longest_cache_hit` (delegates to
super on main) and `get_num_blocks_to_allocate` (optional params). |
| `vllm_ascend/patch/worker/patch_qwen3_5.py` |
[vllm#47006](vllm-project/vllm#47006) changed
Qwen3-Next sequence-parallel gather contracts | Added
`_ascend_all_gather_hidden_and_residual` (main-only). Gated
`Qwen3_5DecoderLayer.forward` to v0.25.1. |
| `vllm_ascend/ops/vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) changed LM
head apply contract | Added `_apply_head` routing to
`quant_method.apply` on v0.25.1 and `super()._apply_head` on main. |
|
`vllm_ascend/patch/worker/__init__.py`<br>`vllm_ascend/patch/worker/patch_v2/patch_eagle_speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/aclgraph.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dflash/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dspark/speculator.py`
| [vllm#48261](vllm-project/vllm#48261) /
[vllm#48167](vllm-project/vllm#48167) unified
`PrefillSpeculatorCudaGraphManager` + `DecodeSpeculatorCudaGraphManager`
-> `SpeculatorCudaGraphManager`; `capture()` parameterless; `set_attn`
new params; `dflash_causal` -> `_group_causal` | Gated MRV2 spec decode
imports to main-only. Consolidated Eagle managers into
`EagleAclGraphManager`. Simplified DFlash/DSpark `capture()`. Added
`target_input_buffers`/`target_attn_groups` to `set_attn`. Renamed
`dflash_causal` -> `_group_causal` in DSpark
`build_draft_attn_metadatas` (DFlash was already updated; DSpark was
missed). |
|
`tests/ut/ops/test_gdn_layerwise_kv.py`<br>`tests/ut/ops/a2/test_gdn_layerwise_kv.py`
| [vllm#46998](vllm-project/vllm#46998) changed
GDN `forward` from `(hidden_states, output)` -> `(hidden_states) ->
Tensor` | Added `_run_gdn_forward` wrapper handling both calling
conventions. |
| `tests/e2e/conftest.py` |
[vllm#48549](vllm-project/vllm#48549) removed
`swap_space` from `LLM` | Removed from `VllmRunner` and `DPVllmRunner`.
|
| `tests/e2e/pull_request/one_card/model_runner_v2/test_basic.py` |
[vllm#48261](vllm-project/vllm#48261) MRV2 spec
decode main-only | Added `_SKIP_V025_MRV2_SPEC_DECODE` skip marker. |
| `tests/ut/core/test_recompute_scheduler.py` | Upstream main's
`_free_request` accesses `self.ec_connector` (new attribute) | Added
`scheduler.ec_connector = None` to test setup — scheduler is constructed
via `__new__` (bypasses `__init__`), so the attribute must be mocked
explicitly. |
|
`tests/ut/distributed/ascend_store/test_config_data.py`<br>`tests/ut/distributed/ascend_store/test_coordinator.py`<br>`tests/ut/distributed/ascend_store/test_pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) /
[vllm#46384](vllm-project/vllm#46384) | Updated
block hash assertions to terminal hash. Added `hash_block_size`.
Version-gated mock returns. |
| `tests/ut/ops/test_vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) | Replaced
`patch()` with context manager for `set_current_vllm_config`. |
| `tests/ut/patch/platform/test_prefix_cache_cp_patches.py` |
[vllm#47782](vllm-project/vllm#47782) | Removed
obsolete `num_prompt_tokens` kwarg. |
| `tests/ut/patch/worker/test_patch_qwen3_5_mtp.py` |
[vllm#48429](vllm-project/vllm#48429) | Set
`use_attn_reduce_scatter_for_moe = False` on mock layer. |
| `tests/ut/test_compressed_prefix_cache.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) |
Version-gated unpacking. Added `test_find_longest_cache_hit_per_group`.
|
| `vllm_ascend/patch/__init__.py` | - | Updated HunyuanVL comment block.
|

- vLLM version: v0.25.1
- vLLM main:
vllm-project/vllm@54503ec
---------
Signed-off-by: wjunLu <wjunlu217@gmail.com>
Signed-off-by: hfadzxy <starmoon_zhang@163.com>
Co-authored-by: hfadzxy <starmoon_zhang@163.com>
shiqiangA pushed a commit to shiqiangA/vllm-ascend that referenced this pull request Aug 20, 2026
### What this PR does / why we need it?

Adapt vllm-ascend to vLLM main commits up to July 17.

### Changes

| Files | Upstream vLLM change | vllm-ascend adaptation |
|-------|---------------------|------------------------|
|
`vllm_ascend/_310p/ops/fla/idex.py`<br>`vllm_ascend/_310p/ops/fla/l2norm.py`<br>`vllm_ascend/ops/bailing_moe_linear_attn.py`<br>`vllm_ascend/ops/gdn.py`<br>`vllm_ascend/ops/triton/fla/chunk.py`<br>`vllm_ascend/patch/worker/patch_idex_310.py`<br>`vllm_ascend/patch/worker/patch_triton.py`<br>`tests/e2e/nightly/.../test_fused_recurrent_gated_delta_rule.py`<br>`tests/e2e/nightly/.../test_fused_sigmoid_gating_delta_rule.py`<br>`tests/ut/ops/test_gdn_attn_builder.py`
| [vllm#48500](vllm-project/vllm#48500)
relocated flash-linear-attention from
`vllm.third_party.flash_linear_attention` to
`vllm.model_executor.layers.fla` | Version-gated all FLA imports and
monkey-patch sites with `vllm_version_is("0.25.1")` |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/config_data.py`
| [vllm#45939](vllm-project/vllm#45939) replaced
SHA-256 rehashed grouped block hashes with chained fine-grained hashes
(terminal hash identifies complete block) | Removed
`_rehash_block_hash_group` and associated constants. `get_block_hashes`
returns `block_hashes[idx + scale_factor - 1]` instead of computing
compound hash. |
|
`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/coordinator.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_scheduler.py`<br>`vllm_ascend/distributed/kv_transfer/kv_pool/ascend_store/pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) added
`hash_block_size` to block pool API;
[vllm#46384](vllm-project/vllm#46384) renamed
`prefix_match_unit` -> `hash_block_size`, moved hash resolution into
individual managers | `ExternalCachedBlockPool` takes `hash_block_size`.
`_find_longest_cache_hit` return version-gated. `block_hashes_for_spec`
no-op on main. `prefix_match_unit` fallback on main. |
|
`vllm_ascend/core/recompute_scheduler.py`<br>`vllm_ascend/core/scheduler_profiling_chunk.py`<br>`vllm_ascend/core/single_type_kv_cache_manager.py`
| [vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
`get_computed_blocks` 3-tuple -> 2-tuple, `get_num_blocks_to_allocate`
params, `find_longest_cache_hit` return | Version-gated unpacking with
`cast`. `num_tokens_main_model` made optional. `find_longest_cache_hit`
returns blocks-only on v0.25.1 vs `(blocks, hit_length)` on main. |
| `vllm_ascend/patch/platform/patch_kv_cache_coordinator.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782)
restructured return contracts; added partial Mamba hash hit support on
main | Added `enable_partial_hash_hits` (main-only). Added
`_cache_hit_alignment_tokens`. `find_longest_cache_hit` tracks per-group
lengths, uses `cdiv`. `find_longest_cache_hit_per_group` simplified to
single-pass; returns per-group `(blocks, lengths)`. Fixed v0.25.1
`hit_length` over-counting: use physical `spec.block_size` (not
`_get_effective_block_size()` which includes `compress_ratio`) when
multiplying by physical block count — affected both
`find_longest_cache_hit` and `find_longest_cache_hit_per_group`; without
this, MLA models (DS-V2-Lite/V3/V4) got 4x inflated hit length ->
scheduler skipped non-cached tokens -> segfault. |
| `vllm_ascend/patch/platform/patch_mamba_manager.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) changed
method signatures | Version-gated `find_longest_cache_hit` (delegates to
super on main) and `get_num_blocks_to_allocate` (optional params). |
| `vllm_ascend/patch/worker/patch_qwen3_5.py` |
[vllm#47006](vllm-project/vllm#47006) changed
Qwen3-Next sequence-parallel gather contracts | Added
`_ascend_all_gather_hidden_and_residual` (main-only). Gated
`Qwen3_5DecoderLayer.forward` to v0.25.1. |
| `vllm_ascend/ops/vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) changed LM
head apply contract | Added `_apply_head` routing to
`quant_method.apply` on v0.25.1 and `super()._apply_head` on main. |
|
`vllm_ascend/patch/worker/__init__.py`<br>`vllm_ascend/patch/worker/patch_v2/patch_eagle_speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/aclgraph.py`<br>`vllm_ascend/worker/v2/spec_decode/eagle/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dflash/speculator.py`<br>`vllm_ascend/worker/v2/spec_decode/dspark/speculator.py`
| [vllm#48261](vllm-project/vllm#48261) /
[vllm#48167](vllm-project/vllm#48167) unified
`PrefillSpeculatorCudaGraphManager` + `DecodeSpeculatorCudaGraphManager`
-> `SpeculatorCudaGraphManager`; `capture()` parameterless; `set_attn`
new params; `dflash_causal` -> `_group_causal` | Gated MRV2 spec decode
imports to main-only. Consolidated Eagle managers into
`EagleAclGraphManager`. Simplified DFlash/DSpark `capture()`. Added
`target_input_buffers`/`target_attn_groups` to `set_attn`. Renamed
`dflash_causal` -> `_group_causal` in DSpark
`build_draft_attn_metadatas` (DFlash was already updated; DSpark was
missed). |
|
`tests/ut/ops/test_gdn_layerwise_kv.py`<br>`tests/ut/ops/a2/test_gdn_layerwise_kv.py`
| [vllm#46998](vllm-project/vllm#46998) changed
GDN `forward` from `(hidden_states, output)` -> `(hidden_states) ->
Tensor` | Added `_run_gdn_forward` wrapper handling both calling
conventions. |
| `tests/e2e/conftest.py` |
[vllm#48549](vllm-project/vllm#48549) removed
`swap_space` from `LLM` | Removed from `VllmRunner` and `DPVllmRunner`.
|
| `tests/e2e/pull_request/one_card/model_runner_v2/test_basic.py` |
[vllm#48261](vllm-project/vllm#48261) MRV2 spec
decode main-only | Added `_SKIP_V025_MRV2_SPEC_DECODE` skip marker. |
| `tests/ut/core/test_recompute_scheduler.py` | Upstream main's
`_free_request` accesses `self.ec_connector` (new attribute) | Added
`scheduler.ec_connector = None` to test setup — scheduler is constructed
via `__new__` (bypasses `__init__`), so the attribute must be mocked
explicitly. |
|
`tests/ut/distributed/ascend_store/test_config_data.py`<br>`tests/ut/distributed/ascend_store/test_coordinator.py`<br>`tests/ut/distributed/ascend_store/test_pool_worker.py`
| [vllm#45939](vllm-project/vllm#45939) /
[vllm#46384](vllm-project/vllm#46384) | Updated
block hash assertions to terminal hash. Added `hash_block_size`.
Version-gated mock returns. |
| `tests/ut/ops/test_vocab_parallel_embedding.py` |
[vllm#48390](vllm-project/vllm#48390) | Replaced
`patch()` with context manager for `set_current_vllm_config`. |
| `tests/ut/patch/platform/test_prefix_cache_cp_patches.py` |
[vllm#47782](vllm-project/vllm#47782) | Removed
obsolete `num_prompt_tokens` kwarg. |
| `tests/ut/patch/worker/test_patch_qwen3_5_mtp.py` |
[vllm#48429](vllm-project/vllm#48429) | Set
`use_attn_reduce_scatter_for_moe = False` on mock layer. |
| `tests/ut/test_compressed_prefix_cache.py` |
[vllm#46384](vllm-project/vllm#46384) /
[vllm#47782](vllm-project/vllm#47782) |
Version-gated unpacking. Added `test_find_longest_cache_hit_per_group`.
|
| `vllm_ascend/patch/__init__.py` | - | Updated HunyuanVL comment block.
|

- vLLM version: v0.25.1
- vLLM main:
vllm-project/vllm@54503ec
---------
Signed-off-by: wjunLu <wjunlu217@gmail.com>
Signed-off-by: hfadzxy <starmoon_zhang@163.com>
Co-authored-by: hfadzxy <starmoon_zhang@163.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

kv-connector ready ONLY add when PR is ready to merge/full CI is needed v1

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants