640 条请求全部成功,终端却退出了非零状态。该把这轮判成失败、重跑一次,还是继续等?
在一次 DeepSeek-V4-Flash / Ascend 910C 实验里,出错的是推理后的 native 统计子进程:它无法导入本轮自定义模块。原请求已经完成,服务仍然健康。补齐子进程环境后,对原始数据执行一次原 AISBench selector,才得到有效结果;没有重新发送模型请求。
从 shell 看,这条命令失败了;从模型看,640 条请求已经跑完。一个命令装进部署、预热、推理、采集和报告,最后却只有一个退出码。代理只盯着它,就会丢掉前面已经完成的工作,把健康服务也重启一遍。
上一篇聊的是下一轮怎么选。这一篇跟着一次实验走:配置怎样传到实际进程,请求怎样结束,数据怎样变成报告,中间出错了又从哪里接。例子来自这次本地工具链,后面的改进建议也从这些具体问题出发。
一、实验配置要能一路追到实际执行
配置文件写好了,后面还会经过几次处理:启动脚本追加参数,helper 设置环境,远端 runner 做检查,报告再读一份配置。只要其中一处沿用旧值,就会出现四处都写着“当前配置”,内容却对不上的情况。
这次就出现过基础脚本写 DP16,实际末尾参数覆盖为 DP4/CP4;也出现过外层设置了 dispatch 容量,helper 执行后又写回旧值。只检查默认脚本,无法证明最终运行身份。
排查这类问题时,我会把“打算执行什么”和“实际执行什么”并排看。左边是数据版本、输入输出长度、N/C/r、缓存和服务变更,右边是最终参数、实际加载的源码与布局、对应运行实例。结果挂到某个候选名下时,这两边就该能对上。
若重新整理配置,我会先保留下面这些字段。这里用的是示意格式:
{
"scenario": "copy-128k-1k-prefix90",
"dataset_id": "immutable-dataset-version",
"load": {"requests": 640, "concurrency": 90, "request_rate": 6},
"service_parent": "verified-service-identity",
"delta": {"concurrency": {"from": 128, "to": 90}},
"measurement": {"whole": true, "native": true},
"recovery_target": "verified-service-identity"
}
示例只改并发,服务父配置和恢复对象相同。执行器拿到这份配置就能走热测路径。若此时还要新写部署逻辑,多半又碰到了历史脚本里的硬编码。
数据另存一份身份记录。N640 对应 640 条真实实体;截取子集就给子集单独的版本和哈希。实际 tokenizer、正文与 Chat token 数、输出长度、独立后缀都在生成时记好,后面直接复用。这比每轮重新分词、重新解释输入长度省事。
二、执行阶段比一个总退出码更有用
沿 PD 链路看,一轮可能经过服务准备、预热、路由验证、缓存准备、正式负载、独立结束、后处理和决策。只改 C 的热测会跳过新增加载、MF 和路由验证;状态记录里写清“复用”就够了。
每个阶段只需要保存足以回答下一步的问题。正式负载之前,要知道当前服务身份符合要求、没有另一轮压测在用机器;负载结束后,要知道原客户端与原请求确实结束;统计之后,要知道结果来自这一轮的哪些原始数据和哪个处理版本。
有时本地控制器先退出,远端客户端还在跑;有时原进程和退出文件都已经确认结束,摘要却还残留 RUNNING。遇到冲突,就回到那一轮的原进程和产物,而不再围着一个旧状态字符串猜。
一轮只有一个 GPU owner,可以减少这类歧义。它负责本轮开始、结束和与负载相关的 API;其它工作读取其状态或处理已有数据。若控制器已经绑定 watcher,就不再启动第二个 collector。否则两个处理器可能同时写结果、释放占用或重复调用统计接口,使原本可判断的状态变得混乱。
网络断开时,这个问题会更明显:调用端超时了,远端可能已经执行。给每个阶段留身份和开始、完成记录,恢复连接后才知道该查什么。锁可以挡住第二轮压测,原始产物则帮助判断第一轮究竟走到了哪里。
三、检查应该放在副作用之前
检查最值得做的位置,是下一步即将支付昂贵成本的边界。
准备修改服务时,应先核对最终有效命令、实际配置来源与恢复目标;准备发送正式请求时,应确认真实入口认识本轮模式和参数;准备启动采集子进程时,应确认它能在新的进程环境里加载原配置。这些检查大多可以在 CPU 上完成,并禁止模型请求发生。
AISBench 就在这里出过问题。父进程里直接构造类没有报错,真实 worker 却要经历配置生成、dump、reload,再从 registry 构建对象。后来把验证接到新进程 reload 和实际构建入口,才覆盖到原来漏掉的位置。重复检查父进程构造,对这个问题没什么帮助。
N 扩大时,要一路看到 collector。外层已经接受 640,底层却可能还卡着请求数白名单或文件大小限制。只改 C 就简单得多:tokenizer、流式解析和原统计逻辑都没动,这些证明继续用。
可以用一张很短的变更表约束检查范围:
表格可左右滑动 →
| 本轮变化 | 应检查的主要边界 | 可以继承的事实 |
|---|---|---|
| N、C、r | 实际参数、实体数量、受影响的采集边界 | 未变服务源码、tokenizer、观测逻辑 |
| AIS 扩展 | 配置生成、dump、fresh-process reload、受影响的 worker 路径 | 未变模型服务 |
| 服务源码或布局 | 最终运行身份、容量与相应服务验证、恢复对象 | 未变数据与计量定义 |
| 后处理环境 | 原数据绑定、子进程导入与原 selector | 已成功的正式推理 |
两轮预热和完整路由验证曾经很耗时。最先能省下来的,是热参数点重复执行的那一份;服务真的变了,再做相应准备。至于预热长度和路由数量还能缩到多少,现有实验尚未给出等效方案,这部分留待专门验证。
四、数据与缓存决定这轮到底测了什么
128K / 1K、约 90% 前缀缓存,准备起来比拼一段公共文本麻烦。分词结果、Chat 包装、页边界和请求路由都会改变实际命中。最后还是要读请求上的 cached token 计数,看缓存到底复用了多少。
这次固定正文 131072 token,实际 Chat 输入 131076 token,输出 1024 token。最终每条请求观测到 118784 个 cached token,按实际 Chat 输入作分母,为约 90.6222%。此前一个按页对齐构造的理论前缀比例还低于 90%,说明“差不多九成”与“逐条至少九成”是不同的要求。
多 DP 还多了一层路由问题:一个实例热了,其它实例可能仍是冷的。所以实际流程使用独立 seed,再逐路由 check,之后才开始正式测量。seed 和 check 的请求数与耗时单列,读者才能知道得到这份热缓存成绩之前,还花了多少准备时间。
重复测试还会引入一个更隐蔽的问题:如果独立后缀也重复,下一轮可能命中比目标更多的内容。若原场景要求固定约 90% 缓存,设计数据和路由时就应避免额外命中成为参数收益。若要求零缓存,也需要相应的唯一性与缓存状态控制。
报告里于是多了几项:正文长度、实际 Chat 长度、实际输出、逐条命中范围和分母。名义输入吞吐仍可展示,只是其中已经命中的 token 没有在这一轮全部重算。把这层关系写清,读者就能理解长输入和高吞吐为什么会同时出现。
五、结果记录要允许“未知”,也要保留统计区间
成功失败数出来以后,还要把延迟分布、吞吐区间和计数范围接上。最早的报告不够全,我追问 ITL 后,代理才补上。结果汇总应该成为默认步骤,否则每问一项,就又要回去找一项。
例如,TPOT 描述平均每输出 token 的时间摊销,ITL 看输出到达间隔。投机解码一轮推进多个 token,打包和传输又会改变到达节奏,两项数值自然可能相差很大。至于 draft 和 verify 各花了多久,那是另一个计时问题,要到实际阶段里测。
AL 也要把公式带出来。最后一轮 128K C90 的 whole AL 为 655360 / 113463 = 5.7760,分子是输出 token 数,分母是 verify 调用数。首 token、额外 token 和输出截断都进入了这套计数,阅读时按这个定义理解就清楚了。
严格 proposal 接受率还缺一个量:实际生成了多少草稿。当前 Compact 观测采到了验证 cap 槽预算,却没采全实际 proposal 总数,所以这一项保留 NOT_OBSERVED。AL 可以算出,严格接受率仍然未知,两件事并不冲突。
计数范围也跟着保存。whole 的接受统计留在 whole,native selector 选出的请求集合和时间区间另存。这样下次看到“244.86”,还能找回它用了多少请求、持续多久、除以几张卡。
下表用同一 128K C90 轮次说明这个边界,条件为真实 P8+D8、16 张物理卡、物理 640 请求、配置 6 req/s、逐条缓存约 90.6222%、固定输出 1024:
表格可左右滑动 →
| 指标 | whole | AISBench native |
|---|---|---|
| 纳入请求数 | 640 | 549 |
| 时间(s) | 179.5766 | 143.4935 |
| 平均系统并发 | 82.8179 | 87.4978 |
| 平均 TPOT(ms) | 10.3 | 10.4 |
| 输出吞吐(tok/s/PD16 卡) | 228.0921 | 244.8612 |
| AL | 5.7760 | 未单独采集 |
这是同一轮的两个窗口。native 区间达到了本项目约定的请求数和时长门槛,AL 却只采了 whole。表格保留这种不整齐,比补上一个范围错误的值更容易用。详细数值见结果摘录。
六、失败以后,沿已经完成的阶段恢复
恢复动作应当由失败位置决定。下面几种情况,在自动化里应走不同分支。
本地配置或导入在连接模型之前失败,正式请求数为零。修复入口后可以重新准备,但不能把它登记成候选性能负例。若服务已加载、两轮预热也已完成,只是正式入口不认识某个模式,应尽量保留热服务,从未完成阶段接续,避免再次安装 observer 或重新加载权重。
如果推理结束、采集失败,先独立确认原客户端结束与当前服务状态,然后只修原数据处理。开篇的导入故障就是这一类。保存原错误,补齐实际 payload 的模块搜索路径,复用原 AIS 算法处理一次;不修改计时点,不重新发送请求。
如果网络断开导致 start、end 或统计 API 的状态未知,就没有安全的“再试一次”默认值。统计开始可能重置计数,dump 也可能产生额外副作用。应先保留本轮占用权,核对原进程、产物和 API 完成证据。恢复连通后只处理缺失事实,不重新执行所有阶段。
确实需要回滚时,先确认恢复到哪一站。场景、布局和源码一直在变,默认脚本可能已经落后好几轮。候选开始前留下恢复对象,出问题后才有明确去处。单纯性能负向则可以先保留原始结果,再按下一轮的选择安排服务切换。
每次只修出错的阶段,前面做完的工作才能留下来。未知请求先查清,也让下一轮负载有一个干净的开始。
七、跨会话继续,靠短入口和可靠产物
实验可能持续几个小时乃至几天,代理上下文却会切换。把全部历史复制进每次唤醒提示,看起来很完整,实际上容易造成两个问题:花更多 token 重读旧事实,以及把旧的 RUNNING、旧 PID 或旧“未完成”当成当前状态。
更适合的交接入口很短:当前授权是暂停、单次还是全自动;当前场景与服务父身份是什么;有没有唯一 owner;已经完成到哪一段;下一步只做什么。详细日志、失败记录和旧实验留在各自目录,需要时按索引查。
结果与事件还需要分开。一个摘要字段可以帮助阅读,但原始结束文件、原始统计和实际变更回执才支撑结论。后处理修好之后,应更新当前结果状态,旧失败记录仍然保留;不能让历史错误消失,也不能让一个旧 PENDING 标签永远遮住已经成功的结果。
用户需要的进度同样是状态事实:配置尚未发出、正在加载、已发送正式请求、原请求已结束而采集失败。这些比“快好了”更有用。普通等待可以安静,重要失败应说清卡在哪个边界,当前是否真的占用 GPU,以及下一步是否会重复请求。
现有工具离这套安排还有距离,尤其是历史薄适配器里的硬编码。先把执行、阶段记录和结果汇总收敛到共享入口,计划层只提供本轮输入。下次只改 C 时,能否快速发出、零新增模型加载、一次交齐报告,才是这项整理真正的验收。
一轮实验的结束,最终包含三个不同事实:负载已结束,结果已正确提取,结果已足以支持本轮决策。有时前两个成立,第三个仍然不成立,例如严格接受率缺失或确认窗口不足。保留这个区别,自动调优才能知道该补哪项证据,什么时候应该停止,而不会一直在重跑和解释之间打转。