Skip to content

Commit 0e7cb91

Browse files
committed
Fix hybrid DCP prefix cache geometry
1 parent e400a5b commit 0e7cb91

5 files changed

Lines changed: 143 additions & 15 deletions

File tree

tests/v1/core/test_dcp_mamba_chunk_alignment.py

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@
55

66
import pytest
77

8+
from vllm.v1.core.kv_cache_utils import resolve_kv_cache_block_sizes
9+
from vllm.v1.kv_cache_interface import (
10+
FullAttentionSpec,
11+
KVCacheConfig,
12+
KVCacheGroupSpec,
13+
MambaSpec,
14+
)
15+
816

917
def test_mamba_align_chunk_split_uses_dcp_effective_block_size():
1018
pytest.importorskip("torch")
@@ -29,7 +37,8 @@ def test_mamba_align_chunk_split_uses_dcp_effective_block_size():
2937
)
3038

3139
assert adjusted == 96
32-
assert adjusted % (scheduler.cache_config.block_size * scheduler.dcp_world_size) == 0
40+
effective_block_size = scheduler.cache_config.block_size * scheduler.dcp_world_size
41+
assert adjusted % effective_block_size == 0
3342

3443

3544
def test_mamba_align_split_keeps_small_chunks_when_dcp_alignment_exceeds_budget():
@@ -55,3 +64,51 @@ def test_mamba_align_split_keeps_small_chunks_when_dcp_alignment_exceeds_budget(
5564
)
5665

5766
assert adjusted == 256
67+
68+
69+
def test_hybrid_dcp_resolves_global_scheduler_block_and_fine_hash_block():
70+
torch = pytest.importorskip("torch")
71+
block_size = 16
72+
kv_cache_config = KVCacheConfig(
73+
num_blocks=32,
74+
kv_cache_tensors=[],
75+
kv_cache_groups=[
76+
KVCacheGroupSpec(
77+
["full"],
78+
FullAttentionSpec(
79+
block_size=block_size,
80+
num_kv_heads=1,
81+
head_size=1,
82+
dtype=torch.float32,
83+
),
84+
),
85+
KVCacheGroupSpec(
86+
["mamba"],
87+
MambaSpec(
88+
block_size=block_size,
89+
shapes=((1, 1),),
90+
dtypes=(torch.float32,),
91+
mamba_cache_mode="align",
92+
),
93+
),
94+
],
95+
)
96+
vllm_config = SimpleNamespace(
97+
cache_config=SimpleNamespace(
98+
block_size=block_size,
99+
enable_prefix_caching=True,
100+
hash_block_size=None,
101+
),
102+
parallel_config=SimpleNamespace(
103+
decode_context_parallel_size=3,
104+
prefill_context_parallel_size=1,
105+
),
106+
kv_transfer_config=None,
107+
)
108+
109+
scheduler_block_size, hash_block_size = resolve_kv_cache_block_sizes(
110+
kv_cache_config, vllm_config
111+
)
112+
113+
assert scheduler_block_size == block_size * 3
114+
assert hash_block_size == block_size

tests/v1/core/test_prefix_caching.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,6 +1024,63 @@ def test_prefill_hybrid_model_mamba_align():
10241024
manager.free(req0)
10251025

10261026

