[perf] reduce overhead of fill_ids list reconstruction and decref (#27965)

This commit is contained in:
Qiaolin Yu
2026-06-14 00:41:11 -07:00
committed by GitHub
parent f2d7d67603
commit f293ddf3ce
8 changed files with 363 additions and 66 deletions
@@ -0,0 +1,163 @@
"""Unit tests for the streaming-session in-place token-array share protocol
(`Session.create_req` / `finish_req` / `abort_req`):
- token arrays are extended in place and shared across turns (no per-turn copy);
- committed_* lengths recorded at finish_req trim away tokens appended by a
turn that aborted before finishing (mid-turn and first-turn aborts);
- max_new_tokens overshoot falls back to a fill_ids rebuild instead of
carrying an inconsistent array.
"""
from sglang.test.ci.ci_register import register_cpu_ci
register_cpu_ci(est_time=15, suite="base-a-test-cpu")
import unittest
from array import array
from types import SimpleNamespace
from sglang.srt.sampling.sampling_params import SamplingParams
from sglang.srt.session.session_controller import Session
from sglang.test.test_utils import CustomTestCase
VOCAB = 1 << 20
def _recv(rid, input_ids, max_new_tokens=8):
return SimpleNamespace(
rid=rid,
input_ids=array("q", input_ids),
mm_inputs=None,
session_params=SimpleNamespace(
id="s", rid=None, offset=None, replace=False, drop_previous_output=False
),
sampling_params=SamplingParams(max_new_tokens=max_new_tokens),
lora_id=None,
custom_logit_processor=None,
stream=False,
return_logprob=False,
top_logprobs_num=0,
token_ids_logprob=None,
require_reasoning=False,
return_hidden_states=False,
return_routed_experts=False,
routed_experts_start_len=0,
priority=None,
routing_key=None,
extra_key=None,
http_worker_ipc=None,
time_stats=None,
)
class TestSessionTokenShare(CustomTestCase):
def setUp(self):
self.session = Session(capacity_of_str_len=0, session_id="s", streaming=True)
def _create(self, rid, input_ids, max_new_tokens=8):
return self.session.create_req(
_recv(rid, input_ids, max_new_tokens=max_new_tokens),
tokenizer=None,
vocab_size=VOCAB,
)
def _decode_and_finish(self, req, output, baked=None):
"""Simulate decode then a successful finish.
`baked` output tokens are folded into the fill array before the rest
arrive (mix_with_running refreshes mid-decode, so the bake is often
partial).
"""
if baked is None:
baked = len(output)
req.output_ids.extend(output[:baked])
req._refresh_fill_ids()
req.output_ids.extend(output[baked:])
self.session.finish_req(req)
def test_normal_multi_turn_share_and_carry(self):
in1, out1 = list(range(100, 110)), [1, 2, 3]
r1 = self._create("r1", in1)
self.assertEqual(list(r1.origin_input_ids), in1)
self._decode_and_finish(r1, out1, baked=2) # partial bake
self.assertEqual(self.session.committed_origin_len, len(in1))
self.assertEqual(self.session.committed_fill_len, len(in1) + 2)
in2, out2 = [7, 8], [4, 5]
r2 = self._create("r2", in2)
# In-place share: same objects, extended to the new prompt.
self.assertIs(r2.origin_input_ids, r1.origin_input_ids)
self.assertEqual(list(r2.origin_input_ids), in1 + out1 + in2)
# Carry: the fill array handed over and equal to the new origin.
self.assertIs(r2.full_untruncated_fill_ids, r1.full_untruncated_fill_ids)
self.assertEqual(list(r2.full_untruncated_fill_ids), list(r2.origin_input_ids))
self._decode_and_finish(r2, out2)
r3 = self._create("r3", [9])
self.assertEqual(list(r3.origin_input_ids), in1 + out1 + in2 + out2 + [9])
self.assertEqual(list(r3.full_untruncated_fill_ids), list(r3.origin_input_ids))
def test_mid_turn_abort_then_continue(self):
in1, out1 = list(range(200, 210)), [1, 2, 3]
r1 = self._create("r1", in1)
self._decode_and_finish(r1, out1)
# Turn 2 extends the shared arrays, decodes a bit, then aborts:
# finish_req never runs, req_nodes still points at r1.
r2 = self._create("r2", [50, 51])
self.assertEqual(list(r2.origin_input_ids), in1 + out1 + [50, 51])
r2.output_ids.extend([6, 7])
r2._refresh_fill_ids()
self.session.abort_req()
self.assertEqual(self.session.committed_origin_len, len(in1))
# Turn 3 must see exactly r1's history — no [50, 51], no doubled out1.
r3 = self._create("r3", [60])
self.assertEqual(list(r3.origin_input_ids), in1 + out1 + [60])
self.assertEqual(list(r3.full_untruncated_fill_ids), list(r3.origin_input_ids))
# Two aborted attempts in a row heal idempotently.
self.session.abort_req()
r4 = self._create("r4", [70])
self.assertEqual(list(r4.origin_input_ids), in1 + out1 + [70])
self.assertEqual(list(r4.full_untruncated_fill_ids), list(r4.origin_input_ids))
def test_first_turn_abort(self):
self._create("r1", [1, 2, 3])
self.assertTrue(self.session._inflight)
self.session.abort_req()
self.assertFalse(self.session._inflight)
# No finish_req ran: nothing committed, next turn starts from scratch.
self.assertIsNone(self.session.committed_origin_len)
r2 = self._create("r2", [4, 5])
self.assertEqual(list(r2.origin_input_ids), [4, 5])
self._decode_and_finish(r2, [9])
r3 = self._create("r3", [6])
self.assertEqual(list(r3.origin_input_ids), [4, 5, 9, 6])
def test_max_new_tokens_overshoot_falls_back(self):
in1 = list(range(300, 310))
r1 = self._create("r1", in1, max_new_tokens=4)
# Spec-decode overshoot: 6 tokens decoded and baked into the fill
# array, then output trimmed to finished_len (like _trim_overshoot)
# before finish.
r1.output_ids.extend([1, 2, 3, 4, 5, 6])
r1._refresh_fill_ids()
del r1.output_ids[4:]
self.session.finish_req(r1)
self.assertEqual(
self.session.committed_fill_len, len(in1) + 6
) # fill kept the overshoot
# Next turn: out_tail is output[:max_new]; the carried fill has more
# baked than out_tail, so the carry is dropped and the fill rebuilds.
r2 = self._create("r2", [50])
self.assertEqual(list(r2.origin_input_ids), in1 + [1, 2, 3, 4] + [50])
self.assertEqual(len(r2.full_untruncated_fill_ids), 0) # carry skipped
r2._refresh_fill_ids()
self.assertEqual(list(r2.full_untruncated_fill_ids), list(r2.origin_input_ids))
if __name__ == "__main__":
unittest.main()