
1. 从“单机能跑”到“多机可控”分布式AI系统要跨过的第一道门槛1.1 单脚本到分布式训练脚本之间差的不只是机器数量我在这个系列前面几篇里把并行策略、通信原语、参数服务器这些基础骨架都梳理过了按理说第八篇可以聊点更细的工程问题。但真落到实际项目里我发现很多团队连“单机改多机”这一步都会在数据加载上翻车而且翻得毫无征兆。单机训练脚本里DataLoader每次shuffle之后把数据集切分成一个个batch喂给模型这个流程没有任何问题。一旦把它改成多进程分布式训练如果每个进程都加载完整数据集、各自shuffle那就相当于每个节点都在看同一批样本。你以为是4卡并行实际上模型一个epoch里看到的有效数据量直接缩水到原来的四分之一。更麻烦的是验证集指标会变得非常诡异loss曲线震荡得像心电图明明模型结构没动结果却完全对不上。这时候我最先怀疑的不是模型而是数据采样器有没有按rank正确切片。PyTorch里有现成的DistributedSampler可以直接解决这个问题但很多人不知道它的一个关键细节每个epoch都要调用set_epoch否则shuffle顺序在多个epoch之间会保持同一个随机序列。分布式训练里每个rank拿到的数据本来就不同如果epoch之间不换顺序模型很容易过拟合到固定数据排列上。sampler DistributedSampler( dataset, num_replicasworld_size, rankrank, shuffleTrue, seedseed ) train_loader DataLoader( dataset, batch_sizebatch_size, samplersampler, num_workersnum_workers, pin_memoryTrue ) for epoch in range(epochs): train_loader.sampler.set_epoch(epoch) for batch in train_loader: ...这段代码看起来简单但set_epoch(epoch)那一行最容易漏。漏掉之后每个epoch的数据排列顺序都一样模型会在固定顺序上产生隐式记忆训练前期不明显跑到几十个epoch之后就会暴露成泛化能力下降。还有一个很容易被忽略的点分布式训练里全局batch size是batch_size * world_size这会让BatchNorm这类依赖batch统计量的层变得非常不稳定。很多人在单卡上跑得好好的BN层换到分布式后验证集指标突然崩了以为是模型实现问题其实是每个rank看到的batch太小统计量噪声过大。处理办法要么把BN换成分组归一化或层归一化要么在分布式环境里同步BN统计量后者通信开销不小小batch场景下我更推荐直接换归一化层。1.2 初始化失败比训练失败更隐蔽通信后端的选择与验证另一类特别折磨人的问题是训练代码写得没问题但启动脚本一执行进程就卡在init_process_group那里半天没有反应。这种卡住不是计算问题而是通信初始化没完成。分布式训练启动时每个进程都要通过MASTER_ADDR、MASTER_PORT、RANK、WORLD_SIZE这些环境变量找到彼此然后建立通信组。现在主流框架都提供了类似torchrun的启动工具会自动帮我们把环境变量配好但底层网络环境依然是自由发挥的领域。最常见的坑有两个端口冲突以及多网卡环境选错了网络接口。一台机器上通常同时存在管理网和计算网管理网可能是千兆计算网可能是RoCE或InfiniBand。NCCL默认会在多个网卡里自动选一个一旦选错集合通信速度可能直接掉到千兆水平甚至根本建立不了连接。我自己碰到过的情况是训练任务在前1个小时一切正常之后突然开始每隔几分钟报一次NCCL超时进程又不退出GPU利用率持续掉到个位数。查到最后发现是网卡bonding配置在运行过程中发生了切换NCCL连接的socket被切到了管理网。遇到这类问题第一步不是改代码而是确认通信后端到底走的哪条路径。设置NCCL_SOCKET_IFNAME指定计算网的网卡名称设置NCCL_IB_DISABLE0启用RDMA如果网卡不支持RoCE就改成NCCL_IB_DISABLE1强制走TCP。这些环境变量看着不起眼但对多机训练的性能影响是数量级的。我建议在正式训练之前跑一个小的allreduce测试脚本打印出实际带宽和延迟再开始大任务十几分钟的投资能省掉后面几天的排查时间。2. 并行策略不是单选题数据、模型、流水线怎么配才不打架2.1 数据并行省钱但不背锅数据并行是分布式AI系统里最普及的并行方式因为实现门槛低、适用范围广几乎所有主流框架都原生支持。但数据并行有一个容易背锅的点它在每个step结束时都要做一次全量梯度同步也就是allreduce。这个同步过程如果做得不好整个系统的扩展效率会非常难看。我见过不少团队为了减少通信频率在每几个batch之后才同步一次梯度相当于把梯度累积和分布式通信组合在一起。短时间内确实能降低通信开销但副作用是模型的实际优化路径和原始训练逻辑产生了偏移收敛轨迹会变得不一样。特别是当你配合学习率调度器使用的时候很容易出现“明明loss下降了但泛化效果反而变差”的情况。学习率这块也值得多说一句。分布式训练把global batch size放大之后很多人会机械地按线性缩放规则调学习率。这个规则在小规模缩放时还好用但放大到几十卡、几百卡时经常失效。真实项目中我倾向于用一个更保守的组合学习率小幅增加、warmup steps拉长、必要时加入梯度裁剪。这三个调节方向同时做往往比单纯线性放大学习率更稳。数据并行还有一个隐性成本如果模型大到单卡放不下数据并行本身无法解决显存问题。这时候就需要切模型问题就变成了下面要说的并行策略组合。2.2 模型并行与流水线并行显存问题的代价模型并行有两种主流形态。一种是张量并行把某一层的权重按行或列切到多张卡上每张卡只负责一部分计算但层与层之间需要做allreduce或all-gather来同步结果。这种切法通信频率高适合放在节点内部用NVLink高速互联的几张卡之间跨节点做张量并行基本是灾难。另一种是流水线并行按层把模型切成多段每张卡负责其中一段数据以micro-batch的形式在卡间流动。流水线并行的通信量比张量并行小很多但它会引入气泡也就是某些卡空闲等待的时间。经典的F-then-B调度气泡非常大后来大家普遍改用1F1B调度让前向和反向交错执行气泡明显减少。很多初学者看到流水线并行就想着把micro-batch调大来填满流水线但micro-batch调大的同时前向和反向中间的激活值缓存也会同步增长显存峰值跟着水涨船高。我在实际项目中通常是先固定一个较小的micro-batch再通过调高流水线stage数量来均衡负载而不是一味加大batch。模型并行落到工程上的代价不只是显存还有调度和调试的复杂度。一个8卡数据并行任务出问题定位起来还相对简单一旦加入流水线并行某个rank上的一个NaN可能要跑好几个step才能被其他rank发现。2.3 混合并行落地的一个可执行参考以现在常见的大语言模型训练为例一套比较稳妥的组合是节点内部用张量并行节点之间用流水线并行外层再套数据并行。这个组合方便调度器把通信量大的部分限制在物理拓扑更近的卡上跨节点流动的只有流水线边界上的激活和梯度压力小很多。但也不是所有模型都适合照搬这套方案。我自己在项目里的选择逻辑是如果模型比较规整、以Transformer结构为主可以放心用Megatron风格的3D并行如果模型结构复杂、自定义算子多我更倾向用ZeRO这类参数分片方案。ZeRO把参数、梯度、优化器状态分片到所有rank上通信模式只有all-gather和reduce-scatter对计算图的侵入比张量并行小得多Python生态里各种自定义层都能直接跑起来。代价当然也有。ZeRO在需要访问完整参数时会触发跨节点all-gather通信开销随模型规模增长长期训练时的稳定性和调试体验各有利弊。我的建议是先用Profiler跑一下看看通信占比再决定用哪种并行组合不要凭感觉选定型。3. 调度的重点资源碎片和节点拓扑对训练速度的影响3.1 调度器要解决的三个实际问题分布式AI系统到了一定规模后瓶颈往往不再是单个训练任务而是多个任务共享集群时的调度策略。我接触过的平台里很多调度器只按“显卡数量”来分配资源——用户申请16张卡调度器就找16张空闲卡出来完全不看这些卡之间的物理位置和网络拓扑。这个做法在小集群里还能忍集群一超过几十台机器就出大问题。一个训练任务被拆到两个节点上每节点各8卡和全部集中在一个节点的8卡另一个节点的8卡通信开销完全不是一个量级。跨节点做allreduce数据要先经过PCIe、网卡、交换机延迟轻松翻倍到三倍以上而节点内走NVLink时延可以控制在微秒级别。如果调度器不做拓扑感知任凭任务跨节点散落用户会以为是模型代码出了问题实际上可能是通信拓扑在拖后腿。调度要解决的第二个问题是排队。共享集群里任务队列经常因为某几个大任务占着资源不释放而整体阻塞。用户等了几个小时发现任务还没调度平台又没有任何提示体验就很差。解决思路是给任务设置明确的排队超时和优先级策略让短任务优先穿插到碎片资源里跑而不是跟大任务硬抢。第三个问题是预占。很多调度框架允许高优先级任务抢低优先级任务的计算资源但在AI训练场景下预占意味着被抢任务需要先保存checkpoint再退出这个开销可能比任务本身还大。生产环境里我一般建议不要轻易启用抢占式调度优先用节点耗尽策略或者挂起等待把“打断”的代价留给真正紧急的任务。3.2 资源碎片动态调度与亲和性的设计思路资源碎片是共享集群里的常态。一台机器上剩下3张卡另一台剩下2张卡而用户任务需要4张卡调度器要是死板地等同一节点凑满4张卡集群利用率就很难上去。一个务实的做法是允许任务跨节点组合但必须让用户感知到性能差异另一个做法是引入“最小资源单元”的概念把GPU按物理拓扑分组任务的亲和性偏好作为调度权重来权衡。结合AI平台的实际设计我会在调度器里同时维护两层约束亲和性约束决定了任务的所有worker尽量落在同一个交换机域里面减少跨机通信反亲和性约束则让不同用户的任务尽量分散在不同节点上避免干扰。这两个约束需要综合成本函数来平衡不能只取其一。真正容易被人忽略的是调度器对显存和CPU资源的一视同仁。一个深度学习任务如果CPU核数给得不够DataLoader的数据加载会成为瓶颈GPU一直在等数据利用率上不去。调度时如果只看GPU显存不看CPU和内存配额就会出现一堆GPU空闲但CPU跑满的“僵尸任务”。我在平台上查问题的时候经常发现某些节点的GPU利用率只有20%数据加载线程却已经把CPU全部打满调度的锅跑到训练代码头上。3.3 从调度策略反推训练任务的资源申请习惯对于正在使用共享集群做分布式训练的同学我建议提交任务的时候明确申请多维度资源不要只说需要几张卡。至少要包含显存大小、CPU核数、内存大小、是否需要RDMA网卡这几个维度。很多平台的调度器支持节点亲和标签如果能给节点打上“有NVLink”、“有RoCE”、“有共享存储”这类标签调度器就可以更精细地匹配任务需求。我自己常用的做法是给每个训练任务加上一个“可接受的跨节点数”参数。如果任务能容忍跨节点通信调度器就可以利用碎片资源快速启动如果任务对通信极度敏感则明确要求节点内分配。这个参数看似简单但能极大缓解集群里的资源碎片问题也能帮用户在性能和启动速度之间做取舍。4. 容错与断点续训分布式训练能不能让人睡个安稳觉4.1 先分清故障类型进程崩溃、节点失联、慢节点分布式训练跑在几十块GPU上的时候故障不是“如果发生”的问题而是“什么时候发生”的问题。最常见的故障类型有三种进程崩溃、节点失联、慢节点。进程崩溃还算好办日志会留下traceback进程退出码也能说明问题只要监测到退出码异常就触发重启流程。节点失联稍微麻烦一点可能是机器重启、网卡挂掉也可能是被管理员维护下线。最难搞的是慢节点也就是所谓的长尾节点或straggler。慢节点不会报错进程还在跑GPU利用率却明显低于其他节点整个并行任务被它拖住所有其他节点都在空等它完成通信。慢节点是最浪费钱的故障类型因为它不会引发告警很多平台只会监控“进程是否存活”不会监控“这个节点的step时间是否比其他节点慢了20%以上”。我建议每个训练任务都记录每个rank的step耗时计算所有rank的P50和P95之间的漂移。一旦漂移超过某个阈值比如P95比P50慢30%以上就认为存在慢节点嫌疑触发自动隔离或人工介入。这一步看起来简单但能挡掉大量莫名其妙的性能问题。4.2 checkpoint不是越频繁越好断点续训听起来很美好实际做起来比想象中麻烦得多。首先要决定checkpoint里存什么东西至少需要包括模型参数、优化器状态、学习率调度器状态、RNG状态、数据采样器状态、当前epoch和step编号。其中优化器状态往往比模型参数大好几倍Adam优化器每个参数要保存一阶矩和二阶矩显存和磁盘开销都很大。存储策略上很多团队会在每个固定step数之后同步保存一份checkpoint并且把历史checkpoint全部保留结果就是共享存储被checkpoint撑爆。我见过一个训练任务跑了一个月checkpoint占了几百GB存储空间最后全平台存储告警。更务实的做法是checkpoint先异步保存到本地磁盘再异步上传到共享存储断开保存环节和训练loop的耦合避免保存过程中阻塞训练。同时只保留最近N份checkpoint加一个周期性的“关键里程碑”保存比如每个epoch结束时保存一份完整的其他中间step只保存模型参数不保存完整优化器状态降低存储压力。4.3 断点恢复的隐性风险恢复训练时最容易翻车的是随机数状态。很多人只恢复了模型参数和优化器状态没有恢复RNG结果训练从同一个loss值继续但数据的shuffle顺序完全变了。模型相当于突然换了一个训练集顺序显示上的loss会有一个明显跳变验证集指标也可能出现短暂波动。另一个恢复到坏点的典型场景是异步保存导致的checkpoint内部不一致。比如模型参数保存的是一份新版本优化器状态保存的却是旧版本中间差了好几百个step。恢复后的训练会挂着奇怪的数值继续走很难察觉。要规避这个问题最好在保存的时候用统一的版本号把模型参数和优化器状态通过同一个commit写入保证恢复时读到的是一份逻辑上一致的快照。并行环境下还有一个更隐蔽的坑模型并行或流水线并行时每个rank只保存自己的分片但如果某个rank的本地文件损坏整个任务就无法恢复。要不要为每个rank保存多副本或者引入checkpoint冗余度这是容量和安全性之间的权衡。我的建议是至少保证同一节点的多个rank不要把checkpoint写到同一块磁盘上否则节点故障会一次性带走多个分片。5. 从训练到推理分布式AI系统后半场的三个实践问题5.1 推理与训练的设计逻辑差异训练阶段的分布式系统追求的是吞吐延迟稍高一点可以容忍反正一个step跑完再跑下一个。但推理服务完全不同用户在意的是P50、P99延迟一次全链路超时就可能让用户体验崩盘。所以当训练系统要走向推理服务时设计逻辑要做相当大的转变。首先是批量策略。训练时为了稳定通常固定batch size推理时却经常遇到变长请求不同用户发来的输入序列长度差异很大。如果数据并行地去填充batch短序列会被浪费长序列又会拖慢整个batch。现在很多推理框架都支持连续批处理也就是在同一个batch里动态插入新请求、移出已完成请求配合流式调度器能显著提升GPU利用率。其次是网络要求。推理服务往往要跟训练任务共享同一套集群网络但如果训练任务在做大流量allreduce推理流量的网络延迟会立刻恶化。我建议推理节点和训练节点在物理网络层面做隔离或至少做QoS优先级配置否则训练任务一跑起来推理P99直接翻倍。5.2 模型路由、版本管理与多租户隔离推理服务起来之后流量入口需要一个路由层。这个路由层不能只是简单地把请求按模型名转发到某个服务实例还要处理模型版本切换、实例容量感知和故障摘除。我见过最简单也最容易出问题的做法用负载均衡按轮询转发请求。一旦某个推理实例因为显存碎片或模型加载失败所有转发到那个实例的请求都会超时用户感知到的就是服务“抽风”。更稳妥的方案是按权重或哈希做一致性路由让同一个模型版本尽量路由到同一组实例在模型版本升级时先启动新版本实例做一小段金丝雀流量验证再把流量逐步切过去。多租户隔离在推理侧也会遇到训练侧没有的问题。多个模型部署在不同实例上有些模型显存占用大有些模型计算耗时长如果某个租户的请求激增可能占满整个推理池的资源影响其他模型的延迟。最简单的手段是按模型维度建立独立的资源池或配额复杂一点的做法是引入优先级队列把离线推理任务和在线推理任务的请求分类到不同队列。5.3 弹性伸缩与冷启动训练任务的扩展是手动可控的推理服务则需要根据流量自动扩缩容。但GPU服务的自动扩缩容比Web服务麻烦很多核心原因是冷启动太慢。一个Web容器可能几十秒就能拉起一个带大模型的推理服务可能需要几分钟加载权重、构建KV cache索引。如果等流量高峰到了再扩容服务早就被打满了。我在实际项目里的做法是维护一个预热的空闲实例池让它预先加载好模型权重只是不接收流量流量上涨时把空闲实例直接接入路由秒级生效流量回落后再把实例从路由摘掉等一段时间确认没有残留请求再销毁。这样缩容不是立刻释放GPU而是先让实例回到预热池待命。另一个容易忽略的点是背压。推理请求量超过服务承载能力时路由层不应该无限重试否则下游被打爆后链路雪崩。我习惯在路由层设置队列长度上限超过上限就快速失败返回“服务忙”同时把重试次数限制在1到2次并且带指数退避。这才是分布式AI系统在推理侧正确的“自我保护”姿势。6. 可观测性排查实录一次训练卡顿背后的完整定位链路6.1 分布式AI系统三板斧指标、日志、链路分布式智能系统的可观测性看起来和普通微服务没有区别无外乎指标、日志、链路追踪三件套但落到训练场景里具体做法完全不同。指标层面除了常规的CPU、内存、网络IO更要关注的是训练step耗时、GPU利用率、显存占用、NCCL通信耗时、数据加载耗时这些训练特有指标。日志层面每一行训练日志都应该带上rank编号和节点标识否则几十个进程的日志混在一起根本无法定位问题。链路追踪在分布式AI里的含义也和微服务不太一样微服务追踪的是“一个用户请求经过哪些服务”而训练任务追踪的是“一个step经过数据加载、前向、反向、梯度同步、参数更新这几个阶段各花了多长时间”。我强烈建议在框架里给每个阶段打耗时标签哪怕是简单的自定义计时器。这个习惯一旦养成排查性能问题的速度会快一个数量级。因为“慢”本身不会告诉你慢在哪一段只有阶段耗时分解能告诉你。6.2 从一次真实卡顿谈起链路是怎么一步步定位出来的前阵子我负责的一个8节点64卡训练任务跑了快两周突然从某天下午开始step耗时从稳定的1.4秒一路涨到4秒GPU利用率掉到50%以下所有节点都没有报错进程也全部存活看起来就像系统突然“发呆”。我当时的排查顺序是这样的。第一步先看worker日志确认没有OOM和NCCL异常报错排除了进程崩溃的可能。第二步按节点维度去看step耗时发现大部分节点依旧保持1.4秒只有两个节点变成了4秒以上问题范围立刻缩小到这两个节点及其连接关系。第三步登录这两个节点看GPU利用率发现GPU确实空闲但CPU使用率偏高说明瓶颈不在计算侧而在数据侧或通信侧。第四步检查网络接口流量发现计算网几乎闲置RDMA流量很小说明也不是跨节点通信的问题。同时检查存储系统发现挂载盘的IO延迟已经跑到几百毫秒数据读取明显卡顿。到这一步根因基本浮出水面共享存储上另一个团队正在批量导出数据集把存储IO带宽全部挤爆我们的训练任务每次从存储读样本都等了几秒钟。训练代码本身没有任何问题但跨团队的共享资源争用把整个任务的效率拖垮了。这个问题暴露出来之后我做了两个调整一是给训练任务的数据目录划分独立的存储配额并限制吞吐防止共享存储被单一大任务打满二是设置存储IO监控告警当IO延迟超过阈值时主动提醒而不是等训练步时间异常才开始查。经过这两项改动同类问题再也没有发生过。6.3 给故障预案留下的“三个抽屉”经历过这次排障之后我把分布式AI系统的故障应急预案整理成了三个抽屉分类存放方便需要时直接取用。第一个抽屉是进程级故障以自动重启为主。只要监测到进程退出、rank掉线或者OOM日志就自动拉起新的进程组并从最近的checkpoint恢复。这里要注意的是重启后rank编号可能发生变化需要把旧rank的本地状态清理干净避免新旧状态混用。第二个抽屉是节点级故障需要在调度层面预分配配额。节点宕机时会带走几个worker训练任务如果只申请了精确的GPU数量就没有了替换空间。我会在初始申请时多预留一小部分冗余节点专门用来承接故障后重调度的worker。第三个抽屉是慢节点响应这是最容易被漏掉的。每个训练任务都要记录rank维度的step耗时设置漂移阈值一旦超过阈值就自动摘除慢节点并触发重新分配。按我的经验先把step耗时按阶段打点再结合这三个抽屉的预案比事后翻日志排查要省太多时间。分布式AI系统的稳定运行靠的不是运气而是提前把每一种失败路径都演练一遍。