四千张卡在跑,7% 的算力被一个不报错的慢节点吃掉了——腾讯把万卡训练里的「慢而不死」抓了出来
ARGUS: Production-Scale Tracing and Performance Diagnosis for over 10,000-GPU Clusters
这是 6 月挂上 arXiv、7 月更新到 v2 的一篇系统论文,八位作者全部来自腾讯。它讲的不是模型,是模型底下那台机器。在一万张卡以上的生产集群里,最贵的故障不是训练崩了,而是训练还在跑、每一步都慢一点、并且没有任何东西报错。论文开头就把账算给你看:一个 4,096 卡的任务,迭代时间反复出现超过预期两倍的尖峰,最后白烧掉约 23,758 GPU 小时,占这次训练总算力的 7%。没有报警,没有异常日志,仪表盘上一切正常。
更值得读的是它承认了一个尴尬事实:想看清这件事,你现成的工具会把训练弄坏。论文把两个最常用的 profiler 常开跑了一遍:PyTorch Profiler 让迭代时间涨了 20%–44%,最后因为 trace 在内存里无限堆积把进程撑爆;NVIDIA Nsight Systems 更直接——8 卡上训练在第 10 步出 NaN,32 卡上卡死在 AllToAll 通信里,而不开它两种情况都不会发生。观测本身改变了被观测的对象。ARGUS 的设计目标因此被钉死在一个很苛刻的位置:常开、细到单个 GPU kernel、总开销必须压在 2% 以内。
「慢而不死」为什么难查
fail-slow(慢而不死)指的是某个部件——一块 GPU、一条网络链路、甚至宿主机上的一段 Python 代码——性能退化了,但没有报错、没有退出。在同步训练里这一点尤其致命:所有 rank 每一步都要在同一个屏障上对齐,一张卡慢,整个任务就跟着慢,木桶效应会把损失乘上几千倍。
真正麻烦的是,同样是这个屏障,会把证据一起抹平。论文的第三个案例里,一个 4,000 卡任务的 rank 3760 反向计算耗时 173 毫秒,同一流水线位置的其他 rank 只要 92 毫秒,将近两倍;但因为末尾的梯度同步是个阻塞操作,所有 rank 的「前向+反向」总时长被对齐到几乎一模一样的 11,178 毫秒。你去看迭代时间,rank 之间毫无差别。慢节点藏在一个所有指标都正常的数字后面。
现有工具正好卡在两端。一类(Greyhound、C4、Minder、ByteRobust 这些)常开、开销低,能告诉你哪台机器不对劲,但答不出「是哪个算子变慢了、为什么」;另一类能给出细粒度 trace,但要么只在检测到异常之后才触发采样、错过转瞬即逝的窗口,要么开销 5%–30%,在生产集群里根本不能一直开着。
三路拆解,加一次 3,700 倍压缩
ARGUS 的第一个选择是按训练的调用层次把观测拆成三路,各管一件事:Python 层的 CPU 调用栈(谁在等、卡在哪个函数)、训练框架层的语义打点(前向、反向、优化器、通信各花了多久)、GPU 运行时层的 kernel 执行。三路各自开销都小,合起来不到 2%。工程上也做得很克制:kernel 追踪编译成一个动态库,靠 CUDA_INJECTION64_PATH 环境变量注入,训练代码和启动脚本一个字都不用改。
第二个选择才是这篇论文真正的技术核心。一万张卡跑起来,每张卡每分钟产生 10⁴ 到 10⁵ 个 kernel 事件,全集群原始数据 6–60 GB 每分钟——拿去做在线的跨卡比对,算不动也存不下。作者的做法是在线做统计压缩:用核密度估计把同一个 kernel 的耗时聚成几簇,每簇只留计数、中位数和 p99。每卡每步的 kernel 数据从 10 MB 压到 2.7 KB,约 3,700 倍。做个对比:同样一步,Nsight Systems 每卡产出 63.56 MB,一万卡就是单步 600 GB,只能事后离线看;ARGUS 压完之后全集群上传速率约 2.7 GB/分钟,塞得进一个普通时序数据库。
这个压缩不是为了省钱,是为了让「比较」这件事在线成立。压缩后的统计量能反推出每个 rank 上每个 kernel 的耗时分布曲线,然后两两算 Wasserstein-1 距离——通俗说就是「把一条分布搬成另一条要花多少力气」,它同时抓得住整体变慢和偶发长尾。同一个并行角色的 rank 本该跑一模一样的 kernel 序列,分布应当高度一致;谁跟所有人的距离都系统性偏大,谁就是嫌疑犯。最后用四分位距设一条上界自动圈人,把排查范围从上万个 rank 收到个位数。
五个线上故障,三个通用教训
案例部分是全文最有价值的地方,因为这种东西平时没人往外发。第一个案例最直白:4,000 卡的多模态任务,步时从 4 秒飙到 200 秒以上,两个 rank 的 self_attention 从正常的 6–16 毫秒涨到 2,252 毫秒,一百五十多倍,摘掉节点就恢复。
第二个案例开始变得有意思:一个 500 卡的音频模型任务,迭代时间完全平稳,没有任何抖动,只是整体吞吐一直低于预期。前两级检测全部沉默,是 kernel 分布比对发现某个并行组的三类通信算子分布集体偏移,顺藤摸瓜查出组里两台机器的 PCIe 链路有硬件故障。任何靠「迭代时间超阈值」触发的系统都发现不了它——它压根没有波动。
第四个案例是所有做工程的人都该记一笔的坑:训练偶发出现几十倍的耗时尖峰,查下去是 FlashAttention 的新后端在运行时做 JIT 编译,某个微批次的反向计算从 95–160 毫秒暴涨到 6,303 毫秒。编译结果默认只缓存在进程内存里,任何一次容错重启、配置热更新都会把缓存清空;而这是个视觉语言模型任务,输入图像尺寸五花八门,于是编译被反复触发,阻塞还顺着流水线依赖传染给下游。解法朴素得让人生气:打开磁盘缓存的环境变量,再在训练前把可能的形状预热编译一遍。
第五个案例给出的教训最普适。一个 10,000 卡的 MoE 任务步时从 30 秒涨到 90 秒,带外监控系统报了某台机器「端口 down」,运维据此判定是网络问题、准备去修网。细粒度追踪给出的却是相反的结论:异常只出现在纯计算算子上,八个 rank 的 mlp 耗时是同组均值的 5.7 倍;而通信算子的耗时反而更短——因为这组卡算得慢,进入集合通信时别人早就等在那儿了。换掉那几台机器,速度立刻恢复。通信层看到的异常,常常只是计算慢的二阶效应。
论文最后提了一句正在做的事:把检测结果喂给 LLM agent,让它结合并行拓扑和历史故障模式自动出诊断报告,初步经验是把平均诊断时间从几十分钟压到分钟级。这句话没有数据支撑,当成方向看就好。
本文为 AKL AI Club 原创撰写的导读,不是原文翻译;著作权归原文作者所有。 篇目由编辑独立选取,来源均经人工核实。