Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
12 changes: 9 additions & 3 deletions tests/v1/kv_connector/unit/test_mooncake_store_connector.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
worker,
)
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.data import (
MooncakeLookupResult,
MooncakeStoreConnectorMetadata,
PartialHitBoundary,
)
from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.metrics import (
MooncakeStoreConnectorStats,
Expand Down Expand Up @@ -410,7 +412,9 @@ def test_lookup_key_client_lookup_prepends_typed_tag():

# Blocking lookup (non_block defaults to False) runs on the executor and
# returns the resolved hit length.
assert client.lookup("req0", num_tokens=128, block_hashes=[]) == 5
result = client.lookup("req0", num_tokens=128, block_hashes=[])
assert result is not None
assert result.hit_length == 5

sent_frames = fake_socket.send_multipart.call_args[0][0]
assert sent_frames[0] == protocol.LOOKUP_MSG
Expand Down Expand Up @@ -445,7 +449,7 @@ def _poll_lookup(client, req_id, num_tokens=128, block_hashes=(), timeout=5.0):
while time.monotonic() < deadline:
result = client.lookup(req_id, num_tokens, list(block_hashes), non_block=True)
if result is not None:
return result
return result.hit_length
time.sleep(0.005)
return None

Expand Down Expand Up @@ -553,11 +557,13 @@ def test_get_num_new_matched_tokens_async_defers_then_reports():

# Lookup ready with a hit -> report need_to_allocate + async-load flag.
hit = 3 * block_size
mock_client.lookup.return_value = hit
boundary = PartialHitBoundary(group_id=0, num_tokens=4 * block_size)
mock_client.lookup.return_value = MooncakeLookupResult(hit, (boundary,))
need, load_async = sched.get_num_new_matched_tokens(request, 0)
assert need == hit
assert load_async == sched.load_async
assert sched.load_specs["r1"].kvpool_cached_tokens == hit
assert sched.load_specs["r1"].partial_hit_boundaries == (boundary,)


def test_protocol_tags_are_distinct_and_non_empty():
Expand Down
8 changes: 4 additions & 4 deletions tests/v1/kv_connector/unit/test_mooncake_store_hma_e2e.py
Original file line number Diff line number Diff line change
Expand Up @@ -240,10 +240,10 @@ def _fake_thread_init(*args, **kwargs):
worker.store = store

# Both groups stored all 4 blocks -> full hit.
assert worker.lookup(num_tokens=65, block_hashes=hs) == 64
assert worker.lookup(num_tokens=65, block_hashes=hs).hit_length == 64
# Exact-multiple prompt: the full hit is re-derived one block lower,
# where both groups' stored blocks still cover the SWA window.
assert worker.lookup(num_tokens=64, block_hashes=hs) == 48
assert worker.lookup(num_tokens=64, block_hashes=hs).hit_length == 48

# Evict SWA's first two blocks (outside its window of 32 tokens = 2 blocks).
swa_keys_outside_window = [
Expand All @@ -256,12 +256,12 @@ def _fake_thread_init(*args, **kwargs):

# SWA window=32 -> only last 2 blocks must be present in SWA group.
# Full has all 4. Coordinator should still return 64.
assert worker.lookup(num_tokens=65, block_hashes=hs) == 64
assert worker.lookup(num_tokens=65, block_hashes=hs).hit_length == 64
# Exact-multiple prompt after eviction: the boundary one block lower
# needs SWA block 1, which is gone — no usable stored boundary remains
# (the pre-fix arithmetic clamp would have returned 48 and livelocked
# on load failure -> recompute -> same lookup).
assert worker.lookup(num_tokens=64, block_hashes=hs) == 0
assert worker.lookup(num_tokens=64, block_hashes=hs).hit_length == 0


def test_recv_skips_swa_blocks_before_window():
Expand Down
5 changes: 3 additions & 2 deletions tests/v1/kv_connector/unit/test_mooncake_store_scheduler.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from vllm.distributed.kv_transfer.kv_connector.v1.mooncake.store.data import (
LoadSpec,
MooncakeLookupResult,
MooncakeStoreWorkerMetadata,
ReqMeta,
RequestTracker,
Expand Down Expand Up @@ -685,9 +686,9 @@ def lookup(
num_tokens: int,
block_hashes: list[bytes],
non_block: bool = False,
) -> int:
) -> MooncakeLookupResult:
self.num_tokens.append(num_tokens)
return self._hit_tokens
return MooncakeLookupResult(self._hit_tokens)


def test_full_external_hit_keeps_kvpool_cached_tokens_block_aligned():
Expand Down
185 changes: 172 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 @@ -30,6 +30,7 @@
ChunkedTokenDatabase,
KeyMetadata,
LoadSpec,
PartialHitBoundary,
PoolKey,
ReqMeta,
)
Expand Down Expand Up @@ -144,6 +145,7 @@ def _make_load_req(
*,
token_len: int,
vllm_cached_tokens: int = 0,
partial_hit_boundaries: tuple[PartialHitBoundary, ...] = (),
) -> ReqMeta:
return ReqMeta(
req_id=req_id,
Expand All @@ -155,6 +157,7 @@ def _make_load_req(
kvpool_cached_tokens=token_len,
can_load=True,
token_len=token_len,
partial_hit_boundaries=partial_hit_boundaries,
),
)

Expand Down Expand Up @@ -299,15 +302,16 @@ def _patch_worker_runtime(
local_ip: str = "10.0.0.7",
tp_rank: int = 0,
tp_size: int = 1,
pcp_size: int = 1,
dcp_size: int = 1,
) -> None:
single_rank_group = SimpleNamespace(world_size=1, rank_in_group=0)
pcp_group = SimpleNamespace(world_size=pcp_size, rank_in_group=0)
# DCP groups are contiguous splits of the TP group (see
# parallel_state.py), so dcp_rank == tp_rank % dcp_size.
dcp_group = SimpleNamespace(world_size=dcp_size, rank_in_group=tp_rank % dcp_size)
monkeypatch.setattr(worker, "get_tensor_model_parallel_rank", lambda: tp_rank)
monkeypatch.setattr(worker, "get_tensor_model_parallel_world_size", lambda: tp_size)
monkeypatch.setattr(worker, "get_pcp_group", lambda: single_rank_group)
monkeypatch.setattr(worker, "get_pcp_group", lambda: pcp_group)
monkeypatch.setattr(worker, "get_dcp_group", lambda: dcp_group)
monkeypatch.setattr(worker, "get_ip", lambda: local_ip)
monkeypatch.setattr(worker, "LookupKeyServer", MagicMock())
Expand Down Expand Up @@ -1427,6 +1431,29 @@ def test_recv_thread_uses_single_batch_when_no_disk_offload_budget(monkeypatch):
store.batch_get_replica_desc.assert_not_called()


def test_recv_thread_keys_chunk_by_lookup_selected_boundary():
store = MagicMock()
store.batch_get_into_multi_buffers.return_value = [256, 256]
thread = _make_store_recving_thread(store)

# 32-token hit over 16-token chunks: chunk 1 would default to the hash at
# the 32-token boundary (a1); the lookup matched the 48-token one (a2).
req = _make_load_req(
"req-a",
[b"a0", b"a1", b"a2"],
token_len=32,
partial_hit_boundaries=(PartialHitBoundary(group_id=0, num_tokens=48),),
)

thread._handle_request(req)

keys = store.batch_get_into_multi_buffers.call_args.args[0]
assert keys == [
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0@6130",
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0@6132",
]


def test_recv_thread_logs_tier_summary_when_enabled(monkeypatch, caplog_vllm):
monkeypatch.setenv("VLLM_MOONCAKE_STORE_TIER_LOG", "1")
caplog_vllm.set_level(logging.INFO, logger=worker.logger.name)
Expand Down Expand Up @@ -2662,12 +2689,12 @@ def test_lookup_rejects_boundary_missing_one_mamba_shard():
# 33 tokens for two 16-token blocks: the hit stops below the request end,
# so the full-hit re-derivation stays out of the shard accounting.
worker.store.batch_is_exist.side_effect = lambda keys: [1] * len(keys)
assert worker.lookup(33, [b"h0", b"h1"]) == 32
assert worker.lookup(33, [b"h0", b"h1"]).hit_length == 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(33, [b"h0", b"h1"]) == 0
assert worker.lookup(33, [b"h0", b"h1"]).hit_length == 0


def test_lookup_requires_all_dcp_rank_namespaces():
Expand All @@ -2678,7 +2705,7 @@ def test_lookup_requires_all_dcp_rank_namespaces():
_refresh_group_tp_replication_factors(worker)
worker.store.batch_is_exist.return_value = [1, 1, 0, 1]

assert worker.lookup(16, [b"a0"]) == 0
assert worker.lookup(16, [b"a0"]).hit_length == 0
assert worker.store.batch_is_exist.call_args.args[0] == [
"test-model@tp_rank:0@pcp0@dcp0@pp_rank:0@group:0@6130",
"test-model@tp_rank:1@pcp0@dcp1@pp_rank:0@group:0@6130",
Expand All @@ -2690,7 +2717,7 @@ def test_lookup_requires_all_dcp_rank_namespaces():
def test_lookup_partial_prefix_returns_first_hit_length():
worker = _make_bare_worker()
worker.store.batch_is_exist.return_value = [1, 1, 0]
assert worker.lookup(48, [b"a0", b"a1", b"a2"]) == 32
assert worker.lookup(48, [b"a0", b"a1", b"a2"]).hit_length == 32


def test_lookup_partial_tail_uses_hash_alignment():
Expand Down Expand Up @@ -2730,15 +2757,15 @@ def test_lookup_partial_tail_uses_hash_alignment():
_refresh_group_tp_replication_factors(worker)
worker.store.batch_is_exist.return_value = [0, 0, 1, 0, 0, 1]

assert worker.lookup(13, [b"h0", b"h1", b"h2"]) == 12
assert worker.lookup(13, [b"h0", b"h1", b"h2"]).hit_length == 12


def test_lookup_full_hit_reuses_existing_boundary():
"""A full hit is re-derived below the request end without another RPC."""
worker = _make_bare_worker(block_size=16)
worker.store.batch_is_exist.return_value = [1, 1]

assert worker.lookup(32, [b"h0", b"h1"]) == 16
assert worker.lookup(32, [b"h0", b"h1"]).hit_length == 16
assert worker.store.batch_is_exist.call_count == 1


Expand All @@ -2759,10 +2786,142 @@ def test_lookup_full_hit_with_eagle_pops_once_not_twice():
# 64-token exact-multiple prompt, all 4 blocks stored: one eagle pop
# gives 48; a spurious re-derivation (anchored at 48) would pop again
# and return 32.
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]) == 48
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]).hit_length == 48
assert worker.store.batch_is_exist.call_count == 1


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

