Files
2026-07-13 12:24:33 +08:00

145 lines
5.1 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""End-to-end tests for multi-model support in the StoreController.
A single ``finish_write`` with mixed-model keys must result in per-model
``submit_store_task`` calls so each submission sees uniform ``(shape, dtype)``.
"""
# Standard
from unittest.mock import patch
import time
# Third Party
import pytest
import torch
# First Party
from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey
from lmcache.v1.distributed.config import (
EvictionConfig,
L1ManagerConfig,
L1MemoryManagerConfig,
StorageManagerConfig,
)
from lmcache.v1.distributed.l2_adapters.config import L2AdaptersConfig
from lmcache.v1.distributed.l2_adapters.mock_l2_adapter import (
MockL2Adapter,
MockL2AdapterConfig,
)
from lmcache.v1.distributed.storage_manager import StorageManager
pytestmark = pytest.mark.skipif(
not torch.cuda.is_available(), reason="CUDA is not available"
)
# =============================================================================
# Helpers
# =============================================================================
def make_object_key(chunk_id: int, model_name: str) -> ObjectKey:
return ObjectKey(
chunk_hash=ObjectKey.IntHash2Bytes(chunk_id),
model_name=model_name,
kv_rank=0,
)
def wait_for_condition(predicate, timeout: float = 10.0, poll_interval: float = 0.05):
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(poll_interval)
return False
def make_storage_manager(l1_size_mb: int = 256) -> StorageManager:
cfg = StorageManagerConfig(
l1_manager_config=L1ManagerConfig(
memory_config=L1MemoryManagerConfig(
size_in_bytes=l1_size_mb * 1024 * 1024,
use_lazy=True,
init_size_in_bytes=min(l1_size_mb, 64) * 1024 * 1024,
),
),
eviction_config=EvictionConfig(eviction_policy="LRU"),
l2_adapter_config=L2AdaptersConfig(
adapters=[MockL2AdapterConfig(max_size_gb=0.1, mock_bandwidth_gb=10.0)],
),
)
return StorageManager(cfg)
# =============================================================================
# Tests
# =============================================================================
class TestStoreControllerMultimodel:
"""Mixed-model keys in one ``finish_write`` must be split per model
before reaching ``submit_store_task``."""
def test_each_submit_store_task_has_uniform_shape(self):
layout_a = MemoryLayoutDesc(
shapes=[torch.Size([100, 2, 512])], dtypes=[torch.bfloat16]
)
layout_b = MemoryLayoutDesc(
shapes=[torch.Size([50, 2, 256])], dtypes=[torch.bfloat16]
)
keys_a = [make_object_key(i, "model_a") for i in range(3)]
keys_b = [make_object_key(100 + i, "model_b") for i in range(3)]
# Record the (shape, dtype) set observed per submit_store_task call.
submit_shape_groups = []
original_submit = MockL2Adapter.submit_store_task
def recording_submit(self, keys, objects):
shape_set = frozenset(
(tuple(obj.get_shapes()), tuple(obj.get_dtypes())) for obj in objects
)
submit_shape_groups.append(shape_set)
return original_submit(self, keys, objects)
with patch.object(MockL2Adapter, "submit_store_task", recording_submit):
sm = make_storage_manager()
adapter = sm._l2_adapters[0]
ret_a = sm.reserve_write(keys_a, layout_a, mode="new")
ret_b = sm.reserve_write(keys_b, layout_b, mode="new")
for i, k in enumerate(keys_a):
ret_a[k].tensor.fill_(float(i + 1))
for i, k in enumerate(keys_b):
ret_b[k].tensor.fill_(float(100 + i))
# Single finish_write forces a mixed-model batch in the listener.
sm.finish_write(keys_a + keys_b)
# Wait until every key has landed in L2 — avoids a race on the
# in_flight counter (which is briefly 0 between pop and submit).
ok = wait_for_condition(
lambda: all(
adapter.debug_has_key(k) # type: ignore[attr-defined]
for k in keys_a + keys_b
),
timeout=10.0,
)
assert ok, "Not all keys were stored in L2 within timeout"
assert submit_shape_groups, "No submit_store_task calls were recorded"
for i, shapes in enumerate(submit_shape_groups):
assert len(shapes) == 1, (
f"submit_store_task #{i} received keys with mixed "
f"(shape, dtype): {shapes}. Mixed-model batches must be "
f"grouped by model before reaching the adapter."
)
distinct_shapes = {s for group in submit_shape_groups for s in group}
assert len(distinct_shapes) == 2, (
f"Expected both models' shapes to appear across submits, "
f"got {len(distinct_shapes)}: {distinct_shapes}"
)
sm.close()