Retain SWA down to the last state checkpoint (#34729)

This commit is contained in:
Ke Bao
2026-08-15 01:12:38 +08:00
committed by GitHub
parent c20aceeb88
commit 7562e741e2
6 changed files with 165 additions and 0 deletions
@@ -21,6 +21,7 @@ import torch
from sglang.srt.managers.schedule_batch import ScheduleBatch
from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator
from sglang.srt.mem_cache.cache_init_params import CacheInitParams
from sglang.srt.mem_cache.common import free_swa_out_of_window_slots
from sglang.srt.mem_cache.memory_pool import ReqToTokenPool
from sglang.srt.mem_cache.swa_memory_pool import SWAKVPool
from sglang.srt.mem_cache.swa_radix_cache import SWARadixCache
@@ -196,6 +197,146 @@ class TestSWAEvictionBoundary(unittest.TestCase):
)
tree.sanity_check()
# -- Retention floor: never free past the last state checkpoint --
def test_retain_floor_clamps_eviction(self):
"""A hybrid cache keeps SWA down to the last state checkpoint, not to the
window behind the tail, because that is where a prefix match lands. The
floor must clamp the frontier even though the tail has moved far past it."""
page_size, window = 8, 16
tree, allocator, pool = _build_swa_tree(
page_size=page_size, sliding_window_size=window
)
seq_len = 200
checkpoint = 96
kv = _swa_alloc(allocator, seq_len)
pool.write((0, slice(0, seq_len)), kv)
req = _make_req(0, list(range(seq_len)), 0, tree)
batch = _make_batch(tree, allocator, pool)
free_swa_out_of_window_slots(
req,
seq_len - 1,
sliding_window_size=window,
page_size=page_size,
req_to_token_pool=batch.req_to_token_pool,
token_to_kv_pool_allocator=batch.token_to_kv_pool_allocator,
retain_floor=checkpoint - window,
)
# Without the floor this would reach page_floor(199 - 16) = 176.
self.assertLessEqual(req.kv.swa_evicted_seqlen, checkpoint - window)
self.assertEqual(req.kv.swa_evicted_seqlen % page_size, 0)
def test_retain_floor_ignored_for_chunk_cache(self):
"""Chunk cache builds no tree, so a retained checkpoint could never be
matched. Holding it would cost SWA slots for nothing."""
page_size, window = 8, 16
seq_len = 200
tree, allocator, pool = _build_swa_tree(
page_size=page_size, sliding_window_size=window
)
kv = _swa_alloc(allocator, seq_len)
pool.write((0, slice(0, seq_len)), kv)
req = _make_req(0, list(range(seq_len)), 0, tree)
batch = _make_batch(tree, allocator, pool)
free_swa_out_of_window_slots(
req,
seq_len - 1,
sliding_window_size=window,
page_size=page_size,
req_to_token_pool=batch.req_to_token_pool,
token_to_kv_pool_allocator=batch.token_to_kv_pool_allocator,
is_chunk_cache=True,
retain_floor=16,
)
expected = (seq_len - 1 - window) // page_size * page_size
self.assertEqual(req.kv.swa_evicted_seqlen, expected)
def test_retain_floor_none_matches_old_behaviour(self):
"""retain_floor=None must reproduce the pre-change frontier exactly, so a
cache without a second state stream is unaffected."""
page_size, window = 8, 16
seq_len = 200
frontiers = []
for floor in (None, "absent"):
tree, allocator, pool = _build_swa_tree(
page_size=page_size, sliding_window_size=window
)
kv = _swa_alloc(allocator, seq_len)
pool.write((0, slice(0, seq_len)), kv)
req = _make_req(0, list(range(seq_len)), 0, tree)
batch = _make_batch(tree, allocator, pool)
kwargs = {} if floor == "absent" else {"retain_floor": None}
free_swa_out_of_window_slots(
req,
seq_len - 1,
sliding_window_size=window,
page_size=page_size,
req_to_token_pool=batch.req_to_token_pool,
token_to_kv_pool_allocator=batch.token_to_kv_pool_allocator,
**kwargs,
)
frontiers.append(req.kv.swa_evicted_seqlen)
expected = (seq_len - 1 - max(window, page_size)) // page_size * page_size
self.assertEqual(frontiers[0], expected)
self.assertEqual(frontiers[1], expected)
def test_retain_floor_above_threshold_is_inert(self):
"""The floor is a min(), so a checkpoint that is already inside the window
must not hold anything extra."""
page_size, window = 8, 16
seq_len = 200
tree, allocator, pool = _build_swa_tree(
page_size=page_size, sliding_window_size=window
)
kv = _swa_alloc(allocator, seq_len)
pool.write((0, slice(0, seq_len)), kv)
req = _make_req(0, list(range(seq_len)), 0, tree)
batch = _make_batch(tree, allocator, pool)
free_swa_out_of_window_slots(
req,
seq_len - 1,
sliding_window_size=window,
page_size=page_size,
req_to_token_pool=batch.req_to_token_pool,
token_to_kv_pool_allocator=batch.token_to_kv_pool_allocator,
retain_floor=seq_len,
)
expected = (seq_len - 1 - max(window, page_size)) // page_size * page_size
self.assertEqual(req.kv.swa_evicted_seqlen, expected)
def test_retain_floor_does_not_unfree(self):
"""The frontier only advances. A floor arriving after slots were already
freed must not claim them back, which would double-free on the next pass."""
page_size, window = 8, 16
tree, allocator, pool = _build_swa_tree(
page_size=page_size, sliding_window_size=window
)
seq_len = 200
kv = _swa_alloc(allocator, seq_len)
pool.write((0, slice(0, seq_len)), kv)
req = _make_req(0, list(range(seq_len)), 0, tree)
batch = _make_batch(tree, allocator, pool)
common_kwargs = dict(
sliding_window_size=window,
page_size=page_size,
req_to_token_pool=batch.req_to_token_pool,
token_to_kv_pool_allocator=batch.token_to_kv_pool_allocator,
)
free_swa_out_of_window_slots(req, seq_len - 1, **common_kwargs)
advanced = req.kv.swa_evicted_seqlen
self.assertGreater(advanced, 0)
free_swa_out_of_window_slots(req, seq_len - 1, retain_floor=0, **common_kwargs)
self.assertEqual(req.kv.swa_evicted_seqlen, advanced)
# -- Eviction formula: page_size == 1 --
def test_formula_page_size_1(self):