worker = _make_bare_worker(block_size=16)
full = 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(["full"], full),
KVCacheGroupSpec(["mamba"], mamba),
]
worker.hash_block_size = 4
worker.token_dbs = [
ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=group_id),
block_size=16,
hash_block_size=4,
)
for group_id in range(2)
]
worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
worker._kv_cache_groups,
scheduler_block_size=16,
hash_block_size=4,
use_eagle=True,
)
_refresh_group_tp_replication_factors(worker)
hashes = [BlockHash(f"h{i}".encode()) for i in range(6)]
present = {
(0, bytes(hashes[3])),
(0, bytes(hashes[5])),
(1, bytes(hashes[4])),
# A later Mamba state also exists, but its exact hit-boundary hash
# must win and therefore needs no override.
(1, bytes(hashes[5])),
}

def exists(keys):
return [
int(
any(
f"@group:{group_id}@{block_hash.hex()}" in key
for group_id, block_hash in present
)
)
for key in keys
]

worker.store.batch_is_exist.side_effect = exists

result = worker.lookup(25, hashes)

# The eagle drop trims the hit to 20 tokens, but the block that survives
# truncation is the one keyed at the 24-token boundary (hashes[5]), which
# the load path cannot derive from hit_length.
assert result.hit_length == 20
assert result.partial_hit_boundaries == (
PartialHitBoundary(group_id=0, num_tokens=24),
)


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

