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

922 lines
31 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Tests for the DAX MP L2 adapter.
"""
# Standard
from typing import cast
import select
import threading
import time
# Third Party
import pytest
import torch
# First Party
from lmcache.native_storage_ops import Bitmap
from lmcache.v1.distributed.api import MemoryLayoutDesc, ObjectKey
from lmcache.v1.distributed.config import (
EvictionConfig,
L1ManagerConfig,
L1MemoryManagerConfig,
StorageManagerConfig,
)
from lmcache.v1.distributed.error import L1Error
from lmcache.v1.distributed.internal_api import L2AdapterListener
from lmcache.v1.distributed.l2_adapters.base import L2AdapterInterface
from lmcache.v1.distributed.l2_adapters.config import (
L2AdaptersConfig,
get_registered_l2_adapter_types,
)
from lmcache.v1.distributed.l2_adapters.dax_l2_adapter import (
DaxDeviceConfig,
DaxL2Adapter,
DaxL2AdapterConfig,
)
from lmcache.v1.distributed.l2_adapters.reconfiguration import (
L2ReconfigurableAdapter,
L2ReconfigureError,
)
from lmcache.v1.distributed.storage_manager import StorageManager
from lmcache.v1.memory_allocators.ad_hoc_memory_allocator import AdHocMemoryAllocator
from lmcache.v1.memory_management import (
MemoryFormat,
MemoryObj,
)
from lmcache.v1.platform import consume_fd
_EMPTY_LAYOUT = MemoryLayoutDesc(shapes=[], dtypes=[])
_DEFAULT_SHAPE = torch.Size([2, 4, 8])
class _RecordingListener(L2AdapterListener):
def __init__(self):
self.stored: list[list[ObjectKey]] = []
self.accessed: list[list[ObjectKey]] = []
self.deleted: list[list[ObjectKey]] = []
def on_l2_keys_stored(self, keys: list[ObjectKey], sizes: list[int]):
self.stored.append(list(keys))
def on_l2_keys_accessed(self, keys: list[ObjectKey]):
self.accessed.append(list(keys))
def on_l2_keys_deleted(self, keys: list[ObjectKey]):
self.deleted.append(list(keys))
def create_object_key(
chunk_id: int,
model_name: str = "test_model",
cache_salt: str = "",
) -> ObjectKey:
return ObjectKey(
chunk_hash=ObjectKey.IntHash2Bytes(chunk_id),
model_name=model_name,
kv_rank=0,
cache_salt=cache_salt,
)
def create_memory_obj(
*,
shape: torch.Size = _DEFAULT_SHAPE,
dtype: torch.dtype = torch.bfloat16,
fill_value: float = 0,
fmt: MemoryFormat = MemoryFormat.KV_2LTD,
) -> MemoryObj:
allocator = AdHocMemoryAllocator(device="cpu")
obj = allocator.allocate([shape], [dtype], fmt=fmt)
assert obj is not None
assert obj.tensor is not None
obj.tensor.fill_(fill_value)
return obj
def wait_for_event_fd(event_fd: int, timeout: float = 5.0) -> bool:
poll = select.poll()
poll.register(event_fd, select.POLLIN)
events = poll.poll(timeout * 1000)
if not events:
return False
consume_fd(event_fd)
return True
def wait_for_condition(predicate, timeout: float = 5.0, interval: float = 0.02) -> bool:
deadline = time.monotonic() + timeout
while time.monotonic() < deadline:
if predicate():
return True
time.sleep(interval)
return False
def make_adapter(
tmp_path,
*,
slot_bytes: int = 4096,
max_slots: int = 2,
num_store_workers: int = 1,
num_lookup_workers: int = 1,
num_load_workers: int = 1,
) -> tuple[DaxL2Adapter, DaxL2AdapterConfig]:
device_path = tmp_path / "dax.bin"
arena_bytes = slot_bytes * max_slots
with open(device_path, "wb") as fout:
fout.truncate(arena_bytes)
config = DaxL2AdapterConfig(
devices=[
DaxDeviceConfig(
device_path=str(device_path),
max_dax_size_gb=arena_bytes / (1024**3),
)
],
slot_bytes=slot_bytes,
num_store_workers=num_store_workers,
num_lookup_workers=num_lookup_workers,
num_load_workers=num_load_workers,
)
return DaxL2Adapter(config), config
def make_hotplug_adapter(tmp_path, *, slot_bytes: int = 2048) -> DaxL2Adapter:
devices = []
for i in range(2):
device_path = tmp_path / f"dax_hotplug_{i}.bin"
with open(device_path, "wb") as fout:
fout.truncate(slot_bytes * 2)
devices.append(
DaxDeviceConfig(
device_path=str(device_path),
max_dax_size_gb=(slot_bytes * 2) / (1024**3),
)
)
return DaxL2Adapter(
DaxL2AdapterConfig(
devices=devices,
hotplug_enabled=True,
slot_bytes=slot_bytes,
num_store_workers=1,
num_lookup_workers=1,
num_load_workers=1,
)
)
def store_and_wait(adapter: DaxL2Adapter, key: ObjectKey, obj: MemoryObj) -> None:
task_id = adapter.submit_store_task([key], [obj])
assert wait_for_event_fd(adapter.get_store_event_fd())
completed = adapter.pop_completed_store_tasks()
assert completed[task_id].is_successful()
def bitmap_to_bools(bitmap: Bitmap, size: int) -> list[bool]:
return [bitmap.test(i) for i in range(size)]
def lookup_and_wait(adapter: DaxL2Adapter, keys: list[ObjectKey]) -> list[bool]:
task_id = adapter.submit_lookup_and_lock_task(keys, _EMPTY_LAYOUT)
assert wait_for_event_fd(adapter.get_lookup_and_lock_event_fd())
bitmap = adapter.query_lookup_and_lock_result(task_id)
assert bitmap is not None
return bitmap_to_bools(bitmap, len(keys))
def load_and_wait(
adapter: DaxL2Adapter,
keys: list[ObjectKey],
objs: list[MemoryObj],
) -> list[bool]:
task_id = adapter.submit_load_task(keys, objs)
assert wait_for_event_fd(adapter.get_load_event_fd())
bitmap = adapter.query_load_result(task_id)
assert bitmap is not None
return bitmap_to_bools(bitmap, len(keys))
def test_dax_adapter_registers_and_has_distinct_eventfds(tmp_path):
adapter, _ = make_adapter(tmp_path)
try:
assert "dax" in get_registered_l2_adapter_types()
store_fd = adapter.get_store_event_fd()
lookup_fd = adapter.get_lookup_and_lock_event_fd()
load_fd = adapter.get_load_event_fd()
assert len({store_fd, lookup_fd, load_fd}) == 3
finally:
adapter.close()
def test_dax_hotplug_remove_migrate_preserves_loadability(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
listener = _RecordingListener()
adapter.register_listener(listener)
obj = create_memory_obj(fill_value=9)
target = create_memory_obj(fill_value=0)
try:
key = create_object_key(80)
store_and_wait(adapter, key, obj)
source_path = adapter.hotplug_status()["devices"][0]["device_path"]
result = adapter.reconfigure(
"remove",
{
"device_path": source_path,
"mode": "migrate",
},
)
assert result["state"] == "removed"
assert result["moved_keys"] == 1
assert result["deleted_keys"] == 0
assert result["source_slots_freed"] == 1
assert listener.deleted == []
assert lookup_and_wait(adapter, [key]) == [True]
assert load_and_wait(adapter, [key], [target]) == [True]
assert target.tensor is not None
assert torch.all(target.tensor == 9)
adapter.submit_unlock([key])
finally:
obj.ref_count_down()
target.ref_count_down()
adapter.close()
def test_dax_adapter_implements_generic_reconfigure_status(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
try:
assert isinstance(adapter, L2ReconfigurableAdapter)
status = adapter.reconfigure("status", {})
assert status == {
"backend": "dax",
"supported_operations": ["status", "add", "remove", "resize"],
"status": adapter.hotplug_status(),
}
finally:
adapter.close()
def test_dax_hotplug_drain_stops_new_writes_to_source_device(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
obj0 = create_memory_obj(fill_value=1)
obj1 = create_memory_obj(fill_value=2)
try:
store_and_wait(adapter, create_object_key(81), obj0)
devices_before = adapter.hotplug_status()["devices"]
source_path = devices_before[0]["device_path"]
source_live_slots = devices_before[0]["live_slot_count"]
result = adapter.hotplug_remove_device(source_path, "drain")
assert result["status"] == "ok"
assert result["state"] == "draining"
store_and_wait(adapter, create_object_key(82), obj1)
devices = adapter.hotplug_status()["devices"]
assert devices[0]["live_slot_count"] == source_live_slots
assert devices[1]["live_slot_count"] == 1
finally:
obj0.ref_count_down()
obj1.ref_count_down()
adapter.close()
def test_dax_hotplug_remove_blocked_restores_active_state(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
obj = create_memory_obj(fill_value=5)
try:
key = create_object_key(84)
store_and_wait(adapter, key, obj)
source_path = adapter.hotplug_status()["devices"][0]["device_path"]
assert lookup_and_wait(adapter, [key]) == [True]
with pytest.raises(L2ReconfigureError) as exc_info:
adapter.hotplug_remove_device(source_path, "migrate")
assert exc_info.value.status_code == 409
assert adapter.hotplug_status()["devices"][0]["state"] == "active"
adapter.submit_unlock([key])
finally:
obj.ref_count_down()
adapter.close()
def test_dax_hotplug_remove_migrate_rejects_duplicate_destination_key(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
source_obj = create_memory_obj(fill_value=1)
duplicate_obj = create_memory_obj(fill_value=9)
target = create_memory_obj(fill_value=0)
try:
key = create_object_key(83)
store_and_wait(adapter, key, source_obj)
source_path = adapter.hotplug_status()["devices"][0]["device_path"]
destination = adapter._devices[1]
assert destination.core.put_many([key], [duplicate_obj]) == [True]
with pytest.raises(L2ReconfigureError) as exc_info:
adapter.hotplug_remove_device(source_path, "migrate")
assert exc_info.value.status_code == 409
assert adapter.hotplug_status()["devices"][0]["state"] == "active"
assert lookup_and_wait(adapter, [key]) == [True]
assert load_and_wait(adapter, [key], [target]) == [True]
assert target.tensor is not None
assert torch.all(target.tensor == 1)
adapter.submit_unlock([key])
finally:
source_obj.ref_count_down()
duplicate_obj.ref_count_down()
target.ref_count_down()
adapter.close()
def test_dax_hotplug_remove_evict_notifies_logical_delete(tmp_path):
adapter = make_hotplug_adapter(tmp_path)
listener = _RecordingListener()
adapter.register_listener(listener)
obj = create_memory_obj(fill_value=1)
try:
key = create_object_key(85)
store_and_wait(adapter, key, obj)
source_path = adapter.hotplug_status()["devices"][0]["device_path"]
result = adapter.hotplug_remove_device(source_path, "evict")
assert result["deleted_keys"] == 1
assert result["source_slots_freed"] == 1
assert listener.deleted == [[key]]
finally:
obj.ref_count_down()
adapter.close()
def test_dax_hotplug_add_sanitizes_mapping_errors(tmp_path):
adapter = DaxL2Adapter(
DaxL2AdapterConfig(
devices=[],
hotplug_enabled=True,
slot_bytes=2048,
num_store_workers=1,
num_lookup_workers=1,
num_load_workers=1,
)
)
missing_path = str(tmp_path / "missing_dax.bin")
try:
with pytest.raises(L2ReconfigureError) as exc_info:
adapter.hotplug_add_device(missing_path, 2048)
assert exc_info.value.status_code == 400
assert exc_info.value.payload == {"error": "failed to map DAX device"}
assert missing_path not in str(exc_info.value.payload)
finally:
adapter.close()
def test_dax_lookup_batches_unmapped_keys_by_device(tmp_path, monkeypatch):
adapter = make_hotplug_adapter(tmp_path)
objs = [create_memory_obj(fill_value=i) for i in range(4)]
keys = [create_object_key(500 + i) for i in range(4)]
calls: list[tuple[int, int, bool]] = []
try:
task_id = adapter.submit_store_task(keys, objs)
assert wait_for_event_fd(adapter.get_store_event_fd())
completed = adapter.pop_completed_store_tasks()
assert completed[task_id].is_successful()
with adapter._device_lock:
adapter._key_to_device.clear()
for entry in adapter._devices:
original = entry.core.exists_many
def wrapped_exists_many(
lookup_keys,
lock=False,
*,
device_id=entry.device_id,
original=original,
):
calls.append((device_id, len(lookup_keys), lock))
return original(lookup_keys, lock=lock)
monkeypatch.setattr(entry.core, "exists_many", wrapped_exists_many)
assert lookup_and_wait(adapter, keys) == [True, True, True, True]
assert [count for _, count, _ in calls] == [4, 2]
assert all(lock for _, _, lock in calls)
finally:
adapter.submit_unlock(keys)
adapter.close()
class _FakeReconfigurableAdapter:
def __init__(self) -> None:
self.calls: list[tuple[str, dict[str, object]]] = []
def reconfigure_status(self) -> dict:
return {
"backend": "fake",
"supported_operations": ["flip"],
"status": {"ready": True},
}
def reconfigure(self, operation: str, payload: dict[str, object]) -> dict:
self.calls.append((operation, payload))
return {"status": "ok", "operation": operation, "payload": payload}
class _SerdeLikeWrapper:
def __init__(self, inner_adapter: _FakeReconfigurableAdapter) -> None:
self.inner_adapter = inner_adapter
class _FakeAdapterDescriptor:
def __init__(self, type_name: str) -> None:
self.type_name = type_name
def test_storage_manager_routes_generic_l2_reconfigure_to_adapter():
sm = StorageManager.__new__(StorageManager)
adapter = _FakeReconfigurableAdapter()
sm._adapters_lock = threading.Lock()
sm._l2_adapters = {0: cast(L2AdapterInterface, adapter)}
result = sm.reconfigure_l2_adapter(0, "flip", {"enabled": True})
assert result == {
"status": "ok",
"operation": "flip",
"payload": {"enabled": True},
"adapter_index": 0,
}
assert adapter.calls == [("flip", {"enabled": True})]
def test_storage_manager_finds_serde_wrapped_reconfigurable_adapter():
sm = StorageManager.__new__(StorageManager)
sm._adapters_lock = threading.Lock()
sm._l2_adapters = {
0: cast(L2AdapterInterface, _SerdeLikeWrapper(_FakeReconfigurableAdapter()))
}
sm._adapter_descriptors = {0: _FakeAdapterDescriptor("configured_fake")}
status = sm.get_l2_adapter_reconfigure_status()
assert status["enabled"] is True
assert status["num_adapters"] == 1
assert status["adapters"][0]["backend"] == "configured_fake"
assert status["adapters"][0]["adapter_index"] == 0
assert status["adapters"][0]["l2_adapter_index"] == 0
def test_dax_adapter_store_lookup_load_and_one_shot_results(tmp_path):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=4)
listener = _RecordingListener()
adapter.register_listener(listener)
obj0 = create_memory_obj(fill_value=3)
obj2 = create_memory_obj(fill_value=7)
miss_target = create_memory_obj(fill_value=0)
load_target0 = create_memory_obj(fill_value=0)
load_target2 = create_memory_obj(fill_value=0)
try:
key0 = create_object_key(10)
key1 = create_object_key(11)
key2 = create_object_key(12)
store_task = adapter.submit_store_task([key0, key2], [obj0, obj2])
assert wait_for_event_fd(adapter.get_store_event_fd())
completed = adapter.pop_completed_store_tasks()
assert completed[store_task].is_successful()
assert adapter.pop_completed_store_tasks() == {}
assert listener.stored == [[key0, key2]]
lookup_task = adapter.submit_lookup_and_lock_task(
[key0, key1, key2], _EMPTY_LAYOUT
)
assert wait_for_event_fd(adapter.get_lookup_and_lock_event_fd())
lookup_bitmap = adapter.query_lookup_and_lock_result(lookup_task)
assert lookup_bitmap is not None
assert bitmap_to_bools(lookup_bitmap, 3) == [True, False, True]
assert adapter.query_lookup_and_lock_result(lookup_task) is None
load_task = adapter.submit_load_task(
[key0, key1, key2],
[load_target0, miss_target, load_target2],
)
assert wait_for_event_fd(adapter.get_load_event_fd())
load_bitmap = adapter.query_load_result(load_task)
assert load_bitmap is not None
assert bitmap_to_bools(load_bitmap, 3) == [True, False, True]
assert adapter.query_load_result(load_task) is None
assert load_target0.tensor is not None
assert load_target2.tensor is not None
assert miss_target.tensor is not None
assert torch.all(load_target0.tensor == 3)
assert torch.all(load_target2.tensor == 7)
assert torch.all(miss_target.tensor == 0)
assert listener.accessed == [[key0, key2]]
finally:
obj0.ref_count_down()
obj2.ref_count_down()
miss_target.ref_count_down()
load_target0.ref_count_down()
load_target2.ref_count_down()
adapter.close()
def test_dax_adapter_unlock_refcount_and_delete_skips_locked_keys(tmp_path):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=2)
listener = _RecordingListener()
adapter.register_listener(listener)
obj = create_memory_obj(fill_value=5)
try:
key = create_object_key(20)
store_and_wait(adapter, key, obj)
first_lookup = adapter.submit_lookup_and_lock_task([key], _EMPTY_LAYOUT)
assert wait_for_event_fd(adapter.get_lookup_and_lock_event_fd())
assert adapter.query_lookup_and_lock_result(first_lookup) is not None
second_lookup = adapter.submit_lookup_and_lock_task([key], _EMPTY_LAYOUT)
assert wait_for_event_fd(adapter.get_lookup_and_lock_event_fd())
assert adapter.query_lookup_and_lock_result(second_lookup) is not None
adapter.delete([key])
assert listener.deleted == []
assert adapter.report_status()["locked_key_count"] == 1
adapter.submit_unlock([key])
adapter.delete([key])
assert listener.deleted == []
adapter.submit_unlock([key])
adapter.delete([key])
assert listener.deleted == [[key]]
finally:
obj.ref_count_down()
adapter.close()
def test_dax_adapter_usage_and_status_track_pending_free_slots(tmp_path):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=2)
obj = create_memory_obj(fill_value=9)
try:
assert adapter.supports_global_eviction is True
key = create_object_key(30)
store_and_wait(adapter, key, obj)
reservations, hits = adapter._core.reserve_reads([key], prefix_only=False)
assert hits == [True]
adapter.delete([key])
usage = adapter.get_usage()
assert usage.usage_fraction == pytest.approx(0.5)
assert usage.total_bytes_used == 2048
status = adapter.report_status()
assert status["live_slot_count"] == 1
assert status["borrowed_slot_count"] == 1
assert status["locked_key_count"] == 0
assert status["inflight_store_tasks"] == 0
assert status["inflight_lookup_tasks"] == 0
assert status["inflight_load_tasks"] == 0
_, usage_after_eviction = adapter._core.usage()
assert usage_after_eviction == pytest.approx(0.0)
assert status["supports_restart_recovery"] is False
adapter._core.finalize_reads(reservations, set())
assert adapter.get_usage().usage_fraction == pytest.approx(0.0)
assert adapter._core.usage()[1] == pytest.approx(0.0)
finally:
obj.ref_count_down()
adapter.close()
def test_dax_adapter_full_arena_does_not_evict_internally(tmp_path):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=2)
listener = _RecordingListener()
adapter.register_listener(listener)
obj0 = create_memory_obj(fill_value=1)
obj1 = create_memory_obj(fill_value=2)
obj2 = create_memory_obj(fill_value=3)
try:
key0 = create_object_key(31)
key1 = create_object_key(32)
key2 = create_object_key(33)
store_and_wait(adapter, key0, obj0)
store_and_wait(adapter, key1, obj1)
task_id = adapter.submit_store_task([key2], [obj2])
assert wait_for_event_fd(adapter.get_store_event_fd())
completed = adapter.pop_completed_store_tasks()
assert not completed[task_id].is_successful()
assert listener.stored == [[key0], [key1]]
assert adapter.get_usage().usage_fraction == pytest.approx(1.0)
lookup_task = adapter.submit_lookup_and_lock_task(
[key0, key1, key2], _EMPTY_LAYOUT
)
assert wait_for_event_fd(adapter.get_lookup_and_lock_event_fd())
bitmap = adapter.query_lookup_and_lock_result(lookup_task)
assert bitmap is not None
assert bitmap_to_bools(bitmap, 3) == [True, True, False]
adapter.submit_unlock([key0, key1])
finally:
obj0.ref_count_down()
obj1.ref_count_down()
obj2.ref_count_down()
adapter.close()
def test_dax_adapter_usage_tracks_cache_salt_by_slot(tmp_path):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=4)
obj0 = create_memory_obj(fill_value=1)
obj1 = create_memory_obj(fill_value=2)
obj2 = create_memory_obj(fill_value=3)
try:
alice0 = create_object_key(34, cache_salt="alice")
bob = create_object_key(35, cache_salt="bob")
alice1 = create_object_key(36, cache_salt="alice")
store_and_wait(adapter, alice0, obj0)
store_and_wait(adapter, bob, obj1)
store_and_wait(adapter, alice1, obj2)
usage = adapter.get_usage()
assert usage.total_bytes_used == 2048 * 3
assert dict(usage.bytes_by_cache_salt) == {
"alice": 2048 * 2,
"bob": 2048,
}
adapter.delete([alice0])
usage = adapter.get_usage()
assert usage.total_bytes_used == 2048 * 2
assert dict(usage.bytes_by_cache_salt) == {
"alice": 2048,
"bob": 2048,
}
adapter.delete([alice1])
usage = adapter.get_usage()
assert usage.total_bytes_used == 2048
assert dict(usage.bytes_by_cache_salt) == {"bob": 2048}
finally:
obj0.ref_count_down()
obj1.ref_count_down()
obj2.ref_count_down()
adapter.close()
def test_dax_adapter_close_waits_for_inflight_tasks(tmp_path, monkeypatch):
adapter, _ = make_adapter(tmp_path, slot_bytes=2048, max_slots=2)
obj = create_memory_obj(fill_value=2)
target = create_memory_obj(fill_value=0)
try:
key = create_object_key(40)
store_and_wait(adapter, key, obj)
load_started = threading.Event()
allow_load = threading.Event()
close_returned = threading.Event()
original_load = adapter._core.load_many_into
def _blocking_load_many_into(keys, objs):
load_started.set()
assert allow_load.wait(timeout=2)
return original_load(keys, objs)
monkeypatch.setattr(adapter._core, "load_many_into", _blocking_load_many_into)
adapter.submit_load_task([key], [target])
assert load_started.wait(timeout=2)
closer = threading.Thread(
target=lambda: (adapter.close(), close_returned.set())
)
closer.start()
time.sleep(0.05)
assert not close_returned.is_set()
allow_load.set()
closer.join(timeout=2)
assert not closer.is_alive()
assert close_returned.is_set()
finally:
obj.ref_count_down()
target.ref_count_down()
adapter.close()
def test_dax_adapter_restart_is_volatile_only(tmp_path):
adapter, config = make_adapter(tmp_path, slot_bytes=2048, max_slots=2)
obj = create_memory_obj(fill_value=4)
try:
key = create_object_key(50)
store_and_wait(adapter, key, obj)
adapter.close()
reopened = DaxL2Adapter(config)
try:
lookup_task = reopened.submit_lookup_and_lock_task([key], _EMPTY_LAYOUT)
assert wait_for_event_fd(reopened.get_lookup_and_lock_event_fd())
bitmap = reopened.query_lookup_and_lock_result(lookup_task)
assert bitmap is not None
assert bitmap_to_bools(bitmap, 1) == [False]
finally:
reopened.close()
finally:
obj.ref_count_down()
adapter.close()
def test_storage_manager_dax_adapter_roundtrip(tmp_path):
shape = torch.Size([2, 4, 8])
dtype = torch.bfloat16
slot_bytes = shape.numel() * dtype.itemsize
device_path = tmp_path / "sm_dax.bin"
with open(device_path, "wb") as fout:
fout.truncate(slot_bytes * 4)
storage_config = StorageManagerConfig(
l1_manager_config=L1ManagerConfig(
memory_config=L1MemoryManagerConfig(
size_in_bytes=1 << 20,
use_lazy=False,
init_size_in_bytes=1 << 20,
align_bytes=0x1000,
),
write_ttl_seconds=60,
read_ttl_seconds=60,
),
eviction_config=EvictionConfig(eviction_policy="noop"),
l2_adapter_config=L2AdaptersConfig(
[
DaxL2AdapterConfig(
devices=[
DaxDeviceConfig(
device_path=str(device_path),
max_dax_size_gb=(slot_bytes * 4) / (1024**3),
)
],
slot_bytes=slot_bytes,
)
]
),
store_policy="skip_l1",
)
layout = MemoryLayoutDesc(shapes=[shape], dtypes=[dtype])
key = create_object_key(60)
sm = StorageManager(storage_config)
try:
adapter = sm._l2_adapters[0]
assert isinstance(adapter, DaxL2Adapter)
reserved = sm.reserve_write([key], layout, mode="new")
assert key in reserved
assert reserved[key].tensor is not None
reserved[key].tensor.fill_(11)
sm.finish_write([key])
assert wait_for_condition(
lambda: (
sm.report_status()["l1_manager"]["total_object_count"] == 0
and adapter.get_usage().usage_fraction > 0
),
timeout=5.0,
)
handle = sm.submit_prefetch_task([key], layout)
assert wait_for_condition(
lambda: sm.query_prefetch_lookup_hits(handle) is not None,
timeout=5.0,
)
lookup_hits = sm.query_prefetch_lookup_hits(handle)
assert lookup_hits == 1
final_result: dict[str, int | None] = {"value": None}
def _capture_prefetch_result() -> bool:
result = sm.query_prefetch_status(handle)
if result is None:
return False
final_result["value"] = result.count_leading_ones()
return True
assert wait_for_condition(_capture_prefetch_result, timeout=5.0)
final_hits = final_result["value"]
assert final_hits == 1
with sm.read_prefetched_results([key]) as results:
assert results is not None
assert len(results) == 1
assert results[0].tensor is not None
assert torch.all(results[0].tensor == 11)
assert adapter.report_status()["locked_key_count"] == 0
assert sm._l1_manager.delete([key])[key] == L1Error.KEY_IS_LOCKED
sm.finish_read_prefetched([key])
assert sm._l1_manager.delete([key])[key] == L1Error.KEY_NOT_EXIST
finally:
sm.close()
def test_storage_manager_dax_adapter_uses_global_l2_eviction(tmp_path):
shape = torch.Size([2, 4, 8])
dtype = torch.bfloat16
slot_bytes = shape.numel() * dtype.itemsize
device_path = tmp_path / "sm_dax_eviction.bin"
with open(device_path, "wb") as fout:
fout.truncate(slot_bytes * 2)
dax_config = DaxL2AdapterConfig(
devices=[
DaxDeviceConfig(
device_path=str(device_path),
max_dax_size_gb=(slot_bytes * 2) / (1024**3),
)
],
slot_bytes=slot_bytes,
)
dax_config.eviction_config = EvictionConfig(
eviction_policy="LRU",
trigger_watermark=0.5,
eviction_ratio=0.5,
)
storage_config = StorageManagerConfig(
l1_manager_config=L1ManagerConfig(
memory_config=L1MemoryManagerConfig(
size_in_bytes=1 << 20,
use_lazy=False,
init_size_in_bytes=1 << 20,
align_bytes=0x1000,
),
write_ttl_seconds=60,
read_ttl_seconds=60,
),
eviction_config=EvictionConfig(eviction_policy="noop"),
l2_adapter_config=L2AdaptersConfig([dax_config]),
store_policy="skip_l1",
)
layout = MemoryLayoutDesc(shapes=[shape], dtypes=[dtype])
sm = StorageManager(storage_config)
try:
sm._l2_eviction_controller.stop()
adapter = sm._l2_adapters[0]
assert isinstance(adapter, DaxL2Adapter)
assert adapter.supports_global_eviction is True
assert len(sm._l2_eviction_controller._adapter_states) == 1
key0 = create_object_key(70)
key1 = create_object_key(71)
key2 = create_object_key(72)
def _write_key(key: ObjectKey, fill_value: int, usage_fraction: float) -> None:
reserved = sm.reserve_write([key], layout, mode="new")
assert key in reserved
assert reserved[key].tensor is not None
reserved[key].tensor.fill_(fill_value)
sm.finish_write([key])
assert wait_for_condition(
lambda: (
adapter.get_usage().usage_fraction == pytest.approx(usage_fraction)
),
timeout=5.0,
)
_write_key(key0, 1, 0.5)
_write_key(key1, 2, 1.0)
assert adapter.get_usage().usage_fraction == pytest.approx(1.0)
eviction_state = sm._l2_eviction_controller._adapter_states[0]
sm._l2_eviction_controller._check_and_evict(eviction_state)
assert adapter.get_usage().usage_fraction == pytest.approx(0.5)
hits = adapter._core.exists_many([key0, key1], lock=True)
assert hits == [False, True]
adapter.submit_unlock([key1])
_write_key(key2, 3, 1.0)
assert adapter.get_usage().usage_fraction == pytest.approx(1.0)
finally:
sm.close()