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
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
"""测试执行管理器:编排用例×并发,流式执行、实时推送、日志落盘、xlsx 汇总。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import re
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Optional
|
|
13
|
+
|
|
14
|
+
from benchscope.benches.base import BenchOptions
|
|
15
|
+
from benchscope.benches.runner import BenchRunner, StopRequested
|
|
16
|
+
from benchscope.benches import sglang_bench, vllm_bench
|
|
17
|
+
from benchscope.constants import (
|
|
18
|
+
DATASET_RANDOM, DATASET_SHAREGPT, FRAMEWORK_NAMES,
|
|
19
|
+
)
|
|
20
|
+
from benchscope.summary import write_summary_csv, write_xlsx
|
|
21
|
+
|
|
22
|
+
log = logging.getLogger("benchscope.test")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def sanitize_name(name: str) -> str:
|
|
26
|
+
"""把模型名/路径转成安全的文件名片段。"""
|
|
27
|
+
return re.sub(r"[^\w.-]", "_", name).strip("_")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def build_cases(dataset: dict, model: str) -> list[dict]:
|
|
31
|
+
"""根据数据集配置生成用例列表。"""
|
|
32
|
+
ds_type = dataset.get("type", DATASET_RANDOM)
|
|
33
|
+
cases: list[dict] = []
|
|
34
|
+
if ds_type == DATASET_RANDOM:
|
|
35
|
+
pairs = dataset.get("length_pairs") or []
|
|
36
|
+
for il, ol, label in pairs:
|
|
37
|
+
cases.append({
|
|
38
|
+
"label": label,
|
|
39
|
+
"input_len": il,
|
|
40
|
+
"output_len": ol,
|
|
41
|
+
"path": None,
|
|
42
|
+
})
|
|
43
|
+
else:
|
|
44
|
+
label = "ShareGPT" if ds_type == DATASET_SHAREGPT else "Custom"
|
|
45
|
+
if ds_type == DATASET_SHAREGPT and dataset.get("label"):
|
|
46
|
+
label = dataset["label"]
|
|
47
|
+
cases.append({
|
|
48
|
+
"label": label,
|
|
49
|
+
"input_len": None,
|
|
50
|
+
"output_len": None,
|
|
51
|
+
"path": dataset.get("path"),
|
|
52
|
+
})
|
|
53
|
+
return cases
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass
|
|
57
|
+
class TestRun:
|
|
58
|
+
run_id: str
|
|
59
|
+
run_dir: Path
|
|
60
|
+
payload: dict
|
|
61
|
+
framework: str
|
|
62
|
+
model: str
|
|
63
|
+
gpu: dict
|
|
64
|
+
cases: list = field(default_factory=list)
|
|
65
|
+
rows: list = field(default_factory=list)
|
|
66
|
+
status: str = "running" # running | done | stopped | error
|
|
67
|
+
error: Optional[str] = None
|
|
68
|
+
summary: Optional[dict] = None
|
|
69
|
+
started_at: str = ""
|
|
70
|
+
finished_at: str = ""
|
|
71
|
+
precision: str = ""
|
|
72
|
+
|
|
73
|
+
def snapshot(self, include_rows: bool = True) -> dict:
|
|
74
|
+
data = {
|
|
75
|
+
"run_id": self.run_id,
|
|
76
|
+
"run_dir": str(self.run_dir),
|
|
77
|
+
"framework": self.framework,
|
|
78
|
+
"framework_name": FRAMEWORK_NAMES.get(self.framework, self.framework),
|
|
79
|
+
"model": self.model,
|
|
80
|
+
"gpu": self.gpu,
|
|
81
|
+
"cases": self.cases,
|
|
82
|
+
"status": self.status,
|
|
83
|
+
"error": self.error,
|
|
84
|
+
"summary": self.summary,
|
|
85
|
+
"started_at": self.started_at,
|
|
86
|
+
"finished_at": self.finished_at,
|
|
87
|
+
"precision": self.precision,
|
|
88
|
+
"concurrency_list": self.payload.get("concurrency_list", []),
|
|
89
|
+
"request_rate": self.payload.get("request_rate", "inf"),
|
|
90
|
+
"tpot_threshold_ms": self.payload.get("tpot_threshold_ms"),
|
|
91
|
+
"dataset": self.payload.get("dataset", {}),
|
|
92
|
+
}
|
|
93
|
+
if include_rows:
|
|
94
|
+
data["rows"] = self.rows
|
|
95
|
+
return data
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def build_single_command(
|
|
99
|
+
framework: str, model: str, tokenizer: str, api: dict, dataset: dict,
|
|
100
|
+
concurrency: int, request_rate, curated: dict, extra_args: list,
|
|
101
|
+
) -> list[str]:
|
|
102
|
+
"""构建单条 bench 命令(供执行与预览共用)。"""
|
|
103
|
+
opts = BenchOptions(
|
|
104
|
+
framework=framework,
|
|
105
|
+
model=model,
|
|
106
|
+
api=api,
|
|
107
|
+
dataset=dataset,
|
|
108
|
+
concurrency=concurrency,
|
|
109
|
+
request_rate=request_rate,
|
|
110
|
+
curated=curated or {},
|
|
111
|
+
extra_args=extra_args or [],
|
|
112
|
+
)
|
|
113
|
+
opts.tokenizer = tokenizer or model
|
|
114
|
+
if framework == "sglang":
|
|
115
|
+
return sglang_bench.build_command(opts)
|
|
116
|
+
return vllm_bench.build_command(opts)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def build_command_lines(payload: dict, config) -> list[dict]:
|
|
120
|
+
"""为 payload 中的所有用例×并发构建命令列表(预览用)。"""
|
|
121
|
+
framework = payload.get("framework", "vllm")
|
|
122
|
+
model = payload.get("model", "")
|
|
123
|
+
tokenizer = payload.get("tokenizer", "")
|
|
124
|
+
dataset = dict(payload.get("dataset", {}))
|
|
125
|
+
cases = build_cases(dataset, model)
|
|
126
|
+
lines = []
|
|
127
|
+
for case in cases:
|
|
128
|
+
ds = dict(dataset)
|
|
129
|
+
ds.update({
|
|
130
|
+
"input_len": case.get("input_len"),
|
|
131
|
+
"output_len": case.get("output_len"),
|
|
132
|
+
"path": case.get("path"),
|
|
133
|
+
})
|
|
134
|
+
for conc in payload.get("concurrency_list", []):
|
|
135
|
+
if conc == "inf" or conc is None:
|
|
136
|
+
continue
|
|
137
|
+
cmd = build_single_command(
|
|
138
|
+
framework, model, tokenizer, dict(config.api), ds,
|
|
139
|
+
int(conc), payload.get("request_rate", "inf"),
|
|
140
|
+
payload.get("curated", {}), payload.get("extra_args", []),
|
|
141
|
+
)
|
|
142
|
+
lines.append({"case": case["label"], "concurrency": int(conc), "cmd": " ".join(cmd)})
|
|
143
|
+
return lines
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
class TestManager:
|
|
147
|
+
def __init__(self, config, hub):
|
|
148
|
+
self.config = config
|
|
149
|
+
self.hub = hub
|
|
150
|
+
self._lock = threading.RLock()
|
|
151
|
+
self.current: Optional[TestRun] = None
|
|
152
|
+
self._thread: Optional[threading.Thread] = None
|
|
153
|
+
self._runner: Optional[BenchRunner] = None
|
|
154
|
+
|
|
155
|
+
# ------------------------------------------------------------------
|
|
156
|
+
@property
|
|
157
|
+
def running(self) -> bool:
|
|
158
|
+
with self._lock:
|
|
159
|
+
return self.current is not None and self.current.status == "running"
|
|
160
|
+
|
|
161
|
+
def start(self, payload: dict) -> TestRun:
|
|
162
|
+
with self._lock:
|
|
163
|
+
if self.running:
|
|
164
|
+
raise RuntimeError("已有测试正在运行,请先停止。")
|
|
165
|
+
# sharegpt 数据集:未指定路径时自动从 modelscope 下载
|
|
166
|
+
dataset = payload.get("dataset", {})
|
|
167
|
+
if dataset.get("type") == DATASET_SHAREGPT and not dataset.get("path"):
|
|
168
|
+
from benchscope.datasets import ensure_sharegpt
|
|
169
|
+
|
|
170
|
+
path = str(ensure_sharegpt(self.config.datasets_dir))
|
|
171
|
+
dataset = dict(dataset)
|
|
172
|
+
dataset["path"] = path
|
|
173
|
+
payload = dict(payload)
|
|
174
|
+
payload["dataset"] = dataset
|
|
175
|
+
run = self._create_run(payload)
|
|
176
|
+
self.current = run
|
|
177
|
+
self._thread = threading.Thread(
|
|
178
|
+
target=self._execute, args=(run,), name="bench-run", daemon=True
|
|
179
|
+
)
|
|
180
|
+
self._thread.start()
|
|
181
|
+
return run
|
|
182
|
+
|
|
183
|
+
def stop(self) -> None:
|
|
184
|
+
runner = self._runner
|
|
185
|
+
if runner:
|
|
186
|
+
runner.kill()
|
|
187
|
+
|
|
188
|
+
def _create_run(self, payload: dict) -> TestRun:
|
|
189
|
+
framework = payload.get("framework", "vllm")
|
|
190
|
+
model = payload.get("model", "")
|
|
191
|
+
gpu = payload.get("gpu", {})
|
|
192
|
+
run_id = datetime.now().strftime("%m%d-%H%M%S")
|
|
193
|
+
run_dir = self.config.logs_dir / run_id
|
|
194
|
+
run_dir.mkdir(parents=True, exist_ok=True)
|
|
195
|
+
run = TestRun(
|
|
196
|
+
run_id=run_id,
|
|
197
|
+
run_dir=run_dir,
|
|
198
|
+
payload=payload,
|
|
199
|
+
framework=framework,
|
|
200
|
+
model=model,
|
|
201
|
+
gpu=gpu,
|
|
202
|
+
precision=payload.get("precision", ""),
|
|
203
|
+
started_at=datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
|
204
|
+
)
|
|
205
|
+
run.cases = build_cases(payload.get("dataset", {}), model)
|
|
206
|
+
return run
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------------------
|
|
209
|
+
def _execute(self, run: TestRun) -> None:
|
|
210
|
+
framework = run.framework
|
|
211
|
+
tmpl = (self.config.get("bench_commands") or {}).get(framework, "")
|
|
212
|
+
runner = BenchRunner(tmpl)
|
|
213
|
+
self._runner = runner
|
|
214
|
+
|
|
215
|
+
model_name = sanitize_name(Path(run.model).name or run.model)
|
|
216
|
+
gpu_count = run.gpu.get("count", "") if isinstance(run.gpu, dict) else run.gpu
|
|
217
|
+
gpu_label = f"{gpu_count}" if gpu_count else ""
|
|
218
|
+
if isinstance(run.gpu, dict) and run.gpu.get("name"):
|
|
219
|
+
gpu_label = f"{run.gpu['name']}×{gpu_count}" if gpu_count else run.gpu["name"]
|
|
220
|
+
|
|
221
|
+
meta = {
|
|
222
|
+
"model": run.model,
|
|
223
|
+
"model_name": model_name,
|
|
224
|
+
"framework": FRAMEWORK_NAMES.get(framework, framework),
|
|
225
|
+
"gpu": gpu_label,
|
|
226
|
+
"precision": run.precision,
|
|
227
|
+
}
|
|
228
|
+
mean_csv = run.run_dir / f"{model_name}_X{gpu_count}.log"
|
|
229
|
+
p99_csv = run.run_dir / f"{model_name}_X{gpu_count}_p99.log"
|
|
230
|
+
|
|
231
|
+
try:
|
|
232
|
+
self.hub.broadcast({"type": "run_started", "run": run.snapshot()})
|
|
233
|
+
cases_done = 0
|
|
234
|
+
for case in run.cases:
|
|
235
|
+
if runner._stop_flag.is_set():
|
|
236
|
+
break
|
|
237
|
+
case_label = case["label"]
|
|
238
|
+
detail_path = run.run_dir / f"{model_name}_{case_label}_X{gpu_count}.log"
|
|
239
|
+
detail_fp = open(detail_path, "a", encoding="utf-8")
|
|
240
|
+
concurrency_ok = 0
|
|
241
|
+
for conc in run.payload.get("concurrency_list", []):
|
|
242
|
+
if runner._stop_flag.is_set():
|
|
243
|
+
break
|
|
244
|
+
if conc == "inf" or conc is None:
|
|
245
|
+
continue
|
|
246
|
+
conc = int(conc)
|
|
247
|
+
try:
|
|
248
|
+
row = self._run_one(runner, run, case, conc, detail_fp, meta)
|
|
249
|
+
run.rows.append(row)
|
|
250
|
+
concurrency_ok += 1
|
|
251
|
+
# 增量写汇总 CSV
|
|
252
|
+
write_summary_csv(
|
|
253
|
+
mean_csv, [row], p99=False, append=True, case_header=concurrency_ok == 1, case=case, meta=meta
|
|
254
|
+
)
|
|
255
|
+
write_summary_csv(
|
|
256
|
+
p99_csv, [row], p99=True, append=True, case_header=concurrency_ok == 1, case=case, meta=meta
|
|
257
|
+
)
|
|
258
|
+
self.hub.broadcast({"type": "result", "run_id": run.run_id, "row": row})
|
|
259
|
+
except StopRequested:
|
|
260
|
+
break
|
|
261
|
+
except Exception as e:
|
|
262
|
+
log.exception("并发 %s 执行失败", conc)
|
|
263
|
+
err_row = {
|
|
264
|
+
"case": case_label, "label": case_label,
|
|
265
|
+
"input_len": case.get("input_len"), "output_len": case.get("output_len"),
|
|
266
|
+
"concurrency": conc, "error": str(e)[:500],
|
|
267
|
+
}
|
|
268
|
+
run.rows.append(err_row)
|
|
269
|
+
self.hub.broadcast({"type": "result", "run_id": run.run_id, "row": err_row})
|
|
270
|
+
detail_fp.close()
|
|
271
|
+
cases_done += 1
|
|
272
|
+
|
|
273
|
+
# 汇总 xlsx
|
|
274
|
+
if run.rows:
|
|
275
|
+
rows_for_xlsx = [r for r in run.rows if "metrics" in r]
|
|
276
|
+
if rows_for_xlsx:
|
|
277
|
+
annotated = self._annotate_best(rows_for_xlsx, run.payload.get("tpot_threshold_ms"))
|
|
278
|
+
xlsx_path = run.run_dir / f"benchmark-{datetime.now().strftime('%d%m%y')}.xlsx"
|
|
279
|
+
write_xlsx(xlsx_path, annotated, meta)
|
|
280
|
+
run.summary = {"xlsx": str(xlsx_path), "rows": len(rows_for_xlsx)}
|
|
281
|
+
|
|
282
|
+
run.status = "stopped" if runner._stop_flag.is_set() else "done"
|
|
283
|
+
run.finished_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
284
|
+
self._save_run_json(run)
|
|
285
|
+
self.hub.broadcast({"type": "run_done", "run_id": run.run_id, "run": run.snapshot()})
|
|
286
|
+
except StopRequested:
|
|
287
|
+
run.status = "stopped"
|
|
288
|
+
run.finished_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
289
|
+
self._save_run_json(run)
|
|
290
|
+
self.hub.broadcast({"type": "run_done", "run_id": run.run_id, "run": run.snapshot()})
|
|
291
|
+
except Exception as e:
|
|
292
|
+
log.exception("测试执行失败")
|
|
293
|
+
run.status = "error"
|
|
294
|
+
run.error = str(e)
|
|
295
|
+
run.finished_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
|
296
|
+
self._save_run_json(run)
|
|
297
|
+
self.hub.broadcast({"type": "run_error", "run_id": run.run_id, "error": str(e), "run": run.snapshot()})
|
|
298
|
+
finally:
|
|
299
|
+
self._runner = None
|
|
300
|
+
|
|
301
|
+
def _run_one(self, runner: BenchRunner, run: TestRun, case: dict,
|
|
302
|
+
concurrency: int, detail_fp, meta: dict) -> dict:
|
|
303
|
+
ds = dict(run.payload.get("dataset", {}))
|
|
304
|
+
ds.update({
|
|
305
|
+
"input_len": case.get("input_len"),
|
|
306
|
+
"output_len": case.get("output_len"),
|
|
307
|
+
"path": case.get("path"),
|
|
308
|
+
})
|
|
309
|
+
cmd = build_single_command(
|
|
310
|
+
run.framework, run.model, run.payload.get("tokenizer", ""),
|
|
311
|
+
dict(self.config.api), ds, concurrency,
|
|
312
|
+
run.payload.get("request_rate", "inf"),
|
|
313
|
+
run.payload.get("curated", {}), run.payload.get("extra_args", []),
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
def stream(line: str):
|
|
317
|
+
detail_fp.write(line)
|
|
318
|
+
self.hub.broadcast({
|
|
319
|
+
"type": "log_line", "run_id": run.run_id,
|
|
320
|
+
"case": case["label"], "concurrency": concurrency, "line": line,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
metrics = runner.run(cmd, stream_cb=stream)
|
|
324
|
+
return {
|
|
325
|
+
"case": case["label"], "label": case["label"],
|
|
326
|
+
"input_len": case.get("input_len"), "output_len": case.get("output_len"),
|
|
327
|
+
"concurrency": concurrency,
|
|
328
|
+
"cmd": " ".join(cmd),
|
|
329
|
+
"metrics": metrics,
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
def _annotate_best(self, rows: list[dict], threshold) -> list[dict]:
|
|
333
|
+
"""为每个用例标记最接近且低于阈值的 TPOT 行为最佳行。"""
|
|
334
|
+
if threshold is None:
|
|
335
|
+
return rows
|
|
336
|
+
try:
|
|
337
|
+
threshold = float(threshold)
|
|
338
|
+
except (TypeError, ValueError):
|
|
339
|
+
return rows
|
|
340
|
+
by_case: dict = {}
|
|
341
|
+
for r in rows:
|
|
342
|
+
by_case.setdefault(r.get("label"), []).append(r)
|
|
343
|
+
for label, items in by_case.items():
|
|
344
|
+
valid = []
|
|
345
|
+
for r in items:
|
|
346
|
+
m = r.get("metrics", {})
|
|
347
|
+
tpot = m.get("tpot_mean")
|
|
348
|
+
if tpot is not None:
|
|
349
|
+
valid.append((float(tpot), r))
|
|
350
|
+
if not valid:
|
|
351
|
+
continue
|
|
352
|
+
below = [(t, r) for t, r in valid if t < threshold]
|
|
353
|
+
if below:
|
|
354
|
+
best_t, best_r = max(below, key=lambda x: x[0]) # 最接近阈值(从下方)
|
|
355
|
+
else:
|
|
356
|
+
best_t, best_r = min(valid, key=lambda x: x[0]) # 无低于阈值则取最小
|
|
357
|
+
best_r["best"] = True
|
|
358
|
+
best_r["best_tpot"] = best_t
|
|
359
|
+
return rows
|
|
360
|
+
|
|
361
|
+
def _save_run_json(self, run: TestRun) -> None:
|
|
362
|
+
try:
|
|
363
|
+
(run.run_dir / "run.json").write_text(
|
|
364
|
+
json.dumps(run.snapshot(), ensure_ascii=False, indent=2), encoding="utf-8"
|
|
365
|
+
)
|
|
366
|
+
except Exception:
|
|
367
|
+
log.exception("保存 run.json 失败")
|
benchscope/server/ws.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""WebSocket 客户端广播。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import logging
|
|
7
|
+
import threading
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from fastapi import WebSocket
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("benchscope.ws")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class WebSocketHub:
|
|
16
|
+
"""向所有已连接的前端客户端广播 JSON 消息(线程安全,可在后台线程调用)。"""
|
|
17
|
+
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self._clients: set[tuple[WebSocket, asyncio.AbstractEventLoop]] = set()
|
|
20
|
+
self._lock = threading.Lock()
|
|
21
|
+
|
|
22
|
+
def register(self, ws: WebSocket, loop: asyncio.AbstractEventLoop) -> None:
|
|
23
|
+
with self._lock:
|
|
24
|
+
self._clients.add((ws, loop))
|
|
25
|
+
|
|
26
|
+
def unregister(self, ws: WebSocket) -> None:
|
|
27
|
+
with self._lock:
|
|
28
|
+
self._clients = {(w, l) for w, l in self._clients if w is not ws}
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def count(self) -> int:
|
|
32
|
+
with self._lock:
|
|
33
|
+
return len(self._clients)
|
|
34
|
+
|
|
35
|
+
def broadcast(self, payload: dict[str, Any]) -> None:
|
|
36
|
+
try:
|
|
37
|
+
msg = json.dumps(payload, ensure_ascii=False, default=str)
|
|
38
|
+
except TypeError:
|
|
39
|
+
log.exception("WS 广播序列化失败")
|
|
40
|
+
return
|
|
41
|
+
with self._lock:
|
|
42
|
+
clients = list(self._clients)
|
|
43
|
+
for ws, loop in clients:
|
|
44
|
+
try:
|
|
45
|
+
fut = asyncio.run_coroutine_threadsafe(ws.send_text(msg), loop)
|
|
46
|
+
fut.add_done_callback(self._on_sent(ws))
|
|
47
|
+
except Exception:
|
|
48
|
+
self.unregister(ws)
|
|
49
|
+
|
|
50
|
+
def _on_sent(self, ws: WebSocket):
|
|
51
|
+
def _cb(fut: asyncio.Future):
|
|
52
|
+
try:
|
|
53
|
+
fut.result()
|
|
54
|
+
except Exception:
|
|
55
|
+
self.unregister(ws)
|
|
56
|
+
return _cb
|
benchscope/summary.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""日志汇总:CSV 汇总日志与 benchmark-*.xlsx 生成(mean / P99 双面板)。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Iterable
|
|
7
|
+
|
|
8
|
+
from openpyxl import Workbook
|
|
9
|
+
from openpyxl.styles import Alignment, Font, PatternFill
|
|
10
|
+
|
|
11
|
+
log = logging.getLogger("benchscope.summary")
|
|
12
|
+
|
|
13
|
+
# xlsx 列定义(与 asserts/benchmark-260821.xlsx 对齐,末尾追加 单用户)
|
|
14
|
+
XLSX_HEADERS = [
|
|
15
|
+
"GPU", "模型", "精度", "推理框架", "输入长度", "输出长度", "并发数",
|
|
16
|
+
"output", "peakoutput", "total", "ttft", "itl", "tpot", "单用户",
|
|
17
|
+
]
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _fmt(value, digits: int = 2) -> str:
|
|
21
|
+
if value is None or value == "":
|
|
22
|
+
return ""
|
|
23
|
+
try:
|
|
24
|
+
return f"{float(value):.{digits}f}"
|
|
25
|
+
except (TypeError, ValueError):
|
|
26
|
+
return str(value)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def write_summary_csv(
|
|
30
|
+
path: Path,
|
|
31
|
+
rows: Iterable[dict],
|
|
32
|
+
p99: bool = False,
|
|
33
|
+
append: bool = False,
|
|
34
|
+
case_header: bool = False,
|
|
35
|
+
case: dict | None = None,
|
|
36
|
+
meta: dict | None = None,
|
|
37
|
+
) -> Path:
|
|
38
|
+
"""按用例分组写汇总 CSV(兼容 asserts/logs 旧格式)。
|
|
39
|
+
|
|
40
|
+
rows: [{case, label, input_len, output_len, concurrency, metrics}]
|
|
41
|
+
p99=False 时取 mean 指标;p99=True 时取 P99 指标。
|
|
42
|
+
append=True 时为增量追加模式(配合 case_header 控制块头写入)。
|
|
43
|
+
"""
|
|
44
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
suffix = "_p99" if p99 else ""
|
|
46
|
+
key = "p99" if p99 else "mean"
|
|
47
|
+
|
|
48
|
+
def metric(r, name):
|
|
49
|
+
m = r.get("metrics", {})
|
|
50
|
+
return m.get(f"{name}_{key}", m.get(name, ""))
|
|
51
|
+
|
|
52
|
+
rows = list(rows)
|
|
53
|
+
if not append:
|
|
54
|
+
with open(path, "w", encoding="utf-8") as f:
|
|
55
|
+
pass # 清空
|
|
56
|
+
|
|
57
|
+
with open(path, "a", encoding="utf-8") as f:
|
|
58
|
+
if append:
|
|
59
|
+
if case_header and case:
|
|
60
|
+
gpu_count = (meta or {}).get("gpu", "") or ""
|
|
61
|
+
f.write("=" * 60 + "\n")
|
|
62
|
+
f.write(
|
|
63
|
+
f"测试条件:{case.get('label')} | 输入={case.get('input_len')} | "
|
|
64
|
+
f"输出={case.get('output_len')} | 部署GPU={gpu_count}\n"
|
|
65
|
+
)
|
|
66
|
+
f.write("=" * 60 + "\n")
|
|
67
|
+
f.write("并发数,Output Token,Peak Output Token,Total Token,TTFT,TPOT,ITL\n")
|
|
68
|
+
for r in rows:
|
|
69
|
+
f.write(
|
|
70
|
+
f"{r.get('concurrency')},{metric(r, 'output')},{metric(r, 'peakoutput')},"
|
|
71
|
+
f"{metric(r, 'total')},{metric(r, 'ttft')},{metric(r, 'tpot')},{metric(r, 'itl')}\n"
|
|
72
|
+
)
|
|
73
|
+
return path
|
|
74
|
+
|
|
75
|
+
# 全量模式:按用例分组
|
|
76
|
+
groups: dict = {}
|
|
77
|
+
for r in rows:
|
|
78
|
+
groups.setdefault(r.get("label", ""), []).append(r)
|
|
79
|
+
for label, items in groups.items():
|
|
80
|
+
first = items[0]
|
|
81
|
+
f.write("=" * 60 + "\n")
|
|
82
|
+
f.write(
|
|
83
|
+
f"测试条件:{label} | 输入={first.get('input_len')} | "
|
|
84
|
+
f"输出={first.get('output_len')} | 部署GPU={(meta or {}).get('gpu', '')}\n"
|
|
85
|
+
)
|
|
86
|
+
f.write("=" * 60 + "\n")
|
|
87
|
+
f.write("并发数,Output Token,Peak Output Token,Total Token,TTFT,TPOT,ITL\n")
|
|
88
|
+
for r in items:
|
|
89
|
+
f.write(
|
|
90
|
+
f"{r.get('concurrency')},{metric(r, 'output')},{metric(r, 'peakoutput')},"
|
|
91
|
+
f"{metric(r, 'total')},{metric(r, 'ttft')},{metric(r, 'tpot')},{metric(r, 'itl')}\n"
|
|
92
|
+
)
|
|
93
|
+
f.write("\n")
|
|
94
|
+
return path
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def write_xlsx(path: Path, rows: Iterable[dict], meta: dict) -> Path:
|
|
98
|
+
"""生成 benchmark-*.xlsx,含 均值 与 P99 两个 sheet。"""
|
|
99
|
+
rows = list(rows)
|
|
100
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
101
|
+
wb = Workbook()
|
|
102
|
+
wb.remove(wb.active)
|
|
103
|
+
|
|
104
|
+
header_font = Font(bold=True, color="FFFFFF")
|
|
105
|
+
header_fill = PatternFill("solid", fgColor="409EFF")
|
|
106
|
+
best_fill = PatternFill("solid", fgColor="FFF3CD")
|
|
107
|
+
|
|
108
|
+
for sheet_name, key in (("均值 Mean", "mean"), ("P99", "p99")):
|
|
109
|
+
ws = wb.create_sheet(sheet_name)
|
|
110
|
+
ws.append(XLSX_HEADERS)
|
|
111
|
+
for cell in ws[1]:
|
|
112
|
+
cell.font = header_font
|
|
113
|
+
cell.fill = header_fill
|
|
114
|
+
cell.alignment = Alignment(horizontal="center")
|
|
115
|
+
|
|
116
|
+
for r in rows:
|
|
117
|
+
m = r.get("metrics", {})
|
|
118
|
+
concurrency = r.get("concurrency")
|
|
119
|
+
# 最佳行(tpot 最接近且低于阈值)加亮
|
|
120
|
+
best = r.get("best", False)
|
|
121
|
+
values = [
|
|
122
|
+
meta.get("gpu", ""),
|
|
123
|
+
meta.get("model", ""),
|
|
124
|
+
meta.get("precision", ""),
|
|
125
|
+
meta.get("framework", ""),
|
|
126
|
+
r.get("input_len", ""),
|
|
127
|
+
r.get("output_len", ""),
|
|
128
|
+
concurrency,
|
|
129
|
+
_fmt(m.get(f"output_{key}", m.get("output_mean", m.get("output")))),
|
|
130
|
+
_fmt(m.get(f"peakoutput_{key}", m.get("peakoutput_mean", m.get("peakoutput")))),
|
|
131
|
+
_fmt(m.get(f"total_{key}", m.get("total_mean", m.get("total")))),
|
|
132
|
+
_fmt(m.get(f"ttft_{key}", m.get("ttft"))),
|
|
133
|
+
_fmt(m.get(f"itl_{key}", m.get("itl"))),
|
|
134
|
+
_fmt(m.get(f"tpot_{key}", m.get("tpot"))),
|
|
135
|
+
_fmt(m.get("single_user")),
|
|
136
|
+
]
|
|
137
|
+
ws.append(values)
|
|
138
|
+
if best:
|
|
139
|
+
for cell in ws[ws.max_row]:
|
|
140
|
+
cell.fill = best_fill
|
|
141
|
+
|
|
142
|
+
# 列宽
|
|
143
|
+
for col, _ in enumerate(XLSX_HEADERS, start=1):
|
|
144
|
+
ws.column_dimensions[chr(64 + col)].width = 14
|
|
145
|
+
ws.freeze_panes = "A2"
|
|
146
|
+
|
|
147
|
+
wb.save(path)
|
|
148
|
+
return path
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.record-list[data-v-85fc4f26]{padding:12px 8px;height:100%;display:flex;flex-direction:column}.record-head[data-v-85fc4f26]{display:flex;align-items:center;justify-content:space-between;margin-bottom:10px;padding:0 8px}.record-title[data-v-85fc4f26]{font-weight:600;font-size:14px}.record-selected[data-v-85fc4f26]{background:#e6f4ff}.run-detail[data-v-53d908f7]{padding:16px;height:100%;overflow:auto}.detail-meta[data-v-53d908f7]{margin-bottom:8px;border-radius:8px}.detail-tabs[data-v-53d908f7]{background:#fff;border:1px solid #f0f0f0;border-radius:8px;padding:0 12px 12px}.preview-pre[data-v-53d908f7]{max-height:520px;overflow:auto;background:#f6f8fa;padding:12px;font-size:12px;white-space:pre-wrap;margin:0}.page-layout[data-v-8fab23f1]{height:100%}.record-sider[data-v-8fab23f1]{border-right:1px solid #f0f0f0;height:100%;overflow:auto}.page-content[data-v-8fab23f1]{height:100%;background:#f5f5f5}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as E,b as Z,a as F}from"./index-Dfr0hp72.js";import{f as x,w as G,o as ee,S as c,U as b,a6 as S,X as w,V as L,W as a,G as v,_ as i,k as t,Y as te,c as O,F as X,a8 as D,r as Y}from"./vue-Ch4zjUb1.js";import{M as H,_ as ae}from"./MetricsCharts-DHO93JrC.js";import{j as W}from"./antd-DWALckI0.js";import"./echarts-Bb6yjXMn.js";const ne={class:"record-list"},oe={class:"record-head"},le={style:{"font-weight":"600","font-size":"13px"}},se={style:{"font-size":"12px","line-height":"1.6"}},re={__name:"RunRecordList",props:{modelValue:{type:String,default:""},framework:{type:String,default:""}},emits:["update:modelValue"],setup(p,{emit:d}){const u=p,r=d,l=Z(),o=O(()=>u.framework),m=x(u.framework||""),T=x([]),f=x(!1),B=O(()=>{const s=o.value||m.value;if(!s)return T.value;const k=s==="vllm"?"vLLM":"SGLang";return T.value.filter(g=>{var e;return(((e=g.meta)==null?void 0:e.framework)||"")===k})});function j(s){return s==="vllm"?"vLLM":"SGLang"}function K(s){return s==="done"?"green":s==="error"?"red":s==="stopped"?"orange":"blue"}function A(s){return s==="done"?"完成":s==="error"?"失败":s==="stopped"?"已停止":"运行中"}async function M(){f.value=!0;try{const s=await F.listRuns();T.value=s.runs||[]}finally{f.value=!1}}return G(B,s=>{if(!s.length){u.modelValue&&r("update:modelValue","");return}if(!(u.modelValue&&s.some(g=>g.run_id===u.modelValue))){const g=l.lastRunId&&s.some(e=>e.run_id===l.lastRunId)?l.lastRunId:s[0].run_id;r("update:modelValue",g)}},{immediate:!0}),G(()=>l.lastRunId,M),G(m,()=>{}),ee(M),(s,k)=>{const g=c("a-select"),e=c("a-tag"),n=c("a-list-item-meta"),C=c("a-list-item"),$=c("a-list"),R=c("a-spin");return b(),S("div",ne,[w("div",oe,[k[1]||(k[1]=w("span",{class:"record-title"},"测试记录",-1)),o.value?(b(),L(e,{key:1,size:"small",color:"blue"},{default:a(()=>[v(i(j(o.value)),1)]),_:1})):(b(),L(g,{key:0,value:m.value,"onUpdate:value":k[0]||(k[0]=_=>m.value=_),size:"small",style:{width:"110px"},options:[{value:"",label:"全部"},{value:"vllm",label:"vLLM"},{value:"sglang",label:"SGLang"}]},null,8,["value"]))]),t(R,{spinning:f.value},{default:a(()=>[t($,{size:"small",bordered:"","data-source":B.value},{renderItem:a(({item:_})=>[t(C,{style:{cursor:"pointer"},class:te({"record-selected":_.run_id===p.modelValue}),onClick:z=>s.$emit("update:modelValue",_.run_id)},{default:a(()=>[t(n,null,{title:a(()=>{var z;return[w("span",le,i(_.run_id),1),t(e,{size:"small",color:K((z=_.meta)==null?void 0:z.status),style:{"margin-left":"6px"}},{default:a(()=>{var h;return[v(i(A((h=_.meta)==null?void 0:h.status)),1)]}),_:2},1032,["color"])]}),description:a(()=>{var z,h,N;return[w("div",se,[w("div",null,i(((z=_.meta)==null?void 0:z.framework)||"-")+" · "+i(((h=_.meta)==null?void 0:h.model)||"-"),1),w("div",null,i(((N=_.meta)==null?void 0:N.started_at)||"")+" · "+i(_.files.length)+" 文件",1)])]}),_:2},1024)]),_:2},1032,["class","onClick"])]),empty:a(()=>[...k[2]||(k[2]=[w("div",{style:{padding:"12px",color:"#999"}},"暂无日志记录",-1)])]),_:1},8,["data-source"])]),_:1},8,["spinning"])])}}},ue=E(re,[["__scopeId","data-v-85fc4f26"]]),de={__name:"RunSummaryBlock",props:{records:{type:Array,default:()=>[]}},setup(p){const d=p,u=O(()=>d.records.map(l=>({label:l.label,concurrency:l.concurrency,metrics:{output_mean:l.output_mean,total_mean:l.total_mean,ttft_mean:l.ttft_mean,tpot_mean:l.tpot_mean,ttft_p99:l.ttft_p99,tpot_p99:l.tpot_p99}}))),r=[{key:"output_mean",label:"Output 吞吐 (tok/s)"},{key:"total_mean",label:"Total 吞吐 (tok/s)"},{key:"ttft_mean",label:"TTFT mean 耗时 (ms)"},{key:"tpot_mean",label:"TPOT mean 耗时 (ms)"},{key:"ttft_p99",label:"TTFT P99 耗时 (ms)"},{key:"tpot_p99",label:"TPOT P99 耗时 (ms)"}];return(l,o)=>(b(),S("div",null,[t(H,{rows:u.value,"metric-defs":r},null,8,["rows"])]))}},ie={style:{"margin-bottom":"8px",color:"#999","font-size":"12px"}},q={__name:"AnalysisBlock",props:{rows:{type:Array,default:()=>[]},best:{type:Object,default:()=>({})},threshold:{type:[Number,String],default:null}},setup(p){const d=p,u=[{key:"output_mean",label:"Output 吞吐 (tok/s)"},{key:"peakoutput_mean",label:"Peak Output 吞吐 (tok/s)"},{key:"total_mean",label:"Total 吞吐 (tok/s)"},{key:"ttft_mean",label:"TTFT mean 耗时 (ms)"},{key:"itl_mean",label:"ITL mean 耗时 (ms)"},{key:"tpot_mean",label:"TPOT mean 耗时 (ms)"}],r=O(()=>{const l={};for(const o of Object.keys(d.best||{})){const m=d.best[o];m&&m.row&&(l[`${o}-${m.concurrency}`]=!0)}return d.rows.map(o=>({label:o.label,concurrency:o.concurrency,best:!!l[`${o.label}-${o.concurrency}`],metrics:{output_mean:o.output,peakoutput_mean:o.peakoutput,total_mean:o.total,ttft_mean:o.ttft,tpot_mean:o.tpot,itl_mean:o.itl}}))});return(l,o)=>(b(),S("div",null,[w("div",ie,i(p.threshold?`TPOT 阈值 ${p.threshold}ms,金色行为最佳并发`:"未设置 TPOT 阈值"),1),t(ae,{rows:r.value,threshold:p.threshold,pagination:{pageSize:20,showSizeChanger:!0}},null,8,["rows","threshold"]),o[0]||(o[0]=w("div",{style:{"margin-top":"12px","font-weight":"600"}},"曲线(横轴:并发数)",-1)),t(H,{rows:r.value,"metric-defs":u},null,8,["rows"])]))}},ce={class:"run-detail"},me={style:{color:"#999","font-size":"12px","margin-bottom":"6px"}},pe={class:"preview-pre"},_e={__name:"RunDetailPanel",props:{runId:{type:String,default:""}},setup(p){const d=p,u=Y({records:[],records_mean:[],records_p99:[],best_mean:{},best_p99:{},threshold:null}),r=x({}),l=x([]),o=x("summary"),m=x(!1),T=x(""),f=Y({content:"",total_lines:0,truncated:0}),B=[{title:"文件名",dataIndex:"name",key:"name"},{title:"大小",dataIndex:"size",key:"size",width:110,customRender:({text:e})=>K(e)},{title:"操作",key:"actions",width:140}],j=O(()=>{var e;return((e=l.value.find(n=>n.name.endsWith(".xlsx")))==null?void 0:e.name)||""});function K(e){return e==null?"-":e>1024*1024?(e/1024/1024).toFixed(1)+" MB":e>1024?(e/1024).toFixed(1)+" KB":e+" B"}function A(e){return e==="done"?"green":e==="error"?"red":e==="stopped"?"orange":"blue"}function M(e){return e==="done"?"完成":e==="error"?"失败":e==="stopped"?"已停止":"运行中"}async function s(){if(d.runId)try{const[e,n]=await Promise.all([F.runSummary(d.runId),F.getRun(d.runId).catch(()=>null)]);Object.assign(u,e),r.value=e.meta||{},l.value=((n==null?void 0:n.files)||[]).map(C=>({name:C[0],size:C[1]}))}catch(e){W.error(e.message)}}async function k(e){T.value=e,f.content="";try{const n=await F.previewFile(d.runId,e);f.content=n.content,f.total_lines=n.total_lines,f.truncated=n.truncated,m.value=!0}catch(n){W.error(n.message)}}function g(e){if(!e){W.warning("该运行暂无 xlsx 汇总(可能没有成功记录)");return}window.open(F.downloadUrl(d.runId,e),"_blank")}return G(()=>d.runId,()=>s(),{immediate:!0}),(e,n)=>{const C=c("a-empty"),$=c("a-tag"),R=c("a-button"),_=c("a-space"),z=c("a-card"),h=c("a-tab-pane"),N=c("a-table"),J=c("a-modal"),Q=c("a-tabs");return b(),S("div",ce,[p.runId?(b(),S(X,{key:1},[t(z,{size:"small",class:"detail-meta",bordered:!0},{default:a(()=>[t(_,{wrap:""},{default:a(()=>{var I,P,U;return[t($,{color:"blue"},{default:a(()=>{var y,V;return[v(i(((y=u.meta)==null?void 0:y.framework_name)||((V=r.value)==null?void 0:V.framework)||"-"),1)]}),_:1}),t($,null,{default:a(()=>{var y,V;return[v(i(((y=u.meta)==null?void 0:y.model)||((V=r.value)==null?void 0:V.model)||"-"),1)]}),_:1}),(I=r.value)!=null&&I.gpu?(b(),L($,{key:0},{default:a(()=>[v("GPU: "+i(r.value.gpu),1)]),_:1})):D("",!0),(P=r.value)!=null&&P.precision?(b(),L($,{key:1},{default:a(()=>[v("精度: "+i(r.value.precision),1)]),_:1})):D("",!0),t($,null,{default:a(()=>{var y,V;return[v(i(((y=r.value)==null?void 0:y.started_at)||"")+" → "+i(((V=r.value)==null?void 0:V.finished_at)||""),1)]}),_:1}),t($,{color:A((U=r.value)==null?void 0:U.status)},{default:a(()=>{var y;return[v(i(M((y=r.value)==null?void 0:y.status)),1)]}),_:1},8,["color"]),t(R,{size:"small",type:"primary",ghost:"",onClick:n[0]||(n[0]=y=>g(j.value))},{default:a(()=>[...n[3]||(n[3]=[v("下载 xlsx 汇总",-1)])]),_:1}),t(R,{size:"small",onClick:s},{default:a(()=>[...n[4]||(n[4]=[v("刷新",-1)])]),_:1})]}),_:1})]),_:1}),t(Q,{activeKey:o.value,"onUpdate:activeKey":n[2]||(n[2]=I=>o.value=I),class:"detail-tabs"},{default:a(()=>[t(h,{key:"summary",tab:"指标汇总"},{default:a(()=>[t(de,{records:u.records||[]},null,8,["records"])]),_:1}),t(h,{key:"mean",tab:"均值分析 Mean"},{default:a(()=>[t(q,{rows:u.records_mean||[],best:u.best_mean||{},threshold:u.threshold},null,8,["rows","best","threshold"])]),_:1}),t(h,{key:"p99",tab:"P99 分析"},{default:a(()=>[t(q,{rows:u.records_p99||[],best:u.best_p99||{},threshold:u.threshold},null,8,["rows","best","threshold"])]),_:1}),t(h,{key:"files",tab:"日志文件"},{default:a(()=>[t(N,{columns:B,"data-source":l.value,size:"small",pagination:!1,"row-key":"name"},{bodyCell:a(({column:I,record:P})=>[I.key==="actions"?(b(),S(X,{key:0},[t(R,{size:"small",type:"link",onClick:U=>k(P.name)},{default:a(()=>[...n[5]||(n[5]=[v("预览",-1)])]),_:1},8,["onClick"]),t(R,{size:"small",type:"link",onClick:U=>g(P.name)},{default:a(()=>[...n[6]||(n[6]=[v("下载",-1)])]),_:1},8,["onClick"])],64)):D("",!0)]),_:1},8,["data-source"]),t(J,{open:m.value,"onUpdate:open":n[1]||(n[1]=I=>m.value=I),title:`预览:${T.value}`,width:"900px",footer:null},{default:a(()=>[w("div",me," 共 "+i(f.total_lines)+" 行,显示末尾 "+i(f.truncated>0?`(省略前 ${f.truncated} 行)`:"全部"),1),w("pre",pe,i(f.content),1)]),_:1},8,["open","title"])]),_:1})]),_:1},8,["activeKey"])],64)):(b(),L(C,{key:0,description:"请选择左侧测试记录",style:{"padding-top":"80px"}}))])}}},fe=E(_e,[["__scopeId","data-v-53d908f7"]]),ye={__name:"LogView",setup(p){const d=x("");return(u,r)=>{const l=c("a-layout-sider"),o=c("a-layout-content"),m=c("a-layout");return b(),L(m,{class:"page-layout"},{default:a(()=>[t(l,{width:"260",theme:"light",class:"record-sider"},{default:a(()=>[t(ue,{modelValue:d.value,"onUpdate:modelValue":r[0]||(r[0]=T=>d.value=T),framework:""},null,8,["modelValue"])]),_:1}),t(o,{class:"page-content"},{default:a(()=>[t(fe,{"run-id":d.value},null,8,["run-id"])]),_:1})]),_:1})}}},he=E(ye,[["__scopeId","data-v-8fab23f1"]]);export{he as default};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.row-near-threshold>td{background-color:#e6fffb!important}.row-best>td{background-color:#fffbe6!important;font-weight:600}.row-error>td{background-color:#fff1f0!important}.chart-title[data-v-99455d6f]{font-size:13px;color:#333;font-weight:600;margin-bottom:4px;padding-left:4px}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{S as g,U as a,V as p,W as y,a6 as b,_ as k,F as T,G as x,a8 as v,c as R,w as z,o as C,j as O,a7 as N,X as S,a9 as I}from"./vue-Ch4zjUb1.js";import{i as P}from"./echarts-Bb6yjXMn.js";import{_ as F}from"./index-Dfr0hp72.js";const L={key:0,style:{fontWeight:600}},A={key:0,style:{color:"#999","font-size":"12px"}},E={__name:"MetricsTable",props:{rows:{type:Array,default:()=>[]},threshold:{type:Number,default:null},pagination:{type:[Boolean,Object],default:!1}},setup(d){const m=d,c=R(()=>[{title:"用例 Case",key:"label",width:180,fixed:"left"},{title:"并发数 Concurrency",key:"concurrency",width:110,fixed:"left"},{title:"Output 吞吐 Output tok/s",dataIndex:["metrics","output_mean"],key:"output",width:130,customRender:s},{title:"Peak Output 吞吐 Peak tok/s",dataIndex:["metrics","peakoutput_mean"],key:"peak",width:140,customRender:s},{title:"Total 吞吐 Total tok/s",dataIndex:["metrics","total_mean"],key:"total",width:130,customRender:s},{title:"TTFT mean (ms)",dataIndex:["metrics","ttft_mean"],key:"ttft_mean",width:120,customRender:s},{title:"TPOT mean (ms)",dataIndex:["metrics","tpot_mean"],key:"tpot_mean",width:120,customRender:s},{title:"ITL mean (ms)",dataIndex:["metrics","itl_mean"],key:"itl_mean",width:120,customRender:s},{title:"TTFT P99 (ms)",dataIndex:["metrics","ttft_p99"],key:"ttft_p99",width:120,customRender:s},{title:"TPOT P99 (ms)",dataIndex:["metrics","tpot_p99"],key:"tpot_p99",width:120,customRender:s},{title:"ITL P99 (ms)",dataIndex:["metrics","itl_p99"],key:"itl_p99",width:120,customRender:s},{title:"单用户 QPS (1000/TPOT)",key:"single_user",width:150,customRender:w},{title:"状态 Status",key:"status",width:90,fixed:"right"}]);function s({text:n}){return n==null?"-":Number(n).toFixed(2)}function w({record:n}){var o;const e=(o=n.metrics)==null?void 0:o.single_user;return e==null?"-":Number(e).toFixed(2)}function h(n){return`${n.label||n.case||""}-${n.concurrency}`}function r(n){var o;if(n.error)return"row-error";if(n.best)return"row-best";const e=(o=n.metrics)==null?void 0:o.tpot_mean;return e!=null&&m.threshold&&Number(e)<Number(m.threshold)?"row-near-threshold":""}return(n,e)=>{const o=g("a-tag"),u=g("a-table");return a(),p(u,{columns:c.value,"data-source":d.rows,"row-key":h,pagination:d.pagination,size:"small",scroll:{x:1400},"row-class-name":r,bordered:""},{bodyCell:y(({column:i,record:t})=>[i.key==="concurrency"?(a(),b("span",L,k(t.concurrency),1)):i.key==="status"?(a(),b(T,{key:1},[t.error?(a(),p(o,{key:0,color:"red"},{default:y(()=>[...e[0]||(e[0]=[x("失败",-1)])]),_:1})):t.best?(a(),p(o,{key:1,color:"gold"},{default:y(()=>[...e[1]||(e[1]=[x("最佳",-1)])]),_:1})):(a(),p(o,{key:2,color:"green"},{default:y(()=>[...e[2]||(e[2]=[x("成功",-1)])]),_:1}))],64)):i.key==="label"?(a(),b(T,{key:2},[x(k(t.label)+" ",1),t.input_len?(a(),b("span",A," ("+k(t.input_len)+"/"+k(t.output_len)+") ",1)):v("",!0)],64)):v("",!0)]),_:1},8,["columns","data-source","pagination"])}}},B={class:"chart-title"},D="260px",M={__name:"MetricsCharts",props:{rows:{type:Array,default:()=>[]},metricDefs:{type:Array,default:()=>[]},height:{type:String,default:"300px"}},setup(d){const m=d,c={},s=[];function w(r,n){n&&(c[r]||(c[r]=P(n),s.push(new ResizeObserver(()=>c[r]&&c[r].resize())),s[s.length-1].observe(n)))}function h(){for(const r of m.metricDefs){const n=c[r.key];if(!n)continue;const e={};for(const t of m.rows){const l=t.metrics||{},f=r.value?r.value(l):l[r.key];if(f==null)continue;const _=t.label||t.case||"unknown";e[_]||(e[_]=[]),e[_].push({concurrency:t.concurrency,value:Number(f)})}const o=Object.keys(e),u={color:["#1677ff","#52c41a","#faad14","#f5222d","#13c2c2","#722ed1","#eb2f96","#fa8c16"],tooltip:{trigger:"axis",backgroundColor:"rgba(255,255,255,0.96)",borderColor:"#f0f0f0",textStyle:{color:"rgba(0,0,0,0.88)",fontSize:12},valueFormatter:t=>t==null?"-":Number(t).toFixed(2)},legend:o.length>1?{data:o,type:"scroll",top:0,textStyle:{fontSize:12}}:void 0,grid:{left:54,right:16,top:o.length>1?32:20,bottom:28},xAxis:{type:"category",name:"并发数",nameTextStyle:{fontSize:11,color:"rgba(0,0,0,0.45)"},axisLine:{lineStyle:{color:"#d9d9d9"}},axisLabel:{fontSize:11}},yAxis:{type:"value",scale:!0,splitLine:{lineStyle:{type:"dashed",color:"#f0f0f0"}},axisLabel:{fontSize:11}},series:o.map(t=>{const l=e[t].sort((f,_)=>f.concurrency-_.concurrency);return{name:t,type:"line",smooth:!0,symbol:"circle",symbolSize:6,lineStyle:{width:2},data:l.map(f=>f.value),markPoint:{data:[{type:"max",name:"峰值"}],symbolSize:44,label:{fontSize:10}}}})},i=[];for(const t of o)for(const l of e[t])i.includes(l.concurrency)||i.push(l.concurrency);u.xAxis.data=i.sort((t,l)=>t-l),n.setOption(u,!0)}}return z(()=>m.rows,h,{deep:!0}),z(()=>m.metricDefs,h,{deep:!0}),C(h),O(()=>{s.forEach(r=>r.disconnect()),Object.values(c).forEach(r=>r.dispose())}),(r,n)=>{const e=g("a-col"),o=g("a-row");return a(),p(o,{gutter:12},{default:y(()=>[(a(!0),b(T,null,N(d.metricDefs,u=>(a(),p(e,{key:u.key,xs:24,sm:12,lg:8,style:{"margin-bottom":"12px"}},{default:y(()=>[S("div",{style:I({height:d.height,width:"100%"})},[S("div",B,k(u.label),1),S("div",{ref_for:!0,ref:i=>w(u.key,i),style:I({height:D,width:"100%"})},null,4)],4)]),_:2},1024))),128))]),_:1})}}},U=F(M,[["__scopeId","data-v-99455d6f"]]);export{U as M,E as _};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
.panel[data-v-a3dbcdbc]{max-width:1200px;margin:0 auto;border-radius:8px;box-shadow:0 1px 2px #0000000a}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as F,u as H,a as K}from"./index-Dfr0hp72.js";import{i as j,t as W,u as Q,v as Y,j as I}from"./antd-DWALckI0.js";import{o as Z,V as ee,W as t,f as k,r as ae,S as r,U as h,k as e,u as w,G as p,a6 as V,_ as x,a8 as E,X as te}from"./vue-Ch4zjUb1.js";const le={key:0,style:{"margin-right":"12px"}},ne={key:0},oe={key:1,style:{color:"#999"}},se={key:0,style:{color:"#999","font-size":"12px","margin-top":"4px"}},ue={__name:"SettingsView",setup(re){const o=H(),A=k(!1),B=k(!1),_=k(null),U=k(null),l=ae({framework:"vllm",api:{base_url:"",endpoint:"/v1/chat/completions",api_key:"",extra_headers:{}},gpu:{auto:!0,name:"",count:8},logs_dir:"./logs",datasets_dir:"./datasets",tpot_threshold_ms:100,request_rate:"inf",bench_commands:{vllm:"vllm bench serve",sglang:"python -m sglang.bench_serving"}}),v=k("{}");Z(async()=>{var d,a,g,m,c,S,s,u,i,f,O,C,N,b,y,q,L,P,G,n,J,z,D,T,M,R;A.value=!0;try{await o.load(),Object.assign(l,{framework:((d=o.config)==null?void 0:d.framework)||"vllm",api:{base_url:((g=(a=o.config)==null?void 0:a.api)==null?void 0:g.base_url)||"",endpoint:((c=(m=o.config)==null?void 0:m.api)==null?void 0:c.endpoint)||"/v1/chat/completions",api_key:((s=(S=o.config)==null?void 0:S.api)==null?void 0:s.api_key)||"",extra_headers:((i=(u=o.config)==null?void 0:u.api)==null?void 0:i.extra_headers)||{}},gpu:{auto:((O=(f=o.config)==null?void 0:f.gpu)==null?void 0:O.auto)??!0,name:((N=(C=o.config)==null?void 0:C.gpu)==null?void 0:N.name)||"",count:((y=(b=o.config)==null?void 0:b.gpu)==null?void 0:y.count)||8},logs_dir:((q=o.config)==null?void 0:q.logs_dir)||"./logs",datasets_dir:((L=o.config)==null?void 0:L.datasets_dir)||"./datasets",tpot_threshold_ms:((P=o.config)==null?void 0:P.tpot_threshold_ms)??100,request_rate:((G=o.config)==null?void 0:G.request_rate)||"inf",bench_commands:{vllm:((J=(n=o.config)==null?void 0:n.bench_commands)==null?void 0:J.vllm)||"vllm bench serve",sglang:((D=(z=o.config)==null?void 0:z.bench_commands)==null?void 0:D.sglang)||"python -m sglang.bench_serving"}}),v.value=JSON.stringify(((M=(T=o.config)==null?void 0:T.api)==null?void 0:M.extra_headers)||{},null,2),(R=o.gpu)!=null&&R.auto_detected&&(U.value=o.gpu.auto_detected)}finally{A.value=!1}});async function X(){B.value=!0,_.value=null;try{let d={};try{d=JSON.parse(v.value||"{}")}catch{I.error("额外请求头不是合法 JSON");return}_.value=await K.testConnection({base_url:l.api.base_url,endpoint:l.api.endpoint,api_key:l.api.api_key,extra_headers:d})}finally{B.value=!1}}async function $(){try{let d={};try{d=JSON.parse(v.value||"{}")}catch{I.error("额外请求头不是合法 JSON");return}const a={framework:l.framework,api:{...l.api,extra_headers:d},gpu:l.gpu,logs_dir:l.logs_dir,datasets_dir:l.datasets_dir,tpot_threshold_ms:l.tpot_threshold_ms,request_rate:l.request_rate,bench_commands:l.bench_commands};await o.save(a),I.success("配置已保存"),o.refreshStatus()}catch(d){I.error(`保存失败:${d.message}`)}}return(d,a)=>{const g=r("a-button"),m=r("a-divider"),c=r("a-radio-button"),S=r("a-radio-group"),s=r("a-form-item"),u=r("a-col"),i=r("a-input"),f=r("a-row"),O=r("a-input-password"),C=r("a-tag"),N=r("a-switch"),b=r("a-input-number"),y=r("a-select-option"),q=r("a-select"),L=r("a-form"),P=r("a-spin"),G=r("a-card");return h(),ee(G,{size:"small",class:"panel",title:"服务设置 Service Settings"},{extra:t(()=>[e(g,{type:"link",size:"small",loading:B.value,onClick:X},{icon:t(()=>[e(w(j))]),default:t(()=>[a[14]||(a[14]=p(" 测试连接 ",-1))]),_:1},8,["loading"])]),default:t(()=>[e(P,{spinning:A.value},{default:t(()=>[e(L,{layout:"vertical"},{default:t(()=>[e(m,{orientation:"left"},{default:t(()=>[e(w(j),{style:{color:"#1677ff"}}),a[15]||(a[15]=p(" 推理服务 API 配置 ",-1))]),_:1}),e(f,{gutter:16},{default:t(()=>[e(u,{span:8},{default:t(()=>[e(s,{label:"默认框架 Default Framework"},{default:t(()=>[e(S,{value:l.framework,"onUpdate:value":a[0]||(a[0]=n=>l.framework=n),"button-style":"solid"},{default:t(()=>[e(c,{value:"vllm"},{default:t(()=>[...a[16]||(a[16]=[p("vLLM",-1)])]),_:1}),e(c,{value:"sglang"},{default:t(()=>[...a[17]||(a[17]=[p("SGLang",-1)])]),_:1})]),_:1},8,["value"])]),_:1})]),_:1}),e(u,{span:8},{default:t(()=>[e(s,{label:"Base URL(OpenAI 兼容)"},{default:t(()=>[e(i,{value:l.api.base_url,"onUpdate:value":a[1]||(a[1]=n=>l.api.base_url=n),placeholder:"http://192.168.1.67:8000"},null,8,["value"])]),_:1})]),_:1}),e(u,{span:8},{default:t(()=>[e(s,{label:"Endpoint"},{default:t(()=>[e(i,{value:l.api.endpoint,"onUpdate:value":a[2]||(a[2]=n=>l.api.endpoint=n),placeholder:"/v1/chat/completions"},null,8,["value"])]),_:1})]),_:1})]),_:1}),e(f,{gutter:16},{default:t(()=>[e(u,{span:8},{default:t(()=>[e(s,{label:"API Key(可选)"},{default:t(()=>[e(O,{value:l.api.api_key,"onUpdate:value":a[3]||(a[3]=n=>l.api.api_key=n),placeholder:"Bearer token"},null,8,["value"])]),_:1})]),_:1}),e(u,{span:16},{default:t(()=>[e(s,{label:"额外请求头(JSON,可选)"},{default:t(()=>[e(i,{value:v.value,"onUpdate:value":a[4]||(a[4]=n=>v.value=n),placeholder:'{"X-Custom": "value"}'},null,8,["value"])]),_:1})]),_:1})]),_:1}),e(s,null,{default:t(()=>{var n;return[_.value?(h(),V("span",le,[e(C,{color:_.value.ok?"green":"red"},{default:t(()=>[p(x(_.value.ok?"连接成功":"连接失败"),1)]),_:1},8,["color"]),_.value.ok?(h(),V("span",ne,x(((n=_.value.models)==null?void 0:n.length)||0)+" 个模型",1)):(h(),V("span",oe,x(_.value.error),1))])):E("",!0)]}),_:1}),e(m,{orientation:"left"},{default:t(()=>[e(w(W),{style:{color:"#1677ff"}}),a[18]||(a[18]=p(" GPU / 目录 / 阈值 ",-1))]),_:1}),e(f,{gutter:16},{default:t(()=>[e(u,{span:6},{default:t(()=>[e(s,{label:"GPU 自动检测"},{default:t(()=>[e(N,{checked:l.gpu.auto,"onUpdate:checked":a[5]||(a[5]=n=>l.gpu.auto=n)},null,8,["checked"]),U.value?(h(),V("div",se," 检测到:"+x(U.value.name)+" × "+x(U.value.count),1)):E("",!0)]),_:1})]),_:1}),e(u,{span:5},{default:t(()=>[e(s,{label:"GPU 型号(回退)"},{default:t(()=>[e(i,{value:l.gpu.name,"onUpdate:value":a[6]||(a[6]=n=>l.gpu.name=n)},null,8,["value"])]),_:1})]),_:1}),e(u,{span:4},{default:t(()=>[e(s,{label:"GPU 数量"},{default:t(()=>[e(b,{value:l.gpu.count,"onUpdate:value":a[7]||(a[7]=n=>l.gpu.count=n),min:1,style:{width:"100%"}},null,8,["value"])]),_:1})]),_:1}),e(u,{span:9},{default:t(()=>[e(s,{label:"TPOT 阈值 (ms)(默认)"},{default:t(()=>[e(b,{value:l.tpot_threshold_ms,"onUpdate:value":a[8]||(a[8]=n=>l.tpot_threshold_ms=n),min:1,style:{width:"100%"}},null,8,["value"])]),_:1})]),_:1})]),_:1}),e(f,{gutter:16},{default:t(()=>[e(u,{span:8},{default:t(()=>[e(s,{label:"日志目录 logs_dir"},{default:t(()=>[e(i,{value:l.logs_dir,"onUpdate:value":a[9]||(a[9]=n=>l.logs_dir=n),placeholder:"./logs"},null,8,["value"])]),_:1})]),_:1}),e(u,{span:8},{default:t(()=>[e(s,{label:"数据集缓存目录 datasets_dir"},{default:t(()=>[e(i,{value:l.datasets_dir,"onUpdate:value":a[10]||(a[10]=n=>l.datasets_dir=n),placeholder:"./datasets"},null,8,["value"])]),_:1})]),_:1}),e(u,{span:8},{default:t(()=>[e(s,{label:"请求速率 Request rate(默认)"},{default:t(()=>[e(q,{value:l.request_rate,"onUpdate:value":a[11]||(a[11]=n=>l.request_rate=n)},{default:t(()=>[e(y,{value:"inf"},{default:t(()=>[...a[19]||(a[19]=[p("inf(不限速)",-1)])]),_:1}),e(y,{value:"custom"},{default:t(()=>[...a[20]||(a[20]=[p("自定义",-1)])]),_:1})]),_:1},8,["value"])]),_:1})]),_:1})]),_:1}),e(m,{orientation:"left"},{default:t(()=>[e(w(Q),{style:{color:"#1677ff"}}),a[21]||(a[21]=p(" bench 执行命令 ",-1))]),_:1}),e(f,{gutter:16},{default:t(()=>[e(u,{span:12},{default:t(()=>[e(s,{label:"vLLM bench 命令模板"},{default:t(()=>[e(i,{value:l.bench_commands.vllm,"onUpdate:value":a[12]||(a[12]=n=>l.bench_commands.vllm=n),placeholder:"vllm bench serve"},null,8,["value"])]),_:1})]),_:1}),e(u,{span:12},{default:t(()=>[e(s,{label:"SGLang bench 命令模板"},{default:t(()=>[e(i,{value:l.bench_commands.sglang,"onUpdate:value":a[13]||(a[13]=n=>l.bench_commands.sglang=n),placeholder:"python -m sglang.bench_serving"},null,8,["value"])]),_:1})]),_:1})]),_:1}),a[23]||(a[23]=te("div",{style:{color:"#999","font-size":"12px","margin-bottom":"12px"}}," 说明:bench 工具在 benchscope 所在机器以子进程运行(需安装 vllm / sglang CLI),推理服务端只需提供 OpenAI 兼容 API,无需安装插件。 ",-1)),e(s,null,{default:t(()=>[e(g,{type:"primary",size:"large",onClick:$},{icon:t(()=>[e(w(Y))]),default:t(()=>[a[22]||(a[22]=p(" 保存配置 ",-1))]),_:1})]),_:1})]),_:1})]),_:1},8,["spinning"])]),_:1})}}},_e=F(ue,[["__scopeId","data-v-a3dbcdbc"]]);export{_e as default};
|