Files
jundot--omlx/tests/test_paged_ssd_cache.py
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

4007 lines
142 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SPDX-License-Identifier: Apache-2.0
"""
Tests for PagedSSDCacheManager and related components.
This module tests SSD-based storage for paged KV cache blocks,
enabling larger effective cache sizes than GPU memory allows.
"""
import errno
import json
import logging
import os
import queue
import shutil
import threading
import time
from pathlib import Path
from unittest.mock import patch
import pytest
from omlx.cache.paged_ssd_cache import (
PagedSSDBlockMetadata,
PagedSSDCacheIndex,
PagedSSDCacheManager,
SharedHotCacheBudget,
_block_turboquant_bits,
_cache_compat_signature,
_signature_turboquant_bits,
_extract_tensor_bytes,
_restore_tensor_from_bytes,
_write_safetensors_no_mx,
parse_size,
)
def _has_mlx() -> bool:
"""Check if MLX is available."""
try:
import mlx.core # noqa: F401
return True
except ImportError:
return False
class TestParseSize:
"""Tests for parse_size utility function."""
def test_parse_bytes(self):
"""Test parsing plain bytes."""
assert parse_size("1024") == 1024
assert parse_size("0") == 0
def test_parse_kb(self):
"""Test parsing kilobytes."""
assert parse_size("1KB") == 1024
assert parse_size("10kb") == 10 * 1024
assert parse_size("1.5KB") == int(1.5 * 1024)
def test_parse_mb(self):
"""Test parsing megabytes."""
assert parse_size("1MB") == 1024**2
assert parse_size("100mb") == 100 * 1024**2
def test_parse_gb(self):
"""Test parsing gigabytes."""
assert parse_size("1GB") == 1024**3
assert parse_size("16gb") == 16 * 1024**3
assert parse_size("0.5GB") == int(0.5 * 1024**3)
def test_parse_tb(self):
"""Test parsing terabytes."""
assert parse_size("1TB") == 1024**4
assert parse_size("2tb") == 2 * 1024**4
def test_parse_with_whitespace(self):
"""Test parsing with whitespace."""
assert parse_size(" 100MB ") == 100 * 1024**2
def test_invalid_format(self):
"""Test invalid format raises ValueError."""
with pytest.raises(ValueError):
parse_size("invalid")
with pytest.raises(ValueError):
parse_size("MB100")
class TestPagedSSDBlockMetadata:
"""Tests for PagedSSDBlockMetadata dataclass."""
def test_creation(self):
"""Test creating metadata."""
metadata = PagedSSDBlockMetadata(
block_hash=b"test_hash_bytes_1234",
file_path=Path("/tmp/cache/test.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
model_name="test-model",
)
assert metadata.block_hash == b"test_hash_bytes_1234"
assert metadata.file_size == 1024
assert metadata.token_count == 64
assert metadata.num_layers == 32
assert metadata.model_name == "test-model"
def test_touch(self):
"""Test touch updates last_access."""
metadata = PagedSSDBlockMetadata(
block_hash=b"test_hash_bytes_1234",
file_path=Path("/tmp/test.safetensors"),
file_size=1024,
token_count=64,
created_at=1000.0,
last_access=1000.0,
num_layers=32,
)
old_access = metadata.last_access
time.sleep(0.01)
metadata.touch()
assert metadata.last_access > old_access
def test_to_dict(self):
"""Test converting to dictionary."""
now = time.time()
metadata = PagedSSDBlockMetadata(
block_hash=b"test_hash_bytes_1234",
file_path=Path("/tmp/test.safetensors"),
file_size=1024,
token_count=64,
created_at=now,
last_access=now,
num_layers=32,
model_name="test-model",
block_size=2048,
cache_signature="sig",
layer_cache_types=["KVCache", "ArraysCache"],
layer_meta_states=[(0,), (1, 2, 3, 4)],
)
d = metadata.to_dict()
assert d["block_hash"] == b"test_hash_bytes_1234".hex()
assert d["file_path"] == "/tmp/test.safetensors"
assert d["file_size"] == 1024
assert d["token_count"] == 64
assert d["num_layers"] == 32
assert d["model_name"] == "test-model"
assert d["block_size"] == 2048
assert d["cache_signature"] == "sig"
assert d["layer_cache_types"] == ["KVCache", "ArraysCache"]
assert d["layer_meta_states"] == [[0], [1, 2, 3, 4]]
def test_from_dict(self):
"""Test creating from dictionary."""
d = {
"block_hash": b"test_hash_bytes_1234".hex(),
"file_path": "/tmp/test.safetensors",
"file_size": 1024,
"token_count": 64,
"created_at": 1000.0,
"last_access": 1000.0,
"num_layers": 32,
"model_name": "test-model",
"block_size": 2048,
"cache_signature": "sig",
"layer_cache_types": ["KVCache", "RotatingKVCache"],
"layer_meta_states": [[0], [1, 2, 3, 4]],
}
metadata = PagedSSDBlockMetadata.from_dict(d)
assert metadata.block_hash == b"test_hash_bytes_1234"
assert metadata.file_path == Path("/tmp/test.safetensors")
assert metadata.file_size == 1024
assert metadata.block_size == 2048
assert metadata.cache_signature == "sig"
assert metadata.layer_cache_types == ["KVCache", "RotatingKVCache"]
assert metadata.layer_meta_states == [(0,), (1, 2, 3, 4)]
def test_from_dict_without_optional_fields(self):
"""Test creating from dict without optional fields."""
d = {
"block_hash": b"test_hash".hex(),
"file_path": "/tmp/test.safetensors",
"file_size": 512,
"token_count": 32,
"created_at": 1000.0,
"last_access": 1000.0,
"num_layers": 16,
}
metadata = PagedSSDBlockMetadata.from_dict(d)
assert metadata.model_name == ""
assert metadata.block_size == 0
assert metadata.cache_signature == ""
assert metadata.layer_cache_types is None
assert metadata.layer_meta_states is None
class TestPagedSSDCacheIndex:
"""Tests for PagedSSDCacheIndex (in-memory index)."""
def test_empty_index(self):
"""Test empty index."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
assert index.count == 0
assert index.total_size == 0
def test_add(self):
"""Test adding metadata."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
metadata = PagedSSDBlockMetadata(
block_hash=b"hash1_bytes_padding",
file_path=Path("/tmp/1.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
assert index.count == 1
assert index.total_size == 1024
def test_add_updates_existing(self):
"""Test adding with same hash updates existing entry."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
block_hash = b"same_hash_bytes_pad"
metadata1 = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/1.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
metadata2 = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/2.safetensors"),
file_size=2048,
token_count=128,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata1)
assert index.total_size == 1024
index.add(metadata2)
# Should update, not add
assert index.count == 1
assert index.total_size == 2048
def test_get(self):
"""Test getting metadata by hash."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
block_hash = b"test_get_hash_bytes"
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/test.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
retrieved = index.get(block_hash)
assert retrieved is metadata
# Non-existent
assert index.get(b"nonexistent_hash_by") is None
def test_remove(self):
"""Test removing metadata."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
block_hash = b"test_remove_hash_by"
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/test.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
assert index.count == 1
removed = index.remove(block_hash)
assert removed is metadata
assert index.count == 0
assert index.total_size == 0
def test_remove_nonexistent(self):
"""Test removing nonexistent entry returns None."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
result = index.remove(b"nonexistent_hash_by")
assert result is None
def test_touch(self):
"""Test touching updates LRU order."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
# Add multiple entries
for i in range(3):
metadata = PagedSSDBlockMetadata(
block_hash=f"hash_{i}_bytes_padding".encode()[:20],
file_path=Path(f"/tmp/{i}.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
time.sleep(0.01) # Ensure different access times
# Touch first entry (should move to end of LRU)
first_hash = b"hash_0_bytes_padding"[:20]
index.touch(first_hash)
# Get LRU entries - first hash should not be first anymore
lru_entries = index.get_lru_entries(3)
lru_hashes = [e.block_hash for e in lru_entries]
assert lru_hashes[0] != first_hash
def test_get_lru_entries(self):
"""Test getting LRU entries."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
# Add entries
for i in range(5):
metadata = PagedSSDBlockMetadata(
block_hash=f"hash_{i}_bytes_padding".encode()[:20],
file_path=Path(f"/tmp/{i}.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
time.sleep(0.001)
lru_entries = index.get_lru_entries(3)
assert len(lru_entries) == 3
def test_evict_until_size(self):
"""Test evicting until size limit."""
index = PagedSSDCacheIndex(max_size_bytes=10240)
# Add 5 entries of 1024 bytes each = 5120 total
for i in range(5):
metadata = PagedSSDBlockMetadata(
block_hash=f"hash_{i}_bytes_padding".encode()[:20],
file_path=Path(f"/tmp/{i}.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
assert index.total_size == 5120
# Evict until size is below 3000
evicted = index.evict_until_size(3000)
assert len(evicted) >= 2 # At least 2 entries evicted
assert index.total_size <= 3000
def test_contains(self):
"""Test checking if block exists."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
block_hash = b"contains_test_hash1"
assert not index.contains(block_hash)
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/test.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
assert index.contains(block_hash)
def test_properties(self):
"""Test index properties."""
max_size = 1024**3
index = PagedSSDCacheIndex(max_size_bytes=max_size)
assert index.max_size == max_size
assert index.count == 0
assert index.total_size == 0
# Add some entries
for i in range(3):
metadata = PagedSSDBlockMetadata(
block_hash=f"hash_{i}_bytes_padding".encode()[:20],
file_path=Path(f"/tmp/{i}.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
assert index.count == 3
assert index.total_size == 3072
def test_get_all_hashes(self):
"""Test getting all indexed hashes."""
index = PagedSSDCacheIndex(max_size_bytes=1024**3)
hashes = []
for i in range(3):
block_hash = f"hash_{i}_bytes_padding".encode()[:20]
hashes.append(block_hash)
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path(f"/tmp/{i}.safetensors"),
file_size=1024,
token_count=64,
created_at=time.time(),
last_access=time.time(),
num_layers=32,
)
index.add(metadata)
all_hashes = index.get_all_hashes()
assert len(all_hashes) == 3
for h in hashes:
assert h in all_hashes
class TestPagedSSDCacheManager:
"""Tests for PagedSSDCacheManager."""
def test_initialization(self, tmp_path: Path):
"""Test manager initialization."""
cache_dir = tmp_path / "ssd_cache"
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
)
assert cache_dir.exists()
# Check subdirectories created
for char in "0123456789abcdef":
assert (cache_dir / char).exists()
def test_has_block(self, tmp_path: Path):
"""Test checking if block exists."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
expected_model_name="test-model",
expected_block_size=64,
)
# Non-existent block
assert not manager.has_block(b"nonexistent_hash_by")
def test_delete_block(self, tmp_path: Path):
"""Test deleting a block."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Delete non-existent
result = manager.delete_block(b"nonexistent_hash_by")
assert result is False
def test_forget_block_tracks_file_as_incompatible(self, tmp_path: Path):
"""Forgetting a compatible block keeps it visible to global eviction."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"\x01" * 32
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=tmp_path / "ssd_cache" / "0" / "dummy.safetensors",
file_size=1234,
token_count=1,
created_at=1.0,
last_access=1.0,
num_layers=1,
model_name="test-model",
)
manager._index.add(metadata)
assert manager.forget_block(block_hash) is True
assert not manager._index.contains(block_hash)
assert manager._incompatible_index.contains(block_hash)
assert manager._tracked_ssd_size() == metadata.file_size
def test_clear(self, tmp_path: Path):
"""Test clearing all cache."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
count = manager.clear()
assert count == 0 # Empty cache
def test_get_stats(self, tmp_path: Path):
"""Test getting statistics."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
stats = manager.get_stats()
assert stats.hits == 0
assert stats.misses == 0
assert stats.saves == 0
assert stats.loads == 0
assert stats.errors == 0
def test_get_stats_dict(self, tmp_path: Path):
"""Test getting statistics as dictionary."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
stats_dict = manager.get_stats_dict()
assert "cache_dir" in stats_dict
assert "max_size" in stats_dict
assert "total_size" in stats_dict
assert "num_files" in stats_dict
assert "utilization" in stats_dict
def test_cache_manager_interface(self, tmp_path: Path):
"""Test CacheManager ABC interface."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Test fetch (miss)
value, hit = manager.fetch(b"nonexistent_key_byt")
assert hit is False
assert value is None
# Test evict
result = manager.evict(b"nonexistent_key_byt")
assert result is False
# Test size and max_size
assert manager.size == 0
assert manager.max_size == 1024**3
def test_close(self, tmp_path: Path):
"""Test closing the manager."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Should not raise
manager.close()
def test_repr(self, tmp_path: Path):
"""Test string representation."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
repr_str = repr(manager)
assert "PagedSSDCacheManager" in repr_str
assert "ssd_cache" in repr_str
def test_file_path_generation(self, tmp_path: Path):
"""Test file path generation uses hash-based subdirectory."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Test internal path generation
block_hash = bytes.fromhex("abc123def456" + "00" * 26) # 32 bytes
file_path = manager._get_file_path(block_hash)
# First hex char of hash determines subdirectory
assert file_path.parent.name == "a"
assert file_path.suffix == ".safetensors"
def test_enforce_size_limit(self, tmp_path: Path):
"""Test enforcing size limit."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Should return 0 when under limit
freed = manager.enforce_size_limit()
assert freed == 0
class TestPagedSSDCacheManagerWithMLX:
"""Tests for PagedSSDCacheManager that require MLX.
These tests are skipped if MLX is not available.
"""
@pytest.fixture
def mock_mlx(self):
"""Mock MLX module for testing save/load without actual tensors."""
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
def test_save_and_load_block(self, tmp_path: Path, mock_mlx):
"""Test saving and loading a block with actual tensors."""
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
# Create test cache data
block_hash = b"test_save_load_hash1"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64)))
for _ in range(4) # 4 layers
]
# Save
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)
# Load
loaded = manager.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 4
# Verify shapes
for keys, values in loaded:
assert keys.shape == (1, 8, 64, 64)
assert values.shape == (1, 8, 64, 64)
def test_load_block_with_metadata(self, tmp_path: Path, mock_mlx):
"""Test loading block with metadata."""
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"test_load_meta_hash"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64))) for _ in range(2)
]
manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache", "RotatingKVCache"],
layer_meta_states=[(0,), (1, 256, 64, 0)],
)
# Load with metadata
loaded_data, loaded_meta = manager.load_block_with_metadata(block_hash)
assert loaded_data is not None
assert loaded_meta is not None
assert loaded_meta["num_layers"] == 2
assert loaded_meta["token_count"] == 64
assert loaded_meta["model_name"] == "test-model"
assert loaded_meta["block_size"] == 64
assert loaded_meta["cache_signature"]
assert loaded_meta["layer_cache_types"] == ["KVCache", "RotatingKVCache"]
def test_save_canonicalizes_buffered_rotating_metadata(
self, tmp_path: Path, mock_mlx
):
"""Transient MTP rotating wrappers should not persist in new metadata."""
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"buffered_rotating_hash"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64))),
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64))),
]
manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache", "BufferedRotatingKVCache"],
layer_meta_states=[(64,), ("0", "1024", "2048", "64", "1984", "64")],
)
_, loaded_meta = manager.load_block_with_metadata(block_hash)
assert loaded_meta is not None
assert loaded_meta["layer_cache_types"] == ["KVCache", "RotatingKVCache"]
signature = json.loads(loaded_meta["cache_signature"])
assert signature["layer_cache_types"] == ["KVCache", "RotatingKVCache"]
def test_get_block_metadata(self, tmp_path: Path, mock_mlx):
"""Test getting block metadata without loading data."""
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"test_get_metadata_h"
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=32,
model_name="test-model",
)
metadata = manager.get_block_metadata(block_hash)
assert metadata is not None
assert metadata.block_hash == block_hash
assert metadata.token_count == 32
assert metadata.num_layers == 1
assert metadata.model_name == "test-model"
def test_save_existing_block_touches(self, tmp_path: Path, mock_mlx):
"""Test saving existing block just touches (updates LRU)."""
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"test_touch_existing"
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
# First save
manager.save_block(block_hash, cache_data, 32)
initial_saves = manager._stats["saves"]
# Second save (should just touch)
manager.save_block(block_hash, cache_data, 32)
# saves count should not increase (just hit)
assert manager._stats["saves"] == initial_saves
assert manager._stats["hits"] >= 1
def test_save_writes_format_version(self, tmp_path: Path, mock_mlx):
"""Saved blocks tag the file with the current format version."""
import time as time_mod
from omlx.cache.paged_ssd_cache import _CACHE_FORMAT_VERSION
mx = mock_mlx
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
)
block_hash = b"test_format_version_save"
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
assert manager.save_block(block_hash, cache_data, 32) is True
# Wait for the background writer to flush the file to disk.
file_path = manager._get_file_path(block_hash)
for _ in range(50):
if file_path.exists():
break
time_mod.sleep(0.1)
assert file_path.exists(), "background writer never produced the file"
_, file_metadata = mx.load(str(file_path), return_metadata=True)
assert file_metadata.get("omlx_cache_format_version") == _CACHE_FORMAT_VERSION
def test_unversioned_block_is_rejected_at_index_scan(
self, tmp_path: Path, mock_mlx
):
"""Pre-fix blocks (no version marker) are skipped during scan.
Older builds saved RotatingKVCache layers zero-padded to max_size.
Loading those after the fix would leak zero positions into
attention via BatchRotatingKVCache.merge(). Treat them as a cache
miss by rejecting blocks without the format version.
"""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
cache_dir.mkdir(parents=True, exist_ok=True)
# Hand-write a cache file without the version tag, mirroring what
# an old-format save_block() would produce.
block_hash = b"\x01" * 32
block_hash_hex = block_hash.hex()
# Match the manager's per-prefix subdirectory layout.
sub_dir = cache_dir / block_hash_hex[:2]
sub_dir.mkdir(parents=True, exist_ok=True)
legacy_file = sub_dir / f"{block_hash_hex}.safetensors"
mx.save_safetensors(
str(legacy_file),
{
"layer_0_keys": mx.zeros((1, 8, 32, 64)),
"layer_0_values": mx.zeros((1, 8, 32, 64)),
},
metadata={
# Intentionally missing omlx_cache_format_version.
"block_hash": block_hash_hex,
"token_count": "32",
"num_layers": "1",
"model_name": "legacy-model",
"created_at": "0",
},
)
manager_after_scan = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
)
# Index scan ran in __init__. The legacy file should not appear.
assert not manager_after_scan.has_block(block_hash)
def _write_versioned_fixture_block(
self,
cache_dir: Path,
mx,
block_hash: bytes,
*,
num_layers: int,
model_name: str,
block_size: int = 256,
layer_cache_types: list[str] | None = None,
) -> Path:
"""Drop a minimally-valid versioned block on disk so we can exercise
the startup scan without relying on the background writer."""
from omlx.cache.paged_ssd_cache import _CACHE_FORMAT_VERSION
cache_dir.mkdir(parents=True, exist_ok=True)
block_hash_hex = block_hash.hex()
sub_dir = cache_dir / block_hash_hex[0]
sub_dir.mkdir(parents=True, exist_ok=True)
file_path = sub_dir / f"{block_hash_hex}.safetensors"
tensors = {}
for i in range(num_layers):
tensors[f"layer_{i}_keys"] = mx.zeros((1, 8, 32, 64))
tensors[f"layer_{i}_values"] = mx.zeros((1, 8, 32, 64))
if layer_cache_types is None:
layer_cache_types = ["KVCache"] * num_layers
mx.save_safetensors(
str(file_path),
tensors,
metadata={
"omlx_cache_format_version": _CACHE_FORMAT_VERSION,
"block_hash": block_hash_hex,
"token_count": "32",
"num_layers": str(num_layers),
"model_name": model_name,
"block_size": str(block_size),
"cache_signature": _cache_compat_signature(
model_name=model_name,
num_layers=num_layers,
block_size=block_size,
layer_cache_types=layer_cache_types,
),
"layer_cache_types": json.dumps(layer_cache_types),
"created_at": "0",
},
)
return file_path
def test_scan_skips_layer_count_mismatch_without_unlinking(
self, tmp_path: Path, mock_mlx
):
"""Blocks with num_layers != expected_num_layers are not indexed.
Models that change their effective layer count across versions (e.g.,
#1404 attaching MTPModule changed 30 -> 40) should not hit the
layer-mismatch reject path on every prefix lookup. The file is still
left on disk so shared cache directories are non-destructive.
"""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
stale_hash = b"\x10" + b"\x00" * 31
fresh_hash = b"\x20" + b"\x00" * 31
stale_path = self._write_versioned_fixture_block(
cache_dir, mx, stale_hash, num_layers=30, model_name="qwen3.6"
)
fresh_path = self._write_versioned_fixture_block(
cache_dir, mx, fresh_hash, num_layers=40, model_name="qwen3.6"
)
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
expected_model_name="qwen3.6",
expected_num_layers=40,
expected_block_size=256,
)
assert stale_path.exists()
assert fresh_path.exists()
assert not manager.has_block(stale_hash)
assert manager.has_block(fresh_hash)
def test_scan_skips_model_name_mismatch_without_unlinking(
self, tmp_path: Path, mock_mlx
):
"""Blocks from a different model stay on disk but are not indexed."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
other_hash = b"\x30" + b"\x00" * 31
match_hash = b"\x40" + b"\x00" * 31
other_path = self._write_versioned_fixture_block(
cache_dir, mx, other_hash, num_layers=40, model_name="llama"
)
match_path = self._write_versioned_fixture_block(
cache_dir, mx, match_hash, num_layers=40, model_name="qwen3.6"
)
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
expected_model_name="qwen3.6",
expected_num_layers=40,
expected_block_size=256,
)
assert other_path.exists()
assert match_path.exists()
assert not manager.has_block(other_hash)
assert manager.has_block(match_hash)
def test_scan_skips_block_size_mismatch_without_unlinking(
self, tmp_path: Path, mock_mlx
):
"""Blocks with another paged cache block size are not indexed."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
wrong_hash = b"\x41" + b"\x00" * 31
match_hash = b"\x42" + b"\x00" * 31
wrong_path = self._write_versioned_fixture_block(
cache_dir,
mx,
wrong_hash,
num_layers=40,
model_name="qwen3.6",
block_size=2048,
)
match_path = self._write_versioned_fixture_block(
cache_dir,
mx,
match_hash,
num_layers=40,
model_name="qwen3.6",
block_size=256,
)
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
expected_model_name="qwen3.6",
expected_num_layers=40,
expected_block_size=256,
)
assert wrong_path.exists()
assert match_path.exists()
assert not manager.has_block(wrong_hash)
assert manager.has_block(match_hash)
def test_scan_keeps_blocks_when_expected_fields_unset(
self, tmp_path: Path, mock_mlx
):
"""Backwards compatibility: callers that omit the new init args see
no behavior change. All blocks survive scan regardless of metadata."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
h1 = b"\x50" + b"\x00" * 31
h2 = b"\x60" + b"\x00" * 31
p1 = self._write_versioned_fixture_block(
cache_dir, mx, h1, num_layers=30, model_name="a"
)
p2 = self._write_versioned_fixture_block(
cache_dir, mx, h2, num_layers=40, model_name="b"
)
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
)
assert p1.exists()
assert p2.exists()
assert manager.has_block(h1)
assert manager.has_block(h2)
def test_scan_logs_skipped_incompatible_count(
self, tmp_path: Path, mock_mlx, caplog
):
"""The completion log line surfaces incompatible blocks skipped at scan."""
import logging
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
for i in range(3):
self._write_versioned_fixture_block(
cache_dir,
mx,
bytes([0x70 + i]) + b"\x00" * 31,
num_layers=30,
model_name="old",
)
with caplog.at_level(logging.INFO, logger="omlx.cache.paged_ssd_cache"):
PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
expected_model_name="old",
expected_num_layers=40,
expected_block_size=256,
)
scan_lines = [
r.message for r in caplog.records if "SSD cache scan complete" in r.message
]
assert scan_lines, "scan completion log not emitted"
assert "skipped_incompatible=3 blocks" in scan_lines[-1]
def test_model_switch_enforces_shared_ssd_limit_on_new_save(
self, tmp_path: Path, mock_mlx
):
"""Incompatible old-model blocks count against the shared SSD budget."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
old_hash = b"\x80" + b"\x00" * 31
old_path = self._write_versioned_fixture_block(
cache_dir,
mx,
old_hash,
num_layers=1,
model_name="old-model",
)
old_size = old_path.stat().st_size
max_size = old_size + old_size // 2
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=max_size,
expected_model_name="new-model",
expected_num_layers=1,
expected_block_size=256,
)
try:
assert not manager.has_block(old_hash)
assert old_path.exists()
assert manager._incompatible_index.total_size == old_size
new_hash = b"\x81" + b"\x00" * 31
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
assert manager.save_block(
block_hash=new_hash,
cache_data=cache_data,
token_count=32,
model_name="new-model",
layer_cache_types=["KVCache"],
)
assert manager.has_block(new_hash)
assert not old_path.exists()
assert manager._incompatible_index.total_size == 0
assert manager._tracked_ssd_size() <= max_size
finally:
manager.close()
def test_scan_cleans_oldest_incompatible_blocks_when_over_limit(
self, tmp_path: Path, mock_mlx
):
"""Startup cleanup converges an already-over-limit shared cache dir."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
paths = []
for i in range(3):
path = self._write_versioned_fixture_block(
cache_dir,
mx,
bytes([0x90 + i]) + b"\x00" * 31,
num_layers=1,
model_name="old-model",
)
os.utime(path, (100 + i, 100 + i))
paths.append(path)
sizes = [path.stat().st_size for path in paths]
max_size = sum(sizes) - sizes[0] + 1
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=max_size,
expected_model_name="new-model",
expected_num_layers=1,
expected_block_size=256,
)
try:
assert not paths[0].exists()
assert paths[1].exists()
assert paths[2].exists()
assert manager._tracked_ssd_size() <= max_size
finally:
manager.close()
def test_clear_removes_incompatible_scanned_files(self, tmp_path: Path, mock_mlx):
"""Global clear deletes compatible and incompatible tracked files."""
mx = mock_mlx
cache_dir = tmp_path / "ssd_cache"
other_hash = b"\xa0" + b"\x00" * 31
match_hash = b"\xa1" + b"\x00" * 31
other_path = self._write_versioned_fixture_block(
cache_dir,
mx,
other_hash,
num_layers=1,
model_name="old-model",
)
match_path = self._write_versioned_fixture_block(
cache_dir,
mx,
match_hash,
num_layers=1,
model_name="new-model",
)
manager = PagedSSDCacheManager(
cache_dir=cache_dir,
max_size_bytes=1024**3,
expected_model_name="new-model",
expected_num_layers=1,
expected_block_size=256,
)
try:
assert manager.clear() == 2
assert not other_path.exists()
assert not match_path.exists()
assert manager._tracked_ssd_count() == 0
finally:
manager.close()
class TestPagedSSDCacheManagerCacheList:
"""Tests for CacheList support in PagedSSDCacheManager."""
@pytest.fixture
def mx(self):
"""Import MLX or skip."""
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
@pytest.fixture
def ssd_cache(self, tmp_path):
"""Create a PagedSSDCacheManager for testing."""
return PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**2,
)
def test_save_load_cache_list_block(self, ssd_cache, mx):
"""Test saving and loading a block with CacheList data."""
block_hash = b"cache_list_test_hash"
# Build cache_data with CacheList marker
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)),
), # Standard KVCache layer
]
layer_cache_types = ["CacheList", "KVCache"]
result = ssd_cache.save_block(
block_hash,
cache_data,
token_count=32,
model_name="test",
layer_cache_types=layer_cache_types,
)
assert result is True
# Load back
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 2
# First layer should be List[Tuple] (CacheList)
assert isinstance(loaded[0], list)
assert len(loaded[0]) == 2
assert loaded[0][0][0].shape == (1, 8, 32, 64)
assert loaded[0][1][0].shape == (1, 4, 32, 64)
# Second layer should be tuple (KVCache)
assert isinstance(loaded[1], tuple)
assert loaded[1][0].shape == (1, 8, 32, 64)
def test_save_load_cache_list_placeholder(self, ssd_cache, mx):
"""Test saving and loading placeholder CacheList block."""
block_hash = b"placeholder_cl_hash_"
# Non-last block: CacheList gets standard placeholder
cache_data = [
(mx.zeros((1,)), mx.zeros((1,))), # CacheList placeholder
(mx.zeros((1, 8, 32, 64)), mx.ones((1, 8, 32, 64))), # KVCache
]
layer_cache_types = ["CacheList", "KVCache"]
result = ssd_cache.save_block(
block_hash,
cache_data,
token_count=32,
model_name="test",
layer_cache_types=layer_cache_types,
)
assert result is True
# Load back — CacheList placeholder loads as standard (keys, values) tuple
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 2
# Placeholder has no sub_count, so loads as standard tuple
assert isinstance(loaded[0], tuple)
assert loaded[0][0].shape == (1,)
def test_load_block_with_metadata_cache_list(self, ssd_cache, mx):
"""Test load_block_with_metadata for CacheList blocks."""
block_hash = b"cl_metadata_test_ha_"
sub_keys = mx.zeros((1, 8, 64, 64))
sub_values = mx.ones((1, 8, 64, 64))
cache_data = [
("__cache_list__", [(sub_keys, sub_values)]),
]
layer_cache_types = ["CacheList"]
layer_meta_states = [
(["KVCache"], [(64,)]), # CacheList meta_state format
]
ssd_cache.save_block(
block_hash,
cache_data,
token_count=64,
model_name="test",
layer_cache_types=layer_cache_types,
layer_meta_states=layer_meta_states,
)
loaded_data, metadata = ssd_cache.load_block_with_metadata(block_hash)
assert loaded_data is not None
assert metadata is not None
assert len(loaded_data) == 1
assert isinstance(loaded_data[0], list)
assert len(loaded_data[0]) == 1
assert loaded_data[0][0][0].shape == (1, 8, 64, 64)
assert metadata["layer_cache_types"] == ["CacheList"]
def test_save_load_cache_list_with_zero_dim_values(self, ssd_cache, mx):
"""Test round-trip for CacheList where sub-cache has zero-dim values.
This covers the deepseek_v32 / GLM-5 case where the DSA indexer
sub-cache stores values with shape (B, 1, N, 0) — head_dim=0.
"""
block_hash = b"zero_dim_cl_test_ha_"
sub_keys1 = mx.zeros((1, 1, 64, 512)) # Main attention kv_latent
sub_values1 = mx.zeros((1, 1, 64, 64)) # Main attention k_pe
sub_keys2 = mx.zeros((1, 1, 64, 128)) # Indexer keys
sub_values2 = mx.zeros((1, 1, 64, 0)) # Indexer values (zero head_dim)
cache_data = [
(
"__cache_list__",
[
(sub_keys1, sub_values1),
(sub_keys2, sub_values2),
],
),
]
layer_cache_types = ["CacheList"]
result = ssd_cache.save_block(
block_hash,
cache_data,
token_count=64,
model_name="test",
layer_cache_types=layer_cache_types,
)
assert result is True
# Load back and verify round-trip correctness
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 1
assert isinstance(loaded[0], list)
assert len(loaded[0]) == 2
# Sub-cache 0: normal tensors preserved
assert loaded[0][0][0].shape == (1, 1, 64, 512)
assert loaded[0][0][1].shape == (1, 1, 64, 64)
# Sub-cache 1: keys normal, values zero-dim reconstructed
assert loaded[0][1][0].shape == (1, 1, 64, 128)
assert loaded[0][1][1].shape == (1, 1, 64, 0)
def test_save_load_zero_dim_with_load_block_with_metadata(self, ssd_cache, mx):
"""Test load_block_with_metadata also handles zero-dim tensors."""
block_hash = b"zero_dim_meta_test_h"
sub_keys = mx.zeros((1, 1, 32, 128))
sub_values = mx.zeros((1, 1, 32, 0))
cache_data = [
("__cache_list__", [(sub_keys, sub_values)]),
]
layer_cache_types = ["CacheList"]
layer_meta_states = [
(["KVCache"], [(32,)]),
]
ssd_cache.save_block(
block_hash,
cache_data,
token_count=32,
model_name="test",
layer_cache_types=layer_cache_types,
layer_meta_states=layer_meta_states,
)
loaded_data, metadata = ssd_cache.load_block_with_metadata(block_hash)
assert loaded_data is not None
assert metadata is not None
assert len(loaded_data) == 1
assert isinstance(loaded_data[0], list)
assert loaded_data[0][0][0].shape == (1, 1, 32, 128)
assert loaded_data[0][0][1].shape == (1, 1, 32, 0)
class TestAsyncWriteAndTimeoutLoad:
"""Tests for the async write / timeout load deadlock fix.
These tests verify:
- save_block() returns immediately (non-blocking)
- Pending writes are served on load (zero I/O)
- Load timeout returns None (cache miss) instead of blocking
- Writer thread errors clean up index entries
- close() gracefully shuts down background threads
"""
@pytest.fixture
def mx(self):
"""Import MLX or skip."""
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
@pytest.fixture
def ssd_cache(self, tmp_path):
"""Create a PagedSSDCacheManager for testing."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**2,
)
yield manager
manager.close()
def test_save_block_non_blocking(self, ssd_cache, mx, tmp_path):
"""Verify save_block() returns immediately and file appears async."""
block_hash = b"async_save_test_hash"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.zeros((1, 8, 64, 64))) for _ in range(4)
]
t0 = time.time()
result = ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache"] * 4,
)
elapsed = time.time() - t0
assert result is True
# save_block should return almost instantly (< 1s),
# not wait for disk I/O
assert elapsed < 1.0
# Block should be in index (optimistic update)
assert ssd_cache.has_block(block_hash)
# Wait for background writer to finish
import time as time_mod
for _ in range(50): # Wait up to 5s
file_path = ssd_cache._get_file_path(block_hash)
if file_path.exists():
break
time_mod.sleep(0.1)
assert file_path.exists(), "File should appear after background write"
def test_pending_writes_served_on_load(self, ssd_cache, mx):
"""Verify that a block saved then immediately loaded is served from memory."""
block_hash = b"pending_load_test_ha"
cache_data = [
(mx.zeros((1, 8, 32, 64)), mx.ones((1, 8, 32, 64))) for _ in range(2)
]
ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=32,
model_name="test-model",
layer_cache_types=["KVCache", "KVCache"],
)
# Immediately load — should come from _pending_writes, not disk
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 2
assert loaded[0][0].shape == (1, 8, 32, 64)
assert loaded[0][1].shape == (1, 8, 32, 64)
def test_pending_writes_served_on_load_with_metadata(self, ssd_cache, mx):
"""Verify load_block_with_metadata also reads from pending writes."""
block_hash = b"pending_meta_test_ha"
cache_data = [(mx.zeros((1, 4, 16, 32)), mx.zeros((1, 4, 16, 32)))]
ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test-model",
layer_cache_types=["KVCache"],
layer_meta_states=[(16,)],
)
loaded_data, metadata = ssd_cache.load_block_with_metadata(block_hash)
assert loaded_data is not None
assert metadata is not None
assert metadata["num_layers"] == 1
assert metadata["token_count"] == 16
assert metadata["model_name"] == "test-model"
assert metadata["layer_cache_types"] == ["KVCache"]
def test_load_error_returns_none(self, ssd_cache, mx):
"""Verify that a corrupted file returns None and cleans up index."""
block_hash = b"error_test_hash_1234"
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
# Save and wait for background write to complete
ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=32,
)
import time as time_mod
for _ in range(50):
with ssd_cache._pending_write_hashes_lock:
if block_hash not in ssd_cache._pending_write_hashes:
break
time_mod.sleep(0.1)
# Remove from hot cache buffer so load goes to disk
ssd_cache._hot_cache_remove(block_hash)
# Mock mx.load to simulate a corrupted file
with patch("mlx.core.load", side_effect=OSError("corrupted file")):
loaded = ssd_cache.load_block(block_hash)
assert loaded is None # Should return None, not raise
# Block should be removed from index (corrupted entry cleanup)
assert not ssd_cache.has_block(block_hash)
def test_load_no_executor_deadlock(self, ssd_cache, mx):
"""Regression test: _load_executor must not exist (prevents deadlock)."""
# The old implementation used ThreadPoolExecutor(max_workers=1) which
# caused deadlocks when mx.load() in a worker thread contested Metal
# GPU resources with the main inference thread. Verify it's gone.
assert not hasattr(
ssd_cache, "_load_executor"
), "_load_executor should not exist — it causes Metal GPU deadlocks"
def test_sequential_loads_no_queue_blocking(self, ssd_cache, mx):
"""Regression test: consecutive loads must not block each other."""
import time as time_mod
# Save 5 different blocks
hashes = []
for i in range(5):
block_hash = f"seq_load_test_{i:04d}_".encode()[:20]
hashes.append(block_hash)
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
ssd_cache.save_block(block_hash, cache_data, token_count=32)
# Wait for all pending writes to flush
for _ in range(100):
with ssd_cache._pending_write_hashes_lock:
if not ssd_cache._pending_write_hashes:
break
time_mod.sleep(0.1)
# Load all 5 blocks sequentially — should complete quickly
t0 = time_mod.time()
for block_hash in hashes:
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None, f"Failed to load {block_hash!r}"
assert len(loaded) == 1
elapsed = time_mod.time() - t0
# 5 loads from SSD should complete in well under 5s
# (each ~2ms read + reconstruction)
assert (
elapsed < 5.0
), f"Sequential loads took {elapsed:.1f}s — possible queue blocking"
def test_writer_error_handling(self, ssd_cache, mx):
"""Verify that background writer errors clean up the index."""
block_hash = b"writer_error_test_ha"
cache_data = [(mx.zeros((1, 4, 16, 32)), mx.zeros((1, 4, 16, 32)))]
# Patch _write_safetensors_no_mx to simulate disk error in background writer
import time as time_mod
with patch(
"omlx.cache.paged_ssd_cache._write_safetensors_no_mx",
side_effect=OSError("Disk full"),
):
result = ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
)
# save_block() succeeds (bytes extracted, queued for background write)
assert result is True
# Wait for background writer to process and fail
for _ in range(50):
if ssd_cache._write_queue.empty():
break
time_mod.sleep(0.05)
time_mod.sleep(0.1)
# Background writer should have removed the block from index on error
assert not ssd_cache.has_block(block_hash)
# And from pending write hashes
with ssd_cache._pending_write_hashes_lock:
assert block_hash not in ssd_cache._pending_write_hashes
def test_writer_enospc_logs_disk_full(self, ssd_cache, mx, caplog):
"""ENOSPC errors should log 'disk full' warning, not generic error."""
block_hash = b"enospc_test_hash_123"
cache_data = [(mx.zeros((1, 4, 16, 32)), mx.zeros((1, 4, 16, 32)))]
enospc = OSError("No space left on device")
enospc.errno = errno.ENOSPC
import time as time_mod
with (
patch(
"omlx.cache.paged_ssd_cache._write_safetensors_no_mx",
side_effect=enospc,
),
caplog.at_level(logging.WARNING),
):
ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
)
for _ in range(50):
if ssd_cache._write_queue.empty():
break
time_mod.sleep(0.05)
time_mod.sleep(0.1)
assert "SSD cache disk full" in caplog.text
def test_graceful_shutdown(self, tmp_path, mx):
"""Verify close() stops the writer thread."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "shutdown_cache",
max_size_bytes=100 * 1024**2,
)
# Save a block to ensure writer is active
block_hash = b"shutdown_test_hash_1"
cache_data = [(mx.zeros((1, 4, 16, 32)), mx.zeros((1, 4, 16, 32)))]
manager.save_block(block_hash, cache_data, 16)
# Close should stop the writer thread
manager.close()
assert not manager._writer_thread.is_alive()
def test_save_existing_block_still_touches(self, ssd_cache, mx):
"""Verify saving an existing block just touches LRU (unchanged behavior)."""
block_hash = b"touch_existing_test_"
cache_data = [(mx.zeros((1, 8, 32, 64)), mx.zeros((1, 8, 32, 64)))]
ssd_cache.save_block(block_hash, cache_data, 32)
initial_saves = ssd_cache._stats["saves"]
# Second save should just touch, not re-enqueue
ssd_cache.save_block(block_hash, cache_data, 32)
assert ssd_cache._stats["saves"] == initial_saves
assert ssd_cache._stats["hits"] >= 1
def test_save_and_load_round_trip_after_flush(self, ssd_cache, mx):
"""Verify full round-trip: save -> flush -> load from disk."""
import time as time_mod
block_hash = b"round_trip_flush_tes"
cache_data = [
(mx.zeros((1, 8, 64, 64)), mx.ones((1, 8, 64, 64))) for _ in range(4)
]
ssd_cache.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache"] * 4,
)
# Wait for background write to complete
for _ in range(50):
with ssd_cache._pending_write_hashes_lock:
if block_hash not in ssd_cache._pending_write_hashes:
break
time_mod.sleep(0.1)
# Remove from hot cache buffer so load goes to disk
ssd_cache._hot_cache_remove(block_hash)
# Now load should come from disk, not pending writes
loaded = ssd_cache.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 4
for keys, values in loaded:
assert keys.shape == (1, 8, 64, 64)
assert values.shape == (1, 8, 64, 64)
# =============================================================================
# Async Background Write Tests
# =============================================================================
@pytest.mark.skipif(not _has_mlx(), reason="MLX not available")
class TestAsyncBackgroundWrite:
"""Tests for the async background write pipeline (no-mx safetensors)."""
@pytest.fixture
def mx(self):
import mlx.core as mx
return mx
def test_extract_and_restore_float32(self, mx):
"""Round-trip test for float32 tensors."""
original = mx.random.normal((2, 4, 8))
mx.eval(original)
raw, dtype_str, shape = _extract_tensor_bytes(original)
assert dtype_str == "F32"
assert shape == [2, 4, 8]
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
assert restored.dtype == mx.float32
assert restored.shape == (2, 4, 8)
assert mx.allclose(original, restored).item()
def test_extract_and_restore_float16(self, mx):
"""Round-trip test for float16 tensors."""
original = mx.random.normal((3, 5)).astype(mx.float16)
mx.eval(original)
raw, dtype_str, shape = _extract_tensor_bytes(original)
assert dtype_str == "F16"
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
assert restored.dtype == mx.float16
assert mx.allclose(original, restored).item()
def test_extract_and_restore_bfloat16(self, mx):
"""Round-trip test for bfloat16 tensors (the key dtype for this feature)."""
original = mx.random.normal((4, 8, 16)).astype(mx.bfloat16)
mx.eval(original)
raw, dtype_str, shape = _extract_tensor_bytes(original)
assert dtype_str == "BF16"
assert shape == [4, 8, 16]
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
assert restored.dtype == mx.bfloat16
assert restored.shape == (4, 8, 16)
# Compare as float32 to avoid bfloat16 precision issues
assert mx.allclose(
original.astype(mx.float32), restored.astype(mx.float32)
).item()
def test_extract_and_restore_int_types(self, mx):
"""Round-trip test for integer dtypes."""
for mx_dtype, st_str in [
(mx.int8, "I8"),
(mx.int32, "I32"),
(mx.uint8, "U8"),
]:
original = mx.array([1, 2, 3, 4], dtype=mx_dtype)
mx.eval(original)
raw, dtype_str, shape = _extract_tensor_bytes(original)
assert dtype_str == st_str
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
assert restored.dtype == mx_dtype
assert mx.array_equal(original, restored).item()
def test_extract_materializes_lazy_slice(self, mx):
"""_extract_tensor_bytes handles lazy block slices."""
base = mx.arange(1 * 2 * 16 * 4, dtype=mx.float32).reshape(1, 2, 16, 4)
mx.eval(base)
lazy_slice = base[:, :, 3:11, :]
raw, dtype_str, shape = _extract_tensor_bytes(lazy_slice)
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
expected = base[:, :, 3:11, :]
mx.eval(expected)
assert dtype_str == "F32"
assert shape == [1, 2, 8, 4]
assert mx.allclose(expected, restored).item()
def test_extract_materializes_lazy_bfloat16_slice(self, mx):
"""_extract_tensor_bytes handles lazy bf16 slices and uint16 views."""
base = mx.arange(1 * 2 * 12 * 4, dtype=mx.float32).reshape(1, 2, 12, 4)
base = base.astype(mx.bfloat16)
mx.eval(base)
lazy_slice = base[:, :, 2:10, :]
raw, dtype_str, shape = _extract_tensor_bytes(lazy_slice)
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
expected = base[:, :, 2:10, :]
mx.eval(expected)
assert dtype_str == "BF16"
assert shape == [1, 2, 8, 4]
assert restored.dtype == mx.bfloat16
assert mx.allclose(
expected.astype(mx.float32), restored.astype(mx.float32)
).item()
def test_extract_materializes_lazy_clone(self, mx):
"""_extract_tensor_bytes handles block-like lazy clone/copy tensors."""
base = mx.arange(1 * 2 * 16 * 4, dtype=mx.float32).reshape(1, 2, 16, 4)
mx.eval(base)
tensor = base[:, :, 4:12, :]
if hasattr(mx, "copy"):
cloned = mx.copy(tensor)
elif hasattr(tensor, "copy"):
cloned = tensor.copy()
else:
cloned = mx.array(tensor)
raw, dtype_str, shape = _extract_tensor_bytes(cloned)
restored = _restore_tensor_from_bytes(raw, dtype_str, shape)
expected = base[:, :, 4:12, :]
mx.eval(expected)
assert dtype_str == "F32"
assert shape == [1, 2, 8, 4]
assert mx.allclose(expected, restored).item()
def test_write_safetensors_no_mx_roundtrip(self, mx, tmp_path):
"""Write safetensors without mx API, then load with mx.load()."""
t1 = mx.random.normal((2, 3, 4))
t2 = mx.ones((5,), dtype=mx.float16)
mx.eval(t1, t2)
tensors_raw = {
"tensor_a": _extract_tensor_bytes(t1),
"tensor_b": _extract_tensor_bytes(t2),
}
metadata = {"test_key": "test_value", "block_hash": "abc123"}
out_path = str(tmp_path / "test.safetensors")
file_size = _write_safetensors_no_mx(out_path, tensors_raw, metadata)
assert file_size > 0
# Load with mx.load and verify
loaded_arrays, loaded_meta = mx.load(out_path, return_metadata=True)
assert "tensor_a" in loaded_arrays
assert "tensor_b" in loaded_arrays
assert loaded_meta["test_key"] == "test_value"
assert loaded_meta["block_hash"] == "abc123"
assert mx.allclose(t1, loaded_arrays["tensor_a"]).item()
assert mx.allclose(t2, loaded_arrays["tensor_b"]).item()
def test_write_safetensors_bfloat16_roundtrip(self, mx, tmp_path):
"""Verify bfloat16 safetensors file is loadable by mx.load."""
original = mx.random.normal((8, 16, 32)).astype(mx.bfloat16)
mx.eval(original)
tensors_raw = {"kv_cache": _extract_tensor_bytes(original)}
out_path = str(tmp_path / "bf16_test.safetensors")
_write_safetensors_no_mx(out_path, tensors_raw)
loaded, _ = mx.load(out_path, return_metadata=True)
assert loaded["kv_cache"].dtype == mx.bfloat16
assert loaded["kv_cache"].shape == (8, 16, 32)
assert mx.allclose(
original.astype(mx.float32),
loaded["kv_cache"].astype(mx.float32),
).item()
def test_save_block_uses_background_write(self, tmp_path, mx):
"""Verify save_block enqueues bytes for background writer (no mx.save_safetensors)."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "async_test",
max_size_bytes=100 * 1024**2,
)
block_hash = b"async_write_test_hsh"
cache_data = [(mx.ones((1, 4, 16, 32)), mx.zeros((1, 4, 16, 32)))]
# Patch mx.save_safetensors to ensure it's NOT called
with patch("mlx.core.save_safetensors") as mock_save:
result = manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
)
assert result is True
# mx.save_safetensors should NOT be called (we use _write_safetensors_no_mx)
mock_save.assert_not_called()
# Hot cache buffer should store tensors_raw (bytes), not arrays (mx.array)
with manager._hot_cache_lock:
pending = manager._hot_cache.get(block_hash)
assert pending is not None
assert "tensors_raw" in pending
assert "arrays" not in pending # Old key should not exist
# Wait for background write and verify file exists
for _ in range(50):
file_path = manager._get_file_path(block_hash)
if file_path.exists():
break
time.sleep(0.05)
assert file_path.exists()
# Verify file is loadable by mx.load. V3 stores state elements as
# ``layer_{i}_state_{k}`` keys with a ``layer_{i}_state_count`` meta
# entry, polyfilled from V2 ``(keys, values)`` 2-tuples on save.
loaded, meta = mx.load(str(file_path), return_metadata=True)
assert "layer_0_state_0" in loaded
assert "layer_0_state_1" in loaded
assert meta.get("layer_0_state_count") == "2"
assert meta["block_hash"] == block_hash.hex()
manager.close()
def test_pending_writes_bytes_readback(self, tmp_path, mx):
"""Verify load_block can restore mx.arrays from bytes-based pending_writes."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "readback_test",
max_size_bytes=100 * 1024**2,
)
block_hash = b"readback_test_hash__"
original_keys = mx.random.normal((1, 8, 32, 64))
original_values = mx.random.normal((1, 8, 32, 64))
mx.eval(original_keys, original_values)
cache_data = [(original_keys, original_values)]
manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=32,
)
# Load immediately from pending_writes (before background write completes)
loaded = manager.load_block(block_hash)
assert loaded is not None
assert len(loaded) == 1
keys, values = loaded[0]
assert mx.allclose(original_keys, keys).item()
assert mx.allclose(original_values, values).item()
manager.close()
def test_index_update_file_size(self):
"""Verify PagedSSDCacheIndex.update_file_size works correctly."""
index = PagedSSDCacheIndex(max_size_bytes=1000)
block_hash = b"size_update_test____"
metadata = PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/test.safetensors"),
file_size=100,
token_count=16,
created_at=time.time(),
last_access=time.time(),
num_layers=1,
)
index.add(metadata)
assert index.total_size == 100
# Update to actual size
index.update_file_size(block_hash, 150)
assert index.total_size == 150
# Non-existent hash should be no-op
index.update_file_size(b"nonexistent_hash____", 999)
assert index.total_size == 150
class TestEffectiveMaxSize:
"""Tests for dynamic effective max size based on disk free space."""
def _make_disk_usage(self, total: int, used: int, free: int):
"""Create a mock disk_usage result."""
return shutil._ntuple_diskusage(total, used, free)
def test_effective_max_size_disk_sufficient(self, tmp_path: Path):
"""When disk has plenty of free space, effective = configured max."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3, # 100GB configured
)
# Mock: 500GB free, cache is empty (0 bytes)
mock_usage = self._make_disk_usage(
total=1000 * 1024**3, used=500 * 1024**3, free=500 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage):
effective = manager._get_effective_max_size()
# disk_available = 0 + 500GB = 500GB, disk_limit = 495GB
# effective = min(100GB, 495GB) = 100GB
assert effective == 100 * 1024**3
def test_effective_max_size_disk_low(self, tmp_path: Path):
"""When disk is low, effective shrinks below configured max."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=110 * 1024**3, # 110GB configured
)
# Simulate: cache currently has 10GB, disk free is 90GB
# So disk_available = 10GB + 90GB = 100GB
manager._index._total_size = 10 * 1024**3
mock_usage = self._make_disk_usage(
total=500 * 1024**3, used=410 * 1024**3, free=90 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage):
effective = manager._get_effective_max_size()
# disk_limit = int(100GB * 0.99) = 99GB
# effective = min(110GB, 99GB) = 99GB
expected = int(100 * 1024**3 * 0.99)
assert effective == expected
def test_effective_max_size_oserror_fallback(self, tmp_path: Path):
"""When disk_usage fails, fall back to configured max."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=50 * 1024**3,
)
with patch("shutil.disk_usage", side_effect=OSError("disk error")):
effective = manager._get_effective_max_size()
assert effective == 50 * 1024**3
def test_effective_max_size_cache_30s(self, tmp_path: Path):
"""disk_usage result is cached for 30 seconds."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3,
)
mock_usage = self._make_disk_usage(
total=1000 * 1024**3, used=500 * 1024**3, free=500 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage) as mock_du:
# First call — should invoke disk_usage
manager._get_effective_max_size()
assert mock_du.call_count == 1
# Second call within 30s — should use cache
manager._get_effective_max_size()
assert mock_du.call_count == 1
# Expire cache by rewinding timestamp
manager._disk_usage_cache_time -= 31.0
# Third call — should invoke disk_usage again
manager._get_effective_max_size()
assert mock_du.call_count == 2
def test_utilization_never_exceeds_1(self, tmp_path: Path):
"""Utilization should never exceed 1.0 with effective max size."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3,
)
# Simulate: cache has 50GB, but disk only has 10GB free
# So disk_available = 50GB + 10GB = 60GB, disk_limit = ~59.4GB
manager._index._total_size = 50 * 1024**3
mock_usage = self._make_disk_usage(
total=200 * 1024**3, used=190 * 1024**3, free=10 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage):
stats = manager.get_stats_dict()
assert stats["utilization"] <= 1.0
assert stats["max_size"] < stats["configured_max_size"]
def test_effective_max_size_uses_tracked_cache_not_filesystem_total(
self, tmp_path: Path
):
"""Disk pressure depends on tracked cache bytes plus free space."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3,
)
manager._index._total_size = 50 * 1024**3
manager._incompatible_index._total_size = 10 * 1024**3
mock_usage = self._make_disk_usage(
total=200 * 1024**3,
used=190 * 1024**3,
free=10 * 1024**3,
)
with patch("shutil.disk_usage", return_value=mock_usage):
effective = manager._get_effective_max_size()
expected = int(70 * 1024**3 * 0.99)
assert effective == expected
assert effective < manager.configured_max_size
def test_stats_includes_effective_and_configured(self, tmp_path: Path):
"""Stats should include both effective and configured max sizes."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3,
)
mock_usage = self._make_disk_usage(
total=500 * 1024**3, used=450 * 1024**3, free=50 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage):
stats_dict = manager.get_stats_dict()
stats_obj = manager.get_stats()
# Dict format
assert "configured_max_size" in stats_dict
assert stats_dict["configured_max_size"] == 100 * 1024**3
# Dataclass format
assert stats_obj.configured_max_size_bytes == 100 * 1024**3
assert stats_obj.max_size_bytes > 0
def test_max_size_property_returns_effective(self, tmp_path: Path):
"""max_size property should return effective (not configured) value."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=200 * 1024**3,
)
# disk_available = 0 + 50GB = 50GB, disk_limit = ~49.5GB
mock_usage = self._make_disk_usage(
total=500 * 1024**3, used=450 * 1024**3, free=50 * 1024**3
)
with patch("shutil.disk_usage", return_value=mock_usage):
assert manager.max_size < 200 * 1024**3
assert manager.configured_max_size == 200 * 1024**3
def test_oserror_fallback_logs_warning(self, tmp_path: Path, caplog):
"""disk_usage failure should log a warning, not fail silently."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=50 * 1024**3,
)
# Expire cache so next call hits disk_usage
manager._disk_usage_cache_time -= 31.0
with (
patch("shutil.disk_usage", side_effect=OSError("mount gone")),
caplog.at_level(logging.WARNING),
):
effective = manager._get_effective_max_size()
assert effective == 50 * 1024**3
assert "Failed to check disk usage" in caplog.text
def test_disk_pressure_warning(self, tmp_path: Path, caplog):
"""Warn when effective max drops below 10% of configured max."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=100 * 1024**3,
)
# Simulate nearly full disk: only 5GB free, cache has 0 bytes
mock_usage = self._make_disk_usage(
total=500 * 1024**3, used=495 * 1024**3, free=5 * 1024**3
)
with (
patch("shutil.disk_usage", return_value=mock_usage),
caplog.at_level(logging.WARNING),
):
manager._enforce_size_limit_for_new_block()
assert "disk pressure" in caplog.text
assert "disk nearly full" in caplog.text
class TestPreloadMatchedBlocks:
"""Tests for parallel block preloading into hot cache."""
@pytest.fixture
def mx(self):
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
@pytest.fixture
def manager_with_hot_cache(self, tmp_path, mx):
"""Create a manager with hot cache enabled."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
yield manager
manager.close()
def _save_test_blocks(
self,
manager,
mx,
count=4,
layers=2,
hot_cache_max_bytes=512 * 1024**2,
hot_cache_budget=None,
):
"""Save test blocks and flush them to SSD (not hot cache)."""
hashes = []
for i in range(count):
block_hash = f"preload_test_block_{i:04d}".encode()
cache_data = [
(
mx.zeros((1, 4, 64, 64)),
mx.zeros((1, 4, 64, 64)),
)
for _ in range(layers)
]
manager.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=64,
model_name="test-model",
layer_cache_types=["KVCache"] * layers,
)
hashes.append(block_hash)
# Flush writer to ensure blocks are on SSD
manager.close()
# Re-open manager (cold start — hot cache is empty)
new_manager = PagedSSDCacheManager(
cache_dir=manager._cache_dir,
max_size_bytes=1024**3,
hot_cache_max_bytes=hot_cache_max_bytes,
hot_cache_budget=hot_cache_budget,
)
return new_manager, hashes
def test_preload_promotes_to_hot_cache(self, tmp_path, mx):
"""After preload, blocks are found in hot cache."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=4)
# Verify blocks are NOT in hot cache before preload
for h in hashes:
assert manager2._hot_cache_get(h) is None
# Preload
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 4
# Verify blocks ARE in hot cache after preload
for h in hashes:
assert manager2._hot_cache_get(h) is not None
manager2.close()
def test_clean_promoted_block_eviction_skips_ssd_write(self, tmp_path, mx):
"""Blocks loaded from SSD are clean and should not be re-written."""
entry_size = 2 * 2 * 1 * 4 * 64 * 64 * 4
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(
manager,
mx,
count=2,
hot_cache_max_bytes=entry_size + 1024,
)
try:
assert manager2.load_block(hashes[0]) is not None
first_entry = manager2._hot_cache_get(hashes[0])
assert first_entry is not None
assert first_entry["dirty"] is False
with patch.object(manager2, "_enqueue_ssd_write") as enqueue_write:
assert manager2.load_block(hashes[1]) is not None
enqueue_write.assert_not_called()
assert manager2._hot_cache_get(hashes[0]) is None
assert manager2._hot_cache_get(hashes[1]) is not None
finally:
manager2.close()
def test_preload_partial_failure(self, tmp_path, mx):
"""If one block file is missing, others still load."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=5)
# Delete one block file from SSD to simulate failure
metadata = manager2._index.get(hashes[1])
metadata.file_path.unlink()
loaded = manager2.preload_matched_blocks(hashes)
# 4 of 5 should succeed (1 deleted)
assert loaded == 4
assert manager2._hot_cache_get(hashes[0]) is not None
assert manager2._hot_cache_get(hashes[1]) is None # deleted file
assert manager2._hot_cache_get(hashes[2]) is not None
manager2.close()
def test_preload_skips_hot_cache_blocks(self, tmp_path, mx):
"""Blocks already in hot cache are not re-loaded."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=5)
# Load one block into hot cache manually
manager2.load_block(hashes[0])
assert manager2._hot_cache_get(hashes[0]) is not None
promotions_before = manager2._stats["hot_cache_promotions"]
# Preload all — should only load the 4 cold blocks
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 4
# Promotion count should increase by exactly 4 (not 5)
assert manager2._stats["hot_cache_promotions"] == promotions_before + 4
manager2.close()
def test_preload_unknown_hashes_ignored(self, tmp_path, mx):
"""Hashes not in the SSD index are silently skipped."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=5)
all_hashes = hashes + [b"nonexistent_hash_01", b"nonexistent_hash_02"]
loaded = manager2.preload_matched_blocks(all_hashes)
assert loaded == 5 # only the real blocks
manager2.close()
def test_preload_noop_without_hot_cache(self, tmp_path, mx):
"""Preload returns 0 when hot cache is disabled."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=0, # hot cache disabled
)
block_hash = b"preload_no_hot_test"
cache_data = [(mx.zeros((1, 4, 32, 64)), mx.zeros((1, 4, 32, 64)))]
manager.save_block(block_hash, cache_data, 32, layer_cache_types=["KVCache"])
manager.close()
manager2 = PagedSSDCacheManager(
cache_dir=manager._cache_dir,
max_size_bytes=1024**3,
hot_cache_max_bytes=0,
)
loaded = manager2.preload_matched_blocks([block_hash])
assert loaded == 0
manager2.close()
def test_preload_skips_when_hot_cache_full(self, tmp_path, mx):
"""Preload returns 0 when hot cache has no remaining capacity."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=1024, # tiny hot cache
)
manager2, hashes = self._save_test_blocks(manager, mx, count=2)
# Fill hot cache to capacity
manager2._hot_cache_total_bytes = manager2._hot_cache_max_bytes
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 0
manager2.close()
def test_preload_respects_available_hot_cache_capacity(self, tmp_path, mx):
"""Preload must not load more cold blocks than remaining hot-cache bytes."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=5)
try:
file_sizes = [manager2._index.get(h).file_size for h in hashes]
manager2._hot_cache_max_bytes = sum(file_sizes[:4])
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 4
for h in hashes[:4]:
assert manager2._hot_cache_get(h) is not None
assert manager2._hot_cache_get(hashes[4]) is None
finally:
manager2.close()
def test_preload_skips_when_shared_hot_cache_budget_full(self, tmp_path, mx):
"""Preload uses remaining shared budget, not only local hot cache bytes."""
budget = SharedHotCacheBudget(1024)
filler = PagedSSDCacheManager(
cache_dir=tmp_path / "budget_filler",
max_size_bytes=1024**3,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_only=True,
hot_cache_budget=budget,
)
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=0,
)
manager2 = None
try:
filler._hot_cache_put(
b"preload_budget_filler",
{
"tensors_raw": {
"layer_0_keys": (bytes(1024), "uint8", [1024]),
},
"file_metadata": {},
"num_layers": 1,
"layer_cache_types": ["KVCache"],
"block_metadata": None,
},
)
assert budget.remaining_bytes == 0
manager2, hashes = self._save_test_blocks(
manager,
mx,
count=4,
hot_cache_max_bytes=budget.max_bytes,
hot_cache_budget=budget,
)
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 0
assert budget.total_bytes == budget.max_bytes
for h in hashes:
assert manager2._hot_cache_get(h) is None
finally:
if manager2 is not None:
manager2.close()
else:
manager.close()
filler.close()
def test_preload_empty_list(self, manager_with_hot_cache):
"""Empty hash list returns 0 immediately."""
loaded = manager_with_hot_cache.preload_matched_blocks([])
assert loaded == 0
def test_preload_skips_below_threshold(self, tmp_path, mx):
"""Preload skips when fewer than 4 cold blocks need loading."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=3)
loaded = manager2.preload_matched_blocks(hashes)
assert loaded == 0
for bh in hashes:
assert manager2._hot_cache_get(bh) is None
manager2.close()
def test_preload_updates_stats(self, tmp_path, mx):
"""Preload increments preload-specific stats counters."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=4)
manager2.preload_matched_blocks(hashes)
assert manager2._stats["preload_blocks_loaded"] == 4
assert manager2._stats["preload_calls"] == 1
assert manager2._stats["preload_time_ms"] > 0
manager2.close()
def test_preloaded_blocks_load_correctly(self, tmp_path, mx):
"""After preload, load_block returns correct data from hot cache."""
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=5, layers=3)
# Preload blocks into hot cache
manager2.preload_matched_blocks(hashes)
# Now load_block should hit hot cache
hot_hits_before = manager2._stats["hot_cache_hits"]
for h in hashes:
data = manager2.load_block(h)
assert data is not None
assert len(data) == 3 # 3 layers
for keys, values in data:
assert keys.shape == (1, 4, 64, 64)
assert values.shape == (1, 4, 64, 64)
# All loads should be hot cache hits (not SSD reads)
assert manager2._stats["hot_cache_hits"] == hot_hits_before + 5
manager2.close()
def test_concurrent_preload_and_load(self, tmp_path, mx):
"""Preload and load_block don't race on hot cache."""
import threading
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=512 * 1024**2,
)
manager2, hashes = self._save_test_blocks(manager, mx, count=8)
results = {"preload": None, "loads": []}
errors = []
def do_preload():
try:
results["preload"] = manager2.preload_matched_blocks(hashes)
except Exception as e:
errors.append(f"preload: {e}")
def do_loads():
try:
for h in hashes:
data = manager2.load_block(h)
results["loads"].append(data is not None)
except Exception as e:
errors.append(f"load: {e}")
t1 = threading.Thread(target=do_preload)
t2 = threading.Thread(target=do_loads)
t1.start()
t2.start()
t1.join(timeout=30)
t2.join(timeout=30)
assert not errors, f"Concurrent errors: {errors}"
assert not t1.is_alive(), "Preload thread hung"
assert not t2.is_alive(), "Load thread hung"
manager2.close()
class TestPreloadBlocks:
"""Tests for BlockAwarePrefixCache.preload_blocks()."""
@pytest.fixture
def mx(self):
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
def test_preload_blocks_calls_ssd_preload(self, tmp_path, mx):
"""preload_blocks extracts hashes from BlockTable and calls SSD preload."""
from unittest.mock import MagicMock
from omlx.cache.paged_cache import PagedCacheManager
# Set up real SSD manager with blocks
ssd_manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=256 * 1024**2,
)
hashes = []
for i in range(5):
bh = f"preload_blocks_test_{i:04d}".encode()
cache_data = [(mx.zeros((1, 4, 32, 64)), mx.zeros((1, 4, 32, 64)))]
ssd_manager.save_block(bh, cache_data, 32, layer_cache_types=["KVCache"])
hashes.append(bh)
ssd_manager.close()
# Re-open cold
ssd_manager2 = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1024**3,
hot_cache_max_bytes=256 * 1024**2,
)
# Set up paged cache with allocated blocks
paged_cache = PagedCacheManager(block_size=256, max_blocks=100)
block_ids = []
for bh in hashes:
block = paged_cache.allocate_block()
block.block_hash = bh
block.token_count = 32
block_ids.append(block.block_id)
# Create BlockAwarePrefixCache
from omlx.cache.prefix_cache import BlockAwarePrefixCache, BlockTable
model = MagicMock()
prefix_cache = BlockAwarePrefixCache(model, paged_cache, ssd_manager2)
# Create a BlockTable
bt = BlockTable(
request_id="test-req",
block_ids=block_ids,
num_tokens=32 * len(block_ids),
)
# Call preload_blocks
loaded = prefix_cache.preload_blocks(bt)
assert loaded == 5
# Verify blocks are in hot cache
for bh in hashes:
assert ssd_manager2._hot_cache_get(bh) is not None
ssd_manager2.close()
class TestComputeMaxPendingWrites:
"""Pin the block-size-aware cap formula.
The pending-writes queue holds raw KV bytes that the background
SSD writer hasn't drained yet — so the cap must scale by bytes per
slot, not just host RAM. Cap should shrink when each block is
expensive (large block_size or fat KV) and grow when each block is
cheap, while the hard byte budget bounds the soft floor.
"""
def test_soft_floor_applies_when_hard_budget_allows(self):
"""The soft floor gives large-block workloads burst headroom
when the byte hard cap still has room."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
def fake_sysconf(name):
if name == "SC_PAGE_SIZE":
return 4096
if name == "SC_PHYS_PAGES":
return (64 * 1024**3) // 4096
raise ValueError(name)
with patch("omlx.cache.paged_ssd_cache.os.sysconf", side_effect=fake_sysconf):
cap = _compute_max_pending_writes(
block_size_tokens=2048,
kv_bytes_per_token=200_000,
)
assert cap == 32
def test_hard_budget_bounds_soft_floor(self):
"""The soft floor must not force the pending pool above 30%
of host RAM for very expensive blocks."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
def fake_sysconf(name):
if name == "SC_PAGE_SIZE":
return 4096
if name == "SC_PHYS_PAGES":
return (64 * 1024**3) // 4096
raise ValueError(name)
with patch("omlx.cache.paged_ssd_cache.os.sysconf", side_effect=fake_sysconf):
cap = _compute_max_pending_writes(
block_size_tokens=2048,
kv_bytes_per_token=500_000,
)
assert cap == 20
def test_ceiling_clamps_high_end(self):
"""A 512 GB host with tiny blocks still tops out at 256
rather than pinning thousands of in-flight writes."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
# Tiny per-slot cost → math would produce a huge number;
# the ceiling must hold at 256.
cap = _compute_max_pending_writes(
block_size_tokens=1,
kv_bytes_per_token=1,
)
assert cap == 256
def test_larger_block_shrinks_cap(self):
"""Doubling block_size_tokens halves the cap target (until
floor/ceiling clamp)."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
cap_256 = _compute_max_pending_writes(block_size_tokens=256)
cap_1024 = _compute_max_pending_writes(block_size_tokens=1024)
# 4× block size → ¼ the target. Both should be in the
# in-band range (above floor, below ceiling) for typical
# Macs; if the smaller one is at the ceiling the test
# degenerates but the ordering still holds.
assert cap_1024 <= cap_256, (
"Larger blocks must yield smaller cap so worst-case "
"pinned bytes stay bounded"
)
def test_larger_kv_bytes_shrinks_cap(self):
"""Heavier per-token KV (bigger model) shrinks the cap so
the byte budget is preserved across model sizes."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
cap_small = _compute_max_pending_writes(kv_bytes_per_token=50_000)
cap_large = _compute_max_pending_writes(kv_bytes_per_token=400_000)
assert cap_large <= cap_small
def test_default_args_produce_sensible_cap(self):
"""The default-args path used by static callers must produce
a cap inside the bounded range."""
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
cap = _compute_max_pending_writes()
assert 1 <= cap <= 256
def test_manager_picks_up_per_instance_cap(self, tmp_path):
"""The PagedSSDCacheManager must recompute the cap from its
constructor args, not just inherit the module-level constant.
Otherwise a non-default block size silently uses a cap sized
for the default block size."""
from omlx.cache.paged_ssd_cache import (
PagedSSDCacheManager,
_compute_max_pending_writes,
)
mgr_small = PagedSSDCacheManager(
cache_dir=tmp_path / "small",
max_size_bytes=1 << 30,
expected_block_size_tokens=256,
expected_kv_bytes_per_token=200_000,
)
mgr_large = PagedSSDCacheManager(
cache_dir=tmp_path / "large",
max_size_bytes=1 << 30,
expected_block_size_tokens=2048,
expected_kv_bytes_per_token=200_000,
)
# Large blocks must produce a cap no larger than small blocks.
# Both should equal what the standalone function computes for
# the same inputs (no surprise rescaling inside __init__).
assert mgr_small._max_pending_writes == _compute_max_pending_writes(
block_size_tokens=256, kv_bytes_per_token=200_000
)
assert mgr_large._max_pending_writes == _compute_max_pending_writes(
block_size_tokens=2048, kv_bytes_per_token=200_000
)
assert mgr_large._max_pending_writes <= mgr_small._max_pending_writes
# The write queue must use the instance cap, not the module
# default — otherwise the formula change is ineffective.
assert mgr_small._write_queue.maxsize == mgr_small._max_pending_writes
assert mgr_large._write_queue.maxsize == mgr_large._max_pending_writes
mgr_small.close()
mgr_large.close()
class TestSchedulerPlumbsBlockSizeToSSDCache:
"""The Scheduler construction path must plumb its final
``paged_cache_block_size`` and a model-derived KV bytes-per-token
estimate into ``PagedSSDCacheManager.__init__`` — otherwise the
block-size-aware queue cap silently falls back to defaults sized
for the wrong workload.
Regression guard for jundot's review on PR #1627:
> PagedSSDCacheManager now accepts ``expected_block_size_tokens``
> / ``expected_kv_bytes_per_token``, but the real scheduler
> construction path still does not pass them.
"""
def _make_scheduler(self, tmp_path, block_size_tokens, model_layers):
"""Build a Scheduler with paged SSD cache enabled at the given
block size and a model whose config exposes ``model_layers``
layers (so the memory monitor produces a real per-token KV
estimate rather than its default)."""
from unittest.mock import MagicMock
from omlx.scheduler import Scheduler, SchedulerConfig
# Real numbers so MemoryMonitor.set_model_info accepts them.
class _Config:
num_hidden_layers = model_layers
num_key_value_heads = 8
num_attention_heads = 32
head_dim = 192
hidden_size = 6144
model = MagicMock()
model.layers = []
model.config = _Config()
tokenizer = MagicMock()
tokenizer.eos_token_id = 2
config = SchedulerConfig(
max_num_seqs=4,
prefill_step_size=2048,
paged_cache_block_size=block_size_tokens,
paged_ssd_cache_dir=str(tmp_path),
paged_ssd_cache_max_size=1 << 30,
hot_cache_max_size=0,
model_name="test-model",
)
return Scheduler(model=model, tokenizer=tokenizer, config=config)
def test_block_size_plumbed_to_ssd_manager(self, tmp_path):
"""Final scheduler block size must reach the SSD cache manager
— not the 256-token default."""
model_layers = 32
sched = self._make_scheduler(
tmp_path / "blk1024",
block_size_tokens=1024,
model_layers=model_layers,
)
mgr = sched.paged_ssd_cache_manager
# The manager stashes the constructor inputs as
# ``_expected_block_size_tokens`` / ``_expected_kv_bytes_per_token``
# so the plumbing-regression check is bulletproof regardless
# of the test host's RAM tier (where the cap math might clamp
# both default and plumbed inputs to the same floor/ceiling).
assert mgr._expected_block_size_tokens == 1024, (
"Scheduler must plumb its final paged_cache_block_size "
"into PagedSSDCacheManager"
)
# The auto-init in ``Scheduler.__init__`` (paired with
# ``_set_model_info_for_monitor()``) must populate dims from
# the fixture's model.config so the writer-queue cap formula
# weighs a real per-token cost. If either step regresses the
# SSD manager would silently fall back to either the monitor's
# ~128 KB 7B-class fiction or the manager's own 200 KB default
# — both miscalibrate the cap on the real model. Assert the
# construction order didn't regress.
assert sched.memory_monitor is not None, (
"Scheduler.__init__ must auto-init MemoryMonitor before "
"_init_tiered_cache; if this regresses the SSD manager "
"silently uses its 200 KB default and the cap math is "
"miscalibrated on every workload"
)
assert sched.memory_monitor.has_model_info(), (
"_set_model_info_for_monitor must populate dims from "
"model.config BEFORE _init_tiered_cache constructs the "
"PagedSSDCacheManager; otherwise estimate_block_memory(1) "
"returns its 7B-class default fiction"
)
# The model-derived per-token KV is what should reach the
# manager. Compute it inline from the fixture's known dims so
# this assertion can't be satisfied by a tautological pass-
# through of whatever ``estimate_block_memory`` happens to
# return — and so it doesn't equal the 200 KB manager default,
# making a silent fallback to the default visible as a numeric
# diff in the failure message.
per_token_bytes = (
1 # block_size token
* 8 # num_kv_heads in _make_scheduler fixture
* 192 # head_dim
* 2 # dtype_size (float16)
* 2 # keys + values
) * model_layers
assert mgr._expected_kv_bytes_per_token == per_token_bytes, (
f"PagedSSDCacheManager received "
f"expected_kv_bytes_per_token={mgr._expected_kv_bytes_per_token} "
f"but the model-derived value is {per_token_bytes}; "
f"a value of 200000 indicates a silent fallback to the "
f"manager default"
)
# And the value the manager received must match
# ``memory_monitor.estimate_block_memory(1)`` — the documented
# source. Keep both checks: the inline calc above catches
# tautological pass-throughs, this one catches drift between
# the monitor's calc and the scheduler's wiring.
assert mgr._expected_kv_bytes_per_token == (
sched.memory_monitor.estimate_block_memory(1)
), (
"Scheduler must plumb memory_monitor.estimate_block_memory"
"(1) as expected_kv_bytes_per_token"
)
# And the cap computed from those plumbed inputs must drive the
# write queue's maxsize — the cap is only useful if the queue
# actually enforces it.
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
expected_cap = _compute_max_pending_writes(
block_size_tokens=mgr._expected_block_size_tokens,
kv_bytes_per_token=mgr._expected_kv_bytes_per_token,
)
assert mgr._max_pending_writes == expected_cap
assert mgr._write_queue.maxsize == mgr._max_pending_writes
mgr.close()
def test_larger_block_size_shrinks_scheduler_cap(self, tmp_path):
"""The same model on the same Mac with a bigger block size
must produce a smaller pending-writes cap — proves the wiring
is not stuck at the default for every scheduler instance."""
sched_small = self._make_scheduler(
tmp_path / "blk256",
block_size_tokens=256,
model_layers=32,
)
sched_large = self._make_scheduler(
tmp_path / "blk2048",
block_size_tokens=2048,
model_layers=32,
)
cap_small = sched_small.paged_ssd_cache_manager._max_pending_writes
cap_large = sched_large.paged_ssd_cache_manager._max_pending_writes
assert cap_large <= cap_small, (
f"Larger blocks must shrink the cap; got "
f"cap(2048)={cap_large} > cap(256)={cap_small}"
)
sched_small.paged_ssd_cache_manager.close()
sched_large.paged_ssd_cache_manager.close()
def test_factory_path_also_plumbs_block_size(self, tmp_path):
"""The cache factory path (used by direct/test callers) must
also pass ``config.block_size`` through — otherwise direct
callers silently get the 256-token default regardless of what
they configured."""
from omlx.cache.factory import CacheConfig, CacheFactory
cfg = CacheConfig(
block_size=1024,
paged_ssd_cache_dir=tmp_path,
max_paged_ssd_cache_size=1 << 30,
)
mgr = CacheFactory.create_paged_ssd_cache(cfg, model_name="m")
assert mgr is not None
from omlx.cache.paged_ssd_cache import _compute_max_pending_writes
# Factory has no memory monitor, so it leaves
# ``kv_bytes_per_token`` at the 200 KB default — that's the
# documented contract for non-Scheduler callers.
expected = _compute_max_pending_writes(
block_size_tokens=1024, kv_bytes_per_token=200_000
)
assert mgr._max_pending_writes == expected
mgr.close()
class TestInlineLRUUnlinks:
"""LRU eviction must unlink inline on the calling thread, not enqueue
``("unlink", path)`` tasks onto ``_write_queue``.
The original async-queued design routed eviction unlinks through the
same bounded queue that carries pending writes. Under sustained save
pressure, the queue saturated, ``save_block``'s pre-eviction
``_write_queue.full()`` short-circuit fired before eviction could
run, and the cache stayed permanently full once the queue saturated.
Inlining removes the bounded-queue contention.
"""
@pytest.fixture
def mx(self):
try:
import mlx.core as mx
return mx
except ImportError:
pytest.skip("MLX not available")
def _entry_size(self, num_layers=2, seq_len=16, heads=2, head_dim=16):
# 2 tensors (K+V) per layer, batch=1, float32=4
return num_layers * 2 * 1 * heads * seq_len * head_dim * 4
def _save_block(self, mgr, mx, block_hash, num_layers=2):
cache_data = [
(mx.zeros((1, 2, 16, 16)), mx.zeros((1, 2, 16, 16)))
for _ in range(num_layers)
]
return mgr.save_block(
block_hash=block_hash,
cache_data=cache_data,
token_count=16,
model_name="test-model",
layer_cache_types=["KVCache"] * num_layers,
)
def test_save_waits_on_full_queue_before_inline_fallback(self, tmp_path, mx):
"""A full queue must not short-circuit before tensor extraction.
The bounded wait gives the writer a chance to drain transient bursts;
sustained saturation writes the block inline.
"""
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "full_queue_wait",
max_size_bytes=1 << 30,
)
original_put = mgr._write_queue.put
original_full = mgr._write_queue.full
calls: list[float | None] = []
def fake_put(item, timeout=None, *args, **kwargs):
calls.append(timeout)
raise queue.Full
try:
mgr._write_queue.full = lambda: True # type: ignore[method-assign]
mgr._write_queue.put = fake_put # type: ignore[method-assign]
block_hash = b"full_queue_wait"
assert self._save_block(mgr, mx, block_hash) 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 mgr.load_block(block_hash) is not None
finally:
mgr._write_queue.put = original_put # type: ignore[method-assign]
mgr._write_queue.full = original_full # type: ignore[method-assign]
mgr.close()
def test_eviction_does_not_enqueue_unlink_tasks(self, tmp_path, mx):
"""Force eviction; assert no ``("unlink", ...)`` items ever enter
``_write_queue``. Regression for the original async-queued
design."""
entry_size = self._entry_size()
# Room for ~2 entries; the third save forces eviction of the first.
max_bytes = entry_size * 2 + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "inline_eviction",
max_size_bytes=max_bytes,
)
# Sentinel: intercept put_nowait and reject any unlink-shaped tuple.
original_put_nowait = mgr._write_queue.put_nowait
unlink_attempts: list = []
def guard_put_nowait(item):
if isinstance(item, tuple) and item and item[0] == "unlink":
unlink_attempts.append(item)
return original_put_nowait(item)
mgr._write_queue.put_nowait = guard_put_nowait # type: ignore[assignment]
try:
for i in range(5):
self._save_block(mgr, mx, f"inline_evict_{i:04d}".encode())
assert unlink_attempts == [], (
"Eviction must unlink inline, not enqueue. Found queued "
f"unlink attempts: {unlink_attempts!r}"
)
finally:
mgr.close()
def test_eviction_frees_capacity_under_pressure(self, tmp_path, mx):
"""Even when the writer thread is paused (mimicking the
saturation scenario), eviction must keep the index size within
the configured cap."""
entry_size = self._entry_size()
max_bytes = entry_size * 2 + 100 # holds exactly 2 entries
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "inline_pressure",
max_size_bytes=max_bytes,
)
try:
# Save more entries than the cap allows; eviction must keep
# the index within ``max_bytes``.
for i in range(8):
self._save_block(mgr, mx, f"pressure_{i:04d}".encode())
# Wait briefly for in-flight writes to settle so the index
# accounting reflects post-eviction state.
time.sleep(0.05)
assert mgr._index.total_size <= max_bytes + entry_size, (
f"Eviction failed to keep total_size ({mgr._index.total_size}) "
f"near cap ({max_bytes})"
)
finally:
mgr.close()
def test_inline_eviction_burst_is_capped(self, tmp_path, mx):
"""A large forced eviction is bounded by
``_MAX_INLINE_UNLINKS_PER_SAVE``; deferred entries reinsert into
the index so subsequent saves drain the remainder."""
from omlx.cache.paged_ssd_cache import _MAX_INLINE_UNLINKS_PER_SAVE
# Use a large cap initially, then shrink to force a mass-eviction.
entry_size = self._entry_size()
n_entries = _MAX_INLINE_UNLINKS_PER_SAVE + 16
initial_max = entry_size * (n_entries + 2)
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "inline_burst",
max_size_bytes=initial_max,
)
try:
for i in range(n_entries):
self._save_block(mgr, mx, f"burst_{i:04d}".encode())
# Wait for writes to flush so file_size in the index matches
# what's on disk.
time.sleep(0.05)
count_before = mgr._index.count
# Shrink the effective cap dramatically. Next eviction must
# cap its inline burst at _MAX_INLINE_UNLINKS_PER_SAVE.
mgr._max_size = entry_size # cap at 1 entry
# Trigger eviction via a fresh save.
self._save_block(mgr, mx, b"burst_trigger___")
# After one save, the index should have shed at most
# _MAX_INLINE_UNLINKS_PER_SAVE entries. The rest must have
# been reinserted so subsequent saves can drain them.
time.sleep(0.05)
count_after = mgr._index.count
removed = count_before + 1 - count_after # +1 for the new save
assert removed <= _MAX_INLINE_UNLINKS_PER_SAVE, (
f"Inline burst removed {removed} entries (cap "
f"{_MAX_INLINE_UNLINKS_PER_SAVE}); ENOSPC-storm protection "
f"is not in effect"
)
assert removed > 0, (
"No entries were evicted despite the new save crossing "
"the (shrunken) cap"
)
finally:
mgr.close()
def test_deferred_eviction_preserves_lru_order(self, tmp_path, monkeypatch):
"""Deferred eviction entries remain older than survivor entries."""
from omlx.cache import paged_ssd_cache as ssd_cache_module
monkeypatch.setattr(ssd_cache_module, "_MAX_INLINE_UNLINKS_PER_SAVE", 2)
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "deferred_lru_order",
max_size_bytes=1024**2 + 20,
hot_cache_only=True,
)
try:
for i in range(6):
block_hash = f"lru_{i}".encode()
mgr._index.add(
PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=tmp_path / f"{block_hash.hex()}.safetensors",
file_size=10,
token_count=1,
created_at=float(i),
last_access=float(i),
num_layers=1,
)
)
mgr._get_effective_max_size = ( # type: ignore[method-assign]
lambda: 1024**2 + 20
)
mgr._enforce_size_limit_for_new_block()
remaining_lru = [
metadata.block_hash
for metadata in mgr._index.get_lru_entries(mgr._index.count)
]
assert remaining_lru == [b"lru_2", b"lru_3", b"lru_4", b"lru_5"]
finally:
mgr.close()
def test_unlink_failure_increments_counter(self, tmp_path, mx):
"""When ``Path.unlink`` raises ``OSError``, the eviction loop
records the failure in ``evict_unlink_failures`` instead of
silently dropping the signal."""
entry_size = self._entry_size()
max_bytes = entry_size + 100
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "unlink_fail",
max_size_bytes=max_bytes,
)
try:
self._save_block(mgr, mx, b"unlink_fail_0001")
time.sleep(0.05)
# Patch unlink to raise OSError on the next eviction attempt.
from pathlib import Path as _Path
def boom_unlink(self, *args, **kwargs):
raise OSError("simulated unlink failure")
with patch.object(_Path, "unlink", boom_unlink):
# Save a second block; eviction of the first triggers the
# patched unlink.
self._save_block(mgr, mx, b"unlink_fail_0002")
time.sleep(0.05)
assert mgr._stats["evict_unlink_failures"] >= 1
finally:
mgr.close()
class TestSharedHotCacheBudgetClearAllOwners:
"""clear_all_owners reaches managers the budget still pins (orphaned)."""
class _FakeOwner:
def __init__(self, budget, n):
self._budget = budget
self._n = n
self.cleared = False
def clear_hot_cache(self):
self._budget.forget_owner(self)
self.cleared = True
return self._n
def test_clears_every_tracked_owner(self):
budget = SharedHotCacheBudget(1 << 20)
o1 = self._FakeOwner(budget, 3)
o2 = self._FakeOwner(budget, 4)
budget.put(o1, b"h1", 100)
budget.put(o1, b"h2", 100)
budget.put(o2, b"h3", 200)
cleared = budget.clear_all_owners()
assert cleared == 7
assert o1.cleared and o2.cleared
assert len(budget._entries) == 0
def test_empty_budget_is_noop(self):
assert SharedHotCacheBudget(1 << 20).clear_all_owners() == 0
class TestLayerSignatureSweep:
"""Tests for ``adopt_layer_signature_if_unset`` /
``invalidate_stale_layer_signature``.
The bug: blocks saved before a cache-config change (e.g., TurboQuant
toggle) carry stale ``layer_cache_types`` metadata, and the
per-block compatibility check in ``reconstruct_cache`` uses block 0
as the reference — so a stale block 0 defines the reference forever
and every newer correctly-typed block trips the mismatch.
"""
def _make_meta(
self,
*,
block_hash: bytes,
model_name: str,
layer_cache_types: list[str] | None,
num_layers: int = 40,
block_size: int = 2048,
) -> PagedSSDBlockMetadata:
now = time.time()
return PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/never-touched.safetensors"),
file_size=1024,
token_count=block_size,
created_at=now,
last_access=now,
num_layers=num_layers,
model_name=model_name,
block_size=block_size,
layer_cache_types=layer_cache_types,
)
def _make_manager(
self,
tmp_path: Path,
*,
model_name: str = "test-model",
expected_layer_cache_types: list[str] | None = None,
) -> PagedSSDCacheManager:
return PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1 << 30,
expected_model_name=model_name,
expected_num_layers=40,
expected_block_size=2048,
expected_layer_cache_types=expected_layer_cache_types,
)
def test_adopt_sets_signature_first_call(self, tmp_path: Path):
mgr = self._make_manager(tmp_path)
assert mgr._expected_layer_cache_types is None
sig = ["ArraysCache", "ArraysCache", "ArraysCache", "TurboQuantKVCache"]
adopted = mgr.adopt_layer_signature_if_unset(sig)
assert adopted is True
assert mgr._expected_layer_cache_types == sig
def test_adopt_noop_when_already_set(self, tmp_path: Path):
original = ["ArraysCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=original)
adopted = mgr.adopt_layer_signature_if_unset(
["ArraysCache", "TurboQuantKVCache"]
)
assert adopted is False
assert mgr._expected_layer_cache_types == original
def test_adopt_noop_when_empty(self, tmp_path: Path):
mgr = self._make_manager(tmp_path)
assert mgr.adopt_layer_signature_if_unset(None) is False
assert mgr.adopt_layer_signature_if_unset([]) is False
assert mgr._expected_layer_cache_types is None
def test_sweep_drops_blocks_with_stale_signature(self, tmp_path: Path):
stock = ["ArraysCache", "ArraysCache", "ArraysCache", "KVCache"]
turbo = [
"ArraysCache",
"ArraysCache",
"ArraysCache",
"TurboQuantKVCache",
]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=turbo)
mgr._index.add(
self._make_meta(
block_hash=b"01" * 10,
model_name="test-model",
layer_cache_types=stock,
num_layers=4,
)
)
mgr._index.add(
self._make_meta(
block_hash=b"02" * 10,
model_name="test-model",
layer_cache_types=turbo,
num_layers=4,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 1
assert mgr._index.get(b"01" * 10) is None
assert mgr._index.get(b"02" * 10) is not None
def test_sweep_canonicalizes_sized_arrays_cache(self, tmp_path: Path):
# SizedArraysCache and ArraysCache must compare equal — both are
# the same on-disk format. The sweep must NOT drop blocks just
# because one side uses the wrapper class name.
a = ["ArraysCache", "ArraysCache", "KVCache"]
b = ["SizedArraysCache", "SizedArraysCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=a)
mgr._index.add(
self._make_meta(
block_hash=b"aa" * 10,
model_name="test-model",
layer_cache_types=b,
num_layers=3,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 0
assert mgr._index.get(b"aa" * 10) is not None
def test_sweep_canonicalizes_prefill_ready_rotating_cache(self, tmp_path: Path):
expected = ["KVCache", "RotatingKVCache", "KVCache"]
stored = ["KVCache", "PrefillReadyRotatingKVCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=expected)
mgr._index.add(
self._make_meta(
block_hash=b"ac" * 10,
model_name="test-model",
layer_cache_types=stored,
num_layers=3,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 0
assert mgr._index.get(b"ac" * 10) is not None
def test_sweep_drops_legacy_buffered_rotating_cache(self, tmp_path: Path):
expected = ["KVCache", "RotatingKVCache", "KVCache"]
stored = ["KVCache", "BufferedRotatingKVCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=expected)
mgr._index.add(
self._make_meta(
block_hash=b"af" * 10,
model_name="test-model",
layer_cache_types=stored,
num_layers=3,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 1
assert mgr._index.get(b"af" * 10) is None
def test_startup_compat_canonicalizes_sized_arrays_cache(self, tmp_path: Path):
expected = ["ArraysCache", "ArraysCache", "KVCache"]
stored = ["SizedArraysCache", "SizedArraysCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=expected)
meta = self._make_meta(
block_hash=b"ab" * 10,
model_name="test-model",
layer_cache_types=stored,
num_layers=40,
)
meta.cache_signature = _cache_compat_signature(
model_name="test-model",
num_layers=40,
block_size=2048,
layer_cache_types=stored,
)
assert mgr._is_compatible_block(meta) is True
def test_startup_compat_canonicalizes_prefill_ready_rotating_cache(
self, tmp_path: Path
):
expected = ["KVCache", "RotatingKVCache", "KVCache"]
stored = ["KVCache", "PrefillReadyRotatingKVCache", "KVCache"]
mgr = self._make_manager(tmp_path, expected_layer_cache_types=expected)
meta = self._make_meta(
block_hash=b"ad" * 10,
model_name="test-model",
layer_cache_types=stored,
num_layers=40,
)
meta.cache_signature = _cache_compat_signature(
model_name="test-model",
num_layers=40,
block_size=2048,
layer_cache_types=stored,
)
assert mgr._is_compatible_block(meta) is True
def test_set_expected_layer_signature_replaces_existing(self, tmp_path: Path):
mgr = self._make_manager(
tmp_path,
expected_layer_cache_types=["ArraysCache", "KVCache"],
)
mgr._signature_sweep_completed = True
changed = mgr.set_expected_layer_signature(["ArraysCache", "TurboQuantKVCache"])
assert changed is True
assert mgr._expected_layer_cache_types == [
"ArraysCache",
"TurboQuantKVCache",
]
assert mgr._signature_sweep_completed is False
def test_set_expected_layer_signature_noop_for_canonical_match(
self, tmp_path: Path
):
mgr = self._make_manager(
tmp_path,
expected_layer_cache_types=["ArraysCache", "KVCache"],
)
mgr._signature_sweep_completed = True
changed = mgr.set_expected_layer_signature(["SizedArraysCache", "KVCache"])
assert changed is False
assert mgr._expected_layer_cache_types == ["SizedArraysCache", "KVCache"]
assert mgr._signature_sweep_completed is True
def test_set_expected_layer_signature_noop_for_prefill_ready_rotating_match(
self, tmp_path: Path
):
mgr = self._make_manager(
tmp_path,
expected_layer_cache_types=["KVCache", "RotatingKVCache"],
)
mgr._signature_sweep_completed = True
changed = mgr.set_expected_layer_signature(
["KVCache", "PrefillReadyRotatingKVCache"]
)
assert changed is False
assert mgr._expected_layer_cache_types == [
"KVCache",
"PrefillReadyRotatingKVCache",
]
assert mgr._signature_sweep_completed is True
def test_sweep_leaves_other_models_alone(self, tmp_path: Path):
turbo = ["ArraysCache", "TurboQuantKVCache"]
stock = ["ArraysCache", "KVCache"]
mgr = self._make_manager(
tmp_path, model_name="model-A", expected_layer_cache_types=turbo
)
# Other-model block with the "wrong" signature for THIS manager
# but perfectly valid for model-B.
mgr._index.add(
self._make_meta(
block_hash=b"bb" * 10,
model_name="model-B",
layer_cache_types=stock,
num_layers=2,
)
)
# Same-model stale block.
mgr._index.add(
self._make_meta(
block_hash=b"cc" * 10,
model_name="model-A",
layer_cache_types=stock,
num_layers=2,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 1
assert (
mgr._index.get(b"bb" * 10) is not None
), "model-B block must not be touched by model-A's sweep"
assert mgr._index.get(b"cc" * 10) is None
def test_sweep_skips_legacy_unnamed_blocks(self, tmp_path: Path):
turbo = ["ArraysCache", "TurboQuantKVCache"]
mgr = self._make_manager(
tmp_path, model_name="model-A", expected_layer_cache_types=turbo
)
# No model_name — cannot safely attribute.
mgr._index.add(
self._make_meta(
block_hash=b"dd" * 10,
model_name="",
layer_cache_types=["ArraysCache", "KVCache"],
num_layers=2,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 0
assert mgr._index.get(b"dd" * 10) is not None
def test_sweep_skips_blocks_without_layer_metadata(self, tmp_path: Path):
turbo = ["ArraysCache", "TurboQuantKVCache"]
mgr = self._make_manager(
tmp_path, model_name="model-A", expected_layer_cache_types=turbo
)
# Pre-signature block: no layer_cache_types recorded.
mgr._index.add(
self._make_meta(
block_hash=b"ee" * 10,
model_name="model-A",
layer_cache_types=None,
num_layers=2,
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 0
assert mgr._index.get(b"ee" * 10) is not None
def test_sweep_idempotent(self, tmp_path: Path):
turbo = ["ArraysCache", "TurboQuantKVCache"]
mgr = self._make_manager(
tmp_path, model_name="model-A", expected_layer_cache_types=turbo
)
mgr._index.add(
self._make_meta(
block_hash=b"ff" * 10,
model_name="model-A",
layer_cache_types=["ArraysCache", "KVCache"],
num_layers=2,
)
)
assert mgr.invalidate_stale_layer_signature() == 1
# Subsequent calls do nothing — flag is set.
assert mgr.invalidate_stale_layer_signature() == 0
def test_sweep_noop_without_signature(self, tmp_path: Path):
mgr = self._make_manager(tmp_path) # signature unset
mgr._index.add(
self._make_meta(
block_hash=b"99" * 10,
model_name="test-model",
layer_cache_types=["ArraysCache", "KVCache"],
num_layers=2,
)
)
assert mgr.invalidate_stale_layer_signature() == 0
assert mgr._index.get(b"99" * 10) is not None
def test_sweep_noop_without_model_name(self, tmp_path: Path):
# Without an expected_model_name we cannot scope safely.
mgr = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1 << 30,
expected_model_name="", # explicit unset
expected_layer_cache_types=["ArraysCache", "KVCache"],
)
mgr._index.add(
self._make_meta(
block_hash=b"77" * 10,
model_name="some-model",
layer_cache_types=["ArraysCache", "TurboQuantKVCache"],
num_layers=2,
)
)
assert mgr.invalidate_stale_layer_signature() == 0
assert mgr._index.get(b"77" * 10) is not None
def test_adopt_resets_sweep_flag(self, tmp_path: Path):
# If a previous signature got adopted-and-swept, but somehow a new
# signature gets adopted later (currently the manager treats this
# as a no-op; we still want the flag to reset if it ever does).
mgr = self._make_manager(tmp_path)
mgr.adopt_layer_signature_if_unset(["ArraysCache", "KVCache"])
mgr._signature_sweep_completed = True
# Same call signature — already set, returns False, flag stays.
assert mgr.adopt_layer_signature_if_unset(["ArraysCache", "KVCache"]) is False
assert mgr._signature_sweep_completed is True
class TestTurboquantBitsSignature:
"""#2045: the cache signature must key the TurboQuant bit depth.
TurboQuant packed state width is ceil(head_dim * bits / 32), so blocks
written at different bit depths are shape-incompatible. Without the
bits field in the signature, 4-bit and 6-bit blocks for the same model
coexist and crash ``_concat_state_batch`` when their requests are
batched together.
"""
TURBO = ["ArraysCache", "TurboQuantKVCache", "TurboQuantKVCache", "KVCache"]
def _make_meta(
self,
*,
block_hash: bytes,
layer_cache_types: list[str] | None,
turboquant_kv_bits: float | None = None,
cache_signature: str | None = None,
model_name: str = "test-model",
num_layers: int = 4,
block_size: int = 2048,
) -> PagedSSDBlockMetadata:
now = time.time()
if cache_signature is None:
cache_signature = _cache_compat_signature(
model_name=model_name,
num_layers=num_layers,
block_size=block_size,
layer_cache_types=layer_cache_types,
turboquant_kv_bits=turboquant_kv_bits,
)
return PagedSSDBlockMetadata(
block_hash=block_hash,
file_path=Path("/tmp/never-touched.safetensors"),
file_size=1024,
token_count=block_size,
created_at=now,
last_access=now,
num_layers=num_layers,
model_name=model_name,
block_size=block_size,
layer_cache_types=layer_cache_types,
cache_signature=cache_signature,
)
def _make_manager(
self,
tmp_path: Path,
*,
layer_cache_types: list[str] | None = None,
turboquant_kv_bits: float | None = None,
) -> PagedSSDCacheManager:
# Expectations are set through set_expected_layer_signature — the
# same call the scheduler's refresh makes — so these tests exercise
# the path production actually runs.
manager = PagedSSDCacheManager(
cache_dir=tmp_path / "ssd_cache",
max_size_bytes=1 << 30,
expected_model_name="test-model",
expected_num_layers=4,
expected_block_size=2048,
)
if layer_cache_types is not None:
manager.set_expected_layer_signature(
layer_cache_types, turboquant_kv_bits=turboquant_kv_bits
)
return manager
def test_signature_unchanged_when_bits_none(self):
# Non-TurboQuant signatures must stay byte-identical to the previous
# format so existing caches of non-TurboQuant models keep validating.
old_format = json.dumps(
{
"model_name": "m",
"num_layers": 4,
"block_size": 2048,
"layer_cache_types": ["KVCache"],
},
sort_keys=True,
separators=(",", ":"),
)
assert (
_cache_compat_signature(
model_name="m",
num_layers=4,
block_size=2048,
layer_cache_types=["KVCache"],
)
== old_format
)
def test_signature_includes_bits_when_set(self):
sig = _cache_compat_signature(
model_name="m",
num_layers=4,
block_size=2048,
layer_cache_types=self.TURBO,
turboquant_kv_bits=6,
)
assert json.loads(sig)["turboquant_kv_bits"] == 6.0
def test_signature_bits_helper_tolerates_non_dict_json(self):
# A corrupted signature that parses as a JSON scalar/list must read
# as "no recorded depth", not raise — an AttributeError here would
# abort the stale-signature sweep and leave mixed-width blocks live.
assert _signature_turboquant_bits("123") is None
assert _signature_turboquant_bits("[1, 2]") is None
assert _signature_turboquant_bits("null") is None
assert _signature_turboquant_bits("{not json") is None
assert _signature_turboquant_bits("") is None
def test_block_bits_read_from_meta_state(self):
# TurboQuant meta_state is (offset, bits, seed, ...) — the same tuple
# the restore path reads.
assert (
_block_turboquant_bits(self.TURBO, [(), (256, 4.0, 0), (256, 4.0, 0), ()])
== 4.0
)
# Batch-form class name counts too.
assert (
_block_turboquant_bits(
["BatchTurboQuantKVCache"], [(256, 6.0, 0)]
)
== 6.0
)
# Non-TurboQuant layouts and absent meta yield None.
assert _block_turboquant_bits(["KVCache"], [(256,)]) is None
assert _block_turboquant_bits(self.TURBO, None) is None
assert _block_turboquant_bits(self.TURBO, [(), (), (), ()]) is None
def test_compat_rejects_other_or_unproven_bit_depth(self, tmp_path: Path):
mgr = self._make_manager(
tmp_path, layer_cache_types=self.TURBO, turboquant_kv_bits=6
)
matching = self._make_meta(
block_hash=b"01" * 10, layer_cache_types=self.TURBO, turboquant_kv_bits=6
)
other_depth = self._make_meta(
block_hash=b"02" * 10, layer_cache_types=self.TURBO, turboquant_kv_bits=4
)
# 0.4.x-era block: TurboQuant layout but no bits field recorded.
legacy = self._make_meta(block_hash=b"03" * 10, layer_cache_types=self.TURBO)
no_signature = self._make_meta(
block_hash=b"04" * 10, layer_cache_types=self.TURBO, cache_signature=""
)
assert mgr._is_compatible_block(matching) is True
assert mgr._is_compatible_block(other_depth) is False
assert mgr._is_compatible_block(legacy) is False
assert mgr._is_compatible_block(no_signature) is False
def test_compat_unchanged_when_bits_not_expected(self, tmp_path: Path):
# TurboQuant disabled: legacy and signature-less blocks keep loading
# exactly as before (zero behavioral change).
mgr = self._make_manager(tmp_path, layer_cache_types=self.TURBO)
legacy = self._make_meta(block_hash=b"05" * 10, layer_cache_types=self.TURBO)
no_signature = self._make_meta(
block_hash=b"06" * 10, layer_cache_types=self.TURBO, cache_signature=""
)
assert mgr._is_compatible_block(legacy) is True
assert mgr._is_compatible_block(no_signature) is True
def test_sweep_drops_other_bit_depth(self, tmp_path: Path):
mgr = self._make_manager(
tmp_path, layer_cache_types=self.TURBO, turboquant_kv_bits=6
)
mgr._index.add(
self._make_meta(
block_hash=b"11" * 10,
layer_cache_types=self.TURBO,
turboquant_kv_bits=4,
)
)
mgr._index.add(
self._make_meta(
block_hash=b"12" * 10,
layer_cache_types=self.TURBO,
turboquant_kv_bits=6,
)
)
mgr._index.add(
self._make_meta(block_hash=b"13" * 10, layer_cache_types=self.TURBO)
)
# Same-model block with no layout metadata at all: with a depth
# expected it can no more prove its width than its layout.
mgr._index.add(
self._make_meta(
block_hash=b"14" * 10, layer_cache_types=None, cache_signature=""
)
)
dropped = mgr.invalidate_stale_layer_signature()
assert dropped == 3
assert mgr._index.get(b"11" * 10) is None # 4-bit under 6-bit expectation
assert mgr._index.get(b"12" * 10) is not None # matching depth survives
assert mgr._index.get(b"13" * 10) is None # unproven depth
assert mgr._index.get(b"14" * 10) is None # no metadata, depth expected
def test_sweep_keeps_legacy_blocks_when_bits_not_expected(self, tmp_path: Path):
mgr = self._make_manager(tmp_path, layer_cache_types=self.TURBO)
mgr._index.add(
self._make_meta(block_hash=b"15" * 10, layer_cache_types=self.TURBO)
)
# No layout metadata, no depth expectation: skip rather than guess
# (pre-existing behavior preserved).
mgr._index.add(
self._make_meta(
block_hash=b"16" * 10, layer_cache_types=None, cache_signature=""
)
)
assert mgr.invalidate_stale_layer_signature() == 0
assert mgr._index.get(b"15" * 10) is not None
assert mgr._index.get(b"16" * 10) is not None
def test_batch_and_single_turboquant_names_compare_equal(self, tmp_path: Path):
# The save path records whichever class name it extracted; the
# predicted layout always says "TurboQuantKVCache". Batch-form
# blocks persist the same packed per-request state and must not be
# swept (that would cold-start the cache every restart).
batch_form = [
"ArraysCache",
"BatchTurboQuantKVCache",
"BatchTurboQuantKVCache",
"KVCache",
]
mgr = self._make_manager(
tmp_path, layer_cache_types=self.TURBO, turboquant_kv_bits=6
)
mgr._index.add(
self._make_meta(
block_hash=b"21" * 10,
layer_cache_types=batch_form,
turboquant_kv_bits=6,
)
)
assert mgr.invalidate_stale_layer_signature() == 0
assert mgr._index.get(b"21" * 10) is not None
assert (
mgr._is_compatible_block(
self._make_meta(
block_hash=b"22" * 10,
layer_cache_types=batch_form,
turboquant_kv_bits=6,
)
)
is True
)
def test_bits_change_alone_triggers_sweep(self, tmp_path: Path):
mgr = self._make_manager(
tmp_path, layer_cache_types=self.TURBO, turboquant_kv_bits=4
)
mgr._signature_sweep_completed = True
changed = mgr.set_expected_layer_signature(self.TURBO, turboquant_kv_bits=6)
assert changed is True
assert mgr._expected_turboquant_kv_bits == 6.0
assert mgr._signature_sweep_completed is False
def test_same_bits_and_types_is_noop(self, tmp_path: Path):
mgr = self._make_manager(
tmp_path, layer_cache_types=self.TURBO, turboquant_kv_bits=6
)
mgr._signature_sweep_completed = True
changed = mgr.set_expected_layer_signature(self.TURBO, turboquant_kv_bits=6)
assert changed is False
assert mgr._signature_sweep_completed is True
@pytest.mark.skipif(not _has_mlx(), reason="requires MLX")
def test_save_path_stamps_block_bits_in_signature(self, tmp_path: Path):
# Drive the REAL save path: the persisted signature must record the
# depth from the block's own TurboQuant meta_state (observation),
# not just the manager's expectation.
import mlx.core as mx
mgr = self._make_manager(
tmp_path,
layer_cache_types=["TurboQuantKVCache", "KVCache"],
turboquant_kv_bits=6,
)
try:
keys = mx.zeros((1, 2, 8, 4), dtype=mx.float16)
values = mx.zeros((1, 2, 8, 4), dtype=mx.float16)
saved = mgr.save_block(
block_hash=b"77" * 16,
cache_data=[(keys, values), (keys, values)],
token_count=8,
model_name="test-model",
layer_cache_types=["TurboQuantKVCache", "KVCache"],
layer_meta_states=[(8, 6.0, 0), (8,)],
)
assert saved is True
# Wait for the background writer to flush the file to disk.
file_path = mgr._get_file_path(b"77" * 16)
for _ in range(50):
if file_path.exists():
break
time.sleep(0.1)
assert file_path.exists(), "background writer never produced the file"
files = list((tmp_path / "ssd_cache").rglob("*.safetensors"))
assert len(files) == 1
_, metadata = mx.load(str(files[0]), return_metadata=True)
payload = json.loads(metadata["cache_signature"])
assert payload["turboquant_kv_bits"] == 6.0
finally:
mgr.close()