735 lines
26 KiB
Python
735 lines
26 KiB
Python
# 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
|