Deferred non-chunked inserts can happen while a CP HiCache per-layer backup is still in flight. Rolling back the unattached prepared backup at unfinished-request time drops the only reservation that final insertion can attach, so cache_finished_req has to start a post-forward backup and catch up all layers synchronously.\n\nKeep the prepared backup for non-chunked deferred inserts and continue rolling it back for chunked middle inserts, where the suffix may be extended by later chunks and the old backup would cover the wrong range. Document the failure mode so future changes do not rediscover the same fallback path.\n\nConstraint: CP HiCache radix split cannot mutate in-flight backup nodes.\nConstraint: Chunked prefill middle inserts still need rollback because their backup range is not final.\nRejected: Always rollback unattached backups | causes post-forward catch_up_all_layers for non-chunked deferred inserts.\nRejected: Always preserve unattached backups | can attach stale backup ranges for chunked middle inserts.\nConfidence: high\nScope-risk: narrow\nDirective: Do not clear req.cp_hicache_prepared_backup on non-chunked deferred insert without proving final insert no longer needs it.\nTested: remote cjy-glm5-new py_compile radix_cache.py\nTested: remote cjy-glm5-new pytest test_cp_hicache_metadata.py::{nonchunked preserve,chunked rollback} => 2 passed\nNot-tested: live ETE replay after restarting prefill with this exact commit
4183 lines
160 KiB
Python
4183 lines
160 KiB
Python
import inspect
|
|
import re
|
|
import sys
|
|
import types
|
|
import unittest
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import torch
|
|
|
|
# Prefer the real sgl_kernel package when the test image provides it so custom
|
|
# Torch operators are registered. Fall back to stubs on local CPU-only hosts.
|
|
try:
|
|
import sgl_kernel # noqa: F401
|
|
import sgl_kernel.kvcacheio # noqa: F401
|
|
except (ImportError, RuntimeError):
|
|
if "sgl_kernel" not in sys.modules:
|
|
sys.modules["sgl_kernel"] = types.ModuleType("sgl_kernel")
|
|
sys.modules["sgl_kernel"].__file__ = "sgl_kernel_stub.py"
|
|
sys.modules["sgl_kernel"].__path__ = []
|
|
if not hasattr(sys.modules["sgl_kernel"], "__getattr__"):
|
|
|
|
def _sgl_kernel_getattr(name):
|
|
if name.startswith("__"):
|
|
raise AttributeError(name)
|
|
fn = lambda *args, **kwargs: None
|
|
setattr(sys.modules["sgl_kernel"], name, fn)
|
|
return fn
|
|
|
|
sys.modules["sgl_kernel"].__getattr__ = _sgl_kernel_getattr
|
|
if "sgl_kernel.quantization" not in sys.modules:
|
|
quantization_stub = types.ModuleType("sgl_kernel.quantization")
|
|
quantization_stub.__file__ = "sgl_kernel_quantization_stub.py"
|
|
|
|
def _quantization_getattr(name):
|
|
if name.startswith("__"):
|
|
raise AttributeError(name)
|
|
fn = lambda *args, **kwargs: None
|
|
setattr(quantization_stub, name, fn)
|
|
return fn
|
|
|
|
quantization_stub.__getattr__ = _quantization_getattr
|
|
for _name in (
|
|
"ggml_dequantize",
|
|
"ggml_moe_a8",
|
|
"ggml_moe_a8_vec",
|
|
"ggml_moe_get_block_size",
|
|
"ggml_mul_mat_a8",
|
|
"ggml_mul_mat_vec_a8",
|
|
):
|
|
setattr(quantization_stub, _name, lambda *args, **kwargs: None)
|
|
sys.modules["sgl_kernel.quantization"] = quantization_stub
|
|
for _name in (
|
|
"sgl_per_token_group_quant_8bit",
|
|
"sgl_per_token_group_quant_fp8",
|
|
"sgl_per_token_quant_fp8",
|
|
"fp8_blockwise_scaled_mm",
|
|
"fp8_scaled_mm",
|
|
"silu_and_mul",
|
|
):
|
|
if not hasattr(sys.modules["sgl_kernel"], _name):
|
|
setattr(sys.modules["sgl_kernel"], _name, lambda *args, **kwargs: None)
|
|
_sgl_kernel_lib = torch.library.Library("sgl_kernel", "FRAGMENT")
|
|
for _schema in (
|
|
"sgl_per_token_group_quant_8bit(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_group_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s) -> ()",
|
|
"fp8_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype, Tensor? bias=None) -> Tensor",
|
|
"fp8_blockwise_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype) -> Tensor",
|
|
"moe_fused_gate(Tensor input, Tensor bias, int num_expert_group, int topk_group, int topk, int num_fused_shared_experts, float routed_scaling_factor, bool apply_routed_scaling_factor_on_output) -> Tensor[]",
|
|
):
|
|
try:
|
|
_sgl_kernel_lib.define(_schema)
|
|
except RuntimeError as exc:
|
|
if (
|
|
"already" not in str(exc).lower()
|
|
and "duplicate" not in str(exc).lower()
|
|
):
|
|
raise
|
|
if "sgl_kernel.kvcacheio" not in sys.modules:
|
|
sys.modules["sgl_kernel.kvcacheio"] = types.ModuleType("sgl_kernel.kvcacheio")
|
|
|
|
_sgl_kernel_lib = torch.library.Library("sgl_kernel", "FRAGMENT")
|
|
for _schema in (
|
|
"sgl_per_token_group_quant_8bit(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_group_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s, int group_size, float eps, float fp8_min, float fp8_max, bool scale_ue8m0) -> ()",
|
|
"sgl_per_token_quant_fp8(Tensor input, Tensor(a!) output_q, Tensor(b!) output_s) -> ()",
|
|
"fp8_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype, Tensor? bias=None) -> Tensor",
|
|
"fp8_blockwise_scaled_mm(Tensor mat_a, Tensor mat_b, Tensor scales_a, Tensor scales_b, ScalarType out_dtype) -> Tensor",
|
|
"moe_fused_gate(Tensor input, Tensor bias, int num_expert_group, int topk_group, int topk, int num_fused_shared_experts, float routed_scaling_factor, bool apply_routed_scaling_factor_on_output) -> Tensor[]",
|
|
):
|
|
try:
|
|
_sgl_kernel_lib.define(_schema)
|
|
except RuntimeError as exc:
|
|
if "already" not in str(exc).lower() and "duplicate" not in str(exc).lower():
|
|
raise
|
|
|
|
from sglang.srt.managers.cache_controller import (
|
|
HiCacheAck,
|
|
HiCacheWriteFailure,
|
|
HiCacheWriteReservation,
|
|
)
|
|
import sglang.srt.mem_cache.common as mem_cache_common
|
|
import sglang.srt.mem_cache.hiradix_cache as hiradix_cache
|
|
from sglang.srt.mem_cache.base_prefix_cache import (
|
|
EvictParams,
|
|
InsertParams,
|
|
MatchPrefixParams,
|
|
)
|
|
from sglang.srt.mem_cache.hiradix_cache import (
|
|
CpHiCacheCapacitySnapshot,
|
|
CpHiCacheEvictionPlan,
|
|
CpHiCacheNodeMetadata,
|
|
CpWriteAdmission,
|
|
HiRadixCache,
|
|
PendingHiCacheBackup,
|
|
PreparedCpHiCacheBackup,
|
|
_compute_shared_hicache_token_capacities,
|
|
)
|
|
from sglang.srt.mem_cache.memory_pool import NSATokenToKVPool
|
|
from sglang.srt.mem_cache.radix_cache import RadixKey, TreeNode, _key_match_paged
|
|
from sglang.srt.mem_cache.session_aware_cache import SessionAwareCache
|
|
from sglang.test.ci.ci_register import register_cpu_ci
|
|
from sglang.test.test_utils import CustomTestCase
|
|
|
|
register_cpu_ci(est_time=2, suite="stage-a-test-cpu")
|
|
|
|
|
|
class TestHiCacheEvictLoggingLevels(CustomTestCase):
|
|
def assert_debug_marker(self, source: str, marker: str):
|
|
self.assertIn(marker, source)
|
|
self.assertRegex(
|
|
source,
|
|
r"logger\.debug\(\s*\n\s*\"" + re.escape(marker),
|
|
msg=f"{marker} should be debug-only on the success/no-op hot path",
|
|
)
|
|
self.assertNotRegex(
|
|
source,
|
|
r"logger\.info\(\s*\n\s*\"" + re.escape(marker),
|
|
msg=f"{marker} must not stay at INFO on the success/no-op hot path",
|
|
)
|
|
|
|
def test_evict_hot_path_success_logs_are_debug_only(self):
|
|
common_source = inspect.getsource(mem_cache_common)
|
|
hiradix_source = inspect.getsource(hiradix_cache)
|
|
|
|
for marker in (
|
|
"[MemCache-evict] evict_from_tree_cache:",
|
|
"[MemCache-evict] _evict_for_compute_owner_lanes: evictable_size",
|
|
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d:",
|
|
"[MemCache-evict] _evict_for_compute_owner_lanes attempt=%d result:",
|
|
):
|
|
self.assert_debug_marker(common_source, marker)
|
|
|
|
for marker in (
|
|
"[HiCache-write] write_backup triggered: node_id=%d",
|
|
"[HiCache-write] write_backup CP SUBMITTED: node_id=%d",
|
|
"[HiCache-write] write_backup non-CP SUCCESS: node_id=%d",
|
|
"[HiCache-write] write_backup non-CP FAILED (host full): node_id=%d",
|
|
"[HiCache-write] _inc_hit_count triggering write_through: node_id=%d",
|
|
"[HiCache-write] writing_check released %d write locks:",
|
|
"[HiCache-load] load_back CP: node_id=%d",
|
|
"[HiCache-load] load_back CP SUCCESS: node_id=%d",
|
|
"[HiCache-load] load_back non-CP SUCCESS: node_id=%d",
|
|
"[HiCache-collective] tag=%s",
|
|
):
|
|
self.assert_debug_marker(hiradix_source, marker)
|
|
|
|
import sglang.srt.managers.cache_controller as cache_controller_mod
|
|
|
|
cache_controller_source = inspect.getsource(cache_controller_mod)
|
|
for marker in (
|
|
"[CacheCtrl-write] write: node_id=%d",
|
|
"[CacheCtrl-write] write non-CP submitted: node_id=%d",
|
|
"[CacheCtrl-write] submit_write_cp_all_layer submitted: node_id=%d",
|
|
"[CacheCtrl-write] submit_write_cp_layer final ack: node_id=%d",
|
|
):
|
|
self.assert_debug_marker(cache_controller_source, marker)
|
|
|
|
self.assertRegex(
|
|
hiradix_source,
|
|
r"logger\.warning\(\s*\n\s*\"\[HiCache-write\] write_backup CP FAILED after deterministic retry:",
|
|
)
|
|
self.assertNotRegex(
|
|
hiradix_source,
|
|
r"logger\.info\(\s*\n\s*\"\[HiCache-write\] write_backup CP FAILED after deterministic retry:",
|
|
)
|
|
|
|
|
|
class TestCpHiCacheImports(CustomTestCase):
|
|
def test_cp_hicache_public_imports_without_sgl_kernel(self):
|
|
import subprocess
|
|
|
|
subprocess.run(
|
|
[
|
|
sys.executable,
|
|
"-c",
|
|
"from sglang.srt.mem_cache.hiradix_cache import HiRadixCache, CpHiCacheNodeMetadata; "
|
|
"from sglang.srt.managers.cache_controller import HiCacheController; "
|
|
"print('OK')",
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
text=True,
|
|
)
|
|
|
|
|
|
class TestHiRadixCacheCPDraftHostPool(CustomTestCase):
|
|
def test_fp8_nsa_hicache_size_estimate_uses_packed_row_width(self):
|
|
pool = object.__new__(NSATokenToKVPool)
|
|
pool.store_dtype = torch.uint8
|
|
pool.kv_cache_dim = 656
|
|
pool.layer_num = 78
|
|
pool.index_head_dim = 128
|
|
pool.quant_block_size = 128
|
|
|
|
size_per_token = hiradix_cache._estimate_hicache_size_per_token(pool)
|
|
|
|
# FP8 NSA stores packed MLA rows as 656 uint8 bytes/token/layer plus
|
|
# the paged index buffer: 128 uint8 K bytes + 4 uint8 scale bytes.
|
|
self.assertEqual(size_per_token, (656 + 128 + 4) * 78)
|
|
|
|
def test_fp8_nsa_hicache_size_estimate_uses_active_index_layers(self):
|
|
pool = object.__new__(NSATokenToKVPool)
|
|
pool.store_dtype = torch.uint8
|
|
pool.kv_cache_dim = 656
|
|
pool.layer_num = 78
|
|
pool.index_head_dim = 128
|
|
pool.quant_block_size = 128
|
|
pool.index_active_layer_ids = tuple(range(0, 78, 4))
|
|
|
|
size_per_token = hiradix_cache._estimate_hicache_size_per_token(pool)
|
|
|
|
indexer_size_per_token = 128 + 4
|
|
self.assertEqual(
|
|
size_per_token,
|
|
656 * 78 + indexer_size_per_token * len(pool.index_active_layer_ids),
|
|
)
|
|
|
|
def test_fp8_shared_budget_matches_target_and_one_layer_draft_capacity(self):
|
|
bytes_per_layer = 656 + 128 + 4
|
|
target_size_per_token = bytes_per_layer * 78
|
|
draft_size_per_token = bytes_per_layer
|
|
|
|
target_tokens, draft_tokens = _compute_shared_hicache_token_capacities(
|
|
total_host_bytes=int(150 * 1e9),
|
|
target_size_per_token=target_size_per_token,
|
|
draft_size_per_token=draft_size_per_token,
|
|
page_size=64,
|
|
)
|
|
|
|
self.assertGreaterEqual(draft_tokens, target_tokens)
|
|
self.assertLessEqual(
|
|
target_tokens * target_size_per_token
|
|
+ draft_tokens * draft_size_per_token,
|
|
int(150 * 1e9),
|
|
)
|
|
# The draft pool is much smaller in bytes because GLM-5 EAGLE draft has
|
|
# one executable layer, but it still has at least target token capacity.
|
|
self.assertLess(
|
|
draft_tokens * draft_size_per_token,
|
|
target_tokens * target_size_per_token // 32,
|
|
)
|
|
|
|
def test_shared_budget_keeps_draft_at_least_target_capacity(self):
|
|
target_tokens, draft_tokens = _compute_shared_hicache_token_capacities(
|
|
total_host_bytes=1000,
|
|
target_size_per_token=6,
|
|
draft_size_per_token=2,
|
|
page_size=10,
|
|
)
|
|
|
|
self.assertEqual(target_tokens, 120)
|
|
self.assertEqual(draft_tokens, 140)
|
|
self.assertGreaterEqual(draft_tokens, target_tokens)
|
|
self.assertLessEqual(target_tokens * 6 + draft_tokens * 2, 1000)
|
|
|
|
def test_shared_budget_handles_equal_target_and_draft_size(self):
|
|
target_tokens, draft_tokens = _compute_shared_hicache_token_capacities(
|
|
total_host_bytes=1000,
|
|
target_size_per_token=6,
|
|
draft_size_per_token=6,
|
|
page_size=10,
|
|
)
|
|
|
|
self.assertEqual(target_tokens, 80)
|
|
self.assertEqual(draft_tokens, 80)
|
|
self.assertLessEqual(target_tokens * 6 + draft_tokens * 6, 1000)
|
|
|
|
def test_reset_clears_target_and_draft_host_pools(self):
|
|
class ClearablePool:
|
|
def __init__(self):
|
|
self.clear_calls = 0
|
|
|
|
def clear(self):
|
|
self.clear_calls += 1
|
|
|
|
class Controller:
|
|
def __init__(self):
|
|
self.reset_calls = 0
|
|
self.clear_draft_calls = 0
|
|
|
|
def reset(self):
|
|
self.reset_calls += 1
|
|
|
|
def clear_draft_host_pool(self):
|
|
self.clear_draft_calls += 1
|
|
|
|
target_pool = ClearablePool()
|
|
controller = Controller()
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.cache_controller = controller
|
|
cache.token_to_kv_pool_host = target_pool
|
|
cache.prefetch_loaded_tokens_by_reqid = {}
|
|
cache.evictable_host_leaves = set()
|
|
cache.pinned_size_ = 1
|
|
cache.evictable_leaves = set()
|
|
cache._record_all_cleared_event = lambda: None
|
|
|
|
cache.reset()
|
|
|
|
self.assertEqual(controller.reset_calls, 1)
|
|
self.assertEqual(target_pool.clear_calls, 1)
|
|
self.assertEqual(controller.clear_draft_calls, 1)
|
|
|
|
|
|
class TestCpHiCacheNodeMetadata(CustomTestCase):
|
|
def test_paged_key_match_returns_valid_tail_length_not_next_page(self):
|
|
key = RadixKey(list(range(6)))
|
|
|
|
self.assertEqual(_key_match_paged(key, key, page_size=4), 6)
|
|
|
|
def test_split_zero_len_moves_all_positions_to_child(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([1, 3, 7], dtype=torch.int64),
|
|
host_indices=torch.tensor([10, 11, 12], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(8, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
parent, child = metadata.split(0)
|
|
|
|
self.assertEqual(parent.logical_len, 0)
|
|
self.assertEqual(parent.owned_positions.tolist(), [])
|
|
self.assertEqual(parent.host_indices.tolist(), [])
|
|
self.assertEqual(child.logical_len, 8)
|
|
self.assertEqual(child.owned_positions.tolist(), [1, 3, 7])
|
|
self.assertEqual(child.host_indices.tolist(), [10, 11, 12])
|
|
|
|
def test_split_rebases_child_positions(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=10,
|
|
owned_positions=torch.tensor([0, 2, 5, 9], dtype=torch.int64),
|
|
host_indices=torch.tensor([20, 21, 22, 23], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(10, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
parent, child = metadata.split(5)
|
|
|
|
self.assertEqual(parent.logical_len, 5)
|
|
self.assertEqual(parent.owned_positions.tolist(), [0, 2])
|
|
self.assertEqual(parent.host_indices.tolist(), [20, 21])
|
|
self.assertEqual(child.logical_len, 5)
|
|
self.assertEqual(child.owned_positions.tolist(), [0, 4])
|
|
self.assertEqual(child.host_indices.tolist(), [22, 23])
|
|
|
|
def test_split_keeps_draft_host_indices_aligned_with_owned_positions(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=10,
|
|
owned_positions=torch.tensor([0, 2, 5, 9], dtype=torch.int64),
|
|
host_indices=torch.tensor([20, 21, 22, 23], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([120, 121, 122, 123], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(10, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
parent, child = metadata.split(5)
|
|
|
|
self.assertEqual(parent.host_indices.tolist(), [20, 21])
|
|
self.assertEqual(parent.draft_host_indices.tolist(), [120, 121])
|
|
self.assertEqual(child.owned_positions.tolist(), [0, 4])
|
|
self.assertEqual(child.host_indices.tolist(), [22, 23])
|
|
self.assertEqual(child.draft_host_indices.tolist(), [122, 123])
|
|
|
|
def test_zero_owned_metadata_is_valid(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.empty((0,), dtype=torch.int32),
|
|
host_indices=torch.empty((0,), dtype=torch.int32),
|
|
page_owners=torch.zeros(max(64, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
self.assertEqual(metadata.logical_len, 64)
|
|
self.assertEqual(metadata.owned_positions.device.type, "cpu")
|
|
self.assertEqual(metadata.host_indices.device.type, "cpu")
|
|
self.assertEqual(metadata.owned_positions.dtype, torch.int64)
|
|
self.assertEqual(metadata.host_indices.dtype, torch.int64)
|
|
|
|
def test_valid_length_can_be_shorter_than_physical_padded_length(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=100,
|
|
padded_len=128,
|
|
owned_positions=torch.tensor([0, 63, 100, 127], dtype=torch.int64),
|
|
host_indices=torch.tensor([10, 11, 12, 13], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
self.assertEqual(metadata.logical_len, 100)
|
|
self.assertEqual(metadata.valid_len, 100)
|
|
self.assertEqual(metadata.padded_len, 128)
|
|
self.assertEqual(metadata.page_owners.tolist(), [0, 1])
|
|
|
|
def test_non_int64_inputs_are_converted(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1, 3], dtype=torch.int32),
|
|
host_indices=torch.tensor([10, 11], dtype=torch.int32),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
self.assertEqual(metadata.owned_positions.dtype, torch.int64)
|
|
self.assertEqual(metadata.host_indices.dtype, torch.int64)
|
|
|
|
def test_negative_logical_len_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "logical_len"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=-1,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(0, dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
def test_metadata_does_not_alias_input_tensors(self):
|
|
owned_positions = torch.tensor([1, 3], dtype=torch.int64)
|
|
host_indices = torch.tensor([10, 11], dtype=torch.int64)
|
|
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=owned_positions,
|
|
host_indices=host_indices,
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
owned_positions[0] = 2
|
|
host_indices[0] = 12
|
|
|
|
self.assertEqual(metadata.owned_positions.tolist(), [1, 3])
|
|
self.assertEqual(metadata.host_indices.tolist(), [10, 11])
|
|
|
|
def test_invalid_split_raises(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1], dtype=torch.int64),
|
|
host_indices=torch.tensor([9], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
with self.assertRaisesRegex(ValueError, "split_len"):
|
|
metadata.split(5)
|
|
|
|
def test_unsorted_positions_raise(self):
|
|
with self.assertRaisesRegex(ValueError, "sorted"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([2, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([9, 10], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
def test_duplicate_positions_raise(self):
|
|
with self.assertRaisesRegex(ValueError, "strictly increasing"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([9, 10], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
def test_length_mismatch_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "same length"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1, 2], dtype=torch.int64),
|
|
host_indices=torch.tensor([9], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
def test_draft_host_length_mismatch_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "draft_host_indices.*same length"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1, 2], dtype=torch.int64),
|
|
host_indices=torch.tensor([9, 10], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([109], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
def test_out_of_range_positions_raise(self):
|
|
with self.assertRaisesRegex(ValueError, r"\[0, logical_len\)"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([4], dtype=torch.int64),
|
|
host_indices=torch.tensor([9], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
# ── New: validators for page_owners + page_size (the fields that carry the
|
|
# CP owner pattern across a HiCache write→load round-trip).
|
|
|
|
def test_zero_page_size_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "page_size must be positive"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.empty((0,), dtype=torch.int8),
|
|
page_size=0,
|
|
)
|
|
|
|
def test_logical_len_not_multiple_of_page_size_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "multiple of"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=10,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(2, dtype=torch.int8),
|
|
page_size=4, # 10 % 4 != 0
|
|
)
|
|
|
|
def test_page_owners_length_mismatch_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "page_owners length"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(3, dtype=torch.int8), # expected 8/4=2
|
|
page_size=4,
|
|
)
|
|
|
|
def test_negative_page_owners_raises(self):
|
|
with self.assertRaisesRegex(ValueError, "page_owners.*non-negative"):
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([-1, 0], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
|
|
def test_page_owners_normalized_to_int8_cpu(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
# Pass int64 to test normalization.
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int64),
|
|
page_size=4,
|
|
)
|
|
self.assertEqual(metadata.page_owners.dtype, torch.int8)
|
|
self.assertEqual(metadata.page_owners.device.type, "cpu")
|
|
self.assertEqual(metadata.page_owners.tolist(), [0, 1])
|
|
|
|
def test_page_owners_not_aliased_to_input(self):
|
|
page_owners = torch.tensor([0, 1, 0, 1], dtype=torch.int8)
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=page_owners,
|
|
page_size=4,
|
|
)
|
|
page_owners[0] = 1
|
|
self.assertEqual(metadata.page_owners.tolist(), [0, 1, 0, 1])
|
|
|
|
def test_split_non_page_aligned_split_len_raises(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.tensor([0, 8], dtype=torch.int64),
|
|
host_indices=torch.tensor([20, 21], dtype=torch.int64),
|
|
page_owners=torch.zeros(4, dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
with self.assertRaisesRegex(ValueError, "must be a multiple of page_size"):
|
|
metadata.split(5) # 5 % 4 != 0
|
|
|
|
def test_split_preserves_page_owners_slice(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=32, # 8 pages of page_size=4
|
|
owned_positions=torch.tensor([0, 4, 16, 28], dtype=torch.int64),
|
|
host_indices=torch.tensor([100, 101, 102, 103], dtype=torch.int64),
|
|
page_owners=torch.tensor(
|
|
[0, 1, 0, 1, 0, 1, 0, 1], dtype=torch.int8
|
|
),
|
|
page_size=4,
|
|
)
|
|
parent, child = metadata.split(16) # split at page 4 of 8.
|
|
self.assertEqual(parent.page_owners.tolist(), [0, 1, 0, 1])
|
|
self.assertEqual(child.page_owners.tolist(), [0, 1, 0, 1])
|
|
self.assertEqual(parent.page_size, 4)
|
|
self.assertEqual(child.page_size, 4)
|
|
|
|
def test_split_at_zero_yields_empty_parent_page_owners(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([0], dtype=torch.int64),
|
|
host_indices=torch.tensor([50], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
parent, child = metadata.split(0)
|
|
self.assertEqual(parent.page_owners.numel(), 0)
|
|
self.assertEqual(child.page_owners.tolist(), [0, 1])
|
|
|
|
def test_split_at_logical_len_yields_empty_child_page_owners(self):
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([0], dtype=torch.int64),
|
|
host_indices=torch.tensor([50], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
parent, child = metadata.split(8)
|
|
self.assertEqual(parent.page_owners.tolist(), [0, 1])
|
|
self.assertEqual(child.page_owners.numel(), 0)
|
|
|
|
|
|
class TestCpHiCacheFreeRoom(CustomTestCase):
|
|
def test_free_room_deficit_does_not_evict_when_required_fits_trigger(self):
|
|
deficit = hiradix_cache._free_room_deficit(
|
|
required=64,
|
|
available=96,
|
|
capacity=256,
|
|
page_size=64,
|
|
target_ratio=0.5,
|
|
trigger_ratio=0.0,
|
|
)
|
|
|
|
self.assertEqual(deficit, 0)
|
|
|
|
def test_free_room_deficit_evicts_to_target_after_trigger(self):
|
|
deficit = hiradix_cache._free_room_deficit(
|
|
required=64,
|
|
available=96,
|
|
capacity=256,
|
|
page_size=64,
|
|
target_ratio=0.5,
|
|
trigger_ratio=0.25,
|
|
)
|
|
|
|
self.assertEqual(deficit, 128)
|
|
|
|
def test_free_room_deficit_trigger_accounts_for_pending_reservation(self):
|
|
deficit = hiradix_cache._free_room_deficit(
|
|
required=64,
|
|
available=96,
|
|
capacity=256,
|
|
page_size=64,
|
|
target_ratio=0.5,
|
|
trigger_ratio=0.25,
|
|
)
|
|
|
|
# available=96 is above trigger_room=64, but after reserving required=64
|
|
# only 32 tokens would remain. Trigger checks available - required,
|
|
# then evicts at least target_room=128 tokens.
|
|
self.assertEqual(deficit, 128)
|
|
|
|
def test_free_room_deficit_rounds_room_to_page(self):
|
|
deficit = hiradix_cache._free_room_deficit(
|
|
required=64,
|
|
available=0,
|
|
capacity=100,
|
|
page_size=64,
|
|
target_ratio=0.01,
|
|
trigger_ratio=0.0,
|
|
)
|
|
|
|
self.assertEqual(deficit, 64)
|
|
|
|
def test_cp_host_write_admission_uses_trigger_target_room(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.page_size = 64
|
|
cache.hicache_host_free_room_ratio = 0.5
|
|
cache.hicache_host_free_room_trigger_ratio = 0.25
|
|
cache._cp_required_host_tokens_by_rank = lambda _indices: (64, 0)
|
|
cache._cp_host_capacity_snapshot = lambda: CpHiCacheCapacitySnapshot(
|
|
target_capacity=(256, 256),
|
|
draft_capacity=None,
|
|
committed_target=(160, 0),
|
|
committed_draft=(0, 0),
|
|
pending_target=(0, 0),
|
|
pending_draft=(0, 0),
|
|
)
|
|
cache._plan_cp_host_evictions = lambda deficit: CpHiCacheEvictionPlan(
|
|
victims=(),
|
|
planned_freed=tuple(0 for _ in deficit),
|
|
remaining_deficit=tuple(0 for _ in deficit),
|
|
)
|
|
|
|
admission = cache._cp_build_write_admission(
|
|
torch.arange(64, dtype=torch.int64),
|
|
node_id=600,
|
|
phase="unit",
|
|
)
|
|
|
|
self.assertEqual(admission.target_available_by_owner, (96, 256))
|
|
self.assertEqual(admission.deficit_by_owner, (128, 0))
|
|
|
|
def test_cp_host_write_admission_does_not_evict_when_trigger_room_fits(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.page_size = 64
|
|
cache.hicache_host_free_room_ratio = 0.5
|
|
cache.hicache_host_free_room_trigger_ratio = 0.0
|
|
cache._cp_required_host_tokens_by_rank = lambda _indices: (64, 0)
|
|
cache._cp_host_capacity_snapshot = lambda: CpHiCacheCapacitySnapshot(
|
|
target_capacity=(256, 256),
|
|
draft_capacity=None,
|
|
committed_target=(160, 0),
|
|
committed_draft=(0, 0),
|
|
pending_target=(0, 0),
|
|
pending_draft=(0, 0),
|
|
)
|
|
cache._plan_cp_host_evictions = lambda deficit: CpHiCacheEvictionPlan(
|
|
victims=(),
|
|
planned_freed=tuple(0 for _ in deficit),
|
|
remaining_deficit=tuple(0 for _ in deficit),
|
|
)
|
|
|
|
admission = cache._cp_build_write_admission(
|
|
torch.arange(64, dtype=torch.int64),
|
|
node_id=601,
|
|
phase="unit",
|
|
)
|
|
|
|
self.assertEqual(admission.target_available_by_owner, (96, 256))
|
|
self.assertEqual(admission.deficit_by_owner, (0, 0))
|
|
|
|
def test_cp_host_write_admission_uses_draft_room_deficit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.page_size = 64
|
|
cache.hicache_host_free_room_ratio = 0.5
|
|
cache.hicache_host_free_room_trigger_ratio = 0.25
|
|
cache._cp_required_host_tokens_by_rank = lambda _indices: (64, 0)
|
|
cache._cp_host_capacity_snapshot = lambda: CpHiCacheCapacitySnapshot(
|
|
target_capacity=(256, 256),
|
|
draft_capacity=(256, 256),
|
|
committed_target=(0, 0),
|
|
committed_draft=(192, 0),
|
|
pending_target=(0, 0),
|
|
pending_draft=(0, 0),
|
|
)
|
|
cache._plan_cp_host_evictions = lambda deficit: CpHiCacheEvictionPlan(
|
|
victims=(),
|
|
planned_freed=tuple(0 for _ in deficit),
|
|
remaining_deficit=tuple(0 for _ in deficit),
|
|
)
|
|
|
|
admission = cache._cp_build_write_admission(
|
|
torch.arange(64, dtype=torch.int64),
|
|
node_id=602,
|
|
phase="unit",
|
|
)
|
|
|
|
self.assertEqual(admission.target_available_by_owner, (256, 256))
|
|
self.assertEqual(admission.draft_available_by_owner, (64, 256))
|
|
self.assertEqual(admission.deficit_by_owner, (128, 0))
|
|
|
|
def test_cp_host_write_batch_admission_uses_aggregate_required(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.page_size = 64
|
|
cache.hicache_host_free_room_ratio = 0.5
|
|
cache.hicache_host_free_room_trigger_ratio = 0.25
|
|
cache._cp_host_capacity_snapshot = lambda: CpHiCacheCapacitySnapshot(
|
|
target_capacity=(256, 256),
|
|
draft_capacity=None,
|
|
committed_target=(160, 0),
|
|
committed_draft=(0, 0),
|
|
pending_target=(0, 0),
|
|
pending_draft=(0, 0),
|
|
)
|
|
cache._plan_cp_host_evictions = lambda deficit: CpHiCacheEvictionPlan(
|
|
victims=(),
|
|
planned_freed=tuple(0 for _ in deficit),
|
|
remaining_deficit=tuple(0 for _ in deficit),
|
|
)
|
|
|
|
admission = cache._cp_build_write_admission_from_required(
|
|
(128, 0),
|
|
node_id=603,
|
|
phase="batch_unit",
|
|
)
|
|
|
|
self.assertEqual(admission.required_by_owner, (128, 0))
|
|
self.assertEqual(admission.target_available_by_owner, (96, 256))
|
|
# required=128, target_room=128, available=96.
|
|
self.assertEqual(admission.deficit_by_owner, (128, 0))
|
|
|
|
def test_prepare_write_backups_for_reqs_runs_one_batch_admission(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 1
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(16, dtype=torch.int64).view(2, 8)
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id, host_start=700
|
|
),
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id, host_start=800
|
|
),
|
|
]
|
|
)
|
|
cache._probe_existing_radix_prefix_len_no_split = lambda key: 0
|
|
cache._cp_zero_counts = lambda cp_size=None: (0,)
|
|
cache._cp_add_counts = lambda lhs, rhs: (lhs[0] + rhs[0],)
|
|
cache._cp_required_host_tokens_by_rank = lambda indices: (int(indices.numel()),)
|
|
batch_admissions = []
|
|
evicted_admissions = []
|
|
|
|
def build_batch_admission(required, *, node_id, phase):
|
|
batch_admissions.append((required, node_id, phase))
|
|
return CpWriteAdmission(
|
|
node_id=node_id,
|
|
phase=phase,
|
|
required_by_owner=required,
|
|
target_available_by_owner=(0,),
|
|
draft_available_by_owner=(0,),
|
|
deficit_by_owner=(1,),
|
|
eviction_plan=CpHiCacheEvictionPlan(
|
|
victims=(),
|
|
planned_freed=(1,),
|
|
remaining_deficit=(0,),
|
|
),
|
|
)
|
|
|
|
cache._cp_build_write_admission_from_required = build_batch_admission
|
|
cache._evict_cp_host_for_write_admission = (
|
|
lambda admission, **_: evicted_admissions.append(admission) or True
|
|
)
|
|
|
|
req0 = types.SimpleNamespace(
|
|
rid="batch-rid-0",
|
|
fill_ids=list(range(4)),
|
|
cache_protected_len=0,
|
|
req_pool_idx=0,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
req1 = types.SimpleNamespace(
|
|
rid="batch-rid-1",
|
|
fill_ids=list(range(6)),
|
|
cache_protected_len=0,
|
|
req_pool_idx=1,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backups_for_reqs([req0, req1])
|
|
|
|
self.assertEqual(len(batch_admissions), 1)
|
|
self.assertEqual(batch_admissions[0][0], (10,))
|
|
self.assertEqual(batch_admissions[0][2], "batch_prepare")
|
|
self.assertEqual(len(evicted_admissions), 1)
|
|
self.assertIsNotNone(req0.cp_hicache_prepared_backup)
|
|
self.assertIsNotNone(req1.cp_hicache_prepared_backup)
|
|
self.assertEqual(len(cache.cache_controller.submitted), 2)
|
|
|
|
|
|
class FakeWriteFailure:
|
|
metadata = None
|
|
|
|
def __init__(self, required_host_slots):
|
|
self.required_host_slots = required_host_slots
|
|
|
|
|
|
class FakeWriteSuccess:
|
|
required_host_slots = 0
|
|
|
|
def __init__(self, metadata):
|
|
self.metadata = metadata
|
|
|
|
|
|
class FakeWriteController:
|
|
def __init__(self, required_host_slots):
|
|
self.required_host_slots = required_host_slots
|
|
self.calls = 0
|
|
self.write_policy = "write_through"
|
|
self.evicted_host_indices = []
|
|
|
|
def write(self, device_indices, node_id=-1, priority=None):
|
|
self.calls += 1
|
|
if self.calls == 1:
|
|
return FakeWriteFailure(self.required_host_slots)
|
|
return FakeWriteSuccess(
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=len(device_indices),
|
|
owned_positions=torch.tensor([0], dtype=torch.int64),
|
|
host_indices=torch.tensor([99], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(len(device_indices), 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
)
|
|
|
|
def evict_host(self, host_indices):
|
|
self.evicted_host_indices.append(host_indices.clone())
|
|
return len(host_indices)
|
|
|
|
|
|
class FakeZeroOwnedWriteController:
|
|
write_policy = "write_through"
|
|
|
|
def write(self, device_indices, node_id=-1, priority=None):
|
|
return FakeWriteSuccess(
|
|
CpHiCacheNodeMetadata(
|
|
logical_len=len(device_indices),
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(max(len(device_indices), 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
)
|
|
|
|
|
|
class FakeReserveWriteController:
|
|
write_policy = "write_through"
|
|
has_draft_hicache = False
|
|
|
|
def __init__(self, results):
|
|
self.results = list(results)
|
|
self.reservations = []
|
|
self.submitted = []
|
|
self.submit_kwargs = []
|
|
self.evicted_host_indices = []
|
|
|
|
def reserve_write_cp(self, device_indices, priority=None, node_id=-1):
|
|
self.reservations.append((device_indices.clone(), node_id))
|
|
result = self.results.pop(0)
|
|
if not callable(result):
|
|
return result
|
|
try:
|
|
return result(device_indices, node_id=node_id)
|
|
except TypeError:
|
|
return result(device_indices)
|
|
|
|
def submit_write_cp_all_layer(self, reservation):
|
|
self.submitted.append(reservation)
|
|
|
|
def submit_write_cp_per_layer(self, reservation, **kwargs):
|
|
self.submitted.append(reservation)
|
|
self.submit_kwargs.append(kwargs)
|
|
|
|
def evict_cp_host(self, metadata):
|
|
self.evicted_host_indices.append(metadata.host_indices.clone())
|
|
return len(metadata.host_indices)
|
|
|
|
|
|
def make_write_reservation(device_indices, node_id=0, host_start=90):
|
|
host_indices = torch.arange(host_start, host_start + len(device_indices))
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=len(device_indices),
|
|
owned_positions=torch.arange(len(device_indices), dtype=torch.int64),
|
|
host_indices=host_indices,
|
|
page_owners=torch.zeros(max(len(device_indices), 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
return HiCacheWriteReservation(
|
|
metadata=metadata,
|
|
host_indices=host_indices,
|
|
physical_device_indices=device_indices.clone(),
|
|
node_id=node_id,
|
|
)
|
|
|
|
|
|
class FakeEvictionStrategy:
|
|
def get_priority(self, node):
|
|
return 0
|
|
|
|
|
|
class FakeCpLayout:
|
|
def __init__(self, cp_size=4, cp_rank=0, page_size=1):
|
|
self.cp_size = cp_size
|
|
self.cp_rank = cp_rank
|
|
self.page_size = page_size
|
|
|
|
def owner_for_logical_pages(self, logical_pages):
|
|
owners = torch.remainder(logical_pages - 1, self.cp_size)
|
|
return torch.where(logical_pages <= 0, torch.full_like(owners, -1), owners)
|
|
|
|
def logical_locs_to_physical(self, logical_locs):
|
|
return logical_locs
|
|
|
|
def owned_by_this_rank(self, logical_locs):
|
|
logical_pages = torch.div(
|
|
logical_locs, self.page_size, rounding_mode="floor"
|
|
)
|
|
return self.owner_for_logical_pages(logical_pages) == self.cp_rank
|
|
|
|
|
|
class FakeEvictDeviceController:
|
|
write_policy = "write_through"
|
|
|
|
def evict_device(self, device_indices):
|
|
return len(device_indices)
|
|
|
|
|
|
class FakeTokenAllocator:
|
|
def available_size(self):
|
|
return 0
|
|
|
|
def compute_owner_lane_stats(self, page_owners):
|
|
if len(page_owners) == 0:
|
|
return [], [], []
|
|
cp_size = max(int(owner) for owner in page_owners) + 1
|
|
required = [0] * cp_size
|
|
for owner in page_owners:
|
|
required[int(owner)] += 1
|
|
available = list(required)
|
|
deficits = [0] * cp_size
|
|
return required, available, deficits
|
|
|
|
def allocator_state_str(self):
|
|
return "FakeTokenAllocator"
|
|
|
|
|
|
class RecordingTokenAllocator:
|
|
def __init__(self):
|
|
self.freed = []
|
|
|
|
def free(self, free_index):
|
|
if free_index.numel() > 0:
|
|
self.freed.append(free_index.detach().cpu().tolist())
|
|
|
|
|
|
class RecordingDeviceAllocator:
|
|
def __init__(self):
|
|
self.freed = []
|
|
|
|
def free(self, free_index):
|
|
self.freed.append(free_index.detach().cpu().tolist())
|
|
|
|
|
|
class TestHiRadixCacheCPBackup(CustomTestCase):
|
|
def _minimal_cp_hiradix_cache(self, *, page_size=64):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.page_size = page_size
|
|
cache.is_eagle = False
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
cache.evictable_size_ = 0
|
|
cache.protected_size_ = 0
|
|
cache.evictable_leaves = set()
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:page_size])
|
|
cache.key_match_fn = lambda key0, key1: _key_match_paged(
|
|
key0, key1, page_size
|
|
)
|
|
cache.cache_controller = types.SimpleNamespace(write_policy="write_back")
|
|
cache._record_store_event = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
|
|
root = TreeNode()
|
|
root.key = RadixKey(token_ids=[], extra_key=None)
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.children = {}
|
|
root.parent = None
|
|
cache.root_node = root
|
|
return cache
|
|
|
|
def test_cp_eagle_finished_cache_preserves_retained_tail_page(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable_finished_insert = False
|
|
cache.disable = False
|
|
cache.is_eagle = True
|
|
cache.page_size = 64
|
|
cache._uses_cp_hicache = True
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(128, dtype=torch.int64).view(1, 128)
|
|
)
|
|
allocator = RecordingTokenAllocator()
|
|
cache.token_to_kv_pool_allocator = allocator
|
|
cache.insert = lambda params: types.SimpleNamespace(prefix_len=0)
|
|
cache.dec_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
origin_input_ids=[10, 11, 12, 13],
|
|
output_ids=[],
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=0,
|
|
last_node=object(),
|
|
cp_hicache_prepared_backup=None,
|
|
pop_committed_kv_cache=lambda: 4,
|
|
)
|
|
|
|
cache.cache_finished_req(req)
|
|
|
|
self.assertEqual(allocator.freed, [])
|
|
|
|
def test_cp_valid_tail_device_accounting_uses_physical_page_span(self):
|
|
cache = self._minimal_cp_hiradix_cache(page_size=64)
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(token_ids=[1, 2, 3], extra_key=None),
|
|
value=torch.arange(3, dtype=torch.int64),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(result.prefix_len, 0)
|
|
self.assertEqual(cache.evictable_size_, 64)
|
|
node = next(iter(cache.root_node.children.values()))
|
|
|
|
cache.inc_node_lock_ref(node)
|
|
self.assertEqual(cache.evictable_size_, 0)
|
|
self.assertEqual(cache.protected_size_, 64)
|
|
|
|
cache.dec_node_lock_ref(node)
|
|
self.assertEqual(cache.evictable_size_, 64)
|
|
self.assertEqual(cache.protected_size_, 0)
|
|
|
|
def test_cp_valid_tail_regular_evict_reports_and_subtracts_physical_page(self):
|
|
cache = self._minimal_cp_hiradix_cache(page_size=64)
|
|
device_allocator = RecordingDeviceAllocator()
|
|
cache.cache_controller.mem_pool_device_allocator = device_allocator
|
|
cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(token_ids=[1, 2, 3], extra_key=None),
|
|
value=torch.arange(3, dtype=torch.int64),
|
|
)
|
|
)
|
|
node = next(iter(cache.root_node.children.values()))
|
|
|
|
num_evicted = cache._evict_regular(node)
|
|
|
|
self.assertEqual(num_evicted, 64)
|
|
self.assertEqual(cache.evictable_size_, 0)
|
|
self.assertEqual(device_allocator.freed, [[0, 1, 2]])
|
|
self.assertEqual(cache.root_node.children, {})
|
|
|
|
def test_cp_partial_split_floors_unbacked_valid_tail_to_page_boundary(self):
|
|
cache = self._minimal_cp_hiradix_cache(page_size=64)
|
|
node = TreeNode()
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
|
|
self.assertEqual(cache._cp_floor_backed_partial_split_len(node, 3), 0)
|
|
self.assertEqual(cache._cp_floor_backed_partial_split_len(node, 70), 64)
|
|
self.assertEqual(cache._cp_floor_backed_partial_split_len(node, 128), 128)
|
|
|
|
def test_session_aware_cache_forwards_cp_hicache_prepare(self):
|
|
calls = []
|
|
|
|
class Inner:
|
|
def prepare_write_backup_for_req(self, req):
|
|
calls.append(req)
|
|
|
|
wrapper = SessionAwareCache(Inner())
|
|
req = object()
|
|
|
|
wrapper.prepare_write_backup_for_req(req)
|
|
|
|
self.assertEqual(calls, [req])
|
|
|
|
def test_session_aware_cache_forwards_cp_hicache_batch_prepare(self):
|
|
calls = []
|
|
|
|
class Inner:
|
|
def prepare_write_backups_for_reqs(self, reqs):
|
|
calls.append(list(reqs))
|
|
|
|
wrapper = SessionAwareCache(Inner())
|
|
reqs = [object(), object()]
|
|
|
|
wrapper.prepare_write_backups_for_reqs(reqs)
|
|
|
|
self.assertEqual(calls, [reqs])
|
|
|
|
def test_node_backuped_uses_cp_metadata(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
node = TreeNode()
|
|
node.host_len = 8
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([1, 2], dtype=torch.int64),
|
|
host_indices=torch.tensor([10, 11], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(8, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
self.assertTrue(cache._node_backuped(node))
|
|
|
|
def test_node_backuped_rejects_missing_draft_metadata_when_draft_attached(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
has_draft_hicache=True,
|
|
cp_shared_kv_layout=types.SimpleNamespace(cp_rank=2),
|
|
)
|
|
node = TreeNode()
|
|
node.id = 123
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError, "node_id=123.*cp_rank=2.*missing draft_host_indices"
|
|
):
|
|
cache._node_backuped(node)
|
|
|
|
def test_node_backuped_rejects_cp_host_len_without_metadata(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=types.SimpleNamespace(cp_rank=1)
|
|
)
|
|
node = TreeNode()
|
|
node.id = 124
|
|
node.host_len = 64
|
|
node.cp_hicache = None
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError, "node_id=124.*host_len=64.*cp_rank=1.*without cp_hicache"
|
|
):
|
|
cache._node_backuped(node)
|
|
|
|
def test_node_backuped_accepts_empty_draft_metadata_for_zero_owned_rank(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=True)
|
|
node = TreeNode()
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
draft_host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
self.assertTrue(cache._node_backuped(node))
|
|
|
|
def test_node_backuped_excludes_inflight_cp_write_until_ack(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=True)
|
|
cache.ongoing_write_through = {}
|
|
node = TreeNode()
|
|
node.id = 125
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
draft_host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
cache.ongoing_write_through[node.id] = node
|
|
self.assertFalse(cache._node_backuped(node))
|
|
|
|
cache.ongoing_write_through.clear()
|
|
self.assertTrue(cache._node_backuped(node))
|
|
|
|
def test_node_backuped_excludes_explicit_pending_cp_backup_until_commit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
dec_locked = []
|
|
cache.dec_node_lock_ref = lambda node: dec_locked.append(node)
|
|
node = TreeNode()
|
|
node.id = 128
|
|
node.host_len = 64
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
node.cp_hicache = metadata
|
|
cache.pending_host_backups[node.id] = PendingHiCacheBackup(
|
|
node=node, metadata=metadata, logical_len=64
|
|
)
|
|
|
|
self.assertFalse(cache._node_backuped(node))
|
|
|
|
cache._commit_pending_backup(node.id)
|
|
|
|
self.assertTrue(cache._node_backuped(node))
|
|
self.assertEqual(dec_locked, [node])
|
|
|
|
def test_rollback_pending_cp_backup_frees_reserved_host_slots(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
evicted = []
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
evict_cp_host=lambda metadata: evicted.append(metadata) or 2,
|
|
cancel_layer_write_state=lambda node_id: False,
|
|
ack_write_queue=[],
|
|
)
|
|
dec_locked = []
|
|
cache.dec_node_lock_ref = lambda node: dec_locked.append(node)
|
|
cache.pending_host_backups = {}
|
|
node = TreeNode()
|
|
node.id = 129
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.tensor([0, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([10, 11], dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
cache.pending_host_backups[node.id] = PendingHiCacheBackup(
|
|
node=node, metadata=metadata, logical_len=64
|
|
)
|
|
|
|
cache._rollback_pending_backup(node.id)
|
|
|
|
self.assertEqual(evicted, [metadata])
|
|
self.assertEqual(dec_locked, [node])
|
|
self.assertNotIn(node.id, cache.pending_host_backups)
|
|
|
|
def test_single_node_write_lock_updates_device_evictable_leaf_set(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_leaves = set()
|
|
cache.evictable_size_ = 4
|
|
cache.protected_size_ = 0
|
|
|
|
node = TreeNode()
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([1, 2, 3, 4])
|
|
node.value = torch.arange(4, dtype=torch.int64)
|
|
cache.root_node.children[1] = node
|
|
cache.evictable_leaves.add(node)
|
|
|
|
cache.inc_node_lock_ref(node)
|
|
|
|
self.assertNotIn(node, cache.evictable_leaves)
|
|
self.assertEqual(cache.evictable_size(), 0)
|
|
self.assertEqual(cache.protected_size(), 4)
|
|
|
|
cache.dec_node_lock_ref(node)
|
|
|
|
self.assertIn(node, cache.evictable_leaves)
|
|
self.assertEqual(cache.evictable_size(), 4)
|
|
self.assertEqual(cache.protected_size(), 0)
|
|
|
|
def test_inc_hit_count_does_not_rewrite_cp_backed_node(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.write_through_threshold = 1
|
|
cache.cache_controller = type(
|
|
"Controller", (), {"write_policy": "write_through"}
|
|
)()
|
|
cache.write_backup = lambda node: (_ for _ in ()).throw(
|
|
AssertionError("must not rewrite")
|
|
)
|
|
node = TreeNode()
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([], dtype=torch.int64),
|
|
host_indices=torch.tensor([], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
cache._inc_hit_count(node)
|
|
|
|
self.assertEqual(node.hit_count, 1)
|
|
|
|
def test_inc_hit_count_does_not_duplicate_inflight_cp_write(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.write_through_threshold = 1
|
|
cache.ongoing_write_through = {}
|
|
cache.cache_controller = type(
|
|
"Controller", (), {"write_policy": "write_through"}
|
|
)()
|
|
cache.write_backup = lambda node: (_ for _ in ()).throw(
|
|
AssertionError("must not launch a second write")
|
|
)
|
|
node = TreeNode()
|
|
node.id = 127
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([], dtype=torch.int64),
|
|
host_indices=torch.tensor([], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.ongoing_write_through[node.id] = node
|
|
|
|
cache._inc_hit_count(node)
|
|
|
|
self.assertEqual(node.hit_count, 1)
|
|
|
|
def test_write_backup_uses_deterministic_host_eviction_before_reserve(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.page_size = 1
|
|
reservation_factory = lambda device_indices: make_write_reservation(
|
|
device_indices, node_id=122
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController([reservation_factory])
|
|
cache.cache_controller.cp_shared_kv_layout = FakeCpLayout(cp_size=1, cp_rank=0)
|
|
cache.token_to_kv_pool_host = types.SimpleNamespace(size=16)
|
|
cache.evictable_host_leaves = set()
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache._record_remove_event = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
|
|
root = TreeNode()
|
|
root.key = RadixKey(token_ids=[], extra_key=None)
|
|
root.value = []
|
|
cache.root_node = root
|
|
evictable_node = TreeNode()
|
|
evictable_node.parent = root
|
|
evictable_node.key = RadixKey(token_ids=[1], extra_key=None)
|
|
evictable_node.value = None
|
|
evictable_node.host_len = 4
|
|
evictable_node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.arange(4, dtype=torch.int64),
|
|
host_indices=torch.arange(55, 59, dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[1] = evictable_node
|
|
cache.evictable_host_leaves.add(evictable_node)
|
|
|
|
node = TreeNode()
|
|
node.id = 122
|
|
node.value = torch.arange(16, dtype=torch.int64)
|
|
|
|
cache.write_backup(node)
|
|
|
|
self.assertEqual(
|
|
cache.cache_controller.evicted_host_indices[0].tolist(),
|
|
list(range(55, 59)),
|
|
)
|
|
self.assertEqual(evictable_node.host_len, 0)
|
|
self.assertIsNone(evictable_node.cp_hicache)
|
|
self.assertNotIn(1, root.children)
|
|
self.assertEqual(node.host_len, 0)
|
|
self.assertIsNone(node.cp_hicache)
|
|
self.assertIn(node.id, cache.pending_host_backups)
|
|
self.assertEqual(len(cache.cache_controller.submitted), 1)
|
|
|
|
def test_write_backup_cp_post_forward_path_warns_fallback(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id
|
|
)
|
|
]
|
|
)
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
|
|
node = TreeNode()
|
|
node.id = 133
|
|
node.value = torch.arange(16, dtype=torch.int64)
|
|
|
|
with self.assertLogs(
|
|
"sglang.srt.mem_cache.hiradix_cache", level="WARNING"
|
|
) as captured:
|
|
cache.write_backup(node)
|
|
|
|
self.assertTrue(
|
|
any(
|
|
"[CP_HICACHE_FALLBACK][post_forward_catch_up_backup]" in message
|
|
for message in captured.output
|
|
)
|
|
)
|
|
|
|
def test_prepare_write_backup_for_req_chunked_reserves_completed_pages(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = FakeReserveWriteController([])
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.root_node.children = {}
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(16, dtype=torch.int64).view(1, 16)
|
|
)
|
|
cache._reserve_write_cp_indices_no_collective = (
|
|
lambda indices, node_id: make_write_reservation(
|
|
indices, node_id=node_id, host_start=300
|
|
)
|
|
)
|
|
|
|
req = types.SimpleNamespace(
|
|
rid="rid-chunked",
|
|
is_chunked=1,
|
|
fill_ids=list(range(10)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backup_for_req(req)
|
|
|
|
self.assertEqual(len(cache.cache_controller.submitted), 1)
|
|
self.assertEqual(
|
|
cache.cache_controller.submitted[0].physical_device_indices.tolist(),
|
|
list(range(8)),
|
|
)
|
|
self.assertEqual(req.cp_hicache_prepared_backup.logical_len, 8)
|
|
|
|
def test_cp_cache_unfinished_chunked_inserts_only_completed_pages(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
class Pool:
|
|
def __init__(self):
|
|
self.req_to_token = torch.arange(16, dtype=torch.int64).view(1, 16)
|
|
self.writes = []
|
|
|
|
def write(self, index, values):
|
|
self.writes.append((index, values.detach().cpu().tolist()))
|
|
|
|
pool = Pool()
|
|
cache.req_to_token_pool = pool
|
|
cache.token_to_kv_pool_allocator = RecordingTokenAllocator()
|
|
inserted = []
|
|
|
|
def insert(params):
|
|
inserted.append(params)
|
|
return types.SimpleNamespace(prefix_len=8, pending_backup_deferred_node=None)
|
|
|
|
cache.insert = insert
|
|
cache.match_prefix = lambda params: types.SimpleNamespace(
|
|
device_indices=torch.arange(8, dtype=torch.int64),
|
|
last_device_node=TreeNode(),
|
|
)
|
|
cache._free_kv_indices_range = lambda *args, **kwargs: None
|
|
cache.dec_lock_ref = lambda node: None
|
|
cache.inc_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
fill_ids=list(range(10)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=0,
|
|
last_node=TreeNode(),
|
|
prefix_indices=torch.empty((0,), dtype=torch.int64),
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.cache_unfinished_req(req, chunked=True)
|
|
|
|
self.assertEqual(len(inserted), 1)
|
|
self.assertEqual(inserted[0].key.token_ids, list(range(8)))
|
|
self.assertEqual(inserted[0].value.tolist(), list(range(8)))
|
|
self.assertEqual(
|
|
pool.writes,
|
|
[((0, slice(0, 8, None)), list(range(8)))],
|
|
)
|
|
self.assertEqual(req.cache_protected_len, 8)
|
|
self.assertEqual(req.prefix_indices.tolist(), list(range(10)))
|
|
|
|
def test_write_backup_deterministic_eviction_avoids_reserve_all_reduce(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.page_size = 1
|
|
cache.tp_world_size = 2
|
|
cache.tp_group = object()
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices: make_write_reservation(
|
|
device_indices, node_id=130, host_start=100
|
|
),
|
|
]
|
|
)
|
|
cache.cache_controller.cp_shared_kv_layout = FakeCpLayout(cp_size=1, cp_rank=0)
|
|
cache.token_to_kv_pool_host = types.SimpleNamespace(size=16)
|
|
evictions = []
|
|
cache._evict_host_for_physical_slots = lambda required, synchronize_across_ranks=False: (
|
|
evictions.append((required, synchronize_across_ranks)) or required
|
|
)
|
|
cache.evictable_host_leaves = set()
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache._record_remove_event = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
|
|
root = TreeNode()
|
|
root.key = RadixKey(token_ids=[], extra_key=None)
|
|
root.value = []
|
|
cache.root_node = root
|
|
evictable_node = TreeNode()
|
|
evictable_node.parent = root
|
|
evictable_node.key = RadixKey(token_ids=[2], extra_key=None)
|
|
evictable_node.value = None
|
|
evictable_node.host_len = 16
|
|
evictable_node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=16,
|
|
owned_positions=torch.arange(16, dtype=torch.int64),
|
|
host_indices=torch.arange(90, 106, dtype=torch.int64),
|
|
page_owners=torch.zeros(16, dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[2] = evictable_node
|
|
cache.evictable_host_leaves.add(evictable_node)
|
|
|
|
node = TreeNode()
|
|
node.id = 130
|
|
node.value = torch.arange(16, dtype=torch.int64)
|
|
|
|
with patch(
|
|
"torch.distributed.all_reduce",
|
|
side_effect=AssertionError("reserve admission must not all_reduce"),
|
|
):
|
|
backed_len = cache.write_backup(node)
|
|
|
|
self.assertEqual(backed_len, 16)
|
|
self.assertEqual(
|
|
cache.cache_controller.evicted_host_indices[0].tolist(),
|
|
list(range(90, 106)),
|
|
)
|
|
self.assertEqual(evictions, [])
|
|
self.assertEqual(len(cache.cache_controller.submitted), 1)
|
|
self.assertEqual(
|
|
cache.cache_controller.submitted[0].metadata.host_indices.tolist(),
|
|
list(range(100, 116)),
|
|
)
|
|
|
|
def test_insert_attaches_prepared_cp_backup_without_catchup_copy(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_size_ = 0
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda lhs, rhs: 0
|
|
cache.maybe_bigram_convert = lambda key, value: (key, value)
|
|
cache.is_eagle = False
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._record_store_event = lambda node: None
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
locked_nodes = []
|
|
cache.inc_node_lock_ref = lambda node: locked_nodes.append(node)
|
|
cache.cache_controller = types.SimpleNamespace(write_policy="write_through")
|
|
|
|
value = torch.arange(16, dtype=torch.int64)
|
|
reservation = make_write_reservation(value, node_id=131, host_start=110)
|
|
prepared = PreparedCpHiCacheBackup(
|
|
node_id=131,
|
|
reservation=reservation,
|
|
metadata=reservation.metadata,
|
|
logical_len=16,
|
|
)
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(list(range(16))),
|
|
value=value,
|
|
cp_hicache_prepared_backup=prepared,
|
|
)
|
|
)
|
|
|
|
node = cache.root_node.children[0]
|
|
self.assertEqual(result.prefix_len, 0)
|
|
self.assertEqual(node.id, 131)
|
|
self.assertTrue(prepared.attached)
|
|
self.assertIs(cache.ongoing_write_through[131], node)
|
|
self.assertIs(cache.pending_host_backups[131].node, node)
|
|
self.assertIs(cache.pending_host_backups[131].metadata, reservation.metadata)
|
|
self.assertEqual(cache.pending_host_backups[131].logical_len, 16)
|
|
self.assertEqual(locked_nodes, [node])
|
|
|
|
def test_prepare_write_backup_for_req_registers_before_forward_without_catchup(
|
|
self,
|
|
):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 1
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(16, dtype=torch.int64).view(1, 16)
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id, host_start=130
|
|
)
|
|
]
|
|
)
|
|
|
|
req = types.SimpleNamespace(
|
|
rid="rid-prepare",
|
|
fill_ids=list(range(16)),
|
|
cache_protected_len=0,
|
|
req_pool_idx=0,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backup_for_req(req)
|
|
|
|
prepared = req.cp_hicache_prepared_backup
|
|
self.assertIsNotNone(prepared)
|
|
self.assertEqual(prepared.logical_len, 16)
|
|
self.assertEqual(len(cache.cache_controller.submitted), 1)
|
|
self.assertIs(cache.cache_controller.submitted[0], prepared.reservation)
|
|
self.assertEqual(
|
|
cache.cache_controller.submit_kwargs,
|
|
[{"catch_up_all_layers": False}],
|
|
)
|
|
|
|
def test_prepare_write_backup_for_req_keeps_valid_tail_length(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id, host_start=170
|
|
)
|
|
]
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.children = {}
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
|
|
req = types.SimpleNamespace(
|
|
rid="rid-tail-prepare",
|
|
fill_ids=list(range(6)),
|
|
cache_protected_len=0,
|
|
req_pool_idx=0,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backup_for_req(req)
|
|
|
|
self.assertIsNotNone(req.cp_hicache_prepared_backup)
|
|
self.assertEqual(req.cp_hicache_prepared_backup.logical_len, 6)
|
|
self.assertEqual(cache.cache_controller.reservations[0][0].tolist(), list(range(6)))
|
|
|
|
def test_prepare_write_backup_for_req_floors_mid_page_prefix_hit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(16, dtype=torch.int64).view(1, 16)
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
lambda device_indices, node_id: make_write_reservation(
|
|
device_indices, node_id=node_id, host_start=180
|
|
)
|
|
]
|
|
)
|
|
cache._probe_existing_radix_prefix_len_no_split = lambda key: 6
|
|
|
|
req = types.SimpleNamespace(
|
|
rid="rid-tail-extend",
|
|
fill_ids=list(range(10)),
|
|
cache_protected_len=6,
|
|
req_pool_idx=0,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backup_for_req(req)
|
|
|
|
self.assertIsNotNone(req.cp_hicache_prepared_backup)
|
|
self.assertEqual(
|
|
cache.cache_controller.reservations[0][0].tolist(),
|
|
list(range(4, 10)),
|
|
)
|
|
|
|
def test_cache_finished_req_keeps_cp_valid_tail_insert_key(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable_finished_insert = False
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
)
|
|
freed = []
|
|
cache.token_to_kv_pool_allocator = types.SimpleNamespace(
|
|
free=lambda indices: freed.append(indices.clone())
|
|
)
|
|
inserted = []
|
|
cache.insert = lambda params: inserted.append(params) or types.SimpleNamespace(
|
|
prefix_len=0
|
|
)
|
|
cache.dec_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
origin_input_ids=list(range(6)),
|
|
output_ids=[],
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=0,
|
|
last_node=TreeNode(),
|
|
cp_hicache_prepared_backup=None,
|
|
pop_committed_kv_cache=lambda: 6,
|
|
)
|
|
|
|
cache.cache_finished_req(req)
|
|
|
|
self.assertEqual(len(inserted), 1)
|
|
self.assertEqual(inserted[0].key.token_ids, list(range(6)))
|
|
self.assertEqual(inserted[0].value.tolist(), list(range(6)))
|
|
self.assertEqual([indices.tolist() for indices in freed], [[], []])
|
|
|
|
def test_cache_finished_req_cp_insert_duplicate_free_skips_partial_page(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable_finished_insert = False
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
)
|
|
allocator = RecordingTokenAllocator()
|
|
cache.token_to_kv_pool_allocator = allocator
|
|
cache.insert = lambda params: types.SimpleNamespace(prefix_len=3)
|
|
cache.dec_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
origin_input_ids=list(range(6)),
|
|
output_ids=[],
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=TreeNode(),
|
|
cp_hicache_prepared_backup=None,
|
|
pop_committed_kv_cache=lambda: 6,
|
|
)
|
|
|
|
cache.cache_finished_req(req)
|
|
|
|
self.assertEqual(allocator.freed, [])
|
|
|
|
def test_cache_finished_req_cp_no_insert_frees_only_full_unprotected_pages(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable_finished_insert = False
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(12, dtype=torch.int64).view(1, 12)
|
|
)
|
|
allocator = RecordingTokenAllocator()
|
|
cache.token_to_kv_pool_allocator = allocator
|
|
cache.dec_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
origin_input_ids=list(range(9)),
|
|
output_ids=[],
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=TreeNode(),
|
|
cp_hicache_prepared_backup=None,
|
|
pop_committed_kv_cache=lambda: 9,
|
|
)
|
|
|
|
cache.cache_finished_req(req, is_insert=False)
|
|
|
|
self.assertEqual(allocator.freed, [[4, 5, 6, 7, 8]])
|
|
|
|
def test_cache_unfinished_req_cp_duplicate_free_skips_partial_page(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
writes = []
|
|
|
|
class Pool:
|
|
req_to_token = torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
|
|
def write(self, index, values):
|
|
writes.append((index, values.clone()))
|
|
|
|
cache.req_to_token_pool = Pool()
|
|
allocator = RecordingTokenAllocator()
|
|
cache.token_to_kv_pool_allocator = allocator
|
|
cache.insert = lambda params: types.SimpleNamespace(prefix_len=3)
|
|
new_last_node = TreeNode()
|
|
cache.match_prefix = lambda params: types.SimpleNamespace(
|
|
device_indices=torch.arange(6, dtype=torch.int64),
|
|
last_device_node=new_last_node,
|
|
)
|
|
cache.dec_lock_ref = lambda node: None
|
|
cache.inc_lock_ref = lambda node: None
|
|
|
|
req = types.SimpleNamespace(
|
|
fill_ids=list(range(6)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=TreeNode(),
|
|
prefix_indices=torch.empty((0,), dtype=torch.int64),
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.cache_unfinished_req(req)
|
|
|
|
self.assertEqual(allocator.freed, [])
|
|
self.assertEqual(req.cache_protected_len, 6)
|
|
|
|
def test_cache_unfinished_req_deferred_insert_preserves_request_kv(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
deferred_node = TreeNode()
|
|
|
|
class Pool:
|
|
req_to_token = torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
|
|
def write(self, index, values):
|
|
raise AssertionError("deferred insert must not rewrite req pool")
|
|
|
|
cache.req_to_token_pool = Pool()
|
|
cache.token_to_kv_pool_allocator = RecordingTokenAllocator()
|
|
cache.insert = lambda params: types.SimpleNamespace(
|
|
prefix_len=1,
|
|
pending_backup_deferred_node=deferred_node,
|
|
)
|
|
cache.match_prefix = lambda params: (_ for _ in ()).throw(
|
|
AssertionError("deferred insert must not rematch")
|
|
)
|
|
cache._free_kv_indices_range = lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AssertionError("deferred unfinished insert must keep request KV")
|
|
)
|
|
old_last_node = TreeNode()
|
|
req = types.SimpleNamespace(
|
|
fill_ids=list(range(6)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=old_last_node,
|
|
prefix_indices=torch.empty((0,), dtype=torch.int64),
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.cache_unfinished_req(req, chunked=True)
|
|
|
|
self.assertEqual(req.cache_protected_len, 1)
|
|
self.assertIs(req.last_node, old_last_node)
|
|
self.assertEqual(req.prefix_indices.tolist(), list(range(6)))
|
|
self.assertEqual(cache.token_to_kv_pool_allocator.freed, [])
|
|
|
|
def test_cache_unfinished_req_deferred_insert_preserves_unattached_backup_for_final_insert(
|
|
self,
|
|
):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
deferred_node = TreeNode()
|
|
rollbacks = []
|
|
|
|
class Pool:
|
|
req_to_token = torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
|
|
cache.req_to_token_pool = Pool()
|
|
cache.token_to_kv_pool_allocator = RecordingTokenAllocator()
|
|
cache.insert = lambda params: types.SimpleNamespace(
|
|
prefix_len=1,
|
|
pending_backup_deferred_node=deferred_node,
|
|
)
|
|
cache.match_prefix = lambda params: (_ for _ in ()).throw(
|
|
AssertionError("deferred insert must not rematch")
|
|
)
|
|
cache._free_kv_indices_range = lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AssertionError("deferred unfinished insert must keep request KV")
|
|
)
|
|
cache._rollback_prepared_cp_backup = lambda prepared, reason: rollbacks.append(
|
|
(prepared, reason)
|
|
)
|
|
prepared = types.SimpleNamespace(attached=False)
|
|
req = types.SimpleNamespace(
|
|
fill_ids=list(range(6)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=TreeNode(),
|
|
prefix_indices=torch.empty((0,), dtype=torch.int64),
|
|
cp_hicache_prepared_backup=prepared,
|
|
)
|
|
|
|
cache.cache_unfinished_req(req, chunked=False)
|
|
|
|
self.assertEqual(rollbacks, [])
|
|
self.assertIs(req.cp_hicache_prepared_backup, prepared)
|
|
self.assertEqual(req.prefix_indices.tolist(), list(range(6)))
|
|
|
|
def test_cache_unfinished_chunked_deferred_insert_rolls_back_unattached_backup(
|
|
self,
|
|
):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
deferred_node = TreeNode()
|
|
rollbacks = []
|
|
|
|
class Pool:
|
|
req_to_token = torch.arange(8, dtype=torch.int64).view(1, 8)
|
|
|
|
cache.req_to_token_pool = Pool()
|
|
cache.token_to_kv_pool_allocator = RecordingTokenAllocator()
|
|
cache.insert = lambda params: types.SimpleNamespace(
|
|
prefix_len=1,
|
|
pending_backup_deferred_node=deferred_node,
|
|
)
|
|
cache.match_prefix = lambda params: (_ for _ in ()).throw(
|
|
AssertionError("deferred insert must not rematch")
|
|
)
|
|
cache._free_kv_indices_range = lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AssertionError("deferred unfinished insert must keep request KV")
|
|
)
|
|
cache._rollback_prepared_cp_backup = lambda prepared, reason: rollbacks.append(
|
|
(prepared, reason)
|
|
)
|
|
prepared = types.SimpleNamespace(attached=False)
|
|
req = types.SimpleNamespace(
|
|
fill_ids=list(range(6)),
|
|
req_pool_idx=0,
|
|
extra_key=None,
|
|
cache_protected_len=1,
|
|
last_node=TreeNode(),
|
|
prefix_indices=torch.empty((0,), dtype=torch.int64),
|
|
cp_hicache_prepared_backup=prepared,
|
|
)
|
|
|
|
cache.cache_unfinished_req(req, chunked=True)
|
|
|
|
self.assertEqual(
|
|
rollbacks, [(prepared, "unfinished_pending_backup_split_deferred")]
|
|
)
|
|
self.assertIsNone(req.cp_hicache_prepared_backup)
|
|
self.assertEqual(req.prefix_indices.tolist(), list(range(6)))
|
|
|
|
def test_prepare_write_backup_for_req_skips_existing_insert_prefix(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.disable = False
|
|
cache._uses_cp_hicache = True
|
|
cache.is_eagle = False
|
|
cache.page_size = 1
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.root_node.children = {}
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
def key_match(lhs, rhs):
|
|
matched = 0
|
|
for left, right in zip(lhs.token_ids, rhs.token_ids):
|
|
if left != right:
|
|
break
|
|
matched += 1
|
|
return matched
|
|
|
|
cache.key_match_fn = key_match
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache.req_to_token_pool = types.SimpleNamespace(
|
|
req_to_token=torch.arange(16, dtype=torch.int64).view(1, 16)
|
|
)
|
|
cache.cache_controller = FakeReserveWriteController([])
|
|
|
|
existing = TreeNode()
|
|
existing.id = 201
|
|
existing.parent = cache.root_node
|
|
existing.key = RadixKey(list(range(16)))
|
|
existing.value = torch.arange(16, dtype=torch.int64)
|
|
cache.root_node.children[0] = existing
|
|
|
|
req = types.SimpleNamespace(
|
|
rid="rid-existing-prefix",
|
|
fill_ids=list(range(16)),
|
|
extra_key=None,
|
|
cache_protected_len=8,
|
|
req_pool_idx=0,
|
|
is_chunked=0,
|
|
cp_hicache_prepared_backup=None,
|
|
)
|
|
|
|
cache.prepare_write_backup_for_req(req)
|
|
|
|
self.assertIsNone(req.cp_hicache_prepared_backup)
|
|
self.assertEqual(cache.cache_controller.reservations, [])
|
|
self.assertEqual(cache.cache_controller.submitted, [])
|
|
|
|
def test_rollback_unattached_prepared_cp_backup_removes_orphan_ack(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
value = torch.arange(4, dtype=torch.int64)
|
|
reservation = make_write_reservation(value, node_id=132, host_start=120)
|
|
prepared = PreparedCpHiCacheBackup(
|
|
node_id=132,
|
|
reservation=reservation,
|
|
metadata=reservation.metadata,
|
|
logical_len=4,
|
|
)
|
|
evicted = []
|
|
|
|
class ReadyEvent:
|
|
def synchronize(self):
|
|
pass
|
|
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
pending_layer_writes={},
|
|
ack_write_queue=[HiCacheAck(ReadyEvent(), ReadyEvent(), [132])],
|
|
evict_cp_host=lambda metadata: evicted.append(metadata),
|
|
)
|
|
|
|
cache._rollback_prepared_cp_backup(prepared, "test")
|
|
|
|
self.assertEqual(cache.cache_controller.ack_write_queue, [])
|
|
self.assertEqual(evicted, [reservation.metadata])
|
|
|
|
def test_writing_check_defers_unattached_prepared_ack(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 1
|
|
cache.enable_storage = False
|
|
cache.pending_host_backups = {}
|
|
|
|
class ReadyEvent:
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
pass
|
|
|
|
attached_node = TreeNode()
|
|
attached_node.id = 53
|
|
dec_locked = []
|
|
cache.ongoing_write_through = {53: attached_node}
|
|
cache.dec_node_lock_ref = lambda node: dec_locked.append(node.id)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [54]),
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [53]),
|
|
],
|
|
)
|
|
|
|
cache.writing_check()
|
|
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in cache.cache_controller.ack_write_queue],
|
|
[[54], [53]],
|
|
)
|
|
self.assertEqual(cache.ongoing_write_through, {53: attached_node})
|
|
self.assertEqual(dec_locked, [])
|
|
|
|
@staticmethod
|
|
def _make_exploding_queue():
|
|
class _ExplodingQueue(list):
|
|
def __iter__(self):
|
|
raise AssertionError("fresh node ids must not scan the ack queue")
|
|
|
|
def __bool__(self):
|
|
return True
|
|
|
|
return _ExplodingQueue()
|
|
|
|
def test_reserve_write_cp_refuses_node_with_undrained_ack(self):
|
|
"""Producer-side invariant: at most one in-flight ack per node_id.
|
|
A reservation for a node whose previous final ack is still queued is
|
|
refused on the normal HiCacheWriteFailure path instead of creating a
|
|
second layer-write state (which would enqueue a duplicate ack)."""
|
|
from sglang.srt.managers.cache_controller import (
|
|
HiCacheController,
|
|
HiCacheWriteFailure,
|
|
)
|
|
|
|
controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(MagicMock(), MagicMock(), [4614]),
|
|
],
|
|
_max_write_ack_node_id=4614,
|
|
)
|
|
controller.node_has_undrained_write_ack = types.MethodType(
|
|
HiCacheController.node_has_undrained_write_ack, controller
|
|
)
|
|
result = HiCacheController.reserve_write_cp(
|
|
controller,
|
|
device_indices=torch.arange(4, dtype=torch.int64),
|
|
node_id=4614,
|
|
)
|
|
self.assertIsInstance(result, HiCacheWriteFailure)
|
|
self.assertEqual(result.required_host_slots, 0)
|
|
# An old (<= max) but absent node id is resolved by the queue scan.
|
|
self.assertFalse(
|
|
HiCacheController.node_has_undrained_write_ack(controller, 4613)
|
|
)
|
|
# Fresh ids (> max ever acked, i.e. every prepare-path reservation)
|
|
# take the O(1) fast path: no queue scan at all.
|
|
controller.ack_write_queue = self._make_exploding_queue()
|
|
self.assertFalse(
|
|
HiCacheController.node_has_undrained_write_ack(controller, 9999)
|
|
)
|
|
|
|
def test_reserve_wrapper_skips_capacity_retry_on_undrained_ack(self):
|
|
"""An undrained_ack refusal must return through
|
|
_reserve_write_cp_indices_no_collective as a skip-this-round failure:
|
|
entering the capacity retry would either trip the
|
|
predicted-no-deficit RuntimeError (free host) or trigger pointless
|
|
evictions (full host)."""
|
|
from sglang.srt.managers.cache_controller import HiCacheWriteFailure
|
|
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
refusal = HiCacheWriteFailure(required_host_slots=0, reason="undrained_ack")
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
reserve_write_cp=lambda device_indices, node_id: refusal,
|
|
)
|
|
|
|
def _admission(device_indices, node_id, phase):
|
|
if phase != "initial":
|
|
raise AssertionError(
|
|
f"undrained_ack must not reach admission phase {phase!r}"
|
|
)
|
|
return types.SimpleNamespace(
|
|
deficit_by_owner=[0],
|
|
eviction_plan=types.SimpleNamespace(remaining_deficit=[0]),
|
|
)
|
|
|
|
cache._cp_build_write_admission = _admission
|
|
cache._evict_cp_host_for_write_admission = lambda *a, **k: (_ for _ in ()).throw(
|
|
AssertionError("undrained_ack must not trigger host eviction")
|
|
)
|
|
|
|
result = cache._reserve_write_cp_indices_no_collective(
|
|
torch.arange(4, dtype=torch.int64), 4614
|
|
)
|
|
self.assertIs(result, refusal)
|
|
self.assertEqual(result.reason, "undrained_ack")
|
|
|
|
def test_rollback_pending_backup_scrubs_acks_and_cancels_state(self):
|
|
"""The write_backup exception rollback must not orphan an ack or a
|
|
half-submitted layer-write state: an orphaned ack re-opens the
|
|
registration guard while still queued, producing the duplicate-ack
|
|
crash; a live state would keep writing into evicted host slots."""
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
node = TreeNode()
|
|
node.id = 4614
|
|
metadata = object()
|
|
dec_locked = []
|
|
cache.pending_host_backups = {
|
|
4614: PendingHiCacheBackup(
|
|
node=node,
|
|
metadata=metadata,
|
|
logical_len=8,
|
|
submitted=True,
|
|
locked=True,
|
|
)
|
|
}
|
|
cache.dec_node_lock_ref = lambda n: dec_locked.append(n.id)
|
|
|
|
finish_event = MagicMock()
|
|
cancelled = []
|
|
evicted = []
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(MagicMock(), finish_event, [4614]),
|
|
HiCacheAck(MagicMock(), MagicMock(), [4615]),
|
|
],
|
|
cancel_layer_write_state=lambda node_id: cancelled.append(node_id),
|
|
evict_cp_host=lambda meta: evicted.append(meta),
|
|
)
|
|
node.cp_hicache = metadata
|
|
|
|
returned = cache._rollback_pending_backup(4614)
|
|
|
|
self.assertIs(returned, node)
|
|
self.assertEqual(cache.pending_host_backups, {})
|
|
self.assertEqual(cancelled, [4614])
|
|
self.assertEqual(evicted, [metadata])
|
|
# The 4614 ack is scrubbed (after syncing its finish event); the
|
|
# unrelated 4615 ack is retained.
|
|
finish_event.synchronize.assert_called_once()
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in cache.cache_controller.ack_write_queue],
|
|
[[4615]],
|
|
)
|
|
self.assertEqual(dec_locked, [4614])
|
|
self.assertIsNone(node.cp_hicache)
|
|
self.assertEqual(node.host_len, 0)
|
|
|
|
def test_remove_undrained_write_acks_splits_shared_ack(self):
|
|
"""A grouped ack covering several nodes only loses the rolled-back
|
|
node id; other nodes' completion is preserved."""
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(MagicMock(), MagicMock(), [4614, 4615]),
|
|
],
|
|
)
|
|
removed = cache._remove_undrained_write_acks(4614)
|
|
self.assertTrue(removed)
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in cache.cache_controller.ack_write_queue],
|
|
[[4615]],
|
|
)
|
|
self.assertFalse(cache._remove_undrained_write_acks(9999))
|
|
|
|
def test_writing_check_ignores_duplicate_ready_ack_for_same_node(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 1
|
|
cache.enable_storage = False
|
|
cache.pending_host_backups = {}
|
|
|
|
class ReadyEvent:
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
pass
|
|
|
|
node = TreeNode()
|
|
node.id = 4614
|
|
dec_locked = []
|
|
cache.ongoing_write_through = {4614: node}
|
|
cache.dec_node_lock_ref = lambda released_node: dec_locked.append(released_node.id)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
|
],
|
|
)
|
|
|
|
cache.writing_check()
|
|
|
|
self.assertEqual(cache.ongoing_write_through, {})
|
|
self.assertEqual(cache.cache_controller.ack_write_queue, [])
|
|
self.assertEqual(dec_locked, [4614])
|
|
|
|
def test_writing_check_drops_stale_duplicate_ack_before_valid_ack(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 1
|
|
cache.enable_storage = False
|
|
cache.pending_host_backups = {}
|
|
cache._completed_write_ack_ids = {4614}
|
|
|
|
class ReadyEvent:
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
pass
|
|
|
|
node = TreeNode()
|
|
node.id = 4615
|
|
dec_locked = []
|
|
cache.ongoing_write_through = {4615: node}
|
|
cache.dec_node_lock_ref = lambda released_node: dec_locked.append(released_node.id)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4614]),
|
|
HiCacheAck(ReadyEvent(), ReadyEvent(), [4615]),
|
|
],
|
|
)
|
|
|
|
cache.writing_check()
|
|
|
|
self.assertEqual(cache.ongoing_write_through, {})
|
|
self.assertEqual(cache.cache_controller.ack_write_queue, [])
|
|
self.assertEqual(dec_locked, [4615])
|
|
self.assertEqual(cache._completed_write_ack_ids, set())
|
|
|
|
def test_pending_split_probe_does_not_run_collective_writing_check(self):
|
|
"""Pending-split probing runs inside radix insert, where only a subset of
|
|
CP ranks may take this branch. It must not opportunistically drain write
|
|
acks via writing_check(), because writing_check can issue a TP collective
|
|
and race against the scheduler's inflight-poll collective on other ranks.
|
|
"""
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 2
|
|
cache.tp_group = object()
|
|
cache.enable_storage = False
|
|
cache.pending_host_backups = {}
|
|
node = TreeNode()
|
|
node.id = 10616
|
|
cache.ongoing_write_through = {10616: node}
|
|
cache.dec_node_lock_ref = lambda released_node: None
|
|
|
|
class ReadyEvent:
|
|
def query(self):
|
|
return True
|
|
|
|
def synchronize(self):
|
|
pass
|
|
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
ack_write_queue=[HiCacheAck(ReadyEvent(), ReadyEvent(), [10616])],
|
|
)
|
|
cache._cp_hicache_all_reduce = lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AssertionError("pending split probe must not issue a collective")
|
|
)
|
|
|
|
cache._cp_drain_write_acks_before_pending_split()
|
|
|
|
self.assertEqual(cache.ongoing_write_through, {10616: node})
|
|
self.assertEqual(
|
|
[ack.node_ids for ack in cache.cache_controller.ack_write_queue],
|
|
[[10616]],
|
|
)
|
|
|
|
def test_write_backup_cp_failfast_on_unplanned_reservation_failure(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[
|
|
HiCacheWriteFailure(required_host_slots=2),
|
|
HiCacheWriteFailure(required_host_slots=2),
|
|
]
|
|
)
|
|
cache.evictable_host_leaves = set()
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache._record_remove_event = lambda node: None
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
|
|
node = TreeNode()
|
|
node.id = 124
|
|
node.value = torch.arange(16, dtype=torch.int64)
|
|
|
|
with self.assertRaisesRegex(
|
|
RuntimeError,
|
|
"owner-lane admission predicted no host deficit",
|
|
):
|
|
cache.write_backup(node)
|
|
|
|
self.assertEqual(node.host_len, 0)
|
|
self.assertIsNone(node.cp_hicache)
|
|
self.assertEqual(cache.pending_host_backups, {})
|
|
self.assertEqual(cache.cache_controller.submitted, [])
|
|
|
|
def test_write_backup_cp_success_returns_logical_length_for_zero_owned_rank(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = FakeReserveWriteController(
|
|
[lambda device_indices: HiCacheWriteReservation(
|
|
metadata=CpHiCacheNodeMetadata(
|
|
logical_len=len(device_indices),
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(max(len(device_indices), 0), dtype=torch.int8),
|
|
page_size=1,
|
|
),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
physical_device_indices=torch.empty((0,), dtype=torch.int64),
|
|
node_id=123,
|
|
)]
|
|
)
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
node = TreeNode()
|
|
node.id = 123
|
|
node.value = torch.arange(16, dtype=torch.int64)
|
|
|
|
backed_len = cache.write_backup(node)
|
|
|
|
self.assertEqual(backed_len, 16)
|
|
self.assertEqual(node.host_len, 0)
|
|
self.assertIsNone(node.cp_hicache)
|
|
self.assertIn(node.id, cache.pending_host_backups)
|
|
self.assertEqual(len(cache.cache_controller.submitted), 1)
|
|
|
|
def test_attach_storage_backend_rejects_cp_hicache_without_controller_call(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"attach_storage_backend": lambda *args, **kwargs: (_ for _ in ()).throw(
|
|
AssertionError("controller attach must not be called")
|
|
)
|
|
},
|
|
)()
|
|
|
|
ok, message = cache.attach_storage_backend("mooncake")
|
|
|
|
self.assertFalse(ok)
|
|
self.assertIn("CP shared KV", message)
|
|
self.assertIn("storage backend", message)
|
|
|
|
def test_evict_demotes_cp_backed_node_without_deleting_radix_child(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = FakeEvictDeviceController()
|
|
cache.evictable_leaves = set()
|
|
cache.evictable_host_leaves = set()
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache.token_to_kv_pool_allocator = FakeTokenAllocator()
|
|
cache.evictable_size_ = 4
|
|
cache.protected_size_ = 0
|
|
cache._record_remove_event = lambda node: (_ for _ in ()).throw(
|
|
AssertionError("must not delete backed radix child")
|
|
)
|
|
|
|
root = TreeNode()
|
|
root.key = RadixKey(token_ids=[], extra_key=None)
|
|
root.value = []
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.parent = root
|
|
node.key = RadixKey(token_ids=[1, 2, 3, 4], extra_key=None)
|
|
node.value = torch.arange(4, dtype=torch.int64)
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([0], dtype=torch.int64),
|
|
host_indices=torch.tensor([55], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[1] = node
|
|
cache.evictable_leaves.add(node)
|
|
|
|
cache.evict(EvictParams(num_tokens=4))
|
|
|
|
self.assertIn(1, root.children)
|
|
self.assertIsNone(node.value)
|
|
self.assertIsNotNone(node.cp_hicache)
|
|
self.assertEqual(node.cp_hicache.host_indices.tolist(), [55])
|
|
|
|
def test_cp_owner_token_counts_use_page_owners_and_page_size(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=4, cp_rank=0)
|
|
)
|
|
|
|
counts = cache._cp_owner_token_counts(
|
|
torch.tensor([0, 1, 1, 2], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
self.assertEqual(counts, (64, 128, 64, 0))
|
|
|
|
def test_cp_metadata_count_asserts_local_owner_lane(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=3, cp_rank=1),
|
|
has_draft_hicache=True,
|
|
)
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=192,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(100, 164, dtype=torch.int64),
|
|
draft_host_indices=torch.arange(200, 264, dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1, 2], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
counts = cache._cp_assert_metadata_counts(metadata, context="unit")
|
|
|
|
self.assertEqual(counts, (64, 64, 64))
|
|
|
|
def test_cp_metadata_count_asserts_mismatched_local_slots(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=2, cp_rank=1),
|
|
has_draft_hicache=False,
|
|
)
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=128,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
with self.assertRaisesRegex(RuntimeError, "owner lane mismatch"):
|
|
cache._cp_assert_metadata_counts(metadata, context="unit")
|
|
|
|
def test_cp_host_capacity_snapshot_counts_committed_and_pending_draft(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=2, cp_rank=0),
|
|
has_draft_hicache=True,
|
|
draft_mem_pool_host=types.SimpleNamespace(size=512),
|
|
)
|
|
cache.token_to_kv_pool_host = types.SimpleNamespace(size=512)
|
|
cache.pending_host_backups = {}
|
|
root = TreeNode()
|
|
root.children = {}
|
|
cache.root_node = root
|
|
|
|
committed = TreeNode()
|
|
committed.id = 501
|
|
committed.parent = root
|
|
committed.key = RadixKey([1])
|
|
committed.host_len = 128
|
|
committed.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=128,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(64, dtype=torch.int64),
|
|
draft_host_indices=torch.arange(100, 164, dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
root.children[1] = committed
|
|
|
|
pending = TreeNode()
|
|
pending.id = 502
|
|
pending_metadata = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(200, 264, dtype=torch.int64),
|
|
draft_host_indices=torch.arange(300, 364, dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
cache.pending_host_backups[pending.id] = PendingHiCacheBackup(
|
|
node=pending,
|
|
metadata=pending_metadata,
|
|
logical_len=64,
|
|
)
|
|
|
|
snapshot = cache._cp_host_capacity_snapshot()
|
|
|
|
self.assertEqual(snapshot.target_capacity, (512, 512))
|
|
self.assertEqual(snapshot.draft_capacity, (512, 512))
|
|
self.assertEqual(snapshot.committed_target, (64, 64))
|
|
self.assertEqual(snapshot.committed_draft, (64, 64))
|
|
self.assertEqual(snapshot.pending_target, (64, 0))
|
|
self.assertEqual(snapshot.pending_draft, (64, 0))
|
|
|
|
def test_cp_write_admission_uses_padded_owner_vector_for_valid_tail(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.page_size = 64
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=8, cp_rank=0, page_size=64),
|
|
has_draft_hicache=True,
|
|
draft_mem_pool_host=types.SimpleNamespace(size=1024),
|
|
)
|
|
cache.token_to_kv_pool_host = types.SimpleNamespace(size=1024)
|
|
cache.pending_host_backups = {}
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.children = {}
|
|
cache.evictable_host_leaves = set()
|
|
|
|
admission = cache._cp_build_write_admission(
|
|
torch.arange(64, 164, dtype=torch.int64),
|
|
node_id=503,
|
|
phase="unit",
|
|
)
|
|
|
|
self.assertEqual(admission.required_by_owner, (64, 64, 0, 0, 0, 0, 0, 0))
|
|
self.assertEqual(
|
|
admission.target_available_by_owner,
|
|
(1024, 1024, 1024, 1024, 1024, 1024, 1024, 1024),
|
|
)
|
|
self.assertEqual(
|
|
admission.target_available_by_owner, admission.draft_available_by_owner
|
|
)
|
|
self.assertEqual(admission.deficit_by_owner, (0, 0, 0, 0, 0, 0, 0, 0))
|
|
|
|
def test_cp_capacity_snapshot_counts_padded_valid_tail_for_target_and_draft(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=2, cp_rank=0, page_size=64),
|
|
has_draft_hicache=True,
|
|
draft_mem_pool_host=types.SimpleNamespace(size=1024),
|
|
)
|
|
cache.token_to_kv_pool_host = types.SimpleNamespace(size=1024)
|
|
cache.pending_host_backups = {}
|
|
root = TreeNode()
|
|
root.children = {}
|
|
cache.root_node = root
|
|
|
|
committed = TreeNode()
|
|
committed.id = 504
|
|
committed.parent = root
|
|
committed.key = RadixKey(list(range(100)))
|
|
committed.host_len = 100
|
|
committed.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=100,
|
|
padded_len=128,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(100, 164, dtype=torch.int64),
|
|
draft_host_indices=torch.arange(200, 264, dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
root.children[0] = committed
|
|
|
|
snapshot = cache._cp_host_capacity_snapshot()
|
|
|
|
self.assertEqual(snapshot.committed_target, (64, 64))
|
|
self.assertEqual(snapshot.committed_draft, (64, 64))
|
|
|
|
|
|
class TestHiRadixCacheCPSplitEvict(CustomTestCase):
|
|
def test_split_node_splits_cp_metadata_by_owned_positions(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.page_size = 1
|
|
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
child = TreeNode()
|
|
child.parent = root
|
|
child.key = RadixKey(list(range(10)))
|
|
child.value = None
|
|
child.hash_value = None
|
|
child.host_len = 10
|
|
child.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=10,
|
|
owned_positions=torch.tensor([0, 2, 5, 9], dtype=torch.int64),
|
|
host_indices=torch.tensor([20, 21, 22, 23], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(10, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[0] = child
|
|
|
|
new_node = cache._split_node(child.key, child, 5)
|
|
|
|
self.assertEqual(new_node.host_len, 5)
|
|
self.assertEqual(child.host_len, 5)
|
|
self.assertEqual(new_node.cp_hicache.owned_positions.tolist(), [0, 2])
|
|
self.assertEqual(new_node.cp_hicache.host_indices.tolist(), [20, 21])
|
|
self.assertEqual(child.cp_hicache.owned_positions.tolist(), [0, 4])
|
|
self.assertEqual(child.cp_hicache.host_indices.tolist(), [22, 23])
|
|
|
|
def test_cp_host_leaf_status_skips_device_valid_host_backed_node(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.evictable_host_leaves = set()
|
|
node = TreeNode()
|
|
node.value = torch.arange(4, dtype=torch.int64)
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([0], dtype=torch.int64),
|
|
host_indices=torch.tensor([55], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.evictable_host_leaves.add(node)
|
|
|
|
cache._update_host_leaf_status(node)
|
|
|
|
self.assertNotIn(node, cache.evictable_host_leaves)
|
|
|
|
def test_cp_host_eviction_uses_physical_freed_slots_for_progress(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.eviction_strategy = type(
|
|
"Strategy", (), {"get_priority": lambda self, node: 0}
|
|
)()
|
|
cache._clear_pin = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._node_host_evict_indices = lambda node: torch.tensor(
|
|
[99], dtype=torch.int64
|
|
)
|
|
freed = []
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"evict_host": lambda self, indices: freed.append(indices.clone())
|
|
or len(indices)
|
|
},
|
|
)()
|
|
node = TreeNode()
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([1, 2, 3, 4])
|
|
node.value = None
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([1], dtype=torch.int64),
|
|
host_indices=torch.tensor([70], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.root_node.children[1] = node
|
|
cache.evictable_host_leaves.add(node)
|
|
|
|
physical_freed = cache._evict_host_for_physical_slots(1)
|
|
|
|
self.assertEqual(physical_freed, 1)
|
|
self.assertEqual(freed[0].tolist(), [99])
|
|
self.assertEqual(node.host_len, 0)
|
|
self.assertIsNone(node.cp_hicache)
|
|
|
|
def test_cp_host_eviction_unlinks_stale_leaf_to_free_parent_slots(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.eviction_strategy = type(
|
|
"Strategy", (), {"get_priority": lambda self, node: 0}
|
|
)()
|
|
cache._clear_pin = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
freed = []
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"evict_host": lambda self, indices: freed.append(indices.clone())
|
|
or len(indices)
|
|
},
|
|
)()
|
|
|
|
parent = TreeNode()
|
|
parent.parent = cache.root_node
|
|
parent.key = RadixKey([1])
|
|
parent.value = None
|
|
parent.host_len = 4
|
|
parent.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([2], dtype=torch.int64),
|
|
host_indices=torch.tensor([80], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.root_node.children[1] = parent
|
|
|
|
stale_child = TreeNode()
|
|
stale_child.parent = parent
|
|
stale_child.key = RadixKey([2])
|
|
stale_child.value = None
|
|
stale_child.host_len = 0
|
|
stale_child.cp_hicache = None
|
|
parent.children[2] = stale_child
|
|
cache.evictable_host_leaves.add(stale_child)
|
|
|
|
physical_freed = cache._evict_host_for_physical_slots(1)
|
|
|
|
self.assertEqual(physical_freed, 1)
|
|
self.assertEqual(freed[0].tolist(), [80])
|
|
self.assertNotIn(2, parent.children)
|
|
self.assertEqual(parent.host_len, 0)
|
|
self.assertIsNone(parent.cp_hicache)
|
|
self.assertNotIn(1, cache.root_node.children)
|
|
|
|
def test_cp_host_eviction_preserves_parent_with_sibling_after_stale_cleanup(
|
|
self,
|
|
):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.eviction_strategy = type(
|
|
"Strategy", (), {"get_priority": lambda self, node: 0}
|
|
)()
|
|
cache._clear_pin = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
freed = []
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"evict_host": lambda self, indices: freed.append(indices.clone())
|
|
or len(indices)
|
|
},
|
|
)()
|
|
|
|
parent = TreeNode()
|
|
parent.parent = cache.root_node
|
|
parent.key = RadixKey([1])
|
|
parent.value = None
|
|
parent.host_len = 4
|
|
parent.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.tensor([2], dtype=torch.int64),
|
|
host_indices=torch.tensor([80], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.root_node.children[1] = parent
|
|
|
|
stale_child = TreeNode()
|
|
stale_child.parent = parent
|
|
stale_child.key = RadixKey([2])
|
|
stale_child.value = None
|
|
stale_child.host_len = 0
|
|
stale_child.cp_hicache = None
|
|
parent.children[2] = stale_child
|
|
|
|
sibling = TreeNode()
|
|
sibling.parent = parent
|
|
sibling.key = RadixKey([3])
|
|
sibling.value = None
|
|
sibling.host_len = 0
|
|
sibling.cp_hicache = None
|
|
parent.children[3] = sibling
|
|
cache.evictable_host_leaves.add(stale_child)
|
|
|
|
physical_freed = cache._evict_host_for_physical_slots(1)
|
|
|
|
self.assertEqual(physical_freed, 0)
|
|
self.assertEqual(freed, [])
|
|
self.assertNotIn(2, parent.children)
|
|
self.assertIn(1, cache.root_node.children)
|
|
self.assertIs(cache.root_node.children[1], parent)
|
|
self.assertIn(3, parent.children)
|
|
self.assertIs(parent.children[3], sibling)
|
|
self.assertEqual(parent.host_len, 4)
|
|
self.assertIsNotNone(parent.cp_hicache)
|
|
self.assertEqual(parent.cp_hicache.host_indices.tolist(), [80])
|
|
|
|
def test_synchronized_cp_host_eviction_removes_zero_owned_logical_leaf(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 2
|
|
cache.tp_group = object()
|
|
cache._tp_group_rank = 0
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.eviction_strategy = type(
|
|
"Strategy", (), {"get_priority": lambda self, node: 0}
|
|
)()
|
|
cache._clear_pin = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
freed = []
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"evict_host": lambda self, indices: freed.append(indices.clone())
|
|
or len(indices)
|
|
},
|
|
)()
|
|
node = TreeNode()
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([1, 2, 3, 4])
|
|
node.value = None
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.root_node.children[1] = node
|
|
cache.evictable_host_leaves.add(node)
|
|
|
|
all_done_states = iter([0, 1])
|
|
|
|
def fake_all_reduce(done, op=None, group=None):
|
|
done.fill_(next(all_done_states, 1))
|
|
|
|
with patch("torch.distributed.all_reduce", side_effect=fake_all_reduce):
|
|
physical_freed = cache._evict_host_for_physical_slots(
|
|
0, synchronize_across_ranks=True
|
|
)
|
|
|
|
self.assertEqual(physical_freed, 0)
|
|
self.assertEqual(freed, [])
|
|
self.assertNotIn(1, cache.root_node.children)
|
|
self.assertEqual(node.host_len, 0)
|
|
self.assertIsNone(node.cp_hicache)
|
|
|
|
def test_cp_host_eviction_skips_all_reduce_without_tp_group(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.tp_world_size = 2
|
|
cache.tp_group = None
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.key = RadixKey([])
|
|
cache.evictable_host_leaves = set()
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.eviction_strategy = type(
|
|
"Strategy", (), {"get_priority": lambda self, node: 0}
|
|
)()
|
|
cache._clear_pin = lambda node: None
|
|
cache._record_remove_event = lambda node: None
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"evict_host": lambda self, indices: (_ for _ in ()).throw(
|
|
AssertionError("must not evict host slots")
|
|
)
|
|
},
|
|
)()
|
|
node = TreeNode()
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([1, 2, 3, 4])
|
|
node.value = None
|
|
node.host_len = 4
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=4,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(max(4, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.root_node.children[1] = node
|
|
cache.evictable_host_leaves.add(node)
|
|
|
|
with patch(
|
|
"torch.distributed.all_reduce",
|
|
side_effect=AssertionError("must not all_reduce without tp_group"),
|
|
):
|
|
physical_freed = cache._evict_host_for_physical_slots(
|
|
0, synchronize_across_ranks=True
|
|
)
|
|
|
|
self.assertEqual(physical_freed, 0)
|
|
self.assertIn(1, cache.root_node.children)
|
|
self.assertEqual(node.host_len, 4)
|
|
self.assertIsNotNone(node.cp_hicache)
|
|
|
|
def test_cp_host_eviction_plan_is_stable_across_leaf_insertion_order(self):
|
|
def make_cache(order):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=2, cp_rank=0),
|
|
has_draft_hicache=False,
|
|
)
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache.pending_host_backups = {}
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.children = {}
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache._clear_pin = lambda node: None
|
|
nodes = []
|
|
for node_id in (42, 41):
|
|
node = TreeNode(id=node_id)
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([node_id])
|
|
node.value = None
|
|
node.lock_ref = 0
|
|
node.host_ref_counter = 0
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(node_id * 100, node_id * 100 + 64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
cache.root_node.children[node_id] = node
|
|
nodes.append(node)
|
|
cache.evictable_host_leaves = {nodes[i] for i in order}
|
|
return cache
|
|
|
|
first = make_cache([0, 1])._plan_cp_host_evictions((64, 0))
|
|
second = make_cache([1, 0])._plan_cp_host_evictions((64, 0))
|
|
|
|
self.assertEqual([node.id for node in first.victims], [41])
|
|
self.assertEqual([node.id for node in second.victims], [41])
|
|
self.assertEqual(first.planned_freed, (64, 0))
|
|
self.assertEqual(first.remaining_deficit, (0, 0))
|
|
|
|
def test_cp_host_eviction_plan_skips_pending_backup_nodes(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
cp_shared_kv_layout=FakeCpLayout(cp_size=1, cp_rank=0),
|
|
has_draft_hicache=False,
|
|
)
|
|
cache.eviction_strategy = FakeEvictionStrategy()
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.children = {}
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.pending_host_backups = {}
|
|
cache.evictable_host_leaves = set()
|
|
|
|
for node_id in (10, 11):
|
|
node = TreeNode(id=node_id)
|
|
node.parent = cache.root_node
|
|
node.key = RadixKey([node_id])
|
|
node.value = None
|
|
node.lock_ref = 0
|
|
node.host_ref_counter = 0
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(node_id * 100, node_id * 100 + 64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
cache.root_node.children[node_id] = node
|
|
cache.evictable_host_leaves.add(node)
|
|
if node_id == 10:
|
|
cache.pending_host_backups[node.id] = PendingHiCacheBackup(
|
|
node=node,
|
|
metadata=node.cp_hicache,
|
|
logical_len=64,
|
|
)
|
|
|
|
plan = cache._plan_cp_host_evictions((64,))
|
|
|
|
self.assertEqual([node.id for node in plan.victims], [11])
|
|
self.assertEqual(plan.planned_freed, (64,))
|
|
|
|
|
|
class TestHiRadixCacheCPLoadBack(CustomTestCase):
|
|
def test_cp_load_back_plan_reports_l1_free_room_without_blocking_exact_fit(self):
|
|
class FreeRoomAllocator:
|
|
def compute_owner_lane_stats(self, _page_owners):
|
|
return [1, 0], [0, 8], [1, 0]
|
|
|
|
def compute_owner_lane_capacity_pages(self):
|
|
return [8, 8]
|
|
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.page_size = 64
|
|
cache.hicache_l1_free_room_ratio = 0.5
|
|
cache.hicache_l1_free_room_trigger_ratio = 0.25
|
|
cache.token_to_kv_pool_allocator = FreeRoomAllocator()
|
|
|
|
node = TreeNode(id=701)
|
|
node.host_len = 64
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=64,
|
|
padded_len=64,
|
|
owned_positions=torch.arange(64, dtype=torch.int64),
|
|
host_indices=torch.arange(64, dtype=torch.int64),
|
|
page_owners=torch.tensor([0], dtype=torch.int8),
|
|
page_size=64,
|
|
)
|
|
|
|
plan = cache._build_cp_load_back_plan([node], node_id=701)
|
|
|
|
self.assertEqual(plan.required_by_owner, [1, 0])
|
|
self.assertEqual(plan.available_by_owner, [0, 8])
|
|
# Synchronous load-back admission must only block on exact capacity.
|
|
self.assertEqual(plan.deficit_by_owner, [1, 0])
|
|
# The free-room target is still reported for observability/proactive policy.
|
|
# required=1 page, available=0, target_room=ceil(8*0.5)=4 pages.
|
|
self.assertEqual(plan.free_room_deficit_by_owner, [4, 0])
|
|
|
|
def test_cp_load_back_uses_host_len_not_host_value(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.device = "cpu"
|
|
cache.page_size = 1
|
|
cache.token_to_kv_pool_allocator = FakeTokenAllocator()
|
|
cache.load_back_threshold = 1
|
|
cache.evictable_size_ = 0
|
|
cache.metrics_collector = None
|
|
cache.ongoing_load_back = {}
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"load_cp": lambda self, nodes, node_id=-1: torch.arange(
|
|
32, 40, dtype=torch.int64
|
|
)
|
|
},
|
|
)()
|
|
cache.inc_lock_ref = lambda node: type("Result", (), {"delta": 0})()
|
|
cache.dec_lock_ref = lambda node: None
|
|
cache.evict = lambda params: None
|
|
parent = cache.root_node
|
|
parent.key = RadixKey([])
|
|
parent.value = torch.empty((0,), dtype=torch.int64)
|
|
node = TreeNode()
|
|
node.parent = parent
|
|
node.key = RadixKey(list(range(8)))
|
|
node.value = None
|
|
node.host_value = None
|
|
node.host_len = 8
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([0, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(8, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
loaded = cache.load_back(node)
|
|
|
|
self.assertEqual(loaded.tolist(), list(range(32, 40)))
|
|
self.assertEqual(node.value.tolist(), list(range(32, 40)))
|
|
|
|
def test_cp_load_back_threshold_uses_logical_length(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.root_node = TreeNode()
|
|
cache.root_node.value = torch.empty((0,), dtype=torch.int64)
|
|
cache.page_size = 1
|
|
cache.token_to_kv_pool_allocator = FakeTokenAllocator()
|
|
cache.load_back_threshold = 5
|
|
cache.evictable_size_ = 0
|
|
cache.metrics_collector = None
|
|
cache.ongoing_load_back = {}
|
|
cache.inc_lock_ref = lambda node: type("Result", (), {"delta": 0})()
|
|
cache.dec_lock_ref = lambda node: None
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"load_cp": lambda self, nodes, node_id=-1: torch.arange(
|
|
10, 16, dtype=torch.int64
|
|
)
|
|
},
|
|
)()
|
|
node = TreeNode()
|
|
node.parent = cache.root_node
|
|
node.value = None
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
owned_positions=torch.empty((0,), dtype=torch.int64),
|
|
host_indices=torch.empty((0,), dtype=torch.int64),
|
|
page_owners=torch.zeros(max(6, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
|
|
loaded = cache.load_back(node)
|
|
|
|
self.assertEqual(loaded.tolist(), [10, 11, 12, 13, 14, 15])
|
|
|
|
def test_cp_match_prefix_counts_logical_host_hit_without_host_value(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 1
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda child_key, key: sum(
|
|
1 for lhs, rhs in zip(child_key.token_ids, key.token_ids) if lhs == rhs
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(8)))
|
|
node.value = None
|
|
node.host_value = None
|
|
node.host_len = 8
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([0, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(8, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[0] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(8)))))
|
|
|
|
self.assertEqual(result.host_hit_length, 8)
|
|
self.assertIs(result.last_host_node, node)
|
|
self.assertIs(result.last_device_node, root)
|
|
|
|
def test_cp_match_prefix_device_valid_host_missing_remains_device_hit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 1
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda child_key, key: sum(
|
|
1 for lhs, rhs in zip(child_key.token_ids, key.token_ids) if lhs == rhs
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(8)))
|
|
node.value = torch.arange(8, dtype=torch.int64)
|
|
node.host_value = None
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[0] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(8)))))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), list(range(8)))
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertIs(result.last_device_node, node)
|
|
self.assertIs(result.last_host_node, root)
|
|
|
|
def test_cp_match_prefix_does_not_admit_inflight_write_as_host_hit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 1
|
|
cache.ongoing_write_through = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=True)
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda child_key, key: sum(
|
|
1 for lhs, rhs in zip(child_key.token_ids, key.token_ids) if lhs == rhs
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 126
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(8)))
|
|
node.value = torch.arange(8, dtype=torch.int64)
|
|
node.host_value = None
|
|
node.host_len = 8
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.tensor([0, 1], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51], dtype=torch.int64),
|
|
draft_host_indices=torch.tensor([150, 151], dtype=torch.int64),
|
|
page_owners=torch.zeros(max(8, 0), dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
root.children[0] = node
|
|
cache.ongoing_write_through[node.id] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(8)))))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), list(range(8)))
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertIs(result.last_device_node, node)
|
|
self.assertIs(result.last_host_node, root)
|
|
|
|
def test_cp_match_prefix_defers_split_when_child_backup_pending(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 1
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda child_key, key: sum(
|
|
1 for lhs, rhs in zip(child_key.token_ids, key.token_ids) if lhs == rhs
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
cache.root_node = root
|
|
child = TreeNode()
|
|
child.id = 130
|
|
child.parent = root
|
|
child.key = RadixKey(list(range(8)))
|
|
child.value = torch.arange(8, dtype=torch.int64)
|
|
child.host_len = 0
|
|
metadata = CpHiCacheNodeMetadata(
|
|
logical_len=8,
|
|
owned_positions=torch.arange(8, dtype=torch.int64),
|
|
host_indices=torch.arange(50, 58, dtype=torch.int64),
|
|
page_owners=torch.zeros(8, dtype=torch.int8),
|
|
page_size=1,
|
|
)
|
|
cache.pending_host_backups[child.id] = PendingHiCacheBackup(
|
|
node=child, metadata=metadata, logical_len=8
|
|
)
|
|
root.children[0] = child
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey([0, 1, 2, 9])))
|
|
|
|
self.assertIs(result.pending_backup_deferred_node, child)
|
|
self.assertIs(root.children[0], child)
|
|
self.assertEqual(child.key.token_ids, list(range(8)))
|
|
self.assertEqual(result.device_indices.tolist(), [])
|
|
self.assertIs(result.last_device_node, root)
|
|
|
|
def test_cp_match_prefix_shorter_than_page_returns_empty_root_match(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.root_node = TreeNode()
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey([1, 2, 3])))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [])
|
|
self.assertIs(result.last_device_node, cache.root_node)
|
|
self.assertIs(result.last_host_node, cache.root_node)
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
|
|
def test_cp_match_prefix_reports_valid_tail_host_hit(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 140
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = None
|
|
node.host_value = None
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(6)))))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [])
|
|
# CP HiCache exposes page-granular host hits. A non-page exact tail is
|
|
# floored and the tail is recomputed by the incoming request.
|
|
self.assertEqual(result.host_hit_length, 4)
|
|
self.assertIs(result.last_device_node, root)
|
|
self.assertEqual(result.last_host_node.key.token_ids, [0, 1, 2, 3])
|
|
self.assertIsNot(result.last_host_node, node)
|
|
|
|
def test_cp_backed_tail_split_floors_to_page_boundary(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 141
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = None
|
|
node.host_value = None
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey([0, 1, 2, 3, 4])))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [])
|
|
self.assertEqual(result.host_hit_length, 4)
|
|
self.assertIs(result.last_device_node, root)
|
|
self.assertEqual(result.last_host_node.key.token_ids, [0, 1, 2, 3])
|
|
self.assertEqual(result.last_host_node.host_len, 4)
|
|
self.assertEqual(result.last_host_node.cp_hicache.logical_len, 4)
|
|
self.assertEqual(result.last_host_node.cp_hicache.padded_len, 4)
|
|
self.assertNotIn((4, 5), result.last_host_node.children)
|
|
|
|
def test_cp_backed_tail_split_before_first_page_returns_root_match(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 142
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = None
|
|
node.host_value = None
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey([0, 1, 2])))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [])
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertIs(result.last_device_node, root)
|
|
self.assertIs(result.last_host_node, root)
|
|
self.assertIs(root.children[(0, 1, 2, 3)], node)
|
|
self.assertEqual(node.key.token_ids, list(range(6)))
|
|
|
|
def test_cp_prepare_probe_floors_backed_tail_partial_hit_to_page(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 143
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = None
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
prefix_len = cache._probe_existing_radix_prefix_len_no_split(
|
|
RadixKey([0, 1, 2, 3, 4])
|
|
)
|
|
|
|
self.assertEqual(prefix_len, 4)
|
|
|
|
def test_cp_prepare_probe_floors_exact_valid_tail_when_request_extends(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 145
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
prefix_len = cache._probe_existing_radix_prefix_len_no_split(
|
|
RadixKey(list(range(10)))
|
|
)
|
|
|
|
self.assertEqual(prefix_len, 4)
|
|
|
|
def test_cp_match_prefix_floors_exact_valid_tail_when_request_extends(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 146
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_value = None
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(10)))))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [0, 1, 2, 3])
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertEqual(result.last_device_node.key.token_ids, [0, 1, 2, 3])
|
|
self.assertNotIn((4, 5), result.last_device_node.children)
|
|
|
|
def test_cp_match_prefix_floors_exact_valid_tail_for_exact_key(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 4
|
|
cache.ongoing_write_through = {}
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(has_draft_hicache=False)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_len = 0
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 149
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_value = None
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(6)))))
|
|
|
|
self.assertEqual(result.device_indices.tolist(), [0, 1, 2, 3])
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertEqual(result.last_device_node.key.token_ids, [0, 1, 2, 3])
|
|
self.assertNotIn((4, 5), result.last_device_node.children)
|
|
|
|
def test_cp_insert_floors_backed_tail_split_to_page_boundary(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
has_draft_hicache=False,
|
|
write_policy="write_back",
|
|
)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._inc_hit_count = lambda *args, **kwargs: None
|
|
cache._record_store_event = lambda node: None
|
|
cache.evictable_size_ = 0
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 144
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_len = 6
|
|
node.cp_hicache = CpHiCacheNodeMetadata(
|
|
logical_len=6,
|
|
padded_len=8,
|
|
owned_positions=torch.tensor([0, 1, 2, 3], dtype=torch.int64),
|
|
host_indices=torch.tensor([50, 51, 52, 53], dtype=torch.int64),
|
|
page_owners=torch.tensor([0, 1], dtype=torch.int8),
|
|
page_size=4,
|
|
)
|
|
root.children[(0, 1, 2, 3)] = node
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey([0, 1, 2, 3, 4]),
|
|
value=torch.arange(5, dtype=torch.int64),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(result.prefix_len, 4)
|
|
parent = root.children[(0, 1, 2, 3)]
|
|
self.assertEqual(parent.key.token_ids, [0, 1, 2, 3])
|
|
self.assertEqual(parent.cp_hicache.logical_len, 4)
|
|
self.assertEqual(parent.cp_hicache.padded_len, 4)
|
|
self.assertNotIn((4, 5), parent.children)
|
|
new_tail = parent.children[(4,)]
|
|
self.assertEqual(new_tail.key.token_ids, [4])
|
|
self.assertEqual(new_tail.value.tolist(), [4])
|
|
|
|
def test_cp_insert_defers_stale_tail_prune_while_backup_pending(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.ongoing_write_through = {}
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
has_draft_hicache=False,
|
|
write_policy="write_through",
|
|
ack_write_queue=[],
|
|
)
|
|
cache.tp_world_size = 1
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._inc_hit_count = lambda *args, **kwargs: None
|
|
cache._record_store_event = lambda node: None
|
|
cache.evictable_size_ = 0
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 152
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[(0, 1, 2, 3)] = node
|
|
cache.ongoing_write_through[node.id] = node
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(list(range(10))),
|
|
value=torch.arange(10, dtype=torch.int64),
|
|
)
|
|
)
|
|
|
|
self.assertEqual(result.prefix_len, 0)
|
|
self.assertIs(result.pending_backup_deferred_node, node)
|
|
self.assertIs(root.children[(0, 1, 2, 3)], node)
|
|
self.assertEqual(node.key.token_ids, list(range(6)))
|
|
self.assertEqual(node.value.tolist(), list(range(6)))
|
|
|
|
def test_cp_insert_extends_from_page_boundary_after_exact_valid_tail(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.ongoing_write_through = {}
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
has_draft_hicache=False,
|
|
write_policy="write_through",
|
|
)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._inc_hit_count = lambda *args, **kwargs: None
|
|
cache._record_store_event = lambda node: None
|
|
cache.evictable_size_ = 0
|
|
cache.protected_size_ = 0
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 147
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[(0, 1, 2, 3)] = node
|
|
reservation = make_write_reservation(
|
|
torch.arange(4, 10, dtype=torch.int64), node_id=148, host_start=190
|
|
)
|
|
prepared = PreparedCpHiCacheBackup(
|
|
node_id=148,
|
|
reservation=reservation,
|
|
metadata=reservation.metadata,
|
|
logical_len=6,
|
|
)
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(list(range(10))),
|
|
value=torch.arange(10, dtype=torch.int64),
|
|
cp_hicache_prepared_backup=prepared,
|
|
)
|
|
)
|
|
|
|
self.assertEqual(result.prefix_len, 4)
|
|
parent = root.children[(0, 1, 2, 3)]
|
|
self.assertEqual(parent.key.token_ids, [0, 1, 2, 3])
|
|
self.assertNotIn((4, 5), parent.children)
|
|
new_tail = parent.children[(4, 5, 6, 7)]
|
|
self.assertEqual(new_tail.key.token_ids, [4, 5, 6, 7, 8, 9])
|
|
self.assertTrue(prepared.attached)
|
|
self.assertIs(cache.pending_host_backups[148].node, new_tail)
|
|
|
|
def test_cp_insert_replaces_exact_valid_tail_from_page_boundary(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.disable = False
|
|
cache.is_eagle = False
|
|
cache.page_size = 4
|
|
cache.pending_host_backups = {}
|
|
cache.ongoing_write_through = {}
|
|
cache.cache_controller = types.SimpleNamespace(
|
|
has_draft_hicache=False,
|
|
write_policy="write_through",
|
|
)
|
|
cache.get_child_key_fn = lambda key: tuple(key.token_ids[:4])
|
|
cache.key_match_fn = lambda child_key, key: _key_match_paged(
|
|
child_key, key, page_size=4
|
|
)
|
|
cache.maybe_bigram_convert = lambda key, value=None: (key, value)
|
|
cache._update_leaf_status = lambda node: None
|
|
cache._update_host_leaf_status = lambda node: None
|
|
cache._inc_hit_count = lambda *args, **kwargs: None
|
|
cache._record_store_event = lambda node: None
|
|
cache.evictable_size_ = 0
|
|
cache.protected_size_ = 0
|
|
cache.enable_storage = False
|
|
cache.enable_kv_cache_events = False
|
|
cache.inc_node_lock_ref = lambda node: None
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.children = {}
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.id = 150
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(6)))
|
|
node.value = torch.arange(6, dtype=torch.int64)
|
|
node.host_len = 0
|
|
node.cp_hicache = None
|
|
root.children[(0, 1, 2, 3)] = node
|
|
reservation = make_write_reservation(
|
|
torch.arange(4, 6, dtype=torch.int64), node_id=151, host_start=210
|
|
)
|
|
prepared = PreparedCpHiCacheBackup(
|
|
node_id=151,
|
|
reservation=reservation,
|
|
metadata=reservation.metadata,
|
|
logical_len=2,
|
|
)
|
|
|
|
result = cache.insert(
|
|
InsertParams(
|
|
key=RadixKey(list(range(6))),
|
|
value=torch.arange(6, dtype=torch.int64),
|
|
cp_hicache_prepared_backup=prepared,
|
|
)
|
|
)
|
|
|
|
self.assertEqual(result.prefix_len, 4)
|
|
parent = root.children[(0, 1, 2, 3)]
|
|
self.assertEqual(parent.key.token_ids, [0, 1, 2, 3])
|
|
new_tail = parent.children[(4, 5)]
|
|
self.assertIsNot(new_tail, node)
|
|
self.assertEqual(new_tail.key.token_ids, [4, 5])
|
|
self.assertTrue(prepared.attached)
|
|
self.assertIs(cache.pending_host_backups[151].node, new_tail)
|
|
|
|
def test_non_cp_match_prefix_uses_root_when_no_host_backup_exists(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = False
|
|
cache.device = "cpu"
|
|
cache.disable = False
|
|
cache.page_size = 1
|
|
cache.get_child_key_fn = lambda key: key.token_ids[0]
|
|
cache.key_match_fn = lambda child_key, key: sum(
|
|
1 for lhs, rhs in zip(child_key.token_ids, key.token_ids) if lhs == rhs
|
|
)
|
|
cache.maybe_bigram_convert = lambda key: (key, None)
|
|
root = TreeNode()
|
|
root.key = RadixKey([])
|
|
root.value = torch.empty((0,), dtype=torch.int64)
|
|
root.host_value = None
|
|
cache.root_node = root
|
|
node = TreeNode()
|
|
node.parent = root
|
|
node.key = RadixKey(list(range(4)))
|
|
node.value = torch.arange(4, dtype=torch.int64)
|
|
node.host_value = None
|
|
root.children[0] = node
|
|
|
|
result = cache.match_prefix(MatchPrefixParams(key=RadixKey(list(range(4)))))
|
|
|
|
self.assertEqual(result.host_hit_length, 0)
|
|
self.assertIs(result.last_host_node, root)
|
|
self.assertIs(result.last_device_node, node)
|
|
|
|
def test_cp_load_back_non_evicted_node_returns_none_without_loading(self):
|
|
cache = HiRadixCache.__new__(HiRadixCache)
|
|
cache._uses_cp_hicache = True
|
|
cache.load_back_threshold = 1
|
|
cache.evictable_size_ = 0
|
|
cache.metrics_collector = None
|
|
cache.ongoing_load_back = {}
|
|
cache.inc_lock_ref = lambda node: type("Result", (), {"delta": 0})()
|
|
cache.dec_lock_ref = lambda node: None
|
|
cache.cache_controller = type(
|
|
"Controller",
|
|
(),
|
|
{
|
|
"load_cp": lambda self, nodes, node_id=-1: (_ for _ in ()).throw(
|
|
AssertionError("must not load without evicted nodes")
|
|
)
|
|
},
|
|
)()
|
|
node = TreeNode()
|
|
node.value = torch.empty((0,), dtype=torch.int64)
|
|
|
|
loaded = cache.load_back(node)
|
|
|
|
self.assertIsNone(loaded)
|
|
self.assertEqual(cache.ongoing_load_back, {})
|
|
|
|
|
|
class TestCPHiCacheLayerBackupNotifications(CustomTestCase):
|
|
def _call_store_index_fast_path(self, out_loc):
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
|
|
notifications = []
|
|
fused_calls = []
|
|
|
|
class FakePool:
|
|
page_size = 64
|
|
start_layer = 0
|
|
|
|
def get_index_k_with_scale_buffer(self, layer_id):
|
|
return torch.empty((1,), dtype=torch.uint8)
|
|
|
|
def notify_layer_kv_stored_for_backup(self, layer_id, source="kv"):
|
|
notifications.append((layer_id, source))
|
|
|
|
forward_batch = types.SimpleNamespace(token_to_kv_pool=FakePool())
|
|
indexer = types.SimpleNamespace()
|
|
|
|
def fake_fused_store(key, buf, loc, page_size):
|
|
fused_calls.append((key, buf, loc.clone(), page_size))
|
|
|
|
with (
|
|
patch.object(nsa_indexer, "_is_cuda", True),
|
|
patch.object(nsa_indexer, "_is_fp8_fnuz", False),
|
|
patch.object(nsa_indexer, "can_use_nsa_fused_store", return_value=True),
|
|
patch.object(
|
|
nsa_indexer, "fused_store_index_k_cache", side_effect=fake_fused_store
|
|
),
|
|
):
|
|
nsa_indexer.Indexer._store_index_k_cache(
|
|
indexer,
|
|
forward_batch,
|
|
layer_id=5,
|
|
key=torch.empty((max(out_loc.numel(), 1), 128), dtype=torch.float32),
|
|
out_loc_override=out_loc,
|
|
)
|
|
|
|
return notifications, fused_calls
|
|
|
|
def test_nsa_indexer_fused_store_does_not_notify_cp_hicache_layer_backup(self):
|
|
notifications, fused_calls = self._call_store_index_fast_path(
|
|
torch.tensor([1, 2, 3], dtype=torch.int64)
|
|
)
|
|
|
|
self.assertEqual(notifications, [])
|
|
self.assertEqual(len(fused_calls), 1)
|
|
self.assertEqual(fused_calls[0][2].tolist(), [1, 2, 3])
|
|
|
|
def test_nsa_indexer_empty_store_does_not_notify_cp_hicache_layer_backup(self):
|
|
notifications, fused_calls = self._call_store_index_fast_path(
|
|
torch.empty((0,), dtype=torch.int64)
|
|
)
|
|
|
|
self.assertEqual(notifications, [])
|
|
self.assertEqual(fused_calls, [])
|
|
|
|
def test_cp_shared_zero_local_index_store_does_not_notify_layer_backup(self):
|
|
from sglang.srt.layers.attention.nsa import nsa_indexer
|
|
|
|
notifications = []
|
|
|
|
class FakePool:
|
|
page_size = 64
|
|
|
|
def notify_layer_kv_stored_for_backup(self, layer_id, source="kv"):
|
|
notifications.append((layer_id, source))
|
|
|
|
forward_batch = types.SimpleNamespace(token_to_kv_pool=FakePool())
|
|
indexer = types.SimpleNamespace(nsa_enable_prefill_cp=True)
|
|
|
|
with (
|
|
patch.object(nsa_indexer, "nsa_use_prefill_cp", return_value=True),
|
|
patch.object(
|
|
nsa_indexer,
|
|
"get_cp_shared_kv_local_out_cache_loc",
|
|
return_value=torch.empty((0,), dtype=torch.int64),
|
|
),
|
|
patch.object(
|
|
nsa_indexer,
|
|
"get_cp_shared_kv_local_physical_out_cache_loc",
|
|
side_effect=AssertionError("must not require physical locs"),
|
|
),
|
|
):
|
|
handled = nsa_indexer.Indexer._store_cp_shared_local_index_k_cache(
|
|
indexer,
|
|
forward_batch=forward_batch,
|
|
layer_id=6,
|
|
local_key=torch.empty((0, 128), dtype=torch.float32),
|
|
act_quant=None,
|
|
)
|
|
|
|
self.assertTrue(handled)
|
|
self.assertEqual(notifications, [])
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|