feat: Session-reference-aware Unified Radix Cache for agentic multi-turn workloads (#29173)
Co-authored-by: Ishan Dhanani <ishandhanani@gmail.com> Co-authored-by: hzh0425 <hzh0425@apache.org> Co-authored-by: ispobock <ispobaoke@gmail.com>
This commit is contained in:
co-authored by
Ishan Dhanani
hzh0425
ispobock
parent
131bd51b01
commit
056474cdb0
@@ -920,6 +920,7 @@
|
|||||||
"pages": [
|
"pages": [
|
||||||
"docs/advanced_features/overview",
|
"docs/advanced_features/overview",
|
||||||
"docs/advanced_features/server_arguments",
|
"docs/advanced_features/server_arguments",
|
||||||
|
"docs/advanced_features/session_radix_cache",
|
||||||
"docs/advanced_features/hyperparameter_tuning",
|
"docs/advanced_features/hyperparameter_tuning",
|
||||||
"docs/advanced_features/attention_backend",
|
"docs/advanced_features/attention_backend",
|
||||||
"docs/advanced_features/hisparse_guide",
|
"docs/advanced_features/hisparse_guide",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ description: Advanced configuration, optimization, and deployment features for S
|
|||||||
---
|
---
|
||||||
|
|
||||||
- [Server Arguments](./server_arguments)
|
- [Server Arguments](./server_arguments)
|
||||||
|
- [Session-Aware Radix Cache](./session_radix_cache)
|
||||||
- [Hyperparameter Tuning](./hyperparameter_tuning)
|
- [Hyperparameter Tuning](./hyperparameter_tuning)
|
||||||
- [Attention Backend](./attention_backend)
|
- [Attention Backend](./attention_backend)
|
||||||
- [Speculative Decoding](./speculative_decoding)
|
- [Speculative Decoding](./speculative_decoding)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
---
|
||||||
|
title: "Session-Aware Radix Cache"
|
||||||
|
metatags:
|
||||||
|
description: "Keep active sessions ahead of unrelated KV in UnifiedRadixCache eviction order."
|
||||||
|
---
|
||||||
|
|
||||||
|
Session-aware radix caching improves cache hits for long-lived, multi-turn workloads under memory pressure. It registers reusable KV to a session and evicts unreferenced KV before KV still referenced by an active session.
|
||||||
|
|
||||||
|
Session references are soft protection, not memory pins. Referenced KV can still be evicted when reclaiming unreferenced KV is insufficient.
|
||||||
|
|
||||||
|
## Enable the cache
|
||||||
|
|
||||||
|
This feature is implemented only by `UnifiedRadixCache`.
|
||||||
|
|
||||||
|
```bash Command
|
||||||
|
SGLANG_ENABLE_UNIFIED_RADIX_TREE=1 python3 -m sglang.launch_server \
|
||||||
|
--model-path MODEL_PATH \
|
||||||
|
--enable-session-radix-cache
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pass and close the session
|
||||||
|
|
||||||
|
Your application must pass the same top-level `session_id` on every request in a session. The ID labels cache references only; it does not append or reconstruct conversation context, so each request must still contain the intended prompt.
|
||||||
|
|
||||||
|
```bash Command
|
||||||
|
curl http://localhost:30000/generate \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"text": "FULL_PROMPT_FOR_THIS_TURN",
|
||||||
|
"sampling_params": {"max_new_tokens": 128},
|
||||||
|
"session_id": "agent-42"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
When a request finishes, SGLang automatically registers its reusable cache leaves under the `session_id`. This reference-only workflow does not require an `/open_session` call.
|
||||||
|
|
||||||
|
Call `/close_session` when the application session ends, including error and cancellation paths:
|
||||||
|
|
||||||
|
```bash Command
|
||||||
|
curl -X POST http://localhost:30000/close_session \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"session_id": "agent-42"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
Closing removes the session's references but does not immediately free its KV. The KV remains reusable and returns to the normal eviction order.
|
||||||
|
|
||||||
|
## Eviction behavior
|
||||||
|
|
||||||
|
The cache tracks references independently for each UnifiedRadixCache component. Device and host eviction use the same session preference.
|
||||||
|
|
||||||
|
| Component | Referenced data | Eviction order |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Full attention | The reusable prefix path from the registered leaf to the root | Unreferenced nodes first, then referenced nodes with fewer session references, then the configured policy such as LRU |
|
||||||
|
| Sliding-window attention (SWA) | The reusable tail covering the sliding window plus page-alignment allowance | Two LRU passes: unreferenced nodes first, then referenced nodes if more space is required |
|
||||||
|
| Mamba | The reusable state on the registered leaf | Two LRU passes: unreferenced nodes first, then referenced nodes if more space is required |
|
||||||
|
|
||||||
|
UnifiedRadixCache still applies component cascade rules. Evicting an internal Full node also evicts its SWA and Mamba data; evicting SWA also evicts Mamba data; evicting Mamba affects only Mamba. Evicting a leaf removes all component data on that leaf.
|
||||||
@@ -868,9 +868,9 @@ class SchedulerDisaggregationPrefillMixin:
|
|||||||
# todo: set Transferring correctly in backend
|
# todo: set Transferring correctly in backend
|
||||||
undone_reqs.append(req)
|
undone_reqs.append(req)
|
||||||
elif poll == KVPoll.Success: # transfer done
|
elif poll == KVPoll.Success: # transfer done
|
||||||
release_kv_cache(req, self.tree_cache) # unlock the tree
|
|
||||||
if not isinstance(req.finished_reason, FINISH_ABORT):
|
if not isinstance(req.finished_reason, FINISH_ABORT):
|
||||||
req.finished_reason = FINISH_LENGTH(length=0)
|
req.finished_reason = FINISH_LENGTH(length=0)
|
||||||
|
release_kv_cache(req, self.tree_cache) # unlock the tree
|
||||||
# FIXME: clean up req's data in transfer engine
|
# FIXME: clean up req's data in transfer engine
|
||||||
req.disagg_kv_sender.clear()
|
req.disagg_kv_sender.clear()
|
||||||
done_reqs.append(req)
|
done_reqs.append(req)
|
||||||
|
|||||||
@@ -789,6 +789,8 @@ class Req(ReqDllmMixin):
|
|||||||
|
|
||||||
self.session = session
|
self.session = session
|
||||||
self.session_id = session_id
|
self.session_id = session_id
|
||||||
|
# Used by the session radix cache to reject registration after a close/reopen.
|
||||||
|
self.session_generation: Optional[int] = None
|
||||||
self.input_embeds = input_embeds
|
self.input_embeds = input_embeds
|
||||||
self.positional_embed_overrides = positional_embed_overrides
|
self.positional_embed_overrides = positional_embed_overrides
|
||||||
self.multi_item_delimiter_indices = multi_item_delimiter_indices
|
self.multi_item_delimiter_indices = multi_item_delimiter_indices
|
||||||
|
|||||||
@@ -410,6 +410,7 @@ class Scheduler(
|
|||||||
)
|
)
|
||||||
self.page_size = server_args.page_size
|
self.page_size = server_args.page_size
|
||||||
self.enable_hierarchical_cache = server_args.enable_hierarchical_cache
|
self.enable_hierarchical_cache = server_args.enable_hierarchical_cache
|
||||||
|
self.enable_session_radix_cache = server_args.enable_session_radix_cache
|
||||||
self.enable_hicache_storage = server_args.hicache_storage_backend is not None
|
self.enable_hicache_storage = server_args.hicache_storage_backend is not None
|
||||||
self.enable_decode_hicache = (
|
self.enable_decode_hicache = (
|
||||||
server_args.disaggregation_decode_enable_radix_cache
|
server_args.disaggregation_decode_enable_radix_cache
|
||||||
@@ -2227,7 +2228,7 @@ class Scheduler(
|
|||||||
)
|
)
|
||||||
# Radix-native sessions use only the top-level session_id.
|
# Radix-native sessions use only the top-level session_id.
|
||||||
radix_native_session = (
|
radix_native_session = (
|
||||||
recv_req.session_id is not None and get_memory().enable_session_radix_cache
|
recv_req.session_id is not None and self.enable_session_radix_cache
|
||||||
)
|
)
|
||||||
|
|
||||||
if session_id is None or radix_native_session:
|
if session_id is None or radix_native_session:
|
||||||
@@ -2286,6 +2287,11 @@ class Scheduler(
|
|||||||
)
|
)
|
||||||
req.tokenizer = self.tokenizer
|
req.tokenizer = self.tokenizer
|
||||||
|
|
||||||
|
if radix_native_session:
|
||||||
|
req.session_generation = self.tree_cache.ensure_session_generation(
|
||||||
|
recv_req.session_id
|
||||||
|
)
|
||||||
|
|
||||||
if self.disaggregation_mode != DisaggregationMode.NULL:
|
if self.disaggregation_mode != DisaggregationMode.NULL:
|
||||||
# Invalid request for disaggregated mode
|
# Invalid request for disaggregated mode
|
||||||
if (
|
if (
|
||||||
@@ -2316,6 +2322,10 @@ class Scheduler(
|
|||||||
self.model_config.vocab_size,
|
self.model_config.vocab_size,
|
||||||
eos_token_ids=self.model_config.hf_eos_token_id,
|
eos_token_ids=self.model_config.hf_eos_token_id,
|
||||||
)
|
)
|
||||||
|
if self.enable_session_radix_cache:
|
||||||
|
req.session_generation = self.tree_cache.ensure_session_generation(
|
||||||
|
session_id
|
||||||
|
)
|
||||||
# TODO: set trace context
|
# TODO: set trace context
|
||||||
if self.metrics_reporter.enable_metrics:
|
if self.metrics_reporter.enable_metrics:
|
||||||
req.time_stats.set_metrics_collector(self.metrics_collector)
|
req.time_stats.set_metrics_collector(self.metrics_collector)
|
||||||
@@ -4605,15 +4615,18 @@ class Scheduler(
|
|||||||
|
|
||||||
def open_session(self, recv_req: OpenSessionReqInput):
|
def open_session(self, recv_req: OpenSessionReqInput):
|
||||||
output = self.session_controller.open(recv_req)
|
output = self.session_controller.open(recv_req)
|
||||||
|
if output.success and self.enable_session_radix_cache:
|
||||||
|
self.tree_cache.open_radix_session(recv_req.session_id)
|
||||||
if self.ps.pp_rank == 0 and self.ps.tp_rank == 0 and self.ps.attn_cp_rank == 0:
|
if self.ps.pp_rank == 0 and self.ps.tp_rank == 0 and self.ps.attn_cp_rank == 0:
|
||||||
return output
|
return output
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def close_session(self, recv_req: CloseSessionReqInput):
|
def close_session(self, recv_req: CloseSessionReqInput):
|
||||||
if get_memory().enable_session_radix_cache:
|
if self.enable_session_radix_cache:
|
||||||
self.tree_cache.release_radix_session(recv_req.session_id)
|
self.tree_cache.release_radix_session(recv_req.session_id)
|
||||||
if recv_req.session_id in self.session_controller or not (
|
if (
|
||||||
get_memory().enable_session_radix_cache
|
recv_req.session_id in self.session_controller
|
||||||
|
or not self.enable_session_radix_cache
|
||||||
):
|
):
|
||||||
self.session_controller.close(recv_req)
|
self.session_controller.close(recv_req)
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ from sglang.srt.mem_cache.base_prefix_cache import (
|
|||||||
MatchResult,
|
MatchResult,
|
||||||
)
|
)
|
||||||
from sglang.srt.mem_cache.events import KVCacheEventMixin
|
from sglang.srt.mem_cache.events import KVCacheEventMixin
|
||||||
from sglang.srt.mem_cache.session_radix_cache import SessionRadixCacheMixin
|
|
||||||
from sglang.srt.mem_cache.utils import (
|
from sglang.srt.mem_cache.utils import (
|
||||||
get_eviction_strategy,
|
get_eviction_strategy,
|
||||||
get_hash_str,
|
get_hash_str,
|
||||||
@@ -277,14 +276,13 @@ class TreeNode:
|
|||||||
return self.last_access_time < other.last_access_time
|
return self.last_access_time < other.last_access_time
|
||||||
|
|
||||||
|
|
||||||
class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
class RadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||||
def __init__(self, params: CacheInitParams):
|
def __init__(self, params: CacheInitParams):
|
||||||
self.disable = params.disable
|
self.disable = params.disable
|
||||||
self.req_to_token_pool = params.req_to_token_pool
|
self.req_to_token_pool = params.req_to_token_pool
|
||||||
self.token_to_kv_pool_allocator = params.token_to_kv_pool_allocator
|
self.token_to_kv_pool_allocator = params.token_to_kv_pool_allocator
|
||||||
self.page_size = params.page_size
|
self.page_size = params.page_size
|
||||||
self.enable_kv_cache_events = params.enable_kv_cache_events
|
self.enable_kv_cache_events = params.enable_kv_cache_events
|
||||||
self.enable_session_radix_cache = params.enable_session_radix_cache
|
|
||||||
self.is_eagle = params.is_eagle
|
self.is_eagle = params.is_eagle
|
||||||
self.disable_finished_insert = params.disable_finished_insert
|
self.disable_finished_insert = params.disable_finished_insert
|
||||||
self.eviction_policy = params.eviction_policy.lower()
|
self.eviction_policy = params.eviction_policy.lower()
|
||||||
@@ -339,7 +337,6 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
|||||||
self.evictable_size_ = 0
|
self.evictable_size_ = 0
|
||||||
self.protected_size_ = 0
|
self.protected_size_ = 0
|
||||||
self.evictable_leaves.clear()
|
self.evictable_leaves.clear()
|
||||||
self._reset_session_radix_state()
|
|
||||||
self._empty_match_result = MatchResult(
|
self._empty_match_result = MatchResult(
|
||||||
device_indices=torch.empty(
|
device_indices=torch.empty(
|
||||||
(0,),
|
(0,),
|
||||||
@@ -469,10 +466,8 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
|||||||
result = self.insert(
|
result = self.insert(
|
||||||
InsertParams(key=radix_key, value=values, priority=priority)
|
InsertParams(key=radix_key, value=values, priority=priority)
|
||||||
)
|
)
|
||||||
session_leaf = result.last_device_node
|
|
||||||
freed_end = result.prefix_len
|
freed_end = result.prefix_len
|
||||||
else:
|
else:
|
||||||
session_leaf = None
|
|
||||||
freed_end = key_len
|
freed_end = key_len
|
||||||
|
|
||||||
# duplicates / uninserted range, then the unaligned tail
|
# duplicates / uninserted range, then the unaligned tail
|
||||||
@@ -486,8 +481,6 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
self._tag_session_leaf(req, radix_key, node=session_leaf)
|
|
||||||
|
|
||||||
# Remove req slot release the cache lock
|
# Remove req slot release the cache lock
|
||||||
if req.last_node is not None:
|
if req.last_node is not None:
|
||||||
self.dec_lock_ref(req.last_node)
|
self.dec_lock_ref(req.last_node)
|
||||||
@@ -559,8 +552,6 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
|||||||
|
|
||||||
req.last_node = new_last_node
|
req.last_node = new_last_node
|
||||||
|
|
||||||
self._tag_session_leaf(req, radix_key, node=new_last_node)
|
|
||||||
|
|
||||||
def pretty_print(self):
|
def pretty_print(self):
|
||||||
self._print_helper(self.root_node, 0)
|
self._print_helper(self.root_node, 0)
|
||||||
print(f"#tokens: {self.total_size()}")
|
print(f"#tokens: {self.total_size()}")
|
||||||
@@ -788,7 +779,6 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
|
|||||||
v = node.parent.children.pop(key, None)
|
v = node.parent.children.pop(key, None)
|
||||||
assert v == node, f"parent does not have child key, {key}"
|
assert v == node, f"parent does not have child key, {key}"
|
||||||
|
|
||||||
self._discard_session_leaf(node)
|
|
||||||
self.evictable_size_ -= len(node.key)
|
self.evictable_size_ -= len(node.key)
|
||||||
if node in self.evictable_leaves:
|
if node in self.evictable_leaves:
|
||||||
self.evictable_leaves.remove(node)
|
self.evictable_leaves.remove(node)
|
||||||
|
|||||||
@@ -210,6 +210,16 @@ def create_tree_cache(ctx: TreeCacheBuildContext) -> BasePrefixCache:
|
|||||||
cache = default_radix_cache_factory(ctx)
|
cache = default_radix_cache_factory(ctx)
|
||||||
source = "default"
|
source = "default"
|
||||||
|
|
||||||
|
if ctx.server_args.enable_session_radix_cache and not getattr(
|
||||||
|
cache, "enable_session_radix_cache", False
|
||||||
|
):
|
||||||
|
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)."
|
||||||
|
)
|
||||||
|
|
||||||
streaming_wrapped = False
|
streaming_wrapped = False
|
||||||
if (
|
if (
|
||||||
ctx.server_args.enable_streaming_session
|
ctx.server_args.enable_streaming_session
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
"""Session radix cache (``--enable-session-radix-cache``): tag each request's KV
|
|
||||||
by session_id; ``release_session`` (close) frees a session's tagged KV."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
|
||||||
from collections import OrderedDict, defaultdict
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import MatchPrefixParams
|
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from sglang.srt.managers.schedule_batch import Req
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
# Bounded guard against a request finishing after close. If a session id falls
|
|
||||||
# out of this LRU after 8192 later closes, an extremely late finish can tag
|
|
||||||
# again; explicit register_session clears the tombstone for intentional id reuse.
|
|
||||||
_CLOSED_SESSION_TOMBSTONE_LIMIT = 8192
|
|
||||||
|
|
||||||
|
|
||||||
class SessionRadixCacheMixin:
|
|
||||||
"""Tags radix KV by session id; ``release_session`` (close) frees a session's
|
|
||||||
tagged chains. A node holds the set of sessions on it, so a node shared by
|
|
||||||
several sessions is freed only when its last holder closes. Tagged KV is
|
|
||||||
ordinary LRU radix -- no pinning, no open. Mixed into RadixCache."""
|
|
||||||
|
|
||||||
def _reset_session_radix_state(self) -> None:
|
|
||||||
self._session_leaves = defaultdict(set)
|
|
||||||
self._closed_session_ids = OrderedDict()
|
|
||||||
|
|
||||||
def _ensure_session_radix_state(self) -> None:
|
|
||||||
if not hasattr(self, "_session_leaves"):
|
|
||||||
self._reset_session_radix_state()
|
|
||||||
|
|
||||||
def _remember_closed_session(self, session_id: str) -> None:
|
|
||||||
self._closed_session_ids[session_id] = None
|
|
||||||
self._closed_session_ids.move_to_end(session_id)
|
|
||||||
while len(self._closed_session_ids) > _CLOSED_SESSION_TOMBSTONE_LIMIT:
|
|
||||||
self._closed_session_ids.popitem(last=False)
|
|
||||||
|
|
||||||
def _discard_session_leaf(self, node) -> None:
|
|
||||||
session_ids = getattr(node, "session_ids", None)
|
|
||||||
if not session_ids or not hasattr(self, "_session_leaves"):
|
|
||||||
return
|
|
||||||
for sid in tuple(session_ids):
|
|
||||||
leaves = self._session_leaves.get(sid)
|
|
||||||
if leaves is not None:
|
|
||||||
leaves.discard(node)
|
|
||||||
if not leaves and sid not in self._closed_session_ids:
|
|
||||||
self._session_leaves.pop(sid, None)
|
|
||||||
if hasattr(node, "session_ids"):
|
|
||||||
delattr(node, "session_ids")
|
|
||||||
|
|
||||||
def _tag_session_leaf(self, req: Req, radix_key, node=None) -> None:
|
|
||||||
"""Add this request's session id to its leaf's holder set; no-op for non-session reqs."""
|
|
||||||
if not self.enable_session_radix_cache:
|
|
||||||
return
|
|
||||||
self._ensure_session_radix_state()
|
|
||||||
sid = getattr(req, "session_id", None)
|
|
||||||
if sid is None or sid in self._closed_session_ids:
|
|
||||||
return
|
|
||||||
if node is None:
|
|
||||||
logger.warning(
|
|
||||||
"_tag_session_leaf called without node; falling back to match_prefix"
|
|
||||||
)
|
|
||||||
node = self.match_prefix(MatchPrefixParams(key=radix_key)).last_device_node
|
|
||||||
if node is not None and node is not self.root_node:
|
|
||||||
session_ids = getattr(node, "session_ids", None)
|
|
||||||
if session_ids is None:
|
|
||||||
session_ids = set()
|
|
||||||
node.session_ids = session_ids
|
|
||||||
session_ids.add(sid)
|
|
||||||
self._session_leaves[sid].add(node)
|
|
||||||
logger.debug(
|
|
||||||
"tag session %s: node=%d holders=%d indexed=%d",
|
|
||||||
sid,
|
|
||||||
node.id,
|
|
||||||
len(session_ids),
|
|
||||||
len(self._session_leaves[sid]),
|
|
||||||
)
|
|
||||||
|
|
||||||
def release_radix_session(self, session_id: str) -> int:
|
|
||||||
"""Close: drop this session from each of its tagged leaves, freeing a node
|
|
||||||
only once no other session still holds it (last holder). Shared
|
|
||||||
prefixes/leaves kept."""
|
|
||||||
self._ensure_session_radix_state()
|
|
||||||
self._remember_closed_session(session_id)
|
|
||||||
indexed = self._session_leaves.pop(session_id, set())
|
|
||||||
freed = 0
|
|
||||||
for leaf in indexed:
|
|
||||||
if session_id not in getattr(leaf, "session_ids", set()):
|
|
||||||
continue
|
|
||||||
node = leaf
|
|
||||||
while True:
|
|
||||||
session_ids = getattr(node, "session_ids", None)
|
|
||||||
if session_ids is not None:
|
|
||||||
session_ids.discard(session_id)
|
|
||||||
if not session_ids:
|
|
||||||
delattr(node, "session_ids")
|
|
||||||
if (
|
|
||||||
node is self.root_node
|
|
||||||
or node.lock_ref != 0
|
|
||||||
or len(node.children) != 0
|
|
||||||
or node not in self.evictable_leaves
|
|
||||||
or getattr(node, "session_ids", None)
|
|
||||||
):
|
|
||||||
break
|
|
||||||
parent = node.parent
|
|
||||||
self.token_to_kv_pool_allocator.free(node.value)
|
|
||||||
self._delete_leaf(node)
|
|
||||||
freed += 1
|
|
||||||
node = parent
|
|
||||||
logger.info(
|
|
||||||
"release_session %s: indexed %d leaves, freed %d nodes",
|
|
||||||
session_id,
|
|
||||||
len(indexed),
|
|
||||||
freed,
|
|
||||||
)
|
|
||||||
return freed
|
|
||||||
@@ -43,6 +43,60 @@ class FullComponent(TreeComponent):
|
|||||||
super().__init__(cache, params)
|
super().__init__(cache, params)
|
||||||
# HiCache state: set to host KV pool when HiCache enabled
|
# HiCache state: set to host KV pool when HiCache enabled
|
||||||
self._full_kv_pool_host = None
|
self._full_kv_pool_host = None
|
||||||
|
# Lazy bind eviction strategy since tree core is initialized after component init.
|
||||||
|
self.session_ref_eviction_strategy = (
|
||||||
|
self._session_ref_eviction_strategy
|
||||||
|
if cache.enable_session_radix_cache
|
||||||
|
else None
|
||||||
|
)
|
||||||
|
|
||||||
|
def _ensure_eviction_strategy(self) -> None:
|
||||||
|
if self.session_ref_eviction_strategy is None:
|
||||||
|
self.session_ref_eviction_strategy = (
|
||||||
|
self.tree_core.eviction_strategy.get_priority
|
||||||
|
)
|
||||||
|
|
||||||
|
def _dec_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
node = leaf
|
||||||
|
while node is not None and node is not self.tree_core.root_node:
|
||||||
|
cd = node.component_data[self.component_type]
|
||||||
|
assert cd.session_ref > 0
|
||||||
|
cd.session_ref -= 1
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
def _advance_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
old_ancestor: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
stop = old_ancestor if old_ancestor is not None else self.tree_core.root_node
|
||||||
|
node = leaf
|
||||||
|
while (
|
||||||
|
node is not None
|
||||||
|
and node is not stop
|
||||||
|
and node is not self.tree_core.root_node
|
||||||
|
):
|
||||||
|
node.component_data[self.component_type].session_ref += 1
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
|
def _recede_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
fallback: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
stop = fallback if fallback is not None else self.tree_core.root_node
|
||||||
|
node = leaf
|
||||||
|
while (
|
||||||
|
node is not None
|
||||||
|
and node is not stop
|
||||||
|
and node is not self.tree_core.root_node
|
||||||
|
):
|
||||||
|
cd = node.component_data[self.component_type]
|
||||||
|
assert cd.session_ref > 0
|
||||||
|
cd.session_ref -= 1
|
||||||
|
node = node.parent
|
||||||
|
|
||||||
def create_match_validator(
|
def create_match_validator(
|
||||||
self, match_device_only: bool = False
|
self, match_device_only: bool = False
|
||||||
@@ -86,7 +140,9 @@ class FullComponent(TreeComponent):
|
|||||||
):
|
):
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
new_parent.component_data[ct].lock_ref = child.component_data[ct].lock_ref
|
new_parent.component_data[ct].lock_ref = child.component_data[ct].lock_ref
|
||||||
|
new_parent.component_data[ct].session_ref = child.component_data[ct].session_ref
|
||||||
child_cd = child.component_data[ct]
|
child_cd = child.component_data[ct]
|
||||||
|
assert new_parent.component_data[ct].session_ids is None
|
||||||
split_len = len(new_parent.key)
|
split_len = len(new_parent.key)
|
||||||
if child_cd.value is not None:
|
if child_cd.value is not None:
|
||||||
new_parent.component_data[ct].value = child_cd.value[:split_len].clone()
|
new_parent.component_data[ct].value = child_cd.value[:split_len].clone()
|
||||||
@@ -127,11 +183,16 @@ class FullComponent(TreeComponent):
|
|||||||
def eviction_priority(self, is_leaf: bool) -> int:
|
def eviction_priority(self, is_leaf: bool) -> int:
|
||||||
return 0 if is_leaf else 2
|
return 0 if is_leaf else 2
|
||||||
|
|
||||||
|
def _session_ref_eviction_strategy(self, node: UnifiedTreeNode):
|
||||||
|
ref = self.session_ref(node)
|
||||||
|
return ref > 0, ref, self.tree_core.eviction_strategy.get_priority(node)
|
||||||
|
|
||||||
def _evict_device_start(self, request_cnt: int) -> None:
|
def _evict_device_start(self, request_cnt: int) -> None:
|
||||||
|
self._ensure_eviction_strategy()
|
||||||
self._evict_device_request_cnt = request_cnt
|
self._evict_device_request_cnt = request_cnt
|
||||||
self._evict_device_last_node = None
|
self._evict_device_last_node = None
|
||||||
self._evict_device_heap = [
|
self._evict_device_heap = [
|
||||||
(self.tree_core.eviction_strategy.get_priority(n), n)
|
(self.session_ref_eviction_strategy(n), n)
|
||||||
for n in self.tree_core.evictable_device_leaves
|
for n in self.tree_core.evictable_device_leaves
|
||||||
]
|
]
|
||||||
heapq.heapify(self._evict_device_heap)
|
heapq.heapify(self._evict_device_heap)
|
||||||
@@ -151,7 +212,7 @@ class FullComponent(TreeComponent):
|
|||||||
):
|
):
|
||||||
heapq.heappush(
|
heapq.heappush(
|
||||||
self._evict_device_heap,
|
self._evict_device_heap,
|
||||||
(self.tree_core.eviction_strategy.get_priority(lv.parent), lv.parent),
|
(self.session_ref_eviction_strategy(lv.parent), lv.parent),
|
||||||
)
|
)
|
||||||
self._evict_device_last_node = None
|
self._evict_device_last_node = None
|
||||||
while tracker[ct] < self._evict_device_request_cnt and self._evict_device_heap:
|
while tracker[ct] < self._evict_device_request_cnt and self._evict_device_heap:
|
||||||
@@ -174,8 +235,9 @@ class FullComponent(TreeComponent):
|
|||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Evict host leaves to free KV host pool space."""
|
"""Evict host leaves to free KV host pool space."""
|
||||||
|
self._ensure_eviction_strategy()
|
||||||
heap = [
|
heap = [
|
||||||
(self.tree_core.eviction_strategy.get_priority(n), n)
|
(self.session_ref_eviction_strategy(n), n)
|
||||||
for n in self.tree_core.evictable_host_leaves
|
for n in self.tree_core.evictable_host_leaves
|
||||||
]
|
]
|
||||||
heapq.heapify(heap)
|
heapq.heapify(heap)
|
||||||
@@ -191,7 +253,7 @@ class FullComponent(TreeComponent):
|
|||||||
):
|
):
|
||||||
heapq.heappush(
|
heapq.heappush(
|
||||||
heap,
|
heap,
|
||||||
(self.tree_core.eviction_strategy.get_priority(x.parent), x.parent),
|
(self.session_ref_eviction_strategy(x.parent), x.parent),
|
||||||
)
|
)
|
||||||
|
|
||||||
def acquire_component_lock(
|
def acquire_component_lock(
|
||||||
|
|||||||
@@ -70,6 +70,39 @@ class MambaComponent(TreeComponent):
|
|||||||
# HiCache state
|
# HiCache state
|
||||||
self._mamba_pool_host = None # set to host mamba pool when HiCache enabled
|
self._mamba_pool_host = None # set to host mamba pool when HiCache enabled
|
||||||
|
|
||||||
|
def _inc_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
cd = leaf.component_data[self.component_type]
|
||||||
|
cd.session_ref += 1
|
||||||
|
if cd.session_ref == 1:
|
||||||
|
self._refresh_session_partition(leaf)
|
||||||
|
|
||||||
|
def _dec_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
cd = leaf.component_data[self.component_type]
|
||||||
|
assert cd.session_ref > 0
|
||||||
|
cd.session_ref -= 1
|
||||||
|
if cd.session_ref == 0:
|
||||||
|
self._refresh_session_partition(leaf)
|
||||||
|
|
||||||
|
def _advance_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
old_ancestor: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
self._inc_session_coverage(session_id, leaf)
|
||||||
|
if old_ancestor is not None:
|
||||||
|
self._dec_session_coverage(session_id, old_ancestor)
|
||||||
|
|
||||||
|
def _recede_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
fallback: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
self._dec_session_coverage(session_id, leaf)
|
||||||
|
if fallback is not None:
|
||||||
|
self._inc_session_coverage(session_id, fallback)
|
||||||
|
|
||||||
def refresh_lru(
|
def refresh_lru(
|
||||||
self,
|
self,
|
||||||
phase: LRURefreshPhase,
|
phase: LRURefreshPhase,
|
||||||
@@ -266,6 +299,8 @@ class MambaComponent(TreeComponent):
|
|||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
new_parent.component_data[ct].value = None
|
new_parent.component_data[ct].value = None
|
||||||
new_parent.component_data[ct].lock_ref = 0
|
new_parent.component_data[ct].lock_ref = 0
|
||||||
|
new_parent.component_data[ct].session_ref = 0
|
||||||
|
new_parent.component_data[ct].session_ids = None
|
||||||
# HiCache: mamba host_value stays on child (mamba = leaf-only data)
|
# HiCache: mamba host_value stays on child (mamba = leaf-only data)
|
||||||
new_parent.component_data[ct].host_value = None
|
new_parent.component_data[ct].host_value = None
|
||||||
new_parent.component_data[ct].host_lock_ref = 0
|
new_parent.component_data[ct].host_lock_ref = 0
|
||||||
@@ -311,6 +346,11 @@ class MambaComponent(TreeComponent):
|
|||||||
def _evict_device_start(self, request_cnt: int) -> None:
|
def _evict_device_start(self, request_cnt: int) -> None:
|
||||||
"""Begin the device-eviction walk from this component's LRU cursor."""
|
"""Begin the device-eviction walk from this component's LRU cursor."""
|
||||||
self._evict_device_request_cnt = request_cnt
|
self._evict_device_request_cnt = request_cnt
|
||||||
|
if self.tree_core.enable_session_radix_cache:
|
||||||
|
lru = self.tree_core.lru_lists[self.component_type]
|
||||||
|
lru.cursor_begin()
|
||||||
|
self._evict_device_cursor = lru.cursor_next()
|
||||||
|
else:
|
||||||
self._evict_device_cursor = self.tree_core.lru_lists[
|
self._evict_device_cursor = self.tree_core.lru_lists[
|
||||||
self.component_type
|
self.component_type
|
||||||
].get_lru_no_lock()
|
].get_lru_no_lock()
|
||||||
@@ -322,14 +362,18 @@ class MambaComponent(TreeComponent):
|
|||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Return the next device-leaf node for the driver to evict, or None.
|
"""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
|
Internal nodes are tombstoned inline (no IO). If the previous node's
|
||||||
(reset to LRU head) if the previous node's eviction removed it."""
|
eviction removed the cursor, the walk resumes from the partition
|
||||||
|
sentinel with session refs on, else it restarts at the LRU tail."""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
lru = self.tree_core.lru_lists[ct]
|
lru = self.tree_core.lru_lists[ct]
|
||||||
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
if self._evict_device_cursor is not None and not lru.in_list(
|
if self._evict_device_cursor is not None and not lru.in_list(
|
||||||
self._evict_device_cursor
|
self._evict_device_cursor
|
||||||
):
|
):
|
||||||
self._evict_device_cursor = lru.get_lru_no_lock()
|
self._evict_device_cursor = (
|
||||||
|
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
||||||
|
)
|
||||||
while (
|
while (
|
||||||
tracker[ct] < self._evict_device_request_cnt
|
tracker[ct] < self._evict_device_request_cnt
|
||||||
and self._evict_device_cursor is not None
|
and self._evict_device_cursor is not None
|
||||||
@@ -337,9 +381,14 @@ class MambaComponent(TreeComponent):
|
|||||||
):
|
):
|
||||||
x = self._evict_device_cursor
|
x = self._evict_device_cursor
|
||||||
assert x.component_data[ct].value is not None
|
assert x.component_data[ct].value is not None
|
||||||
if x in self.tree_core.evictable_device_leaves:
|
if x in self.tree_core.evictable_device_leaves and (
|
||||||
self._evict_device_cursor = lru.get_prev_no_lock(x)
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
|
):
|
||||||
|
self._evict_device_cursor = (
|
||||||
|
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
||||||
|
)
|
||||||
return x.id
|
return x.id
|
||||||
|
if not enabled:
|
||||||
x_next = lru.get_prev_no_lock(x)
|
x_next = lru.get_prev_no_lock(x)
|
||||||
self.tree_core._evict_component_and_detach_lru(
|
self.tree_core._evict_component_and_detach_lru(
|
||||||
x,
|
x,
|
||||||
@@ -352,11 +401,13 @@ class MambaComponent(TreeComponent):
|
|||||||
self.tree_core._cascade_evict(
|
self.tree_core._cascade_evict(
|
||||||
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
||||||
)
|
)
|
||||||
self._evict_device_cursor = x_next
|
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
"""Clear the device-eviction walk cursor state."""
|
"""Clear the device-eviction walk cursor state."""
|
||||||
|
if self.tree_core.enable_session_radix_cache:
|
||||||
|
self.tree_core.lru_lists[self.component_type].cursor_end()
|
||||||
self._evict_device_cursor = None
|
self._evict_device_cursor = None
|
||||||
|
|
||||||
def acquire_component_lock(
|
def acquire_component_lock(
|
||||||
@@ -787,15 +838,23 @@ class MambaComponent(TreeComponent):
|
|||||||
Host leaves: atomic eviction via _evict_host_leaf."""
|
Host leaves: atomic eviction via _evict_host_leaf."""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
host_lru = self.tree_core.host_lru_lists[ct]
|
host_lru = self.tree_core.host_lru_lists[ct]
|
||||||
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
|
if enabled:
|
||||||
|
host_lru.cursor_begin()
|
||||||
|
x = host_lru.cursor_next(host_lock=True)
|
||||||
|
else:
|
||||||
x = host_lru.get_lru_no_host_lock()
|
x = host_lru.get_lru_no_host_lock()
|
||||||
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
|
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
|
||||||
|
if not enabled:
|
||||||
x_next = host_lru.get_prev_no_host_lock(x)
|
x_next = host_lru.get_prev_no_host_lock(x)
|
||||||
cd = x.component_data[ct]
|
cd = x.component_data[ct]
|
||||||
if x in self.tree_core.evictable_host_leaves:
|
if x in self.tree_core.evictable_host_leaves and (
|
||||||
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
|
):
|
||||||
# Host leaf: atomic eviction (all components host + delete)
|
# Host leaf: atomic eviction (all components host + delete)
|
||||||
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
|
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
|
||||||
else:
|
else:
|
||||||
# Internal: tombstone Mamba + cascade
|
# Internal (or a leaf a session still pins): tombstone Mamba + cascade
|
||||||
assert cd.host_value is not None
|
assert cd.host_value is not None
|
||||||
self.tree_core._evict_component_and_detach_lru(
|
self.tree_core._evict_component_and_detach_lru(
|
||||||
x,
|
x,
|
||||||
@@ -814,7 +873,12 @@ class MambaComponent(TreeComponent):
|
|||||||
target=EvictLayer.HOST,
|
target=EvictLayer.HOST,
|
||||||
)
|
)
|
||||||
self.tree_core._update_evictable_leaf_sets(x)
|
self.tree_core._update_evictable_leaf_sets(x)
|
||||||
|
if enabled:
|
||||||
|
x = host_lru.cursor_next(host_lock=True)
|
||||||
|
else:
|
||||||
x = x_next
|
x = x_next
|
||||||
|
if enabled:
|
||||||
|
host_lru.cursor_end()
|
||||||
|
|
||||||
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
|
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
|
||||||
if self._mamba_pool_host is None:
|
if self._mamba_pool_host is None:
|
||||||
|
|||||||
@@ -69,12 +69,101 @@ class SWAComponent(TreeComponent):
|
|||||||
params.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator
|
params.token_to_kv_pool_allocator, SWATokenToKVPoolAllocator
|
||||||
), f"SWAComponent requires SWATokenToKVPoolAllocator, got {type(params.token_to_kv_pool_allocator)}"
|
), f"SWAComponent requires SWATokenToKVPoolAllocator, got {type(params.token_to_kv_pool_allocator)}"
|
||||||
super().__init__(cache, params)
|
super().__init__(cache, params)
|
||||||
|
self._session_leaf_covered_len: dict[str, dict[UnifiedTreeNode, int]] = {}
|
||||||
self.sliding_window_size = params.sliding_window_size
|
self.sliding_window_size = params.sliding_window_size
|
||||||
# HiCache state: set to host SWA pool when HiCache enabled
|
# HiCache state: set to host SWA pool when HiCache enabled
|
||||||
self._swa_kv_pool_host = None
|
self._swa_kv_pool_host = None
|
||||||
|
|
||||||
component_type = ComponentType.SWA
|
component_type = ComponentType.SWA
|
||||||
|
|
||||||
|
def reset_session_state(self) -> None:
|
||||||
|
super().reset_session_state()
|
||||||
|
self._session_leaf_covered_len = {}
|
||||||
|
|
||||||
|
def _walk_session_coverage(
|
||||||
|
self,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
span: int,
|
||||||
|
delta: int,
|
||||||
|
) -> int:
|
||||||
|
node = leaf
|
||||||
|
covered = 0
|
||||||
|
while node is not self.tree_core.root_node and covered < span:
|
||||||
|
cd = node.component_data[self.component_type]
|
||||||
|
if delta < 0:
|
||||||
|
assert cd.session_ref > 0
|
||||||
|
prev_ref = cd.session_ref
|
||||||
|
cd.session_ref += delta
|
||||||
|
if (prev_ref == 0) != (cd.session_ref == 0):
|
||||||
|
self._refresh_session_partition(node)
|
||||||
|
covered += len(node.key)
|
||||||
|
node = node.parent
|
||||||
|
return covered
|
||||||
|
|
||||||
|
def _inc_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
covered_by_leaf = self._session_leaf_covered_len.setdefault(session_id, {})
|
||||||
|
assert leaf not in covered_by_leaf
|
||||||
|
target_span = self.sliding_window_size + self.tree_core.page_size
|
||||||
|
covered = self._walk_session_coverage(leaf, target_span, 1)
|
||||||
|
assert covered > 0
|
||||||
|
covered_by_leaf[leaf] = covered
|
||||||
|
|
||||||
|
def _dec_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
covered_by_leaf = self._session_leaf_covered_len.get(session_id)
|
||||||
|
assert covered_by_leaf is not None and leaf in covered_by_leaf
|
||||||
|
covered_len = covered_by_leaf.pop(leaf)
|
||||||
|
if not covered_by_leaf:
|
||||||
|
self._session_leaf_covered_len.pop(session_id, None)
|
||||||
|
actual = self._walk_session_coverage(leaf, covered_len, -1)
|
||||||
|
assert actual == covered_len
|
||||||
|
|
||||||
|
def _advance_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
old_ancestor: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
self._inc_session_coverage(session_id, leaf)
|
||||||
|
if old_ancestor is not None:
|
||||||
|
self._dec_session_coverage(session_id, old_ancestor)
|
||||||
|
|
||||||
|
def _recede_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
fallback: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
self._dec_session_coverage(session_id, leaf)
|
||||||
|
if fallback is not None:
|
||||||
|
self._inc_session_coverage(session_id, fallback)
|
||||||
|
|
||||||
|
def validate_session_state(
|
||||||
|
self,
|
||||||
|
reachable_nodes: set[UnifiedTreeNode],
|
||||||
|
report_error: Callable[[str], None],
|
||||||
|
) -> None:
|
||||||
|
super().validate_session_state(reachable_nodes, report_error)
|
||||||
|
ct = self.component_type
|
||||||
|
|
||||||
|
for session_id, covered_by_leaf in self._session_leaf_covered_len.items():
|
||||||
|
for leaf, covered_len in covered_by_leaf.items():
|
||||||
|
if leaf not in self._session_leaves.get(session_id, ()):
|
||||||
|
report_error(
|
||||||
|
f"{ct} session {session_id!r} coverage leaf {leaf.id} is not indexed"
|
||||||
|
)
|
||||||
|
if covered_len <= 0:
|
||||||
|
report_error(
|
||||||
|
f"{ct} session {session_id!r} leaf {leaf.id} covered_len={covered_len}"
|
||||||
|
)
|
||||||
|
|
||||||
|
for session_id, leaves in self._session_leaves.items():
|
||||||
|
covered_by_leaf = self._session_leaf_covered_len.get(session_id, {})
|
||||||
|
for leaf in leaves:
|
||||||
|
if leaf not in covered_by_leaf:
|
||||||
|
report_error(
|
||||||
|
f"{ct} session {session_id!r} leaf {leaf.id} has no coverage record"
|
||||||
|
)
|
||||||
|
|
||||||
def _translate_full_to_swa(self, full_indices: torch.Tensor) -> torch.Tensor:
|
def _translate_full_to_swa(self, full_indices: torch.Tensor) -> torch.Tensor:
|
||||||
return self.cache.token_to_kv_pool_allocator.translate_loc_from_full_to_swa(
|
return self.cache.token_to_kv_pool_allocator.translate_loc_from_full_to_swa(
|
||||||
full_indices
|
full_indices
|
||||||
@@ -335,6 +424,10 @@ class SWAComponent(TreeComponent):
|
|||||||
new_parent.component_data[self.component_type].lock_ref = child.component_data[
|
new_parent.component_data[self.component_type].lock_ref = child.component_data[
|
||||||
self.component_type
|
self.component_type
|
||||||
].lock_ref
|
].lock_ref
|
||||||
|
new_parent.component_data[self.component_type].session_ref = (
|
||||||
|
child.component_data[self.component_type].session_ref
|
||||||
|
)
|
||||||
|
assert new_parent.component_data[self.component_type].session_ids is None
|
||||||
|
|
||||||
child_swa_value = child.component_data[self.component_type].value
|
child_swa_value = child.component_data[self.component_type].value
|
||||||
if child_swa_value is not None:
|
if child_swa_value is not None:
|
||||||
@@ -421,6 +514,11 @@ class SWAComponent(TreeComponent):
|
|||||||
def _evict_device_start(self, request_cnt: int) -> None:
|
def _evict_device_start(self, request_cnt: int) -> None:
|
||||||
"""Begin the device-eviction walk from this component's LRU cursor."""
|
"""Begin the device-eviction walk from this component's LRU cursor."""
|
||||||
self._evict_device_request_cnt = request_cnt
|
self._evict_device_request_cnt = request_cnt
|
||||||
|
if self.tree_core.enable_session_radix_cache:
|
||||||
|
lru = self.tree_core.lru_lists[self.component_type]
|
||||||
|
lru.cursor_begin()
|
||||||
|
self._evict_device_cursor = lru.cursor_next()
|
||||||
|
else:
|
||||||
self._evict_device_cursor = self.tree_core.lru_lists[
|
self._evict_device_cursor = self.tree_core.lru_lists[
|
||||||
self.component_type
|
self.component_type
|
||||||
].get_lru_no_lock()
|
].get_lru_no_lock()
|
||||||
@@ -432,14 +530,18 @@ class SWAComponent(TreeComponent):
|
|||||||
host_frees: dict[ComponentType, list[torch.Tensor]],
|
host_frees: dict[ComponentType, list[torch.Tensor]],
|
||||||
) -> Optional[NodeId]:
|
) -> Optional[NodeId]:
|
||||||
"""Return the next device-leaf node for the driver to evict, or None.
|
"""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
|
Internal nodes are tombstoned inline (no IO). If the previous node's
|
||||||
(reset to LRU head) if the previous node's eviction removed it."""
|
eviction removed the cursor, the walk resumes from the partition
|
||||||
|
sentinel with session refs on, else it restarts at the LRU tail."""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
lru = self.tree_core.lru_lists[ct]
|
lru = self.tree_core.lru_lists[ct]
|
||||||
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
if self._evict_device_cursor is not None and not lru.in_list(
|
if self._evict_device_cursor is not None and not lru.in_list(
|
||||||
self._evict_device_cursor
|
self._evict_device_cursor
|
||||||
):
|
):
|
||||||
self._evict_device_cursor = lru.get_lru_no_lock()
|
self._evict_device_cursor = (
|
||||||
|
lru.cursor_next() if enabled else lru.get_lru_no_lock()
|
||||||
|
)
|
||||||
while (
|
while (
|
||||||
tracker[ct] < self._evict_device_request_cnt
|
tracker[ct] < self._evict_device_request_cnt
|
||||||
and self._evict_device_cursor is not None
|
and self._evict_device_cursor is not None
|
||||||
@@ -447,9 +549,14 @@ class SWAComponent(TreeComponent):
|
|||||||
):
|
):
|
||||||
x = self._evict_device_cursor
|
x = self._evict_device_cursor
|
||||||
assert x.component_data[ct].value is not None
|
assert x.component_data[ct].value is not None
|
||||||
if x in self.tree_core.evictable_device_leaves:
|
if x in self.tree_core.evictable_device_leaves and (
|
||||||
self._evict_device_cursor = lru.get_prev_no_lock(x)
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
|
):
|
||||||
|
self._evict_device_cursor = (
|
||||||
|
lru.cursor_next() if enabled else lru.get_prev_no_lock(x)
|
||||||
|
)
|
||||||
return x.id
|
return x.id
|
||||||
|
if not enabled:
|
||||||
x_next = lru.get_prev_no_lock(x)
|
x_next = lru.get_prev_no_lock(x)
|
||||||
self.tree_core._evict_component_and_detach_lru(
|
self.tree_core._evict_component_and_detach_lru(
|
||||||
x,
|
x,
|
||||||
@@ -462,11 +569,13 @@ class SWAComponent(TreeComponent):
|
|||||||
self.tree_core._cascade_evict(
|
self.tree_core._cascade_evict(
|
||||||
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
x, self, tracker, device_frees=device_frees, host_frees=host_frees
|
||||||
)
|
)
|
||||||
self._evict_device_cursor = x_next
|
self._evict_device_cursor = lru.cursor_next() if enabled else x_next
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
"""Clear the device-eviction walk cursor state."""
|
"""Clear the device-eviction walk cursor state."""
|
||||||
|
if self.tree_core.enable_session_radix_cache:
|
||||||
|
self.tree_core.lru_lists[self.component_type].cursor_end()
|
||||||
self._evict_device_cursor = None
|
self._evict_device_cursor = None
|
||||||
|
|
||||||
def acquire_component_lock(
|
def acquire_component_lock(
|
||||||
@@ -938,11 +1047,19 @@ class SWAComponent(TreeComponent):
|
|||||||
Host leaves: atomic eviction via _evict_host_leaf."""
|
Host leaves: atomic eviction via _evict_host_leaf."""
|
||||||
ct = self.component_type
|
ct = self.component_type
|
||||||
host_lru = self.tree_core.host_lru_lists[ct]
|
host_lru = self.tree_core.host_lru_lists[ct]
|
||||||
|
enabled = self.tree_core.enable_session_radix_cache
|
||||||
|
if enabled:
|
||||||
|
host_lru.cursor_begin()
|
||||||
|
x = host_lru.cursor_next(host_lock=True)
|
||||||
|
else:
|
||||||
x = host_lru.get_lru_no_host_lock()
|
x = host_lru.get_lru_no_host_lock()
|
||||||
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
|
while tracker[ct] < num_tokens and x is not None and host_lru.in_list(x):
|
||||||
|
if not enabled:
|
||||||
x_next = host_lru.get_prev_no_host_lock(x)
|
x_next = host_lru.get_prev_no_host_lock(x)
|
||||||
cd = x.component_data[ct]
|
cd = x.component_data[ct]
|
||||||
if x in self.tree_core.evictable_host_leaves:
|
if x in self.tree_core.evictable_host_leaves and (
|
||||||
|
not enabled or self._can_evict_leaf_atomically(x)
|
||||||
|
):
|
||||||
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
|
self.tree_core._evict_host_leaf(x, tracker, device_frees, host_frees)
|
||||||
else:
|
else:
|
||||||
assert cd.host_value is not None
|
assert cd.host_value is not None
|
||||||
@@ -962,7 +1079,12 @@ class SWAComponent(TreeComponent):
|
|||||||
host_frees=host_frees,
|
host_frees=host_frees,
|
||||||
target=EvictLayer.HOST,
|
target=EvictLayer.HOST,
|
||||||
)
|
)
|
||||||
|
if enabled:
|
||||||
|
x = host_lru.cursor_next(host_lock=True)
|
||||||
|
else:
|
||||||
x = x_next
|
x = x_next
|
||||||
|
if enabled:
|
||||||
|
host_lru.cursor_end()
|
||||||
|
|
||||||
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
|
def free_host_values(self, host_values: list[torch.Tensor]) -> None:
|
||||||
if self._swa_kv_pool_host is None:
|
if self._swa_kv_pool_host is None:
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import dataclasses
|
import dataclasses
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
|
from collections import defaultdict
|
||||||
from enum import Enum, IntFlag
|
from enum import Enum, IntFlag
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence
|
from typing import TYPE_CHECKING, Any, Callable, Optional, Sequence
|
||||||
|
|
||||||
@@ -50,6 +51,8 @@ class ComponentData:
|
|||||||
metadata: dict[str, Any] = dataclasses.field(default_factory=dict)
|
metadata: dict[str, Any] = dataclasses.field(default_factory=dict)
|
||||||
host_value: Optional[torch.Tensor] = None
|
host_value: Optional[torch.Tensor] = None
|
||||||
host_lock_ref: int = 0
|
host_lock_ref: int = 0
|
||||||
|
session_ref: int = 0
|
||||||
|
session_ids: Optional[set[str]] = None
|
||||||
|
|
||||||
|
|
||||||
class EvictLayer(IntFlag):
|
class EvictLayer(IntFlag):
|
||||||
@@ -112,10 +115,189 @@ class TreeComponent(ABC):
|
|||||||
# Populated when the component passed to TreeCore constructor.
|
# Populated when the component passed to TreeCore constructor.
|
||||||
self.tree_core: Optional[UnifiedTreeCore] = None
|
self.tree_core: Optional[UnifiedTreeCore] = None
|
||||||
self.is_evict_device_ongoing = False
|
self.is_evict_device_ongoing = False
|
||||||
|
# Per-session frontier nodes (the deepest registered node per cached
|
||||||
|
# path), not physical tree leaves: a frontier node may have children.
|
||||||
|
self._session_leaves: dict[str, set[UnifiedTreeNode]] = defaultdict(set)
|
||||||
|
|
||||||
# Subclasses MUST set this as a class attribute (not @property)
|
# Subclasses MUST set this as a class attribute (not @property)
|
||||||
component_type: ComponentType
|
component_type: ComponentType
|
||||||
|
|
||||||
|
def reset_session_state(self) -> None:
|
||||||
|
self._session_leaves = defaultdict(set)
|
||||||
|
|
||||||
|
def session_ref(self, node: UnifiedTreeNode) -> int:
|
||||||
|
return node.component_data[self.component_type].session_ref
|
||||||
|
|
||||||
|
def _can_evict_leaf_atomically(self, node: UnifiedTreeNode) -> bool:
|
||||||
|
# Don't allow a non-session-ref component to cascade-evict
|
||||||
|
# a high-priority session-ref component.
|
||||||
|
if self.session_ref(node) > 0:
|
||||||
|
return True
|
||||||
|
priority = self.eviction_priority(is_leaf=False)
|
||||||
|
return not any(
|
||||||
|
comp.eviction_priority(is_leaf=False) >= priority
|
||||||
|
and comp.session_ref(node) > 0
|
||||||
|
for comp in self.tree_core.components
|
||||||
|
)
|
||||||
|
|
||||||
|
def _refresh_session_partition(self, node: UnifiedTreeNode) -> None:
|
||||||
|
ct = self.component_type
|
||||||
|
lru = self.tree_core.lru_lists[ct]
|
||||||
|
if lru.in_list(node):
|
||||||
|
lru.reset_node_mru(node)
|
||||||
|
host_lru = self.tree_core.host_lru_lists[ct]
|
||||||
|
if host_lru.in_list(node):
|
||||||
|
host_lru.reset_node_mru(node)
|
||||||
|
|
||||||
|
def _find_reusable_session_leaf(
|
||||||
|
self, node: Optional[UnifiedTreeNode]
|
||||||
|
) -> Optional[UnifiedTreeNode]:
|
||||||
|
root_node = self.tree_core.root_node
|
||||||
|
if node is None or node is root_node:
|
||||||
|
return None
|
||||||
|
|
||||||
|
validator = self.create_match_validator(match_device_only=False)
|
||||||
|
cur = node
|
||||||
|
while cur is not None and cur is not root_node:
|
||||||
|
if validator(cur):
|
||||||
|
return cur
|
||||||
|
cur = cur.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
def resolve_session_leaf(
|
||||||
|
self, req: Req, last_node: Optional[UnifiedTreeNode]
|
||||||
|
) -> Optional[UnifiedTreeNode]:
|
||||||
|
return self._find_reusable_session_leaf(last_node)
|
||||||
|
|
||||||
|
def _mark_session_leaf(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
self._session_leaves[session_id].add(leaf)
|
||||||
|
cd = leaf.component_data[self.component_type]
|
||||||
|
if cd.session_ids is None:
|
||||||
|
cd.session_ids = set()
|
||||||
|
cd.session_ids.add(session_id)
|
||||||
|
|
||||||
|
def _unmark_session_leaf(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
leaves = self._session_leaves.get(session_id)
|
||||||
|
assert leaves is not None and leaf in leaves
|
||||||
|
leaves.remove(leaf)
|
||||||
|
cd = leaf.component_data[self.component_type]
|
||||||
|
assert cd.session_ids is not None and session_id in cd.session_ids
|
||||||
|
cd.session_ids.remove(session_id)
|
||||||
|
if not cd.session_ids:
|
||||||
|
cd.session_ids = None
|
||||||
|
if not leaves:
|
||||||
|
self._session_leaves.pop(session_id, None)
|
||||||
|
|
||||||
|
def _nearest_session_ancestor(
|
||||||
|
self, new_leaf: UnifiedTreeNode, current_leaves: set[UnifiedTreeNode]
|
||||||
|
) -> Optional[UnifiedTreeNode]:
|
||||||
|
cur = new_leaf.parent
|
||||||
|
while cur is not None and cur is not self.tree_core.root_node:
|
||||||
|
if cur in current_leaves:
|
||||||
|
return cur
|
||||||
|
cur = cur.parent
|
||||||
|
return None
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _dec_session_coverage(self, session_id: str, leaf: UnifiedTreeNode) -> None:
|
||||||
|
"""Remove one session reference from the coverage anchored at `leaf`."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _advance_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
old_ancestor: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
"""Move the session's coverage from `old_ancestor` forward to `leaf`."""
|
||||||
|
...
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def _recede_session_coverage(
|
||||||
|
self,
|
||||||
|
session_id: str,
|
||||||
|
leaf: UnifiedTreeNode,
|
||||||
|
fallback: Optional[UnifiedTreeNode],
|
||||||
|
) -> None:
|
||||||
|
"""Move the session's coverage backward from `leaf` to `fallback`."""
|
||||||
|
...
|
||||||
|
|
||||||
|
def register_session_leaf(
|
||||||
|
self, session_id: str, leaf: Optional[UnifiedTreeNode]
|
||||||
|
) -> None:
|
||||||
|
if (
|
||||||
|
not self.tree_core.enable_session_radix_cache
|
||||||
|
or leaf is None
|
||||||
|
or leaf is self.tree_core.root_node
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
current_leaves = self._session_leaves[session_id]
|
||||||
|
if leaf in current_leaves:
|
||||||
|
return
|
||||||
|
|
||||||
|
old_ancestor = self._nearest_session_ancestor(leaf, current_leaves)
|
||||||
|
self._advance_session_coverage(session_id, leaf, old_ancestor)
|
||||||
|
self._mark_session_leaf(session_id, leaf)
|
||||||
|
if old_ancestor is not None:
|
||||||
|
self._unmark_session_leaf(session_id, old_ancestor)
|
||||||
|
|
||||||
|
def release_session(self, session_id: str) -> int:
|
||||||
|
leaves = tuple(self._session_leaves.get(session_id, ()))
|
||||||
|
for leaf in leaves:
|
||||||
|
self._dec_session_coverage(session_id, leaf)
|
||||||
|
self._unmark_session_leaf(session_id, leaf)
|
||||||
|
return len(leaves)
|
||||||
|
|
||||||
|
def discard_deleted_session_leaf(self, node: UnifiedTreeNode) -> None:
|
||||||
|
cd = node.component_data[self.component_type]
|
||||||
|
for session_id in tuple(cd.session_ids or ()):
|
||||||
|
leaves = self._session_leaves.get(session_id)
|
||||||
|
assert leaves is not None and node in leaves
|
||||||
|
fallback = self._find_reusable_session_leaf(node.parent)
|
||||||
|
if fallback is not None and fallback in self._session_leaves.get(
|
||||||
|
session_id, ()
|
||||||
|
):
|
||||||
|
fallback = None
|
||||||
|
self._recede_session_coverage(session_id, node, fallback)
|
||||||
|
self._unmark_session_leaf(session_id, node)
|
||||||
|
if fallback is not None:
|
||||||
|
self._mark_session_leaf(session_id, fallback)
|
||||||
|
|
||||||
|
def validate_session_state(
|
||||||
|
self,
|
||||||
|
reachable_nodes: set[UnifiedTreeNode],
|
||||||
|
report_error: Callable[[str], None],
|
||||||
|
) -> None:
|
||||||
|
reachable_nodes = set(reachable_nodes)
|
||||||
|
ct = self.component_type
|
||||||
|
|
||||||
|
for session_id, leaves in self._session_leaves.items():
|
||||||
|
if not leaves:
|
||||||
|
report_error(f"{ct} session {session_id!r} has an empty leaf index")
|
||||||
|
for leaf in leaves:
|
||||||
|
if leaf not in reachable_nodes:
|
||||||
|
report_error(
|
||||||
|
f"{ct} session {session_id!r} indexes unreachable leaf {leaf.id}"
|
||||||
|
)
|
||||||
|
if session_id not in (leaf.component_data[ct].session_ids or ()):
|
||||||
|
report_error(
|
||||||
|
f"{ct} session {session_id!r} leaf {leaf.id} is missing its marker"
|
||||||
|
)
|
||||||
|
|
||||||
|
for node in reachable_nodes:
|
||||||
|
cd = node.component_data[ct]
|
||||||
|
if cd.session_ref < 0:
|
||||||
|
report_error(f"node {node.id} {ct} session_ref={cd.session_ref}")
|
||||||
|
if cd.session_ids is not None and not cd.session_ids:
|
||||||
|
report_error(f"node {node.id} {ct} has an empty session_ids marker")
|
||||||
|
for session_id in cd.session_ids or ():
|
||||||
|
if node not in self._session_leaves.get(session_id, ()):
|
||||||
|
report_error(
|
||||||
|
f"node {node.id} {ct} session {session_id!r} marker is not indexed"
|
||||||
|
)
|
||||||
|
|
||||||
def node_has_component_data(
|
def node_has_component_data(
|
||||||
self, node: UnifiedTreeNode, target: EvictLayer = EvictLayer.DEVICE
|
self, node: UnifiedTreeNode, target: EvictLayer = EvictLayer.DEVICE
|
||||||
) -> bool:
|
) -> bool:
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
"""Session ref tracking for UnifiedRadixCache (``--enable-session-radix-cache``):
|
||||||
|
tag each request's KV by session_id for each tree component; ``release_radix_session``
|
||||||
|
(close) releases a session's tagged reference.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import logging
|
||||||
|
from collections import OrderedDict
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import TYPE_CHECKING, Optional
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from sglang.srt.managers.schedule_batch import Req
|
||||||
|
from sglang.srt.mem_cache.unified_cache.components.tree_component import (
|
||||||
|
TreeComponent,
|
||||||
|
)
|
||||||
|
from sglang.srt.mem_cache.unified_cache.unified_tree_core import UnifiedTreeCore
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Bounded guard against a request finishing after close. If a session id falls
|
||||||
|
# out of this LRU after 8192 later closes, an extremely late finish can tag
|
||||||
|
# again; explicit register_session clears the tombstone for intentional id reuse.
|
||||||
|
_CLOSED_SESSION_TOMBSTONE_LIMIT = 8192
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(kw_only=True)
|
||||||
|
class UnifiedSessionRefTracker:
|
||||||
|
"""Tags radix KV by session id; ``release_radix_session`` (close) releases a
|
||||||
|
session's tagged reference. Each component maintains its own session_ids,
|
||||||
|
session_ref and so on."""
|
||||||
|
|
||||||
|
components: tuple[TreeComponent, ...]
|
||||||
|
tree_core: UnifiedTreeCore
|
||||||
|
enable_session_radix_cache: bool
|
||||||
|
|
||||||
|
def __post_init__(self) -> None:
|
||||||
|
self.reset()
|
||||||
|
|
||||||
|
def reset(self) -> None:
|
||||||
|
self._closed_session_ids: OrderedDict[str, None] = OrderedDict()
|
||||||
|
self._session_incarnation_counter: int = 0
|
||||||
|
self._session_generations: dict[str, int] = {}
|
||||||
|
for component in self.components:
|
||||||
|
component.reset_session_state()
|
||||||
|
|
||||||
|
def session_id_for_req(self, req: Req) -> Optional[str]:
|
||||||
|
session_id = req.session_id
|
||||||
|
if session_id is None and req.session is not None:
|
||||||
|
session_id = req.session.session_id
|
||||||
|
return session_id
|
||||||
|
|
||||||
|
def register_session_ref(self, req: Req) -> None:
|
||||||
|
"""Register a non-streaming request's reusable leaves with each component."""
|
||||||
|
if not self.enable_session_radix_cache:
|
||||||
|
return
|
||||||
|
|
||||||
|
session = req.session
|
||||||
|
if session is not None and session.streaming:
|
||||||
|
return
|
||||||
|
|
||||||
|
session_id = self.session_id_for_req(req)
|
||||||
|
if session_id is None or session_id in self._closed_session_ids:
|
||||||
|
return
|
||||||
|
|
||||||
|
current_generation = self._session_generations.get(session_id)
|
||||||
|
if current_generation is None or req.session_generation != current_generation:
|
||||||
|
logger.warning("register_session_ref called for stale request; Skip it.")
|
||||||
|
return
|
||||||
|
|
||||||
|
assert req.last_node is not None
|
||||||
|
last_node = self.tree_core.node_by_id(req.last_node)
|
||||||
|
if last_node is self.tree_core.root_node:
|
||||||
|
return
|
||||||
|
|
||||||
|
for component in self.components:
|
||||||
|
leaf = component.resolve_session_leaf(req, last_node)
|
||||||
|
component.register_session_leaf(session_id, leaf)
|
||||||
|
|
||||||
|
def _remember_closed_session(self, session_id: str) -> None:
|
||||||
|
self._closed_session_ids[session_id] = None
|
||||||
|
self._closed_session_ids.move_to_end(session_id)
|
||||||
|
while len(self._closed_session_ids) > _CLOSED_SESSION_TOMBSTONE_LIMIT:
|
||||||
|
self._closed_session_ids.popitem(last=False)
|
||||||
|
|
||||||
|
def open_radix_session(self, session_id: str) -> Optional[int]:
|
||||||
|
self._closed_session_ids.pop(session_id, None)
|
||||||
|
self._session_incarnation_counter += 1
|
||||||
|
self._session_generations[session_id] = self._session_incarnation_counter
|
||||||
|
return self._session_incarnation_counter
|
||||||
|
|
||||||
|
def ensure_session_generation(self, session_id: str) -> int:
|
||||||
|
generation = self._session_generations.get(session_id)
|
||||||
|
if generation is None:
|
||||||
|
generation = self.open_radix_session(session_id)
|
||||||
|
return generation
|
||||||
|
|
||||||
|
def release_radix_session(self, session_id: str) -> int:
|
||||||
|
if not self.enable_session_radix_cache or session_id is None:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
if session_id in self._closed_session_ids:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
self._remember_closed_session(session_id)
|
||||||
|
self._session_generations.pop(session_id, None)
|
||||||
|
|
||||||
|
indexed = 0
|
||||||
|
for component in self.components:
|
||||||
|
indexed += component.release_session(session_id)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"release_session %s: indexed %d component leaves",
|
||||||
|
session_id,
|
||||||
|
indexed,
|
||||||
|
)
|
||||||
|
return 0
|
||||||
@@ -21,7 +21,7 @@ import sys
|
|||||||
from array import array
|
from array import array
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from enum import Enum, auto
|
from enum import Enum, auto
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple, Optional, Sequence
|
from typing import TYPE_CHECKING, Any, Callable, NamedTuple, Optional, Sequence
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
import torch
|
import torch
|
||||||
@@ -161,6 +161,7 @@ class UnifiedLRUList:
|
|||||||
component_type: ComponentType,
|
component_type: ComponentType,
|
||||||
tree_components: tuple[ComponentType, ...],
|
tree_components: tuple[ComponentType, ...],
|
||||||
use_host_ptr: bool = False,
|
use_host_ptr: bool = False,
|
||||||
|
is_referenced: Optional[Callable[[UnifiedTreeNode], bool]] = None,
|
||||||
):
|
):
|
||||||
self.component_type = component_type
|
self.component_type = component_type
|
||||||
# Pointer slot: host LRU uses offset slots so device/host pointers
|
# Pointer slot: host LRU uses offset slots so device/host pointers
|
||||||
@@ -171,6 +172,19 @@ class UnifiedLRUList:
|
|||||||
self.head.lru_next[self._pt] = self.tail
|
self.head.lru_next[self._pt] = self.tail
|
||||||
self.tail.lru_prev[self._pt] = self.head
|
self.tail.lru_prev[self._pt] = self.head
|
||||||
self.cache: dict[int, UnifiedTreeNode] = {}
|
self.cache: dict[int, UnifiedTreeNode] = {}
|
||||||
|
# Session partition: [head .. mid) holds session-referenced nodes and
|
||||||
|
# (mid .. tail] unreferenced ones, so evictions directly walks (tail -> head).
|
||||||
|
self._is_referenced = is_referenced
|
||||||
|
self.mid: Optional[UnifiedTreeNode] = None
|
||||||
|
self.cursor: Optional[UnifiedTreeNode] = None
|
||||||
|
if is_referenced is not None:
|
||||||
|
self.mid = UnifiedTreeNode(tree_components)
|
||||||
|
self.cursor = UnifiedTreeNode(tree_components)
|
||||||
|
for ct in tree_components:
|
||||||
|
for node in (self.mid, self.cursor):
|
||||||
|
node.component_data[ct].lock_ref = 1
|
||||||
|
node.component_data[ct].host_lock_ref = 1
|
||||||
|
self._add_node_after(self.head, self.mid)
|
||||||
|
|
||||||
def _add_node_after(self, prev_node: UnifiedTreeNode, new_node: UnifiedTreeNode):
|
def _add_node_after(self, prev_node: UnifiedTreeNode, new_node: UnifiedTreeNode):
|
||||||
pt = self._pt
|
pt = self._pt
|
||||||
@@ -180,7 +194,10 @@ class UnifiedLRUList:
|
|||||||
prev_node.lru_next[pt] = new_node
|
prev_node.lru_next[pt] = new_node
|
||||||
|
|
||||||
def _add_node(self, node: UnifiedTreeNode):
|
def _add_node(self, node: UnifiedTreeNode):
|
||||||
|
if self._is_referenced is None or self._is_referenced(node):
|
||||||
self._add_node_after(self.head, node)
|
self._add_node_after(self.head, node)
|
||||||
|
else:
|
||||||
|
self._add_node_after(self.mid, node)
|
||||||
|
|
||||||
def _remove_node(self, node: UnifiedTreeNode):
|
def _remove_node(self, node: UnifiedTreeNode):
|
||||||
pt = self._pt
|
pt = self._pt
|
||||||
@@ -205,19 +222,50 @@ class UnifiedLRUList:
|
|||||||
self._remove_node(node)
|
self._remove_node(node)
|
||||||
self._add_node(node)
|
self._add_node(node)
|
||||||
|
|
||||||
|
def cursor_begin(self):
|
||||||
|
if self.cursor.lru_prev[self._pt] is not None:
|
||||||
|
self._remove_node(self.cursor)
|
||||||
|
self._add_node_after(self.tail.lru_prev[self._pt], self.cursor)
|
||||||
|
|
||||||
|
def cursor_next(self, *, host_lock: bool = False):
|
||||||
|
pt = self._pt
|
||||||
|
ct = self.component_type
|
||||||
|
x = self.cursor.lru_prev[pt]
|
||||||
|
while x is not self.head:
|
||||||
|
cd = x.component_data[ct]
|
||||||
|
if (cd.host_lock_ref if host_lock else cd.lock_ref) == 0:
|
||||||
|
break
|
||||||
|
x = x.lru_prev[pt]
|
||||||
|
if x is self.head:
|
||||||
|
return None
|
||||||
|
self._remove_node(self.cursor)
|
||||||
|
self._add_node_after(x.lru_prev[pt], self.cursor)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def cursor_end(self):
|
||||||
|
"""Unlink the walk-cursor sentinel after a walk."""
|
||||||
|
self._remove_node(self.cursor)
|
||||||
|
|
||||||
def reset_node_and_parents_mru(
|
def reset_node_and_parents_mru(
|
||||||
self,
|
self,
|
||||||
node: UnifiedTreeNode,
|
node: UnifiedTreeNode,
|
||||||
root_node: UnifiedTreeNode,
|
root_node: UnifiedTreeNode,
|
||||||
should_include,
|
should_include,
|
||||||
):
|
):
|
||||||
prev_node = self.head
|
is_referenced = self._is_referenced
|
||||||
|
prev_ref = self.head
|
||||||
|
prev_unref = self.head if self.mid is None else self.mid
|
||||||
while node != root_node:
|
while node != root_node:
|
||||||
if should_include(node):
|
if should_include(node):
|
||||||
assert node.id in self.cache
|
assert node.id in self.cache
|
||||||
|
part = is_referenced is None or is_referenced(node)
|
||||||
self._remove_node(node)
|
self._remove_node(node)
|
||||||
self._add_node_after(prev_node, node)
|
if part:
|
||||||
prev_node = node
|
self._add_node_after(prev_ref, node)
|
||||||
|
prev_ref = node
|
||||||
|
else:
|
||||||
|
self._add_node_after(prev_unref, node)
|
||||||
|
prev_unref = node
|
||||||
node = node.parent
|
node = node.parent
|
||||||
|
|
||||||
def reset_node_and_window_ancestors_mru(
|
def reset_node_and_window_ancestors_mru(
|
||||||
@@ -227,14 +275,21 @@ class UnifiedLRUList:
|
|||||||
window_size: int,
|
window_size: int,
|
||||||
should_include,
|
should_include,
|
||||||
):
|
):
|
||||||
prev_node = self.head
|
is_referenced = self._is_referenced
|
||||||
|
prev_ref = self.head
|
||||||
|
prev_unref = self.head if self.mid is None else self.mid
|
||||||
accumulated = 0
|
accumulated = 0
|
||||||
while node != root_node and accumulated < window_size:
|
while node != root_node and accumulated < window_size:
|
||||||
if should_include(node):
|
if should_include(node):
|
||||||
assert node.id in self.cache
|
assert node.id in self.cache
|
||||||
|
part = is_referenced is None or is_referenced(node)
|
||||||
self._remove_node(node)
|
self._remove_node(node)
|
||||||
self._add_node_after(prev_node, node)
|
if part:
|
||||||
prev_node = node
|
self._add_node_after(prev_ref, node)
|
||||||
|
prev_ref = node
|
||||||
|
else:
|
||||||
|
self._add_node_after(prev_unref, node)
|
||||||
|
prev_unref = node
|
||||||
accumulated += len(node.key)
|
accumulated += len(node.key)
|
||||||
node = node.parent
|
node = node.parent
|
||||||
|
|
||||||
@@ -334,6 +389,7 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
self.write_through_threshold = 256
|
self.write_through_threshold = 256
|
||||||
self.is_write_back = False
|
self.is_write_back = False
|
||||||
self.has_swa_host_pool = False
|
self.has_swa_host_pool = False
|
||||||
|
self.enable_session_radix_cache = params.enable_session_radix_cache
|
||||||
self.eviction_strategy = get_eviction_strategy(params.eviction_policy.lower())
|
self.eviction_strategy = get_eviction_strategy(params.eviction_policy.lower())
|
||||||
|
|
||||||
# ``device`` is derived from the construction-time allocator; the
|
# ``device`` is derived from the construction-time allocator; the
|
||||||
@@ -361,6 +417,11 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
|
|
||||||
# ==== Tree API ====
|
# ==== Tree API ====
|
||||||
|
|
||||||
|
def _session_lru_predicate(self, ct: ComponentType):
|
||||||
|
if not self.enable_session_radix_cache or ct is ComponentType.FULL:
|
||||||
|
return None
|
||||||
|
return lambda node: node.component_data[ct].session_ref > 0
|
||||||
|
|
||||||
def reset(self) -> None:
|
def reset(self) -> None:
|
||||||
"""Rebuild the root, LRUs, sizes, evictable-leaf sets, and the empty
|
"""Rebuild the root, LRUs, sizes, evictable-leaf sets, and the empty
|
||||||
match result."""
|
match result."""
|
||||||
@@ -382,13 +443,21 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
self.component_protected_size_ = {ct: 0 for ct in self.component_types}
|
self.component_protected_size_ = {ct: 0 for ct in self.component_types}
|
||||||
|
|
||||||
self.lru_lists = {
|
self.lru_lists = {
|
||||||
ct: UnifiedLRUList(ct, self.component_types) for ct in self.component_types
|
ct: UnifiedLRUList(
|
||||||
|
ct, self.component_types, is_referenced=self._session_lru_predicate(ct)
|
||||||
|
)
|
||||||
|
for ct in self.component_types
|
||||||
}
|
}
|
||||||
|
|
||||||
self.evictable_device_leaves: set[UnifiedTreeNode] = set()
|
self.evictable_device_leaves: set[UnifiedTreeNode] = set()
|
||||||
self.evictable_host_leaves: set[UnifiedTreeNode] = set()
|
self.evictable_host_leaves: set[UnifiedTreeNode] = set()
|
||||||
self.host_lru_lists = {
|
self.host_lru_lists = {
|
||||||
ct: UnifiedLRUList(ct, self.component_types, use_host_ptr=True)
|
ct: UnifiedLRUList(
|
||||||
|
ct,
|
||||||
|
self.component_types,
|
||||||
|
use_host_ptr=True,
|
||||||
|
is_referenced=self._session_lru_predicate(ct),
|
||||||
|
)
|
||||||
for ct in self.component_types
|
for ct in self.component_types
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -767,7 +836,12 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
self.root_node.priority = max(self.root_node.priority, priority)
|
self.root_node.priority = max(self.root_node.priority, priority)
|
||||||
if len(key) == 0:
|
if len(key) == 0:
|
||||||
return InsertStepResult(
|
return InsertStepResult(
|
||||||
actions=[], result=InsertResult(prefix_len=0, mamba_exist=True)
|
actions=[],
|
||||||
|
result=InsertResult(
|
||||||
|
prefix_len=0,
|
||||||
|
mamba_exist=True,
|
||||||
|
last_device_node=self.root_node.id,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
self._ongoing_insert_walk_state = _InsertWalkState(
|
self._ongoing_insert_walk_state = _InsertWalkState(
|
||||||
@@ -902,7 +976,10 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
# e.g. Mamba attaches mamba_value to the leaf node
|
# e.g. Mamba attaches mamba_value to the leaf node
|
||||||
# All hooks run before their emitted actions execute; an action failure
|
# All hooks run before their emitted actions execute; an action failure
|
||||||
# fail-stops the process, so partial-commit state is never observed.
|
# fail-stops the process, so partial-commit state is never observed.
|
||||||
state.result = InsertResult(prefix_len=state.total_prefix_length)
|
state.result = InsertResult(
|
||||||
|
prefix_len=state.total_prefix_length,
|
||||||
|
last_device_node=state.target_node.id,
|
||||||
|
)
|
||||||
for component in self.components:
|
for component in self.components:
|
||||||
component.commit_insert_component_data(
|
component.commit_insert_component_data(
|
||||||
node=state.target_node,
|
node=state.target_node,
|
||||||
@@ -1287,6 +1364,7 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
is_leaf = node in self.evictable_host_leaves
|
is_leaf = node in self.evictable_host_leaves
|
||||||
|
|
||||||
trigger_priority = trigger.eviction_priority(is_leaf)
|
trigger_priority = trigger.eviction_priority(is_leaf)
|
||||||
|
base_evicted = False
|
||||||
|
|
||||||
for comp in self.components:
|
for comp in self.components:
|
||||||
if comp.eviction_priority(is_leaf) <= trigger_priority:
|
if comp.eviction_priority(is_leaf) <= trigger_priority:
|
||||||
@@ -1304,6 +1382,8 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
continue
|
continue
|
||||||
if EvictLayer.HOST in target and cd.host_lock_ref != 0:
|
if EvictLayer.HOST in target and cd.host_lock_ref != 0:
|
||||||
continue
|
continue
|
||||||
|
if cd.session_ref > 0 and trigger.session_ref(node) == 0:
|
||||||
|
continue
|
||||||
if EvictLayer.DEVICE in target:
|
if EvictLayer.DEVICE in target:
|
||||||
assert cd.lock_ref == 0
|
assert cd.lock_ref == 0
|
||||||
if EvictLayer.HOST in target:
|
if EvictLayer.HOST in target:
|
||||||
@@ -1316,6 +1396,8 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
device_frees=device_frees,
|
device_frees=device_frees,
|
||||||
host_frees=host_frees,
|
host_frees=host_frees,
|
||||||
)
|
)
|
||||||
|
if comp.component_type == BASE_COMPONENT_TYPE:
|
||||||
|
base_evicted = True
|
||||||
|
|
||||||
# Now that all components (including SWA which depends on Full.value)
|
# Now that all components (including SWA which depends on Full.value)
|
||||||
# have been freed, we can safely tombstone Full.value.
|
# have been freed, we can safely tombstone Full.value.
|
||||||
@@ -1326,9 +1408,15 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
):
|
):
|
||||||
node.component_data[trigger.component_type].value = None
|
node.component_data[trigger.component_type].value = None
|
||||||
|
|
||||||
|
if EvictLayer.DEVICE in target and base_evicted:
|
||||||
|
node.component_data[BASE_COMPONENT_TYPE].value = None
|
||||||
|
|
||||||
self._update_evictable_leaf_sets(node)
|
self._update_evictable_leaf_sets(node)
|
||||||
|
|
||||||
def _remove_leaf_from_parent(self, node: UnifiedTreeNode):
|
def _remove_leaf_from_parent(self, node: UnifiedTreeNode):
|
||||||
|
for component in self.components:
|
||||||
|
component.discard_deleted_session_leaf(node)
|
||||||
|
|
||||||
key = node.key.child_key(self.page_size)
|
key = node.key.child_key(self.page_size)
|
||||||
v = node.parent.children.pop(key, None)
|
v = node.parent.children.pop(key, None)
|
||||||
assert v == node
|
assert v == node
|
||||||
@@ -1876,6 +1964,10 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
f"[Leaf] {len(overlap)} in both sets: {[n.id for n in list(overlap)[:5]]}"
|
f"[Leaf] {len(overlap)} in both sets: {[n.id for n in list(overlap)[:5]]}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.enable_session_radix_cache:
|
||||||
|
for component in self.components:
|
||||||
|
component.validate_session_state(all_node_set, E)
|
||||||
|
|
||||||
# Stale nodes: leaf sets must only contain tree-reachable nodes
|
# Stale nodes: leaf sets must only contain tree-reachable nodes
|
||||||
stale = self.evictable_device_leaves - all_node_set
|
stale = self.evictable_device_leaves - all_node_set
|
||||||
if stale:
|
if stale:
|
||||||
@@ -2011,6 +2103,10 @@ class UnifiedTreeCore(UnifiedTreeCoreInterface):
|
|||||||
while x is not None and x != lru.tail:
|
while x is not None and x != lru.tail:
|
||||||
if x.lru_prev[pt] != prev:
|
if x.lru_prev[pt] != prev:
|
||||||
errors.append(f"[{label}][{ct}] broken prev at node {x.id}")
|
errors.append(f"[{label}][{ct}] broken prev at node {x.id}")
|
||||||
|
if x is lru.mid or x is lru.cursor:
|
||||||
|
prev = x
|
||||||
|
x = x.lru_next[pt]
|
||||||
|
continue
|
||||||
if x.id not in lru.cache:
|
if x.id not in lru.cache:
|
||||||
errors.append(f"[{label}][{ct}] node {x.id} in list not cache")
|
errors.append(f"[{label}][{ct}] node {x.id} in list not cache")
|
||||||
if x.id in visited:
|
if x.id in visited:
|
||||||
|
|||||||
@@ -54,6 +54,9 @@ from sglang.srt.mem_cache.unified_cache.components import (
|
|||||||
SWAComponent,
|
SWAComponent,
|
||||||
TreeComponent,
|
TreeComponent,
|
||||||
)
|
)
|
||||||
|
from sglang.srt.mem_cache.unified_cache.session_ref_tracker import (
|
||||||
|
UnifiedSessionRefTracker,
|
||||||
|
)
|
||||||
from sglang.srt.mem_cache.unified_cache.tree_core_registry import create_tree_core
|
from sglang.srt.mem_cache.unified_cache.tree_core_registry import create_tree_core
|
||||||
from sglang.srt.mem_cache.unified_cache.unified_tree_core import ( # noqa: F401
|
from sglang.srt.mem_cache.unified_cache.unified_tree_core import ( # noqa: F401
|
||||||
NodeId,
|
NodeId,
|
||||||
@@ -135,6 +138,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
|
|
||||||
assert params.tree_components is not None
|
assert params.tree_components is not None
|
||||||
self.tree_components = tuple(params.tree_components)
|
self.tree_components = tuple(params.tree_components)
|
||||||
|
self.enable_session_radix_cache = params.enable_session_radix_cache
|
||||||
component_registry = COMPONENT_REGISTRY
|
component_registry = COMPONENT_REGISTRY
|
||||||
if params.component_registry_override:
|
if params.component_registry_override:
|
||||||
component_registry = {
|
component_registry = {
|
||||||
@@ -170,6 +174,13 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
for component in self.components.values():
|
for component in self.components.values():
|
||||||
component.tree_core = self.tree_core
|
component.tree_core = self.tree_core
|
||||||
|
|
||||||
|
# Session ref tracking (--enable-session-radix-cache).
|
||||||
|
self.session_refs = UnifiedSessionRefTracker(
|
||||||
|
components=self._components_tuple,
|
||||||
|
tree_core=self.tree_core,
|
||||||
|
enable_session_radix_cache=self.enable_session_radix_cache,
|
||||||
|
)
|
||||||
|
|
||||||
self.sidecar_pool_specs: list[SidecarPoolSpec] = []
|
self.sidecar_pool_specs: list[SidecarPoolSpec] = []
|
||||||
|
|
||||||
# Streaming session: embedded StreamingSession with self as inner.
|
# Streaming session: embedded StreamingSession with self as inner.
|
||||||
@@ -276,6 +287,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
def _reset_full(self) -> None:
|
def _reset_full(self) -> None:
|
||||||
"""Full reset: destroy entire tree and all state."""
|
"""Full reset: destroy entire tree and all state."""
|
||||||
self.tree_core.reset()
|
self.tree_core.reset()
|
||||||
|
self.session_refs.reset()
|
||||||
|
|
||||||
# Reset Controller.
|
# Reset Controller.
|
||||||
self.session.slots.clear()
|
self.session.slots.clear()
|
||||||
@@ -668,12 +680,23 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
|
|
||||||
self._dec_req_lock(req, skip_swa=req.swa_prefix_lock_released)
|
self._dec_req_lock(req, skip_swa=req.swa_prefix_lock_released)
|
||||||
|
|
||||||
|
if is_insert and result is not None and result.last_device_node is not None:
|
||||||
|
req.last_node = result.last_device_node
|
||||||
|
|
||||||
# cleanup
|
# cleanup
|
||||||
for comp in self._components_tuple:
|
for comp in self._components_tuple:
|
||||||
comp.cleanup_after_caching_req(
|
comp.cleanup_after_caching_req(
|
||||||
req, is_finished=True, insert_result=result, insert_params=insert_params
|
req, is_finished=True, insert_result=result, insert_params=insert_params
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if self.enable_session_radix_cache and result is not None:
|
||||||
|
from sglang.srt.managers.schedule_batch import FINISH_ABORT
|
||||||
|
|
||||||
|
if req.finished_reason is not None and not isinstance(
|
||||||
|
req.finished_reason, FINISH_ABORT
|
||||||
|
):
|
||||||
|
self.session_refs.register_session_ref(req)
|
||||||
|
|
||||||
def cache_unfinished_req(self, req: Req, chunked: bool = False, **kwargs) -> None:
|
def cache_unfinished_req(self, req: Req, chunked: bool = False, **kwargs) -> None:
|
||||||
if self.session.try_cache_unfinished_req(req, chunked=chunked, **kwargs):
|
if self.session.try_cache_unfinished_req(req, chunked=chunked, **kwargs):
|
||||||
return
|
return
|
||||||
@@ -1998,6 +2021,17 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
def supports_mamba(self) -> bool:
|
def supports_mamba(self) -> bool:
|
||||||
return self.is_mamba_enabled
|
return self.is_mamba_enabled
|
||||||
|
|
||||||
|
# ---- Session radix cache API (delegates to composed UnifiedSessionRefTracker) ----
|
||||||
|
|
||||||
|
def open_radix_session(self, session_id: str) -> Optional[int]:
|
||||||
|
return self.session_refs.open_radix_session(session_id)
|
||||||
|
|
||||||
|
def ensure_session_generation(self, session_id: str) -> int:
|
||||||
|
return self.session_refs.ensure_session_generation(session_id)
|
||||||
|
|
||||||
|
def release_radix_session(self, session_id: str) -> int:
|
||||||
|
return self.session_refs.release_radix_session(session_id)
|
||||||
|
|
||||||
# ---- Streaming session API (delegates to composed StreamingSession) ----
|
# ---- Streaming session API (delegates to composed StreamingSession) ----
|
||||||
|
|
||||||
def supports_streaming_session(self) -> bool:
|
def supports_streaming_session(self) -> bool:
|
||||||
@@ -2055,6 +2089,7 @@ class UnifiedRadixCache(BasePrefixCache):
|
|||||||
return self.tree_core.all_mamba_values_flatten()
|
return self.tree_core.all_mamba_values_flatten()
|
||||||
|
|
||||||
def available_and_evictable_str(self) -> str:
|
def available_and_evictable_str(self) -> str:
|
||||||
|
# TODO(zhangmj): need more detailed log info for session reference.
|
||||||
if self.supports_swa():
|
if self.supports_swa():
|
||||||
full_available_size = self.token_to_kv_pool_allocator.full_available_size()
|
full_available_size = self.token_to_kv_pool_allocator.full_available_size()
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -1409,7 +1409,7 @@ class ServerArgs:
|
|||||||
] = False
|
] = False
|
||||||
enable_session_radix_cache: A[
|
enable_session_radix_cache: A[
|
||||||
bool,
|
bool,
|
||||||
"Hold per-session KV as ordinary evictable radix entries, tagged by session id and bulk-evicted on close. Requires --radix-eviction-policy priority.",
|
"Track per-session references on UnifiedRadixCache KV: eviction consumes unreferenced entries before referenced ones, and closing a session only dereferences its KV.",
|
||||||
NS("memory"),
|
NS("memory"),
|
||||||
] = False
|
] = False
|
||||||
|
|
||||||
@@ -7568,11 +7568,6 @@ class ServerArgs:
|
|||||||
envs.SGLANG_OPT_FP8_WO_A_GEMM.set(False)
|
envs.SGLANG_OPT_FP8_WO_A_GEMM.set(False)
|
||||||
|
|
||||||
def _handle_cache_compatibility(self):
|
def _handle_cache_compatibility(self):
|
||||||
if self.enable_session_radix_cache and self.radix_eviction_policy != "priority":
|
|
||||||
raise ValueError(
|
|
||||||
"--enable-session-radix-cache requires --radix-eviction-policy priority"
|
|
||||||
)
|
|
||||||
|
|
||||||
if self.enable_hierarchical_cache and self.disable_radix_cache:
|
if self.enable_hierarchical_cache and self.disable_radix_cache:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
"The arguments enable-hierarchical-cache and disable-radix-cache are mutually exclusive "
|
"The arguments enable-hierarchical-cache and disable-radix-cache are mutually exclusive "
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ class SessionController:
|
|||||||
mm.release_features()
|
mm.release_features()
|
||||||
node.req.multimodal_inputs = None
|
node.req.multimodal_inputs = None
|
||||||
|
|
||||||
|
self.tree_cache.release_radix_session(session_id)
|
||||||
self.tree_cache.release_session(session_id)
|
self.tree_cache.release_session(session_id)
|
||||||
del self.sessions[session_id]
|
del self.sessions[session_id]
|
||||||
log_info_on_rank0(
|
log_info_on_rank0(
|
||||||
|
|||||||
@@ -1,174 +0,0 @@
|
|||||||
"""Manual test for the session radix cache (--enable-session-radix-cache).
|
|
||||||
Run directly: python test/manual/core/test_session_radix_cache.py
|
|
||||||
"""
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from array import array
|
|
||||||
from types import SimpleNamespace
|
|
||||||
|
|
||||||
import torch
|
|
||||||
|
|
||||||
from sglang.srt.mem_cache.allocator import TokenToKVPoolAllocator
|
|
||||||
from sglang.srt.mem_cache.base_prefix_cache import (
|
|
||||||
EvictParams,
|
|
||||||
InsertParams,
|
|
||||||
MatchPrefixParams,
|
|
||||||
)
|
|
||||||
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
|
||||||
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool, ReqToTokenPool
|
|
||||||
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey
|
|
||||||
|
|
||||||
|
|
||||||
class TestSessionRadixCache(unittest.TestCase):
|
|
||||||
def setUp(self):
|
|
||||||
dtype = torch.float16
|
|
||||||
kv = MHATokenToKVPool(
|
|
||||||
size=64,
|
|
||||||
page_size=1,
|
|
||||||
dtype=dtype,
|
|
||||||
head_num=2,
|
|
||||||
head_dim=8,
|
|
||||||
layer_num=1,
|
|
||||||
device="cpu",
|
|
||||||
enable_memory_saver=False,
|
|
||||||
)
|
|
||||||
allocator = TokenToKVPoolAllocator(
|
|
||||||
size=64, dtype=dtype, device="cpu", kvcache=kv, need_sort=False
|
|
||||||
)
|
|
||||||
req_to_token_pool = ReqToTokenPool(
|
|
||||||
size=8, max_context_len=1024, device="cpu", enable_memory_saver=False
|
|
||||||
)
|
|
||||||
self.cache = RadixCache(
|
|
||||||
CacheInitParams(
|
|
||||||
disable=False,
|
|
||||||
req_to_token_pool=req_to_token_pool,
|
|
||||||
token_to_kv_pool_allocator=allocator,
|
|
||||||
page_size=1,
|
|
||||||
eviction_policy="lru",
|
|
||||||
enable_kv_cache_events=False,
|
|
||||||
enable_session_radix_cache=True,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _insert(self, toks):
|
|
||||||
idx = self.cache.token_to_kv_pool_allocator.alloc(len(toks))
|
|
||||||
self.cache.insert(
|
|
||||||
InsertParams(key=RadixKey(array("q", toks)), value=idx.to(torch.int64))
|
|
||||||
)
|
|
||||||
|
|
||||||
def _tag(self, toks, sid):
|
|
||||||
self.cache._tag_session_leaf(
|
|
||||||
SimpleNamespace(session_id=sid),
|
|
||||||
RadixKey(array("q", toks)),
|
|
||||||
node=self._leaf(toks),
|
|
||||||
)
|
|
||||||
|
|
||||||
def _cached(self, toks):
|
|
||||||
return int(
|
|
||||||
self.cache.match_prefix(
|
|
||||||
MatchPrefixParams(key=RadixKey(array("q", toks)))
|
|
||||||
).device_indices.numel()
|
|
||||||
)
|
|
||||||
|
|
||||||
def _leaf(self, toks):
|
|
||||||
return self.cache.match_prefix(
|
|
||||||
MatchPrefixParams(key=RadixKey(array("q", toks)))
|
|
||||||
).last_device_node
|
|
||||||
|
|
||||||
def test_tag_with_known_node_skips_match_prefix(self):
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
leaf = self._leaf([1, 2, 3, 4])
|
|
||||||
orig_match_prefix = self.cache.match_prefix
|
|
||||||
|
|
||||||
def fail_match_prefix(_params):
|
|
||||||
raise AssertionError("match_prefix should not run when node is supplied")
|
|
||||||
|
|
||||||
self.cache.match_prefix = fail_match_prefix
|
|
||||||
try:
|
|
||||||
self.cache._tag_session_leaf(
|
|
||||||
SimpleNamespace(session_id="S"),
|
|
||||||
RadixKey(array("q", [1, 2, 3, 4])),
|
|
||||||
node=leaf,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
self.cache.match_prefix = orig_match_prefix
|
|
||||||
self.assertEqual(getattr(leaf, "session_ids", None), {"S"})
|
|
||||||
self.assertIn(leaf, self.cache._session_leaves["S"])
|
|
||||||
|
|
||||||
def test_disabled_cache_does_not_tag_session_kv(self):
|
|
||||||
self.cache.enable_session_radix_cache = False
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "S")
|
|
||||||
self.assertIsNone(getattr(self._leaf([1, 2, 3, 4]), "session_ids", None))
|
|
||||||
|
|
||||||
def test_shared_prefix_frees_only_unique_tail(self):
|
|
||||||
# A/B share prefix [1,2]; close(A) frees only A's tail, B + shared stay.
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "A")
|
|
||||||
self._insert([1, 2, 5, 6])
|
|
||||||
self._tag([1, 2, 5, 6], "B")
|
|
||||||
self.assertGreater(self.cache.release_radix_session("A"), 0)
|
|
||||||
self.assertEqual(self._cached([1, 2, 3, 4]), 2) # only shared [1,2] left
|
|
||||||
self.assertEqual(self._cached([1, 2, 5, 6]), 4) # B intact
|
|
||||||
|
|
||||||
def test_same_leaf_freed_only_on_last_holder(self):
|
|
||||||
# Identical content -> one leaf held by {A,B}; freed only on last close.
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "A")
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "B")
|
|
||||||
self.assertEqual(
|
|
||||||
getattr(self._leaf([1, 2, 3, 4]), "session_ids", None), {"A", "B"}
|
|
||||||
)
|
|
||||||
self.assertEqual(self.cache.release_radix_session("A"), 0) # B still holds
|
|
||||||
self.assertEqual(self._cached([1, 2, 3, 4]), 4)
|
|
||||||
self.assertEqual(self.cache.release_radix_session("B"), 1) # last holder frees
|
|
||||||
self.assertEqual(self._cached([1, 2, 3, 4]), 0)
|
|
||||||
|
|
||||||
def test_legacy_release_does_not_release_radix_session(self):
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "S")
|
|
||||||
self.cache.release_session("S")
|
|
||||||
self.assertEqual(self._cached([1, 2, 3, 4]), 4)
|
|
||||||
self.assertEqual(self.cache.release_radix_session("S"), 1)
|
|
||||||
|
|
||||||
def test_tag_is_lru_neutral_not_pinned(self):
|
|
||||||
# The tag must add no lock/pin: a tagged, never-closed node is evictable.
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "S")
|
|
||||||
leaf = self._leaf([1, 2, 3, 4])
|
|
||||||
self.assertEqual(leaf.lock_ref, 0)
|
|
||||||
self.assertEqual(self.cache.protected_size(), 0)
|
|
||||||
self.assertIn(leaf, self.cache.evictable_leaves)
|
|
||||||
self.cache.evict(EvictParams(num_tokens=4)) # LRU reclaims it while open
|
|
||||||
self.assertEqual(self._cached([1, 2, 3, 4]), 0)
|
|
||||||
self.assertNotIn("S", self.cache._session_leaves)
|
|
||||||
self.assertEqual(
|
|
||||||
self.cache.release_radix_session("S"), 0
|
|
||||||
) # late close is a no-op
|
|
||||||
|
|
||||||
def test_close_tombstone_blocks_late_finish(self):
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "S")
|
|
||||||
self.assertEqual(self.cache.release_radix_session("S"), 1)
|
|
||||||
|
|
||||||
self._insert([5, 6, 7, 8])
|
|
||||||
self._tag([5, 6, 7, 8], "S") # simulates a finish racing after close
|
|
||||||
self.assertIsNone(getattr(self._leaf([5, 6, 7, 8]), "session_ids", None))
|
|
||||||
|
|
||||||
def test_tombstoned_shared_holder_cannot_retag_after_last_holder_close(self):
|
|
||||||
self._insert([1, 2, 3, 4])
|
|
||||||
self._tag([1, 2, 3, 4], "A")
|
|
||||||
self._tag([1, 2, 3, 4], "B")
|
|
||||||
|
|
||||||
self.assertEqual(self.cache.release_radix_session("B"), 0)
|
|
||||||
self.assertEqual(getattr(self._leaf([1, 2, 3, 4]), "session_ids", None), {"A"})
|
|
||||||
self.assertEqual(self.cache.release_radix_session("A"), 1)
|
|
||||||
|
|
||||||
self._insert([5, 6, 7, 8])
|
|
||||||
self._tag([5, 6, 7, 8], "B")
|
|
||||||
self.assertIsNone(getattr(self._leaf([5, 6, 7, 8]), "session_ids", None))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
"""Tests for session references on UnifiedRadixCache."""
|
||||||
|
|
||||||
|
from sglang.test.ci.ci_register import register_cpu_ci
|
||||||
|
|
||||||
|
register_cpu_ci(est_time=5, suite="base-a-test-cpu")
|
||||||
|
|
||||||
|
import ast
|
||||||
|
import unittest
|
||||||
|
from array import array
|
||||||
|
from pathlib import Path
|
||||||
|
from types import SimpleNamespace
|
||||||
|
|
||||||
|
import torch
|
||||||
|
|
||||||
|
from sglang.srt.mem_cache.allocator import TokenToKVPoolAllocator
|
||||||
|
from sglang.srt.mem_cache.base_prefix_cache import (
|
||||||
|
EvictParams,
|
||||||
|
InsertParams,
|
||||||
|
MatchPrefixParams,
|
||||||
|
)
|
||||||
|
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
|
||||||
|
from sglang.srt.mem_cache.memory_pool import MHATokenToKVPool, ReqToTokenPool
|
||||||
|
from sglang.srt.mem_cache.radix_cache import RadixCache, RadixKey
|
||||||
|
from sglang.srt.mem_cache.unified_cache.components import ComponentType
|
||||||
|
from sglang.srt.mem_cache.unified_radix_cache import UnifiedRadixCache
|
||||||
|
from sglang.test.test_utils import CustomTestCase
|
||||||
|
|
||||||
|
REPO_ROOT = Path(__file__).resolve().parents[4]
|
||||||
|
MEM_CACHE_ROOT = REPO_ROOT / "python/sglang/srt/mem_cache"
|
||||||
|
|
||||||
|
|
||||||
|
def class_bases(path: Path, class_name: str) -> set[str]:
|
||||||
|
tree = ast.parse(path.read_text())
|
||||||
|
class_node = next(
|
||||||
|
node
|
||||||
|
for node in tree.body
|
||||||
|
if isinstance(node, ast.ClassDef) and node.name == class_name
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
base.id if isinstance(base, ast.Name) else ast.unparse(base)
|
||||||
|
for base in class_node.bases
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionCacheOwnership(CustomTestCase):
|
||||||
|
def test_only_unified_radix_cache_owns_session_ref_tracker(self):
|
||||||
|
ordinary_mixin = MEM_CACHE_ROOT / "session_radix_cache.py"
|
||||||
|
radix_cache = MEM_CACHE_ROOT / "radix_cache.py"
|
||||||
|
hiradix_cache = MEM_CACHE_ROOT / "hiradix_cache.py"
|
||||||
|
evict_policy = MEM_CACHE_ROOT / "evict_policy.py"
|
||||||
|
unified_cache = MEM_CACHE_ROOT / "unified_radix_cache.py"
|
||||||
|
session_ref_tracker = (
|
||||||
|
MEM_CACHE_ROOT / "unified_cache" / "session_ref_tracker.py"
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertFalse(ordinary_mixin.exists())
|
||||||
|
ordinary_source = "\n".join(
|
||||||
|
path.read_text() for path in (radix_cache, hiradix_cache, evict_policy)
|
||||||
|
)
|
||||||
|
for removed_symbol in (
|
||||||
|
"SessionRadixCacheMixin",
|
||||||
|
"SessionAwareEvictionStrategy",
|
||||||
|
"session_ref",
|
||||||
|
"_session_on_",
|
||||||
|
"_session_forget_node",
|
||||||
|
"_account_new_evictable_node",
|
||||||
|
"_supports_session_radix_cache",
|
||||||
|
"enable_session_radix_cache",
|
||||||
|
):
|
||||||
|
self.assertNotIn(removed_symbol, ordinary_source)
|
||||||
|
self.assertNotIn(
|
||||||
|
"SessionRadixCacheMixin", class_bases(radix_cache, "RadixCache")
|
||||||
|
)
|
||||||
|
# Session behavior is composed, not mixed in (general-code-style rule).
|
||||||
|
self.assertEqual(
|
||||||
|
class_bases(unified_cache, "UnifiedRadixCache"), {"BasePrefixCache"}
|
||||||
|
)
|
||||||
|
self.assertIn("UnifiedSessionRefTracker", session_ref_tracker.read_text())
|
||||||
|
self.assertNotIn("SessionUnifiedRadixCacheMixin", unified_cache.read_text())
|
||||||
|
|
||||||
|
for component in (
|
||||||
|
"full_component.py",
|
||||||
|
"swa_component.py",
|
||||||
|
"mamba_component.py",
|
||||||
|
):
|
||||||
|
self.assertIn(
|
||||||
|
"session_ref",
|
||||||
|
(
|
||||||
|
MEM_CACHE_ROOT / "unified_cache" / "components" / component
|
||||||
|
).read_text(),
|
||||||
|
)
|
||||||
|
|
||||||
|
registry = MEM_CACHE_ROOT / "registry.py"
|
||||||
|
self.assertIn(
|
||||||
|
"--enable-session-radix-cache requires UnifiedRadixCache",
|
||||||
|
registry.read_text(),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def make_params(enable_session: bool) -> CacheInitParams:
|
||||||
|
dtype = torch.float16
|
||||||
|
kv_pool = MHATokenToKVPool(
|
||||||
|
size=64,
|
||||||
|
page_size=1,
|
||||||
|
dtype=dtype,
|
||||||
|
head_num=2,
|
||||||
|
head_dim=8,
|
||||||
|
layer_num=1,
|
||||||
|
device="cpu",
|
||||||
|
enable_memory_saver=False,
|
||||||
|
)
|
||||||
|
allocator = TokenToKVPoolAllocator(
|
||||||
|
size=64,
|
||||||
|
dtype=dtype,
|
||||||
|
device="cpu",
|
||||||
|
kvcache=kv_pool,
|
||||||
|
need_sort=False,
|
||||||
|
)
|
||||||
|
req_pool = ReqToTokenPool(
|
||||||
|
size=8,
|
||||||
|
max_context_len=128,
|
||||||
|
device="cpu",
|
||||||
|
enable_memory_saver=False,
|
||||||
|
)
|
||||||
|
return CacheInitParams(
|
||||||
|
disable=False,
|
||||||
|
req_to_token_pool=req_pool,
|
||||||
|
token_to_kv_pool_allocator=allocator,
|
||||||
|
page_size=1,
|
||||||
|
eviction_policy="lru",
|
||||||
|
enable_session_radix_cache=enable_session,
|
||||||
|
tree_components=(ComponentType.FULL,),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def insert(cache, token_ids):
|
||||||
|
"""Insert and return the tail node; the cache boundary hands back a NodeId."""
|
||||||
|
indices = cache.token_to_kv_pool_allocator.alloc(len(token_ids))
|
||||||
|
node_id = cache.insert(
|
||||||
|
InsertParams(
|
||||||
|
key=RadixKey(array("q", token_ids)),
|
||||||
|
value=indices.to(torch.int64),
|
||||||
|
)
|
||||||
|
).last_device_node
|
||||||
|
return cache.tree_core.node_by_id(node_id)
|
||||||
|
|
||||||
|
|
||||||
|
def match_len(cache, token_ids) -> int:
|
||||||
|
return len(
|
||||||
|
cache.match_prefix(
|
||||||
|
MatchPrefixParams(key=RadixKey(array("q", token_ids)))
|
||||||
|
).device_indices
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def register(cache, token_ids, session_id, generation=None):
|
||||||
|
if generation is None:
|
||||||
|
generation = cache.ensure_session_generation(session_id)
|
||||||
|
cache.session_refs.register_session_ref(
|
||||||
|
SimpleNamespace(
|
||||||
|
session_id=session_id,
|
||||||
|
session_generation=generation,
|
||||||
|
session=None,
|
||||||
|
last_node=cache.match_prefix(
|
||||||
|
MatchPrefixParams(key=RadixKey(array("q", token_ids)))
|
||||||
|
).last_device_node,
|
||||||
|
origin_input_ids=array("q", token_ids),
|
||||||
|
output_ids=array("q"),
|
||||||
|
kv_committed_len=len(token_ids),
|
||||||
|
extra_key=None,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestRadixCacheSessionRemoval(CustomTestCase):
|
||||||
|
def test_plain_radix_cache_does_not_enable_session_references(self):
|
||||||
|
cache = RadixCache(make_params(enable_session=True))
|
||||||
|
|
||||||
|
self.assertFalse(hasattr(cache, "enable_session_radix_cache"))
|
||||||
|
self.assertFalse(hasattr(cache, "register_session_ref"))
|
||||||
|
self.assertFalse(hasattr(cache, "open_radix_session"))
|
||||||
|
|
||||||
|
|
||||||
|
class TestSessionUnifiedRadixCache(CustomTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.cache = UnifiedRadixCache(make_params(enable_session=True))
|
||||||
|
self.full = self.cache.components[ComponentType.FULL]
|
||||||
|
|
||||||
|
def test_register_and_release_update_full_component_reference(self):
|
||||||
|
leaf = insert(self.cache, [1, 2, 3, 4])
|
||||||
|
generation = self.cache.open_radix_session("s1")
|
||||||
|
|
||||||
|
register(self.cache, [1, 2, 3, 4], "s1", generation)
|
||||||
|
self.assertEqual(self.full.session_ref(leaf), 1)
|
||||||
|
|
||||||
|
self.cache.release_radix_session("s1")
|
||||||
|
self.assertEqual(self.full.session_ref(leaf), 0)
|
||||||
|
|
||||||
|
def test_reopen_rejects_stale_generation(self):
|
||||||
|
leaf = insert(self.cache, [1, 2, 3, 4])
|
||||||
|
old_generation = self.cache.open_radix_session("s1")
|
||||||
|
self.cache.release_radix_session("s1")
|
||||||
|
self.cache.open_radix_session("s1")
|
||||||
|
|
||||||
|
register(self.cache, [1, 2, 3, 4], "s1", old_generation)
|
||||||
|
|
||||||
|
self.assertEqual(self.full.session_ref(leaf), 0)
|
||||||
|
|
||||||
|
def test_eviction_prefers_unreferenced_full_kv(self):
|
||||||
|
referenced = insert(self.cache, [1, 2, 3, 4])
|
||||||
|
insert(self.cache, [7, 8, 9])
|
||||||
|
register(self.cache, [1, 2, 3, 4], "s1")
|
||||||
|
|
||||||
|
self.cache.evict(EvictParams(num_tokens=3))
|
||||||
|
|
||||||
|
self.assertEqual(match_len(self.cache, [7, 8, 9]), 0)
|
||||||
|
self.assertEqual(match_len(self.cache, [1, 2, 3, 4]), 4)
|
||||||
|
self.assertEqual(self.full.session_ref(referenced), 1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
@@ -65,6 +65,15 @@ class _StubFullComponent(TreeComponent):
|
|||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _dec_session_coverage(self, session_id, leaf) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _advance_session_coverage(self, session_id, leaf, old_ancestor) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _recede_session_coverage(self, session_id, leaf, fallback) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class _StubMambaComponent(_StubFullComponent):
|
class _StubMambaComponent(_StubFullComponent):
|
||||||
component_type = ComponentType.MAMBA
|
component_type = ComponentType.MAMBA
|
||||||
|
|||||||
@@ -188,6 +188,15 @@ class _FakeFullComponent(TreeComponent):
|
|||||||
def _evict_device_end(self) -> None:
|
def _evict_device_end(self) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def _dec_session_coverage(self, session_id, leaf) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _advance_session_coverage(self, session_id, leaf, old_ancestor) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _recede_session_coverage(self, session_id, leaf, fallback) -> None:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TestUnifiedRadixComponentRegistryOverride(CustomTestCase):
|
class TestUnifiedRadixComponentRegistryOverride(CustomTestCase):
|
||||||
def test_component_registry_override_is_instance_local(self):
|
def test_component_registry_override_is_instance_local(self):
|
||||||
|
|||||||
@@ -1274,17 +1274,6 @@ class TestPrefillOnlyDisableKvCache(unittest.TestCase):
|
|||||||
self._validate_prefill_only_args(kv_cache_dtype=kv_cache_dtype)
|
self._validate_prefill_only_args(kv_cache_dtype=kv_cache_dtype)
|
||||||
|
|
||||||
|
|
||||||
class TestSessionRadixCacheServerArgs(unittest.TestCase):
|
|
||||||
def test_requires_priority_radix_eviction_policy(self):
|
|
||||||
server_args = ServerArgs(
|
|
||||||
model_path="dummy",
|
|
||||||
enable_session_radix_cache=True,
|
|
||||||
radix_eviction_policy="lru",
|
|
||||||
)
|
|
||||||
with self.assertRaisesRegex(ValueError, "--radix-eviction-policy priority"):
|
|
||||||
server_args._handle_cache_compatibility()
|
|
||||||
|
|
||||||
|
|
||||||
class TestCudaGraphConfigDataclassAccess(CustomTestCase):
|
class TestCudaGraphConfigDataclassAccess(CustomTestCase):
|
||||||
@patch(
|
@patch(
|
||||||
"sglang.srt.model_executor.runner_backend."
|
"sglang.srt.model_executor.runner_backend."
|
||||||
|
|||||||
Reference in New Issue
Block a user