Enforce draft KV strong-sync for CP HiCache hits

CP HiCache host hits must not advertise target residency unless the draft payload is also valid when EAGLE/MTP draft HiCache is attached. This closes target-only metadata paths by making the CP host-valid predicate and load replay fail fast, resets draft host storage with the target host pool, and records the P1-P3 strong-sync plan state.

The page-index validator is restored for CPU/fake-test tensors only, preserving unit-test coverage for malformed page spans without reintroducing CUDA hot-path host sync.

Constraint: CP shared KV + HiCache + EAGLE/MTP cannot safely demote malformed target/draft metadata to an ordinary cache miss

Rejected: keep permissive fallback for missing draft_host_indices | it can look like a successful cache hit while poisoning speculative acceptance

Rejected: re-enable generic CUDA tensor page validation | it can force host sync in the HiCache transfer hot path

Confidence: high

Scope-risk: moderate

Reversibility: clean

Directive: Do not add silent fallback around CP draft HiCache metadata; unexpected target/draft divergence should fail fast with node/rank context

Tested: remote container targeted tests: 5 passed

Tested: remote container files test_cp_hicache_metadata.py and test_hicache_controller_cp.py: 77 passed

Tested: remote container test_page_index_utils.py: 8 passed

Tested: local git diff --check and py_compile for modified Python files

Not-tested: full CP shared KV + HiCache + EAGLE/MTP ETE

Co-authored-by: OmX <omx@oh-my-codex.dev>
This commit is contained in:
laoyao0822
2026-05-27 05:23:31 +08:00
co-authored by OmX
parent 8571fe0cd9
commit 71c4f66968
5 changed files with 899 additions and 11 deletions
@@ -541,6 +541,34 @@ class TestHiCacheControllerCPLoad(TestHiCacheControllerCPWrite):
self.assertEqual(host_pool.loads, [])
self.assertEqual(len(controller.ack_load_queue), 1)
def test_cp_load_zero_owned_rejects_missing_draft_metadata_when_draft_attached(self):
host_pool = FakeHostPool(torch.tensor([], dtype=torch.int64))
allocator = FakeAllocator(alloc_result=torch.arange(64, 68, dtype=torch.int64))
controller = self.make_controller(
host_pool,
allocator=allocator,
cp_rank=3,
draft_host_pool=FakeHostPool(torch.tensor([], dtype=torch.int64)),
draft_mem_pool_device=FakeDevicePool("draft"),
)
node = TreeNode()
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.tensor([0], dtype=torch.int8),
page_size=4,
)
with self.assertRaisesRegex(RuntimeError, "draft KV restore requested"):
controller.load_cp([node], node_id=33)
self.assertEqual(allocator.owner_alloc_calls, [[0]])
self.assertEqual(allocator.frees[0].tolist(), [64, 65, 66, 67])
self.assertEqual(controller.load_queue, [])
self.assertEqual(controller.draft_load_queue, [])
def test_cp_load_queues_cpu_host_indices_before_backend_moves(self):
host_pool = FakeHostPool(torch.tensor([100, 101, 102, 103], dtype=torch.int64))
allocator = FakeAllocator(alloc_result=torch.arange(64, 80, dtype=torch.int64))
@@ -147,6 +147,42 @@ class TestHiRadixCacheCPDraftHostPool(CustomTestCase):
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_split_zero_len_moves_all_positions_to_child(self):
@@ -528,6 +564,62 @@ class TestHiRadixCacheCPBackup(CustomTestCase):
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_single_node_write_lock_updates_device_evictable_leaf_set(self):
cache = HiRadixCache.__new__(HiRadixCache)
cache.disable = False