benchscope 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- benchscope/__init__.py +3 -0
- benchscope/__main__.py +4 -0
- benchscope/benches/__init__.py +1 -0
- benchscope/benches/base.py +54 -0
- benchscope/benches/runner.py +185 -0
- benchscope/benches/sglang_bench.py +77 -0
- benchscope/benches/vllm_bench.py +94 -0
- benchscope/cli.py +46 -0
- benchscope/config.py +91 -0
- benchscope/constants.py +49 -0
- benchscope/datasets.py +200 -0
- benchscope/gpu.py +39 -0
- benchscope/parser.py +93 -0
- benchscope/server/__init__.py +3 -0
- benchscope/server/api_config.py +97 -0
- benchscope/server/api_logs.py +356 -0
- benchscope/server/api_test.py +64 -0
- benchscope/server/app.py +99 -0
- benchscope/server/state.py +18 -0
- benchscope/server/status.py +97 -0
- benchscope/server/test_manager.py +367 -0
- benchscope/server/ws.py +56 -0
- benchscope/summary.py +148 -0
- benchscope/webui/assets/LogView-BDFIduo7.css +1 -0
- benchscope/webui/assets/LogView-BMsVPLVq.js +1 -0
- benchscope/webui/assets/MetricsCharts-D1wU0LbK.css +1 -0
- benchscope/webui/assets/MetricsCharts-DHO93JrC.js +1 -0
- benchscope/webui/assets/SettingsView-7uuTFqXU.css +1 -0
- benchscope/webui/assets/SettingsView-DJmFSQbU.js +1 -0
- benchscope/webui/assets/TestView-81VlxBZU.js +4 -0
- benchscope/webui/assets/TestView-BAwcOtR2.css +1 -0
- benchscope/webui/assets/antd-DWALckI0.js +478 -0
- benchscope/webui/assets/echarts-Bb6yjXMn.js +60 -0
- benchscope/webui/assets/index-Dfr0hp72.js +2 -0
- benchscope/webui/assets/index-EhoNt9Gv.css +1 -0
- benchscope/webui/assets/vue-Ch4zjUb1.js +37 -0
- benchscope/webui/index.html +19 -0
- benchscope-1.0.0.dist-info/METADATA +136 -0
- benchscope-1.0.0.dist-info/RECORD +43 -0
- benchscope-1.0.0.dist-info/WHEEL +5 -0
- benchscope-1.0.0.dist-info/entry_points.txt +2 -0
- benchscope-1.0.0.dist-info/licenses/LICENSE +176 -0
- benchscope-1.0.0.dist-info/top_level.txt +1 -0
benchscope/datasets.py
ADDED
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""数据集管理:ShareGPT 自动下载(modelscope)、自定义数据集上传与本地路径。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import logging
|
|
6
|
+
import shutil
|
|
7
|
+
import urllib.parse
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
import requests
|
|
12
|
+
|
|
13
|
+
from benchscope.constants import SHAREGPT_DATASET_ID
|
|
14
|
+
|
|
15
|
+
log = logging.getLogger("benchscope.datasets")
|
|
16
|
+
|
|
17
|
+
# 期望的 ShareGPT 文件(下载源)与转换后的 jsonl 文件名
|
|
18
|
+
SHAREGPT_SOURCE_CANDIDATES = [
|
|
19
|
+
"ShareGPT_V3_unfiltered_cleaned_split.json",
|
|
20
|
+
"ShareGPT_V3_unfiltered_cleaned_split.jsonl",
|
|
21
|
+
"sharegpt_v3_unfiltered_cleaned_split.jsonl",
|
|
22
|
+
"sharegpt.jsonl",
|
|
23
|
+
]
|
|
24
|
+
SHAREGPT_JSONL_NAME = "ShareGPT_V3_unfiltered_cleaned_split.jsonl"
|
|
25
|
+
|
|
26
|
+
MODELSCOPE_API = "https://modelscope.cn/api/v1/datasets/{namespace}/{name}/repo"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _split_dataset_id(dataset_id: str) -> tuple[str, str]:
|
|
30
|
+
parts = dataset_id.split("/", 1)
|
|
31
|
+
if len(parts) != 2:
|
|
32
|
+
raise ValueError(f"无效的 modelscope 数据集 id: {dataset_id}")
|
|
33
|
+
return parts[0], parts[1]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def list_dataset_files(dataset_id: str, revision: str = "master", timeout: int = 30) -> list[dict]:
|
|
37
|
+
"""通过 modelscope HTTP API 列出数据集文件(/repo/tree)。"""
|
|
38
|
+
namespace, name = _split_dataset_id(dataset_id)
|
|
39
|
+
url = MODELSCOPE_API.format(namespace=namespace, name=name) + "/tree"
|
|
40
|
+
params = {"Revision": revision, "Root": "", "Recursive": "true"}
|
|
41
|
+
resp = requests.get(url, params=params, timeout=timeout)
|
|
42
|
+
resp.raise_for_status()
|
|
43
|
+
data = resp.json()
|
|
44
|
+
files = (data.get("Data") or {}).get("Files") or []
|
|
45
|
+
out = []
|
|
46
|
+
for f in files:
|
|
47
|
+
if isinstance(f, dict) and f.get("Type") == "blob":
|
|
48
|
+
out.append(f)
|
|
49
|
+
return out
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def convert_json_array_to_jsonl(src: Path, dst: Path) -> Path:
|
|
53
|
+
"""流式把 JSON 数组文件转换为 jsonl(每行一个对象),内存占用低。
|
|
54
|
+
|
|
55
|
+
逐字符状态机跟踪对象边界与字符串(正确处理字符串内的 {} 与转义);
|
|
56
|
+
每个对象归一化为单行 json 输出(vllm/sglang 按行 json.loads 解析)。
|
|
57
|
+
"""
|
|
58
|
+
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
tmp = dst.with_suffix(dst.suffix + ".part")
|
|
60
|
+
with open(src, "r", encoding="utf-8", errors="replace") as fin, open(tmp, "w", encoding="utf-8") as fout:
|
|
61
|
+
obj_chars: list[str] = []
|
|
62
|
+
depth = 0
|
|
63
|
+
in_string = False
|
|
64
|
+
escaped = False
|
|
65
|
+
for chunk in iter(lambda: fin.read(65536), ""):
|
|
66
|
+
for ch in chunk:
|
|
67
|
+
if in_string:
|
|
68
|
+
if escaped:
|
|
69
|
+
escaped = False
|
|
70
|
+
elif ch == "\\":
|
|
71
|
+
escaped = True
|
|
72
|
+
elif ch == '"':
|
|
73
|
+
in_string = False
|
|
74
|
+
else:
|
|
75
|
+
if ch == '"':
|
|
76
|
+
in_string = True
|
|
77
|
+
elif ch == "{":
|
|
78
|
+
depth += 1
|
|
79
|
+
if depth == 1:
|
|
80
|
+
obj_chars = ["{"]
|
|
81
|
+
continue
|
|
82
|
+
elif ch == "}":
|
|
83
|
+
depth -= 1
|
|
84
|
+
if depth == 0:
|
|
85
|
+
try:
|
|
86
|
+
obj = json.loads("".join(obj_chars) + "}")
|
|
87
|
+
fout.write(json.dumps(obj, ensure_ascii=False) + "\n")
|
|
88
|
+
except Exception:
|
|
89
|
+
pass # 跳过损坏对象
|
|
90
|
+
obj_chars = []
|
|
91
|
+
continue
|
|
92
|
+
if depth > 0:
|
|
93
|
+
obj_chars.append(ch)
|
|
94
|
+
tmp.replace(dst)
|
|
95
|
+
return dst
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def download_file(
|
|
99
|
+
dataset_id: str, file_path: str, dest: Path, revision: str = "master", timeout: int = 60
|
|
100
|
+
) -> Path:
|
|
101
|
+
"""下载 modelscope 数据集中的单个文件。"""
|
|
102
|
+
namespace, name = _split_dataset_id(dataset_id)
|
|
103
|
+
url = MODELSCOPE_API.format(namespace=namespace, name=name)
|
|
104
|
+
params = {"Revision": revision, "FilePath": file_path}
|
|
105
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
tmp = dest.with_suffix(dest.suffix + ".part")
|
|
107
|
+
with requests.get(url, params=params, stream=True, timeout=timeout) as resp:
|
|
108
|
+
resp.raise_for_status()
|
|
109
|
+
with open(tmp, "wb") as f:
|
|
110
|
+
for chunk in resp.iter_content(chunk_size=1 << 20):
|
|
111
|
+
f.write(chunk)
|
|
112
|
+
tmp.replace(dest)
|
|
113
|
+
return dest
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def ensure_sharegpt(datasets_dir: Path, force: bool = False) -> Path:
|
|
117
|
+
"""确保 ShareGPT jsonl 已下载并转换缓存,返回 jsonl 路径。"""
|
|
118
|
+
cache_dir = datasets_dir / "sharegpt"
|
|
119
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
jsonl_path = cache_dir / SHAREGPT_JSONL_NAME
|
|
121
|
+
|
|
122
|
+
# 已缓存的 jsonl
|
|
123
|
+
if not force and jsonl_path.exists() and jsonl_path.stat().st_size > 0:
|
|
124
|
+
return jsonl_path
|
|
125
|
+
|
|
126
|
+
# 已下载但未转换的源文件
|
|
127
|
+
source = None
|
|
128
|
+
if not force:
|
|
129
|
+
for cand in SHAREGPT_SOURCE_CANDIDATES:
|
|
130
|
+
p = cache_dir / cand
|
|
131
|
+
if p.exists() and p.stat().st_size > 0:
|
|
132
|
+
source = p
|
|
133
|
+
break
|
|
134
|
+
if source and jsonl_path.exists() and jsonl_path.stat().st_size > 0:
|
|
135
|
+
return jsonl_path
|
|
136
|
+
|
|
137
|
+
# 尝试 modelscope SDK(若安装)
|
|
138
|
+
try:
|
|
139
|
+
from modelscope import snapshot_download # type: ignore
|
|
140
|
+
|
|
141
|
+
root = Path(snapshot_download(SHAREGPT_DATASET_ID, cache_dir=str(cache_dir)))
|
|
142
|
+
if source is None:
|
|
143
|
+
for cand in SHAREGPT_SOURCE_CANDIDATES:
|
|
144
|
+
p = root / cand
|
|
145
|
+
if p.exists():
|
|
146
|
+
source = p
|
|
147
|
+
break
|
|
148
|
+
if source is None:
|
|
149
|
+
jsonls = sorted(root.rglob("*.json*"))
|
|
150
|
+
if jsonls:
|
|
151
|
+
source = jsonls[0]
|
|
152
|
+
except ImportError:
|
|
153
|
+
pass
|
|
154
|
+
except Exception as e:
|
|
155
|
+
log.warning("modelscope SDK 下载失败,回退 HTTP 下载: %s", e)
|
|
156
|
+
|
|
157
|
+
# HTTP API 方式
|
|
158
|
+
if source is None:
|
|
159
|
+
files = list_dataset_files(SHAREGPT_DATASET_ID)
|
|
160
|
+
cands = []
|
|
161
|
+
for f in files:
|
|
162
|
+
path = f.get("Path") or ""
|
|
163
|
+
if path.endswith((".json", ".jsonl")):
|
|
164
|
+
cands.append((path, f.get("Size") or 0))
|
|
165
|
+
if not cands:
|
|
166
|
+
raise FileNotFoundError(f"数据集 {SHAREGPT_DATASET_ID} 中未找到数据文件: {files[:5]}")
|
|
167
|
+
# 优先文件名含 sharegpt,其次最大文件
|
|
168
|
+
cands.sort(key=lambda x: (0 if "sharegpt" in x[0].lower() else 1, -x[1]))
|
|
169
|
+
candidate, cand_size = cands[0]
|
|
170
|
+
source = cache_dir / Path(candidate).name
|
|
171
|
+
if not source.exists() or source.stat().st_size == 0 or force:
|
|
172
|
+
log.info("正在从 modelscope 下载 %s(约 %.0f MB)...", candidate, cand_size / 1e6)
|
|
173
|
+
download_file(SHAREGPT_DATASET_ID, candidate, source, timeout=300)
|
|
174
|
+
|
|
175
|
+
# 转换为 jsonl
|
|
176
|
+
if jsonl_path.exists() and jsonl_path.stat().st_size > 0 and not force:
|
|
177
|
+
return jsonl_path
|
|
178
|
+
if source.suffix.lower() == ".jsonl":
|
|
179
|
+
if source != jsonl_path:
|
|
180
|
+
import shutil as _shutil
|
|
181
|
+
|
|
182
|
+
_shutil.copyfile(source, jsonl_path)
|
|
183
|
+
else:
|
|
184
|
+
log.info("正在转换 %s 为 jsonl ...", source.name)
|
|
185
|
+
convert_json_array_to_jsonl(source, jsonl_path)
|
|
186
|
+
return jsonl_path
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def is_valid_jsonl(path: Path, sample: int = 2000) -> bool:
|
|
190
|
+
"""粗略校验 jsonl 文件格式。"""
|
|
191
|
+
try:
|
|
192
|
+
with open(path, "r", encoding="utf-8", errors="replace") as f:
|
|
193
|
+
for _ in range(sample):
|
|
194
|
+
line = f.readline()
|
|
195
|
+
if not line:
|
|
196
|
+
break
|
|
197
|
+
json.loads(line)
|
|
198
|
+
return True
|
|
199
|
+
except Exception:
|
|
200
|
+
return False
|
benchscope/gpu.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""GPU 信息自动获取(nvidia-smi),失败时回退手动配置。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def detect_gpu() -> Optional[dict]:
|
|
10
|
+
"""尝试自动获取 GPU 型号与数量。成功返回 {"name":..., "count":N},失败返回 None。"""
|
|
11
|
+
nvidia_smi = shutil.which("nvidia-smi")
|
|
12
|
+
if not nvidia_smi:
|
|
13
|
+
return None
|
|
14
|
+
try:
|
|
15
|
+
result = subprocess.run(
|
|
16
|
+
[nvidia_smi, "--query-gpu=name,count", "--format=csv,noheader,nounits"],
|
|
17
|
+
capture_output=True,
|
|
18
|
+
text=True,
|
|
19
|
+
timeout=10,
|
|
20
|
+
)
|
|
21
|
+
if result.returncode != 0:
|
|
22
|
+
return None
|
|
23
|
+
lines = [ln.strip() for ln in result.stdout.splitlines() if ln.strip()]
|
|
24
|
+
if not lines:
|
|
25
|
+
return None
|
|
26
|
+
names = [ln.split(",")[0].strip() for ln in lines]
|
|
27
|
+
counts = []
|
|
28
|
+
for ln in lines:
|
|
29
|
+
parts = ln.split(",")
|
|
30
|
+
if len(parts) > 1:
|
|
31
|
+
try:
|
|
32
|
+
counts.append(int(parts[1].strip()))
|
|
33
|
+
except ValueError:
|
|
34
|
+
pass
|
|
35
|
+
name = names[0] if names else ""
|
|
36
|
+
count = sum(counts) if counts else len(lines)
|
|
37
|
+
return {"name": name, "count": count}
|
|
38
|
+
except Exception:
|
|
39
|
+
return None
|
benchscope/parser.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""bench 输出解析:同时解析 mean 与 P99 两套指标。
|
|
2
|
+
|
|
3
|
+
兼容 vLLM `vllm bench serve` 与 SGLang `sglang.bench_serving` 两种输出格式。
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
# 指标键(统一命名)
|
|
11
|
+
METRIC_KEYS = [
|
|
12
|
+
"output",
|
|
13
|
+
"peakoutput",
|
|
14
|
+
"total",
|
|
15
|
+
"ttft",
|
|
16
|
+
"itl",
|
|
17
|
+
"tpot",
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# 每个指标在输出中的可能标签(vllm / sglang 写法不同)
|
|
21
|
+
_PATTERNS: list[tuple[str, str, re.Pattern]] = []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _add(kind: str, metric: str, regex: str):
|
|
25
|
+
_PATTERNS.append((kind, metric, re.compile(regex, re.IGNORECASE)))
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# ---- vLLM bench serve ----
|
|
29
|
+
_add("mean", "output", r"Output token throughput \(tok/s\):\s+([\d.]+)")
|
|
30
|
+
_add("mean", "peakoutput", r"Peak output token throughput \(tok/s\):\s+([\d.]+)")
|
|
31
|
+
_add("mean", "total", r"Total token throughput \(tok/s\):\s+([\d.]+)")
|
|
32
|
+
_add("mean", "ttft", r"Mean TTFT \(ms\):\s+([\d.]+)")
|
|
33
|
+
_add("p99", "ttft", r"P99 TTFT \(ms\):\s+([\d.]+)")
|
|
34
|
+
_add("mean", "tpot", r"Mean TPOT \(ms\):\s+([\d.]+)")
|
|
35
|
+
_add("p99", "tpot", r"P99 TPOT \(ms\):\s+([\d.]+)")
|
|
36
|
+
_add("mean", "itl", r"Mean ITL \(ms\):\s+([\d.]+)")
|
|
37
|
+
_add("p99", "itl", r"P99 ITL \(ms\):\s+([\d.]+)")
|
|
38
|
+
_add("concurrency", "concurrency", r"Maximum request concurrency:\s+(\d+)")
|
|
39
|
+
|
|
40
|
+
# ---- SGLang bench_serving ----
|
|
41
|
+
_add("mean", "output", r"Output token throughput \(tok/s\):\s+([\d.]+)")
|
|
42
|
+
_add("mean", "total", r"Total token throughput \(tok/s\):\s+([\d.]+)")
|
|
43
|
+
_add("mean", "ttft", r"Time to first token \(TTFT\) mean \(ms\):\s+([\d.]+)")
|
|
44
|
+
_add("p99", "ttft", r"Time to first token \(TTFT\) p99 \(ms\):\s+([\d.]+)")
|
|
45
|
+
_add("mean", "tpot", r"Time per output token \(TPOT\) mean \(ms\):\s+([\d.]+)")
|
|
46
|
+
_add("p99", "tpot", r"Time per output token \(TPOT\) p99 \(ms\):\s+([\d.]+)")
|
|
47
|
+
_add("mean", "itl", r"Inter-token latency \(ITL\) mean \(ms\):\s+([\d.]+)")
|
|
48
|
+
_add("p99", "itl", r"Inter-token latency \(ITL\) p99 \(ms\):\s+([\d.]+)")
|
|
49
|
+
|
|
50
|
+
# 请求吞吐(req/s),sglang/vllm 均输出
|
|
51
|
+
_add("mean", "req_per_s", r"Request throughput \(req/s\):\s+([\d.]+)")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def parse_metrics(output: str) -> dict:
|
|
55
|
+
"""解析 bench 输出,返回统一指标字典。
|
|
56
|
+
|
|
57
|
+
返回结构:
|
|
58
|
+
{
|
|
59
|
+
"concurrency": int,
|
|
60
|
+
"output": float, "peakoutput": float, "total": float,
|
|
61
|
+
"ttft_mean": float, "tpot_mean": float, "itl_mean": float,
|
|
62
|
+
"ttft_p99": float, "tpot_p99": float, "itl_p99": float,
|
|
63
|
+
"req_per_s": float, "raw": str(完整原始输出)
|
|
64
|
+
}
|
|
65
|
+
"""
|
|
66
|
+
metrics: dict = {}
|
|
67
|
+
for kind, metric, pattern in _PATTERNS:
|
|
68
|
+
m = pattern.search(output)
|
|
69
|
+
if m:
|
|
70
|
+
key = metric if kind == "concurrency" else f"{metric}_{kind}"
|
|
71
|
+
try:
|
|
72
|
+
value = float(m.group(1)) if "." in m.group(1) else int(m.group(1))
|
|
73
|
+
except ValueError:
|
|
74
|
+
value = float(m.group(1))
|
|
75
|
+
metrics[key] = value
|
|
76
|
+
|
|
77
|
+
# 兼容只有 mean 没有 p99 的情况:p99 缺省回退 mean
|
|
78
|
+
for metric in ("ttft", "tpot", "itl"):
|
|
79
|
+
mean_key = f"{metric}_mean"
|
|
80
|
+
p99_key = f"{metric}_p99"
|
|
81
|
+
if mean_key in metrics and p99_key not in metrics:
|
|
82
|
+
metrics[p99_key] = metrics[mean_key]
|
|
83
|
+
|
|
84
|
+
# 单用户 QPS = 1000 / tpot_mean(README 定义)
|
|
85
|
+
if "tpot_mean" in metrics and metrics["tpot_mean"] > 0:
|
|
86
|
+
metrics["single_user"] = round(1000.0 / metrics["tpot_mean"], 2)
|
|
87
|
+
metrics["raw"] = output
|
|
88
|
+
return metrics
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def has_result_block(output: str) -> bool:
|
|
92
|
+
"""判断输出中是否包含结果块(可能成功或失败)。"""
|
|
93
|
+
return "Serving Benchmark Result" in output or "Benchmark Result" in output
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""配置 / 模型 / GPU / 状态 相关 API。"""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import logging
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from fastapi import APIRouter, HTTPException
|
|
8
|
+
from pydantic import BaseModel
|
|
9
|
+
|
|
10
|
+
from benchscope.constants import STATUS_READY
|
|
11
|
+
from benchscope.gpu import detect_gpu
|
|
12
|
+
from benchscope.server.state import state
|
|
13
|
+
|
|
14
|
+
log = logging.getLogger("benchscope.api_config")
|
|
15
|
+
|
|
16
|
+
router = APIRouter(prefix="/api/config", tags=["config"])
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@router.get("")
|
|
20
|
+
def get_config():
|
|
21
|
+
return state.config.snapshot()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConfigPatch(BaseModel):
|
|
25
|
+
api: dict | None = None
|
|
26
|
+
gpu: dict | None = None
|
|
27
|
+
logs_dir: str | None = None
|
|
28
|
+
datasets_dir: str | None = None
|
|
29
|
+
tpot_threshold_ms: float | None = None
|
|
30
|
+
request_rate: str | None = None
|
|
31
|
+
bench_commands: dict | None = None
|
|
32
|
+
framework: str | None = None
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@router.post("")
|
|
36
|
+
def update_config(patch: ConfigPatch):
|
|
37
|
+
data = patch.model_dump(exclude_none=True)
|
|
38
|
+
state.config.update(data)
|
|
39
|
+
return state.config.snapshot()
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@router.get("/status")
|
|
43
|
+
def get_status():
|
|
44
|
+
snap = state.monitor.check_once(broadcast=False)
|
|
45
|
+
snap["web"] = STATUS_READY
|
|
46
|
+
return snap
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.get("/models")
|
|
50
|
+
def get_models():
|
|
51
|
+
"""返回推理服务当前模型列表(优先用状态缓存,必要时实时探测)。"""
|
|
52
|
+
snap = state.monitor.check_once(broadcast=False)
|
|
53
|
+
if not snap["models"] and snap["error"]:
|
|
54
|
+
raise HTTPException(status_code=502, detail=f"推理服务不可达: {snap['error']}")
|
|
55
|
+
return {"models": snap["models"], "inference": snap["inference"], "error": snap["error"]}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class ConnTest(BaseModel):
|
|
59
|
+
base_url: str
|
|
60
|
+
endpoint: str = "/v1/chat/completions"
|
|
61
|
+
api_key: str = ""
|
|
62
|
+
extra_headers: dict = {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.post("/test-connection")
|
|
66
|
+
def test_connection(req: ConnTest):
|
|
67
|
+
base = req.base_url.rstrip("/")
|
|
68
|
+
headers = {}
|
|
69
|
+
if req.api_key:
|
|
70
|
+
headers["Authorization"] = f"Bearer {req.api_key}"
|
|
71
|
+
headers.update(req.extra_headers or {})
|
|
72
|
+
try:
|
|
73
|
+
resp = requests.get(f"{base}/v1/models", headers=headers, timeout=6)
|
|
74
|
+
resp.raise_for_status()
|
|
75
|
+
models = [m.get("id") for m in resp.json().get("data", []) if m.get("id")]
|
|
76
|
+
return {"ok": True, "models": models}
|
|
77
|
+
except Exception as e:
|
|
78
|
+
return {"ok": False, "error": str(e)[:300]}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.get("/gpu")
|
|
82
|
+
def get_gpu():
|
|
83
|
+
auto = detect_gpu()
|
|
84
|
+
cfg = state.config.get("gpu", {"auto": True, "name": "", "count": 8})
|
|
85
|
+
return {"auto_detected": auto, "config": cfg}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@router.get("/params/{framework}")
|
|
89
|
+
def get_params(framework: str):
|
|
90
|
+
"""返回指定框架的可配置参数定义(前端表单渲染用)。"""
|
|
91
|
+
if framework == "sglang":
|
|
92
|
+
from benchscope.benches.sglang_bench import CURATED_PARAMS
|
|
93
|
+
elif framework == "vllm":
|
|
94
|
+
from benchscope.benches.vllm_bench import CURATED_PARAMS
|
|
95
|
+
else:
|
|
96
|
+
raise HTTPException(status_code=404, detail="未知框架")
|
|
97
|
+
return {"framework": framework, "params": [p.__dict__ for p in CURATED_PARAMS]}
|