Let cache backend do not couple with owned committed kv details and avoid kv_committed_freed/kv_overallocated_freed fields (#29428)

This commit is contained in:
fzyzcjy
2026-07-15 14:43:10 +08:00
committed by GitHub
parent d8d76c4d12
commit 27256aee5b
24 changed files with 121 additions and 154 deletions
@@ -255,7 +255,7 @@ class DecodeKVCacheOffloadManager:
if req.req_pool_idx is None or req.req_pool_idx == -1:
return
kv_committed_len = req.pop_committed_kv_cache()
kv_committed_len = req.effective_kv_committed_len()
# Free the prefill-aligned slots. Previously this was done
# eagerly in offload_kv_cache (mid-decode), which raced with
@@ -276,7 +276,7 @@ class DecodeKVCacheOffloadManager:
# Free over-allocated KV cache slots (e.g. from speculative decoding v2).
# Without spec v2, start_p == end_p so this is a no-op.
start_p, end_p = req.pop_overallocated_kv_cache()
start_p, end_p = kv_committed_len, req.kv.kv_allocated_len
if self.page_size > 1:
start_p = ceil_align(start_p, self.page_size)
if start_p < end_p:
+1 -25
View File
@@ -749,8 +749,6 @@ class Req(ReqDllmMixin):
# For req-level memory management
self.kv_committed_len = 0
self.kv: ReqKvInfo = ReqKvInfo(kv_allocated_len=0, swa_evicted_seqlen=0)
self.kv_committed_freed = False
self.kv_overallocated_freed = False
# for cross-encoder model
self.token_type_ids = token_type_ids
@@ -1076,33 +1074,13 @@ class Req(ReqDllmMixin):
or self.mamba_host_hit_length > 0
)
def _cache_commit_len(self) -> int:
def effective_kv_committed_len(self) -> int:
# Report only the prompt prefix so thinking + answer fall into the
# overallocated range and are reclaimed by release_kv_cache. #22373.
if get_server_args().strip_thinking_cache and self.reasoning_tokens > 0:
return min(self.kv_committed_len, len(self.origin_input_ids))
return self.kv_committed_len
def pop_committed_kv_cache(self) -> int:
"""Return the length of committed KV cache and mark them as freed."""
assert (
not self.kv_committed_freed
), f"Committed KV cache already freed ({self.kv_committed_len=})"
self.kv_committed_freed = True
return self._cache_commit_len()
def pop_overallocated_kv_cache(self) -> Tuple[int, int]:
"""Return the range of over-allocated KV cache and mark them as freed."""
# NOTE: This function is called when there is over-allocation of KV cache.
# Over-allocation: we allocate more KV cache than the committed length.
# e.g., speculative decoding may allocate more KV cache than actually used.
assert (
not self.kv_overallocated_freed
), f"Overallocated KV cache already freed, {self.kv_committed_len=}, {self.kv.kv_allocated_len=}"
self.kv_overallocated_freed = True
return self._cache_commit_len(), self.kv.kv_allocated_len
def update_spec_correct_drafts_histogram(self, num_correct_drafts: int):
"""Update the speculative decoding acceptance histogram.
@@ -1525,8 +1503,6 @@ class Req(ReqDllmMixin):
self.already_computed = 0
self.kv.kv_allocated_len = 0
self.kv_committed_len = 0
self.kv_committed_freed = False
self.kv_overallocated_freed = False
self.kv.swa_evicted_seqlen = 0
self.extend_batch_idx = 0
self.decode_batch_idx = 0
+1 -3
View File
@@ -2564,9 +2564,7 @@ class Scheduler(
req.pending_bootstrap = False
if self.enable_hicache_storage:
self.tree_cache.release_aborted_request(req.rid)
if (
req.req_pool_idx is not None or self.tree_cache.supports_mamba()
) and not req.kv_committed_freed:
if req.req_pool_idx is not None or self.tree_cache.supports_mamba():
release_kv_cache(req, self.tree_cache, is_insert=False)
self.chunked_req = None
@@ -246,8 +246,7 @@ class SchedulerInvariantChecker:
swa_uncached = 0
for batch in batches:
for req in batch.reqs:
assert req.kv_committed_freed == req.kv_overallocated_freed
if req.kv_committed_freed or req.req_pool_idx is None:
if req.req_pool_idx is None:
continue
allocated_len = req.kv.kv_allocated_len
+8 -5
View File
@@ -76,11 +76,12 @@ class ChunkCache(BasePrefixCache):
# ChunkCache does not support prefix caching, so insert is a no-op
return InsertResult(prefix_len=0)
def cache_finished_req(self, req: Req, is_insert: bool = True):
kv_committed_len = req.pop_committed_kv_cache()
def cache_finished_req(
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
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
self.token_to_kv_pool_allocator.free(kv_indices)
@@ -150,8 +151,10 @@ class PureSWAChunkCache(SWAChunkCache):
prefix is released here when the request finishes.
"""
def cache_finished_req(self, req: Req, is_insert: bool = True):
kv_committed_len = req.pop_committed_kv_cache()
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
):
kv_committed_len = kv_len_to_handle
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
]
+4 -2
View File
@@ -640,17 +640,19 @@ def release_kv_cache(req: Req, tree_cache: BasePrefixCache, is_insert: bool = Tr
req.mamba_pool_idx = None
return
effective_kv_committed_len = req.effective_kv_committed_len()
tree_cache.cache_finished_req(
req,
is_insert=is_insert and not getattr(req, "skip_radix_cache_insert", False),
kv_len_to_handle=effective_kv_committed_len,
)
# StreamingSession.cache_finished_req handles speculative tail trim
# and bookkeeping flag sync internally, then sets req_pool_idx = None.
# internally, then sets req_pool_idx = None.
if req.req_pool_idx is None:
return
start_p, end_p = req.pop_overallocated_kv_cache()
start_p, end_p = effective_kv_committed_len, req.kv.kv_allocated_len
global_server_args = get_server_args()
page_size = global_server_args.page_size
@@ -523,20 +523,21 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
)
return InsertResult(prefix_len=prefix_len, mamba_exist=mamba_exist)
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
) -> None:
"""Cache request when it finishes."""
kv_committed_len = req.pop_committed_kv_cache()
if self.disable:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
self.token_to_kv_pool_allocator.free(kv_indices)
self.req_to_token_pool.free_mamba_cache(req)
return
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
token_ids = (req.origin_input_ids + req.output_ids)[:kv_len_to_handle]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
if is_insert:
@@ -572,7 +573,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
assert (
cache_len == page_aligned_len
), f"It is required {cache_len=}, {page_aligned_len=}, {kv_committed_len=}, {len(req.origin_input_ids)=}, {len(req.output_ids)=} ping @yizhang2077 if you see this"
), f"It is required {cache_len=}, {page_aligned_len=}, {kv_len_to_handle=}, {len(req.origin_input_ids)=}, {len(req.output_ids)=} ping @yizhang2077 if you see this"
# Radix Cache takes one ref in memory pool
# insert the token_ids and kv_indices into the radix tree
@@ -62,7 +62,9 @@ class PureSWARadixCache(RadixCache):
num_tokens = max(params.num_tokens, params.swa_num_tokens)
return super().evict(EvictParams(num_tokens=num_tokens))
def cache_finished_req(self, req: Req, is_insert: bool = True):
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
):
"""Cache request when it finishes.
Only inserts the prefill portion [0, evict_floor) into the radix tree.
@@ -73,7 +75,7 @@ class PureSWARadixCache(RadixCache):
if self.disable_finished_insert:
is_insert = False
kv_committed_len = req.pop_committed_kv_cache()
kv_committed_len = kv_len_to_handle
if self.disable:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
+5 -4
View File
@@ -434,21 +434,22 @@ class RadixCache(SessionRadixCacheMixin, KVCacheEventMixin, BasePrefixCache):
)
return InsertResult(prefix_len=prefix_len, last_device_node=last_node)
def cache_finished_req(self, req: Req, is_insert: bool = True):
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
):
"""Cache request when it finishes."""
# In deterministic mode, disable finished request insertion to radix cache
if self.disable_finished_insert:
is_insert = False
kv_committed_len = req.pop_committed_kv_cache()
if self.disable:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
self.token_to_kv_pool_allocator.free(kv_indices)
return
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
token_ids = (req.origin_input_ids + req.output_ids)[:kv_len_to_handle]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, : len(token_ids)
]
@@ -169,19 +169,20 @@ class RadixCacheCpp(BasePrefixCache):
def total_size(self):
return self.tree.total_size()
def cache_finished_req(self, req: Req, is_insert: bool = True):
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
):
"""Cache request when it finishes."""
assert req.req_pool_idx is not None
kv_committed_len = req.pop_committed_kv_cache()
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
token_ids = (req.origin_input_ids + req.output_ids)[:kv_len_to_handle]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
].to(dtype=torch.int64, copy=True)
# NOTE: our C++ implementation don't need `token_ids` and `kv_indices` to be page-aligned
# it will automatically align them, but length of them should be equal
old_prefix_len = len(req.prefix_indices) // self.page_size * self.page_size
page_aligned_overall_len = kv_committed_len // self.page_size * self.page_size
page_aligned_overall_len = kv_len_to_handle // self.page_size * self.page_size
if is_insert:
new_prefix_len = self._insert(
@@ -200,7 +201,7 @@ class RadixCacheCpp(BasePrefixCache):
)
# need to free the unaligned part, since it cannot be inserted into the radix tree
if page_aligned_overall_len < kv_committed_len:
if page_aligned_overall_len < kv_len_to_handle:
# NOTE: sglang PagedAllocator support unaligned free (which will automatically align it)
self.token_to_kv_pool_allocator.free(kv_indices[page_aligned_overall_len:])
@@ -379,10 +379,12 @@ class FlexKVRadixCache(RadixCache):
# ------------------------------------------------------------------
def cache_finished_req( # type: ignore[override]
self, req: Req, is_insert: bool = True
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
) -> None:
"""Base cache_finished_req then fire an async FlexKV store."""
super().cache_finished_req(req, is_insert=is_insert)
super().cache_finished_req(
req, is_insert=is_insert, kv_len_to_handle=kv_len_to_handle
)
if not is_insert:
self._load_markers.pop(req.rid, None)
return
@@ -428,10 +428,14 @@ class LMCRadixCache(RadixCache):
)
)
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
) -> None:
"""On request completion, insert device KV into radix and store to LMCache."""
super().cache_finished_req(req, is_insert=is_insert)
super().cache_finished_req(
req, is_insert=is_insert, kv_len_to_handle=kv_len_to_handle
)
if not is_insert:
if self._mode is LMCacheMode.MP:
self._mp_load_back_markers.pop(req.rid, None)
@@ -456,19 +456,20 @@ class SWARadixCache(KVCacheEventMixin, BasePrefixCache):
)
return InsertResult(prefix_len=prefix_len)
def cache_finished_req(self, req: Req, is_insert: bool = True) -> None:
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int
) -> None:
"""Cache request when it finishes."""
kv_committed_len = req.pop_committed_kv_cache()
if self.disable:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
self.token_to_kv_pool_allocator.free(kv_indices)
return
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
token_ids = (req.origin_input_ids + req.output_ids)[:kv_len_to_handle]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
radix_key = RadixKey(
@@ -199,14 +199,14 @@ Unlock a previously locked node path.
---
### `cache_finished_req(req: Req, is_insert: bool = True)`
### `cache_finished_req(req: Req, is_insert: bool = True, *, kv_len_to_handle: int)`
Cache a completed request's KV data into the tree.
| Aspect | Detail |
|--------|--------|
| **Purpose** | After a request finishes, insert its token/KV data into the tree for future reuse |
| **Inputs** | `req` — the finished request; `is_insert` — whether to insert (True) or just release locks (False) |
| **Inputs** | `req` — the finished request; `is_insert` — whether to insert (True) or just release locks (False); `kv_len_to_handle` — committed KV length supplied by the caller |
| **Output** | `None` |
| **Mutation** | Calls component hooks → `insert``dec_lock_ref` → component cleanup. Frees unaligned tail KV indices; frees non-inserted KV indices when `is_insert=False`. |
| **Complexity** | **O(K + D·C)** — insert O(K + D·C) + lock release O(D). Simplifies to **O(K)**. |
@@ -712,24 +712,24 @@ class UnifiedRadixCache(KVCacheEventMixin, BasePrefixCache):
self._update_evictable_leaf_sets(node)
return DecLockRefResult()
def cache_finished_req(self, req: Req, is_insert: bool = True, **kwargs) -> None:
def cache_finished_req(
self, req: Req, is_insert: bool = True, *, kv_len_to_handle: int, **kwargs
) -> None:
if self.session.try_cache_finished_req(req, is_insert=is_insert, **kwargs):
return
kv_committed_len = req.pop_committed_kv_cache()
if self.disable:
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
self.token_to_kv_pool_allocator.free(kv_indices)
for comp in self._components_tuple:
comp.cleanup_after_caching_req(req, is_finished=True)
return
token_ids = (req.origin_input_ids + req.output_ids)[:kv_committed_len]
token_ids = (req.origin_input_ids + req.output_ids)[:kv_len_to_handle]
kv_indices = self.req_to_token_pool.req_to_token[
req.req_pool_idx, :kv_committed_len
req.req_pool_idx, :kv_len_to_handle
]
result = None
@@ -325,7 +325,6 @@ class StreamingSession(BasePrefixCache):
self.release_session(session_id)
req.req_pool_idx = None
req.session.abort_req()
self._mark_kv_freed(req)
return True
if is_first:
@@ -348,7 +347,6 @@ class StreamingSession(BasePrefixCache):
# Update req_nodes to this successfully finished request.
req.session.finish_req(req)
self._mark_kv_freed(req)
return True
def try_cache_unfinished_req(
@@ -570,14 +568,6 @@ class StreamingSession(BasePrefixCache):
tail = self.req_to_token_pool.req_to_token[pool_idx, start:end]
self.token_to_kv_pool_allocator.free(tail)
@staticmethod
def _mark_kv_freed(req: Req) -> None:
"""Set bookkeeping flags so busy check skips this finished req."""
if not req.kv_committed_freed:
req.pop_committed_kv_cache()
if not req.kv_overallocated_freed:
req.pop_overallocated_kv_cache()
# -- Pass-through methods --
def evictable_size(self):