Radix Cache Split: Spin off TreeCore (#29901)

This commit is contained in:
Jialin Ouyang
2026-07-25 14:31:59 -07:00
committed by GitHub
parent a23f6ea090
commit cd145f840f
25 changed files with 6697 additions and 2703 deletions
@@ -71,7 +71,7 @@ class DecodeHiCachePreallocMixin:
l3_storage_hit_length = 0
last_host_node = None
if self.scheduler.enable_decode_hicache:
last_host_node = result.last_host_node
last_host_node = self.tree_cache.resolve_node_handle(result.last_host_node)
if last_host_node.backuped or last_host_node is self.tree_cache.root_node:
matched_len = l1_prefix_len + l2_host_hit_length
suffix_tokens = req.origin_input_ids[matched_len:]
@@ -82,7 +82,7 @@ class DecodeHiCachePreallocMixin:
else None
)
l3_storage_hit_length = self.tree_cache.query_storage_hit_length(
last_host_node,
result.last_host_node,
suffix_tokens,
last_hash,
prefix_keys,
@@ -93,7 +93,9 @@ class DecodeHiCachePreallocMixin:
l2_host_hit_length=l2_host_hit_length,
l3_storage_hit_length=l3_storage_hit_length,
last_device_node=result.last_device_node,
last_host_node=last_host_node if l3_storage_hit_length > 0 else None,
last_host_node=(
result.last_host_node if l3_storage_hit_length > 0 else None
),
)
def _start_hicache_prefetch(
@@ -110,7 +112,7 @@ class DecodeHiCachePreallocMixin:
):
return
try:
node = prefix_match.last_host_node
node = self.tree_cache.resolve_node_handle(prefix_match.last_host_node)
matched_len = prefix_match.l1_prefix_len + prefix_match.l2_host_hit_length
suffix = req.origin_input_ids[
matched_len : matched_len + prefix_match.l3_storage_hit_length
@@ -122,7 +124,7 @@ class DecodeHiCachePreallocMixin:
else None
)
self.tree_cache.prefetch_from_storage(
req.rid, node, suffix, last_hash, prefix_keys
req.rid, prefix_match.last_host_node, suffix, last_hash, prefix_keys
)
prefix_match.prefetch_registered = (
req.rid in self.tree_cache.ongoing_prefetch
+2
View File
@@ -847,6 +847,8 @@ class Envs:
# Unified Radix Tree
SGLANG_ENABLE_UNIFIED_RADIX_TREE = EnvBool(False)
# Registered TreeCore backend serving the unified radix cache.
SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND = EnvStr("python")
# CUDA Graph
SGLANG_USE_BREAKABLE_CUDA_GRAPH = EnvBool(False)
@@ -1,29 +1,19 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import torch
from sglang.srt.mem_cache.radix_cache import RadixCache
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
from sglang.srt.mem_cache.unified_cache_components import (
BASE_COMPONENT_TYPE,
ComponentType,
from sglang.srt.mem_cache.unified_cache.unified_tree_core_interface import (
RadixCacheWalkResult,
)
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
if TYPE_CHECKING:
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
from sglang.srt.mem_cache.radix_cache import TreeNode
from sglang.srt.mem_cache.unified_radix_cache import UnifiedTreeNode
@dataclass(frozen=True, slots=True, kw_only=True)
class RadixCacheWalkResult:
slot_indices: torch.Tensor
positions: torch.Tensor
prev_slot_indices: torch.Tensor
def walk_radix_cache_for_canary(
@@ -40,11 +30,11 @@ def walk_radix_cache_for_canary(
req. ``swa_resident_only=True`` skips SWA-tombstoned nodes (slots evicted from the SWA
window)."""
cache_type = type(radix_cache)
if (
cache_type is not RadixCache
and cache_type is not SWARadixCache
and cache_type is not UnifiedRadixCache
):
if cache_type is UnifiedRadixCache:
return radix_cache.tree_core.walk_for_kv_canary(
unlocked_only=unlocked_only, swa_resident_only=swa_resident_only
)
if cache_type is not RadixCache and cache_type is not SWARadixCache:
raise NotImplementedError(
f"walk_radix_cache_for_canary does not support {cache_type.__name__}"
)
@@ -78,7 +68,7 @@ def walk_radix_cache_for_canary(
def _walk_radix_subtree(
*,
node: TreeNode | UnifiedTreeNode,
node: TreeNode,
radix_cache: BasePrefixCache,
depth: int,
parent_last_slot: int,
@@ -89,13 +79,11 @@ def _walk_radix_subtree(
unlocked_only: bool,
swa_resident_only: bool,
) -> None:
node_slots = _node_slots_for_canary(node=node, radix_cache=radix_cache)
node_slots = _node_slots_for_canary(node=node)
if unlocked_only:
emit_slots = not is_root and _node_is_unlocked_for_canary(
node=node,
radix_cache=radix_cache,
swa_resident_only=swa_resident_only,
node=node, radix_cache=radix_cache
)
else:
emit_slots = not is_root
@@ -114,12 +102,7 @@ def _walk_radix_subtree(
prev_slot_buf.append(prev)
chain_last_slot = slot
child_depth = depth + _node_len_for_canary(
node=node,
radix_cache=radix_cache,
node_slots=node_slots,
is_root=is_root,
)
child_depth = depth + len(node_slots)
for child in node.children.values():
_walk_radix_subtree(
node=child,
@@ -135,42 +118,17 @@ def _walk_radix_subtree(
)
def _node_slots_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
) -> list[int]:
value: Any
if type(radix_cache) is UnifiedRadixCache:
value = node.component_data[BASE_COMPONENT_TYPE].value
else:
value = node.value
def _node_slots_for_canary(*, node: TreeNode) -> list[int]:
value: Any = node.value
if isinstance(value, torch.Tensor):
return [int(s) for s in value.tolist()]
return []
def _node_len_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
radix_cache: BasePrefixCache,
node_slots: list[int],
is_root: bool,
) -> int:
if type(radix_cache) is not UnifiedRadixCache:
return len(node_slots)
if is_root or node.key is None:
return len(node_slots)
return len(node.key)
def _node_is_unlocked_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
node: TreeNode,
radix_cache: BasePrefixCache,
swa_resident_only: bool,
) -> bool:
if type(radix_cache) is RadixCache:
return node.lock_ref == 0
@@ -178,13 +136,6 @@ def _node_is_unlocked_for_canary(
if type(radix_cache) is SWARadixCache:
return node.full_lock_ref == 0
if type(radix_cache) is UnifiedRadixCache:
if swa_resident_only and radix_cache.supports_swa():
# Unified SWA owns an independent component lock. A node can still
# hold Full KV for a running request while its SWA slots are unused.
return node.component_data[ComponentType.SWA].lock_ref == 0
return node.component_data[BASE_COMPONENT_TYPE].lock_ref == 0
raise NotImplementedError(
f"walk_radix_cache_for_canary does not support {type(radix_cache).__name__}"
)
@@ -192,15 +143,10 @@ def _node_is_unlocked_for_canary(
def _node_is_swa_resident_for_canary(
*,
node: TreeNode | UnifiedTreeNode,
node: TreeNode,
radix_cache: BasePrefixCache,
) -> bool:
if type(radix_cache) is SWARadixCache:
return not node.swa_tombstone
if type(radix_cache) is UnifiedRadixCache:
if not radix_cache.supports_swa():
return True
return node.component_data[ComponentType.SWA].value is not None
return True
+3 -1
View File
@@ -1249,7 +1249,9 @@ class Req(ReqDllmMixin):
)
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
match_result = zero_match_result(tree_cache, match_result)
match_result = zero_match_result(
tree_cache, match_result, extra_key=self.extra_key
)
(
self.prefix_indices,
self.last_node,
@@ -115,7 +115,9 @@ def match_prefix_for_req(
)
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
match_result = zero_match_result(tree_cache, match_result)
match_result = zero_match_result(
tree_cache, match_result, extra_key=req.extra_key
)
(
req.prefix_indices,
req.last_node,
@@ -290,7 +292,7 @@ class SchedulePolicy:
)
if envs.SGLANG_RADIX_FORCE_MISS.get():
match_result = zero_match_result(
self.waiting_queue_radix_tree, match_result
self.waiting_queue_radix_tree, match_result, extra_key=extra_key
)
in_batch_matching_prefixes = match_result.device_indices
if (
@@ -328,7 +330,8 @@ class SchedulePolicy:
"""Sorts the waiting queue based on a depth-first search weighting."""
last_node_to_reqs = defaultdict(list)
for req in waiting_queue:
last_node_to_reqs[req.last_node].append(req)
last_node = tree_cache.resolve_node_handle(req.last_node)
last_node_to_reqs[last_node].append(req)
node_to_weight = defaultdict(int)
for node in last_node_to_reqs:
+9 -9
View File
@@ -2418,25 +2418,25 @@ class Scheduler(
def _prefetch_kvcache(self, req: Req):
if self.enable_hicache_storage:
req.init_next_round_input(self.tree_cache, cow_mamba=False)
last_host_node = req.last_host_node
if last_host_node.backuped or last_host_node is self.tree_cache.root_node:
last_hash = last_host_node.get_last_hash_value()
tree_cache = self.tree_cache
if tree_cache.is_backuped(req.last_host_node) or tree_cache.is_root(
req.last_host_node
):
matched_len = len(req.prefix_indices) + req.host_hit_length
match_end = req._compute_max_prefix_len(
len(req.full_untruncated_fill_ids)
)
new_input_tokens = req.full_untruncated_fill_ids[matched_len:match_end]
prefix_keys = (
last_host_node.get_prefix_hash_values(last_host_node.parent)
if self.tree_cache.hicache_storage_pass_prefix_keys
tree_cache.get_prefix_hash_values(req.last_host_node)
if tree_cache.hicache_storage_pass_prefix_keys
else None
)
self.tree_cache.prefetch_from_storage(
tree_cache.prefetch_from_storage(
req.rid,
last_host_node,
req.last_host_node,
new_input_tokens,
last_hash,
tree_cache.get_last_hash_value(req.last_host_node),
prefix_keys,
)
@@ -9,6 +9,7 @@ from typing import (
NamedTuple,
Optional,
Protocol,
Sequence,
Tuple,
runtime_checkable,
)
@@ -26,6 +27,10 @@ from sglang.srt.observability.metrics_collector import (
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.mem_cache.radix_cache import RadixKey
from sglang.srt.mem_cache.unified_cache.cache_action import (
CacheAction,
ComponentAction,
)
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
ComponentType,
)
@@ -78,6 +83,10 @@ class InsertResult:
last_device_node: Any = None
mamba_exist: bool = False
inserted_host_node: Any = None
# Controller-applied actions from the non-stepped channels (e.g. insert_host); the stepped insert emits via InsertStepResult.actions.
cache_actions: list[CacheAction | ComponentAction] = dataclasses.field(
default_factory=list
)
@dataclasses.dataclass
@@ -191,13 +200,17 @@ class MatchResult(NamedTuple):
mamba_branching_seqlen: Optional[int] = None
cache_protected_len: Optional[int] = None
full_kv_hit_length: int = 0
# Actions the Controller applies: CacheActions itself, ComponentActions routed to the owning component.
cache_actions: Sequence[CacheAction | ComponentAction] = ()
def zero_match_result(tree_cache, match_result: MatchResult) -> MatchResult:
def zero_match_result(
tree_cache, match_result: MatchResult, extra_key: Optional[str] = None
) -> MatchResult:
if tree_cache.is_chunk_cache():
# Chunk caches' match_prefix already returns a miss; no root_node to walk back to.
return match_result
root = tree_cache.root_node
root = tree_cache.root_node_handle(extra_key=extra_key)
return match_result._replace(
# [:0] keeps dtype and device of the original tensor (e.g. CUDA int64)
# without allocating a fresh empty tensor.
@@ -258,6 +271,37 @@ class BasePrefixCache(ABC, PrefixCacheTrait):
def supports_fast_match_prefix(self) -> bool:
return False
def resolve_node_handle(self, node_handle: Any) -> Any:
"""Map a node handle to its node -- e.g. UnifiedRadixCache looks up the
node object from its NodeId. Temporary API for the Unified Radix Cache
split migration.
TODO(Jialin): Remove after the Unified Radix Cache split.
"""
return node_handle
def root_node_handle(self, extra_key: Optional[str] = None) -> Any:
"""The root handle as match results carry it -- the raw node by default,
the root's NodeId for UnifiedRadixCache. extra_key scopes the root for
implementations that shard trees per cache namespace."""
return self.root_node
def is_backuped(self, node: Any) -> bool:
"""Whether the node's Full KV is present on host."""
return node.backuped
def is_root(self, node: Any) -> bool:
"""Whether the node is a tree root."""
return node is self.root_node
def get_last_hash_value(self, node: Any) -> Optional[str]:
"""The node's last page hash, or None when it was never hashed."""
return node.get_last_hash_value()
def get_prefix_hash_values(self, node: Any) -> list[str]:
"""The hash chain of the node's ancestors, in root-to-parent order."""
return node.get_prefix_hash_values(node.parent)
@abstractmethod
def cache_finished_req(self, req: Req, is_insert: bool = True, **kwargs):
pass
@@ -0,0 +1,18 @@
"""Components of Unified Radix Cache.
- **TreeCore** (``unified_tree_core.UnifiedTreeCore``): the pure tree mechanism. Owns
the tree structure, per-node values, the LRU(s), bookkeeping, and KV-cache
events -- the only object that touches that state; no scheduling policy or IO,
and never touches the cache. References the cache-owned component drivers to
drive their tree-level hooks.
- **Components** (``unified_cache_components.TreeComponent`` subclasses): the
per-component (FULL/SWA/MAMBA) drivers. Built and owned by the Controller;
hold the cache for cache-level logic and the TreeCore for tree state.
- **Controller** (``unified_radix_cache.UnifiedRadixCache``): the scheduling brain
and scheduler-facing facade. Owns the scheduling policy (when to evict / backup /
load / prefetch), the IO (HiCache controller, device/host allocators, and the
device<->host transfer), the async ack machinery, prefetch/storage, the
distributed groups, and the component drivers; holds and drives the TreeCore.
TODO(Jialin): move all unified radix cache logic into this folder.
"""
@@ -0,0 +1,103 @@
"""A TreeCore emits CacheActions through the TreeCoreInterface to guide
Controller behavior.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
import msgspec
from sglang.srt.mem_cache.unified_cache.component_type import ComponentType
if TYPE_CHECKING:
import torch
from sglang.srt.mem_cache.unified_cache.unified_tree_core_interface import NodeId
class ReplaceWriteThroughOnNodeSplit(msgspec.Struct, frozen=True):
"""Replace the pending write-through node on a node split:
parent -> node => parent -> new_node -> new_child
old_node_id (the pre-split node) is replaced by new_node_id + new_child_node_id.
"""
ack_id: int
old_node_id: NodeId
new_node_id: NodeId
new_child_node_id: NodeId
class FreeDeviceKV(msgspec.Struct, frozen=True):
"""Free unreferenced device KV slots (a SWA-aware combined free)."""
indices: list[torch.Tensor]
class ComponentAction(msgspec.Struct, frozen=True):
"""Base for component-routed actions; the cache dispatches each one to
``component_type``'s class-level ``apply_component_action``; every subclass
declares its ``component_type`` field."""
class FreeComponentDeviceSlot(ComponentAction, frozen=True):
"""Free only the given ``component_type``'s device KV slots."""
indices: list[torch.Tensor]
component_type: ComponentType
class FreeComponentHostSlot(ComponentAction, frozen=True):
"""Free the given ``component_type``'s host KV pages."""
host_indices: list[torch.Tensor]
component_type: ComponentType
class BackupKV(msgspec.Struct, frozen=True):
"""Back up node_ids device->host in order, stopping at the first failure; write-through
ids form a contiguous root-first parent chain (each id's parent precedes it), write-back
actions carry a single eviction victim."""
node_ids: list[NodeId]
class MambaEvictExcessPathStates(ComponentAction, frozen=True):
"""Per-path Mamba state-cap eviction from the tail's root path; applied at
the insert's commit barrier, after the walk-time backups whose
write-through locks shield the backed-up chain."""
tail_node_id: NodeId
component_type: ComponentType = ComponentType.MAMBA
class RebuildFullToSWAMapping(ComponentAction, frozen=True):
"""Rebuild the SWA allocator's full->swa index mapping for loaded chunks."""
full_indices: list[torch.Tensor]
swa_indices: list[torch.Tensor]
component_type: ComponentType = ComponentType.SWA
class RecoverSWAWithLockedFull(ComponentAction, frozen=True):
"""Recover an SWA tombstone whose full is locked: keep the locked full, remap it
onto the incoming full's SWA translation, and free only the incoming full."""
node_id: NodeId
kept_full: torch.Tensor
incoming_full: torch.Tensor
component_type: ComponentType = ComponentType.SWA
class SWARebuild(ComponentAction, frozen=True):
"""Rebuild a node's SWA value by translating its source full value, then store it."""
node_id: NodeId
source_value: torch.Tensor
component_type: ComponentType = ComponentType.SWA
# Cache-owned actions, applied by UnifiedRadixCache itself.
CacheAction = ReplaceWriteThroughOnNodeSplit | FreeDeviceKV | BackupKV
@@ -0,0 +1,29 @@
"""The per-attention component identity; a leaf module importable from anywhere."""
from enum import Enum
class ComponentType(int, Enum):
"""Integer enum so that per-node list/tuple storage can be indexed directly."""
FULL = 0
SWA = 1
MAMBA = 2
def __str__(self) -> str: # keep human-readable logging
return self.name.lower()
@property
def is_full(self) -> bool:
return self == ComponentType.FULL
@property
def is_swa(self) -> bool:
return self == ComponentType.SWA
@property
def is_mamba(self) -> bool:
return self == ComponentType.MAMBA
BASE_COMPONENT_TYPE = ComponentType.FULL
@@ -0,0 +1,73 @@
"""Registry for pluggable TreeCore implementations.
The unified cache constructs its TreeCore through `create_tree_core`, selected
by SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND (default "python"). To plug in a custom
implementation, register it under a string name via
`register_tree_core_backend(name, factory)`.
"""
from __future__ import annotations
from typing import TYPE_CHECKING, Callable, Optional
if TYPE_CHECKING:
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
from sglang.srt.mem_cache.unified_cache.component_type import ComponentType
from sglang.srt.mem_cache.unified_cache.unified_tree_core_interface import (
UnifiedTreeCoreInterface,
)
from sglang.srt.mem_cache.unified_cache_components import TreeComponent
TreeCoreFactory = Callable[
["CacheInitParams", "dict[ComponentType, TreeComponent]"],
"UnifiedTreeCoreInterface",
]
_TREE_CORE_REGISTRY: dict[str, TreeCoreFactory] = {}
def register_tree_core_backend(name: str, factory: TreeCoreFactory) -> None:
"""Register a TreeCore factory under `name`."""
if not name.strip():
raise ValueError(
f"register_tree_core_backend: name must be non-empty, got {name!r}"
)
if name in _TREE_CORE_REGISTRY:
raise ValueError(f"register_tree_core_backend: {name!r} is already registered")
_TREE_CORE_REGISTRY[name] = factory
def get_tree_core_factory(name: str) -> Optional[TreeCoreFactory]:
return _TREE_CORE_REGISTRY.get(name)
def registered_tree_core_backends() -> list[str]:
return list(_TREE_CORE_REGISTRY.keys())
def _python_tree_core_factory(
params: CacheInitParams, components: dict[ComponentType, TreeComponent]
) -> UnifiedTreeCoreInterface:
"""The pure-Python TreeCore."""
from sglang.srt.mem_cache.unified_cache.unified_tree_core import UnifiedTreeCore
return UnifiedTreeCore(params, components)
register_tree_core_backend("python", _python_tree_core_factory)
def create_tree_core(
name: str,
params: CacheInitParams,
components: dict[ComponentType, TreeComponent],
) -> UnifiedTreeCoreInterface:
"""Construct the TreeCore registered under `name`."""
factory = get_tree_core_factory(name)
if factory is None:
raise ValueError(
f"SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND={name!r} is not registered. "
f"Registered backends: {registered_tree_core_backends()}. "
"External backends must call register_tree_core_backend(...) at import time."
)
return factory(params, components)
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,485 @@
"""Interface of the TreeCore: the radix tree mechanism that owns the tree
structure, per-node values, the LRU(s), and bookkeeping. The Controller drives a
TreeCore exclusively through this surface, so an alternative implementation (e.g.
a Rust TreeCore) can satisfy it without subclassing the Python tree.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from collections import defaultdict
from typing import TYPE_CHECKING, Optional, Sequence
import msgspec
from sglang.srt.mem_cache.events import KVCacheEventMixin
# Tree node id -- the node handle used outside the TreeCore. The concrete tree
# node is a TreeCore-internal type.
NodeId = int
class BaseEvictionResult(msgspec.Struct):
"""Base of the eviction step results: the component-keyed device/host
values the step freed, for the Controller to drain right after the call
returns, plus `tracker` -- the step's per-component evicted counts for the
Controller to accumulate."""
device_frees: dict[ComponentType, list[torch.Tensor]] = msgspec.field(
default_factory=lambda: defaultdict(list)
)
host_frees: dict[ComponentType, list[torch.Tensor]] = msgspec.field(
default_factory=lambda: defaultdict(list)
)
tracker: dict[ComponentType, int] = msgspec.field(
default_factory=lambda: defaultdict(int)
)
def __del__(self) -> None:
# Drop tripwire: every returned value must be drained before disposal.
assert (
not self.device_frees and not self.host_frees
), "BaseEvictionResult dropped with undrained values"
class EvictDeviceNextNodeResult(BaseEvictionResult):
node_id: Optional[NodeId] = None
class EvictDeviceLeafResult(BaseEvictionResult):
backup_kv: Optional[BackupKV] = None
class DemoteResult(BaseEvictionResult):
pass
class DropSubtreeNoHostResult(BaseEvictionResult):
is_dropped: bool = False
class DriveHostEvictionResult(BaseEvictionResult):
pass
class DecSwaLockOnlyResult(BaseEvictionResult):
pass
class RadixCacheWalkResult(msgspec.Struct, frozen=True, kw_only=True):
"""Flat (slot, position, prev-slot) rows emitted by the KV-canary walk."""
slot_indices: torch.Tensor
positions: torch.Tensor
prev_slot_indices: torch.Tensor
class InsertStepResult(msgspec.Struct, frozen=True):
"""One step of a resumable insert: the Controller executes ``actions``, then
resumes while ``result`` is None; ``result`` is set on the final step."""
actions: list[CacheAction | ComponentAction]
result: Optional[InsertResult] = None
if TYPE_CHECKING:
import torch
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.mem_cache.base_prefix_cache import (
DecLockRefParams,
DecLockRefResult,
IncLockRefResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
from sglang.srt.mem_cache.hicache_storage import PoolTransfer, PoolTransferResult
from sglang.srt.mem_cache.radix_cache import RadixKey
from sglang.srt.mem_cache.unified_cache.cache_action import (
BackupKV,
CacheAction,
ComponentAction,
)
from sglang.srt.mem_cache.unified_cache.unified_tree_core import (
StorageBackupSpec,
UnifiedTreeNode,
)
from sglang.srt.mem_cache.unified_cache_components import (
CacheTransferPhase,
ComponentType,
)
class UnifiedTreeCoreInterface(KVCacheEventMixin, ABC):
"""Methods the Controller invokes on the Tree Core. The Controller treats the
Tree Core as opaque behind this surface, which grows as tree operations
migrate onto the TreeCore. Inherits KVCacheEventMixin for the KV-event API
(take_events, _record_* recorders)."""
# ==== Tree-owned state the Controller reads (or, via its facade setters, writes) ====
page_size: int
is_eagle: bool
device: torch.device
enable_hicache: bool
enable_storage: bool
write_through_threshold: int
is_write_back: bool
has_swa_host_pool: bool
# ==== Tree API ====
@abstractmethod
def reset(self) -> None:
"""Drop the entire tree and reinitialize empty state."""
...
@abstractmethod
def node_by_id(self, node_id: NodeId) -> UnifiedTreeNode:
"""Resolve a NodeId -- the tree-node identity the Controller passes
across the boundary (e.g. MatchResult fields, lock-ref args) -- back to
its tree node.
TODO(Jialin): Remove after the Unified Radix Cache split.
"""
...
@abstractmethod
def is_backuped(self, node_id: NodeId) -> bool:
"""Whether the node's KV is already backed up to host."""
...
@abstractmethod
def is_root(self, node_id: NodeId) -> bool:
"""Whether the node is the tree root."""
...
@abstractmethod
def get_last_hash_value(self, node_id: NodeId) -> Optional[str]:
"""The node's last page hash, or None when it was never hashed."""
...
@abstractmethod
def get_prefix_hash_values(self, node_id: NodeId) -> list[str]:
"""The hash chain of the node's ancestors, in root-to-parent order."""
...
@abstractmethod
def inc_lock_ref(self, node_id: NodeId) -> IncLockRefResult:
"""Bump the reference count on a node's component locks."""
...
@abstractmethod
def dec_lock_ref(
self,
node_id: NodeId,
params: Optional[DecLockRefParams] = None,
skip_swa: bool = False,
) -> DecLockRefResult:
"""Decrease the reference count on a node's component locks."""
...
@abstractmethod
def dec_swa_lock_only(
self, node_id: NodeId, swa_uuid_for_lock: Optional[int]
) -> DecSwaLockOnlyResult:
"""Decrease only the SWA (and lower-priority co-located) reference
counts; the result carries the freed slots."""
...
# ==== Device eviction (driven step-wise by the Controller's evict()) ====
@abstractmethod
def evict_device_start(
self, component_type: ComponentType, request_cnt: int
) -> None:
"""Begin a device-eviction walk for one component."""
...
@abstractmethod
def evict_device_next_node(
self, component_type: ComponentType, tracker: dict[ComponentType, int]
) -> EvictDeviceNextNodeResult:
"""The next evictable node (None node_id when the walk is exhausted);
tracker is the caller's running totals, read for the doneness check."""
...
@abstractmethod
def evict_device_leaf(
self, node_id: NodeId, is_write_back: bool
) -> EvictDeviceLeafResult:
"""Evict a leaf's device value; the result carries a BackupKV when a
D->H backup must run (write_back) before the node can be demoted."""
...
@abstractmethod
def drop_subtree_no_host(self, node_id: NodeId) -> DropSubtreeNoHostResult:
"""Drop an unbacked D-leaf's subtree when its write-back backup failed
under host pressure; declines (is_dropped=False) if any node is locked."""
...
@abstractmethod
def demote(self, node_id: NodeId) -> DemoteResult:
"""Demote a backed-up node: drop its device value after a successful backup."""
...
@abstractmethod
def evict_device_end(self, component_type: ComponentType) -> None:
"""Finish the component's device-eviction walk."""
...
@abstractmethod
def inc_host_lock_ref(self, node_id: NodeId) -> IncLockRefResult:
"""Bump the reference count on a node's host-side component locks."""
...
@abstractmethod
def dec_host_lock_ref(
self, node_id: NodeId, params: Optional[DecLockRefParams] = None
) -> DecLockRefResult:
"""Decrease the reference count on a node's host-side component locks."""
...
@abstractmethod
def evictable_size(self) -> int: ...
@abstractmethod
def protected_size(self) -> int: ...
@abstractmethod
def component_evictable_size(self, component_type: ComponentType) -> int:
"""Evictable token count for one component (0 if the component is absent)."""
...
@abstractmethod
def full_evictable_size(self) -> int: ...
@abstractmethod
def full_protected_size(self) -> int: ...
@abstractmethod
def swa_evictable_size(self) -> int: ...
@abstractmethod
def mamba_evictable_size(self) -> int: ...
@abstractmethod
def swa_protected_size(self) -> int: ...
@abstractmethod
def mamba_protected_size(self) -> int: ...
@abstractmethod
def total_size(self) -> tuple[int, int]:
"""(full_tokens, aux_tokens) summed across the whole tree."""
...
@abstractmethod
def all_values_flatten(self) -> torch.Tensor: ...
@abstractmethod
def all_mamba_values_flatten(self) -> torch.Tensor: ...
@abstractmethod
def walk_for_kv_canary(
self, unlocked_only: bool, swa_resident_only: bool
) -> RadixCacheWalkResult:
"""Flatten every FULL device slot into (slot, position, prev-slot) rows
for the KV-canary sweep."""
...
@abstractmethod
def match_prefix(self, params: MatchPrefixParams) -> MatchResult:
"""Match a key against the tree; returns device indices + boundary NodeIds."""
...
@property
@abstractmethod
def empty_match_result(self) -> MatchResult:
"""A shared empty MatchResult (empty device indices + boundary NodeIds)."""
...
@abstractmethod
def is_full_device_evicted(self, node_id: NodeId) -> bool:
"""Whether the node's FULL device value has been evicted."""
...
@abstractmethod
def collect_full_device_indices(
self, from_node_id: NodeId, until_node_id: NodeId
) -> torch.Tensor:
"""Concatenate FULL device values from from_node up to (exclusive) until_node."""
...
@abstractmethod
def begin_insert(self, params: InsertParams) -> InsertStepResult:
"""Start the (single-flight) insert, running to its first barrier or completion."""
...
@abstractmethod
def resume_insert(self) -> InsertStepResult:
"""Continue the suspended insert after its step actions were executed."""
...
@abstractmethod
def has_ongoing_insert(self) -> bool:
"""Whether an insert walk is suspended at a barrier."""
...
@abstractmethod
def end_insert(self) -> list[CacheAction | ComponentAction]:
"""Finish the insert (idempotent); returns still-pending actions to drain."""
...
@abstractmethod
def drive_host_eviction(
self, component_type: ComponentType, num_tokens: int
) -> DriveHostEvictionResult:
"""Evict a component's host-side resources; no-op if the component is absent."""
...
# ==== HiCache ====
@abstractmethod
def set_hicache_enabled(self) -> None:
"""Mark the host tier (HiCache) as wired."""
...
@abstractmethod
def insert_host(
self,
node_id: NodeId,
key: RadixKey,
host_value: torch.Tensor,
hash_value: list[str],
) -> InsertResult:
"""Insert a host-side (backuped) tree path descending from the given node."""
...
@abstractmethod
def build_backup_spec(
self, node_id: NodeId
) -> tuple[torch.Tensor, dict[ComponentType, list[PoolTransfer]]]:
"""Read a node's device->host backup spec (device value + transfers) now."""
...
@abstractmethod
def build_storage_backup_spec(
self, node_id: NodeId, pass_prefix_keys: bool
) -> Optional[StorageBackupSpec]:
"""Gather a node's device->storage backup spec; None if not backuped."""
...
@abstractmethod
def build_hicache_transfers(
self,
component_type: ComponentType,
node_id: NodeId,
phase: CacheTransferPhase,
*,
host_indices: Optional[torch.Tensor] = None,
token_ids: Optional[Sequence[int]] = None,
prefetch_tokens: int = 0,
last_hash: Optional[str] = None,
) -> Optional[list[PoolTransfer]]:
"""Build a component's HiCache transfers for the given node and phase."""
...
@abstractmethod
def build_load_back_spec(
self, node_id: NodeId, req: Optional[Req] = None
) -> tuple[PoolTransfer, dict[ComponentType, list[PoolTransfer]]]:
"""Build the H->D load-back KV transfer plus per-component aux transfers."""
...
@abstractmethod
def prefetch_anchor_info(self, node_id: NodeId) -> Optional[str]:
"""The anchor node's key extra_key."""
...
@abstractmethod
def commit_hicache_transfers(
self,
node_id: NodeId,
phase: CacheTransferPhase,
comp_xfers: dict[ComponentType, list[PoolTransfer]],
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
"""Commit each component's HiCache transfers onto the node."""
...
@abstractmethod
def commit_backup(
self,
node_id: NodeId,
host_indices: torch.Tensor,
comp_xfers: dict[ComponentType, list[PoolTransfer]],
) -> None:
"""Commit a successful backup to the node."""
...
@abstractmethod
def commit_load_back(
self,
node_id: NodeId,
device_indices: torch.Tensor,
kv_xfer: PoolTransfer,
comp_xfers: dict[ComponentType, list[PoolTransfer]],
) -> list[CacheAction | ComponentAction]:
"""Commit a successful H->D load-back onto the node; returns any cache actions."""
...
@abstractmethod
def mark_write_through_pending(self, node_id: NodeId) -> None:
"""Mark a node as having an in-flight write-through backup."""
...
@abstractmethod
def finish_write_through(self, node_ids: list[NodeId], ack_id: int) -> None:
"""Clear the write-through-pending mark (when it matches ack_id) and record the
host store event for each acked node."""
...
@abstractmethod
def set_component_device_value(
self, node_id: NodeId, component_type: ComponentType, value: torch.Tensor
) -> None:
"""Store an auxiliary (non-Full) component's device value onto a node."""
...
@abstractmethod
def get_component_device_value(
self, node_id: NodeId, component_type: ComponentType
) -> Optional[torch.Tensor]:
"""The component's device value on the node, or None if evicted."""
...
@abstractmethod
def component_has_host_value_only(
self, node_id: NodeId, component_type: ComponentType
) -> bool:
"""Whether the component's data is device-evicted but host-backed."""
...
# ==== Others ====
@abstractmethod
def sanity_check(
self,
ongoing_write_through: list[tuple[int, NodeId]],
ongoing_load_back: list[tuple[int, NodeId]],
) -> None:
"""Verify tree invariants and raise AssertionError on any violation.
ongoing_write_through/ongoing_load_back are (id, node_id) pairs for in-flight ops.
"""
...
@abstractmethod
def pretty_print(self) -> None:
"""Print the tree structure for debugging."""
...
@@ -17,6 +17,15 @@ A component-based, pluggable prefix cache framework for SGLang that unifies Full
│ UnifiedRadixCache │
│ (unified_radix_cache.py) │
│ │
│ controller: executes all pool/host I/O and │
│ drains the tree's deferred Cache/Component │
│ Actions ("tree decides, cache executes") │
└──────────────────────┬────────────────────────┘
┌───────────────────────────────────────────────┐
│ UnifiedTreeCore │
│ (unified_cache/unified_tree_core.py) │
│ │
│ root_node ──► UnifiedTreeNode (radix tree) │
│ components ► {ComponentType → TreeComponent} │
│ lru_lists ─► {ComponentType → UnifiedLRUList}│
@@ -61,7 +70,10 @@ node.component_data[ComponentType.MAMBA] # MambaComponent data
| File | Contents |
|------|----------|
| `../unified_radix_cache.py` | `UnifiedRadixCache`, `UnifiedTreeNode`, `UnifiedLRUList` |
| `../unified_radix_cache.py` | `UnifiedRadixCache` — the controller: pool/host I/O, deferred-action draining |
| `../unified_cache/unified_tree_core.py` | `UnifiedTreeCore` — the tree, LRUs, and size counters; `UnifiedTreeNode`, `UnifiedLRUList` |
| `../unified_cache/unified_tree_core_interface.py` | `UnifiedTreeCoreInterface`, `NodeId` — the tree/cache boundary contract |
| `../unified_cache/cache_action.py` | Deferred `CacheAction`/`ComponentAction` types emitted by the tree |
| `tree_component.py` | `TreeComponent` ABC, `ComponentType`, `ComponentData`, `get_and_increase_time_counter`, `next_component_uuid` |
| `full_component.py` | `FullComponent` — standard full-attention KV cache component |
| `swa_component.py` | `SWAComponent` — sliding-window attention component with tombstone/window tracking |
@@ -99,7 +111,7 @@ Find the longest cached prefix for a token sequence.
- Promotes matched path to MRU in each component's LRU via `node_has_component_data()` as filter
- Updates `last_access_time` with decreasing timestamps up the path (parent < child)
- Concatenates matched device indices via `torch.cat` (concat length ≤ K, subsumed by O(K))
- Calls `finalize_match_result()` per component (Mamba performs copy-on-write: allocates new pool slot, copies SSM state)
- Calls `finalize_match_result_in_tree_core()` per component (tree-side: Full/SWA host-hit sums, Mamba `branching_seqlen`); the cache then routes the static `finalize_match_result_in_cache()` per component post-walk (Mamba performs copy-on-write: allocates new pool slot, copies SSM state)
---
@@ -143,7 +155,7 @@ Free cached tokens to reclaim memory.
| **Complexity** | **O(E·H + L)** — E = nodes evicted, H = tombstone chain height, L = locked nodes skipped in LRU scan. |
**Algorithm detail:**
1. Calls `drive_eviction()` for each component:
1. Drives each component's walk via `evict_device_start()` / `evict_device_next_node()` / `evict_device_end()`:
- Full: drives eviction from `evictable_device_leaves` using `last_access_time`; only device leaves are evicted atomically
- SWA: scans SWA LRU from tail; **internal** nodes are tombstoned (evict SWA data, keep node), **leaf** nodes are fully deleted; both trigger cascade
- Mamba: scans Mamba LRU from tail; **internal** nodes are tombstoned, **leaf** nodes are fully deleted; both trigger cascade
@@ -254,7 +266,8 @@ Each component implements these hooks. See `tree_component.py` for the ABC and d
| Hook | Purpose | Called By | Default |
|------|---------|-----------|----------|
| `create_match_validator(match_device_only=False)` | Return a per-match stateful predicate that decides whether a node is a valid match boundary. Full: requires Full device data, or host backup when `match_device_only=False`. SWA: tracks accumulated window length across device/host data. Mamba: requires Mamba device data, or host backup when `match_device_only=False`. | `_match_prefix_helper` | *abstract* |
| `finalize_match_result()` | Post-process the match result after prefix matching completes. Full/SWA: pass-through. Mamba: copy-on-write — allocates a new mamba pool slot, copies SSM state into the request pool, records `branching_seqlen`. | `_match_post_processor` | pass-through |
| `finalize_match_result_in_tree_core()` | Tree-side post-processing inside the match walk. Full/SWA: host-hit sums. Mamba: records `branching_seqlen` + the host-hit bump. | `_match_post_processor` | pass-through |
| `finalize_match_result_in_cache()` | Static, cache-level finalize after the walk (receives the cache + NodeId-based result), dispatched class-level by `UnifiedRadixCache.match_prefix`. Mamba: copy-on-write — allocates a new mamba pool slot, copies SSM state into the request pool. | `UnifiedRadixCache.match_prefix` | pass-through |
### Insert Phase
@@ -276,8 +289,8 @@ Each component implements these hooks. See `tree_component.py` for the ABC and d
|------|---------|-----------|----------|
| `evict_component(target=EvictLayer.DEVICE)` | Free this component's device, host, or both resources on a node being evicted. Internal device eviction tombstones (`value = None`); host eviction clears `host_value`. Returns `(device_freed, host_freed)`. | `_evict_component_and_detach_lru` | *abstract* |
| `eviction_priority()` | Return cascade eviction priority (higher = evicted later). Leaf: all 0. Internal: Full(2) > SWA(1) > Mamba(0). When evicting, all components with ≤ priority on the same node are cascade-evicted. | `_cascade_evict` | `0` |
| `drive_eviction()` | Drive device eviction until the target amount is freed. Full: leaf-set heap. SWA/Mamba: component LRUs with internal tombstones and atomic leaf deletion. | `evict` | *abstract* |
| `drive_host_eviction()` | Drive host eviction for this component. Full uses host leaves; SWA/Mamba use host LRUs. | `evict_host` | no-op |
| `evict_device_start()` / `evict_device_next_node()` / `evict_device_end()` | Step-wise device eviction walk the Controller drives: build the cursor/heap, return the next evictable leaf (freed values collected for the Controller to drain), clear the walk. Full: leaf-set heap. SWA/Mamba: component LRUs with internal tombstones and atomic leaf deletion. | `UnifiedRadixCache._evict_components` | *abstract* |
| `drive_host_eviction()` | Drive host eviction for this component, collecting freed values for the Controller to drain. Full uses host leaves; SWA/Mamba use host LRUs. | `evict_host` | no-op |
### Lock Phase
@@ -10,6 +10,7 @@ from sglang.srt.mem_cache.unified_cache_components.tree_component import (
EvictLayer,
LRURefreshPhase,
PrepareLoadBackResult,
PreparePrefetchResult,
TreeComponent,
get_and_increase_time_counter,
next_component_uuid,
@@ -25,6 +26,7 @@ __all__ = [
"LRURefreshPhase",
"MambaComponent",
"PrepareLoadBackResult",
"PreparePrefetchResult",
"SWAComponent",
"TreeComponent",
"_NUM_COMPONENT_TYPES",
@@ -7,7 +7,6 @@ import torch
from sglang.srt.mem_cache.base_prefix_cache import (
DecLockRefParams,
EvictParams,
IncLockRefResult,
InsertResult,
MatchPrefixParams,
@@ -18,6 +17,7 @@ from sglang.srt.mem_cache.hicache_storage import (
PoolTransfer,
PoolTransferResult,
)
from sglang.srt.mem_cache.unified_cache.cache_action import FreeComponentDeviceSlot
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
CacheTransferPhase,
ComponentType,
@@ -26,8 +26,12 @@ from sglang.srt.mem_cache.unified_cache_components.tree_component import (
)
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import Req
from sglang.srt.mem_cache.unified_cache.cache_action import (
CacheAction,
ComponentAction,
)
from sglang.srt.mem_cache.unified_radix_cache import (
NodeId,
UnifiedTreeNode,
)
@@ -37,13 +41,6 @@ class FullComponent(TreeComponent):
def __init__(self, cache, params):
super().__init__(cache, params)
allocator = cache.token_to_kv_pool_allocator
# When SWA is present, only free full-attention KV here;
# SWA KV will be freed by cascade via SWAComponent.evict_component.
if ComponentType.SWA in cache.tree_components:
self._free_full = allocator.full_attn_allocator.free
else:
self._free_full = allocator.free
# HiCache state: set to host KV pool when HiCache enabled
self._full_kv_pool_host = None
@@ -60,7 +57,7 @@ class FullComponent(TreeComponent):
node.component_data[self.component_type].value is not None or node.backuped
)
def finalize_match_result(
def finalize_match_result_in_tree_core(
self,
result: MatchResult,
params: MatchPrefixParams,
@@ -72,7 +69,7 @@ class FullComponent(TreeComponent):
ct = self.component_type
kv_host_hit = 0
node = result.best_match_node
root_node = self.cache.root_node
root_node = self.tree_core.root_node
while node is not result.last_device_node and node is not root_node:
full_host = node.component_data[ct].host_value
if full_host is not None:
@@ -103,6 +100,8 @@ class FullComponent(TreeComponent):
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]
@@ -111,9 +110,9 @@ class FullComponent(TreeComponent):
# Device layer
if EvictLayer.DEVICE in target and cd.value is not None:
self._free_full(cd.value)
device_frees[self.component_type].append(cd.value)
freed = len(cd.value)
self.cache.component_evictable_size_[self.component_type] -= freed
self.tree_core.component_evictable_size_[self.component_type] -= freed
# NOTE: cd.value = None is deferred to _cascade_evict (Full as trigger)
# because SWA's free_swa still needs to read Full.value.
# cd.value = None
@@ -121,54 +120,78 @@ class FullComponent(TreeComponent):
# Host layer
if EvictLayer.HOST in target and cd.host_value is not None:
host_freed = len(cd.host_value)
if self._full_kv_pool_host is not None:
self._full_kv_pool_host.free(cd.host_value)
host_frees[self.component_type].append(cd.host_value)
cd.host_value = None
return freed, host_freed
def eviction_priority(self, is_leaf: bool) -> int:
return 0 if is_leaf else 2
def drive_eviction(
self, params: EvictParams, tracker: dict[ComponentType, int]
) -> None:
request = params.num_tokens
heap = [
(self.cache.eviction_strategy.get_priority(n), n)
for n in self.cache.evictable_device_leaves
def _evict_device_start(self, request_cnt: int) -> None:
self._evict_device_request_cnt = request_cnt
self._evict_device_last_node = None
self._evict_device_heap = [
(self.tree_core.eviction_strategy.get_priority(n), n)
for n in self.tree_core.evictable_device_leaves
]
heapq.heapify(heap)
heapq.heapify(self._evict_device_heap)
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]:
ct = self.component_type
while tracker[ct] < request and heap:
_, x = heapq.heappop(heap)
if x not in self.cache.evictable_device_leaves:
lv = self._evict_device_last_node
if (
lv is not None
and lv.parent is not None
and lv.parent in self.tree_core.evictable_device_leaves
):
heapq.heappush(
self._evict_device_heap,
(self.tree_core.eviction_strategy.get_priority(lv.parent), lv.parent),
)
self._evict_device_last_node = None
while tracker[ct] < self._evict_device_request_cnt and self._evict_device_heap:
_, x = heapq.heappop(self._evict_device_heap)
if x not in self.tree_core.evictable_device_leaves:
continue
self.cache._evict_device_leaf(x, tracker)
if x.parent is not None and x.parent in self.cache.evictable_device_leaves:
heapq.heappush(
heap,
(self.cache.eviction_strategy.get_priority(x.parent), x.parent),
)
self._evict_device_last_node = x
return x.id
return None
def _evict_device_end(self) -> None:
self._evict_device_heap = []
self._evict_device_last_node = None
def drive_host_eviction(
self, num_tokens: int, tracker: dict[ComponentType, int]
self,
num_tokens: int,
tracker: dict[ComponentType, int],
device_frees: dict[ComponentType, list[torch.Tensor]],
host_frees: dict[ComponentType, list[torch.Tensor]],
) -> None:
"""Evict host leaves to free KV host pool space."""
heap = [
(self.cache.eviction_strategy.get_priority(n), n)
for n in self.cache.evictable_host_leaves
(self.tree_core.eviction_strategy.get_priority(n), n)
for n in self.tree_core.evictable_host_leaves
]
heapq.heapify(heap)
ct = self.component_type
while tracker[ct] < num_tokens and heap:
_, x = heapq.heappop(heap)
if x not in self.cache.evictable_host_leaves:
if x not in self.tree_core.evictable_host_leaves:
continue
self.cache._evict_host_leaf(x, tracker)
if x.parent is not None and x.parent in self.cache.evictable_host_leaves:
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
if (
x.parent is not None
and x.parent in self.tree_core.evictable_host_leaves
):
heapq.heappush(
heap,
(self.cache.eviction_strategy.get_priority(x.parent), x.parent),
(self.tree_core.eviction_strategy.get_priority(x.parent), x.parent),
)
def acquire_component_lock(
@@ -182,15 +205,14 @@ class FullComponent(TreeComponent):
# Only the last host node needs to be protected.
if lock_host:
cd = node.component_data[ct]
# write_back mode: the anchor may be device-only (no host_value);
# pin it anyway so the host-pressure drop fallback cannot delete it.
if cd.host_value is None and not self.cache.is_write_back:
# write_back mode: the anchor may be device-only (no host_value); pin it anyway.
if cd.host_value is None and not self.tree_core.is_write_back:
return result
cd.host_lock_ref += 1
self.cache._update_evictable_leaf_sets(node)
self.tree_core._update_evictable_leaf_sets(node)
return result
root = self.cache.root_node
root = self.tree_core.root_node
cur = node
# Skip the bottom evicted segment
@@ -207,11 +229,11 @@ class FullComponent(TreeComponent):
), f"FULL invariant broken: evicted ancestor {cur.id} above device-on segment"
if cd.lock_ref == 0:
key_len = len(cd.value)
self.cache.component_evictable_size_[ct] -= key_len
self.cache.component_protected_size_[ct] += key_len
self.tree_core.component_evictable_size_[ct] -= key_len
self.tree_core.component_protected_size_[ct] += key_len
delta += key_len
cd.lock_ref += 1
self.cache.evictable_device_leaves.discard(cur)
self.tree_core.evictable_device_leaves.discard(cur)
cur = cur.parent
result.delta = delta
return result
@@ -228,13 +250,13 @@ class FullComponent(TreeComponent):
if cd.host_lock_ref == 0:
return
# Mirror of `acquire`. write_back uses a pure counter.
if cd.host_value is None and not self.cache.is_write_back:
if cd.host_value is None and not self.tree_core.is_write_back:
return
cd.host_lock_ref -= 1
self.cache._update_evictable_leaf_sets(node)
self.tree_core._update_evictable_leaf_sets(node)
return
root = self.cache.root_node
root = self.tree_core.root_node
skip_lock_node_ids = params.skip_lock_node_ids.get(ct, ()) if params else ()
cur = node
while cur != root:
@@ -247,11 +269,11 @@ class FullComponent(TreeComponent):
if cd.lock_ref == 1:
key_len = len(cd.value)
self.cache.component_evictable_size_[ct] += key_len
self.cache.component_protected_size_[ct] -= key_len
self.tree_core.component_evictable_size_[ct] += key_len
self.tree_core.component_protected_size_[ct] -= key_len
cd.lock_ref -= 1
if cd.lock_ref == 0:
self.cache._update_evictable_leaf_sets(cur)
self.tree_core._update_evictable_leaf_sets(cur)
cur = cur.parent
# ---- HiCache Hooks ----
@@ -261,7 +283,8 @@ class FullComponent(TreeComponent):
node: UnifiedTreeNode,
phase: CacheTransferPhase,
*,
req: Optional[Req] = None,
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,
@@ -270,7 +293,7 @@ class FullComponent(TreeComponent):
if phase == CacheTransferPhase.BACKUP_HOST:
# Full KV backup is handled by the main flow
# (write_backup → cache_controller.write on host_value directly).
# (cache_controller.write on host_value directly).
# No extra PoolTransfer needed.
return None
@@ -297,7 +320,7 @@ class FullComponent(TreeComponent):
else torch.empty((0,), dtype=torch.int64, device="cpu")
),
device_indices=None,
nodes_to_load=nodes,
nodes_to_load=[n.id for n in nodes],
)
]
@@ -309,6 +332,7 @@ class FullComponent(TreeComponent):
phase: CacheTransferPhase,
transfers: list[PoolTransfer] = (),
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
@@ -320,19 +344,39 @@ class FullComponent(TreeComponent):
elif phase == CacheTransferPhase.LOAD_BACK:
if not transfers or transfers[0].device_indices is None:
self.cache._update_evictable_leaf_sets(node)
self.tree_core._update_evictable_leaf_sets(node)
return
xfer = transfers[0]
device_indices = xfer.device_indices
offset = 0
for n in xfer.nodes_to_load or []:
for nid in xfer.nodes_to_load or []:
n = self.tree_core.node_by_id(nid)
cd = n.component_data[ct]
n_len = len(cd.host_value)
cd.value = device_indices[offset : offset + n_len].clone()
offset += n_len
# Full uses leaf sets, not LRU
self.cache.component_evictable_size_[ct] += n_len
self.cache._update_evictable_leaf_sets(n)
self.tree_core.component_evictable_size_[ct] += n_len
self.tree_core._update_evictable_leaf_sets(n)
self.cache._update_evictable_leaf_sets(node)
self.tree_core._update_evictable_leaf_sets(node)
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
if self._full_kv_pool_host is None:
return
for host_value in host_values:
self._full_kv_pool_host.free(host_value)
def apply_component_action(self, action: ComponentAction) -> None:
if isinstance(action, FreeComponentDeviceSlot):
alloc = self.cache.token_to_kv_pool_allocator
for indices in action.indices:
if self.cache.is_swa_enabled:
alloc.full_attn_allocator.free(indices)
else:
alloc.free(indices)
return
raise AssertionError(
f"FullComponent: unhandled ComponentAction {type(action).__name__}"
)
@@ -1,5 +1,6 @@
from __future__ import annotations
from collections import defaultdict
from typing import TYPE_CHECKING, Callable, Optional, Sequence
import torch
@@ -19,12 +20,18 @@ from sglang.srt.mem_cache.hicache_storage import (
PoolTransfer,
PoolTransferResult,
)
from sglang.srt.mem_cache.unified_cache.cache_action import (
FreeComponentDeviceSlot,
FreeComponentHostSlot,
MambaEvictExcessPathStates,
)
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
CacheTransferPhase,
ComponentType,
EvictLayer,
LRURefreshPhase,
PrepareLoadBackResult,
PreparePrefetchResult,
TreeComponent,
get_and_increase_time_counter,
)
@@ -33,7 +40,12 @@ from sglang.srt.runtime_context import get_server_args
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,
)
@@ -46,15 +58,13 @@ class MambaComponent(TreeComponent):
from sglang.srt.mem_cache.memory_pool import HybridReqToTokenPool
assert isinstance(
cache.req_to_token_pool, HybridReqToTokenPool
), f"MambaComponent requires HybridReqToTokenPool, got {type(cache.req_to_token_pool)}"
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 (
cache.page_size == 1
), f"MambaComponent requires page_size=1 when mamba_extra_buffer is disabled, got {cache.page_size}"
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.enable_mamba_extra_buffer = params.enable_mamba_extra_buffer
self.enable_mamba_extra_buffer_lazy = params.enable_mamba_extra_buffer_lazy
self.mamba_cache_chunk_size = get_server_args().mamba_cache_chunk_size
self.mamba_max_states_per_path = get_server_args().mamba_max_states_per_path
# HiCache state
@@ -78,7 +88,7 @@ class MambaComponent(TreeComponent):
return
case LRURefreshPhase.MATCH_END:
if node.component_data[ct].value is not None:
self.cache.lru_lists[ct].reset_node_mru(node)
self.tree_core.lru_lists[ct].reset_node_mru(node)
case LRURefreshPhase.INSERT_END:
return
case _:
@@ -97,15 +107,13 @@ class MambaComponent(TreeComponent):
or node.component_data[ct].host_value is not None
)
def finalize_match_result(
def finalize_match_result_in_tree_core(
self,
result: MatchResult,
params: MatchPrefixParams,
value_chunks: list[torch.Tensor],
best_value_len: int,
) -> MatchResult:
cow_mamba = params.cow_mamba
req = params.req
last_node = result.best_match_node
mamba_boundary_len = len(result.device_indices) + result.host_hit_length
@@ -120,75 +128,102 @@ class MambaComponent(TreeComponent):
aligned_seqlen if aligned_seqlen > mamba_boundary_len else None
)
mamba_value = last_node.component_data[self.component_type].value
if cow_mamba and mamba_value is not None:
assert req is not None
if req.mamba_pool_idx is None:
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
if dst_index is None:
# Capture the inc result and thread swa_uuid_for_lock back
# into dec. Without it, SWA's release walks past this
# request's window boundary all the way to root and
# over-decrements SWA locks held by other resident requests
# on ancestor nodes.
lock_result = self.cache.inc_lock_ref(last_node)
self.cache.evict(EvictParams(num_tokens=0, mamba_num=1))
dst_index = self.cache.req_to_token_pool.mamba_allocator.alloc(1)
self.cache.dec_lock_ref(last_node, lock_result.to_dec_params())
assert dst_index is not None, "Can not alloc mamba cache"
req.mamba_pool_idx = dst_index[0]
req.mamba_cow_src_index = mamba_value
req.mamba_needs_clear = False
# HiCache: if mamba was evicted from device but has host backup,
# ensure mamba_host_hit_length >= 1 so load_back is triggered.
cd = last_node.component_data[self.component_type]
if cd.value is None and cd.host_value is not None:
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 req.mamba_pool_idx is None:
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(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.mamba_pool_idx = dst_index[0]
req.mamba_cow_src_index = src_index
req.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.cache.lru_lists[self.component_type].insert_mru(node)
self.cache.component_evictable_size_[self.component_type] += len(
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._evict_excess_path_states(node)
self._emit_excess_path_states_eviction(node, cache_actions)
return
if node.component_data[self.component_type].value is None:
node.component_data[self.component_type].value = params.mamba_value
# move from host LRU to device LRU
host_lru = self.cache.host_lru_lists[self.component_type]
host_lru = self.tree_core.host_lru_lists[self.component_type]
if host_lru.in_list(node):
host_lru.remove_node(node)
self.cache.lru_lists[self.component_type].insert_mru(node)
self.cache.component_evictable_size_[self.component_type] += len(
self.tree_core.lru_lists[self.component_type].insert_mru(node)
self.tree_core.component_evictable_size_[self.component_type] += len(
params.mamba_value
)
node.last_access_time = get_and_increase_time_counter()
self._evict_excess_path_states(node)
self._emit_excess_path_states_eviction(node, cache_actions)
return
self.cache.lru_lists[self.component_type].reset_node_mru(node)
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 _evict_excess_path_states(self, tail: UnifiedTreeNode) -> None:
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, and device leaves are preserved, so the cap is a
best-effort soft limit.
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:
@@ -197,7 +232,7 @@ class MambaComponent(TreeComponent):
ct = self.component_type
holders = []
node = tail
while node is not None and node is not self.cache.root_node:
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
@@ -212,12 +247,17 @@ class MambaComponent(TreeComponent):
break
if node.component_data[ct].lock_ref > 0 or len(node.children) != 1:
continue
if node in self.cache.evictable_device_leaves:
if node in self.tree_core.evictable_device_leaves:
continue
self.cache._evict_component_and_detach_lru(
node, self, target=EvictLayer.DEVICE, tracker=tracker
self.tree_core._evict_component_and_detach_lru(
node,
self,
device_frees,
host_frees,
target=EvictLayer.DEVICE,
tracker=tracker,
)
self.cache._cascade_evict(node, self, tracker)
self.tree_core._cascade_evict(node, self, tracker, device_frees, host_frees)
excess -= 1
def redistribute_on_node_split(
@@ -233,6 +273,8 @@ class MambaComponent(TreeComponent):
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]
@@ -241,17 +283,16 @@ class MambaComponent(TreeComponent):
# Device layer
if EvictLayer.DEVICE in target and cd.value is not None:
self._free_mamba_value(cd.value)
device_frees[self.component_type].append(cd.value)
freed = len(cd.value)
self.cache.component_evictable_size_[self.component_type] -= freed
self.tree_core.component_evictable_size_[self.component_type] -= freed
cd.value = None
# Host layer
host_lru = self.cache.host_lru_lists[self.component_type]
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)
if self._mamba_pool_host is not None:
self._mamba_pool_host.free(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)
@@ -267,30 +308,56 @@ class MambaComponent(TreeComponent):
return freed, host_freed
def drive_eviction(
self, params: EvictParams, tracker: dict[ComponentType, int]
) -> None:
request = params.mamba_num
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
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]:
"""Return the next device-leaf node for the driver to evict, or None.
Internal nodes are tombstoned inline (no IO); the cursor is re-validated
(reset to LRU head) if the previous node's eviction removed it."""
ct = self.component_type
lru = self.cache.lru_lists[ct]
x = lru.get_lru_no_lock()
while tracker[ct] < request and x is not None and lru.in_list(x):
lru = self.tree_core.lru_lists[ct]
if self._evict_device_cursor is not None and not lru.in_list(
self._evict_device_cursor
):
self._evict_device_cursor = lru.get_lru_no_lock()
while (
tracker[ct] < self._evict_device_request_cnt
and self._evict_device_cursor is not None
and lru.in_list(self._evict_device_cursor)
):
x = self._evict_device_cursor
assert x.component_data[ct].value is not None
if x in self.cache.evictable_device_leaves:
# D-leaf: atomic eviction of all components
x_next = lru.get_prev_no_lock(x)
self.cache._evict_device_leaf(x, tracker)
if not lru.in_list(x_next):
x_next = lru.get_lru_no_lock()
x = x_next
else:
# Internal: tombstone Mamba + cascade
x_next = lru.get_prev_no_lock(x)
self.cache._evict_component_and_detach_lru(
x, self, target=EvictLayer.DEVICE, tracker=tracker
)
self.cache._cascade_evict(x, self, tracker)
x = x_next
if x in self.tree_core.evictable_device_leaves:
self._evict_device_cursor = lru.get_prev_no_lock(x)
return x.id
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 = x_next
return None
def _evict_device_end(self) -> None:
"""Clear the device-eviction walk cursor state."""
self._evict_device_cursor = None
def acquire_component_lock(
self,
@@ -299,7 +366,7 @@ class MambaComponent(TreeComponent):
lock_host: bool = False,
) -> IncLockRefResult:
ct = self.component_type
if node is self.cache.root_node:
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
@@ -310,15 +377,15 @@ class MambaComponent(TreeComponent):
if lock_host:
if cd.host_lock_ref == 0:
host_lru = self.cache.host_lru_lists[ct]
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:
vlen = len(value)
self.cache.component_evictable_size_[ct] -= vlen
self.cache.component_protected_size_[ct] += vlen
self.tree_core.component_evictable_size_[ct] -= vlen
self.tree_core.component_protected_size_[ct] += vlen
cd.lock_ref += 1
return result
@@ -329,7 +396,7 @@ class MambaComponent(TreeComponent):
lock_host: bool = False,
) -> None:
ct = self.component_type
if node is self.cache.root_node:
if node is self.tree_core.root_node:
return
cd = node.component_data[ct]
skip_lock_node_ids = params.skip_lock_node_ids.get(ct, ()) if params else ()
@@ -340,7 +407,7 @@ class MambaComponent(TreeComponent):
if lock_host:
cd.host_lock_ref -= 1
if cd.host_lock_ref == 0 and cd.value is None and cd.host_value is not None:
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
if not host_lru.in_list(node):
host_lru.insert_mru(node)
return
@@ -348,8 +415,8 @@ class MambaComponent(TreeComponent):
if cd.lock_ref > 0:
if cd.lock_ref == 1:
vlen = len(value)
self.cache.component_evictable_size_[ct] += vlen
self.cache.component_protected_size_[ct] -= vlen
self.tree_core.component_evictable_size_[ct] += vlen
self.tree_core.component_protected_size_[ct] -= vlen
cd.lock_ref -= 1
def _alloc_mamba_slot(self) -> torch.Tensor:
@@ -395,7 +462,7 @@ class MambaComponent(TreeComponent):
token_ids_len: int,
is_finished: bool,
) -> Optional[int]:
if self.enable_mamba_extra_buffer:
if self.cache.enable_mamba_extra_buffer:
cache_len = req.mamba_last_track_seqlen
else:
cache_len = token_ids_len
@@ -416,7 +483,7 @@ class MambaComponent(TreeComponent):
if is_finished:
if cache_len is None:
cache_len = 0
if self.enable_mamba_extra_buffer:
if self.cache.enable_mamba_extra_buffer:
keep_idx = self.cache.req_to_token_pool.get_mamba_ping_pong_keep_idx(
req
)
@@ -435,7 +502,7 @@ class MambaComponent(TreeComponent):
return 0
# Donate the mamba index to the radix cache instead of copying.
if self.int8_ckpt_pool is not None:
if self.enable_mamba_extra_buffer:
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(
@@ -448,7 +515,7 @@ class MambaComponent(TreeComponent):
mamba_value_donated = self._commit_int8_checkpoint(
req.mamba_pool_idx.view(-1)
)
elif self.enable_mamba_extra_buffer:
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(
@@ -491,7 +558,7 @@ class MambaComponent(TreeComponent):
pool.free_mamba_cache(req)
return
if self.enable_mamba_extra_buffer:
if self.cache.enable_mamba_extra_buffer:
keep_idx = (
pool.get_mamba_ping_pong_keep_idx(req)
if mamba_value_inserted
@@ -515,17 +582,16 @@ class MambaComponent(TreeComponent):
def prepare_load_back(
self,
node: UnifiedTreeNode,
node_id: NodeId,
*,
req: Optional[Req] = None,
) -> PrepareLoadBackResult:
cd = node.component_data[self.component_type]
# skip unless the node needs a load-back (device value absent), like build_hicache_transfers
if (
req is None
or req.mamba_pool_idx is not None
or cd.host_value is None
or cd.value is not None
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)
@@ -544,12 +610,27 @@ class MambaComponent(TreeComponent):
self.cache.req_to_token_pool.mamba_allocator.free(prep.allocated_mamba_slot)
req.mamba_pool_idx = None
def prepare_prefetch(
self,
node_id: NodeId,
*,
prefetch_tokens: int = 0,
) -> PreparePrefetchResult:
host_indices = self._mamba_pool_host.alloc(1)
if host_indices is None:
self.cache.evict_host(1, ComponentType.MAMBA)
host_indices = self._mamba_pool_host.alloc(1)
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,
*,
req: Optional[Req] = None,
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,
@@ -580,19 +661,18 @@ class MambaComponent(TreeComponent):
PoolTransfer(
name=PoolName.MAMBA,
host_indices=cd.host_value,
nodes_to_load=[node],
nodes_to_load=[node.id],
)
)
# Per-request mamba CoW: H→D copy into the request's device slot allocated by prepare_load_back.
# 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 req is not None and cd.host_value is not None:
assert req.mamba_pool_idx is not None
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=req.mamba_pool_idx.unsqueeze(0),
device_indices=mamba_pool_idx.unsqueeze(0),
)
)
@@ -612,12 +692,7 @@ class MambaComponent(TreeComponent):
]
if phase == CacheTransferPhase.PREFETCH:
host_indices = self._mamba_pool_host.alloc(1)
if host_indices is None:
self.cache.evict_host(1, ComponentType.MAMBA)
host_indices = self._mamba_pool_host.alloc(1)
if host_indices is None:
return []
assert host_indices is not None
return [
PoolTransfer(
name=PoolName.MAMBA,
@@ -635,6 +710,7 @@ class MambaComponent(TreeComponent):
phase: CacheTransferPhase,
transfers: list[PoolTransfer] = (),
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
@@ -655,11 +731,11 @@ class MambaComponent(TreeComponent):
cd.value = transfer.device_indices.clone()
count = len(cd.value)
# Move from host LRU to device LRU
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
if host_lru.in_list(node):
host_lru.remove_node(node)
self.cache.lru_lists[ct].insert_mru(node)
self.cache.component_evictable_size_[ct] += count
self.tree_core.lru_lists[ct].insert_mru(node)
self.tree_core.component_evictable_size_[ct] += count
elif phase == CacheTransferPhase.PREFETCH:
if not transfers:
@@ -671,7 +747,10 @@ class MambaComponent(TreeComponent):
and pool_storage_result.extra_pool_hit_pages.get(PoolName.MAMBA, 0) >= 1
)
target_node = (
insert_result.inserted_host_node if insert_result is not None else None
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
@@ -679,8 +758,10 @@ class MambaComponent(TreeComponent):
or not loaded
or target_node.component_data[ct].host_value is not None
):
self.cache.cache_controller.append_host_mem_release(
extra_pools=[transfer]
cache_actions.append(
FreeComponentHostSlot(
[host_indices], component_type=ComponentType.MAMBA
)
)
if insert_result is not None:
insert_result.mamba_exist = True
@@ -688,33 +769,86 @@ class MambaComponent(TreeComponent):
target_node.component_data[ct].host_value = host_indices.clone()
if target_node.component_data[ct].value is None:
host_lru = self.cache.host_lru_lists[ct]
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]
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.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
x = host_lru.get_lru_no_host_lock()
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
x_next = host_lru.get_prev_no_host_lock(x)
cd = x.component_data[ct]
if x in self.cache.evictable_host_leaves:
if x in self.tree_core.evictable_host_leaves:
# Host leaf: atomic eviction (all components host + delete)
self.cache._evict_host_leaf(x, tracker)
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
else:
# Internal: tombstone Mamba + cascade
assert cd.host_value is not None
self.cache._evict_component_and_detach_lru(
x, self, target=EvictLayer.HOST, tracker=tracker
self.tree_core._evict_component_and_detach_lru(
x,
self,
target=EvictLayer.HOST,
tracker=tracker,
device_frees=device_frees,
host_frees=host_frees,
)
self.cache._cascade_evict(x, self, tracker, target=EvictLayer.HOST)
self.cache._update_evictable_leaf_sets(x)
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)
x = x_next
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._mamba_pool_host.free(host_value)
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.
try:
self._evict_excess_path_states(
self.tree_core.node_by_id(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__}"
)
@@ -6,7 +6,6 @@ import torch
from sglang.srt.mem_cache.base_prefix_cache import (
DecLockRefParams,
EvictParams,
IncLockRefResult,
InsertParams,
InsertResult,
@@ -20,12 +19,21 @@ from sglang.srt.mem_cache.hicache_storage import (
PoolTransfer,
PoolTransferResult,
)
from sglang.srt.mem_cache.unified_cache.cache_action import (
FreeComponentDeviceSlot,
FreeComponentHostSlot,
FreeDeviceKV,
RebuildFullToSWAMapping,
RecoverSWAWithLockedFull,
SWARebuild,
)
from sglang.srt.mem_cache.unified_cache_components.tree_component import (
BASE_COMPONENT_TYPE,
CacheTransferPhase,
ComponentType,
EvictLayer,
LRURefreshPhase,
PreparePrefetchResult,
TreeComponent,
next_component_uuid,
)
@@ -33,7 +41,12 @@ from sglang.srt.mem_cache.unified_cache_components.tree_component import (
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,
)
@@ -53,8 +66,8 @@ class SWAComponent(TreeComponent):
from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator
assert isinstance(
cache.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator
), f"SWAComponent requires SWATokenToKVPoolAllocator, got {type(cache.token_to_kv_pool_allocator)}"
params.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator
), f"SWAComponent requires SWATokenToKVPoolAllocator, got {type(params.token_to_kv_pool_allocator)}"
super().__init__(cache, params)
self.sliding_window_size = params.sliding_window_size
# HiCache state: set to host SWA pool when HiCache enabled
@@ -81,39 +94,17 @@ class SWAComponent(TreeComponent):
# MATCH_END / INSERT_END instead.
return
case LRURefreshPhase.MATCH_END | LRURefreshPhase.INSERT_END:
self.cache.lru_lists[
self.tree_core.lru_lists[
self.component_type
].reset_node_and_window_ancestors_mru(
node,
root_node,
self.sliding_window_size + self.cache.page_size,
self.sliding_window_size + self.tree_core.page_size,
self.node_has_component_data,
)
case _:
raise ValueError(f"Unknown LRURefreshPhase: {phase}")
def _restore_device_value(self, node: UnifiedTreeNode, value: torch.Tensor) -> None:
ct = self.component_type
node.component_data[ct].value = value
host_lru = self.cache.host_lru_lists[ct]
if host_lru.in_list(node):
host_lru.remove_node(node)
self.cache.lru_lists[ct].insert_mru(node)
self.cache.component_evictable_size_[ct] += len(value)
def _restore_device_value_with_locked_full(
self,
node: UnifiedTreeNode,
full_value: torch.Tensor,
incoming_full_value: torch.Tensor,
) -> None:
allocator = self.cache.token_to_kv_pool_allocator
swa_value = self._translate_full_to_swa(incoming_full_value)
allocator.set_full_to_swa_mapping(full_value, swa_value)
allocator.full_to_swa_index_mapping[incoming_full_value.to(torch.int64)] = 0
allocator.full_attn_allocator.free(incoming_full_value)
self._restore_device_value(node, swa_value)
def create_match_validator(
self, match_device_only: bool = False
) -> Callable[[UnifiedTreeNode], bool]:
@@ -124,7 +115,7 @@ class SWAComponent(TreeComponent):
# unified_kv never caches the SWA ring (per-request, not content-stable),
# so SWA bookkeeping must not gate the match here.
swa_device_only_hicache = (
self._swa_kv_pool_host is None and self.cache.cache_controller is not None
not self.tree_core.has_swa_host_pool and self.tree_core.enable_hicache
)
def validator(node: UnifiedTreeNode) -> bool:
@@ -141,7 +132,7 @@ class SWAComponent(TreeComponent):
return validator
def finalize_match_result(
def finalize_match_result_in_tree_core(
self,
result: MatchResult,
params: MatchPrefixParams,
@@ -152,7 +143,7 @@ class SWAComponent(TreeComponent):
n_swa = 0
swa_host_hit = 0
node = result.best_match_node
root = self.cache.root_node
root = self.tree_core.root_node
while node is not root and n_swa < self.sliding_window_size:
cd = node.component_data[ct]
if cd.value is not None:
@@ -182,6 +173,7 @@ class SWAComponent(TreeComponent):
total_prefix_len: int,
value_slice: torch.Tensor,
params: InsertParams,
cache_actions: list[CacheAction | ComponentAction],
) -> int:
if params.prev_prefix_len >= total_prefix_len + prefix_len:
return prefix_len
@@ -196,40 +188,38 @@ class SWAComponent(TreeComponent):
node.component_data[self.component_type].lock_ref == 0
), f"tombstone {self.component_type} lock_ref should be 0, node {node.id}"
assert (
swa_evicted_seqlen % self.cache.page_size == 0
swa_evicted_seqlen % self.tree_core.page_size == 0
), f"{self.component_type}: swa_evicted_seqlen must be page-aligned, {swa_evicted_seqlen=}"
if swa_evicted_seqlen <= total_prefix_len:
# Branch 1: entire value_slice is within SWA window — recover
old_full = full_cd.value
if full_cd.lock_ref > 0:
self._restore_device_value_with_locked_full(
node, full_cd.value, value_slice
cache_actions.append(
RecoverSWAWithLockedFull(node.id, old_full, value_slice)
)
return 0
self.cache.token_to_kv_pool_allocator.free(full_cd.value)
full_cd.value = value_slice.clone()
swa_value = self._translate_full_to_swa(full_cd.value)
self._restore_device_value(node, swa_value)
cache_actions.append(FreeDeviceKV([old_full]))
cache_actions.append(SWARebuild(node.id, value_slice))
return 0
elif swa_evicted_seqlen < total_prefix_len + prefix_len:
# Branch 2: value_slice[start_idx:] is within SWA window — partial recover
start_idx = swa_evicted_seqlen - total_prefix_len
if full_cd.lock_ref > 0:
self.cache._split_node(node.key, node, start_idx)
full_cd = node.component_data[BASE_COMPONENT_TYPE]
self._restore_device_value_with_locked_full(
node, full_cd.value, value_slice[start_idx:]
is_locked = full_cd.lock_ref > 0
old_full = full_cd.value[start_idx:]
_, action = self.tree_core._split_node(node.key, node, start_idx)
if action is not None:
cache_actions.append(action)
new_full = value_slice[start_idx:]
if is_locked:
cache_actions.append(
RecoverSWAWithLockedFull(node.id, old_full, new_full)
)
return start_idx
self.cache.token_to_kv_pool_allocator.free(full_cd.value[start_idx:])
self.cache._split_node(node.key, node, start_idx)
node.component_data[BASE_COMPONENT_TYPE].value = value_slice[
start_idx:
].clone()
swa_value = self._translate_full_to_swa(
node.component_data[BASE_COMPONENT_TYPE].value
)
self._restore_device_value(node, swa_value)
node.component_data[BASE_COMPONENT_TYPE].value = new_full.clone()
cache_actions.append(FreeDeviceKV([old_full]))
cache_actions.append(SWARebuild(node.id, new_full))
return start_idx
else:
# Branch 3: entire value_slice is outside SWA window — not consumed
@@ -241,6 +231,7 @@ class SWAComponent(TreeComponent):
prefix_len: int,
total_prefix_len: int,
params: InsertParams,
cache_actions: list[CacheAction | ComponentAction],
) -> None:
# _unevict_node_on_insert already wrote the request's fresh KV slice
# into the base value. We just need to rebuild SWA from that slice for
@@ -253,20 +244,24 @@ class SWAComponent(TreeComponent):
), f"tombstone {ct} lock_ref should be 0 on unevict, node {node.id}"
swa_evicted_seqlen = params.swa_evicted_seqlen
assert (
swa_evicted_seqlen % self.cache.page_size == 0
swa_evicted_seqlen % self.tree_core.page_size == 0
), f"{ct}: swa_evicted_seqlen must be page-aligned, {swa_evicted_seqlen=}"
full_value = node.component_data[BASE_COMPONENT_TYPE].value
if swa_evicted_seqlen <= total_prefix_len:
swa_value = self._translate_full_to_swa(full_value)
pass # entire node is within the SWA window
elif swa_evicted_seqlen < total_prefix_len + prefix_len:
start_idx = swa_evicted_seqlen - total_prefix_len
self.cache._split_node(node.key, node, start_idx)
full_value = node.component_data[BASE_COMPONENT_TYPE].value
swa_value = self._translate_full_to_swa(full_value)
_, action = self.tree_core._split_node(node.key, node, start_idx)
if action is not None:
cache_actions.append(action)
else:
return
self._restore_device_value(node, swa_value)
cache_actions.append(
SWARebuild(
node.id,
node.component_data[BASE_COMPONENT_TYPE].value,
)
)
def commit_insert_component_data(
self,
@@ -274,57 +269,65 @@ class SWAComponent(TreeComponent):
is_new_leaf: bool,
params: InsertParams,
result: InsertResult,
cache_actions: list[CacheAction | ComponentAction],
) -> None:
if not is_new_leaf:
return
node_start = result.prefix_len
split_pos = params.swa_evicted_seqlen - node_start
if split_pos <= 0:
swa_value = self._translate_full_to_swa(
node.component_data[BASE_COMPONENT_TYPE].value
)
node.component_data[self.component_type].value = swa_value
self.cache.lru_lists[self.component_type].insert_mru(node)
self.cache.component_evictable_size_[self.component_type] += len(swa_value)
elif split_pos < len(node.key):
# Node straddles the SWA eviction boundary
# Split into parent (tombstone, no SWA) and child (with SWA)
# After _split_node, `node` becomes the child
self.cache._split_node(node.key, node, split_pos)
swa_value = self._translate_full_to_swa(
node.component_data[BASE_COMPONENT_TYPE].value
)
node.component_data[self.component_type].value = swa_value
self.cache.lru_lists[self.component_type].insert_mru(node)
self.cache.component_evictable_size_[self.component_type] += len(swa_value)
else:
if split_pos >= len(node.key):
# Entire leaf is outside the SWA window — left as a tombstone.
return
if split_pos > 0:
# Node straddles the boundary: split into an out-of-window parent
# (tombstone) and an in-window child; `node` becomes the child.
_, action = self.tree_core._split_node(node.key, node, split_pos)
assert action is None, "new leaf cannot be write-through-pending"
# Cap the in-window leaf at one window for lock granularity, then rebuild SWA
# onto the in-window node(s) at apply time; rebuild the older prefix first so
# the in-window tail lands more-MRU.
capped_parent = self._maybe_split_leaf_for_swa_lock(node)
if capped_parent is not None:
cache_actions.append(
SWARebuild(
capped_parent.id,
capped_parent.component_data[BASE_COMPONENT_TYPE].value,
)
)
cache_actions.append(
SWARebuild(
node.id,
node.component_data[BASE_COMPONENT_TYPE].value,
)
)
self._maybe_split_leaf_for_swa_lock(node)
def _maybe_split_leaf_for_swa_lock(self, leaf: UnifiedTreeNode) -> None:
"""Cap a fresh SWA leaf at one page-aligned window so locking it pins
only one window of SWA pool, not the whole (long chunked-prefill) leaf.
def _maybe_split_leaf_for_swa_lock(
self, leaf: UnifiedTreeNode
) -> Optional[UnifiedTreeNode]:
"""Cap a fresh in-window SWA leaf at one page-aligned window so locking it pins
only one window of SWA pool, not the whole (long chunked-prefill) leaf; return
the split-off parent (older window) or None. The SWA value is stamped later, so
this runs on the tombstone leaf.
"""
ct = self.component_type
cd = leaf.component_data[ct]
if leaf is self.cache.root_node or cd.value is None or cd.lock_ref > 0:
return
if leaf is self.tree_core.root_node or cd.lock_ref > 0:
return None
page_size = self.cache.page_size
page_size = self.tree_core.page_size
# Smallest page-aligned size that still covers the sliding window.
tail_size = (self.sliding_window_size + page_size - 1) // page_size * page_size
leaf_len = len(leaf.key)
if leaf_len <= tail_size:
return
return None
split_at = leaf_len - tail_size
if page_size > 1 and (split_at % page_size != 0 or leaf_len % page_size != 0):
return
return None
self.cache._split_node(leaf.key, leaf, split_at)
new_parent, action = self.tree_core._split_node(leaf.key, leaf, split_at)
assert action is None, "fresh SWA leaf cannot be write-through-pending"
return new_parent
def redistribute_on_node_split(
self, new_parent: UnifiedTreeNode, child: UnifiedTreeNode
@@ -354,7 +357,7 @@ class SWAComponent(TreeComponent):
child.component_data[self.component_type].host_value = child_swa_host_value[
split_len:
].clone()
host_lru = self.cache.host_lru_lists[self.component_type]
host_lru = self.tree_core.host_lru_lists[self.component_type]
if new_parent.component_data[self.component_type].value is None:
host_lru.insert_mru(new_parent)
if child.component_data[
@@ -371,6 +374,8 @@ class SWAComponent(TreeComponent):
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]:
ct = self.component_type
@@ -383,19 +388,18 @@ class SWAComponent(TreeComponent):
# Pass full indices to free_swa so slots with no SWA pair are
# skipped. Freeing swa_value directly would double free those
# entries since they all map to the same sentinel slot.
self.cache.token_to_kv_pool_allocator.free_swa(
device_frees[self.component_type].append(
node.component_data[BASE_COMPONENT_TYPE].value
)
freed = len(cd.value)
self.cache.component_evictable_size_[ct] -= freed
self.tree_core.component_evictable_size_[ct] -= freed
cd.value = None
# Host layer
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
if EvictLayer.HOST in target and cd.host_value is not None:
host_freed = len(cd.host_value)
if self._swa_kv_pool_host is not None:
self._swa_kv_pool_host.free(cd.host_value)
host_frees[ct].append(cd.host_value)
cd.host_value = None
if host_lru.in_list(node):
host_lru.remove_node(node)
@@ -414,30 +418,56 @@ class SWAComponent(TreeComponent):
def eviction_priority(self, is_leaf: bool) -> int:
return 0 if is_leaf else 1
def drive_eviction(
self, params: EvictParams, tracker: dict[ComponentType, int]
) -> None:
request = params.swa_num_tokens
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
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]:
"""Return the next device-leaf node for the driver to evict, or None.
Internal nodes are tombstoned inline (no IO); the cursor is re-validated
(reset to LRU head) if the previous node's eviction removed it."""
ct = self.component_type
lru = self.cache.lru_lists[ct]
x = lru.get_lru_no_lock()
while tracker[ct] < request and x is not None and lru.in_list(x):
lru = self.tree_core.lru_lists[ct]
if self._evict_device_cursor is not None and not lru.in_list(
self._evict_device_cursor
):
self._evict_device_cursor = lru.get_lru_no_lock()
while (
tracker[ct] < self._evict_device_request_cnt
and self._evict_device_cursor is not None
and lru.in_list(self._evict_device_cursor)
):
x = self._evict_device_cursor
assert x.component_data[ct].value is not None
if x in self.cache.evictable_device_leaves:
# D-leaf: atomic eviction of all components
x_next = lru.get_prev_no_lock(x)
self.cache._evict_device_leaf(x, tracker)
if not lru.in_list(x_next):
x_next = lru.get_lru_no_lock()
x = x_next
else:
# Internal: tombstone SWA + cascade
x_next = lru.get_prev_no_lock(x)
self.cache._evict_component_and_detach_lru(
x, self, target=EvictLayer.DEVICE, tracker=tracker
)
self.cache._cascade_evict(x, self, tracker)
x = x_next
if x in self.tree_core.evictable_device_leaves:
self._evict_device_cursor = lru.get_prev_no_lock(x)
return x.id
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 = x_next
return None
def _evict_device_end(self) -> None:
"""Clear the device-eviction walk cursor state."""
self._evict_device_cursor = None
def acquire_component_lock(
self,
@@ -446,12 +476,16 @@ class SWAComponent(TreeComponent):
lock_host: bool = False,
) -> IncLockRefResult:
ct = self.component_type
root = self.cache.root_node
root = self.tree_core.root_node
sliding_window_size = self.sliding_window_size
swa_lock_size = 0
swa_uuid = None
uuid_key = "host_uuid" if lock_host else "uuid"
lru = self.cache.host_lru_lists[ct] if lock_host else self.cache.lru_lists[ct]
lru = (
self.tree_core.host_lru_lists[ct]
if lock_host
else self.tree_core.lru_lists[ct]
)
# Tombstoned nodes (cd.value is None) have no SWA chunk to protect
# skip them and keep walking up. This path is hit when HiCache
@@ -472,8 +506,8 @@ class SWAComponent(TreeComponent):
lru.remove_node(cur)
else:
key_len = len(cur.key)
self.cache.component_evictable_size_[ct] -= key_len
self.cache.component_protected_size_[ct] += key_len
self.tree_core.component_evictable_size_[ct] -= key_len
self.tree_core.component_protected_size_[ct] += key_len
if lock_host:
comp.host_lock_ref = ref + 1
else:
@@ -498,7 +532,7 @@ class SWAComponent(TreeComponent):
lock_host: bool = False,
) -> None:
ct = self.component_type
root = self.cache.root_node
root = self.tree_core.root_node
swa_uuid_for_lock = (
(params.swa_uuid_for_host_lock if lock_host else params.swa_uuid_for_lock)
if params
@@ -522,13 +556,13 @@ class SWAComponent(TreeComponent):
if ref == 1:
if lock_host:
if comp.value is None and comp.host_value is not None:
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
if not host_lru.in_list(cur):
host_lru.insert_mru(cur)
else:
key_len = len(comp.value)
self.cache.component_evictable_size_[ct] += key_len
self.cache.component_protected_size_[ct] -= key_len
self.tree_core.component_evictable_size_[ct] += key_len
self.tree_core.component_protected_size_[ct] -= key_len
if lock_host:
comp.host_lock_ref = ref - 1
else:
@@ -540,7 +574,9 @@ class SWAComponent(TreeComponent):
def release_window_lock(
self,
node: UnifiedTreeNode,
swa_uuid_for_lock: Optional[int] = None,
swa_uuid_for_lock: Optional[int],
device_frees: dict[ComponentType, list[torch.Tensor]],
host_frees: dict[ComponentType, list[torch.Tensor]],
) -> None:
"""Early-release the SWA lock along [node, swa_uuid_for_lock] while
leaving Full and Mamba locks intact.
@@ -553,7 +589,7 @@ class SWAComponent(TreeComponent):
invoked at most once per (node, swa_uuid_for_lock) pair.
"""
ct = self.component_type
root = self.cache.root_node
root = self.tree_core.root_node
cur = node
while cur is not root:
@@ -569,11 +605,15 @@ class SWAComponent(TreeComponent):
cd.lock_ref -= 1
if cd.lock_ref == 0:
key_len = len(cur.key)
self.cache.component_protected_size_[ct] -= key_len
self.cache.component_evictable_size_[ct] += key_len
if self.cache._is_device_leaf(cur):
self.cache._evict_component_and_detach_lru(
cur, self, target=EvictLayer.DEVICE
self.tree_core.component_protected_size_[ct] -= key_len
self.tree_core.component_evictable_size_[ct] += key_len
if self.tree_core._is_device_leaf(cur):
self.tree_core._evict_component_and_detach_lru(
cur,
self,
target=EvictLayer.DEVICE,
device_frees=device_frees,
host_frees=host_frees,
)
if swa_uuid_for_lock and cd.metadata.get("uuid") == swa_uuid_for_lock:
@@ -608,12 +648,36 @@ class SWAComponent(TreeComponent):
# ---- HiCache Hooks ----
def prepare_prefetch(
self,
node_id: NodeId,
*,
prefetch_tokens: int = 0,
) -> PreparePrefetchResult:
# unified_kv keeps SWA as a device-only ring -- nothing to prefetch into.
if self._swa_kv_pool_host is None:
return PreparePrefetchResult()
sw_pages = (
self.cache.sliding_window_size + self.cache.page_size - 1
) // self.cache.page_size
if sw_pages == 0 or prefetch_tokens // self.cache.page_size < sw_pages:
return PreparePrefetchResult()
num_tokens = sw_pages * self.cache.page_size
host_indices = self._swa_kv_pool_host.alloc(num_tokens)
if host_indices is None:
self.cache.evict_host(num_tokens, ComponentType.SWA)
host_indices = self._swa_kv_pool_host.alloc(num_tokens)
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,
*,
req: Optional[Req] = None,
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,
@@ -621,7 +685,7 @@ class SWAComponent(TreeComponent):
ct = self.component_type
# unified_kv keeps SWA as a device-only ring.
if self._swa_kv_pool_host is None and self.cache.cache_controller is not None:
if not self.tree_core.has_swa_host_pool and self.tree_core.enable_hicache:
return None
if phase == CacheTransferPhase.BACKUP_HOST:
@@ -644,7 +708,9 @@ class SWAComponent(TreeComponent):
backed_up: list[torch.Tensor] = []
nodes: list = []
cur = node
while cur is not self.cache.root_node and n_swa < self.sliding_window_size:
while (
cur is not self.tree_core.root_node and n_swa < self.sliding_window_size
):
cd = cur.component_data[ct]
assert cd.host_value is not None or cd.value is not None
if cd.value is not None:
@@ -668,7 +734,7 @@ class SWAComponent(TreeComponent):
name=PoolName.SWA,
host_indices=torch.cat(backed_up),
device_indices=None,
nodes_to_load=nodes,
nodes_to_load=[n.id for n in nodes],
)
]
@@ -676,32 +742,21 @@ class SWAComponent(TreeComponent):
cd = node.component_data[ct]
if cd.host_value is None or not node.hash_value:
return None
num_pages = len(cd.host_value) // self.cache.page_size
num_pages = len(cd.host_value) // self.tree_core.page_size
if num_pages == 0:
return None
return [
PoolTransfer(
name=PoolName.SWA,
host_indices=cd.host_value[-num_pages * self.cache.page_size :],
host_indices=cd.host_value[-num_pages * self.tree_core.page_size :],
keys=node.hash_value[-num_pages:],
hit_policy=PoolHitPolicy.TRAILING_PAGES,
)
]
if phase == CacheTransferPhase.PREFETCH:
# Require a full sliding window.
sw_pages = (
self.sliding_window_size + self.cache.page_size - 1
) // self.cache.page_size
if sw_pages == 0 or prefetch_tokens // self.cache.page_size < sw_pages:
return None
num_tokens = sw_pages * self.cache.page_size
host_indices = self._swa_kv_pool_host.alloc(num_tokens)
if host_indices is None:
self.cache.evict_host(num_tokens, ComponentType.SWA)
host_indices = self._swa_kv_pool_host.alloc(num_tokens)
if host_indices is None:
return []
assert host_indices is not None
sw_pages = host_indices.numel() // self.tree_core.page_size
return [
PoolTransfer(
name=PoolName.SWA,
@@ -719,6 +774,7 @@ class SWAComponent(TreeComponent):
phase: CacheTransferPhase,
transfers: list[PoolTransfer] = (),
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
@@ -735,35 +791,47 @@ class SWAComponent(TreeComponent):
assert transfers and transfers[0].device_indices is not None
xfer = transfers[0]
device_indices = xfer.device_indices
allocator = self.cache.token_to_kv_pool_allocator
full_chunks: list[torch.Tensor] = []
swa_chunks: list[torch.Tensor] = []
offset = 0
for n in xfer.nodes_to_load or []:
for nid in xfer.nodes_to_load or []:
n = self.tree_core.node_by_id(nid)
cd_n = n.component_data[ct]
cd_full_n = n.component_data[BASE_COMPONENT_TYPE]
n_tokens = len(cd_n.host_value)
swa_chunk = device_indices[offset : offset + n_tokens].clone()
self._restore_device_value(n, swa_chunk)
self.tree_core.set_component_device_value(
n.id, self.component_type, swa_chunk
)
assert cd_full_n.value is not None and len(cd_full_n.value) == n_tokens
# rebuild the mapping for the loaded SWA chunk
allocator.set_full_to_swa_mapping(cd_full_n.value, swa_chunk)
full_chunks.append(cd_full_n.value)
swa_chunks.append(swa_chunk)
offset += n_tokens
assert offset == len(xfer.host_indices)
# rebuild the mapping for the loaded SWA chunk, defer to orchestrator level
if full_chunks:
cache_actions.append(RebuildFullToSWAMapping(full_chunks, swa_chunks))
return
if phase == CacheTransferPhase.PREFETCH:
self._commit_prefetch(
node,
transfers,
cache_actions=cache_actions,
insert_result=insert_result,
pool_storage_result=pool_storage_result,
)
return
def _release_swa_host(self, host_indices: torch.Tensor) -> None:
def _release_swa_host(
self,
host_indices: torch.Tensor,
cache_actions: list[CacheAction | ComponentAction],
) -> None:
if host_indices is not None and host_indices.numel() > 0:
self.cache.cache_controller.append_host_mem_release(
extra_pools=[PoolTransfer(name=PoolName.SWA, host_indices=host_indices)]
cache_actions.append(
FreeComponentHostSlot([host_indices], component_type=ComponentType.SWA)
)
def _attach_swa_host_value(
@@ -773,18 +841,19 @@ class SWAComponent(TreeComponent):
ct = self.component_type
cd = node.component_data[ct]
cd.host_value = host_indices.clone()
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
if cd.value is None and not host_lru.in_list(node):
host_lru.insert_mru(node)
self.cache._update_evictable_leaf_sets(node)
self.tree_core._update_evictable_leaf_sets(node)
if node.parent:
self.cache._update_evictable_leaf_sets(node.parent)
self.tree_core._update_evictable_leaf_sets(node.parent)
def _commit_prefetch(
self,
anchor,
transfers: list[PoolTransfer],
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
@@ -799,7 +868,7 @@ class SWAComponent(TreeComponent):
if not transfers:
return
ct = self.component_type
page_size = self.cache.page_size
page_size = self.tree_core.page_size
host_indices = transfers[0].host_indices
window_require_pages = (
host_indices.numel() // page_size if host_indices is not None else 0
@@ -809,13 +878,18 @@ class SWAComponent(TreeComponent):
if pool_storage_result
else 0
)
target = insert_result.inserted_host_node if insert_result else None
target = (
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 (
target is None
or window_require_pages == 0
or loaded_pages < window_require_pages
):
self._release_swa_host(host_indices)
self._release_swa_host(host_indices, cache_actions)
return
# Buffer covers token range [loaded_start, total_len).
@@ -835,37 +909,105 @@ class SWAComponent(TreeComponent):
if cd.host_value is None and fill_len > 0:
# Tombstone: split off the in-buffer tail if needed, then fill.
if fill_start > node_start:
self.cache._split_node(cur.key, cur, fill_start - node_start)
_, action = self.tree_core._split_node(
cur.key, cur, fill_start - node_start
)
if action is not None:
cache_actions.append(action)
self._attach_swa_host_value(cur, slice_)
else:
# Already has SWA (or empty overlap): drop this slice.
self._release_swa_host(slice_)
self._release_swa_host(slice_, cache_actions)
pos = node_start
cur = cur.parent
# Buffer prefix that fell outside the anchor→leaf path.
if pos > loaded_start:
self._release_swa_host(host_indices[: pos - loaded_start])
self._release_swa_host(host_indices[: pos - loaded_start], cache_actions)
def drive_host_eviction(
self, num_tokens: int, tracker: dict[ComponentType, int]
self,
num_tokens: int,
tracker: dict[ComponentType, int],
device_frees: dict[ComponentType, list[torch.Tensor]],
host_frees: dict[ComponentType, list[torch.Tensor]],
) -> None:
"""Evict SWA host resources.
Internal nodes: private tombstone (free SWA host only).
Host leaves: atomic eviction via _evict_host_leaf."""
ct = self.component_type
host_lru = self.cache.host_lru_lists[ct]
host_lru = self.tree_core.host_lru_lists[ct]
x = host_lru.get_lru_no_host_lock()
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
x_next = host_lru.get_prev_no_host_lock(x)
cd = x.component_data[ct]
if x in self.cache.evictable_host_leaves:
self.cache._evict_host_leaf(x, tracker)
if x in self.tree_core.evictable_host_leaves:
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
else:
assert cd.host_value is not None
self.cache._evict_component_and_detach_lru(
x, self, target=EvictLayer.HOST, tracker=tracker
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.cache._cascade_evict(x, self, tracker, target=EvictLayer.HOST)
x = x_next
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
if self._swa_kv_pool_host is None:
return
for host_value in host_values:
self._swa_kv_pool_host.free(host_value)
def apply_component_action(self, action: ComponentAction) -> None:
alloc = self.cache.token_to_kv_pool_allocator
if isinstance(action, FreeComponentDeviceSlot):
for indices in action.indices:
alloc.free_swa(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.SWA, host_indices=host_indices)
]
)
return
if isinstance(action, RebuildFullToSWAMapping):
assert len(action.full_indices) == len(action.swa_indices)
for full, swa in zip(action.full_indices, action.swa_indices):
alloc.set_full_to_swa_mapping(full, swa)
return
if isinstance(action, RecoverSWAWithLockedFull):
# Keep the locked full; remap it onto the incoming full's SWA translation,
# freeing only the incoming full, then store the swa on the node.
swa_value = self._translate_full_to_swa(action.incoming_full)
alloc.set_full_to_swa_mapping(action.kept_full, swa_value)
alloc.full_to_swa_index_mapping[action.incoming_full.to(torch.int64)] = 0
alloc.full_attn_allocator.free(action.incoming_full)
self.tree_core.set_component_device_value(
action.node_id, self.component_type, swa_value
)
return
if isinstance(action, SWARebuild):
# Translate the node's source full value to SWA and store it on the node.
swa_value = self._translate_full_to_swa(action.source_value)
self.tree_core.set_component_device_value(
action.node_id, self.component_type, swa_value
)
return
raise AssertionError(
f"SWAComponent: unhandled ComponentAction {type(action).__name__}"
)
@@ -10,7 +10,6 @@ from numpy import float64
from sglang.srt.mem_cache.base_prefix_cache import (
DecLockRefParams,
EvictParams,
IncLockRefResult,
InsertParams,
InsertResult,
@@ -18,40 +17,26 @@ from sglang.srt.mem_cache.base_prefix_cache import (
MatchResult,
)
from sglang.srt.mem_cache.hicache_storage import PoolTransfer, PoolTransferResult
from sglang.srt.mem_cache.unified_cache.component_type import ( # noqa: F401
BASE_COMPONENT_TYPE,
ComponentType,
)
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_cache.unified_tree_core import UnifiedTreeCore
from sglang.srt.mem_cache.unified_radix_cache import (
NodeId,
UnifiedRadixCache,
UnifiedTreeNode,
)
class ComponentType(int, Enum):
"""Integer enum so that per-node list/tuple storage can be indexed directly."""
FULL = 0
SWA = 1
MAMBA = 2
def __str__(self) -> str: # keep human-readable logging
return self.name.lower()
@property
def is_full(self) -> bool:
return self == ComponentType.FULL
@property
def is_swa(self) -> bool:
return self == ComponentType.SWA
@property
def is_mamba(self) -> bool:
return self == ComponentType.MAMBA
BASE_COMPONENT_TYPE = ComponentType.FULL
_NUM_COMPONENT_TYPES = len(ComponentType)
_LAST_ACCESS_TIME_COUNTER_FLOAT = float64(1.0)
@@ -83,6 +68,16 @@ class PrepareLoadBackResult:
allocated_mamba_slot: Optional[torch.Tensor] = None
@dataclasses.dataclass(frozen=True)
class PreparePrefetchResult:
"""Outcome of prepare_prefetch; default = nothing to prepare."""
# Host pool exhausted; the caller aborts the prefetch.
alloc_failed: bool = False
# The component's pre-allocated host buffer (None = skip the build).
host_indices: Optional[torch.Tensor] = None
class CacheTransferPhase(str, Enum):
BACKUP_HOST = "backup_host" # D→H
@@ -114,6 +109,9 @@ def next_component_uuid() -> int:
class TreeComponent(ABC):
def __init__(self, cache: UnifiedRadixCache, params: CacheInitParams):
self.cache = cache
# Populated when the component passed to TreeCore constructor.
self.tree_core: Optional[UnifiedTreeCore] = None
self.is_evict_device_ongoing = False
# Subclasses MUST set this as a class attribute (not @property)
component_type: ComponentType
@@ -130,6 +128,11 @@ class TreeComponent(ABC):
value = node.component_data[self.component_type].value
return len(value) if value is not None else 0
def has_host_value_only(self, node: UnifiedTreeNode) -> bool:
"""Whether this component's data is evicted from device but host-backed."""
cd = node.component_data[self.component_type]
return cd.value is None and cd.host_value is not None
def refresh_lru(
self,
phase: LRURefreshPhase,
@@ -141,9 +144,9 @@ class TreeComponent(ABC):
case LRURefreshPhase.WALKDOWN:
if node.component_data[ct].value is None:
return
self.cache.lru_lists[ct].reset_node_mru(node)
self.tree_core.lru_lists[ct].reset_node_mru(node)
case LRURefreshPhase.MATCH_END:
self.cache.lru_lists[ct].reset_node_and_parents_mru(
self.tree_core.lru_lists[ct].reset_node_and_parents_mru(
node, root_node, self.node_has_component_data
)
case LRURefreshPhase.INSERT_END:
@@ -168,18 +171,22 @@ class TreeComponent(ABC):
- Mamba: returns True iff the node has mamba component data."""
...
def finalize_match_result(
def finalize_match_result_in_tree_core(
self,
result: MatchResult,
params: MatchPrefixParams,
value_chunks: list[torch.Tensor],
best_value_len: int,
) -> MatchResult:
"""Post-process the match result after prefix matching completes.
- Full & SWA: pass through unchanged.
- Mamba: performs copy-on-write allocates a new mamba slot, copies
the matched node's mamba state into the request pool, and records
branching_seqlen in result."""
"""Tree-side post-processing inside the match walk (no cache access)."""
return result
def finalize_match_result_in_cache(
self, params: MatchPrefixParams, result: MatchResult
) -> MatchResult:
"""Cache-level finalize after the match walk, dispatched by
`UnifiedRadixCache.match_prefix`; receives the NodeId-based result.
- Mamba: performs the copy-on-write into a per-request slot."""
return result
def update_component_on_insert_overlap(
@@ -189,6 +196,7 @@ class TreeComponent(ABC):
total_prefix_len: int,
value_slice: torch.Tensor,
params: InsertParams,
cache_actions: list[CacheAction | ComponentAction],
) -> int:
"""Called per-node when an insert's key overlaps an existing node.
Returns the index within value_slice from which this component
@@ -204,6 +212,7 @@ class TreeComponent(ABC):
prefix_len: int,
total_prefix_len: int,
params: InsertParams,
cache_actions: list[CacheAction | ComponentAction],
) -> None:
"""Called after _unevict_node_on_insert restores the base (Full) value
on an evicted node. Aux components (e.g. SWA) override this to rebuild
@@ -217,6 +226,7 @@ class TreeComponent(ABC):
is_new_leaf: bool,
params: InsertParams,
result: InsertResult,
cache_actions: list[CacheAction | ComponentAction],
) -> None:
"""Finalize component data on the target (leaf) node after the insert
walk completes. Called once per insert.
@@ -250,6 +260,8 @@ class TreeComponent(ABC):
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]:
"""Free this component's KV resources on a node being evicted.
@@ -262,6 +274,9 @@ class TreeComponent(ABC):
- ALL: free both device and host memory.
No tombstone caller will delete the node.
Freed indices are collected into *device_frees*/*host_frees* for the
Controller to drain, never freed inline.
Returns (device_freed, host_freed) token counts."""
...
@@ -292,17 +307,53 @@ class TreeComponent(ABC):
- Full evict internal: cascades to SWA + Mamba."""
return 0
def evict_device_start(self, request_cnt: int) -> None:
"""Begin this component's device-eviction walk (build its cursor/heap)."""
assert (
not self.is_evict_device_ongoing
), f"{self.component_type} device eviction already in progress"
self._evict_device_start(request_cnt)
self.is_evict_device_ongoing = True
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]:
"""Return the next device-leaf node for the driver to evict, or None.
Internal nodes are tombstoned inline (no IO)."""
assert (
self.is_evict_device_ongoing
), f"{self.component_type} device eviction not started"
return self._evict_device_next_node(tracker, device_frees, host_frees)
def evict_device_end(self) -> None:
"""Clear this component's device-eviction walk state."""
assert (
self.is_evict_device_ongoing
), f"{self.component_type} device eviction not started"
self._evict_device_end()
self.is_evict_device_ongoing = False
@abstractmethod
def drive_eviction(
self, params: EvictParams, tracker: dict[ComponentType, int]
) -> None:
"""Drive eviction from this component's LRU list.
Each component extracts its own request from params, walks its own
LRU, evicts, and calls cache._cascade_evict for priority cascade.
Updates the shared tracker with freed amounts for all components.
- Full: walks leaf LRU, evicts full then cascades entire leaf.
- Mamba: walks full LRU; tombstones internal nodes (with cascade
to equal-priority components like swa), cascades leaves to all."""
def _evict_device_start(self, request_cnt: int) -> None:
"""Build this component's eviction cursor/heap."""
...
@abstractmethod
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 the walk; return the next device leaf or None."""
...
@abstractmethod
def _evict_device_end(self) -> None:
"""Clear this component's eviction cursor/heap."""
...
@abstractmethod
@@ -386,11 +437,15 @@ class TreeComponent(ABC):
) -> None:
pass
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
"""Free evicted host-tier values back to this component's host pool."""
raise NotImplementedError(f"{self.component_type} must free its host values")
# ---- HiCache Hooks ----
def prepare_load_back(
self,
node: UnifiedTreeNode,
node_id: NodeId,
*,
req: Optional[Req] = None,
) -> PrepareLoadBackResult:
@@ -404,12 +459,22 @@ class TreeComponent(ABC):
not go through."""
pass
def prepare_prefetch(
self,
node_id: NodeId,
*,
prefetch_tokens: int = 0,
) -> PreparePrefetchResult:
"""Cache-level host pre-allocation before a prefetch builds its transfers."""
return PreparePrefetchResult()
def build_hicache_transfers(
self,
node: UnifiedTreeNode,
phase: CacheTransferPhase,
*,
req: Optional[Req] = None,
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,
@@ -424,6 +489,7 @@ class TreeComponent(ABC):
phase: CacheTransferPhase,
transfers: list[PoolTransfer] = (),
*,
cache_actions: list[CacheAction | ComponentAction],
insert_result: Optional[InsertResult] = None,
pool_storage_result: Optional[PoolTransferResult] = None,
) -> None:
@@ -431,9 +497,20 @@ class TreeComponent(ABC):
pass
def drive_host_eviction(
self, num_tokens: int, tracker: dict[ComponentType, int]
self,
num_tokens: int,
tracker: dict[ComponentType, int],
device_frees: dict[ComponentType, list[torch.Tensor]],
host_frees: dict[ComponentType, list[torch.Tensor]],
) -> None:
"""Evict from this component's host-side resources.
"""Evict from this component's host-side resources, collecting freed
values into *device_frees*/*host_frees* for the Controller to drain.
Called by HostPoolGroup when the host pool is full.
Default no-op for components without host storage."""
pass
def apply_component_action(self, action: ComponentAction) -> None:
"""Apply a component-routed cache action; dispatched by the cache."""
raise NotImplementedError(
f"{self.component_type} cannot apply {type(action).__name__}"
)
File diff suppressed because it is too large Load Diff