754 lines
23 KiB
Python
754 lines
23 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
# Standard
|
|
from typing import Generator
|
|
import multiprocessing as mp
|
|
import os
|
|
import time
|
|
|
|
# Third Party
|
|
import pytest
|
|
import torch
|
|
import zmq
|
|
|
|
# First Party
|
|
from lmcache.utils import EngineType
|
|
from lmcache.v1.distributed.config import (
|
|
EvictionConfig,
|
|
L1ManagerConfig,
|
|
L1MemoryManagerConfig,
|
|
StorageManagerConfig,
|
|
)
|
|
from lmcache.v1.mp_observability.config import DEFAULT_OBSERVABILITY_CONFIG
|
|
from lmcache.v1.multiprocess.config import MPServerConfig
|
|
from lmcache.v1.multiprocess.custom_types import (
|
|
IPCCacheServerKey,
|
|
KVCache,
|
|
)
|
|
from lmcache.v1.multiprocess.mq import MessageQueueClient
|
|
from lmcache.v1.multiprocess.protocol import (
|
|
RequestType,
|
|
get_response_class,
|
|
)
|
|
from lmcache.v1.multiprocess.server import run_cache_server
|
|
from lmcache.v1.platform.cuda.ipc_wrapper import CudaIPCWrapper
|
|
|
|
# Configuration constants
|
|
SERVER_HOST = "localhost"
|
|
SERVER_PORT = 5599
|
|
SERVER_URL = f"tcp://{SERVER_HOST}:{SERVER_PORT}"
|
|
CHUNK_SIZE = 256
|
|
CPU_BUFFER_SIZE = 5.0
|
|
DEFAULT_TIMEOUT = 20.0
|
|
|
|
|
|
def _has_working_new_shared_cuda() -> bool:
|
|
if not torch.cuda.is_available():
|
|
print("CUDA is not available, skipping tests that require new_shared_cuda")
|
|
return False
|
|
try:
|
|
# Minimal sanity check — adapt to your real API
|
|
buf = torch.empty(1024, device="cuda")
|
|
shared = buf.untyped_storage()._share_cuda_() # or your exact call
|
|
return shared is not None
|
|
except Exception:
|
|
return False
|
|
|
|
|
|
if not _has_working_new_shared_cuda():
|
|
pytest.skip(
|
|
"new_shared_cuda is not available or not working on this system",
|
|
allow_module_level=True,
|
|
)
|
|
|
|
|
|
def initialize_kv_cache(
|
|
device: torch.device,
|
|
num_pages: int = 1024,
|
|
num_layers: int = 32,
|
|
page_size: int = 16,
|
|
num_heads: int = 8,
|
|
head_size: int = 128,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
) -> list[torch.Tensor]:
|
|
"""
|
|
Initialize KV cache tensors on GPU for testing.
|
|
"""
|
|
torch.random.manual_seed(42)
|
|
|
|
gpu_tensors = [
|
|
torch.rand(
|
|
(2, num_pages, page_size, num_heads, head_size),
|
|
dtype=dtype,
|
|
device=device,
|
|
)
|
|
for _ in range(num_layers)
|
|
]
|
|
|
|
return gpu_tensors
|
|
|
|
|
|
class ClientContext:
|
|
"""
|
|
Client context that manages GPU KV cache tensors.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
device: torch.device,
|
|
num_pages: int = 1024,
|
|
num_layers: int = 32,
|
|
page_size: int = 16,
|
|
num_heads: int = 8,
|
|
head_size: int = 128,
|
|
dtype: torch.dtype = torch.bfloat16,
|
|
):
|
|
self.device = device
|
|
self.num_pages = num_pages
|
|
self.num_layers = num_layers
|
|
self.page_size = page_size
|
|
self.num_heads = num_heads
|
|
self.head_size = head_size
|
|
self.dtype = dtype
|
|
|
|
self.gpu_kv_caches = initialize_kv_cache(
|
|
device, num_pages, num_layers, page_size, num_heads, head_size, dtype
|
|
)
|
|
|
|
def get_kv_cache(self) -> KVCache:
|
|
"""
|
|
Wrap GPU tensors in CudaIPCWrapper for IPC communication.
|
|
"""
|
|
return [CudaIPCWrapper(tensor) for tensor in self.gpu_kv_caches]
|
|
|
|
def get_tensor_slice(
|
|
self, layer: int, start_page: int, num_pages: int
|
|
) -> torch.Tensor:
|
|
"""
|
|
Get a slice of the KV cache tensor for a specific layer.
|
|
"""
|
|
return self.gpu_kv_caches[layer][:, start_page : start_page + num_pages]
|
|
|
|
|
|
def create_cache_key(index: int, model: str = "testmodel") -> IPCCacheServerKey:
|
|
"""
|
|
Create a cache key for testing.
|
|
"""
|
|
global CHUNK_SIZE
|
|
token_ids = [index] * CHUNK_SIZE
|
|
return IPCCacheServerKey.from_token_ids(
|
|
model,
|
|
1,
|
|
0,
|
|
token_ids,
|
|
start=0,
|
|
end=CHUNK_SIZE,
|
|
request_id=f"test_request_{index}",
|
|
)
|
|
|
|
|
|
BLOCKS_PER_KEY = 16
|
|
|
|
|
|
def lookup_all(
|
|
client: MessageQueueClient,
|
|
keys: list[IPCCacheServerKey],
|
|
timeout: float = DEFAULT_TIMEOUT,
|
|
) -> int:
|
|
"""Lookup all keys individually and return total found count.
|
|
|
|
Uses the two-phase lookup protocol: LOOKUP registers the job server-side,
|
|
then QUERY_PREFETCH_STATUS is polled by request_id until the result is ready.
|
|
"""
|
|
total = 0
|
|
for key in keys:
|
|
lookup_key = key.no_worker_id_version()
|
|
# Phase 1: Submit lookup (server tracks by request_id, returns None)
|
|
client.submit_request(
|
|
RequestType.LOOKUP,
|
|
[lookup_key, 1],
|
|
get_response_class(RequestType.LOOKUP),
|
|
).result(timeout=timeout)
|
|
# Phase 2: Poll by request_id until done
|
|
while True:
|
|
result = client.submit_request(
|
|
RequestType.QUERY_PREFETCH_STATUS,
|
|
[lookup_key.request_id],
|
|
get_response_class(RequestType.QUERY_PREFETCH_STATUS),
|
|
).result(timeout=timeout)
|
|
if result is not None:
|
|
total += result
|
|
break
|
|
return total
|
|
|
|
|
|
def store_keys(
|
|
client: MessageQueueClient,
|
|
keys: list[IPCCacheServerKey],
|
|
instance_id: int,
|
|
gpu_block_ids: list[int],
|
|
event: torch.cuda.Event,
|
|
timeout: float = DEFAULT_TIMEOUT,
|
|
) -> None:
|
|
"""Store keys one at a time using the single-key API."""
|
|
for i, key in enumerate(keys):
|
|
start = i * BLOCKS_PER_KEY
|
|
end = start + BLOCKS_PER_KEY
|
|
block_ids = gpu_block_ids[start:end]
|
|
future = client.submit_request(
|
|
RequestType.STORE,
|
|
[key, instance_id, [block_ids], event.ipc_handle()],
|
|
get_response_class(RequestType.STORE),
|
|
)
|
|
result = future.to_cuda_future().result(timeout=timeout)
|
|
assert result is True, f"Store should succeed for key {i}"
|
|
|
|
|
|
def retrieve_keys(
|
|
client: MessageQueueClient,
|
|
keys: list[IPCCacheServerKey],
|
|
instance_id: int,
|
|
gpu_block_ids: list[int],
|
|
event: torch.cuda.Event,
|
|
timeout: float = DEFAULT_TIMEOUT,
|
|
) -> list[bool]:
|
|
"""Retrieve keys one at a time using the single-key API."""
|
|
results = []
|
|
for i, key in enumerate(keys):
|
|
start = i * BLOCKS_PER_KEY
|
|
end = start + BLOCKS_PER_KEY
|
|
block_ids = gpu_block_ids[start:end]
|
|
future = client.submit_request(
|
|
RequestType.RETRIEVE,
|
|
[key, instance_id, [block_ids], event.ipc_handle(), 0],
|
|
get_response_class(RequestType.RETRIEVE),
|
|
)
|
|
result = future.to_cuda_future().result(timeout=timeout)
|
|
results.append(result)
|
|
return results
|
|
|
|
|
|
def server_process_runner(
|
|
host: str, port: int, chunk_size: int, cpu_buffer_size: float
|
|
):
|
|
"""
|
|
Entry point for the server process.
|
|
"""
|
|
mp_config = MPServerConfig(host=host, port=port, chunk_size=chunk_size)
|
|
storage_manager_config = StorageManagerConfig(
|
|
l1_manager_config=L1ManagerConfig(
|
|
memory_config=L1MemoryManagerConfig(
|
|
size_in_bytes=int(cpu_buffer_size * 1024**3),
|
|
use_lazy=True,
|
|
),
|
|
),
|
|
eviction_config=EvictionConfig(eviction_policy="LRU"),
|
|
)
|
|
run_cache_server(
|
|
mp_config=mp_config,
|
|
storage_manager_config=storage_manager_config,
|
|
obs_config=DEFAULT_OBSERVABILITY_CONFIG,
|
|
)
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def server_process() -> Generator[mp.Process, None, None]:
|
|
"""
|
|
Fixture that starts the cache server in a separate process.
|
|
The server runs for the entire test module.
|
|
"""
|
|
# Start server process
|
|
mp.set_start_method("spawn", force=True)
|
|
process = mp.Process(
|
|
target=server_process_runner,
|
|
args=(SERVER_HOST, SERVER_PORT, CHUNK_SIZE, CPU_BUFFER_SIZE),
|
|
daemon=True,
|
|
)
|
|
process.start()
|
|
|
|
# Wait for server to initialize
|
|
time.sleep(2)
|
|
|
|
yield process
|
|
|
|
# Cleanup: terminate the server process
|
|
if process.is_alive():
|
|
process.terminate()
|
|
process.join(timeout=5)
|
|
if process.is_alive():
|
|
process.kill()
|
|
process.join()
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def zmq_context() -> Generator[zmq.Context, None, None]:
|
|
"""
|
|
Fixture that provides a ZMQ context for the test module.
|
|
"""
|
|
context = zmq.Context.instance()
|
|
yield context
|
|
# Context cleanup is handled by ZMQ
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def client(
|
|
server_process: mp.Process, zmq_context: zmq.Context
|
|
) -> Generator[MessageQueueClient, None, None]:
|
|
"""
|
|
Fixture that provides a message queue client for each test function.
|
|
"""
|
|
client = MessageQueueClient(server_url=SERVER_URL, context=zmq_context)
|
|
yield client
|
|
# Client cleanup
|
|
client.close()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def client_context() -> Generator[ClientContext, None, None]:
|
|
"""
|
|
Fixture that provides a client context with initialized KV cache.
|
|
"""
|
|
if not torch.cuda.is_available():
|
|
pytest.skip("CUDA is not available")
|
|
|
|
device = torch.device("cuda:0")
|
|
ctx = ClientContext(device=device)
|
|
yield ctx
|
|
|
|
# Cleanup GPU memory
|
|
del ctx.gpu_kv_caches
|
|
torch.cuda.empty_cache()
|
|
|
|
|
|
@pytest.fixture(scope="function")
|
|
def registered_instance(
|
|
client: MessageQueueClient, client_context: ClientContext
|
|
) -> Generator[int, None, None]:
|
|
"""
|
|
Fixture that registers a KV cache instance and returns the instance ID.
|
|
Automatically unregisters after the test.
|
|
"""
|
|
instance_id = os.getpid()
|
|
|
|
# Register KV cache. No engine group infos are sent, so the server
|
|
# detects ``slots_per_block`` from the tensors and treats every group
|
|
# as uncompressed (``compress_ratio == 1``).
|
|
future = client.submit_request(
|
|
RequestType.REGISTER_KV_CACHE,
|
|
[
|
|
instance_id,
|
|
client_context.get_kv_cache(),
|
|
"testmodel",
|
|
1,
|
|
EngineType.VLLM,
|
|
{},
|
|
[],
|
|
],
|
|
get_response_class(RequestType.REGISTER_KV_CACHE),
|
|
)
|
|
result = future.result(timeout=DEFAULT_TIMEOUT)
|
|
assert result is None, "Register should return None"
|
|
|
|
yield instance_id
|
|
|
|
# Unregister KV cache
|
|
try:
|
|
client.submit_request(
|
|
RequestType.CLEAR, [], get_response_class(RequestType.CLEAR)
|
|
).result(timeout=DEFAULT_TIMEOUT)
|
|
future = client.submit_request(
|
|
RequestType.UNREGISTER_KV_CACHE,
|
|
[instance_id],
|
|
get_response_class(RequestType.UNREGISTER_KV_CACHE),
|
|
)
|
|
future.result(timeout=DEFAULT_TIMEOUT)
|
|
except Exception as e:
|
|
print(f"Error during unregister: {e}")
|
|
|
|
|
|
# ============================================================================
|
|
# Test Functions
|
|
# ============================================================================
|
|
|
|
|
|
def test_server_running(server_process: mp.Process):
|
|
"""
|
|
Test that the server process is running.
|
|
"""
|
|
assert server_process.is_alive(), "Server process should be running"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Register/Unregister KV cache requires CUDA",
|
|
)
|
|
def test_register_unregister_kv_cache(
|
|
client: MessageQueueClient, client_context: ClientContext
|
|
):
|
|
"""
|
|
Test registering and unregistering a KV cache.
|
|
"""
|
|
instance_id = os.getpid()
|
|
|
|
# Register. No engine group infos: geometry is detected from the
|
|
# tensors (uncompressed).
|
|
future = client.submit_request(
|
|
RequestType.REGISTER_KV_CACHE,
|
|
[
|
|
instance_id,
|
|
client_context.get_kv_cache(),
|
|
"testmodel",
|
|
1,
|
|
EngineType.VLLM,
|
|
{},
|
|
[],
|
|
],
|
|
get_response_class(RequestType.REGISTER_KV_CACHE),
|
|
)
|
|
result = future.result(timeout=DEFAULT_TIMEOUT)
|
|
assert result is None
|
|
|
|
# Unregister
|
|
future = client.submit_request(
|
|
RequestType.UNREGISTER_KV_CACHE,
|
|
[instance_id],
|
|
get_response_class(RequestType.UNREGISTER_KV_CACHE),
|
|
)
|
|
result = future.result(timeout=DEFAULT_TIMEOUT)
|
|
assert result is None
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Store and Lookup require CUDA",
|
|
)
|
|
def test_store_and_lookup(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""
|
|
Test storing KV cache entries and looking them up.
|
|
"""
|
|
num_keys = 10
|
|
keys = [create_cache_key(i) for i in range(num_keys)]
|
|
gpu_block_ids = list(range(0, 16 * num_keys))
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
# Store
|
|
store_keys(client, keys, registered_instance, gpu_block_ids, event)
|
|
|
|
# Lookup - keys that exist
|
|
lookup_result = lookup_all(client, keys)
|
|
assert lookup_result == num_keys, "All stored keys should exist"
|
|
|
|
# Lookup - keys that don't exist
|
|
non_existent_keys = [create_cache_key(i + 1000) for i in range(5)]
|
|
lookup_result2 = lookup_all(client, non_existent_keys)
|
|
assert lookup_result2 == 0, "Non-existent keys should not be found"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Store requires CUDA",
|
|
)
|
|
def test_store_fails_closed_on_incomplete_block_ids(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""An under-length block-id list skips the whole store (fail-closed).
|
|
|
|
Regression guard for the all-or-nothing store contract: a ``gpu_block_ids``
|
|
list too short to fully cover a chunk (e.g. a caller/protocol bug) must skip
|
|
the store entirely (returning ``False``) and commit nothing — the previous
|
|
fail-open path raised internally but then ``finish_write``-committed the
|
|
reservation anyway, turning the key into a retrievable garbage entry (lookup
|
|
would find it).
|
|
|
|
The committed-state assertion is on a *miss* (lookup == 0), which is robust
|
|
to this harness's known store->lookup race (that race can only turn a true
|
|
hit into a miss, never the reverse).
|
|
"""
|
|
# One-chunk key (256 tokens == BLOCKS_PER_KEY blocks) but only half the
|
|
# block IDs needed, so the chunk is not fully covered.
|
|
key = create_cache_key(90001)
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
result = (
|
|
client.submit_request(
|
|
RequestType.STORE,
|
|
[
|
|
key,
|
|
registered_instance,
|
|
[list(range(BLOCKS_PER_KEY // 2))],
|
|
event.ipc_handle(),
|
|
],
|
|
get_response_class(RequestType.STORE),
|
|
)
|
|
.to_cuda_future()
|
|
.result(timeout=DEFAULT_TIMEOUT)
|
|
)
|
|
assert result is False, "Store should fail closed (skip) on a short list"
|
|
assert lookup_all(client, [key]) == 0, "An uncovered chunk must not be committed"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Store, Retrieve, and Verify require CUDA",
|
|
)
|
|
def test_store_retrieve_verify(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""
|
|
Test storing and retrieving KV cache entries, verifying correctness.
|
|
"""
|
|
num_keys = 20
|
|
keys = [create_cache_key(i) for i in range(num_keys)]
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
# Store at the beginning of the cache
|
|
store_block_ids = list(range(0, 16 * num_keys))
|
|
store_keys(client, keys, registered_instance, store_block_ids, event)
|
|
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
# Call look up to ensure the data is ready to be retrieved
|
|
lookup_result = lookup_all(client, keys)
|
|
assert lookup_result == num_keys
|
|
|
|
# Retrieve to a different location in the cache
|
|
# Use offset of 40 blocks (640 pages total needed: 320 + 320)
|
|
retrieve_offset = 40 * 16
|
|
retrieve_block_ids = list(range(retrieve_offset, retrieve_offset + 16 * num_keys))
|
|
retrieve_result = retrieve_keys(
|
|
client, keys, registered_instance, retrieve_block_ids, event
|
|
)
|
|
|
|
assert len(retrieve_result) == num_keys
|
|
assert all(retrieve_result), "All keys should be retrieved successfully"
|
|
|
|
# Verify correctness by comparing tensors
|
|
for i in range(num_keys):
|
|
for layer in range(client_context.num_layers):
|
|
original_block = i * 16
|
|
retrieved_block = retrieve_offset + i * 16
|
|
|
|
original_tensor = client_context.gpu_kv_caches[layer][
|
|
:, original_block : original_block + 16
|
|
]
|
|
retrieved_tensor = client_context.gpu_kv_caches[layer][
|
|
:, retrieved_block : retrieved_block + 16
|
|
]
|
|
|
|
assert torch.allclose(original_tensor, retrieved_tensor, atol=1e-4), (
|
|
f"Mismatch for key {i}, layer {layer}"
|
|
)
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Partial miss retrieval requires CUDA",
|
|
)
|
|
def test_retrieve_partial_miss(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""
|
|
Test retrieving when some keys exist and some don't.
|
|
The retrieve should return ALL FALSE if any key is missing.
|
|
"""
|
|
# Store first 30 keys (480 pages)
|
|
num_stored = 30
|
|
stored_keys = [create_cache_key(i) for i in range(num_stored)]
|
|
store_block_ids = list(range(0, 16 * num_stored))
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
store_keys(client, stored_keys, registered_instance, store_block_ids, event)
|
|
|
|
# Lookup to ensure keys are stored
|
|
lookup_result = lookup_all(client, stored_keys)
|
|
assert lookup_result == num_stored
|
|
|
|
# Try to retrieve 60 keys (only first 30 exist)
|
|
# Total pages needed: 60 * 16 = 960 (< 1024)
|
|
num_requested = 60
|
|
all_keys = [create_cache_key(i) for i in range(num_requested)]
|
|
# Start retrieve at offset 2 keys (32 pages)
|
|
retrieve_offset_keys = 2
|
|
retrieve_block_ids = list(
|
|
range(retrieve_offset_keys * 16, (retrieve_offset_keys + num_requested) * 16)
|
|
)
|
|
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
|
|
retrieve_result = retrieve_keys(
|
|
client, all_keys, registered_instance, retrieve_block_ids, event
|
|
)
|
|
|
|
assert len(retrieve_result) == num_requested
|
|
# First 30 keys exist, remaining 30 don't
|
|
assert all(retrieve_result[:num_stored]), "Stored keys should be retrieved"
|
|
assert not any(retrieve_result[num_stored:]), (
|
|
"Non-existent keys should fail to retrieve"
|
|
)
|
|
|
|
# Doing look up again to ensure data is ready
|
|
lookup_result_2 = lookup_all(client, stored_keys)
|
|
assert lookup_result_2 == num_stored
|
|
|
|
# Try to retrieve the first 30 keys only (all exist)
|
|
retrieve_block_ids_2 = list(range(0, 16 * num_stored))
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
retrieve_result_2 = retrieve_keys(
|
|
client, stored_keys, registered_instance, retrieve_block_ids_2, event
|
|
)
|
|
assert len(retrieve_result_2) == num_stored
|
|
assert all(retrieve_result_2), "All stored keys should be retrieved successfully"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Multiple retrieve operations require CUDA",
|
|
)
|
|
def test_multiple_retrieve_operations(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""
|
|
Test multiple retrieve operations:
|
|
Store 8,8,8,8 keys and then retrieve 8,8,8,8 keys in sequence.
|
|
"""
|
|
num_batches = 4
|
|
keys_per_batch = 8
|
|
pages_per_key = 16
|
|
|
|
# Initialize the values in GPU KV cache
|
|
for layer in range(client_context.num_layers):
|
|
layer_cache = client_context.gpu_kv_caches[layer]
|
|
for i in range(num_batches):
|
|
start_page = (i * keys_per_batch) * pages_per_key
|
|
end_page = start_page + (keys_per_batch * pages_per_key)
|
|
layer_cache[:, start_page:end_page] = (i + 1) / num_batches
|
|
|
|
# Store in batches
|
|
for batch_idx in range(num_batches):
|
|
keys = [
|
|
create_cache_key(batch_idx * keys_per_batch + i)
|
|
for i in range(keys_per_batch)
|
|
]
|
|
blocks = list(
|
|
range(
|
|
(batch_idx * keys_per_batch) * 16,
|
|
(batch_idx * keys_per_batch + keys_per_batch) * 16,
|
|
)
|
|
)
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
store_keys(client, keys, registered_instance, blocks, event)
|
|
|
|
# Doing look up to ensure data is ready to be retrieved
|
|
all_keys = [
|
|
create_cache_key(batch_idx * keys_per_batch + i)
|
|
for batch_idx in range(num_batches)
|
|
for i in range(keys_per_batch)
|
|
]
|
|
lookup_result = lookup_all(client, all_keys)
|
|
assert lookup_result == num_batches * keys_per_batch, "All stored keys should exist"
|
|
|
|
# Retrieve in batches
|
|
retrieve_offset = 32 # Start retrieving at offset of 32 chunks
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
for batch_idx in range(num_batches):
|
|
keys = [
|
|
create_cache_key(batch_idx * keys_per_batch + i)
|
|
for i in range(keys_per_batch)
|
|
]
|
|
blocks = list(
|
|
range(
|
|
(batch_idx * keys_per_batch + retrieve_offset) * pages_per_key,
|
|
(batch_idx * keys_per_batch + retrieve_offset + keys_per_batch)
|
|
* pages_per_key,
|
|
)
|
|
)
|
|
|
|
retrieve_result = retrieve_keys(
|
|
client, keys, registered_instance, blocks, event
|
|
)
|
|
assert len(retrieve_result) == keys_per_batch
|
|
assert all(retrieve_result), "All keys should be retrieved successfully"
|
|
|
|
# Verify correctness
|
|
for layer in range(client_context.num_layers):
|
|
layer_cache = client_context.gpu_kv_caches[layer]
|
|
for batch_idx in range(num_batches):
|
|
start_page = (retrieve_offset + batch_idx * keys_per_batch) * pages_per_key
|
|
end_page = start_page + (keys_per_batch * pages_per_key)
|
|
retrieved_tensor = layer_cache[:, start_page:end_page]
|
|
expected_value = (batch_idx + 1) / num_batches
|
|
assert torch.allclose(
|
|
retrieved_tensor,
|
|
torch.full_like(retrieved_tensor, expected_value),
|
|
), f"Mismatch in batch {batch_idx}, layer {layer}"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(),
|
|
reason="Multiple store operations require CUDA",
|
|
)
|
|
def test_multiple_store_operations(
|
|
client: MessageQueueClient,
|
|
client_context: ClientContext,
|
|
registered_instance: int,
|
|
):
|
|
"""
|
|
Test multiple store operations in sequence.
|
|
"""
|
|
# Store batch 1
|
|
keys1 = [create_cache_key(i) for i in range(30)]
|
|
blocks1 = list(range(0, 16 * 30))
|
|
event = torch.cuda.Event(interprocess=True)
|
|
event.record()
|
|
store_keys(client, keys1, registered_instance, blocks1, event)
|
|
|
|
# Store batch 2
|
|
keys2 = [create_cache_key(i + 30) for i in range(20)]
|
|
blocks2 = list(range(30 * 16, 50 * 16))
|
|
|
|
# Test with the same event for 2 store requests
|
|
store_keys(client, keys2, registered_instance, blocks2, event)
|
|
|
|
# Verify all keys exist
|
|
all_keys = keys1 + keys2
|
|
lookup_result = lookup_all(client, all_keys)
|
|
assert lookup_result == 50, "All stored keys from both batches should exist"
|
|
|
|
|
|
@pytest.mark.skipif(
|
|
not torch.cuda.is_available(), reason="Get chunk size requires CUDA"
|
|
)
|
|
def test_get_chunk_size(
|
|
client: MessageQueueClient,
|
|
):
|
|
"""
|
|
Test retrieving the chunk size from the server.
|
|
"""
|
|
chunk_size = client.submit_request(
|
|
RequestType.GET_CHUNK_SIZE,
|
|
[],
|
|
get_response_class(RequestType.GET_CHUNK_SIZE),
|
|
).result(timeout=DEFAULT_TIMEOUT)
|
|
|
|
assert chunk_size == CHUNK_SIZE, f"Chunk size should be {CHUNK_SIZE}"
|