把环搬到 p4 上
引擎与调用方之间的七处契约变更。每一处的失败方式都不一样,而其中只有一处看起来像是出了错。
我们合入了 p4 引擎的一个新版本,环就不再服务了。不是崩溃——加载器报告成功,agent 报告 ready,然后什么也没发生。从这份寂静往回倒推花了一天,翻出了七处我们的调用方与引擎已经各走各路的地方。值得写下来的不是这个数字,而是七处里有六处根本没有报错。
指向不存在节点的事件会被转发,而不是被拒绝
我们的加载器把 LOAD 命令发给了它想要创建的那个节点。但在 LOAD 创建它之前,节点并不存在,而 broker 对一个指名未知节点的事件的规则是把它向外转发,而不是拒绝。于是这条命令离开 agent 去别处找归宿,没找到,被丢弃。没有任何日志,因为在 broker 看来什么错也没发生。
修法是把 LOAD 发给那个 *agent*,包在引擎与后端无关的生命周期信封里,适配器自己的命令作为不透明的载荷。事后看来显而易见;从外面看却完全不可见。
一个必须等于另一个数字的数字
一个模型在某个 load generation 下被加载,而每个节点以某个 node generation 注册。我们一直把这两者当作互不相干——一个用时间戳,另一个写 1——而一切都正常。环加载了。它正确回答了一次请求。然后头部节点死了。
适配器的 release 记账里的那个检查。
let Endpoint::Node { generation, .. } = &event.envelope.source;
if *generation != receipt.load_generation {
return Err("release owner census generation differs from source");
}结束一次请求的那张 receipt 携带着 load generation,而发出它的节点携带着自己的那个。两者不一致,节点就被停掉。所以这个 bug 的形状是:加载成功、首次请求成功、头部死亡、之后每一次会话都半加载地挂住。它读起来完全像负载下的崩溃,一点也不像一次数字不匹配。现在我们的加载器会在任何东西被加载之前,就拒绝两个数字不一致的方案。
一个永远不可能被满足的上界
适配器会把一个 stage 所能返回的最大结果,与 stage server 启动时报出的数字按完全相等比对。而不加载模型我们就无从知道那个数字——于是我们带着一个猜测去加载,失败一次性把四个 stage 的真实数字全告诉了我们:
一次运行,四个答案。
step37-s0: profile=33554432, READY=34419218444 step37-s1: profile=33554432, READY=34419218444 step37-s2: profile=33554432, READY=34419218444 step37-s3: profile=33554432, READY=59136012
34 GB。agent 的保留存储是 256 MiB,所以那个环根本不可能被准入。把 stage server 里的推导读一遍就明白了:这个上界随 n_batch × n_ubatch 增长,而我们从一份早于这项检查的配置里继承了 2048 的批宽。在 128 行——生产布局所用的宽度——上界是 138 MB,放得下。现在我们在方案里用同一个公式推导它,而不是抱着一个记住的常数;尾部 stage 的预测值与另一套部署记录下来的数字分毫不差,而这种一致,正是你在花二十分钟去加载之前最想要的。
简短地说
- 双主机环里的 loopback 地址。一个 stage 按下一个 stage 的 agent 所公示的地址拨过去。公示
127.0.0.1,主机 A 就拨给了自己。那个环记录在案的配置一直都是 loopback——它跨主机从来就不可能工作。 - 日志是强制的。没有 agent 的运行日志(operational journal),模型不会加载。我们在追另一个错误时把它关掉了,结果让症状变得更糟,而且糟得像是有了进展。
- 设备名是分后端的。参考方案生成器面向 CUDA,发出
--device CUDA0。而 HIP 构建把自己的设备叫作ROCm0。那份方案会先把整个模型加载完,*然后*才找不到设备。 - 原生 server 是发行版的一部分。用更新的代码树构建的 agent 想要的能力,已安装的 stage server 并不上报。同样是在一次完整的模型加载之后才被发现。
寂静才是昂贵的失败模式
这些每一个都便宜到好修、昂贵到难找,而且模式一致:真正昂贵的失败,都是那种看起来一切正常的系统什么也没做、或者只做了一次。我们加上的守卫全是同一个形状——尽早拒绝,就在错误还看得懂的那个地方。加载器在第一条命令发出*之前*就把 load generation 写到磁盘上,因为否则它无从找回。它直接拒绝 generation 不匹配,而不是等第一次请求之后才发现。它推导结果上界,而不是记住它。
环正在服务。两台机器上的四个 stage,113 GiB 权重常驻,冷启动首 token 1.4 s、预热后约 0.3 s,而每节点的贡献第一次一路流进了结算账本。