[Hicache][2/2]Support Mamba branching in Unified Radix Cache with HiCache (#33639)

This commit is contained in:
Jincong Chen
2026-08-10 17:16:42 +08:00
committed by GitHub
parent 430f38ea25
commit 3c533acec6
7 changed files with 88 additions and 25 deletions
@@ -1718,15 +1718,17 @@ class HostPoolGroup:
pool_transfers: Optional[list] = None,
) -> None:
# 1. Anchor (KV) backup
anchor_host_indices, anchor_device_indices = self._normalize_backup_indices(
self.anchor_entry, host_indices, device_indices, io_backend
)
self.anchor_entry.host_pool.backup_from_device_all_layer(
self.anchor_entry.device_pool,
anchor_host_indices,
anchor_device_indices,
io_backend,
)
# A zero-length anchor denotes a component-only backup.
if host_indices.numel() > 0:
anchor_host_indices, anchor_device_indices = self._normalize_backup_indices(
self.anchor_entry, host_indices, device_indices, io_backend
)
self.anchor_entry.host_pool.backup_from_device_all_layer(
self.anchor_entry.device_pool,
anchor_host_indices,
anchor_device_indices,
io_backend,
)
# 2. Extra pool backup
for transfer in pool_transfers or []:
entry = self.entry_map.get(transfer.name)
@@ -73,6 +73,10 @@ class MambaComponent(TreeComponent):
# HiCache state
self._mamba_pool_host = None # set to host mamba pool when HiCache enabled
def needs_incremental_backup(self, node: UnifiedTreeNode) -> bool:
data = node.component_data[self.component_type]
return data.value is not None and data.host_value is None
def _inc_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
cd = leaf.component_data[self.component_type]
cd.session_ref += 1
@@ -156,7 +160,9 @@ class MambaComponent(TreeComponent):
# Full KV may extend beyond the latest reusable Mamba state. The branching
# point is the last Mamba-cache-chunk-aligned position within the Full-KV hit
# that lies beyond the current Mamba boundary.
# that lies beyond the current Mamba boundary. With HiCache, incremental
# persistence of a new branching state is currently write-through only;
# write-back eviction may discard the device-only state.
aligned_seqlen = (
result.full_kv_hit_length // self.mamba_cache_chunk_size
) * self.mamba_cache_chunk_size
@@ -76,6 +76,9 @@ class SWAComponent(TreeComponent):
component_type = ComponentType.SWA
def needs_incremental_backup(self, node: UnifiedTreeNode) -> bool:
return False
def reset_session_state(self) -> None:
super().reset_session_state()
self._session_leaf_covered_len = {}
@@ -315,6 +315,10 @@ class TreeComponent(ABC):
cd = node.component_data[self.component_type]
return cd.value is None and cd.host_value is not None
def needs_incremental_backup(self, node: UnifiedTreeNode) -> bool:
"""Whether this component has new device data missing from Host."""
return False
def refresh_lru(
self,
phase: LRURefreshPhase,
@@ -1008,8 +1008,31 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
)
state.phase = _InsertPhase.TAIL
def _needs_incremental_component_backup(self, node: UnifiedTreeNode) -> bool:
return any(
component.needs_incremental_backup(node)
for component in self.components
if component.component_type != BASE_COMPONENT_TYPE
)
def _should_backup_after_insert(self, state: _InsertWalkState) -> bool:
"""Check whether the insert target needs a Host backup."""
if state.is_new_leaf:
return self._inc_hit_count_and_check(
state.target_node, state.params.chunked
)
node = state.target_node
return (
self.enable_hicache
and not self.is_write_back
and node.backuped
and node.write_through_pending_id is None
and self._needs_incremental_component_backup(node)
)
def _insert_tail_step(self, state: _InsertWalkState) -> None:
"""Refresh the LRUs and append the terminal new-leaf backup."""
"""Refresh the LRUs and append terminal backup actions."""
if state.target_node is not self.root_node:
for component in self.components:
if component.component_type == BASE_COMPONENT_TYPE:
@@ -1018,9 +1041,7 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
LRURefreshPhase.INSERT_END, state.target_node, self.root_node
)
if state.is_new_leaf and self._inc_hit_count_and_check(
state.target_node, state.params.chunked
):
if self._should_backup_after_insert(state):
state.pending_actions.append(
self._build_backup_kv_action(state.target_node)
)
@@ -1760,12 +1781,18 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
return self._build_backup_spec(self.node_by_id(node_id))
def _build_backup_spec(self, node: UnifiedTreeNode):
"""Gather device value backup spec."""
"""Gather missing Full and component transfers for Host backup."""
device_value = node.component_data[BASE_COMPONENT_TYPE].value
assert device_value is not None
if node.backuped:
device_value = device_value[:0]
comp_xfers: dict[ComponentType, list] = {}
for comp in self.components:
if comp.component_type == BASE_COMPONENT_TYPE:
continue
if node.component_data[comp.component_type].host_value is not None:
continue
t = comp.build_hicache_transfers(node, CacheTransferPhase.BACKUP_HOST)
if t:
comp_xfers[comp.component_type] = t
@@ -1894,13 +1921,14 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
"""Commit a successful backup to the node."""
node = self.node_by_id(node_id)
cache_actions: list[CacheAction | ComponentAction] = []
kv_xfer = PoolTransfer(name=PoolName.KV, host_indices=host_indices)
self.components_by_type[BASE_COMPONENT_TYPE].commit_hicache_transfer(
node,
CacheTransferPhase.BACKUP_HOST,
transfers=[kv_xfer],
cache_actions=cache_actions,
)
if host_indices.numel() > 0:
kv_xfer = PoolTransfer(name=PoolName.KV, host_indices=host_indices)
self.components_by_type[BASE_COMPONENT_TYPE].commit_hicache_transfer(
node,
CacheTransferPhase.BACKUP_HOST,
transfers=[kv_xfer],
cache_actions=cache_actions,
)
for ct, xfers in comp_xfers.items():
self.components_by_type[ct].commit_hicache_transfer(
node,
@@ -919,10 +919,11 @@ class UnifiedRadixCache(BasePrefixCache):
"""Run a backup action top-down, stopping at the first failed backup."""
written = 0
for node_id in action.node_ids:
# Overlapping chain actions: skip already-backed nodes.
if self.tree_core.is_backuped(node_id):
continue
device_value, comp_xfers = self.tree_core.build_backup_spec(node_id)
# Overlapping chain actions may revisit nodes with Full KV already
# backed up. Skip only when no transfer remains.
if device_value.numel() == 0 and not comp_xfers:
continue
sidecar_xfers = self._build_backup_sidecar(device_value, comp_xfers)
host_indices = self._execute_kv_backup(
node_id, device_value, comp_xfers, sidecar_xfers