v2.2.0:data-chart-tool 双Y轴折线图(预填充左轴虚线/解码右轴实线)+画图CSV一键复制+下载PNG+默认上下文长度增加4096/16384/65536
This commit is contained in:
@@ -17,9 +17,10 @@
|
||||
3. [连接测试](#3-连接测试)
|
||||
4. [速度测试](#4-速度测试-tests)
|
||||
5. [测试详情与日志](#5-测试详情与日志)
|
||||
6. [导出(Excel / JSON)](#6-导出excel--json)
|
||||
7. [数据模型](#7-数据模型)
|
||||
8. [curl 使用示例](#8-curl-使用示例)
|
||||
6. [折线图(data-chart-tool)](#6-折线图data-chart-tool)
|
||||
7. [导出(Excel / JSON)](#7-导出excel--json)
|
||||
8. [数据模型](#8-数据模型)
|
||||
9. [curl 使用示例](#9-curl-使用示例)
|
||||
|
||||
---
|
||||
|
||||
@@ -113,7 +114,7 @@
|
||||
},
|
||||
"gen": {
|
||||
"name": "Qwen3 不同上下文长度速度对比",
|
||||
"context_lengths": [512, 2048, 8192, 32768, 131072],
|
||||
"context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
|
||||
"max_tokens": 128,
|
||||
"samples": 2,
|
||||
"warmup": true,
|
||||
@@ -127,7 +128,7 @@
|
||||
| 字段 | 类型 | 默认 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `name` | string | `""` | 测试名称/主题(会存入测试记录并展示在历史与详情) |
|
||||
| `context_lengths` | number[] | `[512,2048,8192,32768,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 |
|
||||
| `context_lengths` | number[] | `[512,2048,4096,8192,16384,32768,65536,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 |
|
||||
| `max_tokens` | number | `128` | 解码输出 token 长度 |
|
||||
| `samples` | number | `2` | 每个上下文长度的采样次数 |
|
||||
| `warmup` | bool | `true` | 测试前空转预热(不计速度) |
|
||||
@@ -208,7 +209,45 @@
|
||||
|
||||
---
|
||||
|
||||
## 6. 导出(Excel / JSON)
|
||||
## 6. 折线图(data-chart-tool)
|
||||
|
||||
> 调用 [data-chart-tool](http://192.168.2.8:12007/hz4th_coder/data-chart-tool.git) 的 `/api/chart` 接口生成**双Y轴折线图**:左轴=预填充速度(虚线)、右轴=解码速度(实线)、X 轴=上下文长度。本系统在服务端代理转发,前端只需请求本服务的两个接口。
|
||||
|
||||
### `GET /api/tests/<id>/chart`
|
||||
生成折线图 PNG 图片(内部调用 data-chart-tool `/api/chart`,地址可在 `config.py` 的 `CHART_API_BASE` 修改)。
|
||||
|
||||
**成功响应:** `Content-Type: image/png`(可直接用于 `<img>` / 下载)
|
||||
|
||||
**失败响应:**
|
||||
```json
|
||||
{ "ok": false, "error": "无成功采样数据,无法画图" } // 400
|
||||
{ "ok": false, "error": "测试不存在" } // 404
|
||||
{ "ok": false, "error": "图表服务不可用: ..." } // 502
|
||||
```
|
||||
|
||||
### `GET /api/tests/<id>/chart-data`
|
||||
返回画图所用的 CSV 数据与图表请求配置,方便手动快速复制。
|
||||
|
||||
**响应:**
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"csv": "上下文长度, 预填充速度(tok/s), 解码速度(tok/s)\n4096, 126.90, 22.80\n8192, 132.20, 22.40",
|
||||
"rows": [[4096, 126.9, 22.8], [8192, 132.2, 22.4]],
|
||||
"payload": {
|
||||
"data": "...", "chartType": "line", "title": "...",
|
||||
"dualYAxis": true,
|
||||
"leftAxisName": "预填充速度(tok/s)", "rightAxisName": "解码速度(tok/s)",
|
||||
"seriesTypes": ["line", "line"], "seriesAxis": [0, 1],
|
||||
"seriesStyles": ["dashed", "solid"], "width": 1000, "height": 560, "pixelRatio": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
> `csv` 即画图数据(第一列=上下文长度,第二列=预填充速度,第三列=解码速度),前端「复制画图数据」按钮复制的就是它。
|
||||
|
||||
---
|
||||
|
||||
## 7. 导出(Excel / JSON)
|
||||
|
||||
### `GET /api/tests/<id>/export.xlsx`
|
||||
导出 Excel 报告(**3 个 Sheet**:汇总 / 采样明细 / 日志),`Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet`。
|
||||
@@ -222,7 +261,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 7. 数据模型
|
||||
## 8. 数据模型
|
||||
|
||||
| 表 | 说明 | 关键字段 |
|
||||
|----|------|----------|
|
||||
@@ -236,7 +275,7 @@
|
||||
|
||||
---
|
||||
|
||||
## 8. curl 使用示例
|
||||
## 9. curl 使用示例
|
||||
|
||||
```bash
|
||||
BASE=http://<IP>:16097
|
||||
@@ -262,6 +301,11 @@ curl -X POST $BASE/api/tests -H 'Content-Type: application/json' -d '{
|
||||
curl "$BASE/api/tests?limit=10"
|
||||
curl $BASE/api/tests/9
|
||||
|
||||
# 画图数据(CSV)
|
||||
curl $BASE/api/tests/9/chart-data
|
||||
# 折线图 PNG(预填充左轴虚线 / 解码右轴实线)
|
||||
curl -o chart.png $BASE/api/tests/9/chart
|
||||
|
||||
# 导出
|
||||
curl -OJ $BASE/api/tests/9/export.xlsx
|
||||
curl $BASE/api/tests/9/export.json
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
- **访问地址:** `http://<IP>:16097/`
|
||||
- **技术栈:** Python 3 + Flask + SQLite(纯 REST,无额外依赖)
|
||||
- **版本:** v2.0.0
|
||||
- **版本:** v2.2.0
|
||||
|
||||
---
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
|
||||
### 🚀 速度测试配置
|
||||
- **测试名称(主题)**:可为每次测试命名,用于标注测试内容/主题,展示在历史列表、详情弹窗与导出报表中
|
||||
- **多上下文长度测试**:默认为 `512 / 2048 / 8192 / 32768 / 131072` tokens,可通过标签点击启用/禁用,并支持手动添加任意自定义长度(≥16)
|
||||
- **多上下文长度测试**:默认为 `512 / 2048 / 4096 / 8192 / 16384 / 32768 / 65536 / 131072` tokens,可通过标签点击启用/禁用,并支持手动添加任意自定义长度(≥16)
|
||||
- **解码输出长度(max tokens)**:默认为 `128`,可手动自定义
|
||||
- **每个长度采样次数**:默认为 `2`,可手动自定义
|
||||
- **测试前预热(空转)**:默认开启,先发一次不计速度的空转请求,避免冷启动/首请求偏慢污染真实采样数据
|
||||
@@ -31,6 +31,7 @@
|
||||
- 实时控制台日志:校准、预热、每次采样明细全程可追溯
|
||||
- **每次完整测试**支持:
|
||||
- **网页点击查看**:历史记录「查看」按钮弹出详情(整体统计 + 按上下文长度分组 + 每次采样明细 + 完整日志)
|
||||
- **📈 折线图(data-chart-tool)**:详情弹窗内点击「生成/刷新折线图」,调用 data-chart-tool 的 `/api/chart` 画**双Y轴折线图**——左轴=预填充速度(虚线),右轴=解码速度(实线),X 轴为上下文长度;并提供**画图 CSV 数据一键复制 / 下载 PNG**
|
||||
- **文件下载 Excel(xlsx)**:历史记录「Excel」按钮或详情弹窗「导出 Excel」,包含 汇总 / 采样明细 / 日志 三个 Sheet
|
||||
- **文件下载 JSON**:详情弹窗「导出 JSON」
|
||||
- 测试历史留存(含测试名称),可随时刷新、查看、导出、删除
|
||||
@@ -97,6 +98,8 @@ pip install -r requirements.txt
|
||||
| DELETE | `/api/tests/<id>` | 删除测试 |
|
||||
| GET | `/api/tests/<id>/export.xlsx` | 导出 Excel 报告(汇总/采样明细/日志三 Sheet) |
|
||||
| GET | `/api/tests/<id>/export.json` | 导出完整测试 JSON |
|
||||
| GET | `/api/tests/<id>/chart` | 用 data-chart-tool 生成折线图 PNG(预填充左轴虚线 / 解码右轴实线) |
|
||||
| GET | `/api/tests/<id>/chart-data` | 画图数据(CSV + 图表请求配置,供一键复制) |
|
||||
|
||||
> 完整字段说明、响应示例与 curl 示例见 **API.md**。
|
||||
|
||||
@@ -113,7 +116,7 @@ POST /api/tests
|
||||
"temperature": 0.7
|
||||
},
|
||||
"gen": {
|
||||
"context_lengths": [512, 2048, 8192, 32768, 131072],
|
||||
"context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
|
||||
"max_tokens": 128,
|
||||
"samples": 2,
|
||||
"warmup": true,
|
||||
@@ -161,4 +164,4 @@ llm-speed-tester/
|
||||
## Git
|
||||
|
||||
- **仓库:** `hz4th_coder/llm-speed-tester`
|
||||
- **版本:** v2.1.0(多上下文长度测试 + 预热 + Excel/JSON 导出 + 测试名称 + 整体统计平均/最小/最大 + 开放 API + 推理型模型兼容)
|
||||
- **版本:** v2.2.0(多上下文长度测试 + 预热 + Excel/JSON 导出 + 测试名称 + 整体统计平均/最小/最大 + 开放 API + 推理型模型兼容 + 📈 data-chart-tool 双Y轴折线图/画图数据复制 + 默认上下文长度增加 4096/16384/65536)
|
||||
@@ -3,6 +3,7 @@
|
||||
import io
|
||||
import json
|
||||
|
||||
import requests
|
||||
from flask import Flask, jsonify, request, send_file, send_from_directory
|
||||
|
||||
import config
|
||||
@@ -160,6 +161,88 @@ def del_test(tid):
|
||||
return jsonify({"ok": True})
|
||||
|
||||
|
||||
# ───────────────────────── 图表(data-chart-tool 折线图:预填充左轴虚线 / 解码右轴实线) ─────────────────────────
|
||||
|
||||
def _build_chart_csv(t):
|
||||
"""由测试汇总 by_length 构建画图 CSV:上下文长度, 预填充速度(tok/s), 解码速度(tok/s)"""
|
||||
s = t.get("summary") or {}
|
||||
by = s.get("by_length") or {}
|
||||
lens = sorted(int(k) for k in by)
|
||||
rows = []
|
||||
for L in lens:
|
||||
bl = by.get(str(L)) if str(L) in by else by.get(L) or {}
|
||||
pre = bl.get("avg_prefill_speed")
|
||||
dec = bl.get("avg_decode_speed")
|
||||
if pre is None or dec is None:
|
||||
continue
|
||||
rows.append([L, round(pre, 2), round(dec, 2)])
|
||||
if not rows:
|
||||
return None
|
||||
csv_lines = ["上下文长度, 预填充速度(tok/s), 解码速度(tok/s)"]
|
||||
for L, pre, dec in rows:
|
||||
csv_lines.append("%d, %.2f, %.2f" % (L, pre, dec))
|
||||
return {"csv": "\n".join(csv_lines), "rows": rows}
|
||||
|
||||
|
||||
def _chart_payload(t, csv_text):
|
||||
"""组装 data-chart-tool /api/chart 请求体(双Y轴折线图)"""
|
||||
title = ("%s %s" % (t.get("model") or "", t.get("name") or "速度对比")).strip()
|
||||
return {
|
||||
"data": csv_text,
|
||||
"chartType": "line",
|
||||
"title": title,
|
||||
"theme": "default",
|
||||
"showLegend": True,
|
||||
"showGrid": True,
|
||||
"showLabel": False,
|
||||
"smoothLine": True,
|
||||
"dualYAxis": True,
|
||||
"leftAxisName": "预填充速度(tok/s)",
|
||||
"rightAxisName": "解码速度(tok/s)",
|
||||
"seriesTypes": ["line", "line"],
|
||||
"seriesAxis": [0, 1],
|
||||
"seriesStyles": ["dashed", "solid"], # 预填充=左轴虚线,解码=右轴实线
|
||||
"width": 1000,
|
||||
"height": 560,
|
||||
"pixelRatio": 2,
|
||||
}
|
||||
|
||||
|
||||
@app.route("/api/tests/<int:tid>/chart-data")
|
||||
def test_chart_data(tid):
|
||||
t = db.get_test(tid)
|
||||
if not t:
|
||||
return jsonify({"ok": False, "error": "测试不存在"}), 404
|
||||
built = _build_chart_csv(t)
|
||||
if not built:
|
||||
return jsonify({"ok": False, "error": "无成功采样数据,无法画图"}), 400
|
||||
return jsonify({
|
||||
"ok": True,
|
||||
"csv": built["csv"],
|
||||
"rows": built["rows"],
|
||||
"payload": _chart_payload(t, built["csv"]),
|
||||
})
|
||||
|
||||
|
||||
@app.route("/api/tests/<int:tid>/chart")
|
||||
def test_chart(tid):
|
||||
"""用 data-chart-tool 生成折线图 PNG(预填充左轴虚线 / 解码右轴实线)"""
|
||||
t = db.get_test(tid)
|
||||
if not t:
|
||||
return jsonify({"ok": False, "error": "测试不存在"}), 404
|
||||
built = _build_chart_csv(t)
|
||||
if not built:
|
||||
return jsonify({"ok": False, "error": "无成功采样数据,无法画图"}), 400
|
||||
try:
|
||||
resp = requests.post(config.CHART_API_BASE + "/api/chart",
|
||||
json=_chart_payload(t, built["csv"]), timeout=60)
|
||||
except requests.RequestException as e:
|
||||
return jsonify({"ok": False, "error": "图表服务不可用: %s" % e}), 502
|
||||
if resp.status_code != 200:
|
||||
return jsonify({"ok": False, "error": "图表生成失败(%d): %s" % (resp.status_code, resp.text[:300])}), 502
|
||||
return send_file(io.BytesIO(resp.content), mimetype="image/png")
|
||||
|
||||
|
||||
@app.route("/api/tests/<int:tid>/export.json")
|
||||
def export_json(tid):
|
||||
t = db.get_test(tid)
|
||||
|
||||
@@ -13,3 +13,6 @@ DB_PATH = os.path.join(DATA_DIR, "llm_speed_tester.db")
|
||||
# 流式请求超时:连接 60s,两次数据包间隔最长 300s(推理型/长上文模型也够用)
|
||||
CONNECT_TIMEOUT = 60
|
||||
STREAM_READ_TIMEOUT = 300
|
||||
|
||||
# data-chart-tool 图表服务地址(用它的 /api/chart 画折线图:预填充左轴虚线 / 解码右轴实线)
|
||||
CHART_API_BASE = "http://127.0.0.1:16016"
|
||||
@@ -206,6 +206,18 @@ table.mini { width: 100%; border-collapse: collapse; font-size: 12.5px; }
|
||||
.detail-log .ln.ERROR { color: var(--danger); }
|
||||
.detail-log .ln.WARN { color: var(--warn); }
|
||||
|
||||
/* ── 图表(data-chart-tool 折线图) ── */
|
||||
.chart-block { background: var(--panel2); border: 1px solid var(--border); border-radius: 10px; padding: 12px; }
|
||||
.chart-toolbar { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||||
.chart-img-wrap { margin-top: 10px; min-height: 60px; display: flex; align-items: center; justify-content: center; border: 1px dashed var(--border); border-radius: 8px; padding: 10px; background: #0a0e17; }
|
||||
.chart-img-wrap img { max-width: 100%; border-radius: 6px; }
|
||||
.chart-data-head { margin-top: 10px; font-size: 12px; color: var(--muted); }
|
||||
.chart-csv {
|
||||
width: 100%; min-height: 120px; margin-top: 6px; background: #0a0e17;
|
||||
border: 1px solid var(--border); border-radius: 8px; color: #a8f0d0;
|
||||
font: 12px/1.7 var(--mono); padding: 10px; resize: vertical; outline: none;
|
||||
}
|
||||
|
||||
pre.json-box {
|
||||
background: #0a0e17; border: 1px solid var(--border); border-radius: 8px;
|
||||
padding: 10px; font: 11.5px/1.6 var(--mono); overflow: auto; max-height: 260px; color: #a8f0d0;
|
||||
|
||||
+58
-2
@@ -21,8 +21,8 @@ let pollTimer = null;
|
||||
let lastLogId = 0;
|
||||
let consoleLogs = []; // 当前测试已加载日志 [{id,level,msg,rel}]
|
||||
|
||||
// 上下文长度:chips 列表 + 启用集合(默认 512/2048/8192/32768/131072)
|
||||
const DEFAULT_CONTEXT_LENGTHS = [512, 2048, 8192, 32768, 131072];
|
||||
// 上下文长度:chips 列表 + 启用集合(默认 512/2048/4096/8192/16384/32768/65536/131072)
|
||||
const DEFAULT_CONTEXT_LENGTHS = [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072];
|
||||
let contextLengths = [...DEFAULT_CONTEXT_LENGTHS];
|
||||
let contextLengthsActive = new Set(contextLengths);
|
||||
|
||||
@@ -421,6 +421,23 @@ async function viewDetail(id) {
|
||||
<h3>📏 按上下文长度汇总</h3>
|
||||
${byLengthHtml}
|
||||
|
||||
<h3>📈 折线图(预填充左轴虚线 / 解码右轴实线)</h3>
|
||||
<div class="chart-block">
|
||||
<div class="chart-toolbar">
|
||||
<button class="btn small primary" id="dt-chart-gen">🎨 生成/刷新折线图</button>
|
||||
<span class="hint" id="dt-chart-status"></span>
|
||||
</div>
|
||||
<div class="chart-img-wrap" id="dt-chart-img">
|
||||
<div class="hint">点击上方按钮,用 data-chart-tool 生成双Y轴折线图(左轴=预填充虚线,右轴=解码实线)</div>
|
||||
</div>
|
||||
<div class="chart-data-head">📋 画图数据(CSV,可一键复制)</div>
|
||||
<textarea class="chart-csv" id="dt-chart-csv" readonly spellcheck="false" placeholder="(生成图表后自动填充,或手动复制)"></textarea>
|
||||
<div class="chart-toolbar" style="margin-top:6px">
|
||||
<button class="btn small" id="dt-chart-copy">📋 复制画图数据</button>
|
||||
<button class="btn small" id="dt-chart-download">⬇ 下载图表 PNG</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>⚙️ 测试参数</h3>
|
||||
<div class="kv">
|
||||
<div class="kv-item"><div class="kv-k">测试名称</div><div class="kv-v">${esc(t.name || "—")}</div></div>
|
||||
@@ -444,6 +461,45 @@ async function viewDetail(id) {
|
||||
$("#detail-mask").hidden = false;
|
||||
$("#dt-body").scrollTop = 0;
|
||||
window.__detail = t;
|
||||
|
||||
// 图表:生成/刷新 + 复制数据 + 下载
|
||||
const genBtn = $("#dt-chart-gen");
|
||||
const csvTa = $("#dt-chart-csv");
|
||||
const imgWrap = $("#dt-chart-img");
|
||||
const statusEl = $("#dt-chart-status");
|
||||
let chartUrl = "";
|
||||
const genChart = () => {
|
||||
statusEl.textContent = "⏳ 正在生成...";
|
||||
fetch(`/api/tests/${id}/chart-data`).then((r) => r.json()).then((d) => {
|
||||
if (!d.ok) { statusEl.textContent = "❌ " + (d.error || "生成失败"); return; }
|
||||
csvTa.value = d.csv;
|
||||
chartUrl = `/api/tests/${id}/chart?t=${Date.now()}`;
|
||||
const img = document.createElement("img");
|
||||
img.src = chartUrl;
|
||||
img.alt = "预填充/解码 折线图";
|
||||
img.style.maxWidth = "100%";
|
||||
img.onload = () => { imgWrap.innerHTML = ""; imgWrap.appendChild(img); statusEl.textContent = "✅ 生成完成"; };
|
||||
img.onerror = () => { statusEl.textContent = "❌ 图表生成失败"; };
|
||||
}).catch((e) => { statusEl.textContent = "❌ " + e.message; });
|
||||
};
|
||||
genBtn.addEventListener("click", genChart);
|
||||
$("#dt-chart-copy").addEventListener("click", () => {
|
||||
if (!csvTa.value) { toast("暂无画图数据,请先生成图表"); return; }
|
||||
csvTa.select();
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(csvTa.value).then(() => toast("画图数据已复制"));
|
||||
} else {
|
||||
document.execCommand("copy");
|
||||
toast("画图数据已复制");
|
||||
}
|
||||
});
|
||||
$("#dt-chart-download").addEventListener("click", () => {
|
||||
if (!chartUrl) { toast("请先生成图表"); return; }
|
||||
const a = document.createElement("a");
|
||||
a.href = chartUrl;
|
||||
a.download = `llm_speed_chart_${id}.png`;
|
||||
document.body.appendChild(a); a.click(); a.remove();
|
||||
});
|
||||
}
|
||||
|
||||
function closeDetail() { $("#detail-mask").hidden = true; }
|
||||
|
||||
@@ -51,7 +51,7 @@ class TestRunner(threading.Thread):
|
||||
model = self.cfg.get("model", "")
|
||||
gen = self.gen
|
||||
|
||||
# 上下文长度列表(支持手动自定义,默认 512/2048/8192/32768/131072)
|
||||
# 上下文长度列表(支持手动自定义,默认 512/2048/4096/8192/16384/32768/65536/131072)
|
||||
raw_lengths = gen.get("context_lengths") or []
|
||||
if not raw_lengths:
|
||||
# 兼容旧版单值配置
|
||||
|
||||
Reference in New Issue
Block a user