197 lines
6.4 KiB
Python
197 lines
6.4 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
|
|
"""Unit tests for the device-type-driven dispatch in
|
|
``lmcache.v1.platform.cache_context.create_cache_context``.
|
|
|
|
The facade routes by the ``torch.device.type`` reported by the
|
|
wrappers' ``to_tensor()`` output and looks up the registered cache
|
|
context class via :mod:`lmcache.v1.platform._registry`. These tests
|
|
exercise that dispatch without touching CUDA or the real
|
|
``GPUCacheContext`` / ``CPUCacheContext`` constructors -- they
|
|
install fake classes in the registry through ``snapshot``/``restore``
|
|
so the test stays platform-agnostic.
|
|
"""
|
|
|
|
# Standard
|
|
from typing import Any, List
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
|
|
# First Party
|
|
from lmcache.v1.platform import cache_context as cache_context_module
|
|
from lmcache.v1.platform.base_cache_context import BaseCacheContext
|
|
from lmcache.v1.platform.cache_context import create_cache_context
|
|
|
|
|
|
class _FakeWrapper:
|
|
"""Minimal stand-in for a KV-cache IPC wrapper.
|
|
|
|
``create_cache_context`` only ever reads ``to_tensor().device.type``
|
|
from the wrappers it receives, so a 0-byte tensor on the requested
|
|
device is enough.
|
|
"""
|
|
|
|
def __init__(self, device_type: str) -> None:
|
|
self._device_type = device_type
|
|
|
|
def to_tensor(self) -> torch.Tensor:
|
|
return torch.empty(0, device=torch.device(self._device_type))
|
|
|
|
|
|
class _FakeContext(BaseCacheContext):
|
|
"""Bare-bones ``BaseCacheContext`` subclass used as a registry stub.
|
|
|
|
All abstract members are no-ops: the test never invokes them; it
|
|
only checks that the right class is instantiated with the
|
|
forwarded arguments.
|
|
"""
|
|
|
|
device_type = "fake"
|
|
|
|
def __init__(
|
|
self,
|
|
kv_caches: Any,
|
|
lmcache_tokens_per_chunk: int,
|
|
layout_hints: Any,
|
|
engine_group_infos: Any,
|
|
engine_type: Any,
|
|
separate_object_groups: bool = True,
|
|
full_sw_kv: bool = False,
|
|
) -> None:
|
|
# Skip ``BaseCacheContext.__init__`` -- it requires real
|
|
# KVLayerGroupsManager / shape descriptors that are out of
|
|
# scope for the dispatch test.
|
|
self.kv_caches = kv_caches
|
|
self.lmcache_tokens_per_chunk = lmcache_tokens_per_chunk
|
|
self.layout_hints = layout_hints
|
|
self.engine_group_infos = engine_group_infos
|
|
self.engine_type = engine_type
|
|
self.separate_object_groups = separate_object_groups
|
|
self.full_sw_kv = full_sw_kv
|
|
|
|
# ------------------------------------------------------------------
|
|
# Abstract stubs -- never called from these tests.
|
|
# ------------------------------------------------------------------
|
|
|
|
@property
|
|
def dtype(self) -> torch.dtype: # pragma: no cover - never invoked
|
|
return torch.float32
|
|
|
|
@property
|
|
def stream(self) -> Any: # pragma: no cover - never invoked
|
|
return None
|
|
|
|
@property
|
|
def cupy_stream(self) -> Any: # pragma: no cover - never invoked
|
|
return None
|
|
|
|
@property
|
|
def max_batch_size(self) -> int: # pragma: no cover - never invoked
|
|
return 0
|
|
|
|
def close(self) -> None: # pragma: no cover - never invoked
|
|
return None
|
|
|
|
def get_kernel_group_kv_pointers(
|
|
self, kernel_group_idx: int
|
|
) -> torch.Tensor: # pragma: no cover
|
|
return torch.empty(0)
|
|
|
|
def get_temp_kernel_group_buffer(
|
|
self, batch_idx: int, kernel_group_idx: int
|
|
) -> torch.Tensor: # pragma: no cover
|
|
return torch.empty(0)
|
|
|
|
def get_temp_object_group_buffer(
|
|
self, batch_idx: int, object_group_idx: int
|
|
) -> torch.Tensor: # pragma: no cover
|
|
return torch.empty(0)
|
|
|
|
def get_kernel_group_shape_dtype(
|
|
self,
|
|
num_tokens: int,
|
|
kernel_group_idx: int,
|
|
) -> Any: # pragma: no cover
|
|
return torch.Size(()), torch.float32
|
|
|
|
def cache_size_per_token(self) -> int: # pragma: no cover
|
|
return 0
|
|
|
|
|
|
class _FakeCPUContext(_FakeContext):
|
|
device_type = "cpu"
|
|
|
|
|
|
class _FakeCUDAContext(_FakeContext):
|
|
device_type = "cuda"
|
|
|
|
|
|
@pytest.fixture
|
|
def isolated_registry() -> Any:
|
|
"""Snapshot the backend table so each test can install fakes
|
|
without polluting other tests / the production setup."""
|
|
saved = cache_context_module.snapshot_backends()
|
|
# Start each test from an empty backend table so we can assert
|
|
# the "no class registered" branch deterministically.
|
|
cache_context_module.restore_backends({})
|
|
try:
|
|
yield
|
|
finally:
|
|
cache_context_module.restore_backends(saved)
|
|
|
|
|
|
def _install(**backends: type) -> None:
|
|
"""Replace the live backend table with *backends*."""
|
|
cache_context_module.restore_backends(dict(backends))
|
|
|
|
|
|
def test_dispatches_by_cpu_device_type(isolated_registry: None) -> None:
|
|
"""Wrappers reporting ``cpu`` tensors must yield the cpu-registered
|
|
class."""
|
|
_install(cpu=_FakeCPUContext)
|
|
|
|
wrappers: List[_FakeWrapper] = [_FakeWrapper("cpu"), _FakeWrapper("cpu")]
|
|
ctx = create_cache_context(wrappers) # type: ignore[arg-type]
|
|
|
|
assert isinstance(ctx, _FakeCPUContext)
|
|
assert ctx.kv_caches is wrappers
|
|
|
|
|
|
def test_dispatches_by_cuda_device_type(isolated_registry: None) -> None:
|
|
"""Wrappers reporting a non-cpu device type must route to the
|
|
matching registered class -- no isinstance branching."""
|
|
if not torch.cuda.is_available():
|
|
pytest.skip("CUDA not available")
|
|
_install(cuda=_FakeCUDAContext)
|
|
|
|
wrappers = [_FakeWrapper("cuda")]
|
|
ctx = create_cache_context(wrappers, lmcache_tokens_per_chunk=128) # type: ignore[arg-type]
|
|
|
|
assert isinstance(ctx, _FakeCUDAContext)
|
|
assert ctx.lmcache_tokens_per_chunk == 128
|
|
|
|
|
|
def test_empty_kv_caches_raises(isolated_registry: None) -> None:
|
|
with pytest.raises(ValueError, match="non-empty"):
|
|
create_cache_context([])
|
|
|
|
|
|
def test_mixed_device_types_raises(isolated_registry: None) -> None:
|
|
"""Cross-device batches are unsupported and must fail loudly."""
|
|
if not torch.cuda.is_available():
|
|
pytest.skip("CUDA not available")
|
|
_install(cpu=_FakeCPUContext, cuda=_FakeCUDAContext)
|
|
|
|
wrappers = [_FakeWrapper("cpu"), _FakeWrapper("cuda")]
|
|
with pytest.raises(ValueError, match="share one"):
|
|
create_cache_context(wrappers) # type: ignore[arg-type]
|
|
|
|
|
|
def test_unregistered_device_type_raises(isolated_registry: None) -> None:
|
|
"""An unknown device type is a hard failure with a clear hint."""
|
|
wrappers = [_FakeWrapper("cpu")]
|
|
with pytest.raises(ValueError, match="No cache-context class"):
|
|
create_cache_context(wrappers) # type: ignore[arg-type]
|