[sgl] proactively release out-of-window SWA slots after chunked prefill (#27402)

Co-authored-by: ispobock <ispobaoke@gmail.com>
This commit is contained in:
Bi Xue
2026-06-12 14:17:18 +08:00
committed by GitHub
co-authored by ispobock
parent 36d61613a1
commit 3c1f9eafa5
7 changed files with 174 additions and 33 deletions
+3
View File
@@ -782,6 +782,9 @@ class Envs:
SGLANG_OPT_SWA_RELEASE_LEAF_LOCK_AFTER_WINDOW = EnvBool(False)
SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN = EnvBool(False)
# Unified radix cache
SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS = EnvBool(False)
# DeepGemm Mega MoE
SGLANG_OPT_USE_DEEPGEMM_MEGA_MOE = EnvBool(False)
SGLANG_OPT_DEEPGEMM_MEGA_MOE_NUM_MAX_TOKENS_PER_RANK = EnvInt(1024)
+8 -33
View File
@@ -83,6 +83,7 @@ from sglang.srt.mem_cache.common import (
alloc_for_decode,
alloc_for_extend,
evict_from_tree_cache,
free_swa_out_of_window_slots,
get_alloc_reserve_per_decode,
release_kv_cache,
)
@@ -2835,41 +2836,15 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
def _evict_swa(self, req: Req, pre_len: int):
assert self.tree_cache.supports_swa(), "prefix cache must support swa"
sliding_window_size = self.tree_cache.sliding_window_size
# For swa radix cache, we need to evict the tokens that are not in the tree cache and also not in the sliding window
assert (
req.cache_protected_len % self.tree_cache.page_size == 0
), "cache_protected_len must be page aligned"
req.swa_evicted_seqlen = max(req.swa_evicted_seqlen, req.cache_protected_len)
# Subtract an extra page_size so the eviction frontier never reaches the
# radix tree insert boundary (page_floor(seq_len)). This keeps at least one
# page of non-evicted SWA KV for the tree to store as a non-tombstone node,
# preserving cache reuse in multi-turn scenarios. Without this, leaf nodes
# may become tombstoned, causing SWA memory leak.
# See also: _insert_helper case 3 in swa_radix_cache.py (defensive counterpart).
if envs.SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN.get():
evict_threshold = pre_len - sliding_window_size
else:
evict_threshold = pre_len - sliding_window_size - self.tree_cache.page_size
new_swa_evicted_seqlen = max(
req.swa_evicted_seqlen,
evict_threshold,
free_swa_out_of_window_slots(
req,
pre_len,
sliding_window_size=self.tree_cache.sliding_window_size,
page_size=self.tree_cache.page_size,
req_to_token_pool=self.req_to_token_pool,
token_to_kv_pool_allocator=self.token_to_kv_pool_allocator,
)
if self.tree_cache.page_size > 1:
new_swa_evicted_seqlen = (
new_swa_evicted_seqlen // self.tree_cache.page_size
) * self.tree_cache.page_size
if new_swa_evicted_seqlen > req.swa_evicted_seqlen:
free_slots = self.req_to_token_pool.req_to_token[
req.req_pool_idx, req.swa_evicted_seqlen : new_swa_evicted_seqlen
]
self.token_to_kv_pool_allocator.free_swa(free_slots)
req.swa_evicted_seqlen = new_swa_evicted_seqlen
def __str__(self):
return (
f"ScheduleBatch(forward_mode={self.forward_mode.name if self.forward_mode else 'None'}, "
+44
View File
@@ -28,6 +28,7 @@ _is_hip = is_hip()
if TYPE_CHECKING:
from sglang.srt.managers.schedule_batch import Req, ScheduleBatch
from sglang.srt.mem_cache.allocator import BaseTokenToKVPoolAllocator
# Needs 2 + 1 slots for mamba request with prefix cache. 2 for ping pong cache, 1 for running mamba state.
MAMBA_STATE_PER_REQ_PREFIX_CACHE = 3
@@ -54,6 +55,49 @@ def page_align_floor(length: int, page_size: int) -> int:
return (length // page_size) * page_size
def free_swa_out_of_window_slots(
req: Req,
pre_len: int,
*,
sliding_window_size: int,
page_size: int,
req_to_token_pool: ReqToTokenPool,
token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator,
) -> None:
from sglang.srt.environ import envs
# For swa radix cache, we need to evict the tokens that are not in the tree cache and also not in the sliding window
assert (
req.cache_protected_len % page_size == 0
), "cache_protected_len must be page aligned"
req.swa_evicted_seqlen = max(req.swa_evicted_seqlen, req.cache_protected_len)
# Subtract an extra page_size so the eviction frontier never reaches the
# radix tree insert boundary (page_floor(seq_len)). This keeps at least one
# page of non-evicted SWA KV for the tree to store as a non-tombstone node,
# preserving cache reuse in multi-turn scenarios. Without this, leaf nodes
# may become tombstoned, causing SWA memory leak.
# See also: _insert_helper case 3 in swa_radix_cache.py (defensive counterpart).
if envs.SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN.get():
evict_threshold = pre_len - sliding_window_size
else:
evict_threshold = pre_len - sliding_window_size - page_size
new_swa_evicted_seqlen = max(
req.swa_evicted_seqlen,
evict_threshold,
)
if page_size > 1:
new_swa_evicted_seqlen = (new_swa_evicted_seqlen // page_size) * page_size
if new_swa_evicted_seqlen > req.swa_evicted_seqlen:
free_slots = req_to_token_pool.req_to_token[
req.req_pool_idx, req.swa_evicted_seqlen : new_swa_evicted_seqlen
]
token_to_kv_pool_allocator.free_swa(free_slots)
req.swa_evicted_seqlen = new_swa_evicted_seqlen
def maybe_cache_unfinished_req(req: Req, tree_cache: BasePrefixCache, **kwargs):
if getattr(req, "skip_radix_cache_insert", False):
return
@@ -13,6 +13,7 @@ from sglang.srt.mem_cache.base_prefix_cache import (
MatchPrefixParams,
MatchResult,
)
from sglang.srt.mem_cache.common import free_swa_out_of_window_slots
from sglang.srt.mem_cache.hicache_storage import (
PoolHitPolicy,
PoolName,
@@ -524,6 +525,20 @@ class SWAComponent(TreeComponent):
insert_params.swa_evicted_seqlen = req.swa_evicted_seqlen
return None
def free_out_of_window_slots(
self, req: Req, pre_len: int, insert_params: InsertParams
) -> None:
if self.sliding_window_size is not None:
free_swa_out_of_window_slots(
req,
pre_len,
sliding_window_size=self.sliding_window_size,
page_size=self.cache.page_size,
req_to_token_pool=self.cache.req_to_token_pool,
token_to_kv_pool_allocator=self.cache.token_to_kv_pool_allocator,
)
insert_params.swa_evicted_seqlen = req.swa_evicted_seqlen
# ---- HiCache Hooks ----
def build_hicache_transfers(
@@ -380,6 +380,11 @@ class TreeComponent(ABC):
paths it is still provided so components can free their resources."""
pass
def free_out_of_window_slots(
self, req: Req, pre_len: int, insert_params: InsertParams
) -> None:
pass
# ---- HiCache Hooks ----
def build_hicache_transfers(
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, Any, Iterator, Optional, TypeVar
import torch
from sglang.srt.disaggregation.kv_events import StorageMedium
from sglang.srt.environ import envs
from sglang.srt.mem_cache.base_prefix_cache import (
BasePrefixCache,
DecLockRefParams,
@@ -756,6 +757,12 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
if cl is not None:
effective_cache_len = min(effective_cache_len, cl)
if envs.SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS.get():
for comp in self._components_tuple:
comp.free_out_of_window_slots(
req, effective_cache_len - 1, insert_params
)
if effective_cache_len <= 0:
req.prefix_indices = kv_indices_orig.to(dtype=torch.int64, copy=True)
for comp in self._components_tuple: