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

309 lines
11 KiB
Python

# SPDX-License-Identifier: Apache-2.0
"""
Unit tests for prefetch policy interface and DefaultPrefetchPolicy.
Tests are written against the PrefetchPolicy contract defined in
prefetch_policy.py.
"""
# First Party
from lmcache.native_storage_ops import Bitmap
from lmcache.v1.distributed.api import ObjectKey
from lmcache.v1.distributed.l2_adapters.mock_l2_adapter import MockL2AdapterConfig
from lmcache.v1.distributed.storage_controllers.prefetch_policy import (
DefaultPrefetchPolicy,
RetainPrefetchPolicy,
)
from lmcache.v1.distributed.storage_controllers.store_policy import (
AdapterDescriptor,
)
# =============================================================================
# Helpers
# =============================================================================
def make_object_key(chunk_id: int) -> ObjectKey:
"""Create a test ObjectKey with the given chunk ID."""
return ObjectKey(
chunk_hash=ObjectKey.IntHash2Bytes(chunk_id),
model_name="test_model",
kv_rank=0,
)
def make_descriptor(index: int) -> AdapterDescriptor:
"""Create an AdapterDescriptor for testing."""
config = MockL2AdapterConfig(max_size_gb=1.0, mock_bandwidth_gb=10.0)
return AdapterDescriptor(index=index, config=config)
def make_bitmap(size: int, set_bits: list[int]) -> Bitmap:
"""Create a Bitmap with specific bits set."""
bitmap = Bitmap(size)
for i in set_bits:
bitmap.set(i)
return bitmap
def plan_to_indices(plan: dict[int, Bitmap]) -> dict[int, list[int]]:
"""Convert a Bitmap-based load plan to index lists for easy assertion."""
return {
adapter_idx: bitmap.get_indices_list() for adapter_idx, bitmap in plan.items()
}
# =============================================================================
# DefaultPrefetchPolicy Tests
# =============================================================================
class TestDefaultPrefetchPolicy:
"""Test DefaultPrefetchPolicy.select_load_plan behavior."""
def test_single_adapter_all_keys_found(self):
"""All found keys assigned to the single adapter."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(3)]
adapters = [make_descriptor(0)]
lookup_results = {0: make_bitmap(3, [0, 1, 2])}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {0: [0, 1, 2]}
def test_single_adapter_partial_hits(self):
"""Only found keys are in the plan."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(4)]
adapters = [make_descriptor(0)]
# Only keys 0 and 2 found
lookup_results = {0: make_bitmap(4, [0, 2])}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {0: [0, 2]}
def test_multi_adapter_overlap_first_wins(self):
"""When key is in multiple adapters, lowest-index adapter gets it."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(3)]
adapters = [make_descriptor(0), make_descriptor(1)]
# Both adapters have key 1
lookup_results = {
0: make_bitmap(3, [0, 1]),
1: make_bitmap(3, [1, 2]),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
# key 0 → adapter 0, key 1 → adapter 0 (first wins), key 2 → adapter 1
assert plan_to_indices(result) == {0: [0, 1], 1: [2]}
def test_multi_adapter_disjoint(self):
"""Each adapter gets its unique keys."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(4)]
adapters = [make_descriptor(0), make_descriptor(1)]
lookup_results = {
0: make_bitmap(4, [0, 1]),
1: make_bitmap(4, [2, 3]),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {0: [0, 1], 1: [2, 3]}
def test_no_hits_returns_empty(self):
"""Empty bitmaps → empty plan."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(3)]
adapters = [make_descriptor(0), make_descriptor(1)]
lookup_results = {
0: make_bitmap(3, []),
1: make_bitmap(3, []),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {}
def test_empty_adapters_returns_empty(self):
"""No adapters means no plan."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(0)]
result = policy.select_load_plan(keys, {}, [])
assert plan_to_indices(result) == {}
def test_empty_keys_returns_empty(self):
"""Empty keys list → empty plan."""
policy = DefaultPrefetchPolicy()
adapters = [make_descriptor(0)]
lookup_results = {0: make_bitmap(0, [])}
result = policy.select_load_plan([], lookup_results, adapters)
assert plan_to_indices(result) == {}
def test_adapter_order_matters(self):
"""Adapter with lower index always has priority, regardless of order
in the adapters list."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(0)]
# Adapter 1 listed first, but adapter 0 should win
adapters = [make_descriptor(1), make_descriptor(0)]
lookup_results = {
0: make_bitmap(1, [0]),
1: make_bitmap(1, [0]),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
# Adapter 0 gets the key (lower index wins)
assert plan_to_indices(result) == {0: [0]}
def test_three_adapters_with_overlap(self):
"""Three adapters with partial overlaps."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(5)]
adapters = [make_descriptor(0), make_descriptor(1), make_descriptor(2)]
lookup_results = {
0: make_bitmap(5, [0, 3]), # has keys 0, 3
1: make_bitmap(5, [1, 2, 3]), # has keys 1, 2, 3
2: make_bitmap(5, [2, 3, 4]), # has keys 2, 3, 4
}
result = policy.select_load_plan(keys, lookup_results, adapters)
# key 0 → adapter 0
# key 1 → adapter 1
# key 2 → adapter 1 (lower than adapter 2)
# key 3 → adapter 0 (lowest that has it)
# key 4 → adapter 2
assert plan_to_indices(result) == {0: [0, 3], 1: [1, 2], 2: [4]}
def test_missing_lookup_result_for_adapter(self):
"""Adapter with no lookup result is skipped."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(2)]
adapters = [make_descriptor(0), make_descriptor(1)]
# Only adapter 1 has results
lookup_results = {1: make_bitmap(2, [0, 1])}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {1: [0, 1]}
def test_each_key_appears_at_most_once(self):
"""No key should be assigned to multiple adapters."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(3)]
adapters = [make_descriptor(0), make_descriptor(1), make_descriptor(2)]
# All adapters have all keys
lookup_results = {
0: make_bitmap(3, [0, 1, 2]),
1: make_bitmap(3, [0, 1, 2]),
2: make_bitmap(3, [0, 1, 2]),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
# All keys should go to adapter 0 only
assert plan_to_indices(result) == {0: [0, 1, 2]}
assert 1 not in result
assert 2 not in result
# =============================================================================
# DefaultPrefetchPolicy.select_l1_retentions Tests
# =============================================================================
class TestDefaultPrefetchPolicyRetentions:
"""Test DefaultPrefetchPolicy.select_l1_retentions."""
def test_returns_all_false(self):
"""Default policy marks all keys as temporary."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(i) for i in range(5)]
result = policy.select_l1_retentions(keys)
assert result == [False] * 5
def test_empty_keys(self):
"""Empty keys list returns empty list."""
policy = DefaultPrefetchPolicy()
result = policy.select_l1_retentions([])
assert result == []
def test_single_key(self):
"""Single key returns single False."""
policy = DefaultPrefetchPolicy()
keys = [make_object_key(0)]
result = policy.select_l1_retentions(keys)
assert result == [False]
def test_length_matches_input(self):
"""Output length always matches input length."""
policy = DefaultPrefetchPolicy()
for n in [0, 1, 10, 100]:
keys = [make_object_key(i) for i in range(n)]
result = policy.select_l1_retentions(keys)
assert len(result) == n
# =============================================================================
# RetainPrefetchPolicy Tests
# =============================================================================
class TestRetainPrefetchPolicyRetentions:
"""Test RetainPrefetchPolicy.select_l1_retentions."""
def test_returns_all_true(self):
"""Retain policy marks all keys as permanent."""
policy = RetainPrefetchPolicy()
keys = [make_object_key(i) for i in range(5)]
result = policy.select_l1_retentions(keys)
assert result == [True] * 5
def test_empty_keys(self):
"""Empty keys list returns empty list."""
policy = RetainPrefetchPolicy()
result = policy.select_l1_retentions([])
assert result == []
def test_single_key(self):
"""Single key returns single True."""
policy = RetainPrefetchPolicy()
keys = [make_object_key(0)]
result = policy.select_l1_retentions(keys)
assert result == [True]
def test_length_matches_input(self):
"""Output length always matches input length."""
policy = RetainPrefetchPolicy()
for n in [0, 1, 10, 100]:
keys = [make_object_key(i) for i in range(n)]
result = policy.select_l1_retentions(keys)
assert len(result) == n
class TestRetainPrefetchPolicyLoadPlan:
"""RetainPrefetchPolicy inherits load plan from Default."""
def test_inherits_load_plan(self):
"""Load plan should behave identically to Default."""
policy = RetainPrefetchPolicy()
keys = [make_object_key(i) for i in range(3)]
adapters = [make_descriptor(0), make_descriptor(1)]
lookup_results = {
0: make_bitmap(3, [0, 1]),
1: make_bitmap(3, [1, 2]),
}
result = policy.select_load_plan(keys, lookup_results, adapters)
assert plan_to_indices(result) == {0: [0, 1], 1: [2]}