分布式AI系统系列我写到第八篇了。前七篇里,通信原语、数据并行、模型并行、流水线并行、推理引擎这些硬骨头都啃了一遍,不少朋友以为这个系列差不多该收尾了。但我心里清楚,真正决定一个分布式AI系统能不能在团队手里活下去的,从来不是训练跑通的那一刻,而是它进入生产环境之后那些日复一日的麻烦事:任务怎么调度才公平高效,训练中途节点宕了怎么办,GPU明明在跑为什么利用率就是上不去,月底一算账单为什么高得吓人。这篇就专门聊生产落地的工程治理。不管你是自己搭训练集群,还是用云厂商的AI平台,后面这些坑大概率都会踩到。
1. 为什么分布式AI系统难在“工程落地”
1.1 跑通只是一个起点,稳定性才是真正的门槛
分布式训练跑通和能持续稳定跑100小时,完全是两码事。本地跑通,数据量小,单卡或八卡就能搞定;但大规模训练,几十上百张GPU要连续多天协作。期间任何一个节点慢一点、光模块抖一下、显存ECC报错,都可能让整体变慢甚至直接失败。我见过一个团队在1024卡上做检索模型预训练,头两天实验证明收敛正常,第三天凌晨一个IB链路不稳定,NCCL AllReduce超时,整个训练直接挂掉。后来花了两周做容错和自动重启,训练才真正稳定下来。这个问题本质上是“木桶效应”:分布式集群里,最慢的那个节点决定了整步训练的速度,而任何微小的抖动都会让所有等待中的卡都闲着。
很多人会想:都1024卡了,单卡故障概率不高吧?但把概率乘上时间就完全不一样。假设单卡年故障率是0.5%,1024张卡连续训练30天,至少一张卡出故障的概率超过三成。这还没算网络、存储、驱动这些更不稳定的因素。所以分布式AI系统的稳定性,本质上是在跟概率博弈,而不是依赖某一天“运气好”。
1.2 单机问题是“性能问题”,集群问题是“系统问题”
这句话我反复跟团队讲。单机训练基本是确定性场景:代码一样、数据一样,运行结果大致可复现,优化方向很明确。到了集群层面,随机性开始出现:机器故障、网络拥塞、其他任务争抢、散热波动,甚至同一个作业在不同节点上因为驱动版本不一致,跑出来的吞吐都不一样。这些问题按性能优化思路去调,很难调明白。
你必须把它当系统问题处理。需要调度器管资源,需要心跳和探活管故障,需要监控体系管观测,需要副本和检查点管容错。这也是为什么分布式AI系统不止是训练框架的事——Kubernetes、容器网络、存储、可观测性工具都得一起配合。我经常跟团队说,不要只盯着模型代码,要看“系统七巧板”拼得齐不齐。少一块,整体就跑不稳定。
2. 资源调度与任务编排:决定集群能不能“转起来”
2.1 调度方案怎么选?先分清三种主流架构
调度器是AI集群的大脑,任务提交后它决定这个作业该在哪些节点上起多少进程。基于Kubernetes的调度器现在是主流,但Kubernetes默认调度器更适合在线服务,AI训练任务有很强的特殊性:一个大任务往往要同时占用完整的一批GPU,缺一张卡都不行。所以在选型之前,建议先把调度架构的底子搞清楚。
我习惯把调度架构分成三类来对比:
一是单体集中式调度,比如最传统的YARN和Kubernetes默认调度器。所有资源状态集中在一个调度器里,实现简单、公平性可控,但规模大了响应会慢,也存在单点风险。
二是两层调度,比如Mesos。先由中央资源管理器把资源发给各个框架,框架再自己决定如何分配任务。优点是并发性好,缺点是多个框架同时申请同一批资源时容易冲突。
三是共享状态调度,比如Google在早期论文里描述的Omega。用乐观并发控制把集群状态分发给多个调度器,速度和扩展性最好,但工程实现复杂,普通团队没必要自己造轮子。
实际在AI集群里,我更推荐“Kubernetes + 自定义扩展调度器”的组合:Kubernetes管容器生命周期,调度逻辑通过scheduler framework plugins实现。我们团队目前在用Volcano,它天然支持作业队列、优先级和重调度,比K8s默认调度器省心很多。选调度器的核心判断标准就两条:能不能处理多任务多队列的公平性,能不能支持整机调度Gang Scheduling。
2.2 Gang Scheduling、队列配额与抢占,参数怎么设置
大模型训练作业和普通微服务最大的区别在于:一个训练任务如果只分配到30张卡,但需要64张卡,那么这30张卡即使拿到了也会因为缺少通信对端而空转,这就是“部分分配”问题。为了解决它,需要“要么全部占用,要么全部不占用”的全有或全无调度,也就是Gang Scheduling。Volcano里的核心参数是minAvailable,它告诉调度器:这个作业至少同时可用多少张卡,才允许启动。这个值建议设成训练作业的总卡数;如果支持弹性容错,也可以设成更低的数值,比如“主副本组+模型切片的最小卡数”,但千万不要设成1,否则整机调度就失去意义了。
队列配额设置要防止大作业长期霸占资源。推荐按业务线划分队列,每个队列设容量上限。比如A队列负责生产模型训练,配额设为60%,B队列负责超参搜索和实验任务,配额30%,剩下10%留给突发请求。这里有一个容易被忽略的细节:队列配额不等于硬上限,超卖时高优队列可以突破配额抢占低优任务,但要设置好抢占阈值和退出机制。
启用抢占时要尤其小心。Volcano的preemption触发条件是“配额超卖、高优先级作业等不及”。我一般开启“基于优先级抢占”,但会设置软抢占加第二次确认,避免刚起来的大作业频繁被抢,把集群搞成颠簸状态。抢占粒度建议先按整节点释放,再按Pod迁移,这样对RDMA网络的干扰最小。下面是我实际用下来的一组参数参考:
| 调度属性 | 建议设置 | 主要考虑 |
|---|---|---|
| minAvailable | 训练总卡数或容错能接受的下限 | 避免部分分配导致集体空转 |
| 队列容量配额 | 按业务线60%/30%/10%划分 | 防止单一作业或团队长年霸占资源 |
| 抢占策略 | 软抢占+延迟确认 | 防止集群抖动弹跳 |
| 调度重试次数 | 3~5次 | 偶发瞬时资源不足,不立即失败 |
| 节点组亲和 | 尽量绑定同交换机域 | 降低RDMA网络跨域流量 |
这些参数没有标准答案,需要根据集群规模、任务优先级和成本模型去微调。但方向是确定的:一边保证大作业能安心训练,一边让碎片GPU也能被实验任务用起来。
3. 容错与恢复:分布式训练最容易被低估的一环
3.1 检查点:频率、粒度与异步写
容错核心是检查点(checkpoint)。训练每N步保存一次模型权重、优化器状态、分布式随机数状态。关键问题在于保存频率怎么选:频率太高,同步写存储会拖慢训练,因为GPU要等checkpoint落盘;频率太低,一旦故障,回退步数太多,前面的算力就白费了。
我的经验是:早期调参阶段每500到1000步保存一次,正式长训每2000到5000步保存一次。同时开启异步checkpoint,把保存状态交给后台线程,通过共享内存队列缓冲,避免卡住训练主循环。这个做法能覆盖绝大多数故障场景,损失控制在几分钟到十几分钟的计算量内。
存储方面,不要直接写普通NFS。我们踩过一个非常惨的坑:上千个进程并发写同一个checkpoint文件,POSIX锁冲突导致文件损坏,恢复时加载到一半直接崩。建议写分布式文件系统或对象存储,每个rank写独立临时文件,由协调进程在所有rank成功后做一次原子rename。这样即使某个rank的临时文件损坏,也不会影响已经提交的checkpoint版本。
3.2 弹性容错:让训练不因单点故障而干等
单靠定期保存检查点,故障恢复还是需要人工通知、手动重启。要做到自动化,就要有探活、重启和状态恢复。当前主流做法是PyTorch Elastic配合Kubernetes的liveness探针,torchrun自带的elastic模式能处理成员变化和rank重分配。
大致流程是:每个worker周期上报心跳,master节点把心跳聚合;如果一个worker失联超过阈值,系统主动杀掉整组worker,重新从上次checkpoint拉起;拉起时master重新分配rank,训练框架加载checkpoint继续跑。这个过程会牺牲从一个step都不停的理想状态,但换来的是“不用再半夜爬起来救火”。
我通常把心跳超时设为60秒,探针失败重试3次,并且让这个时间和NCCL的超时错开。为什么要错开?因为NCCL集合通信有自己的超时机制,如果探针比NCCL更灵敏,网络一抖动就直接重启训练;如果探针比NCCL迟钝,node已经卡死半天了还在傻等。两个超时都设在60秒附近就会互相干扰,建议NCCL超时设成90到120秒,心跳探针设成45到50秒。
还有一个小提醒:如果用了NCCL的AllReduce集合通信,故障后同一组GPU重新初始化通信域非常容易卡死。建议在重启脚本里把共享内存/dev/shm清掉、释放GPU显存、重置NVLink拓扑相关信息,再重新拉起。否则你会看到“training is unresponsive”的假死状态。
4. 可观测性建设:日志、指标与链路追踪一个不能少
4.1 指标怎么选?别只盯GPU利用率
很多团队监控面板上只有GPU利用率,这是远远不够的。AI训练集群至少需要四个维度:GPU本身(利用率、显存、温度、NVLink带宽、ECC错误);节点层(CPU、内存、IB/RoCE流量、丢包);作业层(吞吐sample/s、loss曲线、心跳状态);调度层(队列等待时间、排队作业数、抢占次数)。
GPU层面用DCGM采集的数据比nvidia-smi丰富得多,比如DCGM_FI_DEV_GPU_UTIL、DCGM_FI_DEV_MEM_COPY_UTIL、DCGM_FI_DEV_NVLINK_BANDWIDTH、DCGM_FI_DEV_ECC_ERRORS。需要注意,利用率最好同时看计算利用率和内存利用率,因为张量并行等场景下,算力和访存经常不同步。举个例子,一个大规模Transformer做张量并行时,每张卡可能只参与矩阵切片运算,计算利用率不太高,但显存占得满满的,只看GPU util会以为资源没用好。
作业级吞吐是判断训练有没有“假跑”的关键。loss在降但吞吐掉到一半,很可能出现了慢节点。慢节点不一定是故障,可能只是散热降频或CPU热迁移。这种问题用丢包、时钟频率和CPU load结合起来查,一查一个准。我建议每个训练作业都要暴露三个核心指标:当前step、累计处理样本数、最近一次梯度同步耗时。前两个用来计算吞吐,第三个用来定位通信瓶颈。
4.2 卡死的识别与告警规则
分布式训练最常见的问题是“卡死”:GPU占用率高,但loss不降、吞吐为零,进程不退出,也不报错。问题可能出现在NCCL通信等待、数据加载死锁、检查点写阻塞。对这种问题,最有效的做法是把“训练心跳”变成可告警事件。
我的做法是:训练主循环每10秒往Prometheus的Gauge里写一次“最近一次完成step的时间戳”,再配一个指标表示“上一个checkpoint保存耗时”。告警规则设三条:连续15分钟训练步数没有任何前进;连续3次梯度同步耗时超过正常值2倍;检查点保存耗时超过30分钟。这三条规则覆盖了90%的线上事故。阈值过大会误报,过小会漏报,建议先跑一周训练,把历史基线的P95拉出来再定阈值。
还有一点经验:不要只配CPU使用率和GPU使用率告警,因为卡死状态下这些指标可能依然很健康。真正管用的是训练心跳。哪怕GPU util是100%,如果“最近完成step的时间戳”已经10分钟没变,就说明训练已经假死了。收到告警后第一步不是重启,而是抓现场:拉CPU火焰图、看NCCL debug日志、查NFS挂载状态,排查之后再决策是否重启。
5. 成本治理与性能调优:算力不浪费才是王道
5.1 数据加载优化,先找“链条上的漏水管”
分布式训练算力昂贵,而数据加载是隐藏的浪费点。单机训练时IO问题可能不明显,多机训练一旦放大就看出来了。GPU计算一个batch只要200毫秒,CPU准备一个batch要500毫秒,那么GPU每跑一个batch就要等300毫秒,利用率只有40%。这就是典型的“数据管线漏水”。
优化分三步。第一步,做IO profiling,确认瓶颈在磁盘、网络、解码还是增强变换。第二步,把图像解码、预处理、batch拼接搬到GPU侧,用DALI或NVImageCodec实现,CPU只负责读原始文件。第三步,开启多级预取,训练主循环里预取下一个epoch的数据,别让数据加载阻塞反向传播。
我在实际项目里做过一次对比:同样的ResNet-50训练任务,优化前GPU利用率只有55%,优化后稳定在92%以上。核心不是换硬件,而是把数据生产者和训练消费者的节奏对齐。很多团队花大价钱加卡,不如先看看数据管线的吞吐是不是早就拉胯了。
5.2 异构资源混部与弹性伸缩,算力账单能省30%
成本治理不能只看节点单价,还要看资源的“时间利用率”。白天是算法工程师做小规模实验高峰期,晚上是核心模型预训练黄金期,这两种任务天然适合错峰混部。我们团队的做法是用Karpenter或集群弹性伸缩组件,按队列水位自动补充或回收节点。
具体策略分两层。第一层是长期包年包月的稳定集群,专门跑核心训练任务,保证有固定算力;第二层是弹性队列,白天跑小实验,晚上自动扩容跑大作业,用抢占式实例或竞价资源降低成本。这里有个警告:使用抢占式实例前,一定要先确认你的训练作业支持容错和断点续训,否则实例被回收一次,前面几十个小时的训练全部作废,省下来的钱还不够买教训。
另外,动态调整GPU大小划分也很值得用。NVIDIA MIG或vGPU可以把一张A100拆成多个实例,把碎片算力喂给推理或小实验。但要注意跨实例的NVLink带宽被隔离,不适合需要高频通信的模型并行任务,我用MIG主要是跑batch inference和超参搜索,效果不错。
6. 踩坑实录与问题排查速查表
6.1 三个真实翻车现场
先说NCCL初始化超时。我们有次在256卡训练BERT,改了一版网络拓扑后,作业频繁报NCCL Timeout。第一反应是加超时时间,但加了没用。后来用NCCL_DEBUG=INFO抓日志,发现是IB网卡的GDR(GPUDirect RDMA)与虚拟化驱动冲突,数据拷贝走了一条慢路径。最后把GDR关掉,问题立刻消失。教训是:遇到通信问题,先debug再调参,别盲目改超时。
再说checkpoint并发写损坏。早期方案是所有rank把自己那分片wit到共享存储,结果某次故障恢复时,发现优化器状态对不上。排查后发现是有两个rank并发写同一个文件名,部分覆盖导致校验和失败。后来改成“rank写独立文件+协调进程原子rename”方案,再没出现过。这个方案不是我们独创,是很多分布式训练框架的标准做法,但真的值得在所有新项目里提前落地。
最后说抢占导致集群空转。我们启用了高优先级抢占后,有个大任务频繁抢小任务的GPU。但大任务真正启动需要64张卡,每次抢到60张就卡在Gang Scheduling等剩余4张,而那些被抢占的小任务已经被杀掉了,集群出现大量空闲碎片。最后通过配置抢占延迟、限制同队列并发大作业数量解决。调度问题有时候不怪调度器,而是策略组合没想清楚。
6.2 踩坑问题速查表
| 现象 | 可能原因 | 排查方式 | 处理手段 |
|---|---|---|---|
| 训练吞吐骤降,但GPU util很高 | 慢节点或数据加载阻塞 | 看train step时间戳、CPU火焰图 | 换节点,offload到CPU |
| loss震荡明显,不同rank差异大 | 数据shuffle或seed不一致 | 对比各rank的batch分布 | 统一shuffle seed,固定RNG状态 |
| NCCL初始化频繁超时 | IB驱动/拓扑配置问题 | NCCL_DEBUG=INFO,ibtrace | 调整GDR、重配置网卡聚合 |
| checkpoint恢复失败 | 并发写同一文件 | 查看文件属性和校验和 | rank独立文件+原子rename |
| 任务排队但GPU有大量空闲 | Gang调度部分分配 | 看minAvailable与空闲节点 | 调整队列配额,减少大作业并发 |
| 抢占后集群空转 | 抢占策略太激进 | 看调度事件历史 | 增加抢占延迟,限制同队列并发大任务 |
| GPU显存ECC报错间歇性出现 | 硬件不稳定 | DCGM ECC计数 | 下线换卡,避免跑长训 |
这套排查表不是万能的,但顺着这几条基本上能覆盖生产环境最常见的故障。遇到陌生问题时,我习惯先保留现场再重启,千万别急着kill——日志、nvidia-smi快照、NCCL debug信息都是后面排障的唯一线索。
最后说一句掏心窝的话:分布式AI系统的工程治理没有一招制敌的秘方。调度、容错、监控、成本这四件事,每一项都得靠反复打磨才能变扎实。我见过太多团队一开始就冲上千卡集群,结果一半时间都在处理基础设施问题;也见过只靠二十张卡,把训练平台治理得井井有条的团队。算力规模大不是优势,能稳定跑完才是。