1027+
def test_prefill_hybrid_model_mamba_align_dcp_replay():
1028+
"""DCP hybrid cache insertion and lookup must use the same block geometry."""
1029+
block_size = 16
1030+
dcp_world_size = 3
1031+
effective_block_size = block_size * dcp_world_size
1032+
kv_cache_config = _make_hybrid_kv_cache_config(
1033+
block_size, 40, ["full", "mamba_align"]
1034+
)
1035+
manager = make_kv_cache_manager(
1036+
kv_cache_config,
1037+
max_model_len=8192,
1038+
enable_caching=True,
1039+
hash_block_size=block_size,
1040+
scheduler_block_size=effective_block_size,
1041+
dcp_world_size=dcp_world_size,
1042+
)
1043+
1044+
common_token_ids = list(range(3 * effective_block_size))
1045+
req0 = make_request("dcp-fill", common_token_ids, block_size, sha256)
1046+
computed_blocks, num_computed_tokens = manager.get_computed_blocks(req0)
1047+
assert num_computed_tokens == 0
1048+
blocks = manager.allocate_slots(
1049+
req0, req0.num_tokens, num_computed_tokens, computed_blocks
1050+
)
1051+
assert blocks is not None
1052+
manager.new_step_starts()
1053+
1054+
coarse_hashes = kv_cache_utils.BlockHashListWithBlockSize(
1055+
req0.block_hashes, block_size, effective_block_size
1056+
)
1057+
cached_by_group = [
1058+
[
1059+
manager.block_pool.get_cached_block(block_hash, [group_id]) is not None
1060+
for block_hash in coarse_hashes
1061+
]
1062+
for group_id in range(2)
1063+
]
1064+
assert cached_by_group == [[True] * 3, [False, False, True]]
1065+
1066+
req1 = make_request("dcp-replay", common_token_ids + [101] * 5, block_size, sha256)
1067+
computed_blocks, num_computed_tokens = manager.get_computed_blocks(req1)
1068+
1069+
assert num_computed_tokens == 3 * effective_block_size
1070+
assert all(len(group) == 3 for group in computed_blocks.blocks)
1071+
1072+
per_group_blocks, per_group_hit_lengths = (
1073+
manager.coordinator.find_longest_cache_hit_per_group(
1074+
req1.block_hashes, req1.num_tokens - 1
1075+
)
1076+
)
1077+
assert per_group_hit_lengths == (3 * effective_block_size,) * 2
1078+
assert all(len(group) == 3 for group in per_group_blocks)
1079+
1080+
manager.free(req0)
1081+
manager.free(req1)
1082+
1083+
10271084
def test_hybrid_cache_mamba_align_shared_prefix_detection():
10281085
"""Test shared prefix detection heuristic for mamba align cache mode
10291086

vllm/v1/core/kv_cache_coordinator.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,8 @@ def __init__(
563563
f"share the same block size, got {group_block_sizes}."
564564
)
565565
assert pcp_world_size == 1, "PCP not support hybrid attn now."
566+
self.dcp_world_size = dcp_world_size
567+
self.pcp_world_size = pcp_world_size
566568
self.verify_and_split_kv_cache_groups()
567569

568570
def verify_and_split_kv_cache_groups(self) -> None:
@@ -658,11 +660,15 @@ def find_longest_cache_hit(
658660
- The number of tokens of the longest cache hit.
659661
"""
660662

663+
def _effective_block_size(kv_cache_spec: KVCacheSpec) -> int:
664+
return kv_cache_spec.block_size * self.dcp_world_size * self.pcp_world_size
665+
661666
def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
662-
if kv_cache_spec.block_size == self.hash_block_size:
667+
effective_block_size = _effective_block_size(kv_cache_spec)
668+
if effective_block_size == self.hash_block_size:
663669
return block_hashes
664670
return BlockHashListWithBlockSize(
665-
block_hashes, self.hash_block_size, kv_cache_spec.block_size
671+
block_hashes, self.hash_block_size, effective_block_size
666672
)
667673

668674
num_groups = len(self.kv_cache_config.kv_cache_groups)
@@ -687,13 +693,14 @@ def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
687693
for idx, (spec, group_ids, manager_cls, use_eagle) in enumerate(
688694
self.attention_groups
689695
):
696+
effective_block_size = _effective_block_size(spec)
690697
cached_blocks = hit_blocks_by_group[group_ids[0]]
691698
if isinstance(spec, FullAttentionSpec) and cached_blocks is not None:
692699
# Full attention is downward-closed: we only need to look
693700
# up cached blocks once; on subsequent iterations just trim
694701
# to the (reduced) current hit length.
695702
curr_hit_length = (
696-
curr_hit_length // spec.block_size * spec.block_size
703+
curr_hit_length // effective_block_size * effective_block_size
697704
)
698705
continue
699706

