[Simulator] Add high-fidelity CPU-based inference simulator (#33824)
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>
This commit is contained in:
co-authored by
zhouhaizhu.zhz
LinSiyuan814
hzh0425
parent
a5f07b1241
commit
59799a3687
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"architectures": ["Qwen3ForCausalLM"],
|
||||
"attention_bias": false,
|
||||
"attention_dropout": 0.0,
|
||||
"bos_token_id": 151643,
|
||||
"eos_token_id": 151645,
|
||||
"head_dim": 128,
|
||||
"hidden_act": "silu",
|
||||
"hidden_size": 4096,
|
||||
"initializer_range": 0.02,
|
||||
"intermediate_size": 12288,
|
||||
"max_position_embeddings": 40960,
|
||||
"max_window_layers": 36,
|
||||
"model_type": "qwen3",
|
||||
"num_attention_heads": 32,
|
||||
"num_hidden_layers": 36,
|
||||
"num_key_value_heads": 8,
|
||||
"rms_norm_eps": 1e-06,
|
||||
"rope_scaling": null,
|
||||
"rope_theta": 1000000,
|
||||
"sliding_window": null,
|
||||
"tie_word_embeddings": false,
|
||||
"torch_dtype": "bfloat16",
|
||||
"transformers_version": "5.12.1",
|
||||
"use_cache": true,
|
||||
"use_sliding_window": false,
|
||||
"vocab_size": 151936
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from sglang_simulator.simulation.benchmark import BenchmarkConfig
|
||||
from test_simulation_sglang_runner import make_fixed_dataset, make_sglang_runner
|
||||
from test_simulation_sglang_serving import (
|
||||
SIM_CONFIGS,
|
||||
SGLangServingRunner,
|
||||
assert_decode_metrics,
|
||||
)
|
||||
|
||||
|
||||
def test_in_process_runner_reports_each_cache_tier(tmp_path):
|
||||
runner = make_sglang_runner(tmp_path)
|
||||
benchmark_config = BenchmarkConfig(request_rate=10, ignore_request_timestamp=False)
|
||||
cached_ds = make_fixed_dataset(1000, 8)
|
||||
evict_l1_ds = make_fixed_dataset(2000, 10)
|
||||
evict_l2_ds = make_fixed_dataset(3000, 20)
|
||||
|
||||
try:
|
||||
metrics = runner.benchmark(benchmark_config, dataset=cached_ds)
|
||||
assert metrics["completed"] == len(cached_ds)
|
||||
assert metrics["prefix_cache_reused_ratio"] == 0
|
||||
|
||||
metrics = runner.benchmark(benchmark_config, dataset=cached_ds)
|
||||
assert metrics["kv_cache_device_hit_ratio"] > 0.95
|
||||
|
||||
runner.benchmark(benchmark_config, dataset=evict_l1_ds)
|
||||
metrics = runner.benchmark(benchmark_config, dataset=cached_ds)
|
||||
assert metrics["kv_cache_host_hit_ratio"] > 0.95
|
||||
|
||||
runner.benchmark(benchmark_config, dataset=evict_l2_ds)
|
||||
metrics = runner.benchmark(benchmark_config, dataset=cached_ds)
|
||||
assert metrics["kv_cache_storage_hit_ratio"] > 0.95
|
||||
finally:
|
||||
runner.shutdown()
|
||||
|
||||
|
||||
def test_second_replay_benchmark_hits_all_reusable_prefix_tokens(tmp_path, monkeypatch):
|
||||
# This test validates cache reuse across consecutive benchmark runs.
|
||||
monkeypatch.setenv("SGLANG_IS_IN_CI", "false")
|
||||
|
||||
runner = SGLangServingRunner(SIM_CONFIGS["replay"], tmp_path)
|
||||
try:
|
||||
first_metrics = runner.benchmark(tmp_path / "benchmark-first.json")
|
||||
second_metrics = runner.benchmark(tmp_path / "benchmark-second.json")
|
||||
finally:
|
||||
runner.shutdown()
|
||||
|
||||
assert_decode_metrics(first_metrics)
|
||||
assert_decode_metrics(second_metrics)
|
||||
|
||||
assert second_metrics["total_input"] == 24
|
||||
assert second_metrics["total_new_input"] == 3
|
||||
assert second_metrics["prefix_cache_reused_ratio"] == pytest.approx(0.875)
|
||||
assert second_metrics["kv_cache_device_hit_ratio"] == pytest.approx(0.875)
|
||||
assert second_metrics["kv_cache_host_hit_ratio"] == 0
|
||||
assert second_metrics["kv_cache_storage_hit_ratio"] == 0
|
||||
|
||||
requests = [
|
||||
json.loads(line)
|
||||
for line in (runner.output_dir / "request.jsonl")
|
||||
.read_text(encoding="utf-8")
|
||||
.splitlines()
|
||||
]
|
||||
assert len(requests) == 3
|
||||
assert all(request["input_length"] == 8 for request in requests)
|
||||
assert all(request["final_device_hit_len"] == 7 for request in requests)
|
||||
@@ -0,0 +1,80 @@
|
||||
import json
|
||||
|
||||
import pytest
|
||||
from test_simulation_sglang_serving import (
|
||||
SIM_CONFIGS,
|
||||
SGLangServingRunner,
|
||||
assert_decode_metrics,
|
||||
)
|
||||
|
||||
REQUEST_RATE = 1
|
||||
SEED = 123
|
||||
RELATIVE_TOLERANCES = {
|
||||
"duration": 0.01,
|
||||
"request_throughput": 0.01,
|
||||
"input_throughput": 0.01,
|
||||
"output_throughput": 0.01,
|
||||
"mean_e2e_latency_ms": 0.10,
|
||||
"mean_ttft_ms": 0.10,
|
||||
"mean_tpot_ms": 0.10,
|
||||
"mean_itl_ms": 0.10,
|
||||
}
|
||||
|
||||
|
||||
def _relative_error(actual, expected):
|
||||
return abs(actual - expected) / abs(expected)
|
||||
|
||||
|
||||
def _run_mode(mode, tmp_path):
|
||||
case_dir = tmp_path / mode
|
||||
case_dir.mkdir()
|
||||
runner = SGLangServingRunner(SIM_CONFIGS["aic_sol"], case_dir, mode=mode)
|
||||
try:
|
||||
metrics = runner.benchmark(
|
||||
case_dir / "benchmark.json", request_rate=REQUEST_RATE, seed=SEED
|
||||
)
|
||||
finally:
|
||||
runner.shutdown()
|
||||
|
||||
requests = [
|
||||
json.loads(line)
|
||||
for line in (runner.output_dir / "request.jsonl")
|
||||
.read_text(encoding="utf-8")
|
||||
.splitlines()
|
||||
]
|
||||
requests.sort(key=lambda request: request["created_time"])
|
||||
return metrics, requests
|
||||
|
||||
|
||||
def test_request_rate_offline_matches_blocking(tmp_path):
|
||||
offline_metrics, offline_requests = _run_mode("offline", tmp_path)
|
||||
blocking_metrics, blocking_requests = _run_mode("blocking", tmp_path)
|
||||
|
||||
for metrics in (offline_metrics, blocking_metrics):
|
||||
assert_decode_metrics(metrics)
|
||||
|
||||
assert len(offline_requests) == len(blocking_requests) == 3
|
||||
|
||||
offline_arrivals = [request["created_time"] for request in offline_requests]
|
||||
blocking_arrivals = [request["created_time"] for request in blocking_requests]
|
||||
assert offline_arrivals[1] > 0.5
|
||||
assert blocking_arrivals[1] > 0.5
|
||||
assert offline_arrivals == pytest.approx(blocking_arrivals, abs=0.02)
|
||||
assert (
|
||||
offline_metrics["max_concurrent_requests"]
|
||||
== blocking_metrics["max_concurrent_requests"]
|
||||
== 1
|
||||
)
|
||||
|
||||
for key in ("completed", "total_input", "total_output"):
|
||||
assert offline_metrics[key] == blocking_metrics[key]
|
||||
|
||||
for key, tolerance in RELATIVE_TOLERANCES.items():
|
||||
error = _relative_error(offline_metrics[key], blocking_metrics[key])
|
||||
assert error <= tolerance, (
|
||||
key,
|
||||
offline_metrics[key],
|
||||
blocking_metrics[key],
|
||||
error,
|
||||
tolerance,
|
||||
)
|
||||
@@ -0,0 +1,118 @@
|
||||
import atexit
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import patch
|
||||
|
||||
from sglang_simulator.dataset import GenericRequest, SimpleDataset
|
||||
from sglang_simulator.simulation.benchmark import BenchmarkConfig
|
||||
|
||||
ASSETS = Path(__file__).parent / "assets"
|
||||
SGLANG_ROOT = Path(__file__).parents[3]
|
||||
if str(SGLANG_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(SGLANG_ROOT))
|
||||
os.environ.setdefault("CUDA_VISIBLE_DEVICES", "")
|
||||
|
||||
|
||||
def make_fixed_dataset(
|
||||
start_token: int,
|
||||
count: int,
|
||||
*,
|
||||
input_length: int = 1025,
|
||||
output_length: int = 1,
|
||||
) -> SimpleDataset:
|
||||
return SimpleDataset(
|
||||
reqs=[
|
||||
GenericRequest(
|
||||
token_ids=[start_token + i] * input_length,
|
||||
input_length=input_length,
|
||||
output_length=output_length,
|
||||
custom_params={"created_time": i / 10},
|
||||
)
|
||||
for i in range(count)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def _write_sim_config(tmp_path: Path) -> Path:
|
||||
table_path = tmp_path / "replay.json"
|
||||
table_path.write_text(
|
||||
json.dumps({"[[1, 1024]]": 0.001, "[[1025, 0]]": 0.01}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
config = {
|
||||
"platform": {
|
||||
"accelerator": {"name": "a100_sxm", "hbm_capacity_gb": 80},
|
||||
"disk_read_bandwidth_gb": 8,
|
||||
"disk_write_bandwidth_gb": 8,
|
||||
"memory_read_bandwidth_gb": 64,
|
||||
"memory_write_bandwidth_gb": 64,
|
||||
"num_device_per_node": 8,
|
||||
},
|
||||
"predictor": {
|
||||
"name": "replay",
|
||||
"database_path": str(table_path),
|
||||
"miss_strategy": "knn",
|
||||
"miss_knn_k": 1,
|
||||
},
|
||||
"scheduler": {"tp_size": 1, "ep_size": 1, "dp_size": 1},
|
||||
}
|
||||
config_path = tmp_path / "sim_config.json"
|
||||
config_path.write_text(json.dumps(config), encoding="utf-8")
|
||||
return config_path
|
||||
|
||||
|
||||
def make_sglang_runner(tmp_path: Path):
|
||||
os.environ["SGLANG_SIMULATOR_CONFIG_PATH"] = str(_write_sim_config(tmp_path))
|
||||
|
||||
from benchmark.simulator.bench_runner import SGLangBenchmarkRunner
|
||||
from sglang.srt.server_args import ServerArgs
|
||||
|
||||
runner = SGLangBenchmarkRunner(
|
||||
server_args=ServerArgs(
|
||||
model_path=str(ASSETS / "qwen3-8b"),
|
||||
load_format="dummy",
|
||||
device="cpu",
|
||||
enable_hierarchical_cache=True,
|
||||
hicache_ratio=2,
|
||||
hicache_storage_backend="file",
|
||||
hicache_storage_prefetch_policy="wait_complete",
|
||||
max_total_tokens=10 * 1024,
|
||||
page_size=256,
|
||||
skip_tokenizer_init=True,
|
||||
)
|
||||
)
|
||||
runner.clear_hicache_storage()
|
||||
return runner
|
||||
|
||||
|
||||
def test_benchmark_sglang_runs_paged_decode(tmp_path):
|
||||
runner = make_sglang_runner(tmp_path)
|
||||
dataset = make_fixed_dataset(
|
||||
1000,
|
||||
2,
|
||||
input_length=1024,
|
||||
output_length=2,
|
||||
)
|
||||
|
||||
try:
|
||||
metrics = runner.benchmark(
|
||||
BenchmarkConfig(request_rate=10, ignore_request_timestamp=False),
|
||||
dataset=dataset,
|
||||
)
|
||||
request_stats = runner.get_request_stats()
|
||||
finally:
|
||||
with patch.object(atexit, "unregister", wraps=atexit.unregister) as unregister:
|
||||
runner.shutdown()
|
||||
runner.shutdown()
|
||||
|
||||
unregister.assert_called_once_with(runner.engine.shutdown)
|
||||
|
||||
assert metrics["completed"] == len(dataset)
|
||||
assert metrics["total_input"] == 2 * 1024
|
||||
assert metrics["total_output"] == 2 * 2
|
||||
assert metrics["mean_tpot_ms"] > 0
|
||||
assert all(
|
||||
idx == 0 or req["created_time"] > 0 for idx, req in enumerate(request_stats)
|
||||
)
|
||||
@@ -0,0 +1,163 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user