跳到主要内容
别信 docker run 的退出码

别信 docker run 的退出码

一键三连
一键三连

· 阅读约 4 分钟

起服务、探端点这个动作,我一周至少干几十次。写脚本的时候一次,改一行配置重启的时候一次,半夜三点怀疑机器是不是挂了的时候再来一次。

docker run 这条命令一秒就返回,退出码 0。看着一切正常。真正的端点要等到第 160 秒才接得上请求——权重加载、torch.compile、计算图捕获,全在这两分半里默默发生。

这种"命令成功但服务没活"的事,我脚本里踩过不知道多少次。每次看到 connection refused,第一反应永远是镜像坏了、GPU 没挂上、ROCm 驱动又抽风,翻日志翻二十分钟,最后发现人家只是还没起完。

sleep 5 是赌命。探活才是答案:

until curl -sf http://localhost:8000/v1/models >/dev/null; do sleep 2; done

外面再套一层 timeout 兜底:

timeout 300 bash -c 'until curl -sf localhost:8000/v1/models >/dev/null; do sleep 2; done'

⚡ 这一下,把"等多久算够"从你要猜的那个数字,变成服务自己告诉你。冷启动 160 秒它就等 160 秒,热启动 8 秒它 8 秒就走,中间不用改一个参数。

这两行现在是我所有起服务脚本的模板。换模型、换镜像、换显卡,都不动。

说回正题。我这两天在看一篇在 MI300X 上跑 Gemma 4 E2B 的基准,单卡 192 GB HBM3,64 并发 1024 上下文算上 prefill 将近四万九千 token/s。数字挺漂亮,但跟我关系不大——我又没有那张卡,横向比价是另一位写手的活。真正让我坐直了的是它脚手架里的几个小习惯。

那套基准开了 vLLM 的前缀缓存。两个测例的输入前缀一样,第二个就吃到缓存命中,你测出来的是缓存,不是卡。它的处理办法很土:每个单元格、每一次重复,塞一个全新的随机种子。

seed = random.randrange(2**31)
run(cell, seed=seed)

这件事泛化出去特别值钱。CI 开了构建缓存之后,你测的到底是构建还是缓存?本地跑 benchmark 图省事复用了上一轮产物呢?还有查询计划缓存。凡是你主动开了缓存来加速的东西,做验证的时候都得先把这条捷径堵上。不然你以为你在测硬件,其实在测缓存命中率。

顺便一句,那套工具里所有子进程都走一个辅助函数,参数列表直接 exec,不经过 shell。省下的不是执行时间,是路径里有个空格就得开始加引号、转义、反复 debug 的时间。这种封装第一版就该这么写,后面再补,得把整条调用链翻一遍,挺烦的。

还有一件事:跑不了的格子,标上"不可行",别删。

16 个单元格里 4 个跑不了,原因很简单,32768 的输入加上 128 的输出,超了 32768 的上下文上限。作者的做法是把它们明明白白标成"不可行",留在表里,而不是从结果里删掉。

这个习惯我觉得比大多数性能优化都值得抄。删掉的那一格,下周换张卡、或者把 max_model_len 降下来就能跑了,但那时候没人记得还有过这么一格。标着"不可行"的四格,就是你下一轮实验的待办清单。

最后说条跟提速反着来的。DigitalOcean 的 GPU droplet,关机不停计费,只有销毁才停,一小时 1.99 美元照走。那套工具包 21 个工具全塞在一个 server.py 里,从实例状态查到部署到日志到 SRE 探针都有,唯独不给"创建实例"和"销毁实例"这两个工具,留成控制台里手点。

我第一反应是这也太偷懒。第二反应是这是克制。自动化该省的是重复劳动,不是花不花钱这个决定。你的 agent 能自己起十台机器的那天,就是它半夜自己给你起十台机器的那天。

探活那两行抄过去,前后三分钟。省下的是每次翻日志找"为什么连不上"的二十分钟——这二十分钟我一周至少付一次,账根本不用算!

前提是你的服务确实会自己起完。它压根起不来,这套循环只会陪你一起等满 timeout,该看的日志一条都跑不掉,老实说。

你们起服务还有什么更快的确认姿势,或者知道哪个云关机也计费的坑,教教我。