[perf] reduce overhead of fill_ids list reconstruction and decref (#27965)
This commit is contained in:
@@ -695,11 +695,14 @@ class Req(ReqDllmMixin):
|
||||
if origin_input_ids_unpadded
|
||||
else self.origin_input_ids
|
||||
) # Before image padding
|
||||
# Each decode stage's output ids
|
||||
# Each decode stage's output ids. Append-only by contract:
|
||||
# _refresh_fill_ids infers how many output tokens are already in
|
||||
# full_untruncated_fill_ids from lengths alone, so in-place rewrites
|
||||
# that preserve length would silently corrupt fill_ids.
|
||||
self.output_ids = array("q")
|
||||
# Full untruncated sequence: origin + output (+ DLLM mask block).
|
||||
# Rebuilt at the top of each init_next_round_input; admission only
|
||||
# updates fill_len, never mutates this array's length.
|
||||
# Kept in sync by _refresh_fill_ids; admission only updates fill_len,
|
||||
# never mutates this array's length.
|
||||
self.full_untruncated_fill_ids = array("q")
|
||||
self.fill_len: int = 0
|
||||
|
||||
@@ -1069,6 +1072,27 @@ class Req(ReqDllmMixin):
|
||||
def get_fill_ids(self) -> array:
|
||||
return self.full_untruncated_fill_ids[: self.fill_len]
|
||||
|
||||
def _refresh_fill_ids(self) -> None:
|
||||
"""Keep full_untruncated_fill_ids == origin_input_ids + output_ids by
|
||||
appending only the new output tokens.
|
||||
|
||||
Falls back to a full rebuild when the in-place append is invalid:
|
||||
- aliasing: scheduler_pp_mixin assigns full_untruncated_fill_ids =
|
||||
origin_input_ids directly, so extending in place would write output
|
||||
tokens into the origin;
|
||||
- lengths disagree: fresh req (array still empty), retraction
|
||||
(output_ids reset to empty), or set_finish_with_abort (origin
|
||||
replaced by a 1-token stub).
|
||||
"""
|
||||
n_have_output = len(self.full_untruncated_fill_ids) - len(self.origin_input_ids)
|
||||
if (
|
||||
self.full_untruncated_fill_ids is not self.origin_input_ids
|
||||
and 0 <= n_have_output <= len(self.output_ids)
|
||||
):
|
||||
self.full_untruncated_fill_ids.extend(self.output_ids[n_have_output:])
|
||||
else:
|
||||
self.full_untruncated_fill_ids = self.origin_input_ids + self.output_ids
|
||||
|
||||
def init_next_round_input(
|
||||
self,
|
||||
tree_cache: Optional[BasePrefixCache] = None,
|
||||
@@ -1078,7 +1102,7 @@ class Req(ReqDllmMixin):
|
||||
self._init_fill_ids_for_dllm()
|
||||
self.determine_dllm_phase()
|
||||
else:
|
||||
self.full_untruncated_fill_ids = self.origin_input_ids + self.output_ids
|
||||
self._refresh_fill_ids()
|
||||
|
||||
input_len = len(self.full_untruncated_fill_ids)
|
||||
|
||||
@@ -1098,14 +1122,16 @@ class Req(ReqDllmMixin):
|
||||
)
|
||||
self.logprob_start_len = -1
|
||||
|
||||
token_ids_to_match = self.full_untruncated_fill_ids[
|
||||
: self._compute_max_prefix_len(input_len)
|
||||
]
|
||||
# Pass the full array with a raw-token cap (limit) instead of slicing,
|
||||
# avoiding an O(context) copy per prefill-batch build.
|
||||
token_ids_to_match = self.full_untruncated_fill_ids
|
||||
key_limit: Optional[int] = self._compute_max_prefix_len(input_len)
|
||||
|
||||
# Disable prefix caching when embed overrides are present: same token IDs
|
||||
# with different override vectors must not share cached KV values.
|
||||
if self.positional_embed_overrides is not None:
|
||||
token_ids_to_match = array("q")
|
||||
key_limit = None
|
||||
|
||||
if tree_cache is not None:
|
||||
if cow_mamba is None:
|
||||
@@ -1113,7 +1139,9 @@ class Req(ReqDllmMixin):
|
||||
match_result = tree_cache.match_prefix(
|
||||
MatchPrefixParams(
|
||||
key=RadixKey(
|
||||
token_ids=token_ids_to_match, extra_key=self.extra_key
|
||||
token_ids=token_ids_to_match,
|
||||
extra_key=self.extra_key,
|
||||
limit=key_limit,
|
||||
),
|
||||
req=self,
|
||||
cow_mamba=cow_mamba,
|
||||
@@ -2314,7 +2342,7 @@ class ScheduleBatch(ScheduleBatchDisaggregationDecodeMixin):
|
||||
running_bs = running_batch.batch_size()
|
||||
|
||||
for req in running_batch.reqs:
|
||||
req.full_untruncated_fill_ids = req.origin_input_ids + req.output_ids
|
||||
req._refresh_fill_ids()
|
||||
req.fill_len = len(req.full_untruncated_fill_ids)
|
||||
req.set_extend_input_len(1)
|
||||
|
||||
|
||||
@@ -507,7 +507,7 @@ class MambaRadixCache(KVCacheEventMixin, BasePrefixCache):
|
||||
prev_prefix_len = params.prev_prefix_len
|
||||
|
||||
if value is None:
|
||||
value = torch.tensor([x for x in key.token_ids], dtype=torch.int64)
|
||||
value = torch.tensor([x for x in key.raw_token_ids()], dtype=torch.int64)
|
||||
prefix_len, mamba_exist = self._insert_helper(
|
||||
self.root_node, key, value, mamba_value, params.chunked, prev_prefix_len
|
||||
)
|
||||
|
||||
@@ -56,13 +56,14 @@ if TYPE_CHECKING:
|
||||
class RadixKey:
|
||||
"""is_bigram=True: token_ids holds raw tokens (N+1 for N bigrams); slices share one boundary token."""
|
||||
|
||||
__slots__ = ("token_ids", "extra_key", "is_bigram")
|
||||
__slots__ = ("token_ids", "extra_key", "is_bigram", "limit")
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
token_ids: array[int],
|
||||
extra_key: Optional[str] = None,
|
||||
is_bigram: bool = False,
|
||||
limit: Optional[int] = None,
|
||||
):
|
||||
# token ids sequence (raw ints in both modes)
|
||||
self.token_ids = token_ids
|
||||
@@ -70,21 +71,40 @@ class RadixKey:
|
||||
self.extra_key = extra_key
|
||||
# bigram view over token_ids: length = max(0, len(token_ids) - 1)
|
||||
self.is_bigram = is_bigram
|
||||
# Optional cap on raw tokens: behave as if token_ids were sliced to
|
||||
# token_ids[:limit], without the O(n) copy. None = use all tokens.
|
||||
self.limit = limit
|
||||
|
||||
def _raw_len(self) -> int:
|
||||
n = len(self.token_ids)
|
||||
if self.limit is not None and self.limit < n:
|
||||
return self.limit
|
||||
return n
|
||||
|
||||
def raw_token_ids(self) -> array:
|
||||
"""token_ids honoring `limit` (copies only when capped)."""
|
||||
n = self._raw_len()
|
||||
t = self.token_ids
|
||||
return t if n == len(t) else t[:n]
|
||||
|
||||
def __len__(self) -> int:
|
||||
n = self._raw_len()
|
||||
if self.is_bigram:
|
||||
n = len(self.token_ids)
|
||||
return n - 1 if n > 0 else 0
|
||||
return len(self.token_ids)
|
||||
return n
|
||||
|
||||
# TODO(Jialin): vectorize with numpy without PyLong boxing
|
||||
def __iter__(self) -> Iterator:
|
||||
t = self.token_ids
|
||||
n = self._raw_len()
|
||||
if self.is_bigram:
|
||||
t = self.token_ids
|
||||
for i in range(len(t) - 1):
|
||||
for i in range(n - 1 if n > 0 else 0):
|
||||
yield (t[i], t[i + 1])
|
||||
elif n == len(t):
|
||||
yield from t
|
||||
else:
|
||||
yield from self.token_ids
|
||||
for i in range(n):
|
||||
yield t[i]
|
||||
|
||||
def __getitem__(self, idx: Union[int, slice]) -> RadixKey:
|
||||
# Normalize int -> 1-element slice so the rest handles one shape.
|
||||
@@ -166,6 +186,7 @@ class RadixKey:
|
||||
matched = max(0, min(matched_tokens - 1, len(self), len(other)))
|
||||
return (matched // page_size) * page_size if page_size > 1 else matched
|
||||
|
||||
matched_tokens = min(matched_tokens, len(self), len(other))
|
||||
if page_size == 1:
|
||||
return matched_tokens
|
||||
return (matched_tokens // page_size) * page_size
|
||||
|
||||
@@ -101,7 +101,7 @@ class RadixCacheCpp(BasePrefixCache):
|
||||
def match_prefix(self, params: MatchPrefixParams) -> MatchResult:
|
||||
key = params.key
|
||||
device_indices_vec, host_indices_length, node_gpu, node_cpu = (
|
||||
self.tree.match_prefix(key.token_ids)
|
||||
self.tree.match_prefix(key.raw_token_ids())
|
||||
)
|
||||
return MatchResult(
|
||||
device_indices=self._merge_tensor(device_indices_vec),
|
||||
|
||||
@@ -46,7 +46,8 @@ class _LMCacheLoadBackMarker:
|
||||
``match_prefix`` call in MP mode.
|
||||
"""
|
||||
|
||||
key: RadixKey # page-aligned key the scheduler matched on
|
||||
key: RadixKey # detached snapshot of the matched key (the live query key
|
||||
# aliases the req's growing fill_ids and must not be retained)
|
||||
value_numel: int # number of tokens already in radix at match time
|
||||
|
||||
|
||||
@@ -216,14 +217,17 @@ class LMCRadixCache(RadixCache):
|
||||
LMCache has tokens beyond radix. Otherwise releases
|
||||
the held read locks and returns the radix-only result.
|
||||
"""
|
||||
matched = self.lmcache_connector.lookup_kv(key.token_ids, req.rid)
|
||||
token_ids = key.raw_token_ids()
|
||||
matched = self.lmcache_connector.lookup_kv(token_ids, req.rid)
|
||||
if matched <= value.numel():
|
||||
# Release the read locks; keep the pending session for end_session.
|
||||
self.lmcache_connector.release_pending(req.rid)
|
||||
return base_res
|
||||
|
||||
if token_ids is key.token_ids:
|
||||
token_ids = token_ids[:]
|
||||
self._mp_load_back_markers[req.rid] = _LMCacheLoadBackMarker(
|
||||
key=key,
|
||||
key=RadixKey(token_ids, key.extra_key, key.is_bigram),
|
||||
value_numel=int(value.numel()),
|
||||
)
|
||||
return MatchResult(
|
||||
@@ -254,13 +258,14 @@ class LMCRadixCache(RadixCache):
|
||||
if uncached_len == 0:
|
||||
return base_res
|
||||
|
||||
token_ids = key.raw_token_ids()
|
||||
result = self._load_back(
|
||||
key=key,
|
||||
value_numel=int(value.numel()),
|
||||
uncached_len=uncached_len,
|
||||
last_node=last_node,
|
||||
load_fn=lambda sm, pp: self._ip_load_back(
|
||||
token_ids=key.token_ids,
|
||||
token_ids=token_ids,
|
||||
value_numel=int(value.numel()),
|
||||
slot_mapping=sm,
|
||||
prefix_pad=pp,
|
||||
|
||||
@@ -15,6 +15,7 @@ from __future__ import annotations
|
||||
import logging
|
||||
import time
|
||||
import uuid
|
||||
from array import array
|
||||
from typing import TYPE_CHECKING, Dict, Optional
|
||||
|
||||
from sglang.srt.managers.io_struct import (
|
||||
@@ -94,12 +95,111 @@ class Session:
|
||||
self.req_nodes: Dict[str, SessionReqNode] = {}
|
||||
self.close_on_finish: bool = False
|
||||
self._inflight: bool = False
|
||||
# Token-array lengths of last_req as of its finish_req. The share path
|
||||
# appends speculatively beyond these; only finish_req confirms them, so
|
||||
# _share_token_arrays trims back first (heals aborted turns).
|
||||
self.committed_origin_len: Optional[int] = None
|
||||
self.committed_unpadded_len: Optional[int] = None
|
||||
self.committed_fill_len: Optional[int] = None
|
||||
|
||||
def is_timed_out(self) -> bool:
|
||||
if self.timeout is None:
|
||||
return False
|
||||
return time.monotonic() - self.last_active_time > self.timeout
|
||||
|
||||
@staticmethod
|
||||
def _strip_bos_token(req: TokenizedGenerateReqInput, tokenizer) -> None:
|
||||
"""Trim a leading BOS on an appended turn; shift mm offsets to match."""
|
||||
if not (
|
||||
tokenizer is not None
|
||||
and req.input_ids
|
||||
and req.input_ids[0] == tokenizer.bos_token_id
|
||||
):
|
||||
return
|
||||
req.input_ids = req.input_ids[1:]
|
||||
if req.mm_inputs:
|
||||
for item in req.mm_inputs.mm_items:
|
||||
if item.offsets:
|
||||
if any(s == 0 for s, _ in item.offsets):
|
||||
logging.warning(
|
||||
"mm_item offset starts at 0 (BOS position), "
|
||||
"clamping to 0 after BOS strip"
|
||||
)
|
||||
item.offsets = [
|
||||
(max(0, s - 1), max(0, e - 1)) for s, e in item.offsets
|
||||
]
|
||||
|
||||
def _share_token_arrays(self, last_req: Req, new_input_ids):
|
||||
"""Plain streaming append: reuse last_req's token arrays in place.
|
||||
|
||||
Trims each array back to its committed length first — an earlier turn
|
||||
may have appended its tokens and then aborted before finish_req, and
|
||||
req_nodes still points at last_req, so anything beyond the committed
|
||||
lengths is unconfirmed. Then extends with last turn's output and the
|
||||
new input. Returns (input_ids, input_ids_unpadded, carry_fill);
|
||||
carry_fill (== the new origin) spares the first fill_ids rebuild.
|
||||
"""
|
||||
out_tail = last_req.output_ids[: last_req.sampling_params.max_new_tokens]
|
||||
|
||||
input_ids = last_req.origin_input_ids
|
||||
del input_ids[self.committed_origin_len :]
|
||||
if last_req.origin_input_ids_unpadded is input_ids:
|
||||
input_ids_unpadded = input_ids
|
||||
else:
|
||||
input_ids_unpadded = last_req.origin_input_ids_unpadded
|
||||
del input_ids_unpadded[self.committed_unpadded_len :]
|
||||
|
||||
carry_fill = last_req.full_untruncated_fill_ids
|
||||
if (
|
||||
not isinstance(carry_fill, array)
|
||||
or carry_fill is input_ids
|
||||
or carry_fill is input_ids_unpadded
|
||||
):
|
||||
# Unexpected type or aliased with an origin array (extending it
|
||||
# below would double-append): let _refresh_fill_ids rebuild.
|
||||
carry_fill = None
|
||||
else:
|
||||
del carry_fill[self.committed_fill_len :]
|
||||
baked = len(carry_fill) - len(input_ids)
|
||||
if 0 <= baked <= len(out_tail):
|
||||
carry_fill.extend(out_tail[baked:])
|
||||
carry_fill.extend(new_input_ids)
|
||||
else:
|
||||
carry_fill = None
|
||||
|
||||
input_ids.extend(out_tail)
|
||||
input_ids.extend(new_input_ids)
|
||||
if input_ids_unpadded is not input_ids:
|
||||
input_ids_unpadded.extend(out_tail)
|
||||
input_ids_unpadded.extend(new_input_ids)
|
||||
return input_ids, input_ids_unpadded, carry_fill
|
||||
|
||||
@staticmethod
|
||||
def _concat_token_arrays(
|
||||
last_req: Req, req: TokenizedGenerateReqInput, session_params
|
||||
):
|
||||
"""Copy-based assembly for replace/offset/drop_previous_output turns."""
|
||||
out_tail = last_req.output_ids[: last_req.sampling_params.max_new_tokens]
|
||||
|
||||
input_ids = last_req.origin_input_ids + out_tail
|
||||
if session_params.drop_previous_output:
|
||||
input_ids = last_req.origin_input_ids[:]
|
||||
if session_params.offset and session_params.offset != 0:
|
||||
input_ids = input_ids[: session_params.offset] + req.input_ids
|
||||
else:
|
||||
input_ids += req.input_ids
|
||||
|
||||
input_ids_unpadded = last_req.origin_input_ids_unpadded + out_tail
|
||||
if session_params.drop_previous_output:
|
||||
input_ids_unpadded = last_req.origin_input_ids_unpadded[:]
|
||||
if session_params.offset and session_params.offset != 0:
|
||||
input_ids_unpadded = (
|
||||
input_ids_unpadded[: session_params.offset] + req.input_ids
|
||||
)
|
||||
else:
|
||||
input_ids_unpadded += req.input_ids
|
||||
return input_ids, input_ids_unpadded
|
||||
|
||||
def create_req(
|
||||
self,
|
||||
req: TokenizedGenerateReqInput,
|
||||
@@ -163,54 +263,28 @@ class Session:
|
||||
abort_message = "Session request is appending to a request that hasn't finished."
|
||||
logging.warning(abort_message)
|
||||
|
||||
carry_fill = None
|
||||
if last_req is not None:
|
||||
# trim bos token if it is an append
|
||||
if (
|
||||
tokenizer is not None
|
||||
and req.input_ids
|
||||
and req.input_ids[0] == tokenizer.bos_token_id
|
||||
):
|
||||
req.input_ids = req.input_ids[1:]
|
||||
# Adjust mm_item offsets since they were computed on
|
||||
# the pre-strip sequence (with BOS at position 0)
|
||||
if req.mm_inputs:
|
||||
for item in req.mm_inputs.mm_items:
|
||||
if item.offsets:
|
||||
if any(s == 0 for s, _ in item.offsets):
|
||||
logging.warning(
|
||||
"mm_item offset starts at 0 (BOS position), "
|
||||
"clamping to 0 after BOS strip"
|
||||
)
|
||||
item.offsets = [
|
||||
(max(0, s - 1), max(0, e - 1)) for s, e in item.offsets
|
||||
]
|
||||
|
||||
input_ids = (
|
||||
last_req.origin_input_ids
|
||||
+ last_req.output_ids[: last_req.sampling_params.max_new_tokens]
|
||||
self._strip_bos_token(req, tokenizer)
|
||||
# In-place sharing is only safe for the plain streaming append:
|
||||
# streaming sessions allow a single inflight request, last_req has
|
||||
# finished, and the committed_* lengths recorded by finish_req let
|
||||
# _share_token_arrays trim away tokens appended by an aborted turn.
|
||||
# offset / drop_previous_output rewrite history and must copy.
|
||||
can_share_token_arrays = (
|
||||
self.streaming
|
||||
and self.committed_origin_len is not None
|
||||
and not session_params.drop_previous_output
|
||||
and not (session_params.offset and session_params.offset != 0)
|
||||
)
|
||||
|
||||
if session_params.drop_previous_output:
|
||||
input_ids = last_req.origin_input_ids[:]
|
||||
|
||||
if session_params.offset and session_params.offset != 0:
|
||||
input_ids = input_ids[: session_params.offset] + req.input_ids
|
||||
else:
|
||||
input_ids += req.input_ids
|
||||
|
||||
input_ids_unpadded = (
|
||||
last_req.origin_input_ids_unpadded
|
||||
+ last_req.output_ids[: last_req.sampling_params.max_new_tokens]
|
||||
)
|
||||
if session_params.drop_previous_output:
|
||||
input_ids_unpadded = last_req.origin_input_ids_unpadded[:]
|
||||
|
||||
if session_params.offset and session_params.offset != 0:
|
||||
input_ids_unpadded = (
|
||||
input_ids_unpadded[: session_params.offset] + req.input_ids
|
||||
if can_share_token_arrays:
|
||||
input_ids, input_ids_unpadded, carry_fill = self._share_token_arrays(
|
||||
last_req, req.input_ids
|
||||
)
|
||||
else:
|
||||
input_ids_unpadded += req.input_ids
|
||||
input_ids, input_ids_unpadded = self._concat_token_arrays(
|
||||
last_req, req, session_params
|
||||
)
|
||||
else:
|
||||
input_ids = req.input_ids
|
||||
input_ids_unpadded = req.input_ids
|
||||
@@ -243,6 +317,8 @@ class Session:
|
||||
if last_req is not None:
|
||||
new_req.multimodal_inputs = last_req.multimodal_inputs
|
||||
new_req.tokenizer = tokenizer
|
||||
if carry_fill is not None:
|
||||
new_req.full_untruncated_fill_ids = carry_fill
|
||||
|
||||
if abort:
|
||||
new_req.set_finish_with_abort(abort_message)
|
||||
@@ -263,6 +339,10 @@ class Session:
|
||||
prev_node.req.session = None
|
||||
self.req_nodes.clear()
|
||||
self.req_nodes[req.rid] = SessionReqNode(req)
|
||||
# Confirm this req's token arrays as the session's rollback point.
|
||||
self.committed_origin_len = len(req.origin_input_ids)
|
||||
self.committed_unpadded_len = len(req.origin_input_ids_unpadded)
|
||||
self.committed_fill_len = len(req.full_untruncated_fill_ids)
|
||||
|
||||
def abort_req(self):
|
||||
"""Clear inflight flag on abort (req_nodes stays unchanged)."""
|
||||
|
||||
@@ -247,7 +247,7 @@ class StreamingSession(BasePrefixCache):
|
||||
# token_ids = get_fill_ids()[:input_len-1] (1-token logit reserve
|
||||
# already applied). min handles retract retry where committed_len
|
||||
# can exceed len(token_ids) by 1.
|
||||
prefix_len = min(req.kv_committed_len, len(params.key.token_ids))
|
||||
prefix_len = min(req.kv_committed_len, len(params.key))
|
||||
|
||||
# Streaming sessions are append-only (session_controller rollback
|
||||
# ensures req_nodes always points to the last successful req).
|
||||
|
||||
Reference in New Issue
Block a user