[RadixTree][3/N Refactor]:Support unified insert/evict params (#17401)

This commit is contained in:
zhangheng
2026-01-22 17:36:31 +08:00
committed by GitHub
parent 2262c5c9b5
commit f33022d039
13 changed files with 462 additions and 138 deletions

View File

@@ -35,7 +35,11 @@ import torch
from sglang.srt.dllm.config import DllmConfig
from sglang.srt.layers.attention.nsa.utils import is_nsa_prefill_cp_in_seq_split
from sglang.srt.managers.schedule_batch import DllmStagingReqs, Req, ScheduleBatch
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache, MatchPrefixParams
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
InsertParams,
MatchPrefixParams,
)
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
from sglang.srt.mem_cache.swa_memory_pool import SWATokenToKVPoolAllocator
from sglang.srt.server_args import ServerArgs
@@ -228,8 +232,10 @@ class SchedulePolicy:
else:
# Insert with a dummy key
self.waiting_queue_radix_tree.insert(
RadixKey(token_ids=prefix_ids, extra_key=extra_key),
torch.empty(len(prefix_ids), dtype=torch.bool),
InsertParams(
key=RadixKey(token_ids=prefix_ids, extra_key=extra_key),
value=torch.empty(len(prefix_ids), dtype=torch.bool),
)
)
return temporary_deprioritized

View File

