Co-authored-by: zhouhaizhu.zhz <zhouhaizhu.zhz@alibaba-inc.com> Co-authored-by: LinSiyuan814 <linsiyuan.lsy@alibaba-inc.com> Co-authored-by: hzh0425 <hzh0425@apache.org>
164 lines
5.1 KiB
Python
164 lines
5.1 KiB
Python
import json
|
|
import os
|
|
import signal
|
|
import socket
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
import requests
|
|
|
|
ASSETS = Path(__file__).parent / "assets"
|
|
SGLANG_ROOT = Path(__file__).parents[3]
|
|
BENCH_SERVING = SGLANG_ROOT / "benchmark" / "simulator" / "bench_serving.py"
|
|
EXAMPLES = Path(__file__).parent.parent / "examples"
|
|
SIM_CONFIGS = {
|
|
"aic_sol": EXAMPLES / "sim_configs" / "aic_sol.json",
|
|
"aic_silicon": EXAMPLES / "sim_configs" / "aic_silicon.json",
|
|
"ml": EXAMPLES / "sim_configs" / "ml.json",
|
|
"replay": EXAMPLES / "sim_configs" / "replay.json",
|
|
}
|
|
|
|
|
|
class SGLangServingRunner:
|
|
def __init__(self, config_path: Path, tmp_path: Path, mode: str = "offline"):
|
|
self.mode = mode
|
|
with socket.socket() as sock:
|
|
sock.bind(("127.0.0.1", 0))
|
|
self.port = sock.getsockname()[1]
|
|
|
|
self.output_dir = tmp_path / "output"
|
|
env = os.environ.copy()
|
|
env.update(
|
|
CUDA_VISIBLE_DEVICES="",
|
|
SGLANG_USE_CPU_ENGINE="1",
|
|
SGLANG_SIMULATOR_CONFIG_PATH=str(config_path),
|
|
SGLANG_SIMULATOR_OUTPUT_MODE=mode.upper(),
|
|
SGLANG_SIMULATOR_OUTPUT_DIR=str(self.output_dir),
|
|
)
|
|
cmd = [
|
|
sys.executable,
|
|
"-m",
|
|
"sglang_simulator.simulation.sglang.launch_server",
|
|
"--model-path",
|
|
str(ASSETS / "qwen3-8b"),
|
|
"--sim-config-path",
|
|
str(config_path),
|
|
"--port",
|
|
str(self.port),
|
|
"--tokenizer-path",
|
|
str(EXAMPLES / "assets" / "tokenizer"),
|
|
"--max-total-tokens",
|
|
"8192",
|
|
"--max-running-requests",
|
|
"8",
|
|
"--disable-overlap-schedule",
|
|
]
|
|
self.server_proc = subprocess.Popen(cmd, env=env, preexec_fn=os.setsid)
|
|
for _ in range(120):
|
|
if self.server_proc.poll() is not None:
|
|
raise RuntimeError("SGLang Simulator server exited during startup")
|
|
try:
|
|
if requests.get(self.base_url, timeout=1).status_code < 500:
|
|
return
|
|
except requests.RequestException:
|
|
pass
|
|
time.sleep(1)
|
|
self.shutdown()
|
|
raise RuntimeError("SGLang Simulator server did not become ready")
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
return f"http://127.0.0.1:{self.port}"
|
|
|
|
def benchmark(
|
|
self,
|
|
output_file: Path,
|
|
workload: str = "sharegpt",
|
|
request_rate=None,
|
|
seed=42,
|
|
) -> dict:
|
|
cmd = [
|
|
sys.executable,
|
|
str(BENCH_SERVING),
|
|
f"--simulator-mode={self.mode}",
|
|
"--backend=sglang",
|
|
f"--base-url={self.base_url}",
|
|
f"--model={ASSETS / 'qwen3-8b'}",
|
|
f"--tokenizer={EXAMPLES / 'assets' / 'tokenizer'}",
|
|
"--num-prompts=3",
|
|
"--disable-tqdm",
|
|
"--profile",
|
|
f"--output-file={output_file}",
|
|
]
|
|
if request_rate is not None:
|
|
cmd.extend([f"--request-rate={request_rate}", f"--seed={seed}"])
|
|
|
|
if workload == "sharegpt":
|
|
cmd.extend(
|
|
[
|
|
"--dataset-name=sharegpt",
|
|
f"--dataset-path={EXAMPLES / 'workloads' / 'sharegpt-example.json'}",
|
|
"--sharegpt-output-len=4",
|
|
]
|
|
)
|
|
else:
|
|
assert workload == "timestamp_trace"
|
|
cmd.extend(
|
|
[
|
|
"--dataset-name=autobench",
|
|
f"--dataset-path={EXAMPLES / 'workloads' / 'timestamp-trace-example.jsonl'}",
|
|
"--use-trace-timestamps",
|
|
]
|
|
)
|
|
|
|
subprocess.run(cmd, check=True)
|
|
assert output_file.is_file()
|
|
return json.loads(
|
|
(self.output_dir / "metrics.json").read_text(encoding="utf-8")
|
|
)
|
|
|
|
def shutdown(self):
|
|
if self.server_proc.poll() is not None:
|
|
return
|
|
os.killpg(self.server_proc.pid, signal.SIGTERM)
|
|
try:
|
|
self.server_proc.wait(timeout=10)
|
|
except subprocess.TimeoutExpired:
|
|
os.killpg(self.server_proc.pid, signal.SIGKILL)
|
|
self.server_proc.wait()
|
|
|
|
|
|
def assert_decode_metrics(metrics):
|
|
assert metrics["completed"] == 3
|
|
assert metrics["total_output"] == 12
|
|
assert metrics["mean_ttft_ms"] >= 0
|
|
assert metrics["mean_tpot_ms"] > 0
|
|
assert metrics["mean_itl_ms"] > 0
|
|
assert metrics["input_throughput"] > 0
|
|
|
|
|
|
@pytest.mark.parametrize("config_name", SIM_CONFIGS)
|
|
def test_benchmark(config_name, tmp_path):
|
|
runner = SGLangServingRunner(SIM_CONFIGS[config_name], tmp_path)
|
|
try:
|
|
metrics = runner.benchmark(tmp_path / "benchmark.json")
|
|
finally:
|
|
runner.shutdown()
|
|
|
|
assert_decode_metrics(metrics)
|
|
|
|
|
|
def test_timestamp_trace(tmp_path):
|
|
runner = SGLangServingRunner(SIM_CONFIGS["replay"], tmp_path)
|
|
try:
|
|
metrics = runner.benchmark(
|
|
tmp_path / "benchmark.json", workload="timestamp_trace"
|
|
)
|
|
finally:
|
|
runner.shutdown()
|
|
|
|
assert_decode_metrics(metrics)
|