Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions tests/v1/kv_connector/unit/test_mooncake_store_hma_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ def test_e2e_swa_plus_full_save_then_lookup_hits():
worker = _build_worker_with_dict_store(vllm_config, cfg, store)
worker.tp_size = 1
worker.pp_size = 1
worker.put_step = 1
worker.num_kv_head = 8

# Register kv_caches using mocked thread classes so register_kv_caches
Expand Down Expand Up @@ -215,7 +214,7 @@ def _fake_thread_init(*args, **kwargs):
block_size=worker.block_size,
coord=worker.coord,
tp_rank=worker.tp_rank,
put_step=worker.put_step,
group_put_steps=worker._group_tp_replication_factors,
kv_role=worker.kv_role,
ready_event=ready,
enable_kv_event=False,
Expand Down
226 changes: 213 additions & 13 deletions tests/v1/kv_connector/unit/test_mooncake_store_worker.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ def _make_store_sending_thread(
block_size=block_size,
coord=coord,
tp_rank=tp_rank,
put_step=put_step,
group_put_steps=[put_step] * len(token_databases),
kv_role="kv_producer",
ready_event=threading.Event(),
replicate_config=replicate_config,
Expand Down Expand Up @@ -527,6 +527,29 @@ def test_store_sending_thread_delta_strides_with_local_phase():
assert store.batch_put_from_multi_buffers.call_args.args[0] == keys


def test_tp_sharded_group_saves_every_block_on_every_rank():
"""Sharded ranks must write every block because peers hold different bytes."""
store = MagicMock()
store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
store.batch_put_from_multi_buffers.side_effect = lambda keys, *a: [256] * len(keys)
thread = _make_store_sending_thread(store, tp_rank=0, put_step=2)
thread.group_put_steps = [1]

thread.add_stored_request("req-a")
thread._handle_request(
ReqMeta(
req_id="req-a",
token_len_chunk=64,
block_ids=([0, 1, 2, 3],),
block_hashes=[b"a0", b"a1", b"a2", b"a3"],
can_save=True,
)
)

keys = store.batch_is_exist.call_args.args[0]
assert len(keys) == 4


def test_store_sending_thread_retries_skipped_range_after_pressure():
store = MagicMock()
store.batch_is_exist.side_effect = lambda keys: [0] * len(keys)
Expand Down Expand Up @@ -1212,7 +1235,8 @@ def test_worker_put_striding_covers_every_rank_get_namespace(
]
assert len(keys) == len(block_hashes)
# PUT side: mirrors KVCacheStoreSendingThread's striding slice.
put_keys.update(keys[w.tp_rank % w.put_step :: w.put_step])
put_step = w._group_tp_replication_factors[0]
put_keys.update(keys[w.tp_rank % put_step :: put_step])
# GET side: KVCacheStoreRecvingThread fetches every key.
get_keys_per_rank[tp_rank] = set(keys)

Expand Down Expand Up @@ -1551,6 +1575,15 @@ def _register_with_mocked_threads(
worker.register_kv_caches(kv_caches)


def _refresh_group_tp_replication_factors(
worker: mooncake_store_worker.MooncakeStoreWorker,
) -> None:
worker._group_tp_replication_factors = (
worker._compute_group_tp_replication_factors()
)
worker._init_lookup_key_prefixes()


def _make_bare_worker(
*,
num_gpu_blocks: int = 10,
Expand All @@ -1568,11 +1601,9 @@ def _make_bare_worker(
worker.cache_config.num_gpu_blocks = num_gpu_blocks
worker.store = MagicMock()
worker.store.register_buffer.return_value = 0
worker.use_mla = False
worker.kv_role = kv_role
worker.block_size = block_size
worker.tp_rank = 0
worker.put_step = 1
worker.enable_kv_events = False
worker.kv_send_thread = None
worker.kv_recv_threads = []
Expand Down Expand Up @@ -1602,7 +1633,6 @@ def _make_bare_worker(
worker.pcp_size = 1
worker.dcp_size = 1
worker.hash_block_size = block_size
worker.metadata = KeyMetadata("test-model", 0, 0, 0, 0)
# Pre-build a single-group token_dbs so lookup-only tests don't have to
# call register_kv_caches.
worker.token_dbs = [
Expand All @@ -1617,7 +1647,7 @@ def _make_bare_worker(
scheduler_block_size=block_size,
hash_block_size=block_size,
)
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)
return worker


Expand All @@ -1626,9 +1656,8 @@ def test_lookup_key_prefixes_cover_dcp_rank_namespaces():
worker.tp_size = 4
worker.num_kv_head = 1
worker.dcp_size = 4
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)

assert worker._lookup_expected_per_key == 4
assert worker._lookup_key_prefixes[0] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
"test-model@tp_rank:1@pcp0@dcp1@pp_rank:0@group:0",
Expand All @@ -1643,21 +1672,192 @@ def test_lookup_key_prefixes_cover_pcp_rank_namespaces():
worker.num_kv_head = 1
worker.pcp_size = 2
worker.dcp_size = 1
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)

assert worker._lookup_expected_per_key == 2
assert worker._lookup_key_prefixes[0] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
"test-model@tp_rank:0@pcp1@dcp0@pp_rank:0@group:0",
)


def test_lookup_key_prefixes_expand_tp_sharded_groups_per_rank():
"""Replicated attention needs one namespace; sharded Mamba needs every rank."""
from vllm.v1.kv_cache_interface import (
FullAttentionSpec,
KVCacheGroupSpec,
MambaSpec,
)

worker = _make_bare_worker(block_size=16)
worker.tp_size = 2
worker.num_kv_head = 1
fa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
mamba = MambaSpec(
block_size=16,
shapes=((1, 1),),
dtypes=(torch.float32,),
mamba_cache_mode="align",
)
worker._kv_cache_groups = [
KVCacheGroupSpec(["l0"], fa),
KVCacheGroupSpec(["l1"], mamba),
]
worker.token_dbs = [
ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=0), block_size=16
),
ChunkedTokenDatabase(
KeyMetadata("test-model", 1, 0, 0, 0, group_id=1), block_size=16
),
]
_refresh_group_tp_replication_factors(worker)