@@ -43,6 +43,51 @@ class MatchPrefixParams:
req: Optional[Req] = None
@dataclasses.dataclass
class InsertParams:
"""Unified parameters for insert across different cache types"""
key: RadixKey
value: Optional[torch.Tensor] = None
# Mamba specific
mamba_value: Optional[torch.Tensor] = None
# SWA specific
prev_prefix_len: int = 0
swa_evicted_seqlen: int = 0
# General
chunked: bool = False
priority: int = 0
@dataclasses.dataclass
class InsertResult:
"""Result of an insert operation"""
prefix_len: int
mamba_exist: bool = False
@dataclasses.dataclass
class EvictParams:
"""Unified parameters for evict across different cache types"""
num_tokens: int
swa_num_tokens: int = 0
mamba_num: int = 0
@dataclasses.dataclass
class EvictResult:
"""Result of an evict operation"""
num_tokens_evicted: int = 0
swa_num_tokens_evicted: int = 0
mamba_num_evicted: int = 0
class MatchResult(NamedTuple):
"""Result of a prefix match operation.
@@ -102,7 +147,7 @@ class BasePrefixCache(ABC, PrefixCacheTrait):
pass
@abstractmethod
def evict(self, num_tokens: int):
def evict(self, params: EvictParams) -> EvictResult:
pass
@abstractmethod

View File

@@ -9,6 +9,10 @@ import torch
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
EvictParams,
EvictResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
@@ -54,6 +58,10 @@ class ChunkCache(BasePrefixCache):
last_host_node=None,
)
def insert(self, params: InsertParams) -> InsertResult:
# ChunkCache does not support prefix caching, so insert is a no-op
return InsertResult(prefix_len=0)
def cache_finished_req(self, req: Req, is_insert: bool = True):
kv_committed_len = req.pop_committed_kv_cache()
# For decode server: if req.output_ids is empty, we want to free all req.origin_input_ids
@@ -70,8 +78,8 @@ class ChunkCache(BasePrefixCache):
# `req.prefix_indices` will be used in `PrefillAdder::add_chunked_req` later
req.prefix_indices = kv_indices.to(dtype=torch.int64, copy=True)
def evict(self, num_tokens: int):
pass
def evict(self, params: EvictParams) -> EvictResult:
return EvictResult()
def inc_lock_ref(self, node: Any):
return 0
@@ -103,5 +111,5 @@ class SWAChunkCache(ChunkCache):
), "sliding_window_size must be set for SWAChunkCache"
return True
def evict(self, num_tokens: int):
pass
def evict(self, params: EvictParams) -> EvictResult:
return EvictResult()

View File

@@ -7,7 +7,7 @@ import torch
import triton
import triton.language as tl
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache
from sglang.srt.mem_cache.base_prefix_cache import BasePrefixCache, EvictParams
from sglang.srt.mem_cache.memory_pool import HybridReqToTokenPool, ReqToTokenPool
from sglang.srt.mem_cache.swa_memory_pool import SWATokenToKVPoolAllocator
from sglang.srt.server_args import get_global_server_args
@@ -244,11 +244,13 @@ def evict_from_tree_cache(tree_cache: BasePrefixCache | None, num_tokens: int):
if full_available_size < num_tokens or swa_available_size < num_tokens:
full_num_tokens = max(0, num_tokens - full_available_size)
swa_num_tokens = max(0, num_tokens - swa_available_size)
tree_cache.evict(full_num_tokens, swa_num_tokens)
tree_cache.evict(
EvictParams(num_tokens=full_num_tokens, swa_num_tokens=swa_num_tokens)
)
else:
# Standard allocator
if allocator.available_size() < num_tokens:
tree_cache.evict(num_tokens)
tree_cache.evict(EvictParams(num_tokens=num_tokens))
def alloc_paged_token_slots_extend(
@@ -311,7 +313,7 @@ def alloc_req_slots(
if mamba_available_size < mamba_state_needed:
if tree_cache is not None and tree_cache.supports_mamba():
mamba_num = max(0, mamba_state_needed - mamba_available_size)
tree_cache.evict_mamba(mamba_num)
tree_cache.evict(EvictParams(num_tokens=0, mamba_num=mamba_num))
req_pool_indices = req_to_token_pool.alloc(num_reqs, reqs)
else:
req_pool_indices = req_to_token_pool.alloc(num_reqs)

View File

@@ -10,7 +10,14 @@ from typing import TYPE_CHECKING, List, Optional
import torch
from sglang.srt.managers.cache_controller import HiCacheController, PrefetchOperation
from sglang.srt.mem_cache.base_prefix_cache import MatchPrefixParams, MatchResult
from sglang.srt.mem_cache.base_prefix_cache import (
EvictParams,
EvictResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool, MLATokenToKVPool
from sglang.srt.mem_cache.memory_pool_host import (
MHATokenToKVPoolHost,
@@ -331,8 +338,9 @@ class HiRadixCache(RadixCache):
def evictable_size(self):
return self.evictable_size_
def evict(self, num_tokens: int):
def evict(self, params: EvictParams) -> EvictResult:
start_time = time.perf_counter()
num_tokens = params.num_tokens
leaves = self._collect_leaves_device()
eviction_heap = [
(self.eviction_strategy.get_priority(node), node) for node in leaves
@@ -374,6 +382,7 @@ class HiRadixCache(RadixCache):
self._evict_backuped(node)
self.update_eviction_metrics(num_evicted, start_time)
return EvictResult(num_tokens_evicted=num_evicted)
def _evict_backuped(self, node: TreeNode):
# evict a node already written to host
@@ -453,7 +462,7 @@ class HiRadixCache(RadixCache):
host_indices=host_indices, node_id=last_hit_node.id
)
if device_indices is None:
self.evict(len(host_indices))
self.evict(EvictParams(num_tokens=len(host_indices)))
device_indices = self.cache_controller.load(
host_indices=host_indices, node_id=last_hit_node.id
)
@@ -854,19 +863,18 @@ class HiRadixCache(RadixCache):
new_node.parent.children[self.get_child_key_fn(key)] = new_node
return new_node
def insert(
self,
key: RadixKey,
value=None,
chunked: bool = False,
priority: int | None = None,
):
def insert(self, params: InsertParams) -> InsertResult:
key = params.key
value = params.value
chunked = params.chunked
priority = params.priority
if priority is None:
priority = 0
key, value = self.maybe_bigram_convert(key, value)
if len(key) == 0:
return 0
return InsertResult(prefix_len=0)
if self.is_eagle and value is not None:
# Make sure the value len equal to the EAGLE bigram key len
@@ -924,7 +932,7 @@ class HiRadixCache(RadixCache):
if self.cache_controller.write_policy != "write_back":
self._inc_hit_count(new_node, chunked)
return total_prefix_length
return InsertResult(prefix_len=total_prefix_length)
def _collect_leaves_device(self):
def is_leaf(node):

View File

@@ -35,6 +35,10 @@ from sglang.srt.mem_cache.allocator import (
)
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
EvictParams,
EvictResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
@@ -454,7 +458,7 @@ class MambaRadixCache(BasePrefixCache):
# try to alloc again, protect last_node from eviction
if dst_index is None:
self.inc_lock_ref(last_node)
self.evict_mamba(1)
self.evict(EvictParams(num_tokens=0, mamba_num=1))
dst_index = self.req_to_token_pool.mamba_pool.alloc(1)
self.dec_lock_ref(last_node)
assert dst_index is not None, "Can not alloc mamba cache"
@@ -478,13 +482,20 @@ class MambaRadixCache(BasePrefixCache):
mamba_branching_seqlen=mamba_branching_seqlen,
)
def insert(self, key: RadixKey, value=None, mamba_value=None) -> Tuple[int, bool]:
def insert(self, params: InsertParams) -> InsertResult:
if self.disable:
return 0, False
return InsertResult(prefix_len=0, mamba_exist=False)
key = params.key
value = params.value
mamba_value = params.mamba_value
if value is None:
value = torch.tensor([x for x in key.token_ids], dtype=torch.int64)
return self._insert_helper(self.root_node, key, value, mamba_value)
prefix_len, mamba_exist = self._insert_helper(
self.root_node, key, value, mamba_value
)
return InsertResult(prefix_len=prefix_len, mamba_exist=mamba_exist)
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
"""Cache request when it finishes."""
@@ -556,11 +567,14 @@ class MambaRadixCache(BasePrefixCache):
mamba_value = req.mamba_pool_idx.unsqueeze(-1).clone()
mamba_ping_pong_track_buffer_to_keep = None
new_prefix_len, mamba_exist = self.insert(
RadixKey(token_ids[:page_aligned_len], req.extra_key),
page_aligned_kv_indices,
mamba_value,
result = self.insert(
InsertParams(
key=RadixKey(token_ids[:page_aligned_len], req.extra_key),
value=page_aligned_kv_indices,
mamba_value=mamba_value,
)
)
new_prefix_len, mamba_exist = result.prefix_len, result.mamba_exist
self.token_to_kv_pool_allocator.free(
kv_indices[req.cache_protected_len : new_prefix_len]
@@ -644,16 +658,19 @@ class MambaRadixCache(BasePrefixCache):
# if alloc mamba cache failed, do evict and alloc again
if mamba_value_forked is None:
self.evict_mamba(1)
self.evict(EvictParams(num_tokens=0, mamba_num=1))
mamba_value_forked = self.req_to_token_pool.mamba_pool.fork_from(
mamba_value
)
assert mamba_value_forked is not None, "Can not alloc mamba cache"
new_prefix_len, mamba_exist = self.insert(
RadixKey(page_aligned_token_ids, req.extra_key),
page_aligned_kv_indices,
mamba_value_forked,
result = self.insert(
InsertParams(
key=RadixKey(page_aligned_token_ids, req.extra_key),
value=page_aligned_kv_indices,
mamba_value=mamba_value_forked,
)
)
new_prefix_len, mamba_exist = result.prefix_len, result.mamba_exist
self.token_to_kv_pool_allocator.free(
kv_indices[req.cache_protected_len : new_prefix_len]
)
@@ -735,9 +752,26 @@ class MambaRadixCache(BasePrefixCache):
full_num_evicted += leaf_full_num_evicted
return full_num_evicted, mamba_num_evicted, x, x_next
def evict_mamba(self, mamba_num: int) -> None:
def evict(self, params: EvictParams) -> EvictResult:
if self.disable:
return EvictResult()
full_num_evicted = 0
mamba_num_evicted = 0
if params.num_tokens > 0:
full_num_evicted = self.evict_full(params.num_tokens)
if params.mamba_num > 0:
mamba_num_evicted = self.evict_mamba(params.mamba_num)
return EvictResult(
num_tokens_evicted=full_num_evicted, mamba_num_evicted=mamba_num_evicted
)
def evict_mamba(self, mamba_num: int) -> int:
"""Evict mamba states. Returns the number of mamba states evicted."""
if self.disable or mamba_num <= 0:
return
return 0
# get the least recently used node that is not locked, doesn't have to be a leaf
x = self.mamba_lru_list.get_lru_no_lock()
mamba_num_evicted = 0
@@ -767,9 +801,12 @@ class MambaRadixCache(BasePrefixCache):
x = x_next
def evict(self, full_num_tokens: int) -> None:
return mamba_num_evicted
def evict_full(self, full_num_tokens: int) -> int:
"""Evict full KV cache. Returns the number of tokens evicted."""
if self.disable or full_num_tokens <= 0:
return
return 0
full_num_evicted = 0
# get the least recently used leaf node that is not locked
@@ -789,6 +826,8 @@ class MambaRadixCache(BasePrefixCache):
x = x_next
return full_num_evicted
def inc_lock_ref(self, node: TreeNode) -> Optional[int]:
"""
Increment the lock reference count for the node.

