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:
@@ -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,
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user