第 13 章 测试台:数字是怎么挣出来的
MoE 大模型低成本部署框架第 13 章 测试台:数字是怎么挣出来的
CHAPTER 13

第 13 章 测试台:数字是怎么挣出来的

11 分钟阅读 · 2,849 字

本书所有"实测数字"的生产现场。本章回答:压测台在哪、有哪些场景、数据长什么样、
用什么工具把日志变成表格
,以及最重要的——六条测量纪律。
改引擎之前必须先读懂这一章,否则你测出来的数字会骗你(10 章 §10.5 已经演示过一次)。

口径:截至 2026-10-19,~/bench/ 工作树;场景数、端口、脚本清单均为当场 ls / 读源码核实。


13.1 测试台的形状

~/bench/(2026-10-04 建)是一套自足的 Python 压测台,venv 独立(httpx / PIL / numpy / matplotlib):

路径 作用
run.py 实验执行器:run.py <场景> [--cases ...] [--out DIR] [--repeat N];一个 run = 一个场景,场景内每个 case 一个目录,全局采样写 samples.jsonl
scenarios.py 场景定义(case 清单,见 13.2)
lib/ client.py(并发 burst、流式计时)、prompts.py(make_filler 造指定长度语料,内置唯一前缀)、sampler.py(GPU / 进程 RSS / cgroup 内存采样)、img.py、toolsets.py
analyze.py 把客户端记录、服务端 /metrics 记录、采样按请求对齐出表
charts.py 出图(CJK 字体在 bench/data/fonts/)
matrix_table.py / parse_engine_log.py 口径工具(13.6)
runs/ 原始数据(44 个 run 目录,13.5)
answers/ 长上下文测试的完整回答落盘处(控制台只给摘要)
report/ REPORT.md / tables.md(2026-10-04 首轮全量结果,已并入 Strata-本Pod部署全景.md 第六节)
data/ 语料与数据集缓存(corpus/、babilong/、字体)

鉴权口径:脚本一律 KEY = open("/home/cloud/logs/strata-api-key.txt").read().strip()——
key 从本地文件读,不写进代码、不进 git(17 章红线的实现方式之一)。

端口口径陷阱(2026-10-19 实测):run.py / smoke.py / soak_chain.py / freeze_ab.py 的默认
BASE 仍是 http://127.0.0.1:8090(双 lane 之前、引擎退到回环 8090 那个时代的默认值)。
当场 ss -ltnp 只看到 8080(网关,0.0.0.0)、8091(快道 A)、8092(普道 B),8090 无监听。
→ 跑老脚本要显式 STRATA_BASE= 或 --url 指到 8091/8092/8080,否则会连不上或测到空气。

13.2 场景清单:REGISTRY 里是 13 个

scenarios.py:281 的 REGISTRY 实测有 13 个场景(旧文档写"十场景"、memory 写 12 个,
都以这张表为准):

场景 测什么 关键参数(源码里的档位)
pilot 冒烟:每维度各一个最小 case,验证测试台与口径 conc-1 / conc-4 / ctx-8k / img-1 / tools-5 / reason-high / ignore-eos
conc 并发吞吐 n = 1, 2, 4, 6, 8, 12,每档 3 轮 × 256 tok,cooldown=8 warmup=1
ctx 上下文长度 → TTFT 2k / 8k / 32k / 64k / 128k / 200k / 250k;外加 ctx-64k-reuse(同一提示词连发 2 次看复用)
decode 解码速度 vs 上下文长度 40 → 250k 八档,固定生成 512 tok,单路、无缓存前缀
best 最优口径复现(外部宣称 130 tok/s) 短上下文 + 2048 tok 长生成 ×3
ctxconc 并发 × 长上下文(大 KV 挤批槽) 2×32k / 4×32k / 4×64k / 6×64k
img 图片张数 0 / 1 / 2 / 4 / 8 张真实截图
imgres 图片分辨率 256² / 512² / 1024² / 2048² / 4096² PNG + 2048×1152 JPEG
tools 工具 schema 体积 0 / 1 / 5 / 25 个工具 + 10 个长描述工具
toolloop agent 多轮工具调用、历史累积 4 轮(用固定桩拼历史)
reason 思考档 none / low / medium / high
lenrun 生成长度 → TPOT 漂移与 MTP 接受率 64 / 256 / 1024 / 4096
agentic 综合:图 + 工具 + 上下文增长的多轮会话 6 轮

每个 case 都带 cooldown(6–12s):给 GPU 与缓存一个回落窗口,避免上一 case 的尾巴污染下一格。

13.3 专项测试脚本(不是场景,是"一次性的判决实验")

