Skip to content

Commit e6960fa

Browse files
Pz1116zqs164Abstreyzmc1997DreamerLeader
authored
[Feature][KV Pool] Introduce AscendStoreCoordinator for hybrid cache-hit and mask coordination (vllm-project#10393)
## Reference Design This PR ports the external-store cache-hit coordination model from vLLM MooncakeStore HybridKVCacheCoordinator and MooncakeStoreCoordinator into AscendStore KV Pool. The implementation keeps AscendStore key and address generation, while using coordinator-style convergence and per-group reachable masks for hybrid cache groups. Ref:vllm-project/vllm#44774 ## Main Changes - Add AscendStoreCoordinator to mirror MooncakeStore external-cache hit coordination over AscendStore keys. - Wire the coordinator into KVPoolWorker and ChunkedTokenDatabase so lookup, save, and load share the same per-group mask semantics. - Preserve DeepSeek V4 compressed cache-family granularity: c4 and c128 keys are generated over raw-token spans, while transfer addresses remain in cache-domain blocks. - Carry num_prompt_tokens through request metadata so retention-aware reachable masks can match the scheduler/cache-manager behavior. - Keep the path disabled for non-hybrid or single-group KV Pool users, preserving the legacy behavior. - Add unit coverage for coordinator hit convergence, compressed effective granularity, and transfer mask application. ## Expected Behavior - Hybrid AscendStore lookup returns the common external prefix only when required KV cache groups satisfy the coordinator policy. - Sliding-window, Mamba align, and compressed DSV4 groups use manager-compatible reachable masks instead of storing or loading every physical chunk. - DSV4 c128 participates at the 16K effective boundary when block size is 128, rather than as 128-token chunks. - Store skips chunks that cannot serve a future hit, reducing external-store footprint for long-context hybrid models. - Load fetches only chunks required by the per-group mask and does not regress single-group KV Pool behavior. ## Validation vLLM version: v0.20.1 vLLM main: vllm-project/vllm@7021be66e - vLLM version: v0.23.0 - vLLM main: vllm-project/vllm@a30addc --------- Signed-off-by: Pz1116 <zpbzpb123123@gmail.com> Co-authored-by: zqs164 <1640410765@qq.com> Co-authored-by: Abstrey <846134527@qq.com> Co-authored-by: zmc1997 <742133686@qq.com> Co-authored-by: DreamerLeader <2270923832@qq.com> Co-authored-by: bowgneo <gavin.gq.bao@gmail.com>
1 parent 4839937 commit e6960fa

11 files changed

Lines changed: 1251 additions & 160 deletions

File tree

tests/ut/distributed/ascend_store/_mock_deps.py

Lines changed: 220 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,23 @@
2323
import tests.ut.distributed.ascend_store._mock_deps # noqa: F401, E402
2424
"""
2525

26+
import importlib.util
2627
import logging
2728
import os
2829
import sys
2930
import types
31+
from typing import Any
3032
from unittest.mock import MagicMock
3133

3234
# ---------------------------------------------------------------------------
3335
# Mock torch / torch_npu
3436
# ---------------------------------------------------------------------------
35-
if "torch" not in sys.modules:
37+
if "torch" not in sys.modules and importlib.util.find_spec("torch") is None:
3638
_torch = types.ModuleType("torch")
3739
_torch.Tensor = MagicMock # type: ignore[attr-defined]
3840
_torch.bool = "bool" # type: ignore[attr-defined]
3941
_torch.float16 = "float16" # type: ignore[attr-defined]
42+
_torch.float32 = "float32" # type: ignore[attr-defined]
4043
_torch.zeros = MagicMock(return_value=MagicMock()) # type: ignore[attr-defined]
4144
_torch.sum = MagicMock(return_value=0) # type: ignore[attr-defined]
4245
_torch.device = MagicMock() # type: ignore[attr-defined]
@@ -56,6 +59,7 @@
5659
# ---------------------------------------------------------------------------
5760
# Mock vllm modules
5861
# ---------------------------------------------------------------------------
62+
_MOCK_VLLM_DEPS = importlib.util.find_spec("vllm") is None
5963
_vllm_mock_modules = [
6064
"vllm",
6165
"vllm.config",
@@ -83,31 +87,38 @@
8387
"vllm.v1.attention",
8488
"vllm.v1.attention.backend",
8589
"vllm.v1.core",
90+
"vllm.v1.core.block_pool",
8691
"vllm.v1.core.kv_cache_manager",
8792
"vllm.v1.core.kv_cache_utils",
8893
"vllm.v1.core.sched",
8994
"vllm.v1.core.sched.output",
95+
"vllm.v1.core.single_type_kv_cache_manager",
9096
"vllm.v1.kv_cache_interface",
97+
"vllm.v1.kv_cache_spec_registry",
9198
"vllm.v1.outputs",
9299
"vllm.v1.request",
93100
"vllm.v1.serial_utils",
94101
]
95-
for _mod_name in _vllm_mock_modules:
96-
if _mod_name not in sys.modules:
97-
sys.modules[_mod_name] = MagicMock()
102+
if _MOCK_VLLM_DEPS:
103+
for _mod_name in _vllm_mock_modules:
104+
if _mod_name not in sys.modules:
105+
sys.modules[_mod_name] = MagicMock()
98106

99-
sys.modules["vllm.utils.math_utils"].cdiv = lambda a, b: -(-a // b) # type: ignore[attr-defined]
100-
sys.modules["vllm.logger"].logger = logging.getLogger("vllm") # type: ignore[attr-defined]
107+
if _MOCK_VLLM_DEPS:
108+
sys.modules["vllm.utils.math_utils"].cdiv = lambda a, b: -(-a // b) # type: ignore[attr-defined]
109+
sys.modules["vllm.logger"].logger = logging.getLogger("vllm") # type: ignore[attr-defined]
101110

102-
_base_mod = sys.modules["vllm.distributed.kv_transfer.kv_connector.v1.base"]
111+
_base_mod: Any = (
112+
sys.modules["vllm.distributed.kv_transfer.kv_connector.v1.base"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
113+
)
103114
_base_mod.KVConnectorBase_V1 = type("KVConnectorBase_V1", (), {"__init__": lambda self, **kw: None}) # type: ignore[attr-defined]
104115
_base_mod.KVConnectorMetadata = type("KVConnectorMetadata", (), {}) # type: ignore[attr-defined]
105116
_base_mod.KVConnectorWorkerMetadata = type("KVConnectorWorkerMetadata", (), {}) # type: ignore[attr-defined]
106117
_base_mod.KVConnectorRole = MagicMock() # type: ignore[attr-defined]
107118
_base_mod.KVConnectorRole.SCHEDULER = "SCHEDULER"
108119
_base_mod.KVConnectorRole.WORKER = "WORKER"
109120

110-
_events_mod = sys.modules["vllm.distributed.kv_events"]
121+
_events_mod: Any = sys.modules["vllm.distributed.kv_events"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
111122
_events_mod.KVCacheEvent = type("KVCacheEvent", (), {}) # type: ignore[attr-defined]
112123
_events_mod.KVConnectorKVEvents = type("KVConnectorKVEvents", (), {}) # type: ignore[attr-defined]
113124

@@ -127,14 +138,212 @@ def __getattr__(self, name):
127138
{"__init__": lambda self, **kwargs: self.__dict__.update(kwargs)},
128139
)
129140

130-
_kv_cache_utils_mod = sys.modules["vllm.v1.core.kv_cache_utils"]
141+
_kv_cache_utils_mod: Any = sys.modules["vllm.v1.core.kv_cache_utils"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
131142
_kv_cache_utils_mod.BlockHash = bytes # type: ignore[attr-defined]
132143
_kv_cache_utils_mod.maybe_convert_block_hash = lambda x: x # type: ignore[attr-defined]
133144

134-
_sched_output_mod = sys.modules["vllm.v1.core.sched.output"]
145+
146+
class _FakeKVCacheBlock:
147+
def __init__(self, block_id=0, **kwargs):
148+
self.block_id = block_id
149+
self.__dict__.update(kwargs)
150+
151+
152+
class _FakeKVCacheSpec:
153+
def __init__(self, block_size=16, **kwargs):
154+
self.block_size = block_size
155+
for key, value in kwargs.items():
156+
setattr(self, key, value)
157+
158+
def __eq__(self, other):
159+
return type(self) is type(other) and self.__dict__ == getattr(other, "__dict__", {})
160+
161+
def copy_with_new_block_size(self, block_size):
162+
kwargs = self.__dict__.copy()
163+
kwargs["block_size"] = block_size
164+
return type(self)(**kwargs)
165+
166+
@property
167+
def page_size_bytes(self):
168+
num_kv_heads = getattr(self, "num_kv_heads", 1)
169+
head_size = getattr(self, "head_size", 1)
170+
dtype = getattr(self, "dtype", None)
171+
dtype_size = getattr(dtype, "itemsize", None)
172+
if dtype_size is None and dtype is not None and hasattr(dtype, "element_size"):
173+
dtype_size = dtype.element_size()
174+
return self.block_size * num_kv_heads * head_size * int(dtype_size or 1) * 2
175+
176+
177+
class _FakeFullAttentionSpec(_FakeKVCacheSpec):
178+
pass
179+
180+
181+
class _FakeSlidingWindowSpec(_FakeKVCacheSpec):
182+
def __init__(self, block_size=16, sliding_window=32, **kwargs):
183+
super().__init__(block_size=block_size, sliding_window=sliding_window, **kwargs)
184+
185+
186+
class _FakeMambaSpec(_FakeKVCacheSpec):
187+
def __init__(self, block_size=16, **kwargs):
188+
super().__init__(block_size=block_size, **kwargs)
189+
self.num_speculative_blocks = getattr(self, "num_speculative_blocks", 0)
190+
191+
192+
class _FakeUniformTypeKVCacheSpecs(_FakeKVCacheSpec):
193+
def __init__(self, block_size=16, kv_cache_specs=None, **kwargs):
194+
super().__init__(block_size=block_size, **kwargs)
195+
self.kv_cache_specs = kv_cache_specs or {}
196+
197+
@classmethod
198+
def from_specs(cls, kv_cache_specs):
199+
if not kv_cache_specs:
200+
return None
201+
first_spec = next(iter(kv_cache_specs.values()))
202+
return cls(
203+
block_size=getattr(first_spec, "block_size", 16),
204+
kv_cache_specs=kv_cache_specs,
205+
)
206+
207+
208+
class _FakeKVCacheGroupSpec:
209+
def __init__(self, layer_names=None, kv_cache_spec=None, is_eagle_group=False):
210+
self.layer_names = layer_names or []
211+
self.kv_cache_spec = kv_cache_spec or _FakeFullAttentionSpec()
212+
self.is_eagle_group = is_eagle_group
213+
214+
215+
class _FakeKVCacheConfig:
216+
def __init__(self, num_blocks=1, kv_cache_tensors=None, kv_cache_groups=None):
217+
self.num_blocks = num_blocks
218+
self.kv_cache_tensors = kv_cache_tensors or []
219+
self.kv_cache_groups = kv_cache_groups or []
220+
221+
222+
_kv_cache_utils_mod.KVCacheBlock = _FakeKVCacheBlock # type: ignore[attr-defined]
223+
_kv_cache_utils_mod.BlockHashList = list # type: ignore[attr-defined]
224+
225+
226+
class _FakeBlockPool:
227+
def __init__(self, *args, **kwargs):
228+
self.null_block = _FakeKVCacheBlock(block_id=0)
229+
self._next_block_id = 1
230+
231+
def get_new_blocks(self, num_blocks):
232+
blocks = []
233+
for _ in range(num_blocks):
234+
blocks.append(_FakeKVCacheBlock(block_id=self._next_block_id))
235+
self._next_block_id += 1
236+
return blocks
237+
238+
239+
if _MOCK_VLLM_DEPS:
240+
sys.modules["vllm.v1.core.block_pool"].BlockPool = _FakeBlockPool # type: ignore[attr-defined]
241+
242+
243+
class _FakeSingleTypeKVCacheManager:
244+
def __init__(self, *args, **kwargs):
245+
self._mock = MagicMock()
246+
247+
def __getattr__(self, name):
248+
return getattr(self._mock, name)
249+
250+
@classmethod
251+
def reachable_block_mask(
252+
cls,
253+
start_block,
254+
end_block,
255+
alignment_tokens,
256+
kv_cache_spec,
257+
use_eagle,
258+
retention_interval=None,
259+
num_prompt_tokens=None,
260+
):
261+
return None
262+
263+
@classmethod
264+
def find_longest_cache_hit(
265+
cls,
266+
block_hashes,
267+
max_length,
268+
kv_cache_group_ids,
269+
block_pool,
270+
kv_cache_spec,
271+
drop_eagle_block=False,
272+
alignment_tokens=16,
273+
dcp_world_size=1,
274+
pcp_world_size=1,
275+
):
276+
computed: tuple[list[object], ...] = tuple([] for _ in kv_cache_group_ids)
277+
max_blocks = max_length // kv_cache_spec.block_size
278+
for block_hash in list(block_hashes)[:max_blocks]:
279+
cached = block_pool.get_cached_block(block_hash, kv_cache_group_ids)
280+
if not cached:
281+
break
282+
for blocks, block in zip(computed, cached):
283+
blocks.append(block)
284+
if drop_eagle_block and computed and computed[0]:
285+
for blocks in computed:
286+
blocks.pop()
287+
return computed
288+
289+
290+
class _FakeSlidingWindowManager(_FakeSingleTypeKVCacheManager):
291+
@classmethod
292+
def reachable_block_mask(
293+
cls,
294+
start_block,
295+
end_block,
296+
alignment_tokens,
297+
kv_cache_spec,
298+
use_eagle,
299+
retention_interval=None,
300+
num_prompt_tokens=None,
301+
):
302+
if alignment_tokens is None:
303+
return None
304+
per_segment = max(alignment_tokens // kv_cache_spec.block_size, 1)
305+
return [(idx + 1) % per_segment == 0 for idx in range(start_block, end_block)]
306+
307+
308+
_single_type_mod: Any = (
309+
sys.modules["vllm.v1.core.single_type_kv_cache_manager"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
310+
)
311+
_single_type_mod.SingleTypeKVCacheManager = _FakeSingleTypeKVCacheManager # type: ignore[attr-defined]
312+
_single_type_mod.FullAttentionManager = _FakeSingleTypeKVCacheManager # type: ignore[attr-defined]
313+
_single_type_mod.SlidingWindowManager = _FakeSlidingWindowManager # type: ignore[attr-defined]
314+
_single_type_mod.MambaManager = _FakeSingleTypeKVCacheManager # type: ignore[attr-defined]
315+
_single_type_mod.spec_manager_map = { # type: ignore[attr-defined]
316+
_FakeFullAttentionSpec: _FakeSingleTypeKVCacheManager,
317+
_FakeSlidingWindowSpec: _FakeSlidingWindowManager,
318+
_FakeMambaSpec: _FakeSingleTypeKVCacheManager,
319+
}
320+
321+
_kv_interface_mod: Any = sys.modules["vllm.v1.kv_cache_interface"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
322+
_kv_interface_mod.KVCacheSpec = _FakeKVCacheSpec # type: ignore[attr-defined]
323+
_kv_interface_mod.FullAttentionSpec = _FakeFullAttentionSpec # type: ignore[attr-defined]
324+
_kv_interface_mod.SlidingWindowSpec = _FakeSlidingWindowSpec # type: ignore[attr-defined]
325+
_kv_interface_mod.MambaSpec = _FakeMambaSpec # type: ignore[attr-defined]
326+
_kv_interface_mod.UniformTypeKVCacheSpecs = _FakeUniformTypeKVCacheSpecs # type: ignore[attr-defined]
327+
_kv_interface_mod.KVCacheGroupSpec = _FakeKVCacheGroupSpec # type: ignore[attr-defined]
328+
_kv_interface_mod.KVCacheConfig = _FakeKVCacheConfig # type: ignore[attr-defined]
329+
330+
331+
class _FakeKVCacheSpecRegistry:
332+
@classmethod
333+
def get_manager_class(cls, kv_cache_spec):
334+
if isinstance(kv_cache_spec, _FakeSlidingWindowSpec):
335+
return _FakeSlidingWindowManager
336+
return _FakeSingleTypeKVCacheManager
337+
338+
339+
if _MOCK_VLLM_DEPS:
340+
sys.modules["vllm.v1.kv_cache_spec_registry"].KVCacheSpecRegistry = _FakeKVCacheSpecRegistry # type: ignore[attr-defined]
341+
342+
_sched_output_mod: Any = sys.modules["vllm.v1.core.sched.output"] if _MOCK_VLLM_DEPS else types.SimpleNamespace()
135343
_sched_output_mod.NewRequestData = MagicMock # type: ignore[attr-defined]
136344

137-
sys.modules["vllm.envs"].VLLM_RPC_BASE_PATH = "/tmp/vllm_rpc" # type: ignore[attr-defined]
345+
if _MOCK_VLLM_DEPS:
346+
sys.modules["vllm.envs"].VLLM_RPC_BASE_PATH = "/tmp/vllm_rpc" # type: ignore[attr-defined]
138347

139348
# ---------------------------------------------------------------------------
140349
# Mock external backends

tests/ut/distributed/ascend_store/test_ascend_store_connector.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,26 @@
1818
import unittest
1919
from unittest.mock import MagicMock, patch
2020

21-
from vllm.distributed.kv_events import KVCacheEvent
22-
21+
# isort: off
2322
import tests.ut.distributed.ascend_store._mock_deps # noqa: F401, E402
23+
from vllm.distributed.kv_events import KVCacheEvent
2424
from vllm_ascend.distributed.kv_transfer.kv_pool.ascend_store.ascend_store_connector import (
2525
AscendStoreConnector,
2626
AscendStoreKVEvents,
2727
)
2828

29+
# isort: on
30+
31+
32+
def _mock_events(num_workers=1):
33+
events = AscendStoreKVEvents(num_workers=num_workers)
34+
events._aggregator = MagicMock()
35+
return events
36+
2937

3038
class TestAscendStoreKVEvents(unittest.TestCase):
3139
def _make_events(self, num_workers=1):
32-
return AscendStoreKVEvents(num_workers=num_workers)
40+
return _mock_events(num_workers=num_workers)
3341

3442
def test_add_and_get_events(self):
3543
ev = self._make_events()
@@ -162,7 +170,7 @@ def test_update_connector_output_with_events(self, mock_scheduler_cls):
162170
role=KVConnectorRole.SCHEDULER,
163171
kv_cache_config=None,
164172
)
165-
events = AscendStoreKVEvents(num_workers=1)
173+
events = _mock_events(num_workers=1)
166174
mock_kv_events = [MagicMock()]
167175
events._aggregator.get_all_events.return_value = mock_kv_events
168176
events._aggregator.get_number_of_workers.return_value = 1
@@ -183,15 +191,15 @@ def test_update_connector_output_accumulate(self, mock_scheduler_cls):
183191
kv_cache_config=None,
184192
)
185193
# First update
186-
events1 = AscendStoreKVEvents(num_workers=1)
194+
events1 = _mock_events(num_workers=1)
187195
events1._aggregator.get_all_events.return_value = [MagicMock()]
188196
events1._aggregator.get_number_of_workers.return_value = 1
189197
output1 = MagicMock()
190198
output1.kv_cache_events = events1
191199
connector.update_connector_output(output1)
192200

193201
# Second update
194-
events2 = AscendStoreKVEvents(num_workers=1)
202+
events2 = _mock_events(num_workers=1)
195203
events2._aggregator.get_all_events.return_value = [MagicMock()]
196204
events2._aggregator.get_number_of_workers.return_value = 1
197205
output2 = MagicMock()
@@ -214,7 +222,7 @@ def test_take_events(self, mock_scheduler_cls):
214222
self.assertEqual(result, [])
215223

216224
# With events
217-
events = AscendStoreKVEvents(num_workers=1)
225+
events = _mock_events(num_workers=1)
218226
mock_event = MagicMock()
219227
events._aggregator.get_common_events.return_value = [mock_event]
220228
events._aggregator.get_all_events.return_value = [mock_event]

0 commit comments

Comments
 (0)