assert worker._lookup_key_prefixes[0] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
)
assert worker._lookup_key_prefixes[1] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:1",
"test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:1",
)


def test_group_tp_replication_factors_mixed_mla_gqa_mamba():
from vllm.v1.kv_cache_interface import (
FullAttentionSpec,
KVCacheGroupSpec,
MambaSpec,
MLAAttentionSpec,
)

worker = _make_bare_worker(block_size=16)
worker.tp_size = 4
worker.num_kv_head = 2
mla = MLAAttentionSpec(block_size=16, num_kv_heads=1, head_size=64, dtype=None)
gqa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
mamba = MambaSpec(
block_size=16,
shapes=((1, 1),),
dtypes=(torch.float32,),
mamba_cache_mode="align",
)
worker._kv_cache_groups = [
KVCacheGroupSpec(["l0"], mla),
KVCacheGroupSpec(["l1"], gqa),
KVCacheGroupSpec(["l2"], mamba),
]
worker.token_dbs = [
ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=g_idx), block_size=16
)
for g_idx in range(3)
]

_refresh_group_tp_replication_factors(worker)
assert worker._group_tp_replication_factors == (4, 2, 1)
assert worker._lookup_key_prefixes[0] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
)
assert worker._lookup_key_prefixes[1] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:1",
"test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:1",
)
assert worker._lookup_key_prefixes[2] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:2",
"test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:2",
"test-model@tp_rank:2@pcp0@dcp0@pp_rank:0@group:2",
"test-model@tp_rank:3@pcp0@dcp0@pp_rank:0@group:2",
)


