Compare commits

..
3 Commits
4 changed files with 45 additions and 9 deletions
+5 -3
View File
@@ -114,11 +114,12 @@
},
"gen": {
"name": "Qwen3 不同上下文长度速度对比",
"context_lengths": [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072],
"context_lengths": [4096, 8192, 16384, 32768, 65536, 98304, 131072],
"max_tokens": 128,
"samples": 2,
"warmup": true,
"avoid_cache": true
"avoid_cache": true,
"interval": 5
}
}
```
@@ -128,12 +129,13 @@
| 字段 | 类型 | 默认 | 说明 |
|------|------|------|------|
| `name` | string | `""` | 测试名称/主题(会存入测试记录并展示在历史与详情) |
| `context_lengths` | number[] | `[512,2048,4096,8192,16384,32768,65536,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 |
| `context_lengths` | number[] | `[4096,8192,16384,32768,65536,98304,131072]` | 要测试的上下文长度列表,每个长度独立校准+预热+采样 |
| `max_tokens` | number | `128` | 解码输出 token 长度 |
| `samples` | number | `2` | 每个(长度×并发)组合的采样次数 |
| `concurrency_levels` | number[] | `[1]` | 并发数列表(默认单流)。>1 时每采样同时发起 N 个并行流,聚合为整批吞吐指标;多档自动并排对比 |
| `warmup` | bool | `true` | 测试前空转预热(不计速度,按并发数预热) |
| `avoid_cache` | bool | `true` | 随机前缀避免缓存命中(每个并发流独立前缀) |
| `interval` | number | `5` | 每次采样之间的间隔秒数,让接口空闲休息(0 表示不等待) |
**响应:** `{ "ok": true, "id": 9 }`
+5
View File
@@ -130,6 +130,11 @@
<div class="icon-input"><span class="icon"></span><input id="gen-timeout" type="number" min="30" step="30" value="1800"></div>
<div class="hint">连接测试与速度采样共用:超过此时长仍未收到数据才判定超时(默认 1800 秒 = 30 分钟)。测慢接口/超长上下文时建议调大,如 3600。</div>
</div>
<div class="field">
<label>测试间隔(秒)<span class="hint-inline">每次采样之间让接口空闲</span></label>
<div class="icon-input"><span class="icon"></span><input id="gen-interval" type="number" min="0" step="1" value="5"></div>
<div class="hint">每次采样完成后等待 N 秒再发下一次请求,让大模型接口空闲休息一下(默认 5 秒,设为 0 关闭)。</div>
</div>
<div class="btn-group">
<button class="btn primary block" id="btn-start">▶ 开始测试</button>
<button class="btn danger block" id="btn-cancel" disabled>■ 停止</button>
+9 -3
View File
@@ -19,10 +19,11 @@ const STATUS_LABEL = {
let currentTestId = null; // 正在跑的测试 id
let pollTimer = null;
let lastLogId = 0;
let polling = false; // 防止轮询请求重叠:上一请求未返回前不再发,避免同批日志被追加两次
let consoleLogs = []; // 当前测试已加载日志 [{id,level,msg,rel}]
// 上下文长度:chips 列表 + 启用集合(默认 512/2048/4096/8192/16384/32768/65536/131072
const DEFAULT_CONTEXT_LENGTHS = [512, 2048, 4096, 8192, 16384, 32768, 65536, 131072];
// 上下文长度:chips 列表 + 启用集合(默认 4096/8192/16384/32768/65536/98304/131072
const DEFAULT_CONTEXT_LENGTHS = [4096, 8192, 16384, 32768, 65536, 98304, 131072];
let contextLengths = [...DEFAULT_CONTEXT_LENGTHS];
let contextLengthsActive = new Set(contextLengths);
@@ -70,6 +71,7 @@ function currentGen() {
avoid_cache: $("#gen-avoid-cache").checked,
warmup: $("#gen-warmup").checked,
read_timeout: parseInt($("#gen-timeout").value) || 1800,
interval: Math.max(0, parseInt($("#gen-interval").value) || 5),
};
}
@@ -326,6 +328,7 @@ function appendLogs(logs) {
html += `<div class="ln ${esc(l.level)}"><span class="ts">[${l.rel.toFixed(3)}s]</span> ${esc(l.msg)}</div>`;
}
box.insertAdjacentHTML("beforeend", html);
consoleLogs.push(...logs); // 记录已加载日志,修正“日志条数”一直显示 0 条的问题
$("#log-count").textContent = `${consoleLogs.length}`;
if (atBottom) box.scrollTop = box.scrollHeight;
}
@@ -386,7 +389,8 @@ function startTest() {
}
async function pollLogs() {
if (currentTestId == null) return;
if (currentTestId == null || polling) return;
polling = true;
try {
const d = await api(`/api/tests/${currentTestId}/logs?after=${lastLogId}`);
if (d.logs && d.logs.length) {
@@ -399,6 +403,8 @@ async function pollLogs() {
}
} catch (e) {
/* 网络抖动忽略 */
} finally {
polling = false;
}
}
+26 -3
View File
@@ -32,6 +32,15 @@ class TestRunner(threading.Thread):
def log(self, level, msg):
db.add_log(self.test_id, level, msg)
def _sleep_interval(self, secs):
"""测试间隔等待:期间可被用户停止,返回 False 表示已被取消"""
deadline = time.time() + secs
while time.time() < deadline:
if self.should_stop():
return False
time.sleep(0.2)
return not self.should_stop()
# ───────────────────────── 主流程 ─────────────────────────
def run(self):
@@ -61,6 +70,11 @@ class TestRunner(threading.Thread):
max_tokens = max(1, int(gen.get("max_tokens", 128))) # 解码输出长度
avoid_cache = bool(gen.get("avoid_cache"))
warmup = bool(gen.get("warmup", True)) # 测试前空转预热
# 测试间隔(秒):每次采样之间让接口空闲休息,默认 5 秒,0 表示不等待
try:
interval = 5 if gen.get("interval") in (None, "") else max(0, float(gen.get("interval")))
except (TypeError, ValueError):
interval = 5
# 并发数列表(默认单流 [1];支持 2/4 及自定义,如 [1,2,4,8]
raw_concs = gen.get("concurrency_levels") or []
@@ -73,10 +87,10 @@ class TestRunner(threading.Thread):
if name:
self.log("INFO", "测试名称(主题): %s" % name)
self.log("INFO", "提供商: %s | 模型: %s" % (lp.PROVIDER_LABELS.get(provider, provider), model))
self.log("INFO", "上下文长度: %s tokens | 生成长度: %d tokens | 并发数: %s | 每个组合采样: %d 次 | 预热: %s | 避免缓存: %s"
self.log("INFO", "上下文长度: %s tokens | 生成长度: %d tokens | 并发数: %s | 每个组合采样: %d 次 | 预热: %s | 避免缓存: %s | 测试间隔: %g"
% (" / ".join(str(x) for x in lengths), max_tokens,
" / ".join(str(x) for x in concurrency_levels), n,
"" if warmup else "", "" if avoid_cache else ""))
"" if warmup else "", "" if avoid_cache else "", interval))
to = self.gen.get("read_timeout") or "默认(1800)"
self.log("INFO", "请求超时: 连接 %s s | 等待首字/预处理 %s s(接口慢可在左侧调大)"
% (self.gen.get("connect_timeout") or 60, to))
@@ -86,6 +100,8 @@ class TestRunner(threading.Thread):
self.log("INFO", "校准完成: %.3f tok/字符(%.2f 字符/token" % (ratio, 1.0 / ratio))
run_seq = 0
sample_done = 0 # 已完成的采样数,用于控制测试间隔(首个采样不等待)
last_conc = None # 只在实际切换并发档时打印一次表头,避免同一并发数重复刷屏
for L in lengths:
if self.should_stop():
raise StopRequested()
@@ -94,13 +110,20 @@ class TestRunner(threading.Thread):
for C in concurrency_levels:
if self.should_stop():
raise StopRequested()
self.log("INFO", "══ 并发数 %d(同时 %d 个流)══" % (C, C))
if C != last_conc:
self.log("INFO", "══ 并发数 %d(同时 %d 个流)══" % (C, C))
last_conc = C
if warmup:
self._warmup(base_prompt, C)
for i in range(1, n + 1):
if self.should_stop():
raise StopRequested()
if sample_done > 0 and interval > 0:
self.log("INFO", "⏳ 接口空闲休息 %g 秒后继续下一采样..." % interval)
if not self._sleep_interval(interval):
raise StopRequested()
run_seq += 1
sample_done += 1
self.log("INFO", "── [%d tok · 并发%d] 采样 %d/%d 开始 ──" % (L, C, i, n))
try:
m = self._run_sample(C, base_prompt, max_tokens, avoid_cache)