# 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()