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.
Files changed (87) hide show
  1. {benchscope-1.0.5 → benchscope-1.0.6}/PKG-INFO +16 -5
  2. {benchscope-1.0.5 → benchscope-1.0.6}/README.md +14 -4
  3. benchscope-1.0.6/benchscope/__init__.py +4 -0
  4. benchscope-1.0.6/benchscope/builtin_datasets.py +103 -0
  5. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/cli.py +20 -0
  6. benchscope-1.0.6/benchscope/config.py +234 -0
  7. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/constants.py +10 -3
  8. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/parser.py +1 -1
  9. benchscope-1.0.6/benchscope/server/api_config.py +431 -0
  10. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_dashboard.py +16 -4
  11. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_logs.py +161 -24
  12. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_tasks.py +8 -2
  13. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/app.py +23 -2
  14. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/state.py +5 -2
  15. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/test_manager.py +4 -1
  16. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/task_manager.py +101 -29
  17. benchscope-1.0.6/benchscope/webui/assets/AccuracyView-DuYXlzdF.js +1 -0
  18. benchscope-1.0.6/benchscope/webui/assets/DashboardView-B9NSQxgY.js +1 -0
  19. benchscope-1.0.6/benchscope/webui/assets/DashboardView-efxhNbZA.css +1 -0
  20. benchscope-1.0.6/benchscope/webui/assets/DatasAnalysisView-D4kMXxJP.css +1 -0
  21. benchscope-1.0.6/benchscope/webui/assets/DatasAnalysisView-Dq2PU74U.js +1 -0
  22. benchscope-1.0.6/benchscope/webui/assets/DatasEvalsView-CKDm5d0o.js +1 -0
  23. benchscope-1.0.6/benchscope/webui/assets/DatasEvalsView-Dw2s8Caq.css +1 -0
  24. benchscope-1.0.6/benchscope/webui/assets/DatasPerfsView-CiHoLV4u.js +22 -0
  25. benchscope-1.0.6/benchscope/webui/assets/DatasPerfsView-iOsgANDF.css +1 -0
  26. benchscope-1.0.6/benchscope/webui/assets/DatasView-CelHF7nF.js +1 -0
  27. benchscope-1.0.6/benchscope/webui/assets/DatasView-RZvG0ZHh.css +1 -0
  28. benchscope-1.0.6/benchscope/webui/assets/MetricsTable-BqMrqOR7.css +1 -0
  29. benchscope-1.0.6/benchscope/webui/assets/MetricsTable-CnuI6iCF.js +1 -0
  30. benchscope-1.0.6/benchscope/webui/assets/PerfCreateView-CuS38i63.js +5 -0
  31. benchscope-1.0.5/benchscope/webui/assets/PerfCreateView-DJpeZoFB.css → benchscope-1.0.6/benchscope/webui/assets/PerfCreateView-snqxkmyk.css +1 -1
  32. benchscope-1.0.6/benchscope/webui/assets/PerformanceView-CHLBhiQ5.css +1 -0
  33. benchscope-1.0.6/benchscope/webui/assets/PerformanceView-KOF5Mglm.js +2 -0
  34. benchscope-1.0.5/benchscope/webui/assets/SessionsView-7iC4EDdc.js → benchscope-1.0.6/benchscope/webui/assets/SessionsView-DD5s41VZ.js +3 -3
  35. benchscope-1.0.6/benchscope/webui/assets/SessionsView-M3-edhM0.css +1 -0
  36. benchscope-1.0.6/benchscope/webui/assets/SettingsView-8-rKWxWv.css +1 -0
  37. benchscope-1.0.6/benchscope/webui/assets/SettingsView-zpGCDV5m.js +1 -0
  38. benchscope-1.0.5/benchscope/webui/assets/antd-4y7KQvbV.js → benchscope-1.0.6/benchscope/webui/assets/antd-CEH7kT1R.js +88 -88
  39. benchscope-1.0.5/benchscope/webui/assets/echarts-BxmYhks9.js → benchscope-1.0.6/benchscope/webui/assets/echarts-52vywGac.js +18 -18
  40. benchscope-1.0.5/benchscope/webui/assets/index-gDzZDiBZ.css → benchscope-1.0.6/benchscope/webui/assets/index-BOkjDk-W.css +1 -1
  41. benchscope-1.0.6/benchscope/webui/assets/index-DN0xzMJ_.js +2 -0
  42. benchscope-1.0.5/benchscope/webui/assets/vue-sRXBoqf9.js → benchscope-1.0.6/benchscope/webui/assets/vue-q1XafS6H.js +1 -1
  43. benchscope-1.0.6/benchscope/webui/blue_logo.png +0 -0
  44. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/index.html +5 -4
  45. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/PKG-INFO +16 -5
  46. benchscope-1.0.6/benchscope.egg-info/SOURCES.txt +69 -0
  47. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/requires.txt +1 -0
  48. {benchscope-1.0.5 → benchscope-1.0.6}/pyproject.toml +6 -1
  49. benchscope-1.0.5/benchscope/__init__.py +0 -3
  50. benchscope-1.0.5/benchscope/config.py +0 -97
  51. benchscope-1.0.5/benchscope/server/api_config.py +0 -178
  52. benchscope-1.0.5/benchscope/webui/assets/AccuracyView-Do9xEpUs.js +0 -1
  53. benchscope-1.0.5/benchscope/webui/assets/DashboardView-B6yTD_57.css +0 -1
  54. benchscope-1.0.5/benchscope/webui/assets/DashboardView-CvSu3Ajb.js +0 -1
  55. benchscope-1.0.5/benchscope/webui/assets/MetricsTable-BAnB6ejY.js +0 -1
  56. benchscope-1.0.5/benchscope/webui/assets/MetricsTable-DcdXLVvt.css +0 -1
  57. benchscope-1.0.5/benchscope/webui/assets/PerfCreateView-Dvwba3-X.js +0 -5
  58. benchscope-1.0.5/benchscope/webui/assets/PerformanceView-BH6lMjsm.js +0 -2
  59. benchscope-1.0.5/benchscope/webui/assets/PerformanceView-CskDmEDz.css +0 -1
  60. benchscope-1.0.5/benchscope/webui/assets/SessionsView-C0TeuY82.css +0 -1
  61. benchscope-1.0.5/benchscope/webui/assets/SettingsView-D9KmYHkj.js +0 -1
  62. benchscope-1.0.5/benchscope/webui/assets/SettingsView-VNgMZDaD.css +0 -1
  63. benchscope-1.0.5/benchscope/webui/assets/index-BQS9GiRJ.js +0 -2
  64. benchscope-1.0.5/benchscope.egg-info/SOURCES.txt +0 -59
  65. {benchscope-1.0.5 → benchscope-1.0.6}/LICENSE +0 -0
  66. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/__main__.py +0 -0
  67. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/__init__.py +0 -0
  68. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/base.py +0 -0
  69. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/runner.py +0 -0
  70. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/sglang_bench.py +0 -0
  71. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/benches/vllm_bench.py +0 -0
  72. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/datasets.py +0 -0
  73. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/env_info.py +0 -0
  74. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/gpu.py +0 -0
  75. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/__init__.py +0 -0
  76. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_sessions.py +0 -0
  77. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/api_test.py +0 -0
  78. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/status.py +0 -0
  79. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/server/ws.py +0 -0
  80. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/session_manager.py +0 -0
  81. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/summary.py +0 -0
  82. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/assets/AccuracyView-BjuVpuFU.css +0 -0
  83. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope/webui/bs-logo.png +0 -0
  84. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/dependency_links.txt +0 -0
  85. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/entry_points.txt +0 -0
  86. {benchscope-1.0.5 → benchscope-1.0.6}/benchscope.egg-info/top_level.txt +0 -0
  87. {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.5
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
- # benchscope
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
- **English** | [简体中文](README.zh-CN.md)
42
+ <p align="center"><strong>English</strong> | <a href="README.zh-CN.md">简体中文</a></p>
38
43
 
39
- > A visualization testing platform for LLM model **performance & accuracy**, supporting models deployed with vLLM / SGLang and any OpenAI-compatible inference service.
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="benchscope main performance screenshot" />
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
- # benchscope
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
- **English** | [简体中文](README.zh-CN.md)
7
+ <p align="center"><strong>English</strong> | <a href="README.zh-CN.md">简体中文</a></p>
4
8
 
5
- > A visualization testing platform for LLM model **performance & accuracy**, supporting models deployed with vLLM / SGLang and any OpenAI-compatible inference service.
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="benchscope main performance screenshot" />
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,4 @@
1
+ """BenchScope - LLM inference performance testing tool."""
2
+
3
+ # PEP 440:开发中带 .dev0 后缀,正式发布时改为 "1.0.6"
4
+ __version__ = "1.0.6"
@@ -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
- "logs_dir": "./logs",
42
- "datasets_dir": "./datasets",
43
- "data_dir": "~/.benchscope", # 服务端数据持久化目录(任务 / 会话等)
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
- "output": float, "peakoutput": float, "total": float,
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(完整原始输出)