@@ -703,7 +710,8 @@ def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
703710
if drop_eagle_block:
704711
# Eagle needs to match one more block and then pop the last.
705712
_max_length = min(
706-
curr_hit_length + spec.block_size, max_cache_hit_length
713+
curr_hit_length + effective_block_size,
714+
max_cache_hit_length,
707715
)
708716
hit_blocks = manager_cls.find_longest_cache_hit(
709717
block_hashes=_get_block_hashes(spec),
@@ -713,8 +721,10 @@ def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
713721
kv_cache_spec=spec,
714722
drop_eagle_block=drop_eagle_block,
715723
alignment_tokens=self.scheduler_block_size,
724+
dcp_world_size=self.dcp_world_size,
725+
pcp_world_size=self.pcp_world_size,
716726
)
717-
_new_hit_length = len(hit_blocks[0]) * spec.block_size
727+
_new_hit_length = len(hit_blocks[0]) * effective_block_size
718728
if drop_eagle_block:
719729
eagle_verified.add(idx)
720730
elif _new_hit_length < curr_hit_length:
@@ -735,7 +745,7 @@ def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
735745
# Truncate full attention blocks to final hit_length (if present)
736746
first_group = self.attention_groups[0]
737747
if isinstance(first_group.spec, FullAttentionSpec):
738-
num_blocks = hit_length // first_group.spec.block_size
748+
num_blocks = hit_length // _effective_block_size(first_group.spec)
739749
for group_id in first_group.group_ids:
740750
if (blks := hit_blocks_by_group[group_id]) is not None:
741751
del blks[num_blocks:]
@@ -758,11 +768,15 @@ def find_longest_cache_hit_per_group(
758768
(blocks_per_group, hit_lengths_per_group)
759769
"""
760770

771+
def _effective_block_size(kv_cache_spec: KVCacheSpec) -> int:
772+
return kv_cache_spec.block_size * self.dcp_world_size * self.pcp_world_size
773+
761774
def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
762-
if kv_cache_spec.block_size == self.hash_block_size:
775+
effective_block_size = _effective_block_size(kv_cache_spec)
776+
if effective_block_size == self.hash_block_size:
763777
return block_hashes
764778
return BlockHashListWithBlockSize(
765-
block_hashes, self.hash_block_size, kv_cache_spec.block_size
779+
block_hashes, self.hash_block_size, effective_block_size
766780
)
767781

768782
num_groups = len(self.kv_cache_config.kv_cache_groups)
@@ -778,8 +792,10 @@ def _get_block_hashes(kv_cache_spec: KVCacheSpec) -> BlockHashList:
778792
kv_cache_spec=spec,
779793
drop_eagle_block=use_eagle,
780794
alignment_tokens=self.scheduler_block_size,
795+
dcp_world_size=self.dcp_world_size,
796+
pcp_world_size=self.pcp_world_size,
781797
)
782-
group_hit = len(blocks[0]) * spec.block_size
798+
group_hit = len(blocks[0]) * _effective_block_size(spec)
783799
for gid, blks in zip(group_ids, blocks):
784800
hit_blocks[gid] = blks
785801
hit_lengths[gid] = group_hit

vllm/v1/core/kv_cache_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -627,7 +627,7 @@ def resolve_kv_cache_block_sizes(
627627
- ``scheduler_block_size`` is the token-alignment invariant used by the
628628
scheduler (e.g. for ``num_computed_tokens`` rounding). Single group:
629629
``cache_config.block_size * dcp * pcp``. Multiple groups: LCM of every
630-
group's block size — context parallelism is not supported here.
630+
group's block size, scaled by the context-parallel world size.
631631
- ``hash_block_size`` is the granularity at which ``Request.block_hashes``
632632
is computed. Single group: equals scheduler block size. Multiple groups:
633633
``cache_config.hash_block_size`` override if set, else the GCD of group
@@ -652,7 +652,7 @@ def resolve_kv_cache_block_sizes(
652652
"support context parallelism (dcp_world_size/pcp_world_size > 1). "
653653
f"Got group block sizes={group_block_sizes}."
654654
)
655-
scheduler_block_size = math.lcm(*group_block_sizes)
655+
scheduler_block_size = math.lcm(*group_block_sizes) * dcp * pcp
656656

657657
# Block hashes are only consumed by prefix caching and KV connectors
658658
# (P/D, offloading); when neither is active, keep hash_block_size equal

vllm/v1/core/single_type_kv_cache_manager.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1057,13 +1057,11 @@ def find_longest_cache_hit(
10571057
assert isinstance(kv_cache_spec, MambaSpec), (
10581058
"MambaManager can only be used for mamba groups"
10591059
)
1060-
assert dcp_world_size == 1, "DCP not support mamba now."
1061-
assert pcp_world_size == 1, "PCP not support mamba now."
10621060
computed_blocks: tuple[list[KVCacheBlock], ...] = tuple(
10631061
[] for _ in range(len(kv_cache_group_ids))
10641062
)
10651063

1066-
block_size = kv_cache_spec.block_size
1064+
block_size = kv_cache_spec.block_size * dcp_world_size * pcp_world_size
10671065
max_num_blocks = max_length // block_size
10681066
# Search from right to left and early stop when a match is found.
10691067
for i in range(max_num_blocks - 1, -1, -1):

0 commit comments

Comments
 (0)