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
parent 8571fe0cd9
commit 71c4f66968
5 changed files with 899 additions and 11 deletions

View File

@@ -40,6 +40,7 @@ from sglang.srt.layers.dp_attention import (
)
from sglang.srt.mem_cache.cp_shared_kv_layout import CpSharedKVLayout
from sglang.srt.mem_cache.memory_pool import MLATokenToKVPool, NSATokenToKVPool
from sglang.srt.mem_cache.page_index_utils import validate_page_aligned_token_indices
from sglang.srt.utils import get_device_module
logger = logging.getLogger(__name__)
@@ -723,6 +724,10 @@ class HiCacheController:
self.prefetch_thread.start()
self.backup_thread.start()
def clear_draft_host_pool(self) -> None:
if self.draft_mem_pool_host is not None:
self.draft_mem_pool_host.clear()
def write(
self,
device_indices: torch.Tensor,
@@ -830,6 +835,30 @@ class HiCacheController:
event.record()
self.ack_load_queue.append(HiCacheAck(event, event, [node_id]))
def _validate_cp_hicache_page_indices(
self,
host_indices: torch.Tensor,
device_indices: torch.Tensor,
) -> None:
"""Validate page-shaped CP HiCache indices without CUDA host sync.
The production invariant is construction-based: HostKVCache alloc/free
preserves page-shaped host spans, and CpSharedKVLayout preserves
per-page contiguity when mapping logical locs to physical locs. The
generic validator uses Tensor truth values and would synchronize CUDA
tensors, so keep the hot path sync-free and validate CPU/fake-test
tensors only.
"""
if not host_indices.is_cuda:
validate_page_aligned_token_indices(
host_indices, self.page_size, "host_indices"
)
if not device_indices.is_cuda:
validate_page_aligned_token_indices(
device_indices, self.page_size, "physical_device_indices"
)
def _write_cp(
self,
device_indices: torch.Tensor,
@@ -909,6 +938,20 @@ class HiCacheController:
required_host_slots=len(physical_device_indices)
)
try:
self._validate_cp_hicache_page_indices(
host_indices, physical_device_indices
)
if draft_host_indices is not None:
self._validate_cp_hicache_page_indices(
draft_host_indices, physical_device_indices
)
except Exception:
self.mem_pool_host.free(host_indices)
if draft_host_indices is not None:
self.draft_mem_pool_host.free(draft_host_indices)
raise
self.write_queue.append(
CacheOperation(host_indices, physical_device_indices, node_id, priority)
)
@@ -1017,6 +1060,19 @@ class HiCacheController:
for node in nodes_to_load:
node_device_indices = device_indices[offset : offset + node.host_len]
offset += node.host_len
if self.has_draft_hicache:
draft_host_indices = getattr(
node.cp_hicache, "draft_host_indices", None
)
if draft_host_indices is None:
self.mem_pool_device_allocator.free(device_indices)
raise RuntimeError(
"CP HiCache draft KV restore requested but node metadata "
"does not contain draft_host_indices"
)
else:
draft_host_indices = None
owned_positions = node.cp_hicache.owned_positions.to(device_indices.device)
if owned_positions.numel() == 0:
continue
@@ -1037,16 +1093,7 @@ class HiCacheController:
self.cp_shared_kv_layout.logical_locs_to_physical(selected_logical_locs)
)
host_chunks.append(node.cp_hicache.host_indices)
if self.has_draft_hicache:
draft_host_indices = getattr(
node.cp_hicache, "draft_host_indices", None
)
if draft_host_indices is None:
self.mem_pool_device_allocator.free(device_indices)
raise RuntimeError(
"CP HiCache draft KV restore requested but node metadata "
"does not contain draft_host_indices"
)
if draft_host_indices is not None:
draft_host_chunks.append(draft_host_indices)
if not host_chunks:
@@ -1070,6 +1117,18 @@ class HiCacheController:
if self.has_draft_hicache:
draft_host_indices = torch.cat(draft_host_chunks)
try:
self._validate_cp_hicache_page_indices(
host_indices, physical_device_indices
)
if draft_host_indices is not None:
self._validate_cp_hicache_page_indices(
draft_host_indices, physical_device_indices
)
except Exception:
self.mem_pool_device_allocator.free(device_indices)
raise
self.load_queue.append(
CacheOperation(
host_indices,

View File

@@ -883,6 +883,7 @@ class HiRadixCache(RadixCache):
TreeNode.counter = 0
self.cache_controller.reset()
self.token_to_kv_pool_host.clear()
self.cache_controller.clear_draft_host_pool()
# Clear per-request tracking dicts
self.prefetch_loaded_tokens_by_reqid.clear()
self.evictable_host_leaves.clear()
@@ -918,9 +919,36 @@ class HiRadixCache(RadixCache):
logger.warning("Hierarchical cache storage backend is not enabled.")
return False
def _node_has_required_draft_hicache(self, node: TreeNode) -> bool:
if not self._uses_cp_hicache:
return True
controller = getattr(self, "cache_controller", None)
layout = getattr(controller, "cp_shared_kv_layout", None)
cp_rank = getattr(layout, "cp_rank", "?")
metadata = getattr(node, "cp_hicache", None)
if metadata is None:
raise RuntimeError(
"CP HiCache invariant violation: "
f"node_id={getattr(node, 'id', '?')} "
f"host_len={getattr(node, 'host_len', '?')} "
f"cp_rank={cp_rank} has host_len without cp_hicache metadata"
)
if controller is None or not getattr(controller, "has_draft_hicache", False):
return True
if getattr(metadata, "draft_host_indices", None) is None:
raise RuntimeError(
"CP HiCache invariant violation: draft HiCache is attached but "
f"node_id={getattr(node, 'id', '?')} "
f"host_len={getattr(node, 'host_len', '?')} "
f"cp_rank={cp_rank} is missing draft_host_indices"
)
return True
def _node_backuped(self, node: TreeNode) -> bool:
if self._uses_cp_hicache:
return node.host_len > 0 and node.cp_hicache is not None
if node.host_len <= 0:
return False
return self._node_has_required_draft_hicache(node)
return node.host_value is not None
def _node_host_len(self, node: TreeNode) -> int: