diff --git a/python/sglang/srt/environ.py b/python/sglang/srt/environ.py index 3938c10a6..ae6c695e1 100644 --- a/python/sglang/srt/environ.py +++ b/python/sglang/srt/environ.py @@ -881,7 +881,6 @@ class Envs: SGLANG_OPT_SWA_RADIX_CACHE_COMPACT = EnvBool(False) SGLANG_OPT_SWA_SPLIT_LEAF_ON_INSERT = EnvBool(False) 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) @@ -1037,6 +1036,7 @@ def _convert_SGL_to_SGLANG(): "SGLANG_ENABLE_TP_MEMORY_INBALANCE_CHECK", ) _print_deprecated_env("SGLANG_PER_TOKEN_GROUP_QUANT_8BIT_V2") + _print_deprecated_env("SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN") _print_deprecated_env("SGLANG_ENABLE_THINKING", "SGLANG_DEFAULT_THINKING") _print_deprecated_env("SGLANG_REASONING_EFFORT", "SGLANG_DSV4_REASONING_EFFORT") _print_deprecated_env( diff --git a/python/sglang/srt/managers/schedule_batch.py b/python/sglang/srt/managers/schedule_batch.py index 16513b9b1..b376e4069 100755 --- a/python/sglang/srt/managers/schedule_batch.py +++ b/python/sglang/srt/managers/schedule_batch.py @@ -2930,7 +2930,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin): 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, - drop_page_margin=self.tree_cache.is_chunk_cache(), + is_chunk_cache=self.tree_cache.is_chunk_cache(), ) def __str__(self): diff --git a/python/sglang/srt/mem_cache/common.py b/python/sglang/srt/mem_cache/common.py index 1ffbe218b..036f412a2 100644 --- a/python/sglang/srt/mem_cache/common.py +++ b/python/sglang/srt/mem_cache/common.py @@ -73,10 +73,8 @@ def free_swa_out_of_window_slots( page_size: int, req_to_token_pool: ReqToTokenPool, token_to_kv_pool_allocator: BaseTokenToKVPoolAllocator, - drop_page_margin: bool = False, + is_chunk_cache: bool = False, ) -> 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 @@ -86,13 +84,16 @@ def free_swa_out_of_window_slots( evict_floor = -(-evict_floor // page_size) * page_size req.swa_evicted_seqlen = max(req.swa_evicted_seqlen, evict_floor) - # Subtract an extra page_size so the eviction frontier never reaches the - # radix tree insert boundary, keeping >=1 page of non-evicted SWA KV for the - # tree to store as a non-tombstone node (else leaf nodes tombstone -> SWA leak). - if drop_page_margin or envs.SGLANG_OPT_SWA_EVICT_DROP_PAGE_MARGIN.get(): + if is_chunk_cache: + # Chunk cache builds no radix tree, so no tombstone-leaf concern; evict + # up to the window boundary (the trailing floor keeps it page-aligned). evict_threshold = pre_len - sliding_window_size else: - evict_threshold = pre_len - sliding_window_size - page_size + # Radix cache: keep max(window, page). The trailing floor page-aligns the + # frontier, and subtracting at least one page keeps it below the insert + # boundary (page_floor(seq_len)) so the last leaf is never all-tombstone. + # No extra page margin is needed. + evict_threshold = pre_len - max(sliding_window_size, page_size) new_swa_evicted_seqlen = max( req.swa_evicted_seqlen, evict_threshold, diff --git a/test/registered/unit/mem_cache/test_swa_eviction_boundary.py b/test/registered/unit/mem_cache/test_swa_eviction_boundary.py index 18896ffd7..763284ceb 100644 --- a/test/registered/unit/mem_cache/test_swa_eviction_boundary.py +++ b/test/registered/unit/mem_cache/test_swa_eviction_boundary.py @@ -7,7 +7,7 @@ handling for this, creating an incorrect non-tombstone node that caused inflated swa_evictable_size_, negative usage, and potential double-free. Two-sided fix: -1. _evict_swa subtracts extra page_size (preventive). +1. _evict_swa subtracts max(window, page) on the radix path (preventive). 2. _insert_helper early-returns on case 3 (defensive). Tests use real tree/allocator/pool with mock Req/ScheduleBatch wrappers. @@ -195,7 +195,7 @@ class TestSWAEvictionBoundary(unittest.TestCase): # -- Eviction formula: page_size == 1 -- def test_formula_page_size_1(self): - """page_size=1: no floor alignment, -1 just means one less token evicted.""" + """page_size=1: radix keeps max(window, page)=window, so the frontier is pre_len - window.""" page_size, window = 1, 4 tree, allocator, pool = _build_swa_tree( page_size=page_size, sliding_window_size=window @@ -210,7 +210,9 @@ class TestSWAEvictionBoundary(unittest.TestCase): ScheduleBatch._evict_swa(batch, req, seq_len - 1) self.assertLess(req.swa_evicted_seqlen, seq_len) - self.assertEqual(req.swa_evicted_seqlen, max(0, seq_len - 1 - window - 1)) + self.assertEqual( + req.swa_evicted_seqlen, max(0, seq_len - 1 - max(window, page_size)) + ) tree.cache_finished_req(req, is_insert=True) tree.sanity_check() @@ -383,7 +385,9 @@ class TestSWAEvictionBoundary(unittest.TestCase): batch = _make_batch(tree, allocator, pool) ScheduleBatch._evict_swa(batch, req, seq_len - 1) - self.assertEqual(req.swa_evicted_seqlen, max(0, seq_len - 1 - window - 1)) + self.assertEqual( + req.swa_evicted_seqlen, max(0, seq_len - 1 - max(window, page_size)) + ) tree.cache_finished_req(req, is_insert=True) tree.sanity_check() diff --git a/test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py b/test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py index ae1d48df0..ebc0a8eb8 100644 --- a/test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py +++ b/test/registered/unit/mem_cache/test_unified_radix_cache_unittest.py @@ -1977,7 +1977,7 @@ class UnifiedRadixCacheSuite: with envs.SGLANG_OPT_UNIFIED_CACHE_FREE_OUT_OF_WINDOW_SLOTS.override(True): tree.cache_unfinished_req(req) - cushion = self.cfg.sliding_window_size + self.cfg.page_size + cushion = max(self.cfg.sliding_window_size, self.cfg.page_size) expected_evicted = (pre_len - 1) - cushion self.assertEqual( req.swa_evicted_seqlen,