Files
wehub-resource-sync e9a2f726c9
CI / test (3.11) (push) Has been cancelled
CI / test (3.12) (push) Has been cancelled
CI / test (3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:29:51 +08:00

1531 lines
56 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""Tests for the in-memory hot cache tier in PagedSSDCacheManager."""
import threading
import time
from pathlib import Path
from typing import List
from unittest.mock import patch
import pytest
from omlx.cache.paged_ssd_cache import (
PagedSSDBlockMetadata,
PagedSSDCacheManager,
SharedHotCacheBudget,
_extract_tensor_bytes,
)
try:
import mlx.core as mx
HAS_MLX = True
except ImportError:
HAS_MLX = False
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheDisabled:
"""Verify that hot_cache_max_bytes=0 preserves existing behaviour."""
@pytest.fixture
def manager(self, tmp_path):
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0,
)
yield mgr
mgr.close()
def test_hot_cache_disabled_by_default(self, manager):
"""hot_cache_max_bytes=0 means hot cache is disabled."""
assert manager._hot_cache_enabled is False
assert manager._hot_cache_max_bytes == 0
def test_save_load_works_without_hot_cache(self, manager):
"""Save/load should work even when hot cache is disabled."""
block_hash = b"disabled_hot_cache_test"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64))) for _ in range(4)
]
result = manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache"] * 4,
)
assert result is True
assert manager.has_block(block_hash)
loaded = manager.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 4
def test_stats_hot_cache_zero_when_disabled(self, manager):
"""Hot cache stats should be zero when disabled."""
stats = manager.get_stats()
assert stats.hot_cache_entries == 0
assert stats.hot_cache_size_bytes == 0
assert stats.hot_cache_max_bytes == 0
assert stats.hot_cache_hits == 0
assert stats.hot_cache_evictions == 0
assert stats.hot_cache_promotions == 0
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheEnabled:
"""Test hot cache with in-memory caching active."""
@pytest.fixture
def manager(self, tmp_path):
# 10 MB hot cache — generous for test blocks
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
yield mgr
mgr.close()
def _make_cache_data(self, num_layers=4, seq_len=32, heads=4, head_dim=32):
"""Create test cache data."""
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
def _save_block(self, manager, block_hash, num_layers=4, model="test-model"):
"""Save a test block and return True on success."""
cache_data = self._make_cache_data(num_layers=num_layers)
return manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name=model,
layer_cache_types=["KVCache"] * num_layers,
)
def test_save_stores_in_hot_cache(self, manager):
"""After save_block(), the entry should be in hot cache."""
block_hash = b"hot_cache_save_test1"
self._save_block(manager, block_hash)
# Verify hot cache has the entry
entry = manager._hot_cache_get(block_hash)
assert entry is not None
assert "tensors_raw" in entry
assert entry["num_layers"] == 4
def test_load_from_hot_cache(self, manager):
"""load_block() should return data from hot cache without SSD I/O."""
block_hash = b"hot_cache_load_test1"
self._save_block(manager, block_hash)
loaded = manager.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 4
stats = manager.get_stats()
assert stats.hot_cache_hits >= 1
def test_hot_cache_hit_updates_stats(self, manager):
"""Hot cache hit should increment hot_cache_hits counter."""
block_hash = b"hot_cache_stats_test1"
self._save_block(manager, block_hash)
initial_stats = manager.get_stats()
initial_hits = initial_stats.hot_cache_hits
manager.load_block(block_hash)
manager.load_block(block_hash)
stats = manager.get_stats()
assert stats.hot_cache_hits >= initial_hits + 2
def test_hot_cache_size_tracking(self, manager):
"""Hot cache should track total size in bytes."""
block_hash = b"hot_cache_size_test1"
self._save_block(manager, block_hash)
stats = manager.get_stats()
assert stats.hot_cache_entries == 1
assert stats.hot_cache_size_bytes > 0
assert stats.hot_cache_max_bytes == 10 * 1024**2
def test_delete_block_removes_from_hot_cache(self, manager):
"""delete_block() should remove entry from hot cache."""
block_hash = b"hot_cache_delete_test"
self._save_block(manager, block_hash)
# Verify it's in hot cache
assert manager._hot_cache_get(block_hash) is not None
manager.delete_block(block_hash)
# Verify it's gone from hot cache
assert manager._hot_cache_get(block_hash) is None
def test_close_clears_hot_cache(self, tmp_path):
"""close() should clear all hot cache entries."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "close_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
block_hash = b"hot_cache_close_test1"
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test",
layer_cache_types=["KVCache"] * 4,
)
assert len(mgr._hot_cache) > 0
mgr.close()
assert len(mgr._hot_cache) == 0
assert mgr._hot_cache_total_bytes == 0
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheLRU:
"""Test LRU eviction behaviour of the hot cache."""
def _make_cache_data(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
"""Create small test cache data."""
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
def _entry_size(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
"""Estimate the raw byte size of one entry."""
# Each tensor: 1 * heads * seq_len * head_dim * 4 bytes (float32)
# 2 tensors (keys + values) per layer, num_layers layers
return num_layers * 2 * 1 * heads * seq_len * head_dim * 4
def test_lru_eviction(self, tmp_path):
"""Old entries should be evicted when capacity is exceeded."""
entry_size = self._entry_size()
# Allow room for exactly 2 entries
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "lru_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
# Save 3 blocks — the first should be evicted
for i in range(3):
block_hash = f"lru_block_{i}".encode()
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Block 0 should have been evicted (LRU)
assert mgr._hot_cache_get(b"lru_block_0") is None
# Blocks 1 and 2 should still be in hot cache
assert mgr._hot_cache_get(b"lru_block_1") is not None
assert mgr._hot_cache_get(b"lru_block_2") is not None
stats = mgr.get_stats()
assert stats.hot_cache_evictions >= 1
finally:
mgr.close()
def test_lru_access_refreshes_order(self, tmp_path):
"""Accessing a block should move it to MRU position."""
entry_size = self._entry_size()
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "lru_order_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
# Save blocks 0 and 1
for i in range(2):
block_hash = f"order_block_{i}".encode()
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Access block 0 to refresh its LRU position
mgr.load_block(b"order_block_0")
# Save block 2 — should evict block 1 (LRU), not block 0
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=b"order_block_2",
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Block 0 was accessed so should still be present
assert mgr._hot_cache_get(b"order_block_0") is not None
# Block 1 was LRU and should be evicted
assert mgr._hot_cache_get(b"order_block_1") is None
# Block 2 was just added
assert mgr._hot_cache_get(b"order_block_2") is not None
finally:
mgr.close()
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCachePromotion:
"""Test promotion from SSD to hot cache on load."""
def _make_cache_data(self, num_layers=4, seq_len=32, heads=4, head_dim=32):
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
def test_ssd_load_promotes_to_hot_cache(self, tmp_path):
"""Loading a block from SSD should promote it to hot cache."""
# Use hot_cache disabled to write directly to SSD first
mgr_cold = PagedSSDCacheManager(
cache_dir=tmp_path / "promote_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0,
)
block_hash = b"promote_test_block1"
cache_data = self._make_cache_data()
mgr_cold.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test",
layer_cache_types=["KVCache"] * 4,
)
# Wait for background SSD write to complete
time.sleep(0.5)
mgr_cold.close()
# Now open with hot cache enabled — block is on SSD only
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "promote_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
try:
assert mgr._hot_cache_get(block_hash) is None
# Load from SSD — should promote to hot cache
loaded = mgr.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 4
# Verify promotion happened
assert mgr._hot_cache_get(block_hash) is not None
stats = mgr.get_stats()
assert stats.hot_cache_promotions >= 1
finally:
mgr.close()
def test_ssd_load_can_skip_hot_cache_promotion(self, tmp_path):
"""SSD loads can reconstruct a request without retaining a hot RAM copy."""
mgr_cold = PagedSSDCacheManager(
cache_dir=tmp_path / "skip_promote_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0,
)
block_hash = b"skip_promote_blk_1"
cache_data = self._make_cache_data()
mgr_cold.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test",
layer_cache_types=["KVCache"] * 4,
)
mgr_cold.close()
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "skip_promote_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
try:
loaded, metadata = mgr.load_block_with_metadata(
block_hash,
promote_to_hot_cache=False,
)
assert loaded is not None
assert metadata is not None
assert mgr._hot_cache_get(block_hash) is None
loaded_again, _ = mgr.load_block_with_metadata(block_hash)
assert loaded_again is not None
assert mgr._hot_cache_get(block_hash) is not None
finally:
mgr.close()
def test_write_through_save_bypasses_hot_cache_retention(self, tmp_path):
"""Pressure write-through keeps SSD durability without hot-cache RAM."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "write_through_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
try:
block_hash = b"write_through_blk1"
cache_data = self._make_cache_data()
result = mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test",
layer_cache_types=["KVCache"] * 4,
hot_cache_write_back=False,
)
assert result is True
assert mgr.has_block(block_hash)
assert mgr._hot_cache_get(block_hash) is None
loaded, metadata = mgr.load_block_with_metadata(
block_hash,
promote_to_hot_cache=False,
)
assert loaded is not None
assert metadata is not None
assert mgr._hot_cache_get(block_hash) is None
finally:
mgr.close()
def test_promotion_does_not_happen_when_disabled(self, tmp_path):
"""No promotion when hot cache is disabled."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "no_promote_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0,
)
try:
block_hash = b"no_promote_block1__"
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test",
layer_cache_types=["KVCache"] * 4,
)
# Wait for background write
time.sleep(0.5)
# Clear the temporary buffer (simulates what happens after write completes)
mgr._hot_cache_remove(block_hash)
# Load from SSD
loaded = mgr.load_block(block_hash)
assert loaded is not None
stats = mgr.get_stats()
assert stats.hot_cache_promotions == 0
finally:
mgr.close()
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheCacheTypes:
"""Test hot cache with various cache types (KVCache, CacheList)."""
@pytest.fixture
def manager(self, tmp_path):
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "types_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
yield mgr
mgr.close()
def test_cache_list_blocks(self, manager):
"""Hot cache should handle CacheList blocks correctly."""
block_hash = b"cache_list_hot_test"
sub_keys1 = mx.zeros((1, 8, 32, 64))
sub_values1 = mx.ones((1, 8, 32, 64))
sub_keys2 = mx.zeros((1, 4, 32, 64))
sub_values2 = mx.ones((1, 4, 32, 64))
cache_data = [
(
"__cache_list__",
[(sub_keys1, sub_values1), (sub_keys2, sub_values2)],
),
(mx.zeros((1, 8, 32, 64)), mx.ones((1, 8, 32, 64))),
]
result = manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=32,
model_name="test",
layer_cache_types=["CacheList", "KVCache"],
)
assert result is True
# Load from hot cache
loaded = manager.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 2
# First layer is CacheList
assert isinstance(loaded[0], list)
assert len(loaded[0]) == 2
# Second layer is KVCache tuple
assert isinstance(loaded[1], tuple)
class TestHotCacheConcurrency:
"""Test thread safety of hot cache internal operations."""
def test_concurrent_put_get(self, tmp_path):
"""Hot cache put/get should be thread-safe under concurrent access."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "concurrent_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=50 * 1024**2,
)
errors: List[Exception] = []
num_threads = 8
ops_per_thread = 20
def worker(thread_id):
try:
for i in range(ops_per_thread):
block_hash = f"conc_{thread_id}_{i}____".encode()
# Create a fake hot cache entry with raw bytes
raw_data = bytes(1024) # 1KB of zeros
entry = {
"tensors_raw": {
"layer_0_keys": (raw_data, "float32", [1, 2, 16, 8]),
"layer_0_values": (raw_data, "float32", [1, 2, 16, 8]),
},
"file_metadata": {},
"num_layers": 1,
"layer_cache_types": ["KVCache"],
"block_metadata": None,
}
mgr._hot_cache_put(block_hash, entry)
# Read back
for i in range(ops_per_thread):
block_hash = f"conc_{thread_id}_{i}____".encode()
result = mgr._hot_cache_get(block_hash)
# May be None if evicted by another thread, that's OK
except Exception as e:
errors.append(e)
threads = [
threading.Thread(target=worker, args=(t,)) for t in range(num_threads)
]
for t in threads:
t.start()
for t in threads:
t.join(timeout=30)
mgr.close()
assert len(errors) == 0, f"Concurrent errors: {errors}"
class TestHotCacheByteAccounting:
"""Regression tests for raw-byte hot cache size accounting."""
def _make_raw_entry(
self,
tmp_path: Path,
block_hash: bytes,
model_name: str = "test-model",
key_size: int = 128,
value_size: int = 256,
):
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=tmp_path / f"{block_hash.hex()}.safetensors",
file_size=key_size + value_size,
token_count=16,
created_at=time.time(),
last_access=time.time(),
num_layers=1,
model_name=model_name,
layer_cache_types=["KVCache"],
)
entry = {
"tensors_raw": {
"layer_0_keys": (bytes(key_size), "float32", [1, 1, 1, key_size // 4]),
"layer_0_values": (
bytes(value_size),
"float32",
[1, 1, 1, value_size // 4],
),
},
"file_metadata": {},
"num_layers": 1,
"layer_cache_types": ["KVCache"],
"block_metadata": metadata,
}
return entry, key_size + value_size
def test_hot_cache_remove_decrements_raw_entry_bytes(self, tmp_path):
"""Removing a tensors_raw entry should subtract its bytes from the counter."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "remove_accounting",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=1024**2,
hot_cache_only=True,
)
try:
block_hash = b"remove_bytes_test"
entry, expected_size = self._make_raw_entry(tmp_path, block_hash)
mgr._hot_cache_put(block_hash, entry)
assert mgr._hot_cache_total_bytes == expected_size
mgr._hot_cache_remove(block_hash)
assert mgr._hot_cache_get(block_hash) is None
assert mgr._hot_cache_total_bytes == 0
assert mgr.get_stats().hot_cache_size_bytes == 0
finally:
mgr.close()
def test_get_stats_for_model_reports_raw_hot_cache_bytes(self, tmp_path):
"""Per-model stats should count tensors_raw entries in the hot cache."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "model_accounting",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=1024**2,
hot_cache_only=True,
)
try:
model_a_hash = b"model_a_hot_bytes"
model_b_hash = b"model_b_hot_bytes"
model_a_entry, model_a_size = self._make_raw_entry(
tmp_path, model_a_hash, model_name="model-a"
)
model_b_entry, _ = self._make_raw_entry(
tmp_path, model_b_hash, model_name="model-b"
)
mgr._hot_cache_put(model_a_hash, model_a_entry)
mgr._hot_cache_put(model_b_hash, model_b_entry)
stats = mgr.get_stats_for_model("model-a")
assert stats.hot_cache_entries == 1
assert stats.hot_cache_size_bytes == model_a_size
finally:
mgr.close()
def test_shared_budget_evicts_across_managers_by_global_lru(self, tmp_path):
"""A shared hot cache budget should cap aggregate manager memory."""
entry, entry_size = self._make_raw_entry(tmp_path, b"budget_size_probe")
del entry
budget = SharedHotCacheBudget(entry_size * 2 + 1)
mgr_a = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_a",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
mgr_b = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_b",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
try:
a0, _ = self._make_raw_entry(tmp_path, b"budget_a_0", model_name="model-a")
b0, _ = self._make_raw_entry(tmp_path, b"budget_b_0", model_name="model-b")
b1, _ = self._make_raw_entry(tmp_path, b"budget_b_1", model_name="model-b")
mgr_a._hot_cache_put(b"budget_a_0", a0)
mgr_b._hot_cache_put(b"budget_b_0", b0)
assert budget.total_bytes == entry_size * 2
# Refresh A, so B's first block is the global LRU victim.
assert mgr_a._hot_cache_get(b"budget_a_0") is not None
mgr_b._hot_cache_put(b"budget_b_1", b1)
assert budget.total_bytes == entry_size * 2
assert mgr_a._hot_cache_get(b"budget_a_0") is not None
assert mgr_b._hot_cache_get(b"budget_b_0") is None
assert mgr_b._hot_cache_get(b"budget_b_1") is not None
assert mgr_a.get_stats().hot_cache_max_bytes == budget.max_bytes
assert mgr_b.get_stats().hot_cache_max_bytes == budget.max_bytes
finally:
mgr_a.close()
mgr_b.close()
def test_shared_budget_forgets_owner_on_clear_and_close(self, tmp_path):
"""Clearing or closing a manager must release shared budget bytes."""
_, entry_size = self._make_raw_entry(tmp_path, b"budget_clear_probe")
budget = SharedHotCacheBudget(entry_size * 4)
mgr_a = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_clear_a",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
mgr_b = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_clear_b",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
try:
a0, _ = self._make_raw_entry(tmp_path, b"budget_clear_a_0")
b0, _ = self._make_raw_entry(tmp_path, b"budget_clear_b_0")
mgr_a._hot_cache_put(b"budget_clear_a_0", a0)
mgr_b._hot_cache_put(b"budget_clear_b_0", b0)
assert budget.total_bytes == entry_size * 2
assert mgr_a.clear_hot_cache() == 1
assert budget.total_bytes == entry_size
mgr_b.close()
assert budget.total_bytes == 0
finally:
mgr_a.close()
mgr_b.close()
def test_shared_budget_shrink_respects_lru_and_protected_hashes(self, tmp_path):
"""Pressure shrink should skip protected active-request chains."""
_, entry_size = self._make_raw_entry(tmp_path, b"budget_shrink_probe")
budget = SharedHotCacheBudget(entry_size * 3)
mgr_a = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_shrink_a",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
mgr_b = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_shrink_b",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
try:
a0, _ = self._make_raw_entry(tmp_path, b"budget_shrink_a0")
b0, _ = self._make_raw_entry(tmp_path, b"budget_shrink_b0")
b1, _ = self._make_raw_entry(tmp_path, b"budget_shrink_b1")
mgr_a._hot_cache_put(b"budget_shrink_a0", a0)
mgr_b._hot_cache_put(b"budget_shrink_b0", b0)
mgr_b._hot_cache_put(b"budget_shrink_b1", b1)
freed = budget.shrink_to(
entry_size,
protected_hashes={b"budget_shrink_b0"},
)
assert freed == entry_size * 2
assert budget.total_bytes == entry_size
assert mgr_a._hot_cache_get(b"budget_shrink_a0") is None
assert mgr_b._hot_cache_get(b"budget_shrink_b0") is not None
assert mgr_b._hot_cache_get(b"budget_shrink_b1") is None
finally:
mgr_a.close()
mgr_b.close()
def test_local_hot_cache_shrink_respects_protected_hashes(self, tmp_path):
_, entry_size = self._make_raw_entry(tmp_path, b"local_shrink_probe")
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "local_shrink",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=entry_size * 3,
hot_cache_only=True,
)
try:
h0 = b"local_shrink_0"
h1 = b"local_shrink_1"
h2 = b"local_shrink_2"
e0, _ = self._make_raw_entry(tmp_path, h0)
e1, _ = self._make_raw_entry(tmp_path, h1)
e2, _ = self._make_raw_entry(tmp_path, h2)
mgr._hot_cache_put(h0, e0)
mgr._hot_cache_put(h1, e1)
mgr._hot_cache_put(h2, e2)
freed = mgr.shrink_hot_cache_to(entry_size, protected_hashes={h1})
assert freed == entry_size * 2
assert mgr.get_stats().hot_cache_size_bytes == entry_size
assert mgr._hot_cache_get(h0) is None
assert mgr._hot_cache_get(h1) is not None
assert mgr._hot_cache_get(h2) is None
finally:
mgr.close()
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheStatsAccuracy:
"""Test that hot cache statistics are accurate."""
def test_all_stats_counters(self, tmp_path):
"""Verify hot cache stats counters are correctly maintained."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4 # ~4096 bytes per entry
# Room for 2 entries
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "stats_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
def save(idx):
block_hash = f"stats_block_{idx}__".encode()
cache_data = [
(mx.zeros((1, 2, 16, 16)), mx.zeros((1, 2, 16, 16)))
for _ in range(2)
]
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Save 2 blocks: fits in hot cache
save(0)
save(1)
stats = mgr.get_stats()
assert stats.hot_cache_entries == 2
assert stats.hot_cache_evictions == 0
# Save 3rd block: triggers eviction of block 0
save(2)
stats = mgr.get_stats()
assert stats.hot_cache_entries == 2
assert stats.hot_cache_evictions >= 1
# Load block 1 (hot cache hit)
mgr.load_block(b"stats_block_1__")
stats = mgr.get_stats()
assert stats.hot_cache_hits >= 1
# Verify size tracking is positive
assert stats.hot_cache_size_bytes > 0
assert stats.hot_cache_max_bytes == max_bytes
finally:
mgr.close()
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestHotCacheWriteBack:
"""Test write-back behavior: no SSD writes until eviction or shutdown."""
def _make_cache_data(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
def test_save_does_not_write_to_ssd(self, tmp_path):
"""With hot cache enabled, save_block should not create SSD files."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "wb_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
try:
block_hash = b"wb_no_ssd_write_t1"
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Block should be in hot cache
assert mgr._hot_cache_get(block_hash) is not None
# No SSD file should exist yet
time.sleep(0.3)
ssd_files = list((tmp_path / "wb_test").rglob("*.safetensors"))
assert len(ssd_files) == 0, f"Unexpected SSD files: {ssd_files}"
finally:
mgr.close()
def test_eviction_writes_to_ssd(self, tmp_path):
"""When hot cache evicts, the evicted block should be written to SSD."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "wb_evict_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
# Save 2 blocks (fits in hot cache)
for i in range(2):
block_hash = f"wb_evict_blk_{i}__".encode()
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# No SSD files yet
time.sleep(0.3)
ssd_files = list((tmp_path / "wb_evict_test").rglob("*.safetensors"))
assert len(ssd_files) == 0
# Save 3rd block → evicts block 0 → should trigger SSD write
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=b"wb_evict_blk_2__",
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# Wait for background writer to process the evicted block
time.sleep(0.5)
ssd_files = list((tmp_path / "wb_evict_test").rglob("*.safetensors"))
assert len(ssd_files) >= 1, "Evicted block should be written to SSD"
finally:
mgr.close()
def test_close_flushes_hot_cache_to_ssd(self, tmp_path):
"""close() should flush all hot cache entries to SSD."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "wb_flush_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
block_hashes = []
for i in range(3):
block_hash = f"wb_flush_blk_{i}__".encode()
block_hashes.append(block_hash)
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# No SSD files before close
time.sleep(0.3)
ssd_files = list((tmp_path / "wb_flush_test").rglob("*.safetensors"))
assert len(ssd_files) == 0
# Close flushes to SSD
mgr.close()
ssd_files = list((tmp_path / "wb_flush_test").rglob("*.safetensors"))
assert (
len(ssd_files) == 3
), f"Expected 3 SSD files after flush, got {len(ssd_files)}"
def test_close_flushes_all_blocks_with_small_queue(self, tmp_path):
"""close() must flush all hot cache blocks even when more blocks
exist than the write queue depth.
Regression test for #1070: put_nowait() in the shutdown flush loop
lost blocks when the bounded write queue filled up faster than the
writer thread can drain it.
"""
queue_depth = 4
block_count = 12 # 3x the queue depth
with patch("omlx.cache.paged_ssd_cache._MAX_PENDING_WRITES", queue_depth):
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "wb_queue_full_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=10 * 1024**2,
)
for i in range(block_count):
block_hash = f"wb_qfull_blk_{i:02d}".encode()
cache_data = self._make_cache_data()
mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test",
layer_cache_types=["KVCache"] * 2,
)
# All blocks in hot cache, no SSD files yet
time.sleep(0.3)
ssd_files = list((tmp_path / "wb_queue_full_test").rglob("*.safetensors"))
assert len(ssd_files) == 0
mgr.close()
ssd_files = list((tmp_path / "wb_queue_full_test").rglob("*.safetensors"))
assert len(ssd_files) == block_count, (
f"Expected {block_count} SSD files after flush, got {len(ssd_files)}. "
f"Blocks were likely not flushed during shutdown."
)
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestPendingWriteBuffer:
"""Tests for the pending-write buffer that bridges hot cache eviction to SSD write."""
def _make_cache_data(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
def _save_block(self, manager, block_hash, model="test-model"):
cache_data = self._make_cache_data()
return manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name=model,
layer_cache_types=["KVCache"] * 2,
)
def test_evicted_block_readable_from_pending_buffer(self, tmp_path):
"""A block evicted from hot cache is still loadable while SSD write is pending."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "pending_buf_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
# Save 2 blocks (fills hot cache)
self._save_block(mgr, b"pending_buf_blk0")
self._save_block(mgr, b"pending_buf_blk1")
# Save 3rd block → evicts block 0
self._save_block(mgr, b"pending_buf_blk2")
# Block 0 should still be loadable (from pending buffer, not SSD)
with mgr._hot_cache_lock:
assert (
b"pending_buf_blk0" not in mgr._hot_cache
), "Block 0 should have been evicted from hot cache"
loaded = mgr.load_block(b"pending_buf_blk0")
assert (
loaded is not None
), "Evicted block should be readable from pending write buffer"
assert len(loaded) == 2
finally:
mgr.close()
def test_writer_cleanup_empties_buffer(self, tmp_path):
"""After background writer completes, pending buffer entry is removed."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "writer_cleanup_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"cleanup_test_blk0")
self._save_block(mgr, b"cleanup_test_blk1")
# Evict block 0
self._save_block(mgr, b"cleanup_test_blk2")
# Block 0 should be in pending buffer
with mgr._pending_write_hashes_lock:
assert b"cleanup_test_blk0" in mgr._pending_write_buffers
# Wait for writer to finish
time.sleep(1.0)
# Buffer should be empty after writer cleanup
with mgr._pending_write_hashes_lock:
assert b"cleanup_test_blk0" not in mgr._pending_write_buffers
assert b"cleanup_test_blk0" not in mgr._pending_write_hashes
finally:
mgr.close()
def test_has_block_sees_pending_entries(self, tmp_path):
"""has_block() returns True for blocks in the pending write buffer."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "has_block_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"has_block_test_b0")
self._save_block(mgr, b"has_block_test_b1")
# Evict block 0
self._save_block(mgr, b"has_block_test_b2")
with mgr._hot_cache_lock:
assert b"has_block_test_b0" not in mgr._hot_cache
assert (
mgr.has_block(b"has_block_test_b0") is True
), "has_block should find blocks in the pending write buffer"
finally:
mgr.close()
def test_delete_block_clears_pending_buffer(self, tmp_path):
"""delete_block() removes the entry from pending write buffer."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "delete_pending_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"del_pending_blk_0")
self._save_block(mgr, b"del_pending_blk_1")
# Evict block 0
self._save_block(mgr, b"del_pending_blk_2")
# Block 0 is in pending buffer
with mgr._pending_write_hashes_lock:
assert b"del_pending_blk_0" in mgr._pending_write_buffers
# Delete it
mgr.delete_block(b"del_pending_blk_0")
# Should no longer be loadable
loaded = mgr.load_block(b"del_pending_blk_0")
assert (
loaded is None
), "Deleted block should not be readable from pending buffer"
finally:
mgr.close()
def test_queue_full_inline_fallback_cleans_pending_buffer(self, tmp_path):
"""Inline fallback removes the block from pending after it reaches SSD."""
import queue as _queue
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "queue_full_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"qf_test_block_00")
self._save_block(mgr, b"qf_test_block_01")
self._save_block(mgr, b"qf_test_block_02")
# This evicts block 1. Force sustained saturation so the bounded
# wait expires and inline fallback runs.
with patch.object(mgr._write_queue, "put", side_effect=_queue.Full):
self._save_block(mgr, b"qf_test_block_03")
# Block 1 was written inline and no longer needs pending storage.
with mgr._pending_write_hashes_lock:
assert (
b"qf_test_block_01" not in mgr._pending_write_buffers
), "Inline-written block should leave the pending buffer"
assert (
b"qf_test_block_01" not in mgr._pending_write_hashes
), "Inline-written block should leave pending hashes"
stats = mgr.get_stats()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 1
assert mgr.load_block(b"qf_test_block_01") is not None
finally:
mgr.close()
def test_evicted_block_loadable_with_metadata(self, tmp_path):
"""load_block_with_metadata returns data for pending-buffer blocks."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "meta_load_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"meta_load_blk_00")
self._save_block(mgr, b"meta_load_blk_01")
# Evict block 0
self._save_block(mgr, b"meta_load_blk_02")
cache_data, metadata = mgr.load_block_with_metadata(b"meta_load_blk_00")
assert (
cache_data is not None
), "load_block_with_metadata should serve evicted block from pending buffer"
assert metadata is not None
assert metadata["num_layers"] == 2
assert metadata["token_count"] == 16
assert metadata["model_name"] == "test-model"
finally:
mgr.close()
def test_end_to_end_lifecycle(self, tmp_path):
"""Full lifecycle: save → evict → pending hit → writer completes → SSD hit."""
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "lifecycle_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
self._save_block(mgr, b"lifecycle_blk_00")
self._save_block(mgr, b"lifecycle_blk_01")
# Evict block 0
self._save_block(mgr, b"lifecycle_blk_02")
# Phase 1: pending buffer hit (before writer completes)
loaded = mgr.load_block(b"lifecycle_blk_00")
assert loaded is not None, "Should load from pending buffer"
assert mgr._stats["hot_cache_hits"] >= 1
# Phase 2: wait for writer to complete
time.sleep(1.0)
# Buffer should be empty
with mgr._pending_write_hashes_lock:
assert b"lifecycle_blk_00" not in mgr._pending_write_buffers
# Phase 3: SSD hit (block now on disk)
loaded_ssd = mgr.load_block(b"lifecycle_blk_00")
assert loaded_ssd is not None, "Should load from SSD after writer completes"
assert len(loaded_ssd) == 2
finally:
mgr.close()
@pytest.mark.skipif(not HAS_MLX, reason="MLX not available")
class TestSSDWriteBackSaturation:
"""Queue saturation falls back to inline writes instead of dropping blocks."""
# Mirrors TestPendingWriteBuffer._make_cache_data — small dimensions
# so the per-entry footprint is small and predictable for queue tests.
def _make_cache_data(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
return [
(
mx.zeros((1, heads, seq_len, head_dim)),
mx.zeros((1, heads, seq_len, head_dim)),
)
for _ in range(num_layers)
]
# Mirrors TestPendingWriteBuffer._save_block — uses 2 layers, token_count=16
# so the entry_size formula `2 * 2 * 1 * 2 * 16 * 16 * 4` calibrates Test A.
def _save_block(self, manager, block_hash, model="test-model"):
cache_data = self._make_cache_data()
return manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name=model,
layer_cache_types=["KVCache"] * 2,
)
def test_paged_ssd_cache_stats_default_and_reset(self):
"""Dataclass: write-back counters default to 0 and reset to 0."""
from omlx.cache.stats import PagedSSDCacheStats
# Default is zero.
stats = PagedSSDCacheStats()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 0
assert stats.evict_unlink_failures == 0
# reset() returns it to zero from a non-zero state.
stats = PagedSSDCacheStats(
ssd_write_drops=5,
ssd_inline_write_fallbacks=4,
evict_unlink_failures=3,
saves=2,
loads=3,
)
stats.reset()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 0
assert stats.evict_unlink_failures == 0
# Verify reset() didn't break the existing fields it already handled.
assert stats.saves == 0
assert stats.loads == 0
def test_ssd_write_back_fields_round_trip_through_get_stats(self, tmp_path):
"""The _stats dict values flow through both stats accessors.
Force a non-zero value into _stats so a missing pass-through in either
accessor would leave the dataclass at the default 0 and fail this test.
The default-0 case is covered by the dataclass-level test in Task 1;
this test exercises the wiring specifically.
"""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "wiring_test",
max_size_bytes=100 * 1024**2,
)
try:
# Directly poke a non-zero value to test the accessor pass-through
# specifically. The real increment sites have their own tests
# further down in this class; this test would catch a regression
# where a future refactor drops the field from get_stats() or
# get_stats_for_model() but leaves the _stats dict key alone.
mgr._stats["ssd_write_drops"] = 7
mgr._stats["ssd_inline_write_fallbacks"] = 5
# Global stats accessor.
stats = mgr.get_stats()
assert stats.ssd_write_drops == 7, (
"get_stats() must pass _stats['ssd_write_drops'] through "
"to the dataclass field"
)
assert stats.ssd_inline_write_fallbacks == 5, (
"get_stats() must pass _stats['ssd_inline_write_fallbacks'] "
"through to the dataclass field"
)
# Per-model stats accessor — same wiring, separate code path.
# Use any model name; the field is global on the manager.
model_stats = mgr.get_stats_for_model("any-model-name")
assert model_stats.ssd_write_drops == 7, (
"get_stats_for_model() must pass _stats['ssd_write_drops'] "
"through to the dataclass field"
)
assert model_stats.ssd_inline_write_fallbacks == 5, (
"get_stats_for_model() must pass "
"_stats['ssd_inline_write_fallbacks'] through to the "
"dataclass field"
)
finally:
mgr.close()
def test_evict_unlink_failures_round_trips_through_get_stats(self, tmp_path):
"""evict_unlink_failures is visible through both stats accessors."""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "unlink_failure_wiring_test",
max_size_bytes=100 * 1024**2,
)
try:
mgr._stats["evict_unlink_failures"] = 3
stats = mgr.get_stats()
assert stats.evict_unlink_failures == 3
model_stats = mgr.get_stats_for_model("any-model-name")
assert model_stats.evict_unlink_failures == 3
finally:
mgr.close()
def test_hot_eviction_queue_full_uses_inline_fallback(self, tmp_path):
"""Site 1: hot-cache eviction → put raises queue.Full → inline write.
Patches the real queue's put to raise queue.Full, guaranteeing the
fallback path fires on the first eviction without any dependency on the
writer thread's drain rate. _enqueue_ssd_write uses put(item,
timeout=...) (not put_nowait) so a transient burst can ride over a
short writer-backlog window; sustained saturation writes inline.
"""
import queue as _queue
from unittest.mock import patch
# entry_size matches the small-dimension helpers above (num_layers=2,
# K+V=2 tensors, batch=1, heads=2, seq=16, head_dim=16, float32=4).
entry_size = 2 * 2 * 1 * 2 * 16 * 16 * 4
max_bytes = entry_size * 2 + 100 # holds exactly 2 entries
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "drops_hot_eviction_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=max_bytes,
)
try:
with patch.object(mgr._write_queue, "put", side_effect=_queue.Full):
self._save_block(mgr, b"qf_drop_block_00")
self._save_block(mgr, b"qf_drop_block_01")
# save_02 evicts block 00 → _enqueue_ssd_write → put raises
# queue.Full → inline fallback writes it immediately.
self._save_block(mgr, b"qf_drop_block_02")
stats = mgr.get_stats()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 1
assert stats.saves_persisted == 1
assert stats.errors == 0
# Block 00 was the one being enqueued when put raised. It should
# no longer need the pending buffer because it is already on SSD.
with mgr._pending_write_hashes_lock:
assert b"qf_drop_block_00" not in mgr._pending_write_buffers
assert b"qf_drop_block_00" not in mgr._pending_write_hashes
metadata = mgr._index.get(b"qf_drop_block_00")
assert metadata is not None
assert metadata.file_path.exists()
assert mgr.load_block(b"qf_drop_block_00") is not None
finally:
mgr.close()
def test_cold_store_sustained_full_uses_inline_fallback(self, tmp_path):
"""Site 2: save_block waits on a full queue before writing inline.
Hot cache disabled. Even if ``full()`` reports saturation, the cold
path must still proceed to ``put(timeout=1.0)`` so transient writer
bursts get a chance to drain. Sustained ``queue.Full`` from the put
call should write inline and return success.
"""
import queue as _queue
from unittest.mock import patch
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "drops_cold_preflight_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0, # hot cache disabled → cold-store path
)
try:
calls: list[float | None] = []
def full_put(*args, timeout=None, **kwargs):
calls.append(timeout)
raise _queue.Full
with (
patch.object(mgr._write_queue, "full", return_value=True),
patch.object(mgr._write_queue, "put", side_effect=full_put),
):
cache_data = self._make_cache_data()
block_hash = b"cold_preflight_drop_00"
ok = mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test-model",
layer_cache_types=["KVCache"] * 2,
)
assert ok is True
assert calls == [1.0]
stats = mgr.get_stats()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 1
assert stats.saves == 1
assert stats.saves_persisted == 1
assert stats.errors == 0
assert mgr._index.contains(block_hash)
with mgr._pending_write_hashes_lock:
assert block_hash not in mgr._pending_write_hashes
assert mgr.load_block(block_hash) is not None
finally:
mgr.close()
def test_cold_store_late_queue_full_uses_inline_fallback(self, tmp_path):
"""Site 3: save_block put raises queue.Full after the preflight passes.
Hot cache disabled. ``put`` is patched to raise queue.Full directly
(simulating a sustained writer-backlog saturation that materializes
after the preflight check). The block must still be written inline
and remain readable.
"""
import queue as _queue
from unittest.mock import patch
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "drops_cold_late_exception_test",
max_size_bytes=100 * 1024**2,
hot_cache_max_bytes=0,
)
try:
cache_data = self._make_cache_data()
block_hash = b"cold_late_drop_00"
with patch.object(mgr._write_queue, "put", side_effect=_queue.Full):
ok = mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test-model",
layer_cache_types=["KVCache"] * 2,
)
assert ok is True
stats = mgr.get_stats()
assert stats.ssd_write_drops == 0
assert stats.ssd_inline_write_fallbacks == 1
assert stats.saves == 1
assert stats.saves_persisted == 1
assert mgr._index.contains(block_hash)
with mgr._pending_write_hashes_lock:
assert block_hash not in mgr._pending_write_hashes
assert mgr.load_block(block_hash) is not None
finally:
mgr.close()