Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
941 lines
36 KiB
Python
941 lines
36 KiB
Python
from __future__ import annotations
|
|
|
|
from collections import defaultdict
|
|
from typing import TYPE_CHECKING, Callable, Optional, Sequence
|
|
|
|
import torch
|
|
|
|
from sglang.srt.mem_cache.base_prefix_cache import (
|
|
DecLockRefParams,
|
|
EvictParams,
|
|
IncLockRefResult,
|
|
InsertParams,
|
|
InsertResult,
|
|
MatchPrefixParams,
|
|
MatchResult,
|
|
)
|
|
from sglang.srt.mem_cache.hicache_storage import (
|
|
PoolHitPolicy,
|
|
PoolName,
|
|
PoolTransfer,
|
|
PoolTransferResult,
|
|
)
|
|
from sglang.srt.mem_cache.unified_cache.cache_action import (
|
|
FreeComponentDeviceSlot,
|
|
FreeComponentHostSlot,
|
|
MambaEvictExcessPathStates,
|
|
)
|
|
from sglang.srt.mem_cache.unified_cache.components.base import (
|
|
CacheTransferPhase,
|
|
ComponentType,
|
|
EvictLayer,
|
|
LinkerTransferPhase,
|
|
LRURefreshPhase,
|
|
PrepareLoadBackResult,
|
|
PreparePrefetchResult,
|
|
TreeComponent,
|
|
get_and_increase_time_counter,
|
|
)
|
|
from sglang.srt.runtime_context import (
|
|
get_exec,
|
|
mamba_cache_chunk_size,
|
|
mamba_checkpoint_grid,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from sglang.srt.managers.schedule_batch import Req
|
|
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
|
from sglang.srt.mem_cache.unified_cache.cache_action import (
|
|
CacheAction,
|
|
ComponentAction,
|
|
)
|
|
from sglang.srt.mem_cache.unified_radix_cache import (
|
|
NodeId,
|
|
UnifiedRadixCache,
|
|
UnifiedTreeNode,
|
|
)
|
|
|
|
|
|
class MambaComponent(TreeComponent):
|
|
component_type = ComponentType.MAMBA
|
|
|
|
def __init__(self, cache: UnifiedRadixCache, params: CacheInitParams):
|
|
from sglang.srt.mem_cache.memory_pool import HybridReqToTokenPool
|
|
|
|
assert isinstance(params.req_to_token_pool, HybridReqToTokenPool), (
|
|
f"MambaComponent requires HybridReqToTokenPool, got {type(params.req_to_token_pool)}"
|
|
)
|
|
if not params.enable_mamba_extra_buffer:
|
|
assert params.page_size == 1, (
|
|
f"MambaComponent requires page_size=1 when mamba_extra_buffer is disabled, got {params.page_size}"
|
|
)
|
|
super().__init__(cache, params)
|
|
self.mamba_cache_chunk_size = mamba_cache_chunk_size()
|
|
# params.page_size is the tree page the allocator actually uses, already
|
|
# widened by dcp_size, so it is the one grid a checkpoint depth can land on.
|
|
self.mamba_checkpoint_grid = mamba_checkpoint_grid(params.page_size)
|
|
self.mamba_max_states_per_path = get_exec().mamba.mamba_max_states_per_path
|
|
# 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
|
|
if cd.session_ref == 1:
|
|
self._refresh_session_partition(leaf)
|
|
|
|
def _dec_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
|
cd = leaf.component_data[self.component_type]
|
|
assert cd.session_ref > 0
|
|
cd.session_ref -= 1
|
|
if cd.session_ref == 0:
|
|
self._refresh_session_partition(leaf)
|
|
|
|
def _advance_session_coverage(
|
|
self,
|
|
session_id: str,
|
|
leaf: UnifiedTreeNode,
|
|
old_ancestor: Optional[UnifiedTreeNode],
|
|
) -> None:
|
|
self._inc_session_coverage(session_id, leaf)
|
|
if old_ancestor is not None:
|
|
self._dec_session_coverage(session_id, old_ancestor)
|
|
|
|
def _recede_session_coverage(
|
|
self,
|
|
session_id: str,
|
|
leaf: UnifiedTreeNode,
|
|
fallback: Optional[UnifiedTreeNode],
|
|
) -> None:
|
|
self._dec_session_coverage(session_id, leaf)
|
|
if fallback is not None:
|
|
self._inc_session_coverage(session_id, fallback)
|
|
|
|
def refresh_lru(
|
|
self,
|
|
phase: LRURefreshPhase,
|
|
node: UnifiedTreeNode,
|
|
root_node: UnifiedTreeNode,
|
|
) -> None:
|
|
# A match consumes only best_match_node's mamba state (cf. inc_lock_ref,
|
|
# which locks just this node's mamba value), unlike Full whose whole matched
|
|
# path is reused as prefix. Refreshing ancestors would keep a whole session's
|
|
# states adjacent in the mamba LRU and evict cold sessions wholesale, so touch
|
|
# only the used state. New leaf states enter the LRU via
|
|
# commit_insert_component_data, so the insert walk (WALKDOWN) is a no-op here.
|
|
ct = self.component_type
|
|
match phase:
|
|
case LRURefreshPhase.WALKDOWN:
|
|
return
|
|
case LRURefreshPhase.MATCH_END:
|
|
if node.component_data[ct].value is not None:
|
|
self.tree_core.lru_lists[ct].reset_node_mru(node)
|
|
case LRURefreshPhase.INSERT_END:
|
|
return
|
|
case _:
|
|
raise ValueError(f"Unknown LRURefreshPhase: {phase}")
|
|
|
|
def create_match_validator(
|
|
self, match_device_only: bool = False
|
|
) -> Callable[[UnifiedTreeNode], bool]:
|
|
ct = self.component_type
|
|
if match_device_only:
|
|
return lambda node: node.component_data[ct].value is not None
|
|
|
|
# HiCache: evicted + backuped (host_value present) is also a valid match
|
|
return lambda node: (
|
|
node.component_data[ct].value is not None
|
|
or node.component_data[ct].host_value is not None
|
|
)
|
|
|
|
def finalize_match_result_in_tree_core(
|
|
self,
|
|
result: MatchResult,
|
|
params: MatchPrefixParams,
|
|
value_chunks: list[torch.Tensor],
|
|
best_value_len: int,
|
|
) -> MatchResult:
|
|
last_node = result.best_match_node
|
|
|
|
mamba_boundary_len = len(result.device_indices) + result.host_hit_length
|
|
|
|
# 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. 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_checkpoint_grid
|
|
) * self.mamba_checkpoint_grid
|
|
branching_seqlen = (
|
|
aligned_seqlen if aligned_seqlen > mamba_boundary_len else None
|
|
)
|
|
|
|
# HiCache: if mamba was evicted from device but has host backup,
|
|
# ensure mamba_host_hit_length >= 1 so load_back is triggered.
|
|
if self.has_host_value_only(last_node):
|
|
result = result._replace(
|
|
mamba_host_hit_length=max(result.mamba_host_hit_length, 1)
|
|
)
|
|
|
|
return result._replace(mamba_branching_seqlen=branching_seqlen)
|
|
|
|
def finalize_match_result_in_cache(
|
|
self, params: MatchPrefixParams, result: MatchResult
|
|
) -> MatchResult:
|
|
# Copy-on-write the matched device mamba state into a per-request slot.
|
|
if not params.cow_mamba:
|
|
return result
|
|
src_index = self.tree_core.get_component_device_value(
|
|
result.best_match_node, self.component_type
|
|
)
|
|
if src_index is None:
|
|
return result
|
|
req = params.req
|
|
assert req is not None
|
|
if not req.kv.holds_mamba:
|
|
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
if dst_index is None:
|
|
# Pin the window via inc/dec_lock_ref so evict's SWA release
|
|
# stops at this request's window boundary instead of walking to
|
|
# root and over-decrementing locks held by other requests.
|
|
lock_result = self.cache.inc_lock_ref(result.best_match_node)
|
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
|
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
self.cache.dec_lock_ref(
|
|
result.best_match_node, lock_result.to_dec_params()
|
|
)
|
|
assert dst_index is not None, "Can not alloc mamba cache"
|
|
req.kv.mamba_pool_idx = dst_index[0]
|
|
req.kv.mamba_cow_src_index = src_index
|
|
req.kv.mamba_needs_clear = False
|
|
return result
|
|
|
|
def commit_insert_component_data(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
is_new_leaf: bool,
|
|
params: InsertParams,
|
|
result: InsertResult,
|
|
cache_actions: list[CacheAction | ComponentAction],
|
|
) -> None:
|
|
assert params.mamba_value is not None
|
|
if is_new_leaf:
|
|
node.component_data[self.component_type].value = params.mamba_value
|
|
self.tree_core.lru_lists[self.component_type].insert_mru(node)
|
|
self.tree_core.component_evictable_size_[self.component_type] += len(
|
|
params.mamba_value
|
|
)
|
|
self._emit_excess_path_states_eviction(node, cache_actions)
|
|
return
|
|
if node.component_data[self.component_type].value is None:
|
|
self.tree_core.set_component_device_value(
|
|
node.id, self.component_type, params.mamba_value
|
|
)
|
|
node.last_access_time = get_and_increase_time_counter()
|
|
self._emit_excess_path_states_eviction(node, cache_actions)
|
|
return
|
|
self.tree_core.lru_lists[self.component_type].reset_node_mru(node)
|
|
node.last_access_time = get_and_increase_time_counter()
|
|
result.mamba_exist = True
|
|
|
|
def _emit_excess_path_states_eviction(
|
|
self,
|
|
tail: UnifiedTreeNode,
|
|
cache_actions: list[CacheAction | ComponentAction],
|
|
) -> None:
|
|
"""Defer the path-cap eviction so it runs after the insert's BackupKV."""
|
|
if self.mamba_max_states_per_path < 0:
|
|
return
|
|
cache_actions.append(MambaEvictExcessPathStates(tail.id))
|
|
|
|
def _evict_excess_path_states(
|
|
self,
|
|
tail: UnifiedTreeNode,
|
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
|
) -> None:
|
|
"""Evict shallow eligible device checkpoints beyond the path cap.
|
|
|
|
Full KV and any existing host backup are retained. The tail, forks,
|
|
locked nodes (including a pending backup chain's write-through locks),
|
|
and device leaves are preserved, so the cap is a best-effort soft
|
|
limit. Freed slots are collected into the caller's dicts.
|
|
"""
|
|
cap = self.mamba_max_states_per_path
|
|
if cap < 0:
|
|
return
|
|
|
|
ct = self.component_type
|
|
holders = []
|
|
node = tail
|
|
while node is not None and node is not self.tree_core.root_node:
|
|
if node.component_data[ct].value is not None:
|
|
holders.append(node)
|
|
node = node.parent
|
|
|
|
excess = len(holders) - cap
|
|
if excess <= 0:
|
|
return
|
|
|
|
tracker = {component: 0 for component in self.cache.tree_components}
|
|
for node in reversed(holders):
|
|
if excess <= 0 or node is tail:
|
|
break
|
|
if node.component_data[ct].lock_ref > 0 or len(node.children) != 1:
|
|
continue
|
|
if node in self.tree_core.evictable_device_leaves:
|
|
continue
|
|
self.tree_core._evict_component_and_detach_lru(
|
|
node,
|
|
self,
|
|
device_frees,
|
|
host_frees,
|
|
target=EvictLayer.DEVICE,
|
|
tracker=tracker,
|
|
)
|
|
self.tree_core._cascade_evict(node, self, tracker, device_frees, host_frees)
|
|
excess -= 1
|
|
|
|
def redistribute_on_node_split(
|
|
self, new_parent: UnifiedTreeNode, child: UnifiedTreeNode
|
|
):
|
|
ct = self.component_type
|
|
new_parent.component_data[ct].value = None
|
|
new_parent.component_data[ct].lock_ref = 0
|
|
new_parent.component_data[ct].session_ref = 0
|
|
new_parent.component_data[ct].session_ids = None
|
|
# HiCache: mamba host_value stays on child (mamba = leaf-only data)
|
|
new_parent.component_data[ct].host_value = None
|
|
new_parent.component_data[ct].host_lock_ref = 0
|
|
|
|
def evict_component(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
|
target: EvictLayer = EvictLayer.DEVICE,
|
|
) -> tuple[int, int]:
|
|
cd = node.component_data[self.component_type]
|
|
freed = 0
|
|
host_freed = 0
|
|
|
|
# Device layer
|
|
if EvictLayer.DEVICE in target and cd.value is not None:
|
|
device_frees[self.component_type].append(cd.value)
|
|
freed = len(cd.value)
|
|
self.tree_core.component_evictable_size_[self.component_type] -= freed
|
|
cd.value = None
|
|
|
|
# Host layer
|
|
host_lru = self.tree_core.host_lru_lists[self.component_type]
|
|
if EvictLayer.HOST in target and cd.host_value is not None:
|
|
host_freed = len(cd.host_value)
|
|
host_frees[self.component_type].append(cd.host_value)
|
|
cd.host_value = None
|
|
if host_lru.in_list(node):
|
|
host_lru.remove_node(node)
|
|
|
|
# After device tombstone: if only host_value remains, insert into host LRU
|
|
if (
|
|
target is EvictLayer.DEVICE
|
|
and cd.value is None
|
|
and cd.host_value is not None
|
|
):
|
|
if not host_lru.in_list(node):
|
|
host_lru.insert_mru(node)
|
|
|
|
return freed, host_freed
|
|
|
|
def _evict_device_start(self, request_cnt: int) -> None:
|
|
"""Begin the device-eviction walk from this component's LRU cursor."""
|
|
self._evict_device_request_cnt = request_cnt
|
|
if self.tree_core.enable_session_radix_cache:
|
|
lru = self.tree_core.lru_lists[self.component_type]
|
|
lru.cursor_begin()
|
|
self._evict_device_cursor = lru.cursor_next()
|
|
else:
|
|
self._evict_device_cursor = self.tree_core.lru_lists[
|
|
self.component_type
|
|
].get_lru_no_lock()
|
|
|
|
def _evict_device_next_node(
|
|
self,
|
|
tracker: dict[ComponentType, int],
|
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
|
) -> Optional[NodeId]:
|
|
"""Advance one device-eviction step and return a leaf, if selected.
|
|
|
|
An internal tombstone is one complete step so the caller can apply its
|
|
pending frees and recheck allocator capacity before the next mutation.
|
|
If the previous node's eviction removed the cursor, the walk resumes
|
|
from the partition sentinel with session refs on, else it restarts at
|
|
the LRU tail.
|
|
"""
|
|
ct = self.component_type
|
|
lru = self.tree_core.lru_lists[ct]
|
|
enabled = self.tree_core.enable_session_radix_cache
|
|
if self._evict_device_cursor is not None and not lru.in_list(
|
|
self._evict_device_cursor
|
|
):
|
|
self._evict_device_cursor = (
|
|
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
|
)
|
|
if (
|
|
tracker[ct] >= self._evict_device_request_cnt
|
|
or self._evict_device_cursor is None
|
|
or not lru.in_list(self._evict_device_cursor)
|
|
):
|
|
return None
|
|
|
|
x = self._evict_device_cursor
|
|
assert x.component_data[ct].value is not None
|
|
if x in self.tree_core.evictable_device_leaves and (
|
|
not enabled or self._can_evict_leaf_atomically(x)
|
|
):
|
|
self._evict_device_cursor = (
|
|
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
|
)
|
|
return x.id
|
|
if not enabled:
|
|
x_next = lru.get_prev_no_lock(x)
|
|
self.tree_core._evict_component_and_detach_lru(
|
|
x,
|
|
self,
|
|
target=EvictLayer.DEVICE,
|
|
tracker=tracker,
|
|
device_frees=device_frees,
|
|
host_frees=host_frees,
|
|
)
|
|
self.tree_core._cascade_evict(
|
|
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
|
)
|
|
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
|
return None
|
|
|
|
def _evict_device_end(self) -> None:
|
|
"""Clear the device-eviction walk cursor state."""
|
|
if self.tree_core.enable_session_radix_cache:
|
|
self.tree_core.lru_lists[self.component_type].cursor_end()
|
|
self._evict_device_cursor = None
|
|
|
|
def acquire_component_lock(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
result: IncLockRefResult,
|
|
lock_host: bool = False,
|
|
) -> IncLockRefResult:
|
|
ct = self.component_type
|
|
if node is self.tree_core.root_node:
|
|
return result
|
|
cd = node.component_data[ct]
|
|
value = cd.host_value if lock_host else cd.value
|
|
# Tombstones are counted too; ledger/LRU track only data-bearing
|
|
# nodes (a value materialized under lock is credited to protected
|
|
# at the materialization site).
|
|
if lock_host:
|
|
if cd.host_lock_ref == 0 and value is not None:
|
|
host_lru = self.tree_core.host_lru_lists[ct]
|
|
if host_lru.in_list(node):
|
|
host_lru.remove_node(node)
|
|
cd.host_lock_ref += 1
|
|
else:
|
|
if cd.lock_ref == 0 and value is not None:
|
|
vlen = len(value)
|
|
self.tree_core.component_evictable_size_[ct] -= vlen
|
|
self.tree_core.component_protected_size_[ct] += vlen
|
|
cd.lock_ref += 1
|
|
return result
|
|
|
|
def release_component_lock(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
params: DecLockRefParams,
|
|
lock_host: bool = False,
|
|
) -> None:
|
|
ct = self.component_type
|
|
if node is self.tree_core.root_node:
|
|
return
|
|
cd = node.component_data[ct]
|
|
|
|
value = cd.host_value if lock_host else cd.value
|
|
if lock_host:
|
|
assert cd.host_lock_ref > 0, (
|
|
f"Mamba release hit host_lock_ref=0 on node {node.id}"
|
|
)
|
|
cd.host_lock_ref -= 1
|
|
if cd.host_lock_ref == 0:
|
|
if cd.value is None and cd.host_value is not None:
|
|
host_lru = self.tree_core.host_lru_lists[ct]
|
|
if not host_lru.in_list(node):
|
|
host_lru.insert_mru(node)
|
|
self.tree_core._update_evictable_leaf_sets(node)
|
|
return
|
|
|
|
assert cd.lock_ref > 0, f"Mamba release hit lock_ref=0 on node {node.id}"
|
|
if cd.lock_ref == 1 and value is not None:
|
|
vlen = len(value)
|
|
self.tree_core.component_evictable_size_[ct] += vlen
|
|
self.tree_core.component_protected_size_[ct] -= vlen
|
|
cd.lock_ref -= 1
|
|
if cd.lock_ref == 0:
|
|
self.tree_core._update_evictable_leaf_sets(node)
|
|
|
|
def _alloc_mamba_slot(self) -> torch.Tensor:
|
|
"""Allocate one mamba pool slot, evicting if necessary."""
|
|
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
if slot is None:
|
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
|
slot = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
assert slot is not None, "Can not alloc mamba cache"
|
|
return slot
|
|
|
|
@property
|
|
def int8_ckpt_pool(self):
|
|
return getattr(self.cache.req_to_token_pool, "mamba_ckpt_pool", None)
|
|
|
|
def _alloc_int8_ckpt_slot(self) -> torch.Tensor:
|
|
slot = self.int8_ckpt_pool.alloc(1)
|
|
if slot is None:
|
|
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
|
|
slot = self.int8_ckpt_pool.alloc(1)
|
|
assert slot is not None, "Can not alloc int8 mamba checkpoint slot"
|
|
return slot
|
|
|
|
def _commit_int8_checkpoint(self, active_slots: torch.Tensor) -> torch.Tensor:
|
|
ckpt_slot = self._alloc_int8_ckpt_slot()
|
|
self.int8_ckpt_pool.store_from_active(
|
|
self.cache.req_to_token_pool.mamba_pool,
|
|
active_slots.view(-1),
|
|
ckpt_slot,
|
|
)
|
|
return ckpt_slot
|
|
|
|
def _free_mamba_value(self, mamba_value: torch.Tensor) -> None:
|
|
if self.int8_ckpt_pool is not None:
|
|
self.int8_ckpt_pool.free(mamba_value)
|
|
else:
|
|
self.cache.req_to_token_pool.mamba_allocator.free(mamba_value)
|
|
|
|
def prepare_for_caching_req(
|
|
self,
|
|
req: Req,
|
|
insert_params: InsertParams,
|
|
token_ids_len: int,
|
|
is_finished: bool,
|
|
) -> Optional[int]:
|
|
if self.cache.enable_mamba_extra_buffer:
|
|
cache_len = req.kv.mamba_last_track_seqlen
|
|
else:
|
|
cache_len = token_ids_len
|
|
# ReplaySSM (no_buffer): `temporal[slot]` lags the live state by the
|
|
# slot's unflushed ring depth (`write_pos`), so on request finish cap
|
|
# the donate to the last flush boundary (where temporal is current)
|
|
# and reset the cursor, keeping the donated checkpoint consistent with
|
|
# its key length. page_size is asserted == 1, so no realign. Mirrors
|
|
# MambaRadixCache.cache_finished_req.
|
|
if is_finished:
|
|
write_pos_buf = (
|
|
self.cache.req_to_token_pool.mamba_pool.replayssm_write_pos
|
|
)
|
|
if write_pos_buf is not None:
|
|
cache_len -= int(write_pos_buf[req.kv.mamba_pool_idx].item())
|
|
write_pos_buf[req.kv.mamba_pool_idx] = 0
|
|
|
|
if is_finished:
|
|
if cache_len is None:
|
|
cache_len = 0
|
|
if self.cache.enable_mamba_extra_buffer:
|
|
keep_idx = self.cache.req_to_token_pool.get_mamba_ping_pong_keep_idx(
|
|
req
|
|
)
|
|
active_value = (
|
|
req.kv.mamba_ping_pong_track_buffer[keep_idx].unsqueeze(-1).clone()
|
|
)
|
|
else:
|
|
active_value = req.kv.mamba_pool_idx.unsqueeze(-1).clone()
|
|
if self.int8_ckpt_pool is not None:
|
|
insert_params.mamba_value = self._commit_int8_checkpoint(active_value)
|
|
else:
|
|
insert_params.mamba_value = active_value
|
|
return cache_len
|
|
else:
|
|
if cache_len is None:
|
|
return 0
|
|
# Donate the mamba index to the radix cache instead of copying.
|
|
if self.int8_ckpt_pool is not None:
|
|
if self.cache.enable_mamba_extra_buffer:
|
|
new_slot = self._alloc_mamba_slot()
|
|
src_active = (
|
|
self.cache.req_to_token_pool.donate_mamba_ping_pong_slot(
|
|
req, new_slot
|
|
)
|
|
)
|
|
mamba_value_donated = self._commit_int8_checkpoint(src_active)
|
|
self.cache.req_to_token_pool.mamba_allocator.free(src_active)
|
|
else:
|
|
mamba_value_donated = self._commit_int8_checkpoint(
|
|
req.kv.mamba_pool_idx.view(-1)
|
|
)
|
|
elif self.cache.enable_mamba_extra_buffer:
|
|
new_slot = self._alloc_mamba_slot()
|
|
mamba_value_donated = (
|
|
self.cache.req_to_token_pool.donate_mamba_ping_pong_slot(
|
|
req, new_slot
|
|
)
|
|
)
|
|
else:
|
|
mamba_value_donated = self._alloc_mamba_slot()
|
|
# mamba_pool is a pure PHYSICAL store; translate both slot ids
|
|
# virtual->physical (identity for the non-unified memory pool) first.
|
|
translate = self.cache.req_to_token_pool.translate_mamba_indices
|
|
self.cache.req_to_token_pool.mamba_pool.copy_from(
|
|
translate(req.kv.mamba_pool_idx.unsqueeze(0)),
|
|
translate(mamba_value_donated),
|
|
)
|
|
insert_params.mamba_value = mamba_value_donated
|
|
return cache_len
|
|
|
|
def cleanup_after_caching_req(
|
|
self,
|
|
req: Req,
|
|
is_finished: bool,
|
|
insert_result: Optional[InsertResult] = None,
|
|
insert_params: Optional[InsertParams] = None,
|
|
) -> None:
|
|
if is_finished:
|
|
mamba_value_inserted = (
|
|
insert_result is not None and not insert_result.mamba_exist
|
|
)
|
|
pool = self.cache.req_to_token_pool
|
|
|
|
if self.int8_ckpt_pool is not None:
|
|
insert_value_unused = (
|
|
not mamba_value_inserted
|
|
and insert_params is not None
|
|
and insert_params.mamba_value is not None
|
|
)
|
|
if insert_value_unused:
|
|
self._free_mamba_value(insert_params.mamba_value)
|
|
pool.free_mamba_cache(req)
|
|
return
|
|
|
|
if self.cache.enable_mamba_extra_buffer:
|
|
keep_idx = (
|
|
pool.get_mamba_ping_pong_keep_idx(req)
|
|
if mamba_value_inserted
|
|
else None
|
|
)
|
|
pool.free_mamba_cache(
|
|
req, mamba_ping_pong_track_buffer_to_keep=keep_idx
|
|
)
|
|
return
|
|
|
|
if not mamba_value_inserted:
|
|
pool.free_mamba_cache(req)
|
|
else:
|
|
if insert_params.mamba_value is not None and (
|
|
insert_result is None or insert_result.mamba_exist
|
|
):
|
|
self._free_mamba_value(insert_params.mamba_value)
|
|
req.kv.mamba_last_track_seqlen = None
|
|
|
|
def build_external_linker_transfer(
|
|
self,
|
|
phase: LinkerTransferPhase,
|
|
node: Optional[UnifiedTreeNode],
|
|
keys: Optional[Sequence[str]],
|
|
) -> Optional[PoolTransfer]:
|
|
raise AssertionError(
|
|
"MambaComponent does not support external linker mode, will support soon"
|
|
)
|
|
|
|
# ---- HiCache Hooks ----
|
|
|
|
def prepare_load_back(
|
|
self,
|
|
node_id: NodeId,
|
|
*,
|
|
req: Optional[Req] = None,
|
|
) -> PrepareLoadBackResult:
|
|
if (
|
|
req is None
|
|
or req.kv.holds_mamba
|
|
or not self.tree_core.component_has_host_value_only(
|
|
node_id, self.component_type
|
|
)
|
|
):
|
|
return PrepareLoadBackResult()
|
|
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
if dst is None:
|
|
self.cache.evict_for_alloc(EvictParams(num_tokens=0, mamba_num=1))
|
|
dst = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
|
|
assert dst is not None, "Cannot alloc mamba for load_back"
|
|
req.kv.mamba_pool_idx = dst[0]
|
|
return PrepareLoadBackResult(allocated_mamba_slot=dst)
|
|
|
|
def finalize_load_back(
|
|
self, req: Optional[Req], prep: PrepareLoadBackResult, success: bool
|
|
) -> None:
|
|
# A called-off load-back returns the slot prepare allocated and clears req (the H->D copy never ran).
|
|
if not success and prep.allocated_mamba_slot is not None:
|
|
self.cache.req_to_token_pool.mamba_allocator.free(prep.allocated_mamba_slot)
|
|
req.kv.mamba_pool_idx = None
|
|
|
|
def prepare_prefetch(
|
|
self,
|
|
node_id: NodeId,
|
|
*,
|
|
prefetch_tokens: int = 0,
|
|
) -> PreparePrefetchResult:
|
|
host_indices = self.cache.host_pool_group.alloc(
|
|
1,
|
|
pool=PoolName.MAMBA,
|
|
reclaim=lambda size: self.cache.evict_host(size, ComponentType.MAMBA),
|
|
)
|
|
if host_indices is None:
|
|
return PreparePrefetchResult(alloc_failed=True)
|
|
return PreparePrefetchResult(host_indices=host_indices)
|
|
|
|
def build_hicache_transfers(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
phase: CacheTransferPhase,
|
|
*,
|
|
mamba_pool_idx: Optional[torch.Tensor] = None,
|
|
host_indices: Optional[torch.Tensor] = None,
|
|
token_ids: Optional[Sequence[int]] = None,
|
|
prefetch_tokens: int = 0,
|
|
last_hash: Optional[str] = None,
|
|
) -> Optional[list[PoolTransfer]]:
|
|
ct = self.component_type
|
|
|
|
if phase == CacheTransferPhase.BACKUP_HOST:
|
|
cd = node.component_data[ct]
|
|
if cd.value is None:
|
|
return None
|
|
return [
|
|
PoolTransfer(
|
|
name=PoolName.MAMBA,
|
|
device_indices=cd.value,
|
|
)
|
|
]
|
|
|
|
if phase == CacheTransferPhase.LOAD_BACK:
|
|
transfers: list[PoolTransfer] = []
|
|
|
|
cd = node.component_data[ct]
|
|
if cd.value is not None:
|
|
return None
|
|
|
|
# restore single node if host_value exists
|
|
if cd.host_value is not None and cd.value is None:
|
|
transfers.append(
|
|
PoolTransfer(
|
|
name=PoolName.MAMBA,
|
|
host_indices=cd.host_value,
|
|
nodes_to_load=[node.id],
|
|
)
|
|
)
|
|
|
|
# Per-request mamba CoW: H→D copy into the request's device slot pre-allocated on the caller side.
|
|
cd = node.component_data[ct]
|
|
if mamba_pool_idx is not None and cd.host_value is not None:
|
|
transfers.append(
|
|
PoolTransfer(
|
|
name=PoolName.MAMBA,
|
|
host_indices=cd.host_value,
|
|
device_indices=mamba_pool_idx.unsqueeze(0),
|
|
)
|
|
)
|
|
|
|
return transfers if transfers else None
|
|
|
|
if phase == CacheTransferPhase.BACKUP_STORAGE:
|
|
cd = node.component_data[ct]
|
|
if cd.host_value is None or not node.hash_value:
|
|
return None
|
|
return [
|
|
PoolTransfer(
|
|
name=PoolName.MAMBA,
|
|
host_indices=cd.host_value,
|
|
keys=[node.hash_value[-1]],
|
|
hit_policy=PoolHitPolicy.TRAILING_PAGES,
|
|
)
|
|
]
|
|
|
|
if phase == CacheTransferPhase.PREFETCH:
|
|
assert host_indices is not None
|
|
return [
|
|
PoolTransfer(
|
|
name=PoolName.MAMBA,
|
|
host_indices=host_indices,
|
|
keys=["__placeholder__"],
|
|
hit_policy=PoolHitPolicy.TRAILING_PAGES,
|
|
)
|
|
]
|
|
|
|
return None
|
|
|
|
def commit_hicache_transfer(
|
|
self,
|
|
node: UnifiedTreeNode,
|
|
phase: CacheTransferPhase,
|
|
transfers: list[PoolTransfer] = (),
|
|
*,
|
|
cache_actions: list[CacheAction | ComponentAction],
|
|
insert_result: Optional[InsertResult] = None,
|
|
pool_storage_result: Optional[PoolTransferResult] = None,
|
|
) -> None:
|
|
ct = self.component_type
|
|
|
|
if phase == CacheTransferPhase.BACKUP_HOST:
|
|
if transfers and transfers[0].host_indices is not None:
|
|
cd = node.component_data[ct]
|
|
if cd.host_value is None:
|
|
cd.host_value = transfers[0].host_indices.clone()
|
|
|
|
elif phase == CacheTransferPhase.LOAD_BACK:
|
|
if not transfers:
|
|
return
|
|
transfer = transfers[0]
|
|
if transfer.device_indices is not None:
|
|
# The materialization primitive owns the ledger/LRU moves,
|
|
# including crediting protected when restored under lock.
|
|
self.tree_core.set_component_device_value(
|
|
node.id, ct, transfer.device_indices.clone()
|
|
)
|
|
|
|
elif phase == CacheTransferPhase.PREFETCH:
|
|
if not transfers:
|
|
return
|
|
transfer = transfers[0]
|
|
host_indices = transfer.host_indices
|
|
loaded = (
|
|
pool_storage_result is not None
|
|
and pool_storage_result.extra_pool_hit_pages.get(PoolName.MAMBA, 0) >= 1
|
|
)
|
|
target_node = (
|
|
self.tree_core.node_by_id(insert_result.inserted_host_node)
|
|
if insert_result is not None
|
|
and insert_result.inserted_host_node is not None
|
|
else None
|
|
)
|
|
if (
|
|
host_indices is None
|
|
or target_node is None
|
|
or not loaded
|
|
or target_node.component_data[ct].host_value is not None
|
|
):
|
|
cache_actions.append(
|
|
FreeComponentHostSlot(
|
|
[host_indices], component_type=ComponentType.MAMBA
|
|
)
|
|
)
|
|
if insert_result is not None:
|
|
insert_result.mamba_exist = True
|
|
return
|
|
|
|
target_node.component_data[ct].host_value = host_indices.clone()
|
|
if target_node.component_data[ct].value is None:
|
|
host_lru = self.tree_core.host_lru_lists[ct]
|
|
if not host_lru.in_list(target_node):
|
|
host_lru.insert_mru(target_node)
|
|
if insert_result is not None:
|
|
insert_result.mamba_exist = False
|
|
|
|
def drive_host_eviction(
|
|
self,
|
|
num_tokens: int,
|
|
tracker: dict[ComponentType, int],
|
|
device_frees: dict[ComponentType, list[torch.Tensor]],
|
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
|
) -> None:
|
|
"""Evict mamba host resources.
|
|
Internal nodes: private tombstone (free host mamba only).
|
|
Host leaves: atomic eviction via _evict_host_leaf."""
|
|
ct = self.component_type
|
|
host_lru = self.tree_core.host_lru_lists[ct]
|
|
enabled = self.tree_core.enable_session_radix_cache
|
|
if enabled:
|
|
host_lru.cursor_begin()
|
|
x = host_lru.cursor_next(host_lock=True)
|
|
else:
|
|
x = host_lru.get_lru_no_host_lock()
|
|
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
|
|
if not enabled:
|
|
x_next = host_lru.get_prev_no_host_lock(x)
|
|
cd = x.component_data[ct]
|
|
if x in self.tree_core.evictable_host_leaves and (
|
|
not enabled or self._can_evict_leaf_atomically(x)
|
|
):
|
|
# Host leaf: atomic eviction (all components host + delete)
|
|
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
|
|
else:
|
|
# Internal (or a leaf a session still pins): tombstone Mamba + cascade
|
|
assert cd.host_value is not None
|
|
self.tree_core._evict_component_and_detach_lru(
|
|
x,
|
|
self,
|
|
target=EvictLayer.HOST,
|
|
tracker=tracker,
|
|
device_frees=device_frees,
|
|
host_frees=host_frees,
|
|
)
|
|
self.tree_core._cascade_evict(
|
|
x,
|
|
self,
|
|
tracker,
|
|
device_frees=device_frees,
|
|
host_frees=host_frees,
|
|
target=EvictLayer.HOST,
|
|
)
|
|
self.tree_core._update_evictable_leaf_sets(x)
|
|
if enabled:
|
|
x = host_lru.cursor_next(host_lock=True)
|
|
else:
|
|
x = x_next
|
|
if enabled:
|
|
host_lru.cursor_end()
|
|
|
|
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
|
|
if self._mamba_pool_host is None:
|
|
return
|
|
for host_value in host_values:
|
|
self.cache.host_pool_group.free(host_value, pool=PoolName.MAMBA)
|
|
|
|
def apply_component_action(self, action: ComponentAction) -> None:
|
|
if isinstance(action, MambaEvictExcessPathStates):
|
|
device_frees: dict[ComponentType, list[torch.Tensor]] = defaultdict(list)
|
|
host_frees: dict[ComponentType, list[torch.Tensor]] = defaultdict(list)
|
|
# Drain even if the walk raises so tombstoned slots are not leaked;
|
|
# the walk runs behind the tree-core interface (Rust runs it natively).
|
|
try:
|
|
self.tree_core.evict_excess_path_states(
|
|
action.tail_node_id, device_frees, host_frees
|
|
)
|
|
finally:
|
|
self.cache._free_values(device_frees, host_frees)
|
|
return
|
|
if isinstance(action, FreeComponentDeviceSlot):
|
|
for indices in action.indices:
|
|
self._free_mamba_value(indices)
|
|
return
|
|
if isinstance(action, FreeComponentHostSlot):
|
|
for host_indices in action.host_indices:
|
|
if host_indices is not None and host_indices.numel() > 0:
|
|
self.cache.cache_controller.append_host_mem_release(
|
|
extra_pools=[
|
|
PoolTransfer(name=PoolName.MAMBA, host_indices=host_indices)
|
|
]
|
|
)
|
|
return
|
|
raise AssertionError(
|
|
f"MambaComponent: unhandled ComponentAction {type(action).__name__}"
|
|
)
|