# SPDX-License-Identifier: Apache-2.0 """ Tests for tensor parallel (TP) support in the multiprocess cache engine. This module tests the TP lookup mechanism where: - scheduler uses worker_id=None to lookup cache across all workers - workers use specific worker_id for store/retrieve operations - lookup requires ALL workers to have the cache for a hit Key scenarios tested: - TP=2 with both workers having all chunks cached - TP=2 with only one worker having cache (asymmetric) - TP=2 with different partial hits across workers - Various world sizes (TP=1, TP=2, TP=4, TP=8) """ # Standard import threading # 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.storage_manager import StorageManager from lmcache.v1.memory_management import MemoryFormat # ============================================================================== # Test Fixtures # ============================================================================== @pytest.fixture def storage_manager(): """Create a storage manager with 1GB buffer for testing.""" l1_memory_config = L1MemoryManagerConfig( size_in_bytes=1 << 30, use_lazy=True, init_size_in_bytes=1 << 30, ) storage_manager_config = StorageManagerConfig( l1_manager_config=L1ManagerConfig( memory_config=l1_memory_config, ), eviction_config=EvictionConfig( eviction_policy="LRU", ), ) manager = StorageManager(config=storage_manager_config) yield manager manager.close() @pytest.fixture def test_shape(): """Standard test shape for tensors.""" return torch.Size((2, 16, 16, 128)) @pytest.fixture def test_dtype(): """Standard test dtype for tensors.""" return torch.float16 @pytest.fixture def test_layout(test_shape, test_dtype): return MemoryLayoutDesc( shapes=[test_shape], dtypes=[test_dtype], ) @pytest.fixture def test_format(): """Standard test memory format.""" return MemoryFormat.KV_2LTD # ============================================================================== # Helper Functions # ============================================================================== def create_object_key( chunk_hash: int, worker_id: int, world_size: int = 2, model_name: str = "test_model", ) -> ObjectKey: """Create an ObjectKey for testing.""" kv_rank = ObjectKey.ComputeKVRank( world_size=world_size, global_rank=worker_id, local_world_size=world_size, local_rank=worker_id, ) return ObjectKey( chunk_hash=ObjectKey.IntHash2Bytes(chunk_hash), model_name=model_name, kv_rank=kv_rank, ) def create_interleaved_lookup_keys( num_chunks: int, world_size: int, model_name: str = "test_model", ) -> list[ObjectKey]: """ Create interleaved lookup keys for scheduler-style TP lookup. The order matches what the scheduler expects: [chunk0_worker0, chunk0_worker1, ..., chunk0_workerN, chunk1_worker0, chunk1_worker1, ..., chunk1_workerN, ...] This simulates the key expansion that happens for scheduler lookups where worker_id=None gets expanded to all workers. """ keys = [] for chunk_idx in range(num_chunks): for worker_id in range(world_size): keys.append( create_object_key( chunk_hash=chunk_idx, worker_id=worker_id, world_size=world_size, model_name=model_name, ) ) return keys # ============================================================================== # Tests for Storage Manager with TP Scenarios # ============================================================================== @pytest.mark.skipif( not torch.cuda.is_available(), reason="CUDA is required for tensor parallel tests", ) class TestStorageManagerTPLookup: """ Tests for storage manager lookup with tensor parallel scenarios. The key invariant: for a scheduler lookup (worker_id=None) to succeed, ALL workers must have the cache stored for that chunk. """ def test_tp2_both_workers_have_all_chunks(self, storage_manager, test_layout): """ Test TP=2 lookup when both workers have all chunks cached. Expected: All lookups return True. """ world_size = 2 num_chunks = 5 # Store chunks for both workers for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Create interleaved lookup keys for scheduler-style lookup lookup_keys = create_interleaved_lookup_keys(num_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # All keys should be found (5 chunks * 2 workers = 10) assert found_count == num_chunks * world_size # Simulating MPCacheServer.lookup logic found_ipc_count = found_count // world_size assert found_ipc_count == num_chunks def test_tp2_only_worker0_has_cache_asymmetric(self, storage_manager, test_layout): """ Test TP=2 lookup when only worker 0 has cache (asymmetric). Expected: Lookup returns 0 (no complete cache hit). """ world_size = 2 num_chunks = 5 # Store chunks for worker 0 only storage_keys = [ create_object_key(chunk_hash=i, worker_id=0, world_size=world_size) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write(storage_keys, test_layout, "new") storage_manager.finish_write(list(reserved_dict.keys())) # Create interleaved lookup keys for scheduler-style lookup lookup_keys = create_interleaved_lookup_keys(num_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # Only worker 0's first chunk is found, then lookup stops # at worker 1's missing chunk # The ordering is: [chunk0_worker0, chunk0_worker1, chunk1_worker0, ...] # So we find chunk0_worker0 (1), then miss chunk0_worker1 assert found_count == 1 # Simulating MPCacheServer.lookup logic found_ipc_count = found_count // world_size # 1 // 2 = 0, so no complete cache hit assert found_ipc_count == 0 def test_tp2_only_worker1_has_cache_asymmetric(self, storage_manager, test_layout): """ Test TP=2 lookup when only worker 1 has cache (asymmetric). Expected: Lookup returns 0 (first key for worker 0 is missing). """ world_size = 2 num_chunks = 5 # Store chunks for worker 1 only storage_keys = [ create_object_key(chunk_hash=i, worker_id=1, world_size=world_size) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write(storage_keys, test_layout, "new") storage_manager.finish_write(list(reserved_dict.keys())) # Create interleaved lookup keys for scheduler-style lookup lookup_keys = create_interleaved_lookup_keys(num_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # First lookup key is chunk0_worker0 which is missing assert found_count == 0 # Simulating MPCacheServer.lookup logic found_ipc_count = found_count // world_size assert found_ipc_count == 0 def test_tp2_partial_prefix_both_workers(self, storage_manager, test_layout): """ Test TP=2 lookup with partial prefix: both workers have first 3 chunks. Expected: First 3 chunks return True, rest return False. """ world_size = 2 num_stored_chunks = 3 num_requested_chunks = 5 # Store first 3 chunks for both workers for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_stored_chunks) ] reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Request 5 chunks with scheduler-style interleaved lookup lookup_keys = create_interleaved_lookup_keys(num_requested_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # First 3 chunks * 2 workers = 6 keys found, then stops at chunk3_worker0 assert found_count == num_stored_chunks * world_size # Simulating MPCacheServer.lookup logic found_ipc_count = found_count // world_size assert found_ipc_count == num_stored_chunks def test_tp2_different_partial_hits_min_common_prefix( self, storage_manager, test_layout ): """ Test TP=2 with different partial hits across workers. Worker 0: has chunks 0, 1, 2, 3, 4 (5 chunks) Worker 1: has chunks 0, 1 (2 chunks) Expected: Only first 2 chunks are counted (minimum common prefix). """ world_size = 2 # Worker 0 has 5 chunks storage_keys_w0 = [ create_object_key(chunk_hash=i, worker_id=0, world_size=world_size) for i in range(5) ] reserved_dict = storage_manager.reserve_write( storage_keys_w0, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Worker 1 has only 2 chunks storage_keys_w1 = [ create_object_key(chunk_hash=i, worker_id=1, world_size=world_size) for i in range(2) ] reserved_dict = storage_manager.reserve_write( storage_keys_w1, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Request 5 chunks with scheduler-style interleaved lookup lookup_keys = create_interleaved_lookup_keys(5, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # Lookup order: # chunk0_w0, chunk0_w1, chunk1_w0, chunk1_w1, chunk2_w0, chunk2_w1... # chunk0_w0: found (1) # chunk0_w1: found (2) # chunk1_w0: found (3) # chunk1_w1: found (4) # chunk2_w0: found (5) # chunk2_w1: NOT found (stops) assert found_count == 5 # 2 complete chunks * 2 workers + 1 partial # Simulating MPCacheServer.lookup logic found_ipc_count = found_count // world_size # 5 // 2 = 2, so only 2 complete chunks assert found_ipc_count == 2 def test_tp4_all_workers_have_cache(self, storage_manager, test_layout): """ Test TP=4 lookup when all 4 workers have all chunks cached. """ world_size = 4 num_chunks = 3 # Store chunks for all workers for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Scheduler-style interleaved lookup lookup_keys = create_interleaved_lookup_keys(num_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # All keys found: 3 chunks * 4 workers = 12 assert found_count == num_chunks * world_size found_ipc_count = found_count // world_size assert found_ipc_count == num_chunks def test_tp4_one_worker_missing_causes_no_hit(self, storage_manager, test_layout): """ Test TP=4 where one worker (worker 2) is missing all cache. Expected: No complete hits due to prefix matching. """ world_size = 4 num_chunks = 3 # Store chunks for workers 0, 1, 3 (skip worker 2) for worker_id in [0, 1, 3]: storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Scheduler-style interleaved lookup lookup_keys = create_interleaved_lookup_keys(num_chunks, world_size) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # Lookup order: chunk0_w0, chunk0_w1, chunk0_w2, chunk0_w3, ... # chunk0_w0: found (1) # chunk0_w1: found (2) # chunk0_w2: NOT found (stops) assert found_count == 2 found_ipc_count = found_count // world_size # 2 // 4 = 0, no complete chunks assert found_ipc_count == 0 # ============================================================================== # Tests for Store and Retrieve with TP # ============================================================================== @pytest.mark.skipif( not torch.cuda.is_available(), reason="CUDA is required for tensor parallel tests", ) class TestStorageManagerTPStoreRetrieve: """Tests for store and retrieve operations with tensor parallel.""" def test_tp2_store_creates_separate_keys(self, storage_manager, test_layout): """ Test that storing with different worker_ids creates separate entries. """ world_size = 2 # Store same chunk hash but different worker_ids key_w0 = create_object_key(chunk_hash=100, worker_id=0, world_size=world_size) key_w1 = create_object_key(chunk_hash=100, worker_id=1, world_size=world_size) # Store worker 0's data reserved_dict0 = storage_manager.reserve_write([key_w0], test_layout, "new") assert len(reserved_dict0) == 1 storage_manager.finish_write(list(reserved_dict0.keys())) # Store worker 1's data reserved_dict1 = storage_manager.reserve_write([key_w1], test_layout, "new") assert len(reserved_dict1) == 1 storage_manager.finish_write(list(reserved_dict1.keys())) # Prefetch to secure both entries handle = storage_manager.submit_prefetch_task([key_w0, key_w1], test_layout) _ = storage_manager.query_prefetch_status(handle) # Both should be retrievable independently with storage_manager.read_prefetched_results([key_w0]) as objs: assert len(objs) == 1 with storage_manager.read_prefetched_results([key_w1]) as objs: assert len(objs) == 1 def test_tp2_retrieve_specific_worker(self, storage_manager, test_layout): """ Test that retrieve with specific worker_id only gets that worker's data. """ world_size = 2 # Store for both workers all_keys = [] for worker_id in range(world_size): keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(3) ] reserved_dict = storage_manager.reserve_write(keys, test_layout, "new") storage_manager.finish_write(list(reserved_dict.keys())) all_keys.extend(keys) # Prefetch to secure all entries handle = storage_manager.submit_prefetch_task(all_keys, test_layout) _ = storage_manager.query_prefetch_status(handle) # Retrieve only worker 0's data keys_w0 = [ create_object_key(chunk_hash=i, worker_id=0, world_size=world_size) for i in range(3) ] with storage_manager.read_prefetched_results(keys_w0) as objs: assert len(objs) == 3 # Retrieve only worker 1's data keys_w1 = [ create_object_key(chunk_hash=i, worker_id=1, world_size=world_size) for i in range(3) ] with storage_manager.read_prefetched_results(keys_w1) as objs: assert len(objs) == 3 # ============================================================================== # Tests for Edge Cases # ============================================================================== @pytest.mark.skipif( not torch.cuda.is_available(), reason="CUDA is required for tensor parallel tests", ) class TestTPEdgeCases: """Edge case tests for tensor parallel support.""" def test_world_size_1_stores_and_retrieves(self, storage_manager, test_layout): """ Test that world_size=1 (no TP) works correctly through the API. Single worker stores and retrieves data successfully. """ world_size = 1 num_chunks = 3 # Store chunks for worker 0 storage_keys = [ create_object_key(chunk_hash=i, worker_id=0, world_size=world_size) for i in range(num_chunks) ] reserved_dict = storage_manager.reserve_write(storage_keys, test_layout, "new") storage_manager.finish_write(list(reserved_dict.keys())) # Lookup should find all chunks handle = storage_manager.submit_prefetch_task(storage_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() assert found_count == num_chunks # Retrieve should work with storage_manager.read_prefetched_results(storage_keys) as objs: assert len(objs) == num_chunks def test_large_world_size_tp8(self, storage_manager, test_layout): """ Test with larger world_size (TP=8) through the API. All 8 workers store and lookup works correctly. """ world_size = 8 num_chunks = 3 # Store chunks for all workers all_keys = [] for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] all_keys.extend(storage_keys) reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Create interleaved lookup keys (simulating scheduler lookup) # Order: [chunk0_w0, chunk0_w1, ..., chunk0_w7, chunk1_w0, ...] lookup_keys = [] for chunk_idx in range(num_chunks): for worker_id in range(world_size): lookup_keys.append( create_object_key( chunk_hash=chunk_idx, worker_id=worker_id, world_size=world_size ) ) # All keys should be found handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() assert found_count == num_chunks * world_size # Verify retrieval for each worker for worker_id in range(world_size): worker_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] with storage_manager.read_prefetched_results(worker_keys) as objs: assert len(objs) == num_chunks def test_all_workers_same_chunk_different_keys(self, storage_manager, test_layout): """ Test that same chunk_hash with different worker_ids creates distinct entries in storage and can be stored/retrieved independently. """ world_size = 4 chunk_hash = 42 # Create storage keys for all workers with same chunk_hash storage_keys = [ create_object_key(chunk_hash=chunk_hash, worker_id=i, world_size=world_size) for i in range(world_size) ] # All keys should be distinct assert len(set(storage_keys)) == world_size # Store all keys reserved_dict = storage_manager.reserve_write(storage_keys, test_layout, "new") assert len(reserved_dict) == world_size storage_manager.finish_write(list(reserved_dict.keys())) # Lookup all keys handle = storage_manager.submit_prefetch_task(storage_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() assert found_count == world_size # Retrieve each worker's key independently for worker_id in range(world_size): worker_key = create_object_key( chunk_hash=chunk_hash, worker_id=worker_id, world_size=world_size ) with storage_manager.read_prefetched_results([worker_key]) as objs: assert len(objs) == 1 assert objs[0] is not None # ============================================================================== # Integration Tests # ============================================================================== @pytest.mark.skipif( not torch.cuda.is_available(), reason="CUDA is required for tensor parallel tests", ) class TestTPIntegration: """Integration tests simulating real TP workflows.""" def test_full_tp2_workflow(self, storage_manager, test_layout): """ Simulate a full TP=2 workflow: 1. Worker 0 stores chunks 0, 1, 2 2. Worker 1 stores chunks 0, 1, 2 3. Scheduler looks up chunks 0, 1, 2, 3, 4 (interleaved for all workers) 4. Verify correct hit count 5. Workers retrieve their respective chunks """ world_size = 2 stored_chunks = 3 requested_chunks = 5 # Step 1 & 2: Workers store their chunks for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(stored_chunks) ] reserved_dict = storage_manager.reserve_write( storage_keys, test_layout, "new" ) storage_manager.finish_write(list(reserved_dict.keys())) # Step 3: Scheduler lookup with interleaved keys # Order: [chunk0_w0, chunk0_w1, chunk1_w0, chunk1_w1, ...] lookup_keys = [] for chunk_idx in range(requested_chunks): for worker_id in range(world_size): lookup_keys.append( create_object_key( chunk_hash=chunk_idx, worker_id=worker_id, world_size=world_size, ) ) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() # Step 4: Verify hit count # First 3 chunks * 2 workers = 6 keys found, then stops at chunk3_worker0 assert found_count == stored_chunks * world_size # Compute number of complete IPC-level hits found_ipc_count = found_count // world_size assert found_ipc_count == stored_chunks # Step 5: Workers retrieve their chunks for worker_id in range(world_size): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(stored_chunks) ] with storage_manager.read_prefetched_results(storage_keys) as objs: assert len(objs) == stored_chunks for obj in objs: assert obj is not None def test_concurrent_tp2_stores(self, storage_manager, test_layout): """ Test concurrent stores from multiple "workers" (threads). """ world_size = 2 num_chunks = 10 results = {} def worker_store(worker_id: int): storage_keys = [ create_object_key( chunk_hash=i, worker_id=worker_id, world_size=world_size ) for i in range(num_chunks) ] reserved = storage_manager.reserve_write(storage_keys, test_layout, "new") storage_manager.finish_write(list(reserved.keys())) results[worker_id] = len(reserved) # Run stores concurrently threads = [] for worker_id in range(world_size): t = threading.Thread(target=worker_store, args=(worker_id,)) threads.append(t) t.start() for t in threads: t.join() # Verify both workers stored their chunks assert results[0] == num_chunks assert results[1] == num_chunks # Verify lookup works with interleaved keys lookup_keys = [] for chunk_idx in range(num_chunks): for worker_id in range(world_size): lookup_keys.append( create_object_key( chunk_hash=chunk_idx, worker_id=worker_id, world_size=world_size, ) ) handle = storage_manager.submit_prefetch_task(lookup_keys, test_layout) found_count = storage_manager.query_prefetch_status(handle).count_leading_ones() assert found_count == num_chunks * world_size