Fix chunked prefill and KV cache leaks for streaming sessions (#20476)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: hnyls2002 <lsyincs@gmail.com>
This commit is contained in:
@@ -156,10 +156,12 @@ class ReqToTokenPool:
|
||||
# Indices of reqs that already have a req_pool_idx and will reuse
|
||||
# their existing slot (e.g. chunked prefill continuing across chunks).
|
||||
reusing = [i for i, r in enumerate(reqs) if r.req_pool_idx is not None]
|
||||
if not any(r.is_dllm() for r in reqs):
|
||||
assert (
|
||||
sum(1 for i in reusing if reqs[i].is_chunked > 0) <= 1
|
||||
), "only one chunked request may reuse req_pool_idx in a batch"
|
||||
# NOTE: this check is relaxed temporarily
|
||||
# https://github.com/sgl-project/sglang/pull/20476
|
||||
# if not any(r.is_dllm() for r in reqs):
|
||||
# assert (
|
||||
# sum(1 for i in reusing if reqs[i].is_chunked > 0) <= 1
|
||||
# ), "only one chunked request may reuse req_pool_idx in a batch"
|
||||
assert all(
|
||||
reqs[i].is_chunked > 0 or reqs[i].kv_committed_len > 0 for i in reusing
|
||||
), "reusing request must be chunked or have committed KV"
|
||||
|
||||
@@ -94,8 +94,11 @@ class SessionSlot:
|
||||
req.mamba_last_track_seqlen = self.mamba_last_track_seqlen
|
||||
req.mamba_branching_seqlen = self.mamba_branching_seqlen
|
||||
|
||||
self.req_pool_idx = None
|
||||
self.mamba_pool_idx = None
|
||||
# NOTE: req_pool_idx and mamba_pool_idx are intentionally NOT cleared
|
||||
# from the slot. During chunked prefill, a request may be rejected by
|
||||
# the scheduler (e.g. budget exhausted) and retried in the next cycle.
|
||||
# Each retry calls match_prefix -> restore_to_req again, so the slot
|
||||
# must remain intact for idempotent restoration.
|
||||
|
||||
|
||||
def _is_streaming(req: Optional[Req]) -> bool:
|
||||
@@ -201,8 +204,20 @@ class SessionAwareCache(BasePrefixCache):
|
||||
slot.save_from_req(req, is_first=is_first)
|
||||
|
||||
def cache_unfinished_req(self, req: Req, **kwargs):
|
||||
if _is_streaming(req) and req.session.session_id in self.slots:
|
||||
return
|
||||
if _is_streaming(req):
|
||||
# in chunked_prefill for streaming, we skip the stash path which triggers radix.
|
||||
# only the last chunk in first turn trigger a full prompt radix insert.
|
||||
if kwargs.get("chunked", False):
|
||||
kv_indices = self.req_to_token_pool.req_to_token[
|
||||
req.req_pool_idx, : len(req.fill_ids)
|
||||
]
|
||||
req.prefix_indices = kv_indices.to(dtype=torch.int64, copy=True)
|
||||
return
|
||||
if req.session.session_id in self.slots:
|
||||
# Subsequent turns: slot exists, skip inner entirely.
|
||||
return
|
||||
# First turn (no slot): fall through to inner for lock management,
|
||||
# tree insertion, and cache_protected_len updates between chunks.
|
||||
self.inner.cache_unfinished_req(req, **kwargs)
|
||||
|
||||
def evict(self, params: EvictParams) -> EvictResult:
|
||||
|
||||
Reference in New Issue
Block a user