Keep CP HiCache valid tails page-owned during extension
CP shared KV HiCache now treats non-page-aligned valid-tail nodes as page-owned when a later request extends beyond them. The prefix probe, match path, insert path, and prepared backup start now agree on flooring the reusable prefix to the previous physical page boundary, so prepared backup metadata cannot start mid-page or fail to attach at insertion. Duplicate frees under CP HiCache now go through a page-safe free helper. Insert and unfinished duplicate ranges free only fully unprotected pages; no-insert completion still releases the right tail owned only by the finishing request. Constraint: CP HiCache allocator frees whole physical pages even when called with token-granular locs. Rejected: Partial-page sharing/refcounting | too complex for the current page-as-minimum-unit contract. Rejected: Fix only prepare_write_backup_for_req | match_prefix and insert would still expose exact valid-tail hits and desynchronize prepared backup length. Confidence: medium Scope-risk: moderate Directive: Do not expose non-page-aligned CP valid-tail hits to extending requests unless partial-page ownership is explicitly implemented end-to-end. Tested: remote g0034 py_compile for touched files Tested: remote g0034 test_cp_hicache_metadata.py 97 passed Tested: remote g0034 test_cp_shared_kv_runtime.py 73 passed Not-tested: test_cp_shared_kv_layout.py aborts during installed sgl_kernel architecture-specific op loading before assertions
This commit is contained in:
@@ -1344,6 +1344,41 @@ class TestHiRadixCacheCPBackup(CustomTestCase):
|
||||
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
|
||||
@@ -1382,6 +1417,106 @@ class TestHiRadixCacheCPBackup(CustomTestCase):
|
||||
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_prepare_write_backup_for_req_skips_existing_insert_prefix(self):
|
||||
cache = HiRadixCache.__new__(HiRadixCache)
|
||||
cache.disable = False
|
||||
@@ -2601,6 +2736,83 @@ class TestHiRadixCacheCPLoadBack(CustomTestCase):
|
||||
|
||||
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
|
||||
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.assertEqual(result.last_device_node.children[(4, 5)].key.token_ids, [4, 5])
|
||||
|
||||
def test_cp_insert_floors_backed_tail_split_to_page_boundary(self):
|
||||
cache = HiRadixCache.__new__(HiRadixCache)
|
||||
cache._uses_cp_hicache = True
|
||||
@@ -2665,6 +2877,72 @@ class TestHiRadixCacheCPLoadBack(CustomTestCase):
|
||||
self.assertEqual(new_tail.key.token_ids, [4])
|
||||
self.assertEqual(new_tail.value.tolist(), [4])
|
||||
|
||||
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.assertEqual(parent.children[(4, 5)].key.token_ids, [4, 5])
|
||||
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_non_cp_match_prefix_uses_root_when_no_host_backup_exists(self):
|
||||
cache = HiRadixCache.__new__(HiRadixCache)
|
||||
cache._uses_cp_hicache = False
|
||||
|
||||
Reference in New Issue
Block a user