[Fix] Respect cache_protected_len in ChunkCache and disabled-radix release paths (#31662)

This commit is contained in:
Liangsheng Yin
2026-07-18 12:04:25 -07:00
committed by GitHub
parent b3a0185cab
commit 10908a6793
3 changed files with 32 additions and 10 deletions
+14 -8
View File
@@ -80,8 +80,9 @@ class ChunkCache(BasePrefixCache):
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
): ):
# For decode server: if req.output_ids is empty, we want to free all req.origin_input_ids # For decode server: if req.output_ids is empty, we want to free all req.origin_input_ids
# The protected prefix is not this req's to free.
kv_indices = self.req_to_token_pool.req_to_token[ kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_len_to_handle req.req_pool_idx, req.cache_protected_len : kv_len_to_handle
] ]
self.token_to_kv_pool_allocator.free(kv_indices) self.token_to_kv_pool_allocator.free(kv_indices)
@@ -146,9 +147,10 @@ class PureSWAChunkCache(SWAChunkCache):
explicitly skip the range already freed by ``free_swa_out_of_window_slots`` explicitly skip the range already freed by ``free_swa_out_of_window_slots``
(a.k.a. _evict_swa) during decode. (a.k.a. _evict_swa) during decode.
``req.swa_evict_floor`` only protects the prompt/image KV while the request ``req.swa_evict_floor`` shields the prompt/image KV from window eviction
is active. ChunkCache does not retain finished prefixes, so the protected only while the request is active, so that range IS released here on
prefix is released here when the request finishes. finish. Distinct from the ``cache_protected_len`` prefix, which is owned
elsewhere and never freed by this path.
""" """
def cache_finished_req( def cache_finished_req(
@@ -158,15 +160,19 @@ class PureSWAChunkCache(SWAChunkCache):
kv_indices = self.req_to_token_pool.req_to_token[ kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len req.req_pool_idx, :kv_committed_len
] ]
# The cache_protected_len prefix is not this req's to free.
protected_len = req.cache_protected_len
evict_floor = req.swa_evict_floor evict_floor = req.swa_evict_floor
evicted_seqlen = req.kv.swa_evicted_seqlen evicted_seqlen = req.kv.swa_evicted_seqlen
if evicted_seqlen > evict_floor: if evicted_seqlen > evict_floor:
parts = [] parts = []
if evict_floor > 0: if evict_floor > protected_len:
parts.append(kv_indices[:evict_floor]) parts.append(kv_indices[protected_len:evict_floor])
if evicted_seqlen < kv_committed_len: if evicted_seqlen < kv_committed_len:
parts.append(kv_indices[evicted_seqlen:kv_committed_len]) parts.append(
kv_indices[max(evicted_seqlen, protected_len) : kv_committed_len]
)
if parts: if parts:
self.token_to_kv_pool_allocator.free(torch.cat(parts)) self.token_to_kv_pool_allocator.free(torch.cat(parts))
else: else:
self.token_to_kv_pool_allocator.free(kv_indices) self.token_to_kv_pool_allocator.free(kv_indices[protected_len:])
+2 -1
View File
@@ -443,8 +443,9 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
is_insert = False is_insert = False
if self.disable: if self.disable:
# The protected prefix is not this req's to free.
kv_indices = self.req_to_token_pool.req_to_token[ kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_len_to_handle req.req_pool_idx, req.cache_protected_len : kv_len_to_handle
] ]
self.token_to_kv_pool_allocator.free(kv_indices) self.token_to_kv_pool_allocator.free(kv_indices)
return return
@@ -23,6 +23,7 @@ class _FakeAllocator:
class _FakeReq: class _FakeReq:
req_pool_idx = 0 req_pool_idx = 0
swa_evict_floor = 3 swa_evict_floor = 3
cache_protected_len = 0
kv = SimpleNamespace(swa_evicted_seqlen=6) kv = SimpleNamespace(swa_evicted_seqlen=6)
def pop_committed_kv_cache(self): def pop_committed_kv_cache(self):
@@ -30,12 +31,16 @@ class _FakeReq:
class TestPureSWAChunkCache(CustomTestCase): class TestPureSWAChunkCache(CustomTestCase):
def test_finished_req_skips_already_evicted_swa_range(self): def _make_cache(self):
cache = PureSWAChunkCache.__new__(PureSWAChunkCache) cache = PureSWAChunkCache.__new__(PureSWAChunkCache)
cache.req_to_token_pool = SimpleNamespace( cache.req_to_token_pool = SimpleNamespace(
req_to_token=torch.arange(10, dtype=torch.int64).unsqueeze(0) req_to_token=torch.arange(10, dtype=torch.int64).unsqueeze(0)
) )
cache.token_to_kv_pool_allocator = _FakeAllocator() cache.token_to_kv_pool_allocator = _FakeAllocator()
return cache
def test_finished_req_skips_already_evicted_swa_range(self):
cache = self._make_cache()
cache.cache_finished_req(_FakeReq(), kv_len_to_handle=8) cache.cache_finished_req(_FakeReq(), kv_len_to_handle=8)
@@ -43,6 +48,16 @@ class TestPureSWAChunkCache(CustomTestCase):
freed = cache.token_to_kv_pool_allocator.freed[0] freed = cache.token_to_kv_pool_allocator.freed[0]
self.assertTrue(torch.equal(freed, torch.tensor([0, 1, 2, 6, 7]))) self.assertTrue(torch.equal(freed, torch.tensor([0, 1, 2, 6, 7])))
def test_finished_req_skips_protected_prefix(self):
cache = self._make_cache()
req = _FakeReq()
req.cache_protected_len = 2
cache.cache_finished_req(req, kv_len_to_handle=8)
freed = cache.token_to_kv_pool_allocator.freed[0]
self.assertTrue(torch.equal(freed, torch.tensor([2, 6, 7])))
if __name__ == "__main__": if __name__ == "__main__":
unittest.main() unittest.main()