94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
436 lines
16 KiB
Python
436 lines
16 KiB
Python
"""Streaming-session test fixture.
|
|
|
|
`TestStreamingSession` is the base class for all streaming-session tests
|
|
(default config — Llama-3.1-8B, no spec). Variants in
|
|
test_streaming_session.py and test_streaming_session_extra.py inherit
|
|
it and only override `setUpClass`.
|
|
|
|
Also exports:
|
|
- ABORT_REPRO_* constants used by the basic file's abort-leak repro.
|
|
- _abort_repro_run_all coroutine reused by the basic file.
|
|
|
|
Lives under sglang.test.server_fixtures so siblings under test/registered
|
|
can `import` it without sys.path hacks.
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
from typing import Any, Optional
|
|
|
|
import aiohttp
|
|
import requests
|
|
|
|
from sglang.srt.environ import envs
|
|
from sglang.srt.utils import kill_process_tree
|
|
from sglang.srt.utils.hf_transformers_utils import get_tokenizer
|
|
from sglang.test.test_utils import (
|
|
DEFAULT_SMALL_MODEL_NAME_FOR_TEST,
|
|
DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
|
DEFAULT_URL_FOR_TEST,
|
|
CustomTestCase,
|
|
popen_launch_server,
|
|
)
|
|
|
|
LOGPROB_PROMPTS = [
|
|
"The quick brown fox jumps over the lazy dog.",
|
|
"Pack my box with five dozen liquor jugs.",
|
|
"How vexingly quick daft zebras jump.",
|
|
"Sphinx of black quartz judge my vow.",
|
|
"The five boxing wizards jump quickly.",
|
|
]
|
|
|
|
# Long enough to trigger chunked prefill at 200+ tokens per slice.
|
|
LEAK_FILLER = (
|
|
"The quick brown fox jumps over the lazy dog. "
|
|
"Pack my box with five dozen liquor jugs. "
|
|
"How vexingly quick daft zebras jump. "
|
|
"Sphinx of black quartz, judge my vow. "
|
|
"The five boxing wizards jump quickly. "
|
|
"Jackdaws love my big sphinx of quartz. "
|
|
"A wizard's job is to vex chumps quickly in fog. "
|
|
"We promptly judged antique ivory buckles for the next prize. "
|
|
) * 20
|
|
|
|
ABORT_REPRO_CONTEXT_LEN = 512
|
|
ABORT_REPRO_PAGE_SIZE = 256
|
|
ABORT_REPRO_GEN_LEN = 4
|
|
ABORT_REPRO_SESSIONS = 4
|
|
ABORT_REPRO_WARMUP_TURNS = 1
|
|
ABORT_REPRO_ROUNDS = 8
|
|
ABORT_REPRO_STREAM_TOKENS = 16
|
|
ABORT_REPRO_ABORT_TOKENS = 600
|
|
ABORT_REPRO_NON_STREAMING_TOKENS = 16
|
|
ABORT_REPRO_CHUNKED_PREFILL_SIZE = 4096
|
|
|
|
CONCURRENT_LOGPROB_SESSIONS = 6
|
|
CONCURRENT_LOGPROB_TURNS = 5
|
|
CONCURRENT_LOGPROB_ROUNDS = 10
|
|
|
|
STRESS_NUM_SESSIONS = 8
|
|
STRESS_NUM_NON_STREAMING = 4
|
|
STRESS_NUM_TURNS = 6
|
|
STRESS_GEN_LEN = 16
|
|
|
|
|
|
def _make_token_sized_ids(
|
|
tokenizer: Any, prefix: str, min_tokens: int, max_tokens: Optional[int] = None
|
|
) -> list[int]:
|
|
text = prefix
|
|
chunk = " pack quartz wizard sphinx zebra fox " * 16
|
|
token_ids = tokenizer.encode(text)
|
|
while len(token_ids) < min_tokens:
|
|
text += chunk
|
|
token_ids = tokenizer.encode(text)
|
|
if max_tokens is not None:
|
|
token_ids = token_ids[:max_tokens]
|
|
return token_ids
|
|
|
|
|
|
async def _abort_repro_generate(
|
|
base_url: str,
|
|
session: aiohttp.ClientSession,
|
|
input_ids: list[int],
|
|
max_new_tokens: int,
|
|
session_params: Optional[dict[str, Any]] = None,
|
|
expect_abort: bool = False,
|
|
) -> Optional[dict[str, Any]]:
|
|
payload: dict[str, Any] = {
|
|
"input_ids": input_ids,
|
|
"sampling_params": {
|
|
"temperature": 0,
|
|
"max_new_tokens": max_new_tokens,
|
|
"no_stop_trim": True,
|
|
"skip_special_tokens": False,
|
|
},
|
|
}
|
|
if session_params:
|
|
payload["session_params"] = session_params
|
|
|
|
async with session.post(base_url + "/generate", json=payload) as resp:
|
|
text = await resp.text()
|
|
if expect_abort:
|
|
if resp.status == 200:
|
|
data = json.loads(text)
|
|
finish_reason = data.get("meta_info", {}).get("finish_reason", {})
|
|
assert finish_reason.get("type") == "abort", text
|
|
assert "maximum allowed length" in finish_reason.get(
|
|
"message", ""
|
|
) or "context length" in finish_reason.get("message", ""), text
|
|
return data
|
|
assert resp.status == 400, text
|
|
assert "maximum allowed length" in text or "context length" in text, text
|
|
return None
|
|
|
|
assert resp.status == 200, text
|
|
data = json.loads(text)
|
|
finish_reason = data.get("meta_info", {}).get("finish_reason", {})
|
|
assert finish_reason.get("type") != "abort", text
|
|
return data
|
|
|
|
|
|
async def _abort_repro_run_all(base_url: str, tokenizer: Any) -> None:
|
|
timeout = aiohttp.ClientTimeout(total=300)
|
|
async with aiohttp.ClientSession(timeout=timeout) as http:
|
|
session_ids = []
|
|
for _ in range(ABORT_REPRO_SESSIONS):
|
|
async with http.post(
|
|
base_url + "/open_session",
|
|
json={"capacity_of_str_len": 50000, "streaming": True},
|
|
) as resp:
|
|
assert resp.status == 200, await resp.text()
|
|
session_ids.append(await resp.json())
|
|
|
|
try:
|
|
for warmup_turn in range(ABORT_REPRO_WARMUP_TURNS):
|
|
warmup_tasks = []
|
|
for session_idx, session_id in enumerate(session_ids):
|
|
input_ids = _make_token_sized_ids(
|
|
tokenizer,
|
|
prefix=f"[warmup={warmup_turn} session={session_idx}]",
|
|
min_tokens=ABORT_REPRO_STREAM_TOKENS,
|
|
max_tokens=ABORT_REPRO_STREAM_TOKENS + 8,
|
|
)
|
|
warmup_tasks.append(
|
|
_abort_repro_generate(
|
|
base_url,
|
|
http,
|
|
input_ids,
|
|
ABORT_REPRO_GEN_LEN,
|
|
session_params={"id": session_id, "rid": None},
|
|
)
|
|
)
|
|
await asyncio.gather(*warmup_tasks)
|
|
|
|
for round_idx in range(ABORT_REPRO_ROUNDS):
|
|
mixed_tasks = []
|
|
for session_idx, session_id in enumerate(session_ids):
|
|
input_ids = _make_token_sized_ids(
|
|
tokenizer,
|
|
prefix=f"[round={round_idx} ok session={session_idx}]",
|
|
min_tokens=ABORT_REPRO_STREAM_TOKENS,
|
|
max_tokens=ABORT_REPRO_STREAM_TOKENS + 8,
|
|
)
|
|
mixed_tasks.append(
|
|
_abort_repro_generate(
|
|
base_url,
|
|
http,
|
|
input_ids,
|
|
ABORT_REPRO_GEN_LEN,
|
|
session_params={"id": session_id, "rid": None},
|
|
)
|
|
)
|
|
|
|
for ns_idx in range(2):
|
|
input_ids = _make_token_sized_ids(
|
|
tokenizer,
|
|
prefix=f"[round={round_idx} ns={ns_idx}]",
|
|
min_tokens=ABORT_REPRO_NON_STREAMING_TOKENS,
|
|
max_tokens=ABORT_REPRO_NON_STREAMING_TOKENS + 8,
|
|
)
|
|
mixed_tasks.append(
|
|
_abort_repro_generate(
|
|
base_url,
|
|
http,
|
|
input_ids,
|
|
ABORT_REPRO_GEN_LEN,
|
|
)
|
|
)
|
|
await asyncio.gather(*mixed_tasks)
|
|
|
|
abort_tasks = []
|
|
for session_idx, session_id in enumerate(session_ids):
|
|
input_ids = _make_token_sized_ids(
|
|
tokenizer,
|
|
prefix=f"[round={round_idx} abort session={session_idx}]",
|
|
min_tokens=ABORT_REPRO_ABORT_TOKENS,
|
|
)
|
|
abort_tasks.append(
|
|
_abort_repro_generate(
|
|
base_url,
|
|
http,
|
|
input_ids,
|
|
ABORT_REPRO_GEN_LEN,
|
|
session_params={"id": session_id, "rid": None},
|
|
expect_abort=True,
|
|
)
|
|
)
|
|
await asyncio.gather(*abort_tasks)
|
|
|
|
recovery_tasks = []
|
|
for session_idx, session_id in enumerate(session_ids):
|
|
input_ids = _make_token_sized_ids(
|
|
tokenizer,
|
|
prefix=f"[round={round_idx} recover session={session_idx}]",
|
|
min_tokens=ABORT_REPRO_NON_STREAMING_TOKENS,
|
|
max_tokens=ABORT_REPRO_NON_STREAMING_TOKENS + 8,
|
|
)
|
|
recovery_tasks.append(
|
|
_abort_repro_generate(
|
|
base_url,
|
|
http,
|
|
input_ids,
|
|
ABORT_REPRO_GEN_LEN,
|
|
session_params={"id": session_id, "rid": None},
|
|
)
|
|
)
|
|
recovery_results = await asyncio.gather(*recovery_tasks)
|
|
for result in recovery_results:
|
|
assert result is not None
|
|
assert result["meta_info"]["cached_tokens"] > 0, result
|
|
|
|
health = requests.get(base_url + "/health", timeout=10)
|
|
if health.status_code != 200:
|
|
raise RuntimeError(
|
|
f"server unhealthy after round={round_idx}: "
|
|
f"{health.status_code} {health.text}"
|
|
)
|
|
finally:
|
|
for session_id in session_ids:
|
|
async with http.post(
|
|
base_url + "/close_session", json={"session_id": session_id}
|
|
) as resp:
|
|
assert resp.status == 200, await resp.text()
|
|
|
|
|
|
async def _async_generate(
|
|
base_url: str,
|
|
session: aiohttp.ClientSession,
|
|
input_ids: list[int],
|
|
max_new_tokens: int = 8,
|
|
session_params: Optional[dict[str, Any]] = None,
|
|
return_logprob: bool = False,
|
|
logprob_start_len: Optional[int] = None,
|
|
) -> dict[str, Any]:
|
|
payload: dict[str, Any] = {
|
|
"input_ids": input_ids,
|
|
"sampling_params": {
|
|
"temperature": 0,
|
|
"max_new_tokens": max_new_tokens,
|
|
"no_stop_trim": True,
|
|
"skip_special_tokens": False,
|
|
},
|
|
}
|
|
if session_params:
|
|
payload["session_params"] = session_params
|
|
if return_logprob:
|
|
payload["return_logprob"] = True
|
|
if logprob_start_len is not None:
|
|
payload["logprob_start_len"] = logprob_start_len
|
|
timeout = aiohttp.ClientTimeout(total=300)
|
|
async with session.post(
|
|
base_url + "/generate", json=payload, timeout=timeout
|
|
) as resp:
|
|
assert resp.status == 200, f"Generate failed: {await resp.text()}"
|
|
return await resp.json()
|
|
|
|
|
|
async def _concurrent_logprob_run(base_url: str, tokenizer: Any, **gen_kwargs) -> None:
|
|
"""N sessions per round, all requests fired simultaneously per turn so
|
|
the running batch has real concurrency (retract can actually kick one).
|
|
"""
|
|
timeout = aiohttp.ClientTimeout(total=300)
|
|
async with aiohttp.ClientSession(timeout=timeout) as http:
|
|
for _ in range(CONCURRENT_LOGPROB_ROUNDS):
|
|
sids: list[str] = []
|
|
for _ in range(CONCURRENT_LOGPROB_SESSIONS):
|
|
async with http.post(
|
|
base_url + "/open_session",
|
|
json={"capacity_of_str_len": 50000, "streaming": True},
|
|
) as resp:
|
|
assert resp.status == 200
|
|
sids.append(await resp.json())
|
|
|
|
rids: list[Optional[str]] = [None] * CONCURRENT_LOGPROB_SESSIONS
|
|
for turn in range(CONCURRENT_LOGPROB_TURNS):
|
|
tasks = []
|
|
for s in range(CONCURRENT_LOGPROB_SESSIONS):
|
|
text = (
|
|
f"S{s} T{turn}: "
|
|
f"{LOGPROB_PROMPTS[turn % len(LOGPROB_PROMPTS)]}"
|
|
)
|
|
ids = tokenizer.encode(text)
|
|
tasks.append(
|
|
_async_generate(
|
|
base_url,
|
|
http,
|
|
ids,
|
|
session_params={"id": sids[s], "rid": rids[s]},
|
|
**gen_kwargs,
|
|
)
|
|
)
|
|
results = await asyncio.gather(*tasks)
|
|
for s in range(CONCURRENT_LOGPROB_SESSIONS):
|
|
rids[s] = results[s]["meta_info"]["id"]
|
|
|
|
for sid in sids:
|
|
async with http.post(
|
|
base_url + "/close_session", json={"session_id": sid}
|
|
) as resp:
|
|
assert resp.status == 200
|
|
|
|
|
|
async def _stress_run_all(base_url: str, tokenizer: Any) -> None:
|
|
"""Streaming + non-streaming mixed batches under retract pressure.
|
|
Long prompts (~200+ tokens) trigger chunked prefill so retract can
|
|
interrupt mid-extend.
|
|
"""
|
|
timeout = aiohttp.ClientTimeout(total=300)
|
|
async with aiohttp.ClientSession(timeout=timeout) as http:
|
|
sids: list[str] = []
|
|
for _ in range(STRESS_NUM_SESSIONS):
|
|
async with http.post(
|
|
base_url + "/open_session",
|
|
json={"capacity_of_str_len": 50000, "streaming": True},
|
|
) as resp:
|
|
assert resp.status == 200
|
|
sids.append(await resp.json())
|
|
|
|
rids: list[Optional[str]] = [None] * STRESS_NUM_SESSIONS
|
|
for turn in range(STRESS_NUM_TURNS):
|
|
tasks = []
|
|
# Streaming requests — long prompts to trigger chunked prefill.
|
|
for s in range(STRESS_NUM_SESSIONS):
|
|
offset = (s * STRESS_NUM_TURNS + turn) * 200
|
|
text = (
|
|
f"Session {s} turn {turn}: " f"{LEAK_FILLER[offset : offset + 800]}"
|
|
)
|
|
ids = tokenizer.encode(text)
|
|
tasks.append(
|
|
_async_generate(
|
|
base_url,
|
|
http,
|
|
ids,
|
|
max_new_tokens=STRESS_GEN_LEN,
|
|
session_params={"id": sids[s], "rid": rids[s]},
|
|
)
|
|
)
|
|
|
|
# Non-streaming requests interleaved.
|
|
for ns in range(STRESS_NUM_NON_STREAMING):
|
|
text = (
|
|
f"Non-streaming {ns} turn {turn}: "
|
|
f"{LEAK_FILLER[ns * 100 : ns * 100 + 400]}"
|
|
)
|
|
ids = tokenizer.encode(text)
|
|
tasks.append(
|
|
_async_generate(
|
|
base_url,
|
|
http,
|
|
ids,
|
|
max_new_tokens=STRESS_GEN_LEN,
|
|
)
|
|
)
|
|
|
|
results = await asyncio.gather(*tasks)
|
|
for s in range(STRESS_NUM_SESSIONS):
|
|
rids[s] = results[s]["meta_info"]["id"]
|
|
|
|
for sid in sids:
|
|
async with http.post(
|
|
base_url + "/close_session", json={"session_id": sid}
|
|
) as resp:
|
|
assert resp.status == 200
|
|
|
|
|
|
class StreamingSessionServerBase(CustomTestCase):
|
|
"""Minimal streaming-session server fixture.
|
|
|
|
Subclasses override class attrs to customize launch:
|
|
- `model`: defaults to the small model.
|
|
- `extra_args`: appended after `--enable-streaming-session` (set
|
|
`--chunked-prefill-size`, `--page-size`, spec args, etc. here).
|
|
- `env_overrides`: list of `(env_attr_name, value)` tuples; each is
|
|
pushed onto the `setUpClass` context stack so the env override is
|
|
live during `popen_launch_server` and torn down on
|
|
`tearDownClass`-time. `SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY=1`
|
|
is always applied on top of these.
|
|
"""
|
|
|
|
model = DEFAULT_SMALL_MODEL_NAME_FOR_TEST
|
|
base_url = DEFAULT_URL_FOR_TEST
|
|
extra_args: list = []
|
|
env_overrides: list = []
|
|
|
|
@classmethod
|
|
def setUpClass(cls):
|
|
import contextlib
|
|
|
|
with contextlib.ExitStack() as stack:
|
|
stack.enter_context(
|
|
envs.SGLANG_ENABLE_STRICT_MEM_CHECK_DURING_BUSY.override(1)
|
|
)
|
|
stack.enter_context(envs.SGLANG_CHECK_KV_PAGE_INVARIANTS.override(True))
|
|
for name, val in cls.env_overrides:
|
|
stack.enter_context(getattr(envs, name).override(val))
|
|
cls.process = popen_launch_server(
|
|
cls.model,
|
|
cls.base_url,
|
|
timeout=DEFAULT_TIMEOUT_FOR_SERVER_LAUNCH,
|
|
other_args=["--enable-streaming-session"] + list(cls.extra_args),
|
|
)
|
|
cls.tokenizer = get_tokenizer(cls.model)
|
|
|
|
@classmethod
|
|
def tearDownClass(cls):
|
|
kill_process_tree(cls.process.pid)
|