第 13 章 测试台:数字是怎么挣出来的
本书所有"实测数字"的生产现场。本章回答:压测台在哪、有哪些场景、数据长什么样、
用什么工具把日志变成表格,以及最重要的——六条测量纪律。
改引擎之前必须先读懂这一章,否则你测出来的数字会骗你(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,再矩阵;先唯一前缀,再谈复用。