Files
2026-07-13 12:24:33 +08:00

282 lines
8.5 KiB
Python

# SPDX-License-Identifier: Apache-2.0
# Copyright (c) 2026 Samsung Electronics Co., Ltd. All Rights Reserved
# Standard
import asyncio
import threading
# Third Party
from google.cloud.bigtable import Client
import pytest
import torch
# First Party
from lmcache.utils import CacheEngineKey
from lmcache.v1.config import LMCacheEngineConfig
from lmcache.v1.memory_allocators.mixed_memory_allocator import MixedMemoryAllocator
from lmcache.v1.metadata import LMCacheMetadata
from lmcache.v1.storage_backend.local_cpu_backend import LocalCPUBackend
from lmcache.v1.storage_backend.remote_backend import RemoteBackend
from tests.v1.utils import create_test_memory_obj
# Simple test helpers
def create_test_metadata(kv_shape=(2, 2, 256, 8, 128)):
return LMCacheMetadata(
model_name="test_model",
world_size=1,
local_world_size=1,
worker_id=0,
local_worker_id=0,
kv_dtype=torch.bfloat16,
kv_shape=kv_shape,
use_mla=False,
role="worker",
)
def create_test_config(extra_overrides=None):
extras = {
"bigtable_project_id": "test-project",
"bigtable_instance_id": "test-instance",
"bigtable_table_name": "test-table",
"bigtable_family_name": "cf",
"bigtable_column_name": "data",
"bigtable_max_chunk_size_mb": 5.0, # 5MB threshold for testing skip logic
"bigtable_max_retries": 2,
}
if extra_overrides:
extras.update(extra_overrides)
return LMCacheEngineConfig.from_defaults(
chunk_size=256,
remote_storage_plugins=["bigtable"],
remote_serde="naive",
lmcache_instance_id="test_instance",
extra_config=extras,
)
@pytest.fixture
def async_loop():
loop = asyncio.new_event_loop()
# Standard
import threading
# First Party
from lmcache.utils import start_loop_in_thread_with_exceptions
thread = threading.Thread(
target=start_loop_in_thread_with_exceptions,
args=(loop,),
name="test-async-loop",
)
thread.start()
yield loop
loop.call_soon_threadsafe(loop.stop)
thread.join(timeout=5.0)
@pytest.mark.integration
class TestBigtableEmulatorIntegration:
@pytest.fixture(autouse=True)
def setup_emulator_table(self, bigtable_emulator):
"""Prepare instance, table, and column family in the emulator."""
project_id = "test-project"
instance_id = "test-instance"
table_name = "test-table"
family_name = "cf"
# Initialize sync admin client using the emulator host
client = Client(project=project_id, admin=True)
instance = client.instance(instance_id)
table = instance.table(table_name)
try:
if table.exists():
table.delete()
except Exception:
pass
table.create()
cf = table.column_family(family_name)
cf.create()
yield
# Clean up table after each test
try:
if table.exists():
table.delete()
except Exception:
pass
@pytest.fixture
def memory_allocator(self):
alloc = MixedMemoryAllocator(100 * 1024 * 1024) # 100MB
yield alloc
alloc.close()
@pytest.fixture
def local_cpu_backend(self, memory_allocator):
config = LMCacheEngineConfig.from_defaults(chunk_size=256)
metadata = create_test_metadata()
backend = LocalCPUBackend(config, metadata, memory_allocator=memory_allocator)
yield backend
backend.close()
def test_integration_put_and_get(self, async_loop, local_cpu_backend):
"""Verify standard put and get of chunk bytes with emulator."""
config = create_test_config()
metadata = create_test_metadata()
backend = RemoteBackend(
config=config,
metadata=metadata,
loop=async_loop,
local_cpu_backend=local_cpu_backend,
dst_device="cpu",
plugin_name="bigtable",
)
key = CacheEngineKey("test_model", 0, 0, 256, torch.bfloat16)
# Use test helper to create concrete MemoryObj and fill it with values
memory_obj = create_test_memory_obj(
shape=torch.Size([2, 2, 256, 8, 128]), dtype=torch.bfloat16
)
memory_obj.tensor.fill_(3.14)
# Write asynchronously and wait for future
fut = backend.submit_put_task(key, memory_obj)
fut.result(timeout=5.0)
# Query contains
assert backend.contains(key)
# Read back using get_blocking (allocates automatically)
retrieved_obj = backend.get_blocking(key)
assert retrieved_obj is not None
assert torch.all(retrieved_obj.tensor == 3.14)
backend.close()
def test_integration_batched_put_and_get(self, async_loop, local_cpu_backend):
"""Verify dynamic batching and batched operations with emulator."""
config = create_test_config()
metadata = create_test_metadata()
backend = RemoteBackend(
config=config,
metadata=metadata,
loop=async_loop,
local_cpu_backend=local_cpu_backend,
dst_device="cpu",
plugin_name="bigtable",
)
keys = [
CacheEngineKey("test_model", 0, i, 256, torch.bfloat16) for i in range(5)
]
memory_objs = []
for i in range(5):
memory_obj = create_test_memory_obj(
shape=torch.Size([2, 2, 256, 8, 128]), dtype=torch.bfloat16
)
memory_obj.tensor.fill_(float(i))
memory_objs.append(memory_obj)
# Use threading events to wait for batched write completion
done_events = [threading.Event() for _ in range(5)]
def on_complete(key):
idx = keys.index(key)
done_events[idx].set()
# Batched Put
backend.batched_submit_put_task(
keys, memory_objs, on_complete_callback=on_complete
)
# Wait for all writes to finish
for ev in done_events:
assert ev.wait(timeout=10.0)
# Assert all keys are in backend
for key in keys:
assert backend.contains(key)
# Read all back using batched_get_blocking
retrieved_objs = backend.batched_get_blocking(keys)
assert len(retrieved_objs) == 5
for i in range(5):
assert retrieved_objs[i] is not None
assert torch.all(retrieved_objs[i].tensor == float(i))
backend.close()
def test_integration_remove(self, async_loop, local_cpu_backend):
"""Verify deleting chunks from emulator works."""
config = create_test_config()
metadata = create_test_metadata()
backend = RemoteBackend(
config=config,
metadata=metadata,
loop=async_loop,
local_cpu_backend=local_cpu_backend,
dst_device="cpu",
plugin_name="bigtable",
)
key = CacheEngineKey("test_model", 0, 10, 256, torch.bfloat16)
memory_obj = create_test_memory_obj(
shape=torch.Size([2, 2, 256, 8, 128]), dtype=torch.bfloat16
)
memory_obj.tensor.fill_(1.0)
fut = backend.submit_put_task(key, memory_obj)
fut.result(timeout=5.0)
assert backend.contains(key)
# Remove
assert backend.remove(key)
assert not backend.contains(key)
backend.close()
def test_integration_skips_large_writes(self, async_loop, local_cpu_backend):
"""Verify writing a chunk larger than max_chunk_size_mb is skipped
without failure.
"""
config = create_test_config() # Max size is 5.0 MB
metadata = create_test_metadata()
backend = RemoteBackend(
config=config,
metadata=metadata,
loop=async_loop,
local_cpu_backend=local_cpu_backend,
dst_device="cpu",
plugin_name="bigtable",
)
key = CacheEngineKey("test_model", 0, 99, 256, torch.bfloat16)
# Create a payload of ~8.38 MB (exceeds the 5.0 MB threshold)
# 8 * 2 * 256 * 8 * 128 * 2 bytes = 8,388,608 bytes
memory_obj = create_test_memory_obj(
shape=torch.Size([8, 2, 256, 8, 128]), dtype=torch.bfloat16
)
# Try to put and wait for it
fut = backend.submit_put_task(key, memory_obj)
fut.result(timeout=5.0)
# Verify it was NOT written (skips write)
assert not backend.contains(key)
backend.close()