v2.0.0:多上下文长度测试(默认512/2048/8192/32768/131072)+测试前空转预热(不计速度)+解码输出默认128+每长度采样默认2+Excel(xlsx)导出+详情按长度分组+接口输入框图标美化+README文档
This commit is contained in:
@@ -48,43 +48,59 @@ class TestRunner(threading.Thread):
|
||||
def _run(self):
|
||||
provider = self.cfg.get("provider", "openai")
|
||||
model = self.cfg.get("model", "")
|
||||
n = max(1, int(self.gen.get("samples", 3)))
|
||||
target_tokens = max(16, int(self.gen.get("prompt_tokens", 2048)))
|
||||
max_tokens = max(1, int(self.gen.get("max_tokens", 256)))
|
||||
avoid_cache = bool(self.gen.get("avoid_cache"))
|
||||
gen = self.gen
|
||||
|
||||
# 上下文长度列表(支持手动自定义,默认 512/2048/8192/32768/131072)
|
||||
raw_lengths = gen.get("context_lengths") or []
|
||||
if not raw_lengths:
|
||||
# 兼容旧版单值配置
|
||||
raw_lengths = [int(gen.get("prompt_tokens", 2048))]
|
||||
lengths = sorted(set(int(x) for x in raw_lengths if int(x) >= 16)) or [2048]
|
||||
n = max(1, int(gen.get("samples", 2))) # 每个长度采样次数
|
||||
max_tokens = max(1, int(gen.get("max_tokens", 128))) # 解码输出长度
|
||||
avoid_cache = bool(gen.get("avoid_cache"))
|
||||
warmup = bool(gen.get("warmup", True)) # 测试前空转预热
|
||||
|
||||
self.log("INFO", "═══ 开始速度测试 ═══")
|
||||
self.log("INFO", "提供商: %s | 模型: %s" % (lp.PROVIDER_LABELS.get(provider, provider), model))
|
||||
self.log("INFO", "目标上文: %d tokens | 生成长度: %d tokens | 采样: %d 次 | 避免缓存: %s"
|
||||
% (target_tokens, max_tokens, n, "开" if avoid_cache else "关"))
|
||||
self.log("INFO", "上下文长度: %s tokens | 生成长度: %d tokens | 每个长度采样: %d 次 | 预热: %s | 避免缓存: %s"
|
||||
% (" / ".join(str(x) for x in lengths), max_tokens, n,
|
||||
"开" if warmup else "关", "开" if avoid_cache else "关"))
|
||||
|
||||
ratio = self._calibrate()
|
||||
self.ratio = ratio
|
||||
base_prompt = self._build_prompt(target_tokens, ratio)
|
||||
self.log("INFO", "构造基准提示词完成,目标约 %d tokens" % target_tokens)
|
||||
self.log("INFO", "校准完成: %.3f tok/字符(%.2f 字符/token)" % (ratio, 1.0 / ratio))
|
||||
|
||||
for i in range(1, n + 1):
|
||||
for L in lengths:
|
||||
if self.should_stop():
|
||||
raise StopRequested()
|
||||
prompt = self._finalize_prompt(base_prompt)
|
||||
self.log("INFO", "── 采样 %d/%d 开始 ──" % (i, n))
|
||||
try:
|
||||
m = lp.call_stream(
|
||||
self.cfg, prompt,
|
||||
{"max_tokens": max_tokens, "avoid_cache": avoid_cache},
|
||||
log=lambda lv, msg: self.log(lv, msg),
|
||||
should_stop=self.should_stop)
|
||||
m["run_index"] = i
|
||||
self.samples.append({"run_index": i, "ok": True, "metrics": m})
|
||||
db.add_run(self.test_id, i, m)
|
||||
self.log("METRIC", self._fmt_metric(i, n, m))
|
||||
except StopRequested:
|
||||
raise
|
||||
except ProviderError as e:
|
||||
self.log("ERROR", "采样 %d/%d 失败: %s" % (i, n, e))
|
||||
self.samples.append({"run_index": i, "ok": False, "error": str(e)})
|
||||
db.add_run(self.test_id, i, {}, str(e))
|
||||
raise e
|
||||
base_prompt = self._build_prompt(L, ratio)
|
||||
self.log("INFO", "▸▸ 上下文长度 %d tokens(基准提示词构造完成)" % L)
|
||||
if warmup:
|
||||
self._warmup(base_prompt)
|
||||
for i in range(1, n + 1):
|
||||
if self.should_stop():
|
||||
raise StopRequested()
|
||||
prompt = self._finalize_prompt(base_prompt)
|
||||
self.log("INFO", "── [%d tok] 采样 %d/%d 开始 ──" % (L, i, n))
|
||||
try:
|
||||
m = lp.call_stream(
|
||||
self.cfg, prompt,
|
||||
{"max_tokens": max_tokens, "avoid_cache": avoid_cache},
|
||||
log=lambda lv, msg: self.log(lv, msg),
|
||||
should_stop=self.should_stop)
|
||||
m["run_index"] = i
|
||||
m["context_length"] = L
|
||||
self.samples.append({"run_index": i, "context_length": L, "ok": True, "metrics": m})
|
||||
db.add_run(self.test_id, i, m, context_length=L)
|
||||
self.log("METRIC", self._fmt_metric(L, i, n, m))
|
||||
except StopRequested:
|
||||
raise
|
||||
except ProviderError as e:
|
||||
self.log("ERROR", "[%d tok] 采样 %d/%d 失败: %s" % (L, i, n, e))
|
||||
self.samples.append({"run_index": i, "context_length": L, "ok": False, "error": str(e)})
|
||||
db.add_run(self.test_id, i, {}, str(e), context_length=L)
|
||||
raise e
|
||||
|
||||
summary = self._make_summary()
|
||||
db.update_status(self.test_id, "done", summary=summary)
|
||||
@@ -94,6 +110,20 @@ class TestRunner(threading.Thread):
|
||||
summary.get("avg_prefill_speed") or 0,
|
||||
summary.get("avg_decode_speed") or 0))
|
||||
|
||||
def _warmup(self, base_prompt):
|
||||
"""空转预热:不计入任何速度统计,用于避免冷启动/首次请求偏慢影响采样"""
|
||||
self.log("INFO", "预热(空转,不计速度)...")
|
||||
try:
|
||||
lp.call_stream(self.cfg, base_prompt,
|
||||
{"max_tokens": 8, "avoid_cache": False},
|
||||
log=lambda lv, msg: self.log(lv, msg),
|
||||
should_stop=self.should_stop)
|
||||
self.log("INFO", "预热完成(不纳入统计)")
|
||||
except StopRequested:
|
||||
raise
|
||||
except Exception as e:
|
||||
self.log("WARN", "预热失败(继续测试): %s" % e)
|
||||
|
||||
# ───────────────────────── 工具方法 ─────────────────────────
|
||||
|
||||
def _calibrate(self):
|
||||
@@ -129,15 +159,15 @@ class TestRunner(threading.Thread):
|
||||
return "[cache-bust %s]\n%s" % (uuid.uuid4().hex, base)
|
||||
return base
|
||||
|
||||
def _fmt_metric(self, i, n, m):
|
||||
return ("采样 %d/%d 完成 | 提示词 %d tok | 缓存 %d tok | 首字 %s ms | 预填充 %s tok/s"
|
||||
def _fmt_metric(self, L, i, n, m):
|
||||
return ("[%d tok] 采样 %d/%d 完成 | 提示词 %d tok | 缓存 %d tok | 首字 %s ms | 预填充 %s tok/s"
|
||||
" | 输出 %d tok | 解码 %s tok/s | 总耗时 %s ms"
|
||||
% (i, n, m.get("prompt_tokens") or 0, m.get("cached_tokens") or 0,
|
||||
% (L, i, n, m.get("prompt_tokens") or 0, m.get("cached_tokens") or 0,
|
||||
m.get("ttft_ms"), m.get("prefill_speed"), m.get("output_tokens") or 0,
|
||||
m.get("decode_speed"), m.get("total_ms")))
|
||||
|
||||
def _make_summary(self):
|
||||
ok = [s["metrics"] for s in self.samples if s.get("ok")]
|
||||
ok = [s for s in self.samples if s.get("ok")]
|
||||
base = {
|
||||
"provider": self.cfg.get("provider"),
|
||||
"model": self.cfg.get("model"),
|
||||
@@ -149,19 +179,36 @@ class TestRunner(threading.Thread):
|
||||
if not ok:
|
||||
return base
|
||||
|
||||
def avg(k):
|
||||
vals = [m[k] for m in ok if m.get(k) is not None]
|
||||
def avg(ms, k):
|
||||
vals = [m[k] for m in ms if m.get(k) is not None]
|
||||
return round(statistics.mean(vals), 1) if vals else None
|
||||
|
||||
# 按上下文长度分组汇总
|
||||
by_length = {}
|
||||
for L in sorted(set(s["context_length"] for s in ok)):
|
||||
group = [s["metrics"] for s in ok if s["context_length"] == L]
|
||||
by_length[L] = {
|
||||
"samples_total": sum(1 for s in self.samples if s["context_length"] == L),
|
||||
"samples_ok": len(group),
|
||||
"avg_ttft_ms": avg(group, "ttft_ms"),
|
||||
"avg_prefill_speed": avg(group, "prefill_speed"),
|
||||
"avg_decode_speed": avg(group, "decode_speed"),
|
||||
"avg_prompt_tokens": avg(group, "prompt_tokens"),
|
||||
"avg_output_tokens": avg(group, "output_tokens"),
|
||||
"avg_total_ms": avg(group, "total_ms"),
|
||||
}
|
||||
|
||||
okm = [s["metrics"] for s in ok]
|
||||
summary = dict(base)
|
||||
summary.update({
|
||||
"avg_ttft_ms": avg("ttft_ms"),
|
||||
"avg_prefill_speed": avg("prefill_speed"),
|
||||
"avg_decode_speed": avg("decode_speed"),
|
||||
"avg_prompt_tokens": avg("prompt_tokens"),
|
||||
"avg_output_tokens": avg("output_tokens"),
|
||||
"avg_cached_tokens": avg("cached_tokens"),
|
||||
"avg_total_ms": avg("total_ms"),
|
||||
"best_ttft_ms": min([m["ttft_ms"] for m in ok if m.get("ttft_ms") is not None], default=None),
|
||||
"by_length": by_length,
|
||||
"avg_ttft_ms": avg(okm, "ttft_ms"),
|
||||
"avg_prefill_speed": avg(okm, "prefill_speed"),
|
||||
"avg_decode_speed": avg(okm, "decode_speed"),
|
||||
"avg_prompt_tokens": avg(okm, "prompt_tokens"),
|
||||
"avg_output_tokens": avg(okm, "output_tokens"),
|
||||
"avg_cached_tokens": avg(okm, "cached_tokens"),
|
||||
"avg_total_ms": avg(okm, "total_ms"),
|
||||
"best_ttft_ms": min([m["ttft_ms"] for m in okm if m.get("ttft_ms") is not None], default=None),
|
||||
})
|
||||
return summary
|
||||
Reference in New Issue
Block a user