worker = _make_bare_worker(block_size=16)
full = 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(["full"], full),
KVCacheGroupSpec(["mamba"], mamba),
]
worker.hash_block_size = 4
worker.token_dbs = [
ChunkedTokenDatabase(
KeyMetadata("test-model", 0, 0, 0, 0, group_id=group_id),
block_size=16,
hash_block_size=4,
)
for group_id in range(2)
]
worker.coord = mooncake_store_worker.MooncakeStoreCoordinator(
worker._kv_cache_groups,
scheduler_block_size=16,
hash_block_size=4,
)
_refresh_group_tp_replication_factors(worker)
hashes = [BlockHash(f"h{i}".encode()) for i in range(12)]
present = {
(0, bytes(hashes[3])),
(0, bytes(hashes[7])),
(0, bytes(hashes[11])),
(1, bytes(hashes[4])),
}

def exists(keys):
return [
int(
any(
f"@group:{group_id}@{block_hash.hex()}" in key
for group_id, block_hash in present
)
)
for key in keys
]

worker.store.batch_is_exist.side_effect = exists

result = worker.lookup(49, hashes)

assert result.hit_length == 20
assert result.partial_hit_boundaries == (
PartialHitBoundary(group_id=0, num_tokens=32),
)


def test_lookup_full_hit_swa_degrades_when_no_stored_boundary_is_usable():
"""The motivating livelock: the producer of a 64-token prompt stored
only its SWA tail window (blocks 2-3). The old arithmetic clamp turned
Expand All @@ -2784,7 +2943,7 @@ def test_lookup_full_hit_swa_degrades_when_no_stored_boundary_is_usable():
)
worker.store.batch_is_exist.return_value = [0, 0, 1, 1]

assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]) == 0
assert worker.lookup(64, [b"h0", b"h1", b"h2", b"h3"]).hit_length == 0
assert worker.store.batch_is_exist.call_count == 1


Expand All @@ -2805,7 +2964,7 @@ def test_lookup_swa_single_group_returns_full_when_tail_window_present():
hash_block_size=worker.hash_block_size,
)
worker.store.batch_is_exist.return_value = [0, 0, 1, 1]
assert worker.lookup(65, [b"h0", b"h1", b"h2", b"h3"]) == 64
assert worker.lookup(65, [b"h0", b"h1", b"h2", b"h3"]).hit_length == 64


def test_lookup_checks_all_potential_swa_hit_boundaries():
Expand Down Expand Up @@ -2859,7 +3018,7 @@ def test_lookup_checks_all_potential_swa_hit_boundaries():
[f"h{i}".encode() for i in range(12)],
)

assert result == 32
assert result.hit_length == 32
keys = worker.store.batch_is_exist.call_args.args[0]
assert len(keys) == 6
swa_keys = [key for key in keys if "@group:1@" in key]
Expand Down Expand Up @@ -3408,7 +3567,7 @@ def test_lookup_records_mooncake_metrics():
result = worker.lookup(33, [b"a0", b"a1"])
stats = worker.get_kv_connector_stats()

assert result == 32
assert result.hit_length == 32
assert isinstance(stats, MooncakeStoreConnectorStats)
assert len(stats.data["lookup_exists"]) == 1
assert stats.data["lookup_exists"][0]["num_keys"] == 2
Expand Down
Loading
Loading