脚本 判决什么 口径要点
speedtest.py 实时生成速度;--matrix 一键扫"上下文 × 并发"矩阵 每路同时给四种口径:端到端 tok/s(含 prefill 的体感)、解码 tok/s(首 token 后)、引擎自报 decode/prefill、TTFT + 命中缓存 token + MTP 接受率;长度用原生 tokenizer 构造
longctx_test.py 长上下文召回质量 + 速度(可用你自己的文档) 埋针自动判分(问题只问"键"不含"值");>262k 走网关组装;默认先预热(max_tokens=1 触发分块嵌入、等队列排空);每档连发两次,第二次测前缀复用(判据是 TTFT 骤降,引擎日志写 “N reused”);判分只计首次
niah_run.py 标准 NIAH(Kamradt 法) 语料 PaulGrahamEssays,针 “eat a sandwich”,深度 5–95%,长度用原生 tokenizer
babilong_run.py BABILong 1M(qa1…qa5,散落事实,最严档) 官方 prompt 格式 f"{input}\n{question}\nAnswer:",判"回复含 target"
dual_lane_test.py 双实例车道:solo-A/solo-B、1+1、2+2、iso(B 灌 60k 长 prefill 时测 A 的 TTFT) 直连 8091/8092 绕过代理;每路 512 tok、贪心、唯一前缀
channel_test.py 双通道(模型 ID 路由)经真实网关 needle / conc / ctxbatch(N 路 × k-token 全新长提示词,观察 prefill 串行化)/ mixed(A 测速同时 B 4 路并发,测隔离性)
freeze_ab.py 长 prefill 期间活跃流冻结多久 两条流正在流式时插入一条长而新鲜(未缓存)的提示词,报每路"最长 token 间隔"及落在读取窗口内的比例;同一命令跑两次 = 基线 vs STRATA_PREFILL_YIELD_CHUNKS
soak_chain.py 复现批链崩溃(生产形态浸泡) 4–6 个会话线程错峰启动、长短输出混合、贪心(走批链)与 temperature=1.0 top_k=40(走回退路径)混合采样、持续 M 分钟或直到引擎不可用
window_guard_live.py 窗口守卫的真引擎验收(G 档) 见 13.4
smoke.py 服务端工具调用 / 图像 / 流式 / 思考档的真实行为 非流式 + timings、流式事件、工具强制调用

编排脚本(把上面这些串成一条链):

脚本 串什么
ctx1m_full.sh 1M 上下文精度全量:B NIAH 全网格 → C BABILong → A 生产形态 300 轮 ×3300 tok → D 中文真实文档 1M 埋针 → E 一次性灌 1M 多轮历史 → F 单条 1M 随机噪声(已知会失败,作对照)→ H 外部出口一致性。串行跑、每步 sleep 15,日志落 runs/ctx1m-full-<stamp>/,目录名写进 runs/ctx1m-full-latest
run_guard.sh 窗口守卫 G 档:window_guard_live.py --model strata-b → runs/window-guard-latest/G_window_guard.log
speed_matrix.sh / mtp_ab.sh / single_ab.sh / datasets_exit.sh 矩阵扫描、MTP A/B、单路 A/B、数据集收尾

纪律写进脚本注释里(ctx1m_full.sh 原文):
“全部走 strata-b(普道 8092);不碰 strata-a(快道 8091,留给真实用户)。”

13.4 两类"必须用真引擎"的验收

① 窗口守卫(window_guard_live.py)——它存在的理由写在文件头:
离线 mock 测试(~/rag-proxy/test_window_guard.py)能证明判据算得对,
但"引擎自己数到多少"只有真引擎说了算——2026-10-08 生产翻车那次就是
网关记 219,021、引擎数到 231,775(差 5.8%)。做法:

  • 用模型原生 tokenizer 把提示词精确构造到指定 token 数(不是字符估算);
  • 同一份 body 先走网关 :8080(期望 200),再直连引擎 8092(期望 400)——
    对照组证明"没 400"是网关的功劳,不是引擎变宽松了;
  • 每次请求后读引擎 /metrics 的 prompt_tokens,断言 引擎 prompt + 送出去的 max_tokens + 8 ≤ 262144。

② 崩溃注入(2026-10-04 晚,批并发集成时的验收,docs/deployment/POD-ENV-NOTES.md :111):

注入 期望 实测
在途批请求时 kill -9 引擎 全部请求干净快速失败 全部 503
挂起的慢客户端 连接干净关闭 不串流、不悬挂
引擎自动重启 恢复服务 418 s 后恢复

同一批验收还包括:官方 tools/batch_test.py 逐 token 一致(精确档)、超槽 5 路全对题、断连不串流。

13.5 数据布局:一个 run 长什么样

bench/runs/<场景>-<MMDD>-<HHMMSS>/
├── run.json                 # 本次 run 的元数据(场景、case 顺序、参数)
├── samples.jsonl            # 全局采样:ts / 每卡 used_mib+util / 各进程 rss+pss+read_bytes / cgroup_mem_mib / 新 metrics 记录
├── joined.jsonl             # 对齐后的合并记录(analyze.py 的输入之一)
└── <case 名>/               # 每个 case 一个目录,如 conc-4/ ctx-8k/ img-1/ tools-5/ reason-high/ ignore-eos/
    ├── requests.jsonl       # 每条请求:请求体、客户端时间戳、TTFT、总时长、token 计数、错误
    └── case.json            # 该 case 的元数据(并发数、cooldown、目标上下文长度等)

