[mem_cache] Move cache_protected_len and swa_evict_floor into ReqKvInfo (#36982)

This commit is contained in:
Liangsheng Yin
2026-08-29 19:37:53 -07:00
committed by GitHub
parent ed39568e79
commit 6be767c2d2
29 changed files with 146 additions and 139 deletions
@@ -171,12 +171,12 @@ class DecodeStagingHandler:
# exact only when the prefix is page-aligned. Fail just this request on a # exact only when the prefix is page-aligned. Fail just this request on a
# mismatch instead of raising, which would kill the prefill scheduler. # mismatch instead of raising, which would kill the prefill scheduler.
page_size = self.kv_buffer_info["page_size"] page_size = self.kv_buffer_info["page_size"]
if decode_req.req.cache_protected_len % page_size != 0: if decode_req.req.kv.cache_protected_len % page_size != 0:
logger.error( logger.error(
"[STAGING] decode prefix length %s is not page-aligned " "[STAGING] decode prefix length %s is not page-aligned "
"(page_size=%s); failing room=%s (staging scatter offsets " "(page_size=%s); failing room=%s (staging scatter offsets "
"would be wrong).", "would be wrong).",
decode_req.req.cache_protected_len, decode_req.req.kv.cache_protected_len,
page_size, page_size,
room, room,
) )
@@ -414,7 +414,7 @@ class DecodeStagingHandler:
req_pool_idx = decode_req.req.req_pool_idx req_pool_idx = decode_req.req.req_pool_idx
# page_start is suffix-relative (pages after the decode-side cached # page_start is suffix-relative (pages after the decode-side cached
# prefix); req_to_token rows are absolute. # prefix); req_to_token rows are absolute.
prefix_tokens = decode_req.req.cache_protected_len prefix_tokens = decode_req.req.kv.cache_protected_len
token_start = prefix_tokens + page_start * page_size token_start = prefix_tokens + page_start * page_size
token_end = token_start + num_pages * page_size token_end = token_start + num_pages * page_size
prefill_tp = receiver.prefill_info.attn_tp_size prefill_tp = receiver.prefill_info.attn_tp_size
+1 -1
View File
@@ -1360,7 +1360,7 @@ class DecodePreallocQueue(DecodeHiCachePreallocMixin):
count_retracted=True, count_retracted=True,
extra_reserved_reqs=len(preallocated_reqs) + 1, extra_reserved_reqs=len(preallocated_reqs) + 1,
) )
decode_req.req.cache_protected_len = total_prefix_len decode_req.req.kv.cache_protected_len = total_prefix_len
page_size = self.token_to_kv_pool_allocator.page_size page_size = self.token_to_kv_pool_allocator.page_size
kv_transfer_page_size = page_size kv_transfer_page_size = page_size
+18 -15
View File
@@ -817,13 +817,22 @@ class ReqLogprob:
class ReqKvInfo: class ReqKvInfo:
# Device KV a request holds outside the prefix cache. Always present on the Req; # Device KV a request holds outside the prefix cache. Always present on the Req;
# whether any KV is held is `req.req_pool_idx is not None` (Req.is_holding_kv). # whether any KV is held is `req.req_pool_idx is not None` (Req.is_holding_kv).
# The request's own KV is [cache_protected_len, kv_allocated_len).
cache_protected_len: int = 0 # tree cache owns [0, here) (matched or inserted)
kv_allocated_len: int = 0 kv_allocated_len: int = 0
# The length of KV that have been removed in swa cache.
# SWA KV cache eviction behavior differs by cache type: # SWA slots in [swa_dead_lo(page_size), swa_evicted_seqlen) are already freed.
# - Radix cache: KV in range [cache_protected_len, swa_evicted_seqlen) is freed manually in swa_evict_floor: int = 0 # [0, here) never window-evicted (prefill-aware SWA)
# `ScheduleBatch.maybe_evict_swa`; KV in range [0, cache_protected_len) is freed during radix cache eviction. swa_evicted_seqlen: int = 0 # SWA eviction cursor
# - Chunk cache: KV in range [0, swa_evicted_seqlen) is freed manually in `ScheduleBatch.maybe_evict_swa`.
swa_evicted_seqlen: int = 0 def swa_dead_lo(self, page_size: int) -> int:
# Lowest SWA position this request may free itself: above the tree-owned
# prefix and above the eviction shield, page-aligned upward.
lo = max(self.cache_protected_len, self.swa_evict_floor)
if page_size > 1 and lo > self.cache_protected_len:
lo = ceil_align(lo, page_size)
return lo
@property @property
def is_released(self) -> bool: def is_released(self) -> bool:
@@ -920,10 +929,6 @@ class Req(ReqDllmMixin):
# for cross-encoder model # for cross-encoder model
self.token_type_ids = token_type_ids self.token_type_ids = token_type_ids
# Tokens in [0, swa_evict_floor) are protected from SWA window eviction.
# This is used by prefill-aware SWA models such as Unlimited-OCR to keep prompt/image KV visible during decode.
self.swa_evict_floor: int = 0
# The index of the extend / decode batch # The index of the extend / decode batch
self.extend_batch_idx = 0 self.extend_batch_idx = 0
self.decode_batch_idx = 0 self.decode_batch_idx = 0
@@ -1051,8 +1056,6 @@ class Req(ReqDllmMixin):
# per-component nodes this req skipped locking (e.g. mamba on the decode # per-component nodes this req skipped locking (e.g. mamba on the decode
# hold, already COW'd), so their dec releases only what it took. # hold, already COW'd), so their dec releases only what it took.
self.skip_lock_node_ids: dict = {} self.skip_lock_node_ids: dict = {}
# The prefix length that is inserted into the tree cache
self.cache_protected_len: int = 0
# Whether or not if it is chunked. It increments whenever # Whether or not if it is chunked. It increments whenever
# it is chunked, and decrement whenever chunked request is # it is chunked, and decrement whenever chunked request is
@@ -1436,9 +1439,9 @@ class Req(ReqDllmMixin):
match_result.mamba_branching_seqlen, match_result.mamba_branching_seqlen,
) )
if match_result.cache_protected_len is not None: if match_result.cache_protected_len is not None:
self.cache_protected_len = match_result.cache_protected_len self.kv.cache_protected_len = match_result.cache_protected_len
else: else:
self.cache_protected_len = len(self.prefix_indices) self.kv.cache_protected_len = len(self.prefix_indices)
if self.is_dllm(): if self.is_dllm():
self._update_block_offset_for_dllm() self._update_block_offset_for_dllm()
@@ -1721,7 +1724,7 @@ class Req(ReqDllmMixin):
self.routed_experts = None self.routed_experts = None
self.indexer_topk = None self.indexer_topk = None
self.last_node = None self.last_node = None
self.cache_protected_len = 0 self.kv.cache_protected_len = 0
self.num_matched_prefix_tokens = 0 self.num_matched_prefix_tokens = 0
self.swa_uuid_for_lock = None self.swa_uuid_for_lock = None
self.swa_prefix_lock_released = False self.swa_prefix_lock_released = False
@@ -193,7 +193,7 @@ def match_prefix_for_req(
if match_result.mamba_branching_seqlen is not None: if match_result.mamba_branching_seqlen is not None:
req.mamba_branching_seqlen = match_result.mamba_branching_seqlen req.mamba_branching_seqlen = match_result.mamba_branching_seqlen
if match_result.cache_protected_len is not None: if match_result.cache_protected_len is not None:
req.cache_protected_len = match_result.cache_protected_len req.kv.cache_protected_len = match_result.cache_protected_len
return match_result return match_result
@@ -1327,7 +1327,7 @@ class PrefillAdder:
) )
req.prefix_indices = torch.cat([req.prefix_indices, new_indices]) req.prefix_indices = torch.cat([req.prefix_indices, new_indices])
prefix_len = len(req.prefix_indices) prefix_len = len(req.prefix_indices)
req.cache_protected_len = prefix_len req.kv.cache_protected_len = prefix_len
input_tokens = self.ceil_paged_tokens( input_tokens = self.ceil_paged_tokens(
len(req.full_untruncated_fill_ids) - len(req.prefix_indices) len(req.full_untruncated_fill_ids) - len(req.prefix_indices)
+1 -1
View File
@@ -3647,7 +3647,7 @@ class Scheduler(
if self.tp_worker.model_runner.prefill_aware_swa: if self.tp_worker.model_runner.prefill_aware_swa:
for req in can_run_list: for req in can_run_list:
req.swa_evict_floor = req.extend_range.end req.kv.swa_evict_floor = req.extend_range.end
# Record prefill stats for logging after forward. # Record prefill stats for logging after forward.
new_batch.prefill_stats = PrefillStats.from_adder( new_batch.prefill_stats = PrefillStats.from_adder(
@@ -258,12 +258,12 @@ class SchedulerInvariantChecker:
allocated_len = req.kv.kv_allocated_len allocated_len = req.kv.kv_allocated_len
if self.page_size > 1: if self.page_size > 1:
allocated_len = ceil_align(allocated_len, self.page_size) allocated_len = ceil_align(allocated_len, self.page_size)
assert req.cache_protected_len % self.page_size == 0 assert req.kv.cache_protected_len % self.page_size == 0
full_uncached += allocated_len - req.cache_protected_len full_uncached += allocated_len - req.kv.cache_protected_len
if self.is_hybrid_swa: if self.is_hybrid_swa:
swa_uncached += allocated_len - max( swa_uncached += allocated_len - max(
req.cache_protected_len, req.kv.swa_evicted_seqlen req.kv.cache_protected_len, req.kv.swa_evicted_seqlen
) )
if req.beam_group is not None: if req.beam_group is not None:
+4 -4
View File
@@ -82,7 +82,7 @@ class ChunkCache(BasePrefixCache):
# 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. # 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, req.cache_protected_len : kv_len_to_handle req.req_pool_idx, req.kv.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)
@@ -147,7 +147,7 @@ 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`` shields the prompt/image KV from window eviction ``req.kv.swa_evict_floor`` shields the prompt/image KV from window eviction
only while the request is active, so that range IS released here on only while the request is active, so that range IS released here on
finish. Distinct from the ``cache_protected_len`` prefix, which is owned finish. Distinct from the ``cache_protected_len`` prefix, which is owned
elsewhere and never freed by this path. elsewhere and never freed by this path.
@@ -161,8 +161,8 @@ class PureSWAChunkCache(SWAChunkCache):
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. # The cache_protected_len prefix is not this req's to free.
protected_len = req.cache_protected_len protected_len = req.kv.cache_protected_len
evict_floor = req.swa_evict_floor evict_floor = req.kv.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 = []
+4 -5
View File
@@ -67,12 +67,11 @@ def free_swa_out_of_window_slots(
# For swa radix cache, we need to evict the tokens that are not in the tree cache and also not in the sliding window # 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 ( assert (
req.cache_protected_len % page_size == 0 req.kv.cache_protected_len % page_size == 0
), "cache_protected_len must be page aligned" ), "cache_protected_len must be page aligned"
evict_floor = max(req.cache_protected_len, getattr(req, "swa_evict_floor", 0)) req.kv.swa_evicted_seqlen = max(
if page_size > 1 and evict_floor > req.cache_protected_len: req.kv.swa_evicted_seqlen, req.kv.swa_dead_lo(page_size)
evict_floor = -(-evict_floor // page_size) * page_size )
req.kv.swa_evicted_seqlen = max(req.kv.swa_evicted_seqlen, evict_floor)
if is_chunk_cache: if is_chunk_cache:
# Chunk cache builds no radix tree, so no tombstone-leaf concern; evict # Chunk cache builds no radix tree, so no tombstone-leaf concern; evict
@@ -577,7 +577,7 @@ class MambaRadixCache(BasePrefixCache):
if cache_len is None: if cache_len is None:
cache_len = 0 cache_len = 0
if cache_len != len(token_ids): if cache_len != len(token_ids):
cache_end_idx = max(cache_len, req.cache_protected_len) cache_end_idx = max(cache_len, req.kv.cache_protected_len)
self.token_to_kv_pool_allocator.free_segment( self.token_to_kv_pool_allocator.free_segment(
kv_indices[cache_end_idx:], start_pos=cache_end_idx kv_indices[cache_end_idx:], start_pos=cache_end_idx
) )
@@ -639,7 +639,7 @@ class MambaRadixCache(BasePrefixCache):
), ),
value=page_aligned_kv_indices, value=page_aligned_kv_indices,
mamba_value=mamba_value, mamba_value=mamba_value,
prev_prefix_len=req.cache_protected_len, prev_prefix_len=req.kv.cache_protected_len,
) )
) )
mamba_exist = result.mamba_exist mamba_exist = result.mamba_exist
@@ -648,8 +648,8 @@ class MambaRadixCache(BasePrefixCache):
self.int8_ckpt_pool.free(mamba_value) self.int8_ckpt_pool.free(mamba_value)
else: else:
self.token_to_kv_pool_allocator.free_segment( self.token_to_kv_pool_allocator.free_segment(
kv_indices[req.cache_protected_len :], kv_indices[req.kv.cache_protected_len :],
start_pos=req.cache_protected_len, start_pos=req.kv.cache_protected_len,
) )
mamba_exist = True mamba_exist = True
@@ -753,7 +753,7 @@ class MambaRadixCache(BasePrefixCache):
), ),
value=page_aligned_kv_indices, value=page_aligned_kv_indices,
mamba_value=mamba_value_donated, mamba_value=mamba_value_donated,
prev_prefix_len=req.cache_protected_len, prev_prefix_len=req.kv.cache_protected_len,
chunked=chunked, chunked=chunked,
) )
) )
@@ -780,15 +780,15 @@ class MambaRadixCache(BasePrefixCache):
assert torch.equal(new_last_node.mamba_value, mamba_value_donated) assert torch.equal(new_last_node.mamba_value, mamba_value_donated)
assert ( assert (
req.cache_protected_len <= len(new_indices) + self.page_size - 1 req.kv.cache_protected_len <= len(new_indices) + self.page_size - 1
), f"{req.cache_protected_len=}, {len(new_indices)=}, {len(page_aligned_token_ids)=}, {mamba_exist=}" ), f"{req.kv.cache_protected_len=}, {len(new_indices)=}, {len(page_aligned_token_ids)=}, {mamba_exist=}"
assert new_prefix_len <= len( assert new_prefix_len <= len(
new_indices new_indices
), f"{new_prefix_len=}, {len(new_indices)=}" ), f"{new_prefix_len=}, {len(new_indices)=}"
self.req_to_token_pool.write( self.req_to_token_pool.write(
(req.req_pool_idx, slice(req.cache_protected_len, len(new_indices))), (req.req_pool_idx, slice(req.kv.cache_protected_len, len(new_indices))),
new_indices[req.cache_protected_len :], new_indices[req.kv.cache_protected_len :],
) )
self.dec_lock_ref(req.last_node) self.dec_lock_ref(req.last_node)
@@ -799,7 +799,7 @@ class MambaRadixCache(BasePrefixCache):
req.prefix_indices = torch.cat( req.prefix_indices = torch.cat(
[new_indices, kv_indices_orig[len(new_indices) :]] [new_indices, kv_indices_orig[len(new_indices) :]]
) )
req.cache_protected_len = len(new_indices) req.kv.cache_protected_len = len(new_indices)
req.mamba_last_track_seqlen = None req.mamba_last_track_seqlen = None
req.last_node = new_last_node req.last_node = new_last_node
@@ -96,8 +96,8 @@ class PureSWARadixCache(RadixCache):
).page_aligned(self.page_size) ).page_aligned(self.page_size)
keys_len = len(radix_key) keys_len = len(radix_key)
old_prefix_len = req.cache_protected_len old_prefix_len = req.kv.cache_protected_len
swa_evict_floor = req.swa_evict_floor swa_evict_floor = req.kv.swa_evict_floor
swa_evicted_seqlen = req.kv.swa_evicted_seqlen swa_evicted_seqlen = req.kv.swa_evicted_seqlen
if self.page_size > 1 and swa_evict_floor > 0: if self.page_size > 1 and swa_evict_floor > 0:
+9 -9
View File
@@ -467,10 +467,10 @@ class RadixCache(BasePrefixCache):
if self.disable: if self.disable:
# The protected prefix is not this req's to free. # 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, req.cache_protected_len : kv_len_to_handle req.req_pool_idx, req.kv.cache_protected_len : kv_len_to_handle
] ]
self.token_to_kv_pool_allocator.free_segment( self.token_to_kv_pool_allocator.free_segment(
kv_indices, start_pos=req.cache_protected_len kv_indices, start_pos=req.kv.cache_protected_len
) )
return return
@@ -502,8 +502,8 @@ class RadixCache(BasePrefixCache):
self.token_to_kv_pool_allocator.free_segments( self.token_to_kv_pool_allocator.free_segments(
[ [
( (
kv_indices[req.cache_protected_len : freed_end], kv_indices[req.kv.cache_protected_len : freed_end],
req.cache_protected_len, req.kv.cache_protected_len,
), ),
(kv_indices[key_len:], key_len), (kv_indices[key_len:], key_len),
] ]
@@ -543,8 +543,8 @@ class RadixCache(BasePrefixCache):
new_prefix_len = result.prefix_len new_prefix_len = result.prefix_len
self.token_to_kv_pool_allocator.free_segment( self.token_to_kv_pool_allocator.free_segment(
kv_indices[req.cache_protected_len : new_prefix_len], kv_indices[req.kv.cache_protected_len : new_prefix_len],
start_pos=req.cache_protected_len, start_pos=req.kv.cache_protected_len,
) )
# The prefix indices could be updated, reuse it # The prefix indices could be updated, reuse it
@@ -558,15 +558,15 @@ class RadixCache(BasePrefixCache):
), f"{len(new_indices)=}, {len(radix_key)=}" ), f"{len(new_indices)=}, {len(radix_key)=}"
self.req_to_token_pool.write( self.req_to_token_pool.write(
(req.req_pool_idx, slice(req.cache_protected_len, len(new_indices))), (req.req_pool_idx, slice(req.kv.cache_protected_len, len(new_indices))),
new_indices[req.cache_protected_len :], new_indices[req.kv.cache_protected_len :],
) )
# The cache_protected_len is not always equal to len(req.prefix_indices) # The cache_protected_len is not always equal to len(req.prefix_indices)
# since for page_size > 1, the partial part is added to req.prefix_indices, but that part of kv indices is not added to the tree. # since for page_size > 1, the partial part is added to req.prefix_indices, but that part of kv indices is not added to the tree.
# It should be freed in the next cache_unfinished_req and final cache_finished_req to avoid memory leak. # It should be freed in the next cache_unfinished_req and final cache_finished_req to avoid memory leak.
# So we introduce this `cache_protected_len` field to make sure the partial part can be freed correctly. # So we introduce this `cache_protected_len` field to make sure the partial part can be freed correctly.
req.cache_protected_len = len(new_indices) req.kv.cache_protected_len = len(new_indices)
self.dec_lock_ref(req.last_node) self.dec_lock_ref(req.last_node)
self.inc_lock_ref(new_last_node) self.inc_lock_ref(new_last_node)
@@ -483,7 +483,7 @@ class SWARadixCache(BasePrefixCache):
).page_aligned(self.page_size) ).page_aligned(self.page_size)
page_aligned_len = len(radix_key) page_aligned_len = len(radix_key)
values = kv_indices[:page_aligned_len].to(dtype=torch.int64, copy=True) values = kv_indices[:page_aligned_len].to(dtype=torch.int64, copy=True)
old_prefix_len = req.cache_protected_len old_prefix_len = req.kv.cache_protected_len
# Radix Cache takes one ref in memory pool # Radix Cache takes one ref in memory pool
# Note: the insert function already frees the overlapped kv_indices # Note: the insert function already frees the overlapped kv_indices
@@ -535,7 +535,7 @@ class SWARadixCache(BasePrefixCache):
cache_salt=req.cache_salt, cache_salt=req.cache_salt,
).page_aligned(self.page_size) ).page_aligned(self.page_size)
values = kv_indices[: len(radix_key)].to(dtype=torch.int64, copy=True) values = kv_indices[: len(radix_key)].to(dtype=torch.int64, copy=True)
old_prefix_len = req.cache_protected_len old_prefix_len = req.kv.cache_protected_len
# Radix Cache takes one ref in memory pool # Radix Cache takes one ref in memory pool
# Note: the insert function already frees the overlapped kv_indices # Note: the insert function already frees the overlapped kv_indices
@@ -565,7 +565,7 @@ class SWARadixCache(BasePrefixCache):
new_indices[old_prefix_len:], new_indices[old_prefix_len:],
) )
req.cache_protected_len = len(new_indices) req.kv.cache_protected_len = len(new_indices)
self.dec_lock_ref( self.dec_lock_ref(
req.last_node, req.last_node,
@@ -242,7 +242,7 @@ Cache an in-progress request's partial KV data (chunked prefill).
| **Purpose** | During chunked prefill, insert partial results so the next chunk can match the prefix | | **Purpose** | During chunked prefill, insert partial results so the next chunk can match the prefix |
| **Inputs** | `req` — the in-progress request | | **Inputs** | `req` — the in-progress request |
| **Output** | `None` | | **Output** | `None` |
| **Mutation** | Inserts partial KV → re-matches prefix → updates `req.prefix_indices`, `req.cache_protected_len`, `req.last_node`; transfers lock from old node to new node | | **Mutation** | Inserts partial KV → re-matches prefix → updates `req.prefix_indices`, `req.kv.cache_protected_len`, `req.last_node`; transfers lock from old node to new node |
| **Complexity** | **O(K + D·C)** — two tree traversals: insert O(K + D·C) + re-match O(K + D·C) + lock transfer O(D). Simplifies to **O(K)**. | | **Complexity** | **O(K + D·C)** — two tree traversals: insert O(K + D·C) + re-match O(K + D·C) + lock transfer O(D). Simplifies to **O(K)**. |
**Algorithm detail:** **Algorithm detail:**
@@ -252,7 +252,7 @@ Cache an in-progress request's partial KV data (chunked prefill).
4. Writes new prefix indices into `req_to_token_pool` 4. Writes new prefix indices into `req_to_token_pool`
5. `dec_lock_ref()` on old `req.last_node` 5. `dec_lock_ref()` on old `req.last_node`
6. `inc_lock_ref()` on new matched node 6. `inc_lock_ref()` on new matched node
7. Updates `req.prefix_indices`, `req.cache_protected_len`, `req.last_node` 7. Updates `req.prefix_indices`, `req.kv.cache_protected_len`, `req.last_node`
8. `cleanup_after_caching_req()` per component 8. `cleanup_after_caching_req()` per component
--- ---
@@ -838,7 +838,7 @@ class UnifiedRadixCache(BasePrefixCache):
if is_insert: if is_insert:
insert_params = InsertParams( insert_params = InsertParams(
prev_prefix_len=req.cache_protected_len, prev_prefix_len=req.kv.cache_protected_len,
priority=getattr(req, "priority", 0) or 0, priority=getattr(req, "priority", 0) or 0,
) )
@@ -859,7 +859,7 @@ class UnifiedRadixCache(BasePrefixCache):
kv_indices_full = kv_indices kv_indices_full = kv_indices
tail_free_start = None tail_free_start = None
if effective_cache_len < len(token_ids): if effective_cache_len < len(token_ids):
tail_free_start = max(effective_cache_len, req.cache_protected_len) tail_free_start = max(effective_cache_len, req.kv.cache_protected_len)
token_ids = token_ids[:effective_cache_len] token_ids = token_ids[:effective_cache_len]
kv_indices = kv_indices[:effective_cache_len] kv_indices = kv_indices[:effective_cache_len]
@@ -883,8 +883,8 @@ class UnifiedRadixCache(BasePrefixCache):
self.token_to_kv_pool_allocator.free_segments(segments) self.token_to_kv_pool_allocator.free_segments(segments)
else: else:
self.token_to_kv_pool_allocator.free_segment( self.token_to_kv_pool_allocator.free_segment(
kv_indices[req.cache_protected_len :], kv_indices[req.kv.cache_protected_len :],
start_pos=req.cache_protected_len, start_pos=req.kv.cache_protected_len,
) )
self._dec_req_lock(req, skip_swa=req.swa_prefix_lock_released) self._dec_req_lock(req, skip_swa=req.swa_prefix_lock_released)
@@ -925,7 +925,7 @@ class UnifiedRadixCache(BasePrefixCache):
# components prepare insert data + return effective cache_len # components prepare insert data + return effective cache_len
insert_params = InsertParams( insert_params = InsertParams(
prev_prefix_len=req.cache_protected_len, prev_prefix_len=req.kv.cache_protected_len,
chunked=chunked, chunked=chunked,
priority=getattr(req, "priority", 0) or 0, priority=getattr(req, "priority", 0) or 0,
) )
@@ -981,14 +981,14 @@ class UnifiedRadixCache(BasePrefixCache):
new_last_node = match_result.last_device_node new_last_node = match_result.last_device_node
new_prefix_len = result.prefix_len new_prefix_len = result.prefix_len
assert ( assert (
req.cache_protected_len <= len(new_indices) + self.page_size - 1 req.kv.cache_protected_len <= len(new_indices) + self.page_size - 1
), f"{req.cache_protected_len=}, {len(new_indices)=}, {page_aligned_len=}" ), f"{req.kv.cache_protected_len=}, {len(new_indices)=}, {page_aligned_len=}"
assert new_prefix_len <= len( assert new_prefix_len <= len(
new_indices new_indices
), f"{new_prefix_len=}, {len(new_indices)=}" ), f"{new_prefix_len=}, {len(new_indices)=}"
self.req_to_token_pool.write( self.req_to_token_pool.write(
(req.req_pool_idx, slice(req.cache_protected_len, len(new_indices))), (req.req_pool_idx, slice(req.kv.cache_protected_len, len(new_indices))),
new_indices[req.cache_protected_len :], new_indices[req.kv.cache_protected_len :],
) )
self._dec_req_lock(req) self._dec_req_lock(req)
@@ -1014,7 +1014,7 @@ class UnifiedRadixCache(BasePrefixCache):
) )
else: else:
req.prefix_indices = new_indices req.prefix_indices = new_indices
req.cache_protected_len = len(new_indices) req.kv.cache_protected_len = len(new_indices)
req.last_node = new_last_node req.last_node = new_last_node
req.swa_uuid_for_lock = lock_result.swa_uuid_for_lock req.swa_uuid_for_lock = lock_result.swa_uuid_for_lock
# carry the skip set so this node's dec releases only what we locked # carry the skip set so this node's dec releases only what we locked
+20 -20
View File
@@ -51,7 +51,6 @@ class SessionSlot:
# First req's radix tree node (for dec_lock_ref on session close) # First req's radix tree node (for dec_lock_ref on session close)
last_node: Any = None last_node: Any = None
cache_protected_len: int = 0
swa_uuid_for_lock: Optional[str] = None swa_uuid_for_lock: Optional[str] = None
# components the first req skipped locking on last_node, so release dec # components the first req skipped locking on last_node, so release dec
# releases only what it took (may share the node with another req). # releases only what it took (may share the node with another req).
@@ -74,13 +73,18 @@ class SessionSlot:
"""Save KV state from a finishing request into this slot.""" """Save KV state from a finishing request into this slot."""
self.req_pool_idx = req.req_pool_idx self.req_pool_idx = req.req_pool_idx
self.kv_committed_len = req.kv_committed_len self.kv_committed_len = req.kv_committed_len
self.kv = copy.copy(req.kv)
if is_first: if is_first:
self.last_node = req.last_node self.last_node = req.last_node
self.cache_protected_len = req.cache_protected_len
self.swa_uuid_for_lock = req.swa_uuid_for_lock self.swa_uuid_for_lock = req.swa_uuid_for_lock
self.skip_lock_node_ids = req.skip_lock_node_ids self.skip_lock_node_ids = req.skip_lock_node_ids
else:
# The protected prefix is the first request's tree lock; nothing hands
# KV to the tree after that, so later turns must not have moved it.
assert req.kv.cache_protected_len == self.kv.cache_protected_len
# Transfer the ownership of this kv row
self.kv = copy.copy(req.kv)
self.mamba_pool_idx = req.mamba_pool_idx self.mamba_pool_idx = req.mamba_pool_idx
self.mamba_ping_pong_track_buffer = req.mamba_ping_pong_track_buffer self.mamba_ping_pong_track_buffer = req.mamba_ping_pong_track_buffer
@@ -89,14 +93,8 @@ class SessionSlot:
self.mamba_last_track_seqlen = req.mamba_last_track_seqlen self.mamba_last_track_seqlen = req.mamba_last_track_seqlen
self.mamba_branching_seqlen = req.mamba_branching_seqlen self.mamba_branching_seqlen = req.mamba_branching_seqlen
# Ownership has transferred to the slot. Null *all* of the req's # Ownership moved to the slot; clear the req's references so a later
# references so any later alloc()/free path that inspects the req # alloc/retract path cannot mistake slot-owned mamba state for its own.
# (e.g. the alloc-skip check on `req.mamba_ping_pong_track_buffer
# is None`, or the retract cleanup) sees no dangling pointers
# into slot-owned tensors. Without this the alloc path can decide
# the req still has a ping-pong buffer and skip alloc, causing
# the slot's tensor to be reused by a new req and leaked when
# the slot is later freed.
req.req_pool_idx = None req.req_pool_idx = None
req.kv = ReqKvInfo() req.kv = ReqKvInfo()
req.mamba_pool_idx = None req.mamba_pool_idx = None
@@ -258,7 +256,10 @@ class StreamingSession(BasePrefixCache):
aligned_prefix_len = ( aligned_prefix_len = (
expected_prefix_len // self.page_size expected_prefix_len // self.page_size
) * self.page_size ) * self.page_size
if aligned_prefix_len < slot.cache_protected_len or aligned_prefix_len == 0: if (
aligned_prefix_len < slot.kv.cache_protected_len
or aligned_prefix_len == 0
):
# Release KV to avoid leak and fallback to full prefill. # Release KV to avoid leak and fallback to full prefill.
# req remains unassigned, so alloc_for_extend treats it as new. # req remains unassigned, so alloc_for_extend treats it as new.
self.release_session(req.session.session_id) self.release_session(req.session.session_id)
@@ -273,9 +274,9 @@ class StreamingSession(BasePrefixCache):
# Streaming sessions are append-only (session_controller rollback # Streaming sessions are append-only (session_controller rollback
# ensures req_nodes always points to the last successful req). # ensures req_nodes always points to the last successful req).
assert prefix_len >= slot.cache_protected_len, ( assert prefix_len >= slot.kv.cache_protected_len, (
f"streaming session prefix shrank: {prefix_len=} < " f"streaming session prefix shrank: {prefix_len=} < "
f"{slot.cache_protected_len=}" f"{slot.kv.cache_protected_len=}"
) )
# Floor-align prefix_len to page boundary (NPU workaround). # Floor-align prefix_len to page boundary (NPU workaround).
@@ -300,7 +301,7 @@ class StreamingSession(BasePrefixCache):
last_device_node=slot.virtual_node, last_device_node=slot.virtual_node,
last_host_node=slot.virtual_node, last_host_node=slot.virtual_node,
best_match_node=slot.virtual_node, best_match_node=slot.virtual_node,
cache_protected_len=slot.cache_protected_len, cache_protected_len=slot.kv.cache_protected_len,
) )
def try_cache_finished_req( def try_cache_finished_req(
@@ -326,7 +327,7 @@ class StreamingSession(BasePrefixCache):
if slot is None: if slot is None:
# First-request mid-processing abort: create ephemeral # First-request mid-processing abort: create ephemeral
# slot from req state so release_session handles cleanup. # slot from req state so release_session handles cleanup.
# Include last_node/cache_protected_len from the req so # Include last_node from the req so
# release_session calls dec_lock_ref on the tree lock. # release_session calls dec_lock_ref on the tree lock.
# Also carry the mamba refs over so _free_slot_mamba can # Also carry the mamba refs over so _free_slot_mamba can
# return the (possibly extra_buffer ping-pong) slots to # return the (possibly extra_buffer ping-pong) slots to
@@ -335,7 +336,6 @@ class StreamingSession(BasePrefixCache):
req_pool_idx=req.req_pool_idx, req_pool_idx=req.req_pool_idx,
kv=copy.copy(req.kv), kv=copy.copy(req.kv),
last_node=req.last_node, last_node=req.last_node,
cache_protected_len=req.cache_protected_len,
swa_uuid_for_lock=req.swa_uuid_for_lock, swa_uuid_for_lock=req.swa_uuid_for_lock,
skip_lock_node_ids=req.skip_lock_node_ids, skip_lock_node_ids=req.skip_lock_node_ids,
mamba_pool_idx=req.mamba_pool_idx, mamba_pool_idx=req.mamba_pool_idx,
@@ -441,7 +441,7 @@ class StreamingSession(BasePrefixCache):
slot = self.slots.pop(session_id, None) slot = self.slots.pop(session_id, None)
if slot is None: if slot is None:
return return
protected_len = slot.cache_protected_len protected_len = slot.kv.cache_protected_len
lock_node = slot.last_node lock_node = slot.last_node
tokens_freed = ( tokens_freed = (
max(0, slot.kv.kv_allocated_len - protected_len) max(0, slot.kv.kv_allocated_len - protected_len)
@@ -490,7 +490,7 @@ class StreamingSession(BasePrefixCache):
) )
if slot.is_holding_kv and not in_batch: if slot.is_holding_kv and not in_batch:
allocated = ceil_align(slot.kv.kv_allocated_len, self.page_size) allocated = ceil_align(slot.kv.kv_allocated_len, self.page_size)
total += allocated - slot.cache_protected_len total += allocated - slot.kv.cache_protected_len
return total return total
def session_held_full_tokens(self, active_pool_idxs: Optional[set] = None) -> int: def session_held_full_tokens(self, active_pool_idxs: Optional[set] = None) -> int:
@@ -507,7 +507,7 @@ class StreamingSession(BasePrefixCache):
if slot.is_holding_kv and not in_batch: if slot.is_holding_kv and not in_batch:
allocated = ceil_align(slot.kv.kv_allocated_len, self.page_size) allocated = ceil_align(slot.kv.kv_allocated_len, self.page_size)
total += allocated - max( total += allocated - max(
slot.cache_protected_len, slot.kv.swa_evicted_seqlen slot.kv.cache_protected_len, slot.kv.swa_evicted_seqlen
) )
return total return total
@@ -304,7 +304,7 @@ class TestRadixNoTailChunked(ScriptedTestCase):
if req is not None and req.rid == r.rid: if req is not None and req.rid == r.rid:
observed_mid_chunk = True observed_mid_chunk = True
prefix_len: int = len(req.prefix_indices) prefix_len: int = len(req.prefix_indices)
protected_len: int = req.cache_protected_len protected_len: int = req.kv.cache_protected_len
assert prefix_len == protected_len, ( assert prefix_len == protected_len, (
f"page_size=1 must take the no-tail else branch: " f"page_size=1 must take the no-tail else branch: "
f"len(prefix_indices)={prefix_len} != " f"len(prefix_indices)={prefix_len} != "
@@ -460,7 +460,7 @@ class TestRadixPartialPage(ScriptedTestCase):
req = s.chunked_req req = s.chunked_req
if req is not None and req.rid == r.rid: if req is not None and req.rid == r.rid:
prefix_len: int = len(req.prefix_indices) prefix_len: int = len(req.prefix_indices)
protected_len: int = req.cache_protected_len protected_len: int = req.kv.cache_protected_len
assert prefix_len >= protected_len, ( assert prefix_len >= protected_len, (
f"len(prefix_indices)={prefix_len} dropped below " f"len(prefix_indices)={prefix_len} dropped below "
f"cache_protected_len={protected_len}: tail was freed " f"cache_protected_len={protected_len}: tail was freed "
@@ -226,7 +226,7 @@ def test_streaming_session_release_frees_compressed_slots():
) )
session.slots["session-a"] = SessionSlot( session.slots["session-a"] = SessionSlot(
req_pool_idx=req_pool_idx, req_pool_idx=req_pool_idx,
kv=SimpleNamespace(kv_allocated_len=16), kv=SimpleNamespace(kv_allocated_len=16, cache_protected_len=0),
) )
session.release_session("session-a") session.release_session("session-a")
@@ -97,7 +97,7 @@ class TestDecodePreallocQueuePriority(unittest.TestCase):
finished_reason=FINISH_ABORT("failed") if failed else None, finished_reason=FINISH_ABORT("failed") if failed else None,
return_logprob=False, return_logprob=False,
sampling_params=SimpleNamespace(max_new_tokens=8), sampling_params=SimpleNamespace(max_new_tokens=8),
cache_protected_len=0, kv=SimpleNamespace(cache_protected_len=0),
time_stats=MagicMock(), time_stats=MagicMock(),
) )
return SimpleNamespace( return SimpleNamespace(
@@ -12,7 +12,7 @@ from sglang.test.test_utils import CustomTestCase, maybe_stub_sgl_kernel
maybe_stub_sgl_kernel() maybe_stub_sgl_kernel()
from sglang.srt.managers.schedule_batch import NextBatchPlan, Req from sglang.srt.managers.schedule_batch import NextBatchPlan, Req, ReqKvInfo
from sglang.srt.managers.scheduler import Scheduler from sglang.srt.managers.scheduler import Scheduler
from sglang.srt.mem_cache.chunk_cache import ChunkCache from sglang.srt.mem_cache.chunk_cache import ChunkCache
from sglang.srt.utils.common import Range from sglang.srt.utils.common import Range
@@ -38,7 +38,7 @@ def _make_req(
req.extend_range = Range(fill_len - extend_input_len, fill_len) req.extend_range = Range(fill_len - extend_input_len, fill_len)
req.inflight_middle_chunks = 0 req.inflight_middle_chunks = 0
req.host_hit_length = 0 req.host_hit_length = 0
req.cache_protected_len = 0 req.kv = ReqKvInfo()
req.skip_radix_cache_insert = False req.skip_radix_cache_insert = False
req.last_node = None req.last_node = None
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
@@ -76,14 +76,15 @@ class MockReq:
) )
self.output_ids = array("q", [fill_ids[-1]] if len(fill_ids) > 1 else []) self.output_ids = array("q", [fill_ids[-1]] if len(fill_ids) > 1 else [])
self.req_pool_idx = req_pool_idx self.req_pool_idx = req_pool_idx
self.cache_protected_len = cache_protected_len
self.last_node = last_node self.last_node = last_node
self.extra_key = None self.extra_key = None
self.cache_salt = None self.cache_salt = None
self.prefix_indices = torch.empty(0, dtype=torch.int64) self.prefix_indices = torch.empty(0, dtype=torch.int64)
self.priority = 0 self.priority = 0
self.kv_committed_len = len(fill_ids) self.kv_committed_len = len(fill_ids)
self.kv = SimpleNamespace(kv_allocated_len=len(fill_ids)) self.kv = SimpleNamespace(
kv_allocated_len=len(fill_ids), cache_protected_len=cache_protected_len
)
def get_fill_ids(self): def get_fill_ids(self):
return self.full_untruncated_fill_ids[: self.extend_range.end] return self.full_untruncated_fill_ids[: self.extend_range.end]
@@ -368,7 +369,7 @@ class TestDecodeLockRefScenarios(unittest.TestCase):
req.output_ids = [99] req.output_ids = [99]
req.last_node = object() req.last_node = object()
req.finished_reason = None req.finished_reason = None
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = 123 req.swa_uuid_for_lock = 123
req.swa_prefix_lock_released = False req.swa_prefix_lock_released = False
req.pd_rebootstrap_in_progress = False req.pd_rebootstrap_in_progress = False
@@ -22,9 +22,11 @@ class _FakeAllocator:
class _FakeReq: class _FakeReq:
req_pool_idx = 0 req_pool_idx = 0
swa_evict_floor = 3
cache_protected_len = 0 def __init__(self):
kv = SimpleNamespace(swa_evicted_seqlen=6) self.kv = SimpleNamespace(
swa_evicted_seqlen=6, swa_evict_floor=3, cache_protected_len=0
)
def pop_committed_kv_cache(self): def pop_committed_kv_cache(self):
return 8 return 8
@@ -51,7 +53,7 @@ class TestPureSWAChunkCache(CustomTestCase):
def test_finished_req_skips_protected_prefix(self): def test_finished_req_skips_protected_prefix(self):
cache = self._make_cache() cache = self._make_cache()
req = _FakeReq() req = _FakeReq()
req.cache_protected_len = 2 req.kv.cache_protected_len = 2
cache.cache_finished_req(req, kv_len_to_handle=8) cache.cache_finished_req(req, kv_len_to_handle=8)
@@ -27,6 +27,7 @@ import random
import unittest import unittest
import unittest.mock import unittest.mock
from array import array from array import array
from types import SimpleNamespace
import torch import torch
@@ -518,7 +519,7 @@ class TestRadixCache(unittest.TestCase):
cache.req_to_token_pool = ReqToTokenPool(request_indices.clone()) cache.req_to_token_pool = ReqToTokenPool(request_indices.clone())
req = unittest.mock.Mock( req = unittest.mock.Mock(
req_pool_idx=0, req_pool_idx=0,
cache_protected_len=0, kv=SimpleNamespace(cache_protected_len=0),
extra_key=None, extra_key=None,
cache_salt=None, cache_salt=None,
priority=0, priority=0,
@@ -12,6 +12,7 @@ register_cpu_ci(est_time=5, suite="base-a-test-cpu")
import unittest import unittest
import unittest.mock import unittest.mock
from array import array from array import array
from types import SimpleNamespace
import torch import torch
@@ -39,7 +40,7 @@ class _StubReq:
self.host_hit_length = None self.host_hit_length = None
self.num_matched_prefix_tokens = 0 self.num_matched_prefix_tokens = 0
self.mamba_branching_seqlen = None self.mamba_branching_seqlen = None
self.cache_protected_len = None self.kv = SimpleNamespace(cache_protected_len=None)
def _compute_max_prefix_len(self, input_len): def _compute_max_prefix_len(self, input_len):
return max(input_len - 1, 0) return max(input_len - 1, 0)
@@ -72,13 +72,13 @@ class _FakeReq:
self.kv = SimpleNamespace( self.kv = SimpleNamespace(
kv_allocated_len=allocated, kv_allocated_len=allocated,
swa_evicted_seqlen=0, swa_evicted_seqlen=0,
cache_protected_len=0,
) )
self.origin_input_ids = list(range(committed)) self.origin_input_ids = list(range(committed))
self.output_ids = [] self.output_ids = []
self.extra_key = None self.extra_key = None
self.cache_salt = None self.cache_salt = None
self.last_node = None self.last_node = None
self.cache_protected_len = 0
self.swa_uuid_for_lock = None self.swa_uuid_for_lock = None
self.skip_lock_node_ids = {} self.skip_lock_node_ids = {}
self.mamba_pool_idx = None self.mamba_pool_idx = None
@@ -114,8 +114,9 @@ def test_preabort_detaches_session_and_preserves_slot():
tree_cache.slots["session-a"] = SessionSlot( tree_cache.slots["session-a"] = SessionSlot(
req_pool_idx=0, req_pool_idx=0,
kv_committed_len=48, kv_committed_len=48,
kv=SimpleNamespace(kv_allocated_len=48, swa_evicted_seqlen=0), kv=SimpleNamespace(
cache_protected_len=16, kv_allocated_len=48, swa_evicted_seqlen=0, cache_protected_len=16
),
) )
req = _FakeReq("session-a", req_pool_idx=1, committed=1, allocated=1) req = _FakeReq("session-a", req_pool_idx=1, committed=1, allocated=1)
@@ -178,9 +179,10 @@ def test_nth_mid_abort_nukes_session_slot():
tree_cache.slots["session-a"] = SessionSlot( tree_cache.slots["session-a"] = SessionSlot(
req_pool_idx=0, req_pool_idx=0,
kv_committed_len=50, kv_committed_len=50,
kv=SimpleNamespace(kv_allocated_len=50, swa_evicted_seqlen=0), kv=SimpleNamespace(
kv_allocated_len=50, swa_evicted_seqlen=0, cache_protected_len=0
),
last_node=None, last_node=None,
cache_protected_len=0,
) )
# Mid-processing abort: req has the SESSION slot's pool_idx (restore_to_req ran). # Mid-processing abort: req has the SESSION slot's pool_idx (restore_to_req ran).
@@ -216,9 +218,10 @@ def test_release_session_threads_mamba_skip_ids():
tree_cache.slots["session-a"] = SessionSlot( tree_cache.slots["session-a"] = SessionSlot(
req_pool_idx=0, req_pool_idx=0,
kv_committed_len=50, kv_committed_len=50,
kv=SimpleNamespace(kv_allocated_len=50, swa_evicted_seqlen=0), kv=SimpleNamespace(
kv_allocated_len=50, swa_evicted_seqlen=0, cache_protected_len=0
),
last_node=lock_node, last_node=lock_node,
cache_protected_len=0,
skip_lock_node_ids={ComponentType.MAMBA: {42}}, skip_lock_node_ids={ComponentType.MAMBA: {42}},
) )
@@ -18,7 +18,7 @@ from types import SimpleNamespace
import torch import torch
from sglang.srt.managers.schedule_batch import ScheduleBatch from sglang.srt.managers.schedule_batch import ReqKvInfo, ScheduleBatch
from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator from sglang.srt.mem_cache.allocator.swa import SWATokenToKVPoolAllocator
from sglang.srt.mem_cache.cache_init_params import CacheInitParams 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.common import free_swa_out_of_window_slots
@@ -107,10 +107,7 @@ def _make_req(req_pool_idx, token_ids, cache_protected_len, tree):
is_holding_kv=True, is_holding_kv=True,
origin_input_ids=token_ids, origin_input_ids=token_ids,
output_ids=[], output_ids=[],
cache_protected_len=cache_protected_len, kv=ReqKvInfo(cache_protected_len=cache_protected_len),
kv=SimpleNamespace(
swa_evicted_seqlen=0,
),
extra_key=None, extra_key=None,
cache_salt=None, cache_salt=None,
last_node=tree.root_node, last_node=tree.root_node,
@@ -36,7 +36,7 @@ class _DummyReq:
def __init__(self): def __init__(self):
self._kv_committed_len = 0 self._kv_committed_len = 0
self.swa_prefix_lock_released = False self.swa_prefix_lock_released = False
self.kv = SimpleNamespace(swa_evicted_seqlen=0) self.kv = SimpleNamespace(swa_evicted_seqlen=0, cache_protected_len=0)
def _build_swa_tree( def _build_swa_tree(
@@ -682,7 +682,7 @@ class TestSWA(unittest.TestCase):
req.last_node = tree.root_node req.last_node = tree.root_node
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.kv.swa_evicted_seqlen = 0 req.kv.swa_evicted_seqlen = 0
req.cache_protected_len = 1 req.kv.cache_protected_len = 1
# Intentionally mismatch to ensure code does not use len(prefix_indices). # Intentionally mismatch to ensure code does not use len(prefix_indices).
req.prefix_indices = torch.tensor([7, 8, 9, 10, 11], device=tree.device) req.prefix_indices = torch.tensor([7, 8, 9, 10, 11], device=tree.device)
@@ -700,7 +700,7 @@ class TestSWA(unittest.TestCase):
req, is_insert=True, kv_len_to_handle=req._kv_committed_len req, is_insert=True, kv_len_to_handle=req._kv_committed_len
) )
self.assertEqual(captured["prev_prefix_len"], req.cache_protected_len) self.assertEqual(captured["prev_prefix_len"], req.kv.cache_protected_len)
self.assertTrue(captured["is_bigram"]) self.assertTrue(captured["is_bigram"])
self.assertEqual(captured["key_len"], len(req.origin_input_ids) - 1) self.assertEqual(captured["key_len"], len(req.origin_input_ids) - 1)
@@ -720,7 +720,7 @@ class TestSWA(unittest.TestCase):
req2.last_node = tree.root_node req2.last_node = tree.root_node
req2.swa_uuid_for_lock = None req2.swa_uuid_for_lock = None
req2.kv.swa_evicted_seqlen = 0 req2.kv.swa_evicted_seqlen = 0
req2.cache_protected_len = 1 req2.kv.cache_protected_len = 1
req2.prefix_indices = torch.tensor([21, 22, 23, 24, 25], device=tree.device) req2.prefix_indices = torch.tensor([21, 22, 23, 24, 25], device=tree.device)
freed_lens = [] freed_lens = []
@@ -917,7 +917,7 @@ class TestCacheUnfinishedReqEvictedPrefix(CustomTestCase):
req.get_fill_ids = lambda: token_ids req.get_fill_ids = lambda: token_ids
req.extra_key = None req.extra_key = None
req.cache_salt = None req.cache_salt = None
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.last_node = tree.root_node req.last_node = tree.root_node
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.prefix_indices = torch.empty(0, dtype=torch.int64, device=tree.device) req.prefix_indices = torch.empty(0, dtype=torch.int64, device=tree.device)
@@ -928,7 +928,7 @@ class TestCacheUnfinishedReqEvictedPrefix(CustomTestCase):
# The insert itself frees nothing. # The insert itself frees nothing.
self.assertEqual(allocator.swa_available_size(), swa_before) self.assertEqual(allocator.swa_available_size(), swa_before)
# The live leaf holds a full window, so the whole key stays matchable. # The live leaf holds a full window, so the whole key stays matchable.
self.assertEqual(req.cache_protected_len, num_tokens) self.assertEqual(req.kv.cache_protected_len, num_tokens)
# [0, evicted) is a tombstone; only [evicted, num_tokens) counts as SWA. # [0, evicted) is a tombstone; only [evicted, num_tokens) counts as SWA.
(first,) = tree.root_node.children.values() (first,) = tree.root_node.children.values()
self.assertTrue(first.swa_tombstone) self.assertTrue(first.swa_tombstone)
@@ -642,7 +642,7 @@ def bench_cache_finished(
len(req.prefix_indices), len(req.full_untruncated_fill_ids) len(req.prefix_indices), len(req.full_untruncated_fill_ids)
) )
req.last_node = node req.last_node = node
req.cache_protected_len = matched_len req.kv.cache_protected_len = matched_len
req.kv_committed_len = len(seq) req.kv_committed_len = len(seq)
if hasattr(lr, "swa_uuid_for_lock"): if hasattr(lr, "swa_uuid_for_lock"):
req.swa_uuid_for_lock = lr.swa_uuid_for_lock req.swa_uuid_for_lock = lr.swa_uuid_for_lock
@@ -1228,7 +1228,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices)
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
req.full_untruncated_fill_ids = array("q", input_ids + output_ids) req.full_untruncated_fill_ids = array("q", input_ids + output_ids)
@@ -1269,7 +1269,7 @@ class UnifiedRadixCacheSuite:
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.kv.kv_allocated_len = kv_len req.kv.kv_allocated_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
if self.cfg.has_mamba: if self.cfg.has_mamba:
@@ -1313,7 +1313,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices)
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.swa_prefix_lock_released = True req.swa_prefix_lock_released = True
req.extra_key = None req.extra_key = None
@@ -1348,7 +1348,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices)
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
if self.cfg.has_mamba: if self.cfg.has_mamba:
@@ -1357,7 +1357,7 @@ class UnifiedRadixCacheSuite:
cache.cache_unfinished_req(req) cache.cache_unfinished_req(req)
self.assertGreater(len(req.prefix_indices), 0) self.assertGreater(len(req.prefix_indices), 0)
self.assertEqual(req.cache_protected_len, len(req.prefix_indices)) self.assertEqual(req.kv.cache_protected_len, len(req.prefix_indices))
self.assertIsNotNone(req.last_node) self.assertIsNotNone(req.last_node)
self.assertFalse(req.swa_prefix_lock_released) self.assertFalse(req.swa_prefix_lock_released)
@@ -1385,7 +1385,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, len(tokens))), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, len(tokens))), kv_indices)
req.kv_committed_len = len(tokens) req.kv_committed_len = len(tokens)
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
req.kv.swa_evicted_seqlen = evicted_len req.kv.swa_evicted_seqlen = evicted_len
@@ -1487,7 +1487,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), kv_indices)
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
req.full_untruncated_fill_ids = array("q", input_ids) req.full_untruncated_fill_ids = array("q", input_ids)
@@ -1612,7 +1612,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), fresh_value) req_to_token_pool.write((req.req_pool_idx, slice(0, kv_len)), fresh_value)
req.kv_committed_len = kv_len req.kv_committed_len = kv_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
req.kv.swa_evicted_seqlen = 0 req.kv.swa_evicted_seqlen = 0
@@ -1639,7 +1639,7 @@ class UnifiedRadixCacheSuite:
swa_value, swa_value,
) )
) )
self.assertEqual(req.cache_protected_len, len(tokens)) self.assertEqual(req.kv.cache_protected_len, len(tokens))
cache.dec_lock_ref( cache.dec_lock_ref(
req.last_node, req.last_node,
@@ -2199,7 +2199,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, pre_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, pre_len)), kv_indices)
req.kv_committed_len = pre_len req.kv_committed_len = pre_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
@@ -2288,7 +2288,7 @@ class UnifiedRadixCacheSuite:
req_to_token_pool.write((req.req_pool_idx, slice(0, pre_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, pre_len)), kv_indices)
req.kv_committed_len = pre_len req.kv_committed_len = pre_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
@@ -6770,7 +6770,7 @@ class TestUnifiedRadixCacheInt8MambaCheckpoint(CustomTestCase):
req.output_ids = array("q") req.output_ids = array("q")
req.kv_committed_len = len(tokens) req.kv_committed_len = len(tokens)
req.kv.kv_allocated_len = len(tokens) req.kv.kv_allocated_len = len(tokens)
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
req.mamba_last_track_seqlen = len(tokens) req.mamba_last_track_seqlen = len(tokens)
@@ -8097,7 +8097,7 @@ class TestSWAWindowUnderBigramKey(CustomTestCase):
req_to_token_pool.write((req.req_pool_idx, slice(0, seq_len)), kv_indices) req_to_token_pool.write((req.req_pool_idx, slice(0, seq_len)), kv_indices)
req.kv_committed_len = seq_len req.kv_committed_len = seq_len
req.last_node = cache.root_node_handle() req.last_node = cache.root_node_handle()
req.cache_protected_len = 0 req.kv.cache_protected_len = 0
req.swa_uuid_for_lock = None req.swa_uuid_for_lock = None
req.extra_key = None req.extra_key = None
@@ -8113,7 +8113,7 @@ class TestSWAWindowUnderBigramKey(CustomTestCase):
f"{self.cfg.sliding_window_size} window", f"{self.cfg.sliding_window_size} window",
) )
self.assertEqual( self.assertEqual(
req.cache_protected_len, req.kv.cache_protected_len,
boundary, boundary,
"the match after the insert must reach the leaf the insert created", "the match after the insert must reach the leaf the insert created",
) )
@@ -72,7 +72,7 @@ def _make_req(rid, req_pool_idx, token_ids, tree):
extra_key=None, extra_key=None,
cache_salt=None, cache_salt=None,
last_node=tree.root_node, last_node=tree.root_node,
cache_protected_len=0, kv=SimpleNamespace(cache_protected_len=0),
priority=0, priority=0,
kv_committed_freed=False, kv_committed_freed=False,
kv_committed_len=len(token_ids), kv_committed_len=len(token_ids),