Cache completed chunked prefill pages in CP HiCache

Chunked prefill was skipping prepared CP HiCache backup entirely while still inserting unfinished valid-tail radix state. That combination caused middle chunks to miss reusable host cache and repeatedly hit stale-tail split fallback paths.\n\nThis keeps CP HiCache page-granular: middle chunks now reserve/write only completed pages, and unfinished radix inserts expose only page-complete prefixes while preserving the request-owned sub-page tail in prefix_indices for later chunks.\n\nConstraint: CP HiCache radix and host residency are managed at page granularity, but chunked prefill can end on a sub-page tail.\nRejected: Continue skipping chunked backup | loses middle-chunk reuse and triggers fallback storms.\nRejected: Insert sub-page chunk tails into CP HiCache | creates stale-tail prune/split conflicts against request-owned KV.\nConfidence: medium\nScope-risk: moderate\nDirective: Do not reintroduce chunked_req backup skip; keep CP HiCache chunked inserts page-complete unless the radix/host cache contract becomes sub-page aware.\nTested: python -m py_compile python/sglang/srt/mem_cache/radix_cache.py python/sglang/srt/mem_cache/hiradix_cache.py test/registered/unit/mem_cache/test_cp_hicache_metadata.py\nTested: g0034 targeted chunked CP HiCache regression tests, 2 passed\nTested: g0034 PYTHONPATH=python python -m pytest -q test/registered/unit/mem_cache/test_cp_hicache_metadata.py, 107 passed\nNot-tested: Fresh chunked-prefill ETE traffic run after process restart
This commit is contained in:
laoyao0822
2026-06-02 06:22:17 +08:00
parent 5bd68768d9
commit 6a25c312c7
4 changed files with 182 additions and 24 deletions
@@ -1221,30 +1221,102 @@ class TestHiRadixCacheCPBackup(CustomTestCase):
)
)
def test_prepare_write_backup_for_req_chunked_skip_warns_fallback(self):
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,
)
with self.assertLogs(
"sglang.srt.mem_cache.hiradix_cache", level="WARNING"
) as captured:
cache.prepare_write_backup_for_req(req)
cache.prepare_write_backup_for_req(req)
self.assertTrue(
any(
"[CP_HICACHE_FALLBACK][prepare_write_backup_skipped]" in message
and "reason=chunked_req" in message
for message in captured.output
)
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)