A high-throughput and memory-efficient inference and serving engine for LLMs
[Hybrid] Marconi-style admission policy for hybrid cache (#37898)
Signed-off-by: Stanislaw Wozniak <stw@zurich.ibm.com>
Stan Wozniak committed
Jun 10, 2026 at 19:03 UTC
dc66e01a7040fdfa8287fbfbbf73f9ac46a1b679
3 files changed
+89
tests/v1/core/test_prefix_caching.py
+62
@@ -5,6 +5,7 @@
5
import copy
6
from collections.abc import Callable
7
from math import lcm
8
+from types import SimpleNamespace
9
10
import pytest
11
import torch
@@ -33,6 +34,7 @@ from vllm.v1.core.kv_cache_utils import (
34
init_none_hash,
35
make_block_hash_with_group_id,
36
)
37
+from vllm.v1.core.sched.scheduler import Scheduler
38
from vllm.v1.kv_cache_interface import (
39
FullAttentionSpec,
40
KVCacheConfig,
@@ -1023,6 +1025,66 @@ def test_prefill_hybrid_model_mamba_align():
1025
manager.free(req0)
1026
1027
1028
+def test_hybrid_cache_mamba_align_shared_prefix_detection():
1029
+ """Test shared prefix detection heuristic for mamba align cache mode
1030
+
1031
+ HybridKVCacheCoordinator returns num_uncached_common > 0 when a shared
1032
+ uncached prefix is detected. With mamba_align cache, _mamba_block_aligned_split
1033
+ enforces scheduling aligned with the common prefix.
1034
+ """
1035
+ block_size = 16
1036
+ manager = make_kv_cache_manager(
1037
+ _make_hybrid_kv_cache_config(block_size, 30, ["full", "mamba_align"]),
1038
+ max_model_len=8192,
1039
+ enable_caching=True,
1040
+ hash_block_size=block_size,
1041
+ )
1042
+ hash_fn = sha256
1043
+
1044
+ # Request: 3 blocks
1045
+ prefix = [i for i in range(3) for _ in range(block_size)]
1046
+ req_0 = make_request("0", prefix, block_size, hash_fn)
1047
+ computed_blocks, num_computed = manager.get_computed_blocks(req_0)
1048
+ num_uncached_common = manager.coordinator.num_uncached_common_prefix_tokens
1049
+ assert num_computed == 0 # nothing cached yet
1050
+ assert num_uncached_common == 0
1051
+ manager.allocate_slots(req_0, 3 * block_size, 0, computed_blocks)
1052
+
1053
+ # Request: 3 blocks (shared with above) + 7 different tokens
1054
+ req_1 = make_request("1", prefix + [100] * 7, block_size, hash_fn)
1055
+ computed_blocks, num_computed = manager.get_computed_blocks(req_1)
1056
+ num_uncached_common = manager.coordinator.num_uncached_common_prefix_tokens
1057
+ assert num_computed == 3 * block_size # we should observe a 3-block cache hit
1058
+ assert num_uncached_common == 0
1059
+ manager.allocate_slots(req_1, 7, 3 * block_size, computed_blocks)
1060
+
1061
+ # Request: 3 blocks, but only 2 blocks shared (replace the last token in 3rd block):
1062
+ req_2 = make_request("2", prefix[:-1] + [101], block_size, hash_fn)
1063
+ computed_blocks, num_computed = manager.get_computed_blocks(req_2)
1064
+ num_uncached_common = manager.coordinator.num_uncached_common_prefix_tokens
1065
+ assert num_computed == 0 # mamba_align doesn't cache intermediate blocks
1066
+ assert num_uncached_common == 2 * block_size # heuristic detects a shared prefix
1067
+
1068
+ # Next, validate scheduler logic for num_uncached_common_prefix_tokens > 0
1069
+ # Create minimal mock with just the needed attributes
1070
+ mock = SimpleNamespace(
1071
+ cache_config=SimpleNamespace(block_size=block_size), use_eagle=False
1072
+ )
1073
+ num_new_tokens_adjusted = Scheduler._mamba_block_aligned_split(
1074
+ self=mock,
1075
+ request=req_2,
1076
+ num_new_tokens=3 * block_size,
1077
+ num_uncached_common_prefix_tokens=num_uncached_common,
1078
+ )
1079
+ assert num_new_tokens_adjusted == 2 * block_size # adjust to the common prefix
1080
+
1081
+ manager.allocate_slots(req_2, 3 * block_size, 0, computed_blocks)
1082
+ # Cleanup
1083
+ manager.free(req_0)
1084
+ manager.free(req_1)
1085
+ manager.free(req_2)
1086
+
1087
+
1088
def test_hybrid_model_mamba_align_with_dynamic_draft_tokens():
1089
"""Regression test for https://github.com/vllm-project/vllm/issues/39271.
1090
vllm/v1/core/kv_cache_coordinator.py
+6
@@ -611,6 +611,7 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
611
612
num_groups = len(self.kv_cache_config.kv_cache_groups)
613
hit_length = max_cache_hit_length
614
+ longest_hit_length = 0
615
hit_blocks_by_group: list[list[KVCacheBlock] | None] = [None] * num_groups
616
617
# Simple hybrid (1 full attn + 1 other): one iteration suffices.
@@ -667,6 +668,8 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
668
for group_id, blocks in zip(group_ids, hit_blocks):
669
hit_blocks_by_group[group_id] = blocks
670
671
+ longest_hit_length = max(longest_hit_length, curr_hit_length)
672
+
673
if curr_hit_length >= hit_length:
674
break
675
hit_length = curr_hit_length
@@ -681,6 +684,9 @@ class HybridKVCacheCoordinator(KVCacheCoordinator):
684
if (blks := hit_blocks_by_group[group_id]) is not None:
685
del blks[num_blocks:]
686
687
+ # Uncached shared prefix detection: If any attn. group cached a longer prefix
688
+ # than the current prefix, it is an uncached common prefix across requests:
689
+ self.num_uncached_common_prefix_tokens = longest_hit_length - hit_length
690
return tuple(
691
blocks if blocks is not None else [] for blocks in hit_blocks_by_group
692
), hit_length
vllm/v1/core/sched/scheduler.py
+21
@@ -296,6 +296,7 @@ class Scheduler(SchedulerInterface):
296
num_new_tokens: int,
297
num_new_local_computed_tokens: int = 0,
298
num_external_computed_tokens: int = 0,
299
+ num_uncached_common_prefix_tokens: int = 0,
300
) -> int:
301
num_computed_tokens = (
302
request.num_computed_tokens
@@ -335,6 +336,16 @@ class Scheduler(SchedulerInterface):
336
else:
337
# prefill the last few tokens
338
pass
339
+
340
+ # Marconi cache admission optimization:
341
+ # cache common prefixes by scheduling num_new_tokens = common prefix length
342
+ if (
343
+ num_uncached_common_prefix_tokens >= block_size
344
+ and num_new_tokens > num_uncached_common_prefix_tokens
345
+ ):
346
+ num_new_tokens = num_uncached_common_prefix_tokens
347
+ # keep alignment to block_size
348
+ num_new_tokens = num_new_tokens // block_size * block_size
349
return num_new_tokens
350
351
def schedule(self) -> SchedulerOutput:
@@ -604,6 +615,7 @@ class Scheduler(SchedulerInterface):
615
num_external_computed_tokens = 0
616
load_kv_async = False
617
connector_prefix_cache_queries, connector_prefix_cache_hits = 0, 0
618
+ num_uncached_common_prefix_tokens = 0
619
620
# Get already-cached tokens.
621
if request.num_computed_tokens == 0:
@@ -612,6 +624,14 @@ class Scheduler(SchedulerInterface):
624
self.kv_cache_manager.get_computed_blocks(request)
625
)
626
627
+ # In case of hybrid models, obtain hint for Marconi-style APC logic
628
+ if self.has_mamba_layers:
629
+ num_uncached_common_prefix_tokens = getattr(
630
+ self.kv_cache_manager.coordinator,
631
+ "num_uncached_common_prefix_tokens",
632
+ 0,
633
+ )
634
+
635
# Get externally-cached tokens if using a KVConnector.
636
if self.connector is not None:
637
ext_tokens, load_kv_async = (
@@ -724,6 +744,7 @@ class Scheduler(SchedulerInterface):
744
num_new_tokens,
745
num_new_local_computed_tokens,
746
num_external_computed_tokens,
747
+ num_uncached_common_prefix_tokens,
748
)
749
if num_new_tokens == 0:
750
break