View File

@@ -41,6 +41,10 @@ from sglang.srt.disaggregation.kv_events import (
)
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
EvictParams,
EvictResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
@@ -413,16 +417,21 @@ class RadixCache(BasePrefixCache):
last_host_node=last_node,
)
def insert(self, key: RadixKey, value=None, chunked=False, priority: int = 0):
def insert(self, params: InsertParams) -> InsertResult:
if self.disable:
return 0
return InsertResult(prefix_len=0)
key = params.key
value = params.value
priority = params.priority
if value is None:
value = torch.tensor(key.token_ids, dtype=torch.int64)
key, value = self.maybe_bigram_convert(key, value)
return self._insert_helper(self.root_node, key, value, priority)
prefix_len = self._insert_helper(self.root_node, key, value, priority)
return InsertResult(prefix_len=prefix_len)
def _page_align_keys(self, key: list) -> list:
if self.page_size == 1:
@@ -459,7 +468,10 @@ class RadixCache(BasePrefixCache):
# Radix Cache takes one ref in memory pool
if is_insert:
priority = getattr(req, "priority", 0) or 0
new_prefix_len = self.insert(radix_key, values, priority=priority)
result = self.insert(
InsertParams(key=radix_key, value=values, priority=priority)
)
new_prefix_len = result.prefix_len
# Free the duplicates that were already in the tree
self.token_to_kv_pool_allocator.free(
kv_indices[req.cache_protected_len : new_prefix_len]
@@ -493,12 +505,15 @@ class RadixCache(BasePrefixCache):
radix_key = RadixKey(keys, req.extra_key, is_bigram=self.is_eagle)
# Radix Cache takes one ref in memory pool
new_prefix_len = self.insert(
radix_key,
values,
chunked=chunked,
priority=getattr(req, "priority", 0) or 0,
result = self.insert(
InsertParams(
key=radix_key,
value=values,
chunked=chunked,
priority=getattr(req, "priority", 0) or 0,
)
)
new_prefix_len = result.prefix_len
self.token_to_kv_pool_allocator.free(
kv_indices[req.cache_protected_len : new_prefix_len]
@@ -545,11 +560,12 @@ class RadixCache(BasePrefixCache):
def total_size(self):
return self._total_size_helper()
def evict(self, num_tokens: int):
def evict(self, params: EvictParams) -> EvictResult:
if self.disable:
return
return EvictResult()
start_time = time.perf_counter()
num_tokens = params.num_tokens
leaves = self._collect_leaves()
eviction_heap = [
(self.eviction_strategy.get_priority(node), node) for node in leaves
@@ -571,6 +587,7 @@ class RadixCache(BasePrefixCache):
self._record_remove_event(x)
self.update_eviction_metrics(num_evicted, start_time)
return EvictResult(num_tokens_evicted=num_evicted)
def inc_lock_ref(self, node: TreeNode):
if self.disable:
@@ -842,11 +859,15 @@ if __name__ == "__main__":
tree = RadixCache.create_simulated()
# Example token id sequences (as lists of ints)
tree.insert(RadixKey(token_ids=[1, 2, 3], extra_key=None))
tree.insert(RadixKey(token_ids=[1, 2, 3], extra_key=None))
tree.insert(RadixKey(token_ids=[1, 2, 4, 5], extra_key=None))
tree.insert(RadixKey(token_ids=[1, 2, 4, 5, 6, 7], extra_key=None))
tree.insert(RadixKey(token_ids=[8, 9, 10, 11, 12], extra_key=None))
tree.insert(InsertParams(key=RadixKey(token_ids=[1, 2, 3], extra_key=None)))
tree.insert(InsertParams(key=RadixKey(token_ids=[1, 2, 3], extra_key=None)))
tree.insert(InsertParams(key=RadixKey(token_ids=[1, 2, 4, 5], extra_key=None)))
tree.insert(
InsertParams(key=RadixKey(token_ids=[1, 2, 4, 5, 6, 7], extra_key=None))
)
tree.insert(
InsertParams(key=RadixKey(token_ids=[8, 9, 10, 11, 12], extra_key=None))
)
tree.pretty_print()
print(

View File

@@ -8,6 +8,8 @@ import torch
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
EvictParams,
EvictResult,
MatchPrefixParams,
MatchResult,
)
@@ -137,14 +139,18 @@ class RadixCacheCpp(BasePrefixCache):
"""
self.tree.lock_ref(node, True)
def evict(self, num_tokens: int):
def evict(self, params: EvictParams) -> EvictResult:
start_time = time.perf_counter()
num_tokens = params.num_tokens
evicted_device_indices = self.tree.evict(num_tokens)
num_evicted = 0
for indice in evicted_device_indices:
num_evicted += len(indice)
self.token_to_kv_pool_allocator.free(indice)
# FIXME: not sure about the real evict length here
self.update_eviction_metrics(num_tokens, start_time)
self.update_eviction_metrics(num_evicted, start_time)
return EvictResult(num_tokens_evicted=num_evicted)
def evictable_size(self):
return self.tree.evictable_size()

View File

@@ -6,7 +6,12 @@ from typing import TYPE_CHECKING, Optional
import torch
from sglang.srt.mem_cache.base_prefix_cache import MatchPrefixParams, MatchResult
from sglang.srt.mem_cache.base_prefix_cache import (
EvictParams,
EvictResult,
MatchPrefixParams,
MatchResult,
)
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey, TreeNode
try:
@@ -151,7 +156,7 @@ class LMCRadixCache(RadixCache):
prefix_pad = value.numel() % chunk_size
if self.token_to_kv_pool_allocator.available_size() < uncached_len:
self.evict(uncached_len)
self.evict(EvictParams(num_tokens=uncached_len))
token_slots = self.token_to_kv_pool_allocator.alloc(uncached_len)
if token_slots is None:
@@ -248,10 +253,10 @@ class LMCRadixCache(RadixCache):
with self._node_lock:
self._in_flight_nodes.append(new_last_node)
def evict(self, num_tokens: int) -> None: # type: ignore[override]
def evict(self, params: EvictParams) -> EvictResult:
"""Before base eviction, wait for any outstanding stores and release locks."""
if self.disable:
return
return EvictResult()
self.store_stream.synchronize()
with self._node_lock:
@@ -259,7 +264,7 @@ class LMCRadixCache(RadixCache):
self.dec_lock_ref(node)
self._in_flight_nodes.clear()
super().evict(num_tokens)
return super().evict(params)
def pretty_print(self): # type: ignore[override]
super().pretty_print()

View File

@@ -30,6 +30,10 @@ from numpy import float64
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
EvictParams,
EvictResult,
InsertParams,
InsertResult,
MatchPrefixParams,
MatchResult,
)
@@ -426,15 +430,14 @@ class SWARadixCache(BasePrefixCache):
last_host_node=last_node,
)
def insert(
self,
key: RadixKey,
value=None,
prev_prefix_len: int = 0,
swa_evicted_seqlen: int = 0,
) -> int:
def insert(self, params: InsertParams) -> InsertResult:
if self.disable:
return 0
return InsertResult(prefix_len=0)
key = params.key
value = params.value
prev_prefix_len = params.prev_prefix_len
swa_evicted_seqlen = params.swa_evicted_seqlen
key.token_ids = self.key_convert_fn(key.token_ids)
@@ -445,9 +448,10 @@ class SWARadixCache(BasePrefixCache):
# Make sure the value len equal to the EAGLE bigram key len
value = value[: len(key)]
return self._insert_helper(
prefix_len = self._insert_helper(
self.root_node, key, value, prev_prefix_len, swa_evicted_seqlen
)
return InsertResult(prefix_len=prefix_len)
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
"""Cache request when it finishes."""
@@ -492,10 +496,12 @@ class SWARadixCache(BasePrefixCache):
# Note: the insert function already frees the overlapped kv_indices
if is_insert:
self.insert(
RadixKey(token_ids[:page_aligned_token_len], req.extra_key),
page_aligned_kv_indices,
old_prefix_len,
req.swa_evicted_seqlen,
InsertParams(
key=RadixKey(token_ids[:page_aligned_token_len], req.extra_key),
value=page_aligned_kv_indices,
prev_prefix_len=old_prefix_len,
swa_evicted_seqlen=req.swa_evicted_seqlen,
)
)
else:
self.token_to_kv_pool_allocator.free(
@@ -552,11 +558,14 @@ class SWARadixCache(BasePrefixCache):
# Radix Cache takes one ref in memory pool
# Note: the insert function already frees the overlapped kv_indices
new_prefix_len = self.insert(
RadixKey(page_aligned_token_ids, req.extra_key),
page_aligned_kv_indices,
old_prefix_len,
result = self.insert(
InsertParams(
key=RadixKey(page_aligned_token_ids, req.extra_key),
value=page_aligned_kv_indices,
prev_prefix_len=old_prefix_len,
)
)
new_prefix_len = result.prefix_len
# The prefix indices could be updated, reuse it
match_result = self.match_prefix(
@@ -605,10 +614,12 @@ class SWARadixCache(BasePrefixCache):
def total_size(self) -> Tuple[int, int]:
return self._total_size_helper()
def evict(self, full_num_tokens: int, swa_num_tokens: int = 0) -> None:
def evict(self, params: EvictParams) -> EvictResult:
if self.disable:
return
return EvictResult()
start_time = time.perf_counter()
full_num_tokens = params.num_tokens
swa_num_tokens = params.swa_num_tokens
full_num_evicted = 0
swa_num_evicted = 0
if full_num_tokens > 0:
@@ -689,6 +700,9 @@ class SWARadixCache(BasePrefixCache):
x = x_next
self.update_eviction_metrics(full_num_evicted + swa_num_evicted, start_time)
return EvictResult(
num_tokens_evicted=full_num_evicted, swa_num_tokens_evicted=swa_num_evicted
)
def inc_lock_ref(self, node: TreeNode) -> Optional[int]:
"""