@pytest.mark.parametrize("spec_order", [("mla", "gqa"), ("gqa", "mla")])
def test_uniform_group_uses_common_inner_replication_factor(spec_order):
from vllm.v1.kv_cache_interface import (
FullAttentionSpec,
KVCacheGroupSpec,
MLAAttentionSpec,
UniformTypeKVCacheSpecs,
)

worker = _make_bare_worker(block_size=16)
worker.tp_size = 4
worker.num_kv_head = 2
specs_by_name = {
"mla": MLAAttentionSpec(
block_size=16, num_kv_heads=1, head_size=64, dtype=None
),
"gqa": FullAttentionSpec(
block_size=16, num_kv_heads=1, head_size=64, dtype=None
),
}
inner_specs = {name: specs_by_name[name] for name in spec_order}
uniform_spec = UniformTypeKVCacheSpecs(
block_size=16,
kv_cache_specs=inner_specs,
)
worker._kv_cache_groups = [
KVCacheGroupSpec(list(inner_specs), uniform_spec),
]

_refresh_group_tp_replication_factors(worker)

assert worker._group_tp_replication_factors == (2,)
assert worker._lookup_key_prefixes[0] == (
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0",
"test-model@tp_rank:1@pcp0@dcp0@pp_rank:0@group:0",
)


def test_lookup_rejects_boundary_missing_one_mamba_shard():
from vllm.v1.kv_cache_interface import (
FullAttentionSpec,
KVCacheGroupSpec,
MambaSpec,
)

worker = _make_bare_worker(block_size=16)
worker.tp_size = 2
worker.num_kv_head = 1
fa = FullAttentionSpec(block_size=16, num_kv_heads=8, head_size=64, dtype=None)
mamba = MambaSpec(
block_size=16,
shapes=((1, 1),),
dtypes=(torch.float32,),
mamba_cache_mode="align",
)
worker._kv_cache_groups = [
KVCacheGroupSpec(["l0"], fa),
KVCacheGroupSpec(["l1"], mamba),
]
worker.token_dbs = [
ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=0), block_size=16
),
ChunkedTokenDatabase(
KeyMetadata("test-model", 1, 0, 0, 0, group_id=1), block_size=16
),
]
worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
worker._kv_cache_groups,
scheduler_block_size=16,
hash_block_size=16,
)
_refresh_group_tp_replication_factors(worker)

worker.store.batch_is_exist.side_effect = lambda keys: [1] * len(keys)
assert worker.lookup(32, [b"h0", b"h1"]) == 32

worker.store.batch_is_exist.side_effect = lambda keys: [
0 if "tp_rank:1" in k and "group:1" in k else 1 for k in keys
]
assert worker.lookup(32, [b"h0", b"h1"]) == 0


def test_lookup_requires_all_dcp_rank_namespaces():
worker = _make_bare_worker(block_size=16)
worker.tp_size = 4
worker.num_kv_head = 1
worker.dcp_size = 4
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)
worker.store.batch_is_exist.return_value = [1, 1, 0, 1]

assert worker.lookup(16, [b"a0"]) == 0
Expand Down Expand Up @@ -1733,7 +1933,7 @@ def test_lookup_checks_all_potential_swa_hit_boundaries():
hash_block_size=8,
retention_interval=0,
)
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)
# Candidate order: 3 full-attention chunks, then SWA chunks 3, 7, 11.
# Only the first full chunk and the SWA chunk ending at token 32 exist, so
# lookup should recover a 32-token external prefix hit. A sparse
Expand Down Expand Up @@ -1794,7 +1994,7 @@ def test_lookup_applies_swa_mask_before_accessing_hashes():
hash_block_size=8,
retention_interval=0,
)
worker._init_lookup_key_prefixes()
_refresh_group_tp_replication_factors(worker)

block_hashes = _RecordingBlockHashes([f"h{i}".encode() for i in range(12)])
accessed_before_rpc: list[int] = []
Expand Down
Loading
Loading