560 lines
19 KiB
Python
560 lines
19 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""
|
|
Tests for LMCacheManager.
|
|
"""
|
|
|
|
# Standard
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
# Third Party
|
|
import pytest
|
|
|
|
# First Party
|
|
from lmcache.integration.base_service_factory import BaseServiceFactory
|
|
from lmcache.v1.config import LMCacheEngineConfig
|
|
from lmcache.v1.manager import LMCacheManager
|
|
|
|
|
|
def _make_mock_factory(**overrides):
|
|
"""Create a mock BaseServiceFactory with sensible defaults."""
|
|
factory = MagicMock(spec=BaseServiceFactory)
|
|
factory.get_or_create_metadata.return_value = overrides.get("metadata", None)
|
|
factory.get_or_create_lmcache_engine.return_value = overrides.get("engine", None)
|
|
factory.maybe_create_lookup_client.return_value = overrides.get(
|
|
"lookup_client", None
|
|
)
|
|
factory.maybe_create_lookup_server.return_value = overrides.get(
|
|
"lookup_server", None
|
|
)
|
|
factory.maybe_create_offload_server.return_value = overrides.get(
|
|
"offload_server", None
|
|
)
|
|
factory.maybe_create_runtime_plugin_launcher.return_value = overrides.get(
|
|
"plugin_launcher", None
|
|
)
|
|
factory.maybe_create_internal_api_server.return_value = overrides.get(
|
|
"api_server", None
|
|
)
|
|
factory.maybe_create_health_monitor.return_value = overrides.get(
|
|
"health_monitor", None
|
|
)
|
|
return factory
|
|
|
|
|
|
class TestLMCacheManagerInit:
|
|
"""Tests for LMCacheManager initialization."""
|
|
|
|
def test_init_stores_config(self):
|
|
"""Test that __init__ stores config correctly."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
connector = MagicMock()
|
|
factory = _make_mock_factory()
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
connector=connector,
|
|
)
|
|
|
|
assert manager._config is config
|
|
assert manager._connector is connector
|
|
|
|
def test_init_calls_factory_methods(self):
|
|
"""Test that __init__ calls all factory creation methods."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
|
|
LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
factory.get_or_create_metadata.assert_called_once()
|
|
factory.get_or_create_lmcache_engine.assert_called_once()
|
|
factory.maybe_create_lookup_client.assert_called_once()
|
|
factory.maybe_create_lookup_server.assert_called_once()
|
|
factory.maybe_create_offload_server.assert_called_once()
|
|
factory.maybe_create_runtime_plugin_launcher.assert_called_once()
|
|
factory.maybe_create_internal_api_server.assert_called_once()
|
|
|
|
def test_init_does_not_call_health_monitor(self):
|
|
"""Test that __init__ does NOT create health monitor (deferred to post_init)."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
|
|
LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
factory.maybe_create_health_monitor.assert_not_called()
|
|
|
|
|
|
class TestLMCacheManagerProperties:
|
|
"""Tests for LMCacheManager property accessors."""
|
|
|
|
@pytest.fixture
|
|
def manager_with_mocked_init(self):
|
|
"""Create a manager with mocked factory."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
connector=MagicMock(),
|
|
)
|
|
return manager
|
|
|
|
def test_lmcache_engine_property(self, manager_with_mocked_init):
|
|
"""Test lmcache_engine property returns the engine."""
|
|
manager = manager_with_mocked_init
|
|
mock_engine = MagicMock()
|
|
manager._lmcache_engine = mock_engine
|
|
|
|
assert manager.lmcache_engine is mock_engine
|
|
|
|
def test_lmcache_engine_metadata_property(self, manager_with_mocked_init):
|
|
"""Test lmcache_engine_metadata property returns the metadata."""
|
|
manager = manager_with_mocked_init
|
|
mock_metadata = MagicMock()
|
|
manager._lmcache_engine_metadata = mock_metadata
|
|
|
|
assert manager.lmcache_engine_metadata is mock_metadata
|
|
|
|
def test_lookup_client_property(self, manager_with_mocked_init):
|
|
"""Test lookup_client property returns the client."""
|
|
manager = manager_with_mocked_init
|
|
mock_client = MagicMock()
|
|
manager._lookup_client = mock_client
|
|
|
|
assert manager.lookup_client is mock_client
|
|
|
|
def test_lookup_server_property(self, manager_with_mocked_init):
|
|
"""Test lookup_server property returns the server."""
|
|
manager = manager_with_mocked_init
|
|
mock_server = MagicMock()
|
|
manager._lookup_server = mock_server
|
|
|
|
assert manager.lookup_server is mock_server
|
|
|
|
def test_offload_server_property(self, manager_with_mocked_init):
|
|
"""Test offload_server property returns the server."""
|
|
manager = manager_with_mocked_init
|
|
mock_server = MagicMock()
|
|
manager._offload_server = mock_server
|
|
|
|
assert manager.offload_server is mock_server
|
|
|
|
def test_config_property(self, manager_with_mocked_init):
|
|
"""Test config property returns the config."""
|
|
manager = manager_with_mocked_init
|
|
assert manager.config is manager._config
|
|
|
|
|
|
class TestLMCacheManagerStart:
|
|
"""Tests for LMCacheManager start method."""
|
|
|
|
def test_start_calls_api_server_start(self):
|
|
"""Test start_services() calls api_server.start() when api_server exists."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_api_server = MagicMock()
|
|
mock_plugin_launcher = MagicMock()
|
|
factory = _make_mock_factory(
|
|
api_server=mock_api_server,
|
|
plugin_launcher=mock_plugin_launcher,
|
|
)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
manager.start_services()
|
|
|
|
mock_api_server.start.assert_called_once()
|
|
mock_plugin_launcher.launch_plugins.assert_called_once()
|
|
|
|
def test_start_handles_none_api_server(self):
|
|
"""Test start_services() handles None api_server gracefully."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
# Should not raise any exception
|
|
manager.start_services()
|
|
|
|
|
|
class TestLMCacheManagerPostInit:
|
|
"""Tests for LMCacheManager post_init method."""
|
|
|
|
def test_post_init_without_engine(self):
|
|
"""Test post_init initializes health monitor when engine is None."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_health_monitor = MagicMock()
|
|
factory = _make_mock_factory(health_monitor=mock_health_monitor)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
manager.post_init()
|
|
|
|
# Health monitor should still be created even without engine
|
|
factory.maybe_create_health_monitor.assert_called_once_with(
|
|
lmcache_manager=manager
|
|
)
|
|
|
|
def test_post_init_with_engine_and_async_loading(self):
|
|
"""Test post_init calls engine.post_init with async_lookup_server."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
config.enable_async_loading = True
|
|
mock_engine = MagicMock()
|
|
factory = _make_mock_factory(engine=mock_engine)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
manager.post_init()
|
|
|
|
# When lookup_server is None, engine.post_init should be called
|
|
# with async_lookup_server=None
|
|
mock_engine.post_init.assert_called_once_with(async_lookup_server=None)
|
|
|
|
def test_post_init_with_engine_and_async_server(self):
|
|
"""Test post_init calls engine.post_init when async lookup server exists."""
|
|
# First Party
|
|
from lmcache.v1.lookup_client.lmcache_async_lookup_client import (
|
|
LMCacheAsyncLookupServer,
|
|
)
|
|
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
config.enable_async_loading = True
|
|
mock_engine = MagicMock()
|
|
mock_lookup_server = MagicMock(spec=LMCacheAsyncLookupServer)
|
|
factory = _make_mock_factory(
|
|
engine=mock_engine,
|
|
lookup_server=mock_lookup_server,
|
|
)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
manager.post_init()
|
|
|
|
# When lookup_server is LMCacheAsyncLookupServer, it should be passed
|
|
mock_engine.post_init.assert_called_once_with(
|
|
async_lookup_server=mock_lookup_server
|
|
)
|
|
|
|
|
|
class TestLMCacheManagerShutdown:
|
|
"""Tests for LMCacheManager shutdown method."""
|
|
|
|
def test_shutdown_closes_all_components(self):
|
|
"""Test stop_services() closes all components."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_offload = MagicMock()
|
|
mock_plugin = MagicMock()
|
|
mock_api = MagicMock()
|
|
mock_lookup_server = MagicMock()
|
|
mock_lookup_client = MagicMock()
|
|
factory = _make_mock_factory(
|
|
offload_server=mock_offload,
|
|
plugin_launcher=mock_plugin,
|
|
api_server=mock_api,
|
|
lookup_server=mock_lookup_server,
|
|
lookup_client=mock_lookup_client,
|
|
)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
with patch("lmcache.v1.manager.LMCacheEngineBuilder") as mock_builder:
|
|
manager.stop_services()
|
|
|
|
# Verify all components were closed
|
|
mock_offload.close.assert_called_once()
|
|
mock_plugin.stop_plugins.assert_called_once()
|
|
mock_api.stop.assert_called_once()
|
|
mock_lookup_server.close.assert_called_once()
|
|
mock_lookup_client.close.assert_called_once()
|
|
mock_builder.destroy.assert_called_once()
|
|
|
|
def test_shutdown_handles_none_components(self):
|
|
"""Test stop_services() handles None components gracefully."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
with patch("lmcache.v1.manager.LMCacheEngineBuilder"):
|
|
# Should not raise any exception
|
|
manager.stop_services()
|
|
|
|
def test_shutdown_handles_component_errors(self):
|
|
"""Test stop_services() handles errors from components gracefully."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_offload = MagicMock()
|
|
mock_offload.close.side_effect = RuntimeError("Test error")
|
|
mock_lookup_client = MagicMock()
|
|
factory = _make_mock_factory(
|
|
offload_server=mock_offload,
|
|
lookup_client=mock_lookup_client,
|
|
)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
with patch("lmcache.v1.manager.LMCacheEngineBuilder"):
|
|
# Should not raise exception, but should continue shutdown
|
|
manager.stop_services()
|
|
|
|
# lookup_client should still be closed even if offload_server failed
|
|
mock_lookup_client.close.assert_called_once()
|
|
|
|
|
|
class TestLMCacheManagerHelpers:
|
|
"""Tests for LMCacheManager helper methods."""
|
|
|
|
def test_need_gpu_interm_buffer_returns_not_enable_pd(self):
|
|
"""Test _need_gpu_interm_buffer returns opposite of enable_pd."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
config.enable_pd = False
|
|
# First Party
|
|
from lmcache.v1.gpu_connector.utils import need_gpu_interm_buffer
|
|
|
|
assert need_gpu_interm_buffer(config) is True
|
|
|
|
config.enable_pd = True
|
|
assert need_gpu_interm_buffer(config) is False
|
|
|
|
|
|
class TestLMCacheManagerValidation:
|
|
"""Tests for validate_mla_config (now in utils)."""
|
|
|
|
def test_validate_mla_config_raises_on_wrong_serde(self):
|
|
"""Test validate_mla_config raises error for non-naive serde with MLA."""
|
|
# First Party
|
|
from lmcache.integration.vllm.utils import validate_mla_config
|
|
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
config.remote_serde = "cachegen"
|
|
|
|
with pytest.raises(ValueError, match="MLA only works with naive serde mode"):
|
|
validate_mla_config(config, use_mla=True)
|
|
|
|
def test_validate_mla_config_raises_on_layerwise_with_blending(self):
|
|
"""Test validate_mla_config raises with MLA + layerwise + blending."""
|
|
# First Party
|
|
from lmcache.integration.vllm.utils import validate_mla_config
|
|
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
config.remote_serde = "naive"
|
|
config.use_layerwise = True
|
|
config.enable_blending = True
|
|
|
|
with pytest.raises(ValueError, match="MLA with Cacheblend"):
|
|
validate_mla_config(config, use_mla=True)
|
|
|
|
|
|
class TestLMCacheManagerCalculateDraftLayers:
|
|
"""Tests for calculate_draft_layers (now in utils)."""
|
|
|
|
def test_calculate_draft_layers_no_speculative_config(self):
|
|
"""Test returns 0 when no speculative_config."""
|
|
# First Party
|
|
from lmcache.integration.vllm.utils import calculate_draft_layers
|
|
|
|
vllm_config = MagicMock()
|
|
vllm_config.speculative_config = None
|
|
|
|
assert calculate_draft_layers(vllm_config) == 0
|
|
|
|
def test_calculate_draft_layers_deepseek_mtp(self):
|
|
"""Test returns correct layers for deepseek_mtp method."""
|
|
# First Party
|
|
from lmcache.integration.vllm.utils import calculate_draft_layers
|
|
|
|
vllm_config = MagicMock()
|
|
vllm_config.speculative_config = MagicMock()
|
|
vllm_config.speculative_config.method = "deepseek_mtp"
|
|
vllm_config.model_config = MagicMock()
|
|
vllm_config.model_config.hf_config = MagicMock()
|
|
vllm_config.model_config.hf_config.num_nextn_predict_layers = 3
|
|
|
|
assert calculate_draft_layers(vllm_config) == 3
|
|
|
|
|
|
class TestLMCacheManagerInitFailure:
|
|
"""Tests for LMCacheManager initialization failure handling."""
|
|
|
|
def test_init_components_exception_makes_unhealthy(self):
|
|
"""Test that exception during factory calls makes manager unhealthy."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
factory.get_or_create_metadata.side_effect = RuntimeError("Test init failure")
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
# Verify through is_healthy API
|
|
assert manager.is_healthy() is False
|
|
|
|
def test_post_init_skipped_when_init_failed(self):
|
|
"""Test that post_init marks engine unhealthy when init failed."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
factory = _make_mock_factory()
|
|
factory.get_or_create_metadata.side_effect = RuntimeError("Test init failure")
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
mock_engine = MagicMock()
|
|
manager._lmcache_engine = mock_engine
|
|
|
|
manager.post_init()
|
|
|
|
# Engine should be marked as init failed
|
|
mock_engine.mark_init_failed.assert_called_once()
|
|
# Manager should report unhealthy
|
|
assert manager.is_healthy() is False
|
|
|
|
def test_post_init_exception_makes_unhealthy(self):
|
|
"""Test that exception during post_init makes system unhealthy."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_engine = MagicMock()
|
|
mock_engine.post_init.side_effect = RuntimeError("Test post_init failure")
|
|
factory = _make_mock_factory(engine=mock_engine)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
manager.post_init()
|
|
|
|
# Verify engine was marked as failed
|
|
mock_engine.mark_init_failed.assert_called_once()
|
|
# Manager should report unhealthy
|
|
assert manager.is_healthy() is False
|
|
|
|
def test_unhealthy_engine_makes_manager_unhealthy(self):
|
|
"""Test that manager is unhealthy when engine is unhealthy."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_engine = MagicMock()
|
|
mock_engine.is_healthy.return_value = False
|
|
factory = _make_mock_factory(engine=mock_engine)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
assert manager.is_healthy() is False
|
|
|
|
def test_healthy_when_all_components_healthy(self):
|
|
"""Test that manager is healthy when all components are healthy."""
|
|
config = LMCacheEngineConfig.from_defaults()
|
|
mock_engine = MagicMock()
|
|
mock_engine.is_healthy.return_value = True
|
|
factory = _make_mock_factory(engine=mock_engine)
|
|
|
|
manager = LMCacheManager(
|
|
config=config,
|
|
service_factory=factory,
|
|
)
|
|
|
|
mock_health_monitor = MagicMock()
|
|
mock_health_monitor.is_healthy.return_value = True
|
|
manager._health_monitor = mock_health_monitor
|
|
|
|
assert manager.is_healthy() is True
|
|
|
|
|
|
class TestLMCacheEngineInitFailure:
|
|
"""Tests for LMCacheEngine init failure behavior on lookup/retrieve."""
|
|
|
|
def test_lookup_returns_zero_when_init_failed(self):
|
|
"""Test that lookup returns 0 when engine is marked as init failed."""
|
|
# First Party
|
|
from lmcache.v1.cache_engine import LMCacheEngine
|
|
|
|
engine = MagicMock(spec=LMCacheEngine)
|
|
engine._init_failed = False
|
|
engine._health_monitor = None
|
|
|
|
# Attach real methods to the mock
|
|
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
|
|
engine, LMCacheEngine
|
|
)
|
|
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
|
|
|
|
# Call mark_init_failed and verify is_healthy returns False
|
|
engine.mark_init_failed("Test reason")
|
|
assert engine.is_healthy() is False
|
|
|
|
def test_retrieve_returns_empty_mask_when_init_failed(self):
|
|
"""Test retrieve returns all-False mask when engine init failed."""
|
|
# First Party
|
|
from lmcache.v1.cache_engine import LMCacheEngine
|
|
|
|
engine = MagicMock(spec=LMCacheEngine)
|
|
engine._init_failed = False
|
|
engine._health_monitor = None
|
|
|
|
# Attach real methods to the mock
|
|
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
|
|
engine, LMCacheEngine
|
|
)
|
|
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
|
|
|
|
# Mark as init failed and verify is_healthy returns False
|
|
# (which would cause retrieve to return all-False mask)
|
|
engine.mark_init_failed("Test reason")
|
|
assert engine.is_healthy() is False
|
|
|
|
def test_mark_init_failed_makes_engine_unhealthy(self):
|
|
"""Test that mark_init_failed makes engine report unhealthy."""
|
|
# First Party
|
|
from lmcache.v1.cache_engine import LMCacheEngine
|
|
|
|
engine = MagicMock(spec=LMCacheEngine)
|
|
engine._init_failed = False
|
|
engine._health_monitor = None
|
|
|
|
# Attach real methods to the mock
|
|
engine.mark_init_failed = LMCacheEngine.mark_init_failed.__get__(
|
|
engine, LMCacheEngine
|
|
)
|
|
engine.is_healthy = LMCacheEngine.is_healthy.__get__(engine, LMCacheEngine)
|
|
|
|
# Verify initially healthy
|
|
assert engine.is_healthy() is True
|
|
|
|
# Call the method under test
|
|
engine.mark_init_failed("Test reason")
|
|
|
|
# Assert on the outcome
|
|
assert engine.is_healthy() is False
|