[HiCache] L3 storage prefetch lifecycle metrics and cross-tier attribution fixes (#37503)
This commit is contained in:
@@ -9,6 +9,7 @@ import torch
|
||||
|
||||
from sglang.srt.managers.cache_controller import CacheOperation, HiCacheController
|
||||
from sglang.srt.mem_cache import l2_transfer as transfer_module
|
||||
from sglang.srt.mem_cache.buffer_mode.pipeline import BufferModePipeline
|
||||
from sglang.srt.mem_cache.hicache_storage import (
|
||||
PoolHitPolicy,
|
||||
PoolName,
|
||||
@@ -262,6 +263,35 @@ class TestHiCacheStagedWriteBackDispatch(CustomTestCase):
|
||||
controller._num_tokens_by_pool.assert_called_once_with(merged_op)
|
||||
self.assertEqual(controller.ack_load_queue[0].node_ids, [7, 7])
|
||||
|
||||
def test_short_staged_swa_tail_resolves_device_covered_head(self):
|
||||
pipeline = BufferModePipeline.__new__(BufferModePipeline)
|
||||
pipeline._cache = mock.Mock()
|
||||
pipeline.release_staged_hold = mock.Mock(return_value=True)
|
||||
pipeline.staged_prefetches = {
|
||||
"r": SimpleNamespace(
|
||||
req_id="r",
|
||||
key_tokens=list(range(8)),
|
||||
extra_key=None,
|
||||
cache_salt=None,
|
||||
matched_len=2,
|
||||
num_tokens=6,
|
||||
occupied_tokens=6,
|
||||
host_indices=_indices(0, 6),
|
||||
aux_xfers=[
|
||||
PoolTransfer(
|
||||
name=PoolName.SWA,
|
||||
host_indices=_indices(0, 4),
|
||||
)
|
||||
],
|
||||
hash_values=[],
|
||||
operation_id=1,
|
||||
)
|
||||
}
|
||||
|
||||
self.assertEqual(pipeline.plan_staged_splice("r", device_prefix_len=6), (0, 0))
|
||||
pipeline._cache._resolve_storage_prefetch_tokens.assert_called_once_with("r", 4)
|
||||
pipeline.release_staged_hold.assert_called_once_with("r", reason="shrunk")
|
||||
|
||||
def test_l2_transfer_maps_global_layers(self):
|
||||
host_pool = mock.Mock()
|
||||
transfer = L2Transfer(
|
||||
|
||||
@@ -47,6 +47,10 @@ class _FakeTreeCore:
|
||||
}
|
||||
self.evicted = []
|
||||
self.cascaded = []
|
||||
# The real eviction helper reports unbacked FULL evictions to the
|
||||
# write-through drop counter; this fake never tracks a walk.
|
||||
self._is_tracking_unbacked_tokens = False
|
||||
self._tracked_unbacked_tokens = 0
|
||||
|
||||
def _evict_component_and_detach_lru(self, node, component, *args, **kwargs):
|
||||
self.evicted.append(node)
|
||||
|
||||
@@ -3676,6 +3676,8 @@ class UnifiedRadixCacheSuite:
|
||||
self.cfg, enable_kv_cache_events=True
|
||||
)
|
||||
self._init_buffer_hicache(cons, storage_dir)
|
||||
cons.enable_storage_metrics = True
|
||||
cons.storage_metrics_collector = mock.Mock()
|
||||
cons.take_events()
|
||||
avail0 = self._host_avail_sizes(cons)
|
||||
dev_avail0 = cons.token_to_kv_pool_allocator.available_size()
|
||||
@@ -3783,8 +3785,14 @@ class UnifiedRadixCacheSuite:
|
||||
and e.medium == StorageMedium.CPU
|
||||
]
|
||||
self.assertEqual(cpu_events, [])
|
||||
cons.storage_metrics_collector.log_storage_prefetch_hit_tokens.assert_called_once_with(
|
||||
len(seq)
|
||||
)
|
||||
cons.storage_metrics_collector.log_prefetched_tokens.assert_called_once_with(
|
||||
len(seq)
|
||||
)
|
||||
cons.storage_metrics_collector.log_storage_prefetch_unfulfilled_tokens.assert_not_called()
|
||||
|
||||
self.assertIn("occupancy_ratio", cons.prefetch_outcome_stats_snapshot())
|
||||
cons.sanity_check()
|
||||
|
||||
def test_buffer_only_cache_salt_uses_the_request_namespace(self):
|
||||
@@ -4148,6 +4156,8 @@ class UnifiedRadixCacheSuite:
|
||||
|
||||
cons, cons_alloc, cons_rtp = build_fixture(self.cfg)
|
||||
self._init_buffer_hicache(cons, storage_dir)
|
||||
cons.enable_storage_metrics = True
|
||||
cons.storage_metrics_collector = mock.Mock()
|
||||
avail0 = self._host_avail_sizes(cons)
|
||||
|
||||
req_id = "sibling-publish"
|
||||
@@ -4188,6 +4198,7 @@ class UnifiedRadixCacheSuite:
|
||||
k, v = self._snapshot_full_kv(cons_alloc, m.device_indices)
|
||||
self.assertTrue(torch.equal(k, sib_kv[0]))
|
||||
self.assertTrue(torch.equal(v, sib_kv[1]))
|
||||
cons.storage_metrics_collector.log_storage_prefetch_unfulfilled_tokens.assert_not_called()
|
||||
cons.sanity_check()
|
||||
|
||||
def test_buffer_only_load_back_drops_on_full_overlap_masked_by_swa_tombstone(
|
||||
@@ -8448,6 +8459,7 @@ class TestPrefetchCommitOrdering(CustomTestCase):
|
||||
cache.tree_core.insert_host.return_value = insert_result
|
||||
operation = mock.MagicMock()
|
||||
operation.request_id = "req"
|
||||
operation.completed_tokens = 8
|
||||
cache.ongoing_prefetch = {
|
||||
operation.request_id: (
|
||||
7,
|
||||
@@ -8588,6 +8600,8 @@ class TestUnifiedRadixPrefetchCorruption(CustomTestCase):
|
||||
"""The caller owns every completed buffer when host insertion drops."""
|
||||
cache, allocator, _ = build_fixture(self.cfg)
|
||||
self._init_hicache(cache)
|
||||
cache.enable_storage_metrics = True
|
||||
cache.storage_metrics_collector = mock.Mock()
|
||||
|
||||
parent_id = self._insert_device(
|
||||
cache, allocator, list(range(1, 1 + 3 * self.ps))
|
||||
@@ -8633,6 +8647,7 @@ class TestUnifiedRadixPrefetchCorruption(CustomTestCase):
|
||||
anchor_lock_params,
|
||||
comp_xfers,
|
||||
)
|
||||
cache._record_storage_prefetch_hit(req_id, completed_tokens)
|
||||
cache.cache_controller.prefetch_tokens_occupied = completed_tokens
|
||||
hashes = [f"h{i}" for i in range(completed_tokens // self.ps)]
|
||||
operation.hash_value = hashes
|
||||
@@ -8681,9 +8696,38 @@ class TestUnifiedRadixPrefetchCorruption(CustomTestCase):
|
||||
)
|
||||
self.assertIs(drop_releases[0].kwargs["extra_pools"][0], swa_transfer)
|
||||
self.assertIs(drop_releases[0].kwargs["extra_pools"][1], mamba_transfer)
|
||||
cache.storage_metrics_collector.log_storage_prefetch_hit_tokens.assert_called_once_with(
|
||||
completed_tokens
|
||||
)
|
||||
cache.storage_metrics_collector.log_storage_prefetch_unfulfilled_tokens.assert_called_once_with(
|
||||
completed_tokens, "dropped"
|
||||
)
|
||||
|
||||
cache.sanity_check()
|
||||
|
||||
def test_write_through_eviction_counts_unbacked_tokens(self):
|
||||
if _selected_tree_core_test_backend() == "rust":
|
||||
# The unbacked-eviction tracker is a Python tree-core feature;
|
||||
# UnifiedRadixCache only enables it for that backend.
|
||||
self.skipTest(
|
||||
"write-through unbacked-eviction tracking is Python-core only"
|
||||
)
|
||||
cache, allocator, _ = build_fixture(self.cfg)
|
||||
self._init_hicache(cache)
|
||||
cache.metrics_collector = mock.Mock()
|
||||
|
||||
seq = list(range(1, 1 + 2 * self.ps))
|
||||
self._insert_device(cache, allocator, seq)
|
||||
result = cache.evict(EvictParams(num_tokens=len(seq)))
|
||||
|
||||
self.assertGreaterEqual(result.num_tokens_evicted, len(seq))
|
||||
cache.metrics_collector.increment_dropped_tokens.assert_called_once_with(
|
||||
num_tokens=len(seq),
|
||||
reason="write_through_unbacked_eviction",
|
||||
pool=PoolName.KV.value,
|
||||
)
|
||||
cache.sanity_check()
|
||||
|
||||
def test_prefetch_refill_leaves_eviction_path_uncorrupted(self):
|
||||
"""Write-through: eviction after such a refill must not corrupt the tree."""
|
||||
cache, allocator, _ = build_fixture(self.cfg)
|
||||
@@ -8946,6 +8990,20 @@ class TestAnchorLockOutcomePolicy(CustomTestCase):
|
||||
self.assertEqual(pipeline.anchor_locks, {})
|
||||
self.assertEqual(pipeline.anchor_locked_tokens_, 0)
|
||||
|
||||
def test_positive_hit_with_lost_anchor_is_reported_as_shrunk(self):
|
||||
cache = UnifiedRadixCache.__new__(UnifiedRadixCache)
|
||||
cache._storage_prefetch_missed_rids = set()
|
||||
cache._finish_storage_prefetch = mock.Mock()
|
||||
cache.revoke_pending_prefetch = mock.Mock()
|
||||
|
||||
cache._handle_storage_prefetch_anchor_loss(self._REQ)
|
||||
|
||||
cache._finish_storage_prefetch.assert_called_once_with(
|
||||
self._REQ, fulfilled_tokens=0, reason="shrunk"
|
||||
)
|
||||
self.assertIn(self._REQ, cache._storage_prefetch_missed_rids)
|
||||
cache.revoke_pending_prefetch.assert_called_once_with(self._REQ)
|
||||
|
||||
def test_over_cap_reports_cap_skip_before_matching(self):
|
||||
cache = self._make_cache(live_match_len=len(self._PREFIX))
|
||||
pipeline = self._make_pipeline(cache, cap_tokens=len(self._PREFIX) - 1)
|
||||
|
||||
Reference in New Issue
Block a user