benchscope 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchscope/__init__.py +3 -0
- benchscope/__main__.py +4 -0
- benchscope/benches/__init__.py +1 -0
- benchscope/benches/base.py +54 -0
- benchscope/benches/runner.py +185 -0
- benchscope/benches/sglang_bench.py +77 -0
- benchscope/benches/vllm_bench.py +94 -0
- benchscope/cli.py +46 -0
- benchscope/config.py +91 -0
- benchscope/constants.py +49 -0
- benchscope/datasets.py +200 -0
- benchscope/gpu.py +39 -0
- benchscope/parser.py +93 -0
- benchscope/server/__init__.py +3 -0
- benchscope/server/api_config.py +97 -0
- benchscope/server/api_logs.py +356 -0
- benchscope/server/api_test.py +64 -0
- benchscope/server/app.py +99 -0
- benchscope/server/state.py +18 -0
- benchscope/server/status.py +97 -0
- benchscope/server/test_manager.py +367 -0
- benchscope/server/ws.py +56 -0
- benchscope/summary.py +148 -0
- benchscope/webui/assets/LogView-BDFIduo7.css +1 -0
- benchscope/webui/assets/LogView-BMsVPLVq.js +1 -0
- benchscope/webui/assets/MetricsCharts-D1wU0LbK.css +1 -0
- benchscope/webui/assets/MetricsCharts-DHO93JrC.js +1 -0
- benchscope/webui/assets/SettingsView-7uuTFqXU.css +1 -0
- benchscope/webui/assets/SettingsView-DJmFSQbU.js +1 -0
- benchscope/webui/assets/TestView-81VlxBZU.js +4 -0
- benchscope/webui/assets/TestView-BAwcOtR2.css +1 -0
- benchscope/webui/assets/antd-DWALckI0.js +478 -0
- benchscope/webui/assets/echarts-Bb6yjXMn.js +60 -0
- benchscope/webui/assets/index-Dfr0hp72.js +2 -0
- benchscope/webui/assets/index-EhoNt9Gv.css +1 -0
- benchscope/webui/assets/vue-Ch4zjUb1.js +37 -0
- benchscope/webui/index.html +19 -0
- benchscope-1.0.0.dist-info/METADATA +136 -0
- benchscope-1.0.0.dist-info/RECORD +43 -0
- benchscope-1.0.0.dist-info/WHEEL +5 -0
- benchscope-1.0.0.dist-info/entry_points.txt +2 -0
- benchscope-1.0.0.dist-info/licenses/LICENSE +176 -0
- benchscope-1.0.0.dist-info/top_level.txt +1 -0
benchscope/__init__.py
ADDED
benchscope/__main__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""bench 命令构建与执行。"""
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""bench 命令构建的公共定义。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass
|
|
9
|
+
class ParamDef:
|
|
10
|
+
"""UI 表单中一个可配置参数的定义。"""
|
|
11
|
+
|
|
12
|
+
key: str # 表单字段名
|
|
13
|
+
flag: str # 实际 CLI flag,如 "--temperature"
|
|
14
|
+
label: str # 中文标签
|
|
15
|
+
help: str = ""
|
|
16
|
+
type: str = "str" # str | int | float | bool | select
|
|
17
|
+
default: Any = None
|
|
18
|
+
options: list = field(default_factory=list)
|
|
19
|
+
advanced: bool = False # 是否归入“高级参数”折叠区
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass
|
|
23
|
+
class BenchOptions:
|
|
24
|
+
"""一次 bench 执行所需的全部选项。"""
|
|
25
|
+
|
|
26
|
+
framework: str
|
|
27
|
+
model: str
|
|
28
|
+
api: dict # {host, port, base_url, endpoint, api_key, extra_headers}
|
|
29
|
+
dataset: dict # {type, path, input_len, output_len, sharegpt_output_len}
|
|
30
|
+
concurrency: int
|
|
31
|
+
request_rate: str | float = "inf"
|
|
32
|
+
curated: dict = field(default_factory=dict) # 表单参数 key -> value
|
|
33
|
+
extra_args: list = field(default_factory=list) # [{"flag": "--x", "value": "y"}]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def build_arg_list(flags: list[list]) -> list[str]:
|
|
37
|
+
"""将 [["--flag","value"], ["--bool",""]] 展开为命令行列表。"""
|
|
38
|
+
out: list[str] = []
|
|
39
|
+
for item in flags:
|
|
40
|
+
flag, value = item[0], item[1] if len(item) > 1 else ""
|
|
41
|
+
if isinstance(value, bool):
|
|
42
|
+
if value:
|
|
43
|
+
out.append(flag)
|
|
44
|
+
continue
|
|
45
|
+
if value is None or value == "":
|
|
46
|
+
out.append(flag)
|
|
47
|
+
else:
|
|
48
|
+
out.extend([flag, str(value)])
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def flag_value(flag: str, value: Any) -> list[str]:
|
|
53
|
+
"""单个 flag 的展开(供参数校验后使用)。"""
|
|
54
|
+
return build_arg_list([[flag, value]])
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""bench 子进程流式执行器。
|
|
2
|
+
|
|
3
|
+
支持真实执行(vllm/sglang CLI)与 FAKE 模式(BENCHSCOPE_FAKE_BENCH=1,
|
|
4
|
+
生成仿真输出,便于无 vllm/sglang 环境下联调 UI 全流程)。
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import math
|
|
10
|
+
import os
|
|
11
|
+
import random
|
|
12
|
+
import shlex
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
import threading
|
|
16
|
+
import time
|
|
17
|
+
from typing import Callable, Optional
|
|
18
|
+
|
|
19
|
+
from benchscope.parser import parse_metrics
|
|
20
|
+
|
|
21
|
+
log = logging.getLogger("benchscope.runner")
|
|
22
|
+
|
|
23
|
+
StreamCallback = Callable[[str], None] # 每行输出回调
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class StopRequested(RuntimeError):
|
|
27
|
+
"""测试被人为停止。"""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class BenchRunner:
|
|
31
|
+
def __init__(self, command_template: str | None = None):
|
|
32
|
+
"""command_template 形如 "vllm bench serve" / "python -m sglang.bench_serving"。"""
|
|
33
|
+
self.command_template = command_template or "vllm bench serve"
|
|
34
|
+
self._proc: Optional[subprocess.Popen] = None
|
|
35
|
+
self._stop_flag = threading.Event()
|
|
36
|
+
|
|
37
|
+
def kill(self) -> None:
|
|
38
|
+
"""终止当前执行的子进程(用于停止测试)。"""
|
|
39
|
+
self._stop_flag.set()
|
|
40
|
+
proc = self._proc
|
|
41
|
+
if proc and proc.poll() is None:
|
|
42
|
+
try:
|
|
43
|
+
proc.kill()
|
|
44
|
+
except Exception:
|
|
45
|
+
pass
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------------
|
|
48
|
+
def run(
|
|
49
|
+
self,
|
|
50
|
+
cmd: list[str],
|
|
51
|
+
stream_cb: Optional[StreamCallback] = None,
|
|
52
|
+
timeout: float | None = None,
|
|
53
|
+
) -> dict:
|
|
54
|
+
"""执行命令,返回 parse_metrics 结果(含 raw)。失败抛 RuntimeError。"""
|
|
55
|
+
self._stop_flag.clear()
|
|
56
|
+
if os.environ.get("BENCHSCOPE_FAKE_BENCH") == "1":
|
|
57
|
+
return self._run_fake(cmd, stream_cb)
|
|
58
|
+
|
|
59
|
+
# 用模板指定的可执行文件替换命令头部(vllm / python -m sglang...)
|
|
60
|
+
full_cmd = self._resolve(cmd)
|
|
61
|
+
log.info("执行命令: %s", " ".join(full_cmd))
|
|
62
|
+
if stream_cb:
|
|
63
|
+
stream_cb("$ " + " ".join(full_cmd) + "\n")
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
proc = subprocess.Popen(
|
|
67
|
+
full_cmd,
|
|
68
|
+
stdout=subprocess.PIPE,
|
|
69
|
+
stderr=subprocess.STDOUT,
|
|
70
|
+
text=True,
|
|
71
|
+
encoding="utf-8",
|
|
72
|
+
errors="replace",
|
|
73
|
+
bufsize=1,
|
|
74
|
+
)
|
|
75
|
+
self._proc = proc
|
|
76
|
+
except FileNotFoundError as e:
|
|
77
|
+
raise RuntimeError(
|
|
78
|
+
f"未找到命令执行环境:{full_cmd[0]}。请确认已安装 "
|
|
79
|
+
f"{self.command_template.split()[0]} 相关 CLI(并在服务设置中配置 bench 命令)。"
|
|
80
|
+
) from e
|
|
81
|
+
|
|
82
|
+
chunks: list[str] = []
|
|
83
|
+
start = time.time()
|
|
84
|
+
try:
|
|
85
|
+
assert proc.stdout is not None
|
|
86
|
+
for line in proc.stdout:
|
|
87
|
+
chunks.append(line)
|
|
88
|
+
if stream_cb:
|
|
89
|
+
stream_cb(line)
|
|
90
|
+
if timeout and time.time() - start > timeout:
|
|
91
|
+
proc.kill()
|
|
92
|
+
raise RuntimeError(f"bench 执行超时(>{timeout}s)")
|
|
93
|
+
proc.wait()
|
|
94
|
+
except KeyboardInterrupt:
|
|
95
|
+
proc.kill()
|
|
96
|
+
raise
|
|
97
|
+
finally:
|
|
98
|
+
self._proc = None
|
|
99
|
+
if proc.returncode != 0:
|
|
100
|
+
output = "".join(chunks)
|
|
101
|
+
if proc.returncode == -9 or proc.returncode == 137:
|
|
102
|
+
raise StopRequested("测试已被停止")
|
|
103
|
+
raise RuntimeError(
|
|
104
|
+
f"bench 命令执行失败(返回码 {proc.returncode})。\n完整日志:\n{output[-4000:]}"
|
|
105
|
+
)
|
|
106
|
+
output = "".join(chunks)
|
|
107
|
+
metrics = parse_metrics(output)
|
|
108
|
+
if "output_mean" not in metrics:
|
|
109
|
+
raise RuntimeError(f"未能从 bench 输出解析出指标,输出如下:\n{output[-3000:]}")
|
|
110
|
+
return metrics
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------------
|
|
113
|
+
def _resolve(self, cmd: list[str]) -> list[str]:
|
|
114
|
+
"""把命令头替换为模板指定的执行方式。"""
|
|
115
|
+
tmpl = shlex.split(self.command_template)
|
|
116
|
+
# 保留原始参数(从模板之后开始)
|
|
117
|
+
return tmpl + cmd[len(tmpl):] if cmd[:len(tmpl)] == tmpl else tmpl + cmd
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
# FAKE 模式:生成仿真 vllm 风格输出
|
|
121
|
+
def _run_fake(self, cmd: list[str], stream_cb: Optional[StreamCallback] = None) -> dict:
|
|
122
|
+
args = " ".join(cmd)
|
|
123
|
+
concurrency = 1
|
|
124
|
+
input_len, output_len = 1024, 1024
|
|
125
|
+
for i, tok in enumerate(cmd):
|
|
126
|
+
if tok == "--max-concurrency" and i + 1 < len(cmd):
|
|
127
|
+
concurrency = int(cmd[i + 1])
|
|
128
|
+
if tok == "--random-input-len" and i + 1 < len(cmd):
|
|
129
|
+
input_len = int(cmd[i + 1])
|
|
130
|
+
if tok == "--random-output-len" and i + 1 < len(cmd):
|
|
131
|
+
output_len = int(cmd[i + 1])
|
|
132
|
+
rng = random.Random(int(time.time() * 1000) % 2**31)
|
|
133
|
+
|
|
134
|
+
c = max(concurrency, 1)
|
|
135
|
+
out_tps = round(50 * c**0.62 * rng.uniform(0.95, 1.05), 2)
|
|
136
|
+
total = round(out_tps * (input_len + output_len) / output_len, 2)
|
|
137
|
+
ttft = round(60 + 9 * c + rng.uniform(0, 20), 2)
|
|
138
|
+
tpot = round(18 + 0.55 * c + rng.uniform(0, 3), 2)
|
|
139
|
+
itl = round(tpot * rng.uniform(0.97, 1.02), 2)
|
|
140
|
+
|
|
141
|
+
lines = [
|
|
142
|
+
"============ Serving Benchmark Result ============",
|
|
143
|
+
"Successful requests: %d" % c,
|
|
144
|
+
"Failed requests: 0",
|
|
145
|
+
"Maximum request concurrency: %d" % c,
|
|
146
|
+
"Benchmark duration (s): %.2f" % rng.uniform(5, 40),
|
|
147
|
+
"Total input tokens: %d" % (input_len * c),
|
|
148
|
+
"Total generated tokens: %d" % (output_len * c),
|
|
149
|
+
"Request throughput (req/s): %.2f" % rng.uniform(0.1, c),
|
|
150
|
+
"Output token throughput (tok/s): %s" % out_tps,
|
|
151
|
+
"Peak output token throughput (tok/s): %s" % round(out_tps * 1.02, 2),
|
|
152
|
+
"Peak concurrent requests: %.2f" % c,
|
|
153
|
+
"Total token throughput (tok/s): %s" % total,
|
|
154
|
+
"---------------Time to First Token----------------",
|
|
155
|
+
"Mean TTFT (ms): %s" % ttft,
|
|
156
|
+
"Median TTFT (ms): %s" % round(ttft * 0.98, 2),
|
|
157
|
+
"P99 TTFT (ms): %s" % round(ttft * rng.uniform(1.05, 1.3), 2),
|
|
158
|
+
"-----Time per Output Token (excl. 1st token)------",
|
|
159
|
+
"Mean TPOT (ms): %s" % tpot,
|
|
160
|
+
"Median TPOT (ms): %s" % round(tpot * 0.97, 2),
|
|
161
|
+
"P99 TPOT (ms): %s" % round(tpot * rng.uniform(1.06, 1.35), 2),
|
|
162
|
+
"---------------Inter-token Latency----------------",
|
|
163
|
+
"Mean ITL (ms): %s" % itl,
|
|
164
|
+
"Median ITL (ms): %s" % round(itl * 0.97, 2),
|
|
165
|
+
"P99 ITL (ms): %s" % round(itl * rng.uniform(1.05, 1.3), 2),
|
|
166
|
+
"==================================================",
|
|
167
|
+
"",
|
|
168
|
+
]
|
|
169
|
+
output = "\n".join(lines)
|
|
170
|
+
# 模拟耗时(可被 kill 中断)
|
|
171
|
+
total_sleep = min(0.6, 0.2 + c * 0.01)
|
|
172
|
+
slept = 0.0
|
|
173
|
+
while slept < total_sleep:
|
|
174
|
+
if self._stop_flag.is_set():
|
|
175
|
+
raise StopRequested("测试已被停止")
|
|
176
|
+
time.sleep(0.05)
|
|
177
|
+
slept += 0.05
|
|
178
|
+
if self._stop_flag.is_set():
|
|
179
|
+
raise StopRequested("测试已被停止")
|
|
180
|
+
if stream_cb:
|
|
181
|
+
stream_cb(f"$ {args}\n")
|
|
182
|
+
for ln in lines:
|
|
183
|
+
stream_cb(ln + "\n")
|
|
184
|
+
metrics = parse_metrics(output)
|
|
185
|
+
return metrics
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""SGLang `python -m sglang.bench_serving` 命令构建。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from benchscope.benches.base import BenchOptions, ParamDef, build_arg_list
|
|
5
|
+
|
|
6
|
+
FRAMEWORK = "sglang"
|
|
7
|
+
|
|
8
|
+
CURATED_PARAMS: list[ParamDef] = [
|
|
9
|
+
ParamDef("backend", "--backend", "后端 Backend", "openai / sglang", "select",
|
|
10
|
+
default="openai", options=["openai", "sglang"]),
|
|
11
|
+
ParamDef("apply_chat_template", "--apply-chat-template", "应用聊天模板", "sharegpt/自定义数据集时按模板构造", "bool", default=True),
|
|
12
|
+
ParamDef("disable_ignore_eos", "--disable-ignore-eos", "不忽略 EOS", "开启后不忽略 EOS", "bool", default=False),
|
|
13
|
+
ParamDef("seed", "--seed", "随机种子 Seed", "", "int", default=0),
|
|
14
|
+
ParamDef("warmup_requests", "--warmup-requests", "预热请求数 Warmups", "", "int", default=0),
|
|
15
|
+
ParamDef("tokenize_prompt", "--tokenize-prompt", "预分词 tokenize-prompt", "", "bool", default=True),
|
|
16
|
+
ParamDef("flush_cache", "--flush-cache", "刷新缓存 flush-cache", "每次运行前清空 radix cache", "bool", default=False, advanced=True),
|
|
17
|
+
ParamDef("print_requests", "--print-requests", "打印请求", "", "bool", default=False, advanced=True),
|
|
18
|
+
ParamDef("disable_tqdm", "--disable-tqdm", "禁用进度条", "", "bool", default=False, advanced=True),
|
|
19
|
+
ParamDef("sharegpt_output_len", "--sharegpt-output-len", "ShareGPT 输出长度", "sharegpt 数据集平均输出 token 数", "int", default=128, advanced=True),
|
|
20
|
+
ParamDef("sharegpt_context_len", "--sharegpt-context-len", "ShareGPT 上下文长度", "", "int", default=None, advanced=True),
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
_CORE_KEYS = {"model", "tokenizer", "max-concurrency", "num-prompts",
|
|
24
|
+
"random-input-len", "random-output-len", "dataset-name",
|
|
25
|
+
"dataset-path", "request-rate", "base-url"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def build_command(opts: BenchOptions) -> list[str]:
|
|
29
|
+
"""构建 sglang.bench_serving 命令。"""
|
|
30
|
+
api = opts.api
|
|
31
|
+
ds = opts.dataset
|
|
32
|
+
base_url = api.get("base_url") or f"http://{api.get('host', '127.0.0.1')}:{api.get('port', '8000')}"
|
|
33
|
+
|
|
34
|
+
cmd = ["python", "-m", "sglang.bench_serving"]
|
|
35
|
+
flags: list[list] = [
|
|
36
|
+
["--backend", opts.curated.get("backend", "openai")],
|
|
37
|
+
["--base-url", base_url],
|
|
38
|
+
["--model", opts.model],
|
|
39
|
+
["--tokenizer", opts.tokenizer if opts.tokenizer else opts.model],
|
|
40
|
+
["--num-prompts", opts.concurrency],
|
|
41
|
+
["--max-concurrency", opts.concurrency],
|
|
42
|
+
["--request-rate", str(opts.request_rate)],
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
if ds.get("type") in ("sharegpt", "custom"):
|
|
46
|
+
flags.append(["--dataset-name", "sharegpt"])
|
|
47
|
+
if ds.get("path"):
|
|
48
|
+
flags.append(["--dataset-path", ds["path"]])
|
|
49
|
+
if ds.get("sharegpt_output_len"):
|
|
50
|
+
flags.append(["--sharegpt-output-len", ds["sharegpt_output_len"]])
|
|
51
|
+
else:
|
|
52
|
+
flags.append(["--dataset-name", "random"])
|
|
53
|
+
flags.append(["--random-input-len", ds.get("input_len", 1024)])
|
|
54
|
+
flags.append(["--random-output-len", ds.get("output_len", 1024)])
|
|
55
|
+
|
|
56
|
+
used = {f[0] for f in flags}
|
|
57
|
+
for item in _expand_curated(opts):
|
|
58
|
+
if item[0] not in used:
|
|
59
|
+
flags.append(item)
|
|
60
|
+
|
|
61
|
+
return cmd + build_arg_list(flags)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _expand_curated(opts: BenchOptions) -> list[list]:
|
|
65
|
+
out: list[list] = []
|
|
66
|
+
for key, value in opts.curated.items():
|
|
67
|
+
if value is None or value == "" or value is False:
|
|
68
|
+
continue
|
|
69
|
+
param = next((p for p in CURATED_PARAMS if p.key == key), None)
|
|
70
|
+
flag = param.flag if param else f"--{key.replace('_', '-')}"
|
|
71
|
+
if key == "disable_ignore_eos" and value is False:
|
|
72
|
+
continue
|
|
73
|
+
if param and param.type == "bool" and value is True:
|
|
74
|
+
out.append([flag, ""])
|
|
75
|
+
else:
|
|
76
|
+
out.append([flag, value])
|
|
77
|
+
return out
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
"""vLLM `vllm bench serve` 命令构建。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import urllib.parse
|
|
5
|
+
|
|
6
|
+
from benchscope.benches.base import BenchOptions, ParamDef, build_arg_list
|
|
7
|
+
|
|
8
|
+
FRAMEWORK = "vllm"
|
|
9
|
+
|
|
10
|
+
# 常用参数表单(前端按此渲染,勾选/填写的值会合并进 extra_args)
|
|
11
|
+
CURATED_PARAMS: list[ParamDef] = [
|
|
12
|
+
ParamDef("backend", "--backend", "后端 Backend", "openai-chat / openai 等", "select",
|
|
13
|
+
default="openai-chat", options=["openai-chat", "openai"]),
|
|
14
|
+
ParamDef("endpoint", "--endpoint", "接口 Endpoint", "默认 /v1/chat/completions", "str",
|
|
15
|
+
default="/v1/chat/completions"),
|
|
16
|
+
ParamDef("trust_remote_code", "--trust-remote-code", "trust-remote-code", "", "bool", default=True),
|
|
17
|
+
ParamDef("ignore_eos", "--ignore-eos", "忽略 EOS ignore-eos", "", "bool", default=True),
|
|
18
|
+
ParamDef("burstiness", "--burstiness", "突发因子 Burstiness", "请求到达的突发程度", "float", default=1.0),
|
|
19
|
+
ParamDef("seed", "--seed", "随机种子 Seed", "", "int", default=0),
|
|
20
|
+
ParamDef("num_warmups", "--num-warmups", "预热请求数 Warmups", "", "int", default=0),
|
|
21
|
+
ParamDef("metric_percentiles", "--metric-percentiles", "百分位 Percentiles", "如 99", "str", default="99"),
|
|
22
|
+
ParamDef("temperature", "--temperature", "采样温度 Temperature", "", "float", default=0.0),
|
|
23
|
+
ParamDef("top_p", "--top-p", "top-p", "", "float", default=1.0),
|
|
24
|
+
ParamDef("top_k", "--top-k", "top-k", "", "int", default=-1),
|
|
25
|
+
ParamDef("min_p", "--min-p", "min-p", "", "float", default=0.0),
|
|
26
|
+
ParamDef("frequency_penalty", "--frequency-penalty", "频率惩罚", "", "float", default=0.0),
|
|
27
|
+
ParamDef("presence_penalty", "--presence-penalty", "存在惩罚", "", "float", default=0.0),
|
|
28
|
+
ParamDef("sharegpt_output_len", "--sharegpt-output-len", "ShareGPT 输出长度", "sharegpt 数据集平均输出 token 数", "int", default=128, advanced=True),
|
|
29
|
+
ParamDef("no_stream", "--no-stream", "禁用流式输出", "", "bool", default=False, advanced=True),
|
|
30
|
+
ParamDef("disable_tqdm", "--disable-tqdm", "禁用进度条", "", "bool", default=False, advanced=True),
|
|
31
|
+
ParamDef("save_result", "--save-result", "保存结果 save-result", "保存详细结果文件", "bool", default=False, advanced=True),
|
|
32
|
+
ParamDef("profile", "--profile", "性能剖析 profile", "", "bool", default=False, advanced=True),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
_CORE_KEYS = {"model", "tokenizer", "max-concurrency", "num-prompts",
|
|
36
|
+
"random-input-len", "random-output-len", "dataset-name",
|
|
37
|
+
"dataset-path", "request-rate", "host", "port", "endpoint"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def build_command(opts: BenchOptions) -> list[str]:
|
|
41
|
+
"""构建 vllm bench serve 命令。"""
|
|
42
|
+
api = opts.api
|
|
43
|
+
ds = opts.dataset
|
|
44
|
+
base_url = api.get("base_url") or ""
|
|
45
|
+
parsed = urllib.parse.urlparse(base_url)
|
|
46
|
+
host = api.get("host") or parsed.hostname or "127.0.0.1"
|
|
47
|
+
port = api.get("port") or parsed.port or "8000"
|
|
48
|
+
|
|
49
|
+
base = ["vllm", "bench", "serve"]
|
|
50
|
+
flags: list[list] = [
|
|
51
|
+
["--max-concurrency", opts.concurrency],
|
|
52
|
+
["--num-prompts", opts.concurrency],
|
|
53
|
+
["--model", opts.model],
|
|
54
|
+
["--tokenizer", opts.tokenizer if opts.tokenizer else opts.model],
|
|
55
|
+
["--host", host],
|
|
56
|
+
["--port", port],
|
|
57
|
+
["--request-rate", str(opts.request_rate)],
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
if ds.get("type") in ("sharegpt", "custom"):
|
|
61
|
+
flags.append(["--dataset-name", "sharegpt"])
|
|
62
|
+
if ds.get("path"):
|
|
63
|
+
flags.append(["--dataset-path", ds["path"]])
|
|
64
|
+
if ds.get("sharegpt_output_len"):
|
|
65
|
+
flags.append(["--sharegpt-output-len", ds["sharegpt_output_len"]])
|
|
66
|
+
else:
|
|
67
|
+
flags.append(["--dataset-name", "random"])
|
|
68
|
+
flags.append(["--random-input-len", ds.get("input_len", 1024)])
|
|
69
|
+
flags.append(["--random-output-len", ds.get("output_len", 1024)])
|
|
70
|
+
|
|
71
|
+
# 表单单选参数(curated),已存在于核心参数中的跳过
|
|
72
|
+
used = set()
|
|
73
|
+
for f in flags:
|
|
74
|
+
used.add(f[0])
|
|
75
|
+
for item in _expand_curated(opts):
|
|
76
|
+
if item[0] not in used:
|
|
77
|
+
flags.append(item)
|
|
78
|
+
|
|
79
|
+
return base + build_arg_list(flags)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _expand_curated(opts: BenchOptions) -> list[list]:
|
|
83
|
+
"""将 curated 表单值展开为 flag 列表。"""
|
|
84
|
+
out: list[list] = []
|
|
85
|
+
for key, value in opts.curated.items():
|
|
86
|
+
if value is None or value == "" or value is False:
|
|
87
|
+
continue
|
|
88
|
+
param = next((p for p in CURATED_PARAMS if p.key == key), None)
|
|
89
|
+
flag = param.flag if param else f"--{key.replace('_', '-')}"
|
|
90
|
+
if param and param.type == "bool" and value is True:
|
|
91
|
+
out.append([flag, ""])
|
|
92
|
+
else:
|
|
93
|
+
out.append([flag, value])
|
|
94
|
+
return out
|
benchscope/cli.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""benchscope 命令行入口。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import logging
|
|
6
|
+
import sys
|
|
7
|
+
import webbrowser
|
|
8
|
+
import threading
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def main(argv=None) -> int:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="benchscope",
|
|
14
|
+
description="vLLM / SGLang 推理服务性能测试 Web 工具",
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("--host", default="0.0.0.0", help="监听地址(默认 0.0.0.0)")
|
|
17
|
+
parser.add_argument("--port", type=int, default=8080, help="监听端口(默认 8080)")
|
|
18
|
+
parser.add_argument("--no-browser", action="store_true", help="不自动打开浏览器")
|
|
19
|
+
parser.add_argument("--debug", action="store_true", help="开启调试日志")
|
|
20
|
+
args = parser.parse_args(argv)
|
|
21
|
+
|
|
22
|
+
logging.basicConfig(
|
|
23
|
+
level=logging.DEBUG if args.debug else logging.INFO,
|
|
24
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
import uvicorn
|
|
28
|
+
|
|
29
|
+
from benchscope.server.app import create_app
|
|
30
|
+
|
|
31
|
+
app = create_app()
|
|
32
|
+
url = f"http://127.0.0.1:{args.port}"
|
|
33
|
+
|
|
34
|
+
if not args.no_browser:
|
|
35
|
+
threading.Timer(1.2, lambda: webbrowser.open(url)).start()
|
|
36
|
+
|
|
37
|
+
print("=" * 56)
|
|
38
|
+
print(" benchscope - vLLM / SGLang 性能测试工具")
|
|
39
|
+
print(f" 打开浏览器访问: {url}")
|
|
40
|
+
print("=" * 56)
|
|
41
|
+
uvicorn.run(app, host=args.host, port=args.port, log_level="info")
|
|
42
|
+
return 0
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
if __name__ == "__main__":
|
|
46
|
+
sys.exit(main())
|
benchscope/config.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""配置持久化:config.json 读写与运行时配置单例。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import threading
|
|
7
|
+
from copy import deepcopy
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
from benchscope.constants import DEFAULT_CONFIG
|
|
11
|
+
|
|
12
|
+
DEFAULT_CONFIG_PATH = Path.home() / ".benchscope" / "config.json"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ConfigManager:
|
|
16
|
+
"""线程安全的配置管理。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self, path: Path | str | None = None):
|
|
19
|
+
self.path = Path(path) if path else DEFAULT_CONFIG_PATH
|
|
20
|
+
self._lock = threading.RLock()
|
|
21
|
+
self._data: dict = deepcopy(DEFAULT_CONFIG)
|
|
22
|
+
self.load()
|
|
23
|
+
|
|
24
|
+
# ---------- 持久化 ----------
|
|
25
|
+
def load(self) -> None:
|
|
26
|
+
with self._lock:
|
|
27
|
+
if self.path.exists():
|
|
28
|
+
try:
|
|
29
|
+
loaded = json.loads(self.path.read_text(encoding="utf-8"))
|
|
30
|
+
self._merge(self._data, loaded)
|
|
31
|
+
except Exception:
|
|
32
|
+
pass # 配置损坏时使用默认配置
|
|
33
|
+
|
|
34
|
+
def save(self) -> None:
|
|
35
|
+
with self._lock:
|
|
36
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
37
|
+
self.path.write_text(
|
|
38
|
+
json.dumps(self._data, ensure_ascii=False, indent=2),
|
|
39
|
+
encoding="utf-8",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@staticmethod
|
|
43
|
+
def _merge(base: dict, overlay: dict) -> None:
|
|
44
|
+
for key, value in overlay.items():
|
|
45
|
+
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
46
|
+
ConfigManager._merge(base[key], value)
|
|
47
|
+
else:
|
|
48
|
+
base[key] = value
|
|
49
|
+
|
|
50
|
+
# ---------- 访问 ----------
|
|
51
|
+
def get(self, key: str, default=None):
|
|
52
|
+
with self._lock:
|
|
53
|
+
return self._data.get(key, default)
|
|
54
|
+
|
|
55
|
+
def set(self, key: str, value) -> None:
|
|
56
|
+
with self._lock:
|
|
57
|
+
self._data[key] = value
|
|
58
|
+
self.save()
|
|
59
|
+
|
|
60
|
+
def update(self, patch: dict) -> dict:
|
|
61
|
+
with self._lock:
|
|
62
|
+
self._merge(self._data, patch)
|
|
63
|
+
self.save()
|
|
64
|
+
return deepcopy(self._data)
|
|
65
|
+
|
|
66
|
+
def snapshot(self) -> dict:
|
|
67
|
+
with self._lock:
|
|
68
|
+
return deepcopy(self._data)
|
|
69
|
+
|
|
70
|
+
# ---------- 常用辅助 ----------
|
|
71
|
+
@property
|
|
72
|
+
def api(self) -> dict:
|
|
73
|
+
return self.get("api", {})
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def logs_dir(self) -> Path:
|
|
77
|
+
raw = self.get("logs_dir", "./logs")
|
|
78
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def datasets_dir(self) -> Path:
|
|
82
|
+
raw = self.get("datasets_dir", "./datasets")
|
|
83
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
84
|
+
|
|
85
|
+
def set_api(self, patch: dict) -> dict:
|
|
86
|
+
with self._lock:
|
|
87
|
+
api = deepcopy(self._data.setdefault("api", {}))
|
|
88
|
+
api.update(patch)
|
|
89
|
+
self._data["api"] = api
|
|
90
|
+
self.save()
|
|
91
|
+
return deepcopy(api)
|
benchscope/constants.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""全局常量与默认值。"""
|
|
2
|
+
|
|
3
|
+
# 默认并发数列表(可编辑、可添加、可删除)
|
|
4
|
+
DEFAULT_CONCURRENCY_LIST = [1, 4, 8, 16, 32, 40, 64, 128]
|
|
5
|
+
|
|
6
|
+
# random 数据集默认输入/输出长度组合 (input_len, output_len, 显示后缀)
|
|
7
|
+
DEFAULT_LENGTH_PAIRS = [
|
|
8
|
+
(3072, 1024, "3K1K"),
|
|
9
|
+
(1024, 1024, "1K1K"),
|
|
10
|
+
(256, 256, "256X256"),
|
|
11
|
+
]
|
|
12
|
+
|
|
13
|
+
# 框架标识
|
|
14
|
+
FRAMEWORK_VLLM = "vllm"
|
|
15
|
+
FRAMEWORK_SGLANG = "sglang"
|
|
16
|
+
FRAMEWORK_NAMES = {FRAMEWORK_VLLM: "vLLM", FRAMEWORK_SGLANG: "SGLang"}
|
|
17
|
+
|
|
18
|
+
# 数据集类型
|
|
19
|
+
DATASET_RANDOM = "random"
|
|
20
|
+
DATASET_SHAREGPT = "sharegpt"
|
|
21
|
+
DATASET_CUSTOM = "custom"
|
|
22
|
+
|
|
23
|
+
# ShareGPT 数据集(modelscope)
|
|
24
|
+
SHAREGPT_DATASET_ID = "gliang1001/ShareGPT_V3_unfiltered_cleaned_split"
|
|
25
|
+
|
|
26
|
+
# 状态
|
|
27
|
+
STATUS_READY = "ready"
|
|
28
|
+
STATUS_OFFLINE = "offline"
|
|
29
|
+
STATUS_RUNNING = "running"
|
|
30
|
+
|
|
31
|
+
# 默认配置
|
|
32
|
+
DEFAULT_CONFIG = {
|
|
33
|
+
"framework": FRAMEWORK_VLLM,
|
|
34
|
+
"api": {
|
|
35
|
+
"base_url": "http://192.168.1.67:8000",
|
|
36
|
+
"endpoint": "/v1/chat/completions",
|
|
37
|
+
"api_key": "",
|
|
38
|
+
"extra_headers": {},
|
|
39
|
+
},
|
|
40
|
+
"gpu": {"auto": True, "name": "", "count": 8},
|
|
41
|
+
"logs_dir": "./logs",
|
|
42
|
+
"datasets_dir": "./datasets",
|
|
43
|
+
"tpot_threshold_ms": 100,
|
|
44
|
+
"request_rate": "inf", # inf 或数字
|
|
45
|
+
"bench_commands": {
|
|
46
|
+
"vllm": "vllm bench serve",
|
|
47
|
+
"sglang": "python -m sglang.bench_serving",
|
|
48
|
+
},
|
|
49
|
+
}
|