benchscope 1.0.5__tar.gz → 1.0.6__tar.gz
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-1.0.5 → benchscope-1.0.6}/PKG-INFO +16 -5
- {benchscope-1.0.5 → benchscope-1.0.6}/README.md +14 -4
- benchscope-1.0.6/benchscope/__init__.py +4 -0
- benchscope-1.0.6/benchscope/builtin_datasets.py +103 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/cli.py +20 -0
- benchscope-1.0.6/benchscope/config.py +234 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/constants.py +10 -3
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/parser.py +1 -1
- benchscope-1.0.6/benchscope/server/api_config.py +431 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_dashboard.py +16 -4
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_logs.py +161 -24
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_tasks.py +8 -2
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/app.py +23 -2
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/state.py +5 -2
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/test_manager.py +4 -1
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/task_manager.py +101 -29
- benchscope-1.0.6/benchscope/webui/assets/AccuracyView-DuYXlzdF.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DashboardView-B9NSQxgY.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DashboardView-efxhNbZA.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasAnalysisView-D4kMXxJP.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasAnalysisView-Dq2PU74U.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasEvalsView-CKDm5d0o.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasEvalsView-Dw2s8Caq.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasPerfsView-CiHoLV4u.js +22 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasPerfsView-iOsgANDF.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasView-CelHF7nF.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/DatasView-RZvG0ZHh.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/MetricsTable-BqMrqOR7.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/MetricsTable-CnuI6iCF.js +1 -0
- benchscope-1.0.6/benchscope/webui/assets/PerfCreateView-CuS38i63.js +5 -0
- benchscope-1.0.5/benchscope/webui/assets/PerfCreateView-DJpeZoFB.css → benchscope-1.0.6/benchscope/webui/assets/PerfCreateView-snqxkmyk.css +1 -1
- benchscope-1.0.6/benchscope/webui/assets/PerformanceView-CHLBhiQ5.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/PerformanceView-KOF5Mglm.js +2 -0
- benchscope-1.0.5/benchscope/webui/assets/SessionsView-7iC4EDdc.js → benchscope-1.0.6/benchscope/webui/assets/SessionsView-DD5s41VZ.js +3 -3
- benchscope-1.0.6/benchscope/webui/assets/SessionsView-M3-edhM0.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/SettingsView-8-rKWxWv.css +1 -0
- benchscope-1.0.6/benchscope/webui/assets/SettingsView-zpGCDV5m.js +1 -0
- benchscope-1.0.5/benchscope/webui/assets/antd-4y7KQvbV.js → benchscope-1.0.6/benchscope/webui/assets/antd-CEH7kT1R.js +88 -88
- benchscope-1.0.5/benchscope/webui/assets/echarts-BxmYhks9.js → benchscope-1.0.6/benchscope/webui/assets/echarts-52vywGac.js +18 -18
- benchscope-1.0.5/benchscope/webui/assets/index-gDzZDiBZ.css → benchscope-1.0.6/benchscope/webui/assets/index-BOkjDk-W.css +1 -1
- benchscope-1.0.6/benchscope/webui/assets/index-DN0xzMJ_.js +2 -0
- benchscope-1.0.5/benchscope/webui/assets/vue-sRXBoqf9.js → benchscope-1.0.6/benchscope/webui/assets/vue-q1XafS6H.js +1 -1
- benchscope-1.0.6/benchscope/webui/blue_logo.png +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/index.html +5 -4
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/PKG-INFO +16 -5
- benchscope-1.0.6/benchscope.egg-info/SOURCES.txt +69 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/requires.txt +1 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/pyproject.toml +6 -1
- benchscope-1.0.5/benchscope/__init__.py +0 -3
- benchscope-1.0.5/benchscope/config.py +0 -97
- benchscope-1.0.5/benchscope/server/api_config.py +0 -178
- benchscope-1.0.5/benchscope/webui/assets/AccuracyView-Do9xEpUs.js +0 -1
- benchscope-1.0.5/benchscope/webui/assets/DashboardView-B6yTD_57.css +0 -1
- benchscope-1.0.5/benchscope/webui/assets/DashboardView-CvSu3Ajb.js +0 -1
- benchscope-1.0.5/benchscope/webui/assets/MetricsTable-BAnB6ejY.js +0 -1
- benchscope-1.0.5/benchscope/webui/assets/MetricsTable-DcdXLVvt.css +0 -1
- benchscope-1.0.5/benchscope/webui/assets/PerfCreateView-Dvwba3-X.js +0 -5
- benchscope-1.0.5/benchscope/webui/assets/PerformanceView-BH6lMjsm.js +0 -2
- benchscope-1.0.5/benchscope/webui/assets/PerformanceView-CskDmEDz.css +0 -1
- benchscope-1.0.5/benchscope/webui/assets/SessionsView-C0TeuY82.css +0 -1
- benchscope-1.0.5/benchscope/webui/assets/SettingsView-D9KmYHkj.js +0 -1
- benchscope-1.0.5/benchscope/webui/assets/SettingsView-VNgMZDaD.css +0 -1
- benchscope-1.0.5/benchscope/webui/assets/index-BQS9GiRJ.js +0 -2
- benchscope-1.0.5/benchscope.egg-info/SOURCES.txt +0 -59
- {benchscope-1.0.5 → benchscope-1.0.6}/LICENSE +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/__main__.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/__init__.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/base.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/runner.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/sglang_bench.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/vllm_bench.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/datasets.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/env_info.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/gpu.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/__init__.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_sessions.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_test.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/status.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/ws.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/session_manager.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/summary.py +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/assets/AccuracyView-BjuVpuFU.css +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/bs-logo.png +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/dependency_links.txt +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/entry_points.txt +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/top_level.txt +0 -0
- {benchscope-1.0.5 → benchscope-1.0.6}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: benchscope
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.6
|
|
4
4
|
Summary: LLM inference performance testing tool. Supports vLLM, SGLang, and any OpenAI-compatible API.
|
|
5
5
|
Author-email: benchscope <labelnet@foxmail.com>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -28,18 +28,25 @@ Requires-Dist: requests>=2.31
|
|
|
28
28
|
Requires-Dist: openpyxl>=3.1
|
|
29
29
|
Requires-Dist: pydantic>=2
|
|
30
30
|
Requires-Dist: python-multipart>=0.0.9
|
|
31
|
+
Requires-Dist: pyyaml>=6.0
|
|
31
32
|
Provides-Extra: modelscope
|
|
32
33
|
Requires-Dist: modelscope>=1.15; extra == "modelscope"
|
|
33
34
|
Dynamic: license-file
|
|
34
35
|
|
|
35
|
-
|
|
36
|
+
<div align="center">
|
|
37
|
+
<img src="asserts/black_logo.png" width="120" height="120" alt="BenchScope logo" />
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<h1 align="center" style="font-size: 48px; margin-top: 12px;">BenchScope</h1>
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
<p align="center"><strong>English</strong> | <a href="README.zh-CN.md">简体中文</a></p>
|
|
38
43
|
|
|
39
|
-
|
|
44
|
+
BenchScope is an open‑source LLM inference benchmarking platform built with herness coding.
|
|
45
|
+
|
|
46
|
+
A visualization testing platform for LLM model **performance & accuracy**, supporting models deployed with vLLM / SGLang and any OpenAI-compatible inference service.
|
|
40
47
|
|
|
41
48
|
<div align="center">
|
|
42
|
-
<img src="asserts/main-performance.png" width="72%" alt="
|
|
49
|
+
<img src="asserts/main-performance.png" width="72%" alt="BenchScope main performance screenshot" />
|
|
43
50
|
</div>
|
|
44
51
|
|
|
45
52
|
---
|
|
@@ -66,6 +73,10 @@ benchscope
|
|
|
66
73
|
benchscope --port 8080 --no-browser
|
|
67
74
|
```
|
|
68
75
|
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
See [docs/Readme.md](docs/Readme.md).
|
|
79
|
+
|
|
69
80
|
## Open Source
|
|
70
81
|
|
|
71
82
|
- **License** — [Apache License 2.0](LICENSE)
|
|
@@ -1,11 +1,17 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
|
+
<img src="asserts/black_logo.png" width="120" height="120" alt="BenchScope logo" />
|
|
3
|
+
</div>
|
|
4
|
+
|
|
5
|
+
<h1 align="center" style="font-size: 48px; margin-top: 12px;">BenchScope</h1>
|
|
2
6
|
|
|
3
|
-
|
|
7
|
+
<p align="center"><strong>English</strong> | <a href="README.zh-CN.md">简体中文</a></p>
|
|
4
8
|
|
|
5
|
-
|
|
9
|
+
BenchScope is an open‑source LLM inference benchmarking platform built with herness coding.
|
|
10
|
+
|
|
11
|
+
A visualization testing platform for LLM model **performance & accuracy**, supporting models deployed with vLLM / SGLang and any OpenAI-compatible inference service.
|
|
6
12
|
|
|
7
13
|
<div align="center">
|
|
8
|
-
<img src="asserts/main-performance.png" width="72%" alt="
|
|
14
|
+
<img src="asserts/main-performance.png" width="72%" alt="BenchScope main performance screenshot" />
|
|
9
15
|
</div>
|
|
10
16
|
|
|
11
17
|
---
|
|
@@ -32,6 +38,10 @@ benchscope
|
|
|
32
38
|
benchscope --port 8080 --no-browser
|
|
33
39
|
```
|
|
34
40
|
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
See [docs/Readme.md](docs/Readme.md).
|
|
44
|
+
|
|
35
45
|
## Open Source
|
|
36
46
|
|
|
37
47
|
- **License** — [Apache License 2.0](LICENSE)
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"""内置数据集模块:读取 configs/datasets.yaml,支持下载并缓存到 datasets_dir。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import yaml
|
|
9
|
+
|
|
10
|
+
from benchscope.datasets import download_file, list_dataset_files
|
|
11
|
+
|
|
12
|
+
log = logging.getLogger("benchscope.builtin_datasets")
|
|
13
|
+
|
|
14
|
+
CONFIGS_DIR = Path(__file__).resolve().parent / "configs"
|
|
15
|
+
DATASETS_YAML = CONFIGS_DIR / "datasets.yaml"
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def load_builtin_datasets() -> list[dict]:
|
|
19
|
+
"""读取内置数据集定义(configs/datasets.yaml)。"""
|
|
20
|
+
if not DATASETS_YAML.exists():
|
|
21
|
+
log.warning("datasets.yaml 不存在: %s", DATASETS_YAML)
|
|
22
|
+
return []
|
|
23
|
+
try:
|
|
24
|
+
data = yaml.safe_load(DATASETS_YAML.read_text(encoding="utf-8"))
|
|
25
|
+
datasets = data.get("datasets", []) if isinstance(data, dict) else []
|
|
26
|
+
return [d for d in datasets if isinstance(d, dict) and d.get("id")]
|
|
27
|
+
except Exception:
|
|
28
|
+
log.exception("解析 datasets.yaml 失败")
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _cache_root(datasets_dir: Path) -> Path:
|
|
33
|
+
"""内置数据集缓存根目录:datasets_dir(Settings/Datasets 管理的数据集下载目录)。"""
|
|
34
|
+
return datasets_dir
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ds_cache_dir(cache_root: Path, ds_id: str) -> Path:
|
|
38
|
+
return cache_root / ds_id
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def dataset_status(ds: dict, cache_root: Path) -> dict:
|
|
42
|
+
"""返回数据集缓存状态:cached(文件列表)/ not_cached。"""
|
|
43
|
+
d = _ds_cache_dir(cache_root, ds.get("id", ""))
|
|
44
|
+
files: list[str] = []
|
|
45
|
+
if d.is_dir():
|
|
46
|
+
files = sorted(p.name for p in d.iterdir() if p.is_file())
|
|
47
|
+
return {"id": ds.get("id"), "cached": bool(files), "files": files}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def download_builtin_dataset(ds: dict, data_dir: Path, timeout: int = 300) -> dict:
|
|
51
|
+
"""下载内置数据集到缓存目录(data_dir/datasets/{id}/),返回下载结果。"""
|
|
52
|
+
ds_id = ds.get("id", "")
|
|
53
|
+
if not ds_id:
|
|
54
|
+
raise ValueError("数据集缺少 id")
|
|
55
|
+
source = ds.get("source") or {}
|
|
56
|
+
stype = source.get("type", "")
|
|
57
|
+
dest_dir = _ds_cache_dir(_cache_root(data_dir), ds_id)
|
|
58
|
+
dest_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
if stype == "modelscope":
|
|
61
|
+
dataset_id = source.get("dataset_id")
|
|
62
|
+
if not dataset_id:
|
|
63
|
+
raise ValueError(f"数据集 {ds_id} 缺少 modelscope dataset_id")
|
|
64
|
+
files = list_dataset_files(dataset_id, timeout=timeout)
|
|
65
|
+
cands = [
|
|
66
|
+
(f.get("Path"), f.get("Size") or 0)
|
|
67
|
+
for f in files
|
|
68
|
+
if (f.get("Path") or "").endswith((".json", ".jsonl", ".parquet", ".csv"))
|
|
69
|
+
]
|
|
70
|
+
if not cands:
|
|
71
|
+
raise FileNotFoundError(f"数据集 {dataset_id} 中未找到可下载文件")
|
|
72
|
+
wanted = source.get("file")
|
|
73
|
+
if wanted:
|
|
74
|
+
matched = [c for c in cands if c[0].endswith(wanted)]
|
|
75
|
+
if matched:
|
|
76
|
+
cands = matched
|
|
77
|
+
# 优先真正的数据文件:按大小降序(排除 dataset_infos.json 等小元数据)
|
|
78
|
+
cands.sort(key=lambda x: -x[1])
|
|
79
|
+
target = cands[0][0]
|
|
80
|
+
dest = dest_dir / Path(target).name
|
|
81
|
+
if not dest.exists() or dest.stat().st_size == 0:
|
|
82
|
+
log.info("下载内置数据集 %s 文件 %s ...", ds_id, target)
|
|
83
|
+
download_file(dataset_id, target, dest, timeout=timeout)
|
|
84
|
+
return {"id": ds_id, "path": str(dest), "size": dest.stat().st_size}
|
|
85
|
+
elif stype == "url":
|
|
86
|
+
url = source.get("url")
|
|
87
|
+
if not url:
|
|
88
|
+
raise ValueError(f"数据集 {ds_id} 缺少 url")
|
|
89
|
+
import requests
|
|
90
|
+
|
|
91
|
+
dest = dest_dir / Path(url.split("?")[0]).name
|
|
92
|
+
if not dest.exists() or dest.stat().st_size == 0:
|
|
93
|
+
log.info("下载内置数据集 %s(%s)...", ds_id, url)
|
|
94
|
+
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
95
|
+
with requests.get(url, stream=True, timeout=timeout) as resp:
|
|
96
|
+
resp.raise_for_status()
|
|
97
|
+
with open(tmp, "wb") as f:
|
|
98
|
+
for chunk in resp.iter_content(chunk_size=1 << 20):
|
|
99
|
+
f.write(chunk)
|
|
100
|
+
tmp.replace(dest)
|
|
101
|
+
return {"id": ds_id, "path": str(dest), "size": dest.stat().st_size}
|
|
102
|
+
else:
|
|
103
|
+
raise ValueError(f"数据集 {ds_id} 不支持的类型: {stype!r}")
|
|
@@ -6,6 +6,25 @@ import logging
|
|
|
6
6
|
import sys
|
|
7
7
|
import webbrowser
|
|
8
8
|
import threading
|
|
9
|
+
from datetime import datetime
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _setup_runtime_logging() -> None:
|
|
13
|
+
"""将服务运行日志落盘到 logs 目录:runtime_年月日.log。"""
|
|
14
|
+
try:
|
|
15
|
+
from benchscope.server.state import state
|
|
16
|
+
|
|
17
|
+
logs_dir = state.config.logs_dir
|
|
18
|
+
logs_dir.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
rt_file = logs_dir / f"runtime_{datetime.now().strftime('%Y%m%d')}.log"
|
|
20
|
+
fh = logging.FileHandler(rt_file, encoding="utf-8")
|
|
21
|
+
fh.setFormatter(
|
|
22
|
+
logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s")
|
|
23
|
+
)
|
|
24
|
+
logging.getLogger().addHandler(fh)
|
|
25
|
+
logging.getLogger("benchscope").info("runtime 日志已落盘: %s", rt_file)
|
|
26
|
+
except Exception:
|
|
27
|
+
logging.getLogger("benchscope").exception("runtime 日志配置失败")
|
|
9
28
|
|
|
10
29
|
|
|
11
30
|
def main(argv=None) -> int:
|
|
@@ -29,6 +48,7 @@ def main(argv=None) -> int:
|
|
|
29
48
|
from benchscope.server.app import create_app
|
|
30
49
|
|
|
31
50
|
app = create_app()
|
|
51
|
+
_setup_runtime_logging()
|
|
32
52
|
url = f"http://127.0.0.1:{args.port}"
|
|
33
53
|
|
|
34
54
|
if not args.no_browser:
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""配置持久化:settings.json 读写与运行时配置单例。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
import threading
|
|
8
|
+
from copy import deepcopy
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from benchscope.constants import DEFAULT_CONFIG
|
|
12
|
+
|
|
13
|
+
log = logging.getLogger("benchscope.config")
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _data_root() -> Path:
|
|
17
|
+
"""数据根目录:默认 ~/.benchscope;可通过环境变量 BENCHSCOPE_DATA_DIR 覆盖(测试隔离)。"""
|
|
18
|
+
env = os.environ.get("BENCHSCOPE_DATA_DIR")
|
|
19
|
+
if env:
|
|
20
|
+
return Path(os.path.expanduser(env)).resolve()
|
|
21
|
+
return Path.home() / ".benchscope"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
DATA_ROOT = _data_root()
|
|
25
|
+
|
|
26
|
+
# 设置持久化文件:~/.benchscope/settings.json
|
|
27
|
+
DEFAULT_CONFIG_PATH = DATA_ROOT / "settings.json"
|
|
28
|
+
# 旧版配置(兼容迁移)
|
|
29
|
+
LEGACY_CONFIG_PATH = DATA_ROOT / "config.json"
|
|
30
|
+
# 旧版默认路径 -> 新目录体系(迁移归一化)
|
|
31
|
+
LEGACY_DEFAULT_PATHS = {
|
|
32
|
+
"./logs": str(DATA_ROOT / "logs"),
|
|
33
|
+
"./datasets": str(DATA_ROOT / "datasets"),
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
# data_dir 下的默认子目录映射:配置 key -> 子目录名
|
|
37
|
+
DATA_SUBDIRS = {
|
|
38
|
+
"perfs_dir": "perfs",
|
|
39
|
+
"evals_dir": "evals",
|
|
40
|
+
"analysis_dir": "analysys",
|
|
41
|
+
"logs_dir": "logs",
|
|
42
|
+
"sessions_dir": "sessions",
|
|
43
|
+
"models_dir": "models",
|
|
44
|
+
"datasets_dir": "datasets",
|
|
45
|
+
"plugins_dir": "plugins",
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConfigManager:
|
|
50
|
+
"""线程安全的配置管理。"""
|
|
51
|
+
|
|
52
|
+
def __init__(self, path: Path | str | None = None):
|
|
53
|
+
self.path = Path(path) if path else DEFAULT_CONFIG_PATH
|
|
54
|
+
# 旧版 config.json 与 settings.json 同目录(默认均为 ~/.benchscope)
|
|
55
|
+
self.legacy_path = self.path.parent / "config.json"
|
|
56
|
+
self._lock = threading.RLock()
|
|
57
|
+
self._data: dict = deepcopy(DEFAULT_CONFIG)
|
|
58
|
+
self._redirect_default_dirs()
|
|
59
|
+
self.load()
|
|
60
|
+
|
|
61
|
+
def _redirect_default_dirs(self) -> None:
|
|
62
|
+
"""测试隔离:设置 BENCHSCOPE_DATA_DIR 时,把默认缓存目录重定向到该根目录下。
|
|
63
|
+
|
|
64
|
+
仅作用于默认值;settings.json 中已持久化的自定义目录仍以持久化值为准。
|
|
65
|
+
"""
|
|
66
|
+
env = os.environ.get("BENCHSCOPE_DATA_DIR")
|
|
67
|
+
if not env:
|
|
68
|
+
return
|
|
69
|
+
root = Path(os.path.expanduser(env)).resolve()
|
|
70
|
+
self._data["data_dir"] = str(root)
|
|
71
|
+
for key, sub in DATA_SUBDIRS.items():
|
|
72
|
+
self._data[key] = str(root / sub)
|
|
73
|
+
|
|
74
|
+
# ---------- 持久化 ----------
|
|
75
|
+
def load(self) -> None:
|
|
76
|
+
with self._lock:
|
|
77
|
+
# 优先读 settings.json;若不存在则尝试旧版 config.json(兼容迁移)
|
|
78
|
+
source = self.path if self.path.exists() else self.legacy_path
|
|
79
|
+
if source and source.exists():
|
|
80
|
+
try:
|
|
81
|
+
loaded = json.loads(source.read_text(encoding="utf-8"))
|
|
82
|
+
self._merge(self._data, loaded)
|
|
83
|
+
self._normalize_legacy_defaults()
|
|
84
|
+
except Exception:
|
|
85
|
+
log.exception("配置加载失败,使用默认配置: %s", source)
|
|
86
|
+
# 若读取的是旧版 config.json,则一次性落盘 settings.json 完成迁移
|
|
87
|
+
if source != self.path:
|
|
88
|
+
self.save()
|
|
89
|
+
else:
|
|
90
|
+
# 首次启动:落盘默认配置,确保 settings.json 存在
|
|
91
|
+
self.save()
|
|
92
|
+
# 启动时检查目录是否齐全(缺失则创建)
|
|
93
|
+
self.ensure_dirs()
|
|
94
|
+
|
|
95
|
+
def _normalize_legacy_defaults(self) -> None:
|
|
96
|
+
"""旧版默认相对路径(./logs、./datasets)归一化为新目录体系默认值。"""
|
|
97
|
+
for old, new in LEGACY_DEFAULT_PATHS.items():
|
|
98
|
+
for key in ("logs_dir", "datasets_dir"):
|
|
99
|
+
if self._data.get(key) == old:
|
|
100
|
+
self._data[key] = new
|
|
101
|
+
|
|
102
|
+
def save(self) -> None:
|
|
103
|
+
with self._lock:
|
|
104
|
+
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
105
|
+
self.path.write_text(
|
|
106
|
+
json.dumps(self._data, ensure_ascii=False, indent=2),
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
@staticmethod
|
|
111
|
+
def _merge(base: dict, overlay: dict) -> None:
|
|
112
|
+
for key, value in overlay.items():
|
|
113
|
+
if isinstance(value, dict) and isinstance(base.get(key), dict):
|
|
114
|
+
ConfigManager._merge(base[key], value)
|
|
115
|
+
else:
|
|
116
|
+
base[key] = value
|
|
117
|
+
|
|
118
|
+
# ---------- 访问 ----------
|
|
119
|
+
def get(self, key: str, default=None):
|
|
120
|
+
with self._lock:
|
|
121
|
+
return self._data.get(key, default)
|
|
122
|
+
|
|
123
|
+
def set(self, key: str, value) -> None:
|
|
124
|
+
with self._lock:
|
|
125
|
+
self._data[key] = value
|
|
126
|
+
self.save()
|
|
127
|
+
|
|
128
|
+
def update(self, patch: dict) -> dict:
|
|
129
|
+
with self._lock:
|
|
130
|
+
# data_dir 联动:当数据根目录变化时,未自定义的子目录跟随新 data_dir
|
|
131
|
+
new_data = patch.get("data_dir")
|
|
132
|
+
if new_data:
|
|
133
|
+
old_data = self._data.get("data_dir", DEFAULT_CONFIG["data_dir"])
|
|
134
|
+
old_root = str(Path(os.path.expanduser(old_data)).resolve())
|
|
135
|
+
new_root = str(Path(os.path.expanduser(new_data)).resolve())
|
|
136
|
+
for key, sub in DATA_SUBDIRS.items():
|
|
137
|
+
if key in patch: # 本次显式指定,不联动
|
|
138
|
+
continue
|
|
139
|
+
cur = self._data.get(key)
|
|
140
|
+
if cur:
|
|
141
|
+
cur_resolved = str(Path(os.path.expanduser(cur)).resolve())
|
|
142
|
+
if cur_resolved == str(Path(old_root) / sub):
|
|
143
|
+
patch[key] = str(Path(new_root) / sub)
|
|
144
|
+
self._merge(self._data, patch)
|
|
145
|
+
self.save()
|
|
146
|
+
self.ensure_dirs()
|
|
147
|
+
return deepcopy(self._data)
|
|
148
|
+
|
|
149
|
+
def snapshot(self) -> dict:
|
|
150
|
+
with self._lock:
|
|
151
|
+
return deepcopy(self._data)
|
|
152
|
+
|
|
153
|
+
def resolve_dir(self, key: str) -> Path:
|
|
154
|
+
"""解析配置中的目录(展开 ~ 并 resolve)。"""
|
|
155
|
+
raw = self._data.get(key) or DEFAULT_CONFIG.get(key, "")
|
|
156
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
157
|
+
|
|
158
|
+
def ensure_dirs(self) -> None:
|
|
159
|
+
"""检查并创建所有缓存目录(启动时调用)。"""
|
|
160
|
+
with self._lock:
|
|
161
|
+
for key in ("data_dir", "perfs_dir", "evals_dir", "analysis_dir",
|
|
162
|
+
"logs_dir", "sessions_dir", "models_dir", "datasets_dir", "plugins_dir"):
|
|
163
|
+
raw = self._data.get(key, DEFAULT_CONFIG.get(key, ""))
|
|
164
|
+
try:
|
|
165
|
+
Path(os.path.expanduser(raw)).mkdir(parents=True, exist_ok=True)
|
|
166
|
+
except Exception:
|
|
167
|
+
log.exception("目录创建失败: %s=%s", key, raw)
|
|
168
|
+
|
|
169
|
+
# ---------- 常用辅助 ----------
|
|
170
|
+
@property
|
|
171
|
+
def api(self) -> dict:
|
|
172
|
+
return self.get("api", {})
|
|
173
|
+
|
|
174
|
+
@property
|
|
175
|
+
def data_dir(self) -> Path:
|
|
176
|
+
"""数据根目录,默认 ~/.benchscope。"""
|
|
177
|
+
raw = self.get("data_dir", DEFAULT_CONFIG["data_dir"])
|
|
178
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
179
|
+
|
|
180
|
+
@property
|
|
181
|
+
def perfs_dir(self) -> Path:
|
|
182
|
+
"""性能测试任务目录,默认 ~/.benchscope/perfs。"""
|
|
183
|
+
raw = self.get("perfs_dir", DEFAULT_CONFIG["perfs_dir"])
|
|
184
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def evals_dir(self) -> Path:
|
|
188
|
+
"""精度测试任务目录,默认 ~/.benchscope/evals。"""
|
|
189
|
+
raw = self.get("evals_dir", DEFAULT_CONFIG["evals_dir"])
|
|
190
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
191
|
+
|
|
192
|
+
@property
|
|
193
|
+
def analysis_dir(self) -> Path:
|
|
194
|
+
"""数据分析目录,默认 ~/.benchscope/analysys(联动 Datas)。"""
|
|
195
|
+
raw = self.get("analysis_dir", DEFAULT_CONFIG["analysis_dir"])
|
|
196
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
197
|
+
|
|
198
|
+
@property
|
|
199
|
+
def logs_dir(self) -> Path:
|
|
200
|
+
"""日志目录(runtime 日志 + 任务终端输出),默认 ~/.benchscope/logs。"""
|
|
201
|
+
raw = self.get("logs_dir", DEFAULT_CONFIG["logs_dir"])
|
|
202
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def sessions_dir(self) -> Path:
|
|
206
|
+
"""会话缓存目录,默认 ~/.benchscope/sessions。"""
|
|
207
|
+
raw = self.get("sessions_dir", DEFAULT_CONFIG["sessions_dir"])
|
|
208
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
209
|
+
|
|
210
|
+
@property
|
|
211
|
+
def datasets_dir(self) -> Path:
|
|
212
|
+
"""数据集下载目录,默认 ~/.benchscope/datasets。"""
|
|
213
|
+
raw = self.get("datasets_dir", DEFAULT_CONFIG["datasets_dir"])
|
|
214
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
215
|
+
|
|
216
|
+
@property
|
|
217
|
+
def models_dir(self) -> Path:
|
|
218
|
+
"""模型下载缓存目录,默认 ~/.benchscope/models。"""
|
|
219
|
+
raw = self.get("models_dir", DEFAULT_CONFIG["models_dir"])
|
|
220
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
221
|
+
|
|
222
|
+
@property
|
|
223
|
+
def plugins_dir(self) -> Path:
|
|
224
|
+
"""插件安装加载目录,默认 ~/.benchscope/plugins。"""
|
|
225
|
+
raw = self.get("plugins_dir", DEFAULT_CONFIG["plugins_dir"])
|
|
226
|
+
return Path(os.path.expanduser(raw)).resolve()
|
|
227
|
+
|
|
228
|
+
def set_api(self, patch: dict) -> dict:
|
|
229
|
+
with self._lock:
|
|
230
|
+
api = deepcopy(self._data.setdefault("api", {}))
|
|
231
|
+
api.update(patch)
|
|
232
|
+
self._data["api"] = api
|
|
233
|
+
self.save()
|
|
234
|
+
return deepcopy(api)
|
|
@@ -38,9 +38,16 @@ DEFAULT_CONFIG = {
|
|
|
38
38
|
"extra_headers": {},
|
|
39
39
|
},
|
|
40
40
|
"gpu": {"auto": True, "name": "", "count": 8},
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
"
|
|
41
|
+
# 缓存目录体系(数据根目录为 data_dir,其余为各功能目录)
|
|
42
|
+
"data_dir": "~/.benchscope", # 数据根目录(服务端数据持久化 / 任务 / 会话等)
|
|
43
|
+
"perfs_dir": "~/.benchscope/perfs", # 性能测试任务目录
|
|
44
|
+
"evals_dir": "~/.benchscope/evals", # 精度测试任务目录
|
|
45
|
+
"analysis_dir": "~/.benchscope/analysys", # 数据分析目录(联动 Datas)
|
|
46
|
+
"logs_dir": "~/.benchscope/logs", # 日志目录(runtime 日志 + 任务终端输出)
|
|
47
|
+
"sessions_dir": "~/.benchscope/sessions", # 会话缓存目录
|
|
48
|
+
"models_dir": "~/.benchscope/models", # 模型下载缓存目录(联动 Settings/Models)
|
|
49
|
+
"datasets_dir": "~/.benchscope/datasets", # 数据集下载目录(联动 Settings/Datasets)
|
|
50
|
+
"plugins_dir": "~/.benchscope/plugins", # 插件安装加载目录(联动 Settings/Plugins)
|
|
44
51
|
"tpot_threshold_ms": 100,
|
|
45
52
|
"request_rate": "inf", # inf 或数字
|
|
46
53
|
"bench_commands": {
|
|
@@ -73,7 +73,7 @@ def parse_metrics(output: str) -> dict:
|
|
|
73
73
|
返回结构:
|
|
74
74
|
{
|
|
75
75
|
"concurrency": int,
|
|
76
|
-
"
|
|
76
|
+
"output_mean": float, "peakoutput_mean": float, "total_mean": float,
|
|
77
77
|
"ttft_mean": float, "tpot_mean": float, "itl_mean": float,
|
|
78
78
|
"ttft_p99": float, "tpot_p99": float, "itl_p99": float,
|
|
79
79
|
"req_per_s": float, "raw": str(完整原始输出)
|