采样行的实际形状(pilot-1004-143140/samples.jsonl 首行):
{"ts":…, "gpu":[{"i":0,"used_mib":22266,"free_mib":1816,"util":86}, …], "procs":{"engine":{"rss_kb":89036116,"pss_kb":…,"read_bytes":43655221248,…}}, …}
——GPU 利用率、显存、进程 RSS、磁盘读字节、cgroup 内存同时刻抓下来,才能事后解释"这格为什么慢"。

runs/ 里还有 chain.log、conc.log、soak-1937.log 这类散日志,以及
ctx1m-full-latest、window-guard-latest 两个指针文件(内容是一行目录路径)——
编排脚本靠它们把日志写进当次 run 目录。

13.6 把日志变成表格的三个工具

工具 输入 输出
analyze.py runs/xxx [runs/yyy …] [--md 输出.md] run 目录 客户端记录 × 服务端 /metrics × 采样,按请求对齐出表(case 顺序取自 run.json)
matrix_table.py runs/speed-ext/matrix-exit-1802.log speedtest.py --matrix 的日志 markdown 表:上下文 × 并发 → 每路解码 tok/s、聚合、TTFT、MTP 接受
parse_engine_log.py --since 18:03 [--csv out.csv] [--min-gen 2] 引擎明细日志 每条请求的 prefill/decode/MTP 接受率

parse_engine_log.py 解析的就是 12 章那条命门日志(正则原文):

strata serve: prompt 1000 tokens = 0 reused + 1000 read in 755 ms (1324.1 tok/s),
              256 generated in 2116 ms (121.0 tok/s), drafts accepted 112 of 225, 1 checkpoints

它没有时间戳(脚本里的 TS 正则只在有前缀时生效),所以跨进程对齐必须借
~/deploy/logs/rag-proxy.out.log 的 ctx[...] 时间戳(12 章 §12.7)。
--min-gen 2 用来滤掉探针请求。

13.7 六条测量纪律

这一节是本章的全部意义所在——前五条都是踩出来的。

# 纪律 凭什么是纪律
1 长度用模型原生 tokenizer 构造,不用字符估算、不用 tiktoken 网关上界估算对英文高估 ~3.3×、客户端对中文低估 ~2.4×(3.7 章);估算造不出"恰好 262,144"这种判决点
2 每条请求唯一前缀(prompts.make_filler 已内置) 前缀复用能把 TTFT 从 13.4s 打到 0.79s(12 章);不隔离缓存,所有 TTFT 数字都是假象
3 并发下以客户端流式时间戳为准 批槽下 /metrics 每批只留一条请求记录(engine.last 共享),decode_tok_s 会出现 0.1 tok/s 这种假值(10 章 §10.5)
4 A/B 交替顺序,并发改动必须过官方 tools/batch_test.py 的逐 token 一致性 总览 §协作约定 3;batch_test 要独占 GPU,只能在停服窗口跑(docs/reports/pr559-review.md)
5 >262k 先预热再判分,判分只计首次请求 嵌入是后台异步的,不预热首次组装的检索块是空的,会假判"未命中";第二次请求是缓存测试
6 压测只打普道 8092,快道留给真实用户 ctx1m_full.sh 注释原文;10-07 那次驱逐风暴的第二燃料正是压测流量与真实用户共享普道(12 章 §12.4)

另外两条技术性兜底:手工构造 body 时 _sse() 会补 stream: true 与
stream_options.include_usage(漏了会整包 JSON 返回,TTFT == 总时长);
run.py 的 snapshot() 会在每个 case 前后抓 GPU / 进程 / cgroup,事后能区分"引擎慢"与"机器被别人占了"。

还有一条前置动作(memory strata-bench-harness):新实验先跑一个 pilot 验证口径,
再跑正式矩阵。

13.8 本章小结

  • 测试台 = run.py + 13 个场景 + 一批判决脚本 + runs/ 原始数据 + 三个口径工具;
    它独立于服务,venv 独立,key 从本地文件读。
  • 默认端口 8090 已经过期(现只有 8080/8091/8092)——老脚本要显式指路。
  • 数据布局是 run.json + samples.jsonl + <case>/{requests.jsonl, case.json};
    采样把 GPU/内存/IO 与请求同时刻记下,这是事后解释慢的唯一依据。
  • 两类必须真引擎的验收:窗口守卫(网关 200 / 引擎 400 的对照组)与崩溃注入(kill -9 → 503 → 418s 自愈)。
  • 六条纪律里任何一条破掉,数字都会以你察觉不到的方向失真——先 pilot,再矩阵;先唯一前缀,再谈复用。
全部章节