Using unified radix tree by default for all case (#35081)

This commit is contained in:
Zhangheng
2026-08-21 10:45:46 +08:00
committed by GitHub
parent e0cf75d9bd
commit 44806dc507
10 changed files with 140 additions and 80 deletions
+7
View File
@@ -595,6 +595,9 @@ class Envs:
SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS = EnvBool(True)
# Decode batches between SWA out-of-window evictions.
SGLANG_SWA_EVICTION_INTERVAL = EnvInt(128)
# Deprecated: the unified radix tree is the default tree cache now, so the
# registry no longer reads this. Kept because a few model/arch call sites
# still assert on it; do not use in new code.
SGLANG_ENABLE_UNIFIED_RADIX_TREE = EnvBool(False)
# Registered TreeCore backend serving the unified radix cache.
SGLANG_UNIFIED_RADIX_TREE_CORE_BACKEND = EnvStr("python")
@@ -1624,6 +1627,10 @@ _DEPRECATED_ENVS: Dict[str, _DeprecatedEnv] = {
note="DFlash now auto-enables the min-free-slots delay; unset this env. "
"To override the threshold, use '--min-free-slots-delay'."
),
"SGLANG_ENABLE_UNIFIED_RADIX_TREE": _DeprecatedEnv(
note="The unified radix tree is the default tree cache now; unset this "
"env. The field is still defined for legacy call sites."
),
}
@@ -1045,12 +1045,20 @@ class HostPoolGroup:
def get_ksize_per_token(self):
return self.anchor_entry.host_pool.get_ksize_per_token()
def get_size_per_token(self):
return self.anchor_entry.host_pool.get_size_per_token()
def get_pool(self, name: PoolName):
return self.entry_map[name].host_pool
def get_page_buffer_meta(self, indices):
return self.anchor_entry.host_pool.get_page_buffer_meta(indices)
def get_split_heads_page_buffer_meta(self, indices, split_factor: int):
return self.anchor_entry.host_pool.get_split_heads_page_buffer_meta(
indices, split_factor
)
def is_stride_page_aligned(self, page_size_bytes: int = 4096) -> bool:
return self.anchor_entry.host_pool.is_stride_page_aligned(page_size_bytes)
+6 -31
View File
@@ -108,32 +108,10 @@ def default_radix_cache_factory(ctx: TreeCacheBuildContext) -> BasePrefixCache:
logger.info("Using experimental C++ radix tree implementation.")
return RadixCacheCpp(params=params, server_args=server_args)
if envs.SGLANG_ENABLE_UNIFIED_RADIX_TREE.get() or use_mlx():
return _create_unified_radix_cache(ctx, server_args, params)
if ctx.is_hybrid_swa and ctx.full_tokens_per_layer == 0:
from sglang.srt.mem_cache.pure_swa_radix_cache import PureSWARadixCache
if ctx.is_hybrid_swa:
if ctx.full_tokens_per_layer == 0:
from sglang.srt.mem_cache.pure_swa_radix_cache import PureSWARadixCache
return PureSWARadixCache(params=params)
return _create_unified_radix_cache(ctx, server_args, params)
if ctx.is_hybrid_ssm:
return _create_unified_radix_cache(ctx, server_args, params)
if ctx.enable_hierarchical_cache:
if ctx.is_hybrid_ssm or ctx.is_hybrid_swa or ctx.is_dsa:
# HybridModel and DSA (e.g. DeepSeek V3.2 / GLM-5.1) launch
# HiCache via UnifiedRadixCache by default.
return _create_unified_radix_cache(ctx, server_args, params)
else:
from sglang.srt.mem_cache.hiradix_cache import HiRadixCache
cache = HiRadixCache(params=params, server_args=server_args)
ctx.tp_worker.register_hicache_layer_transfer_counter(
cache.cache_controller.layer_done_counter
)
return cache
return PureSWARadixCache(params=params)
if get_memory().enable_lmcache:
from sglang.srt.mem_cache.storage.lmcache.lmc_radix_cache import (
@@ -162,9 +140,7 @@ def default_radix_cache_factory(ctx: TreeCacheBuildContext) -> BasePrefixCache:
os.environ["FLEXKV_CONFIG_PATH"] = get_memory().flexkv_config_file
return _flexkv_factory(ctx)
from sglang.srt.mem_cache.radix_cache import RadixCache
return RadixCache(params)
return _create_unified_radix_cache(ctx, server_args, params)
def _create_unified_radix_cache(
@@ -254,9 +230,8 @@ def create_tree_cache(ctx: TreeCacheBuildContext) -> BasePrefixCache:
):
raise ValueError(
"--enable-session-radix-cache requires UnifiedRadixCache, but "
f"tree_cache is {type(cache).__name__}. Set "
"SGLANG_ENABLE_UNIFIED_RADIX_TREE=1 (or remove "
"--enable-session-radix-cache)."
f"tree_cache is {type(cache).__name__}. Drop the flag or the "
"option that selected another tree cache for this model."
)
hicache_attached = cache.cache_controller is not None
@@ -329,7 +329,7 @@ Each component implements these hooks. See `tree_component.py` for the ABC and d
## Construction
`UnifiedRadixCache` is constructed directly from `mem_cache/registry.py` when `SGLANG_ENABLE_UNIFIED_RADIX_TREE` is enabled. The registry sets `params.tree_components` before construction:
`UnifiedRadixCache` is the default tree cache and is constructed directly from `mem_cache/registry.py`. The registry sets `params.tree_components` before construction:
- Regular full-attention models → `(ComponentType.FULL,)`
- Hybrid SWA models → `(ComponentType.FULL, ComponentType.SWA)`
@@ -1540,6 +1540,50 @@ class UnifiedRadixCache(BasePrefixCache):
def get_prefix_hash_values(self, node_id: NodeId) -> list[str]:
return self.tree_core.get_prefix_hash_values(node_id)
def query_storage_hit_length(
self,
last_host_node_id: NodeId,
new_input_tokens: list[int],
last_hash: Optional[str] = None,
prefix_keys: Optional[list[str]] = None,
) -> int:
"""Synchronously probe L3 storage for the reusable prefix length."""
if (
not self.enable_storage
or self.cache_controller is None
or self.cache_controller.prefetch_rate_limited()
):
return 0
extra_key, cache_salt = self.tree_core.prefetch_anchor_info(last_host_node_id)
prefetch_key = RadixKey(
new_input_tokens,
extra_key=extra_key,
is_bigram=self.tree_core.is_eagle,
cache_salt=cache_salt,
).page_aligned(self.page_size)
if len(prefetch_key) < self.prefetch_threshold:
return 0
from sglang.srt.mem_cache.hybrid_cache.hybrid_cache_controller import (
PrefetchOperation,
)
operation = PrefetchOperation(
"__storage_hit_query__",
prefetch_key,
last_hash,
prefix_keys,
)
_, storage_hit_count = self.cache_controller._storage_hit_query(operation)
storage_hit_count_tensor = torch.tensor(storage_hit_count, dtype=torch.int)
self._all_reduce_attn_groups(
storage_hit_count_tensor, torch.distributed.ReduceOp.MIN
)
storage_hit_count = storage_hit_count_tensor.item()
storage_hit_count -= storage_hit_count % self.page_size
return storage_hit_count
def prefetch_from_storage(
self,
req_id: str,
@@ -2593,6 +2637,27 @@ class UnifiedRadixCache(BasePrefixCache):
return self.cache_controller.start_loading()
return 0
def is_load_back_event_done(self, consumer_index: int) -> bool:
"""Return True after the local load-back event is complete.
Mirrors ``HiRadixCache`` so the disagg decode restore state machine
(``DecodeHiCacheTransferMixin``) can gate on load-back completion; the
controller-level ``layer_done_counter`` event is shared across cache
implementations, while the tree-side bookkeeping runs in
``loading_check``.
"""
if consumer_index < 0 or self.cache_controller is None:
return True
finish_event = self.cache_controller.layer_done_counter.events[
consumer_index
].finish_event
if not finish_event.query():
return False
self.loading_check()
return True
# ---- Query / Inspection APIs ----
# These APIs exist for compatibility with other RadixTree implementations.
# TODO: simplify and consolidate in a future refactor.
+5 -11
View File
@@ -5656,17 +5656,11 @@ class ServerArgs:
# enable_multi_layer_eagle for EAGLE moved to the override registry
# (arg_groups/overrides.py: _mimo_v2_overrides).
if self.enable_hierarchical_cache:
if not envs.SGLANG_ENABLE_UNIFIED_RADIX_TREE.get():
raise ValueError(
"Hierarchical cache for MiMoV2 requires the unified "
"radix tree. Set SGLANG_ENABLE_UNIFIED_RADIX_TREE=1 "
"to enable --enable-hierarchical-cache for this model."
)
# MiMoV2 has head_dim != v_head_dim, so the host KV pool uses
# asymmetric K/V allocation. Both kernel/page_first and
# direct/page_first_direct have split K/V transfer paths.
# MiMoV2 hierarchical cache runs on the unified radix tree, which
# is the default tree cache now. MiMoV2 has head_dim != v_head_dim,
# so the host KV pool uses asymmetric K/V allocation. Both
# kernel/page_first and direct/page_first_direct have split K/V
# transfer paths.
elif (
"Step3p5ForCausalLM" in model_arch
or "Step3p7ForConditionalGeneration" in model_arch
@@ -2,7 +2,7 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, List
from sglang.test.scripted_runtime.context.radix import _node_lock_ref
from sglang.test.scripted_runtime.context.radix import _node_lock_ref, to_node_handle
if TYPE_CHECKING:
from sglang.srt.managers.scheduler import Scheduler
@@ -25,7 +25,7 @@ class ScriptedLockRefExhauster:
return
target = evictable[0]
tree_cache.inc_lock_ref(target)
tree_cache.inc_lock_ref(to_node_handle(tree_cache, target))
newly_locked = [node for node in evictable if _node_lock_ref(node) > 0]
if not newly_locked:
@@ -35,7 +35,7 @@ class ScriptedLockRefExhauster:
def release(self) -> None:
tree_cache = self.scheduler.tree_cache
for node in self._locked:
tree_cache.dec_lock_ref(node)
tree_cache.dec_lock_ref(to_node_handle(tree_cache, node))
self._locked.clear()
def _evictable_nodes(self) -> List[Any]:
@@ -25,6 +25,30 @@ def _node_lock_ref(node: Any) -> int:
return node.lock_ref
def resolve_node(tree_cache: Any, node_handle: Any) -> Any:
"""Resolve whatever a req or match result carries into a tree node.
``UnifiedRadixCache`` hands out NodeIds (ints); every other cache hands out
the node object itself. Returns None when the handle no longer maps to a
live node, which only happens once the node has been freed -- and a freed
node holds no locks.
"""
resolve = getattr(tree_cache, "resolve_node_handle", None)
if resolve is None:
return node_handle
try:
return resolve(node_handle)
except KeyError:
return None
def to_node_handle(tree_cache: Any, node: Any) -> Any:
"""Inverse of `resolve_node`: what the tree_cache lock APIs accept."""
if isinstance(node, UnifiedTreeNode):
return node.id
return node
def _collect_node_attr(
ctx: ScriptedContext, get_value: Callable[[Any], int]
) -> Dict[int, int]:
@@ -3,7 +3,7 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional
from sglang.test.scripted_runtime.context.radix import _node_lock_ref
from sglang.test.scripted_runtime.context.radix import _node_lock_ref, resolve_node
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import Req
@@ -52,7 +52,7 @@ class ScriptedReqHandle:
req = self.req
if req is None:
return 0
node = req.last_node
node = resolve_node(self.context.scheduler.tree_cache, req.last_node)
if node is None:
return 0
return _node_lock_ref(node)