# SPDX-License-Identifier: Apache-2.0 """ Integration test for the RESP L2 adapter in MP mode. Requires a running Redis server. Skipped if Redis or the C++ extension is unavailable. """ # Standard import os import select import subprocess # Third Party import pytest import torch # First Party from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey from lmcache.v1.memory_management import ( MemoryFormat, MemoryObjMetadata, TensorMemoryObj, ) from lmcache.v1.platform import consume_fd _EMPTY_LAYOUT = MemoryLayoutDesc(shapes=[], dtypes=[]) REDIS_HOST = os.environ.get("REDIS_HOST", "localhost") REDIS_PORT = int(os.environ.get("REDIS_PORT", "6399")) def _redis_available() -> bool: """Check if Redis is reachable.""" try: result = subprocess.run( ["redis-cli", "-h", REDIS_HOST, "-p", str(REDIS_PORT), "ping"], capture_output=True, text=True, timeout=3, ) return result.stdout.strip() == "PONG" except Exception: return False def _native_client_available() -> bool: """Check if the C++ Redis extension can be imported.""" try: # First Party from lmcache.lmcache_redis import LMCacheRedisClient # noqa: F401 return True except ImportError: return False requires_redis = pytest.mark.skipif( not _redis_available(), reason=f"Redis not available at {REDIS_HOST}:{REDIS_PORT}", ) requires_native = pytest.mark.skipif( not _native_client_available(), reason="C++ Redis extension (lmcache_redis) not available", ) def create_object_key(chunk_id: int, model_name: str = "test_model") -> ObjectKey: return ObjectKey( chunk_hash=ObjectKey.IntHash2Bytes(chunk_id), model_name=model_name, kv_rank=0, ) def create_memory_obj(size: int = 256, fill_value: float = 1.0) -> TensorMemoryObj: raw_data = torch.empty(size, dtype=torch.float32) raw_data.fill_(fill_value) metadata = MemoryObjMetadata( shape=torch.Size([size]), dtype=torch.float32, address=0, phy_size=size * 4, fmt=MemoryFormat.KV_2LTD, ref_count=1, ) return TensorMemoryObj(raw_data, metadata, parent_allocator=None) def wait_for_event_fd(event_fd: int, timeout: float = 10.0) -> bool: poll = select.poll() poll.register(event_fd, select.POLLIN) events = poll.poll(timeout * 1000) if events: try: consume_fd(event_fd) except BlockingIOError: pass return True return False def flush_redis(): """Flush the test Redis database.""" subprocess.run( ["redis-cli", "-h", REDIS_HOST, "-p", str(REDIS_PORT), "FLUSHDB"], capture_output=True, timeout=3, ) @requires_redis @requires_native class TestRESPL2AdapterIntegration: """Integration tests using real Redis + real C++ RESP connector.""" @pytest.fixture(autouse=True) def setup_adapter(self): flush_redis() # First Party from lmcache.lmcache_redis import LMCacheRedisClient from lmcache.v1.distributed.l2_adapters.native_connector_l2_adapter import ( NativeConnectorL2Adapter, ) native_client = LMCacheRedisClient(REDIS_HOST, REDIS_PORT, 4) self.adapter = NativeConnectorL2Adapter(native_client) yield self.adapter.close() flush_redis() def test_event_fds_are_distinct(self): fds = { self.adapter.get_store_event_fd(), self.adapter.get_lookup_and_lock_event_fd(), self.adapter.get_load_event_fd(), } assert len(fds) == 3 def test_store_and_lookup(self): """Store objects to Redis, then verify lookup finds them.""" keys = [create_object_key(i) for i in range(5)] objs = [create_memory_obj(size=64, fill_value=float(i)) for i in range(5)] store_fd = self.adapter.get_store_event_fd() lookup_fd = self.adapter.get_lookup_and_lock_event_fd() # Store store_tid = self.adapter.submit_store_task(keys, objs) assert wait_for_event_fd(store_fd) completed = self.adapter.pop_completed_store_tasks() assert completed[store_tid].is_successful() # Lookup all — should find everything lookup_tid = self.adapter.submit_lookup_and_lock_task(keys, _EMPTY_LAYOUT) assert wait_for_event_fd(lookup_fd) bitmap = self.adapter.query_lookup_and_lock_result(lookup_tid) assert bitmap is not None for i in range(5): assert bitmap.test(i) is True, f"Key {i} not found in lookup" # Unlock self.adapter.submit_unlock(keys) def test_lookup_nonexistent_keys(self): """Lookup for keys not in Redis should return all zeros.""" keys = [create_object_key(i + 1000) for i in range(3)] lookup_fd = self.adapter.get_lookup_and_lock_event_fd() lookup_tid = self.adapter.submit_lookup_and_lock_task(keys, _EMPTY_LAYOUT) assert wait_for_event_fd(lookup_fd) bitmap = self.adapter.query_lookup_and_lock_result(lookup_tid) assert bitmap is not None for i in range(3): assert bitmap.test(i) is False def test_full_store_lookup_load_workflow(self): """End-to-end: store → lookup → load, verify data integrity.""" key = create_object_key(42) store_obj = create_memory_obj(size=512, fill_value=3.14) load_obj = create_memory_obj(size=512, fill_value=0.0) store_fd = self.adapter.get_store_event_fd() lookup_fd = self.adapter.get_lookup_and_lock_event_fd() load_fd = self.adapter.get_load_event_fd() # Store store_tid = self.adapter.submit_store_task([key], [store_obj]) assert wait_for_event_fd(store_fd) assert self.adapter.pop_completed_store_tasks()[store_tid].is_successful() # Lookup lookup_tid = self.adapter.submit_lookup_and_lock_task([key], _EMPTY_LAYOUT) assert wait_for_event_fd(lookup_fd) bitmap = self.adapter.query_lookup_and_lock_result(lookup_tid) assert bitmap.test(0) is True # Load load_tid = self.adapter.submit_load_task([key], [load_obj]) assert wait_for_event_fd(load_fd) bitmap = self.adapter.query_load_result(load_tid) assert bitmap.test(0) is True # Verify data integrity assert torch.allclose(load_obj.tensor, store_obj.tensor), ( "Loaded data does not match stored data" ) # Unlock self.adapter.submit_unlock([key]) def test_batch_store_lookup_load(self): """Batch workflow with multiple objects.""" n = 10 keys = [create_object_key(i) for i in range(n)] store_objs = [ create_memory_obj(size=128, fill_value=float(i * 7)) for i in range(n) ] load_objs = [create_memory_obj(size=128, fill_value=0.0) for _ in range(n)] store_fd = self.adapter.get_store_event_fd() lookup_fd = self.adapter.get_lookup_and_lock_event_fd() load_fd = self.adapter.get_load_event_fd() # Store all store_tid = self.adapter.submit_store_task(keys, store_objs) assert wait_for_event_fd(store_fd) assert self.adapter.pop_completed_store_tasks()[store_tid].is_successful() # Lookup all lookup_tid = self.adapter.submit_lookup_and_lock_task(keys, _EMPTY_LAYOUT) assert wait_for_event_fd(lookup_fd) bitmap = self.adapter.query_lookup_and_lock_result(lookup_tid) for i in range(n): assert bitmap.test(i) is True # Load all load_tid = self.adapter.submit_load_task(keys, load_objs) assert wait_for_event_fd(load_fd) bitmap = self.adapter.query_load_result(load_tid) for i in range(n): assert bitmap.test(i) is True assert torch.allclose(load_objs[i].tensor, store_objs[i].tensor), ( f"Data mismatch for key {i}" ) self.adapter.submit_unlock(keys) def test_mixed_lookup_existing_and_missing(self): """Lookup a mix of stored and non-stored keys.""" stored_keys = [create_object_key(i) for i in range(3)] stored_objs = [create_memory_obj(fill_value=float(i)) for i in range(3)] store_fd = self.adapter.get_store_event_fd() lookup_fd = self.adapter.get_lookup_and_lock_event_fd() # Store first 3 self.adapter.submit_store_task(stored_keys, stored_objs) assert wait_for_event_fd(store_fd) self.adapter.pop_completed_store_tasks() # Lookup 5 keys (3 stored + 2 missing) all_keys = stored_keys + [create_object_key(100), create_object_key(101)] lookup_tid = self.adapter.submit_lookup_and_lock_task(all_keys, _EMPTY_LAYOUT) assert wait_for_event_fd(lookup_fd) bitmap = self.adapter.query_lookup_and_lock_result(lookup_tid) for i in range(3): assert bitmap.test(i) is True, f"Stored key {i} should be found" assert bitmap.test(3) is False, "Missing key should not be found" assert bitmap.test(4) is False, "Missing key should not be found" self.adapter.submit_unlock(stored_keys) def test_factory_creates_adapter(self): """Verify the factory can create a RESP L2 adapter from config.""" # First Party from lmcache.v1.distributed.l2_adapters import create_l2_adapter from lmcache.v1.distributed.l2_adapters.resp_l2_adapter import ( RESPL2AdapterConfig, ) config = RESPL2AdapterConfig(host=REDIS_HOST, port=REDIS_PORT, num_workers=2) adapter = create_l2_adapter(config) try: # Should have valid event fds assert adapter.get_store_event_fd() >= 0 assert adapter.get_lookup_and_lock_event_fd() >= 0 assert adapter.get_load_event_fd() >= 0 finally: adapter.close()