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.
Files changed (43) hide show
  1. benchscope/__init__.py +3 -0
  2. benchscope/__main__.py +4 -0
  3. benchscope/benches/__init__.py +1 -0
  4. benchscope/benches/base.py +54 -0
  5. benchscope/benches/runner.py +185 -0
  6. benchscope/benches/sglang_bench.py +77 -0
  7. benchscope/benches/vllm_bench.py +94 -0
  8. benchscope/cli.py +46 -0
  9. benchscope/config.py +91 -0
  10. benchscope/constants.py +49 -0
  11. benchscope/datasets.py +200 -0
  12. benchscope/gpu.py +39 -0
  13. benchscope/parser.py +93 -0
  14. benchscope/server/__init__.py +3 -0
  15. benchscope/server/api_config.py +97 -0
  16. benchscope/server/api_logs.py +356 -0
  17. benchscope/server/api_test.py +64 -0
  18. benchscope/server/app.py +99 -0
  19. benchscope/server/state.py +18 -0
  20. benchscope/server/status.py +97 -0
  21. benchscope/server/test_manager.py +367 -0
  22. benchscope/server/ws.py +56 -0
  23. benchscope/summary.py +148 -0
  24. benchscope/webui/assets/LogView-BDFIduo7.css +1 -0
  25. benchscope/webui/assets/LogView-BMsVPLVq.js +1 -0
  26. benchscope/webui/assets/MetricsCharts-D1wU0LbK.css +1 -0
  27. benchscope/webui/assets/MetricsCharts-DHO93JrC.js +1 -0
  28. benchscope/webui/assets/SettingsView-7uuTFqXU.css +1 -0
  29. benchscope/webui/assets/SettingsView-DJmFSQbU.js +1 -0
  30. benchscope/webui/assets/TestView-81VlxBZU.js +4 -0
  31. benchscope/webui/assets/TestView-BAwcOtR2.css +1 -0
  32. benchscope/webui/assets/antd-DWALckI0.js +478 -0
  33. benchscope/webui/assets/echarts-Bb6yjXMn.js +60 -0
  34. benchscope/webui/assets/index-Dfr0hp72.js +2 -0
  35. benchscope/webui/assets/index-EhoNt9Gv.css +1 -0
  36. benchscope/webui/assets/vue-Ch4zjUb1.js +37 -0
  37. benchscope/webui/index.html +19 -0
  38. benchscope-1.0.0.dist-info/METADATA +136 -0
  39. benchscope-1.0.0.dist-info/RECORD +43 -0
  40. benchscope-1.0.0.dist-info/WHEEL +5 -0
  41. benchscope-1.0.0.dist-info/entry_points.txt +2 -0
  42. benchscope-1.0.0.dist-info/licenses/LICENSE +176 -0
  43. 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 失败")
@@ -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};