news 2026/9/15 7:37:46

详解分布式训练8大集合通信原语:从Send/Recv到All2All

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
详解分布式训练8大集合通信原语:从Send/Recv到All2All

我写过不少分布式训练相关的代码,也帮人排查过无数次训练卡死、慢节点的问题。说句实在话,大部分训练任务跑不动,根因都不在模型结构,而在通信。模型并行、数据并行、专家并行、序列并行,这些并行策略落到硬件上,全部要翻译成一张张通信图。Send/Recv、AllReduce、All2All 这些名字你可能都见过,也可能在框架文档里扫过一眼,但真要让你说清楚它们之间有什么区别、什么时候该用哪一个、底层到底怎么流转的,很多人就含糊了。

这篇文章我就把这 8 个最常用的集合通信原语逐个拆开,用场景化的方式讲明白每个原语的数据流向、适用场景、通信量计算,以及我在实际项目中踩过的坑。不管你是刚接触大模型训练,还是已经在调多机多卡任务,这篇内容都值得花二十分钟读完。

1. 从单卡到千卡:为什么你必须理解集合通信

1.1 一次模型并行训练,通信到底占了多少时间

很多人第一次接触分布式训练,是直接在 PyTorch 里写了DistributedDataParallel,然后发现多卡训练加速比远达不到线性。比如 8 张卡,理想情况应该快 8 倍,实际能快 5 倍就算不错了。如果模型再大一点,通信占比甚至会超过计算占比,出现“加卡反而变慢”的反直觉现象。

我在一个 70B 参数模型的训练任务里做过一次 profiling,纯计算时间占 38%,剩余 62% 全部消耗在通信等待上。当时用的是 64 张 A100,数据并行加张量并行混合,通信模式覆盖了 AllReduce、AllGather、ReduceScatter 三种。你不需要做到这个颗粒度的 profiling,但你至少要知道通信不是“顺便传一下”,它是训练主链路上的一等公民。

集合通信原语就是构成这条链路的砖块。不同并行策略其实就是不同的砖块组合方式:数据并行主要用 AllReduce,张量并行用 AllReduce 和 All2All,序列并行用 AllGather 和 ReduceScatter,专家并行用 All2All。理解了每个砖块的形状,你才能理解框架为什么这么设计,也才能在自己调优时不瞎猜。

1.2 逻辑视图和物理拓扑,别再傻傻分不清

学习集合通信最容易混淆的两个概念,一个是逻辑视图,一个是物理拓扑。我用一句话来区分:逻辑视图解决的是“数据从哪来到哪去”的问题,物理拓扑解决的是“数据走哪条路”的问题。

举个例子,AllReduce的逻辑视图是所有 GPU 把自己的数据贡献出来,最终每张卡都拿到一份完整的总和。这只是一个逻辑约定,至于底层是走环形、树形还是分块流水,那是通信库(NCCL)根据硬件拓扑自行决定的。你在写代码的时候不用关心 NCCL 里用的是 Ring AllReduce 还是 Tree AllReduce,但你必须知道:不同拓扑下通信量不一样,最终影响的是扩展效率。

所以这篇文章先讲逻辑视图,把 8 个原语的数据流图和适用场景讲透。物理拓扑、网卡绑定这些属于下一层的话题,在最后一节我结合项目经验单独说。

2. 点到点通信:Send/Recv 是所有原语的地基

2.1 Send/Recv 到底在传什么

点点通信是集合通信的最小单元,也是最容易被忽略但最重要的原语。它做的事情一句话就能说清:一个进程把数据发给另一个进程。但正是这个最简单的操作,构成了所有其他集合原语的基础。

你看 NCCL 源码就会发现,Broadcast底层是拆成一连串的 Send/Recv,AllReduce的 Ring 实现也是每个节点只跟相邻节点做 Send/Recv。所以如果你把 Send/Recv 理解透了,再看其他原语就会觉得全是套路。

实际操作中,Send/Recv 通常不是单个数据包发完就结束的。大模型训练里一个张量动辄几百 MB,直接整块发送会占满显存带宽和网络带宽。所以实际工程实现都会做分块,每一块单独收发,同时用 CUDA 流来做计算和通信的重叠。我见过不少新手在自定义通信逻辑时忽略了分块,结果显存直接 OOM,或者在等待通信时把计算停住了,性能惨不忍睹。

2.2 为什么说没有 Send/Recv 就没有后面的一切

我举个例子帮你建立直觉。想象一个 4 人的流水线:A 负责把原材料切好,B 负责组装,C 负责质检,D 负责包装。如果 A 必须等 B 完全做完才能把下一批料送过去,整条流水线就退化成串行。Send/Recv 在这里干的事就是“送料”这个动作,但怎么送、什么时候送,直接决定流水线吞吐量。

放到模型训练里,流水线并行(Pipeline Parallelism)就是这种场景。每个 GPU 负责模型的一部分层,前向传播时数据一层层往后传,反向传播时梯度一层层往前传。这里的层间传输用的就是 Send/Recv(在 PyTorch 里封装为torch.distributed.sendrecv,或者通过P2P操作完成)。

踩过的坑提醒一下:手写 Send/Recv 时,收发顺序必须严格一致,否则就会死锁。比如进程 0 先发后收,进程 1 先收后发,如果进程 0 的send是同步阻塞的,而进程 1 还在等recv,两边就都卡死了。我早期调试流水线并行时,就因为这种顺序问题干等了一晚上。解决办法是给收发加 tag,或者统一用异步版本,再配合超时检测。

3. 基础集合三件套:Broadcast、Reduce、Scatter

3.1 Broadcast:让所有人都拿到同一份数据

Broadcast的逻辑很直白:一个节点把自己的数据复制给所有其他节点。典型场景是模型参数初始化。训练开始前,rank 0 负责初始化参数,然后通过 Broadcast 同步给所有进程,保证每张卡从同一组参数出发。

实现层面,朴素方式是 rank 0 逐个给每个节点发一份,复杂度是 O(N),N 是节点数。更高效的做法是二叉树传播:rank 0 发给两个节点,这两个节点再各自发给两个节点,这样只需要 log(N) 轮就能完成广播。NCCL 在节点规模大的时候会自动选择树形算法,这也是为什么你经常在日志里看到Broadcast走的是 Tree 而非 Ring 的原因。

3.2 Reduce:把多份数据算成一份

如果说 Broadcast 是一对多,那Reduce就是多对一。每个节点提供一份数据,最终汇聚到一个节点上,聚合方式可以是求和、求最大值、求最小值等,由op参数指定。分布式训练里最常用的op就是 SUM,也就是把各卡算出的梯度加起来。

你可能要问:既然 Reduce 是聚合,那AllReduce和它什么关系?一句话回答:AllReduce=Reduce+Broadcast。先归约到某个节点,再广播回所有节点。但这个实现不是最高效的,所以工业界才发明了 Ring AllReduce 这类更聪明的算法。

在实践中,Reduce 单独出现的场景并不多,更多是作为更大通信模式的一个环节。比如在参数服务器架构中,worker 把梯度 Reduce 到 server 节点上,server 更新参数后再 Broadcast 回去。现在大模型训练基本不用参数服务器了,但理解这个模式能帮你理解分布式训练的历史演进。

3.3 Scatter:把数据按块分发出去

ScatterBroadcast很像,都是从一个节点把数据分发到多个节点,但有一个本质区别:Broadcast 是每个节点拿到的数据相同,Scatter 是每个节点拿到的是整块数据的一个切片。

生活化的类比是发试卷:Broadcast 是给每个人发一套一模一样的卷子,Scatter 是把一本习题册撕开,按章节分给不同的人去完成。训练中的典型场景是数据并行中的数据分片,每个进程只处理一个 batch 的一部分样本。因为每个进程拿到的数据不同,所以梯度计算完后需要 AllReduce 来汇总。

4. 训练中最常用的两个主角:AllReduce 与 AllGather

4.1 AllReduce:梯度同步的绝对主力

如果说大模型分布式通信里只能记住一个原语,那一定是AllReduce。数据并行训练里,每个进程用不同的数据算出各自的梯度,这些梯度必须求和,然后每个进程才能用完全一样的梯度去更新参数。这个过程就需要 AllReduce。

它的逻辑视图一句话:N 个节点各输入一份数据,经过逐元素求和(或取最大、最小、平均等操作),每个节点得到完全相同的结果。

通信量分析我直接给结论:朴素 AllReduce 的通信量是2(N-1) × M,M 是单个节点的数据量。Ring AllReduce 优化后的通信量是2(N-1)/N × M,当 N 很大的时候接近2M,也就是和节点数基本无关。这是 Ring 算法最核心的价值:通信成本不随节点规模线性增长,所以才能支撑超大规模集群。

实际项目里,我看到很多人纠结要不要手动调 NCCL 算法。我的建议是不要动。NCCL 会根据拓扑自动选择 Ring 或者 Tree,手动指定往往适得其反。你需要关注的不是算法,而是确保通信数据没走错网络路径,这个后面再讲。

4.2 AllGather:分片算、聚合用的核心原语

AllGather和 Scatter 恰好是相反的操作。Scatter 是把一份数据切分给多个人,AllGather 是每个人手里有一份不同的数据,最后每个人手里都拿到所有人的完整数据。

打个比方:小组作业分工,每个人查一个章节的资料,最后每个人都拿到一份完整的资料合集。注意,每个人拿到的是完整合集而不是自己那份。

在张量并行(Tensor Parallelism)中,AllGather 是高频操作。比如按列切分的线性层,前向传播时需要把各卡算出的结果拼成完整的输出,这时就要用 AllGather。在 PyTorch 的Tensor Parallelism实现里,AllGatherReduceScatter这对组合的使用频率非常高。

实现注意点:AllGather 要求每个进程的输入块大小一致,而且最终拼接的顺序是按 rank 顺序排列。如果你用的是torch.distributed.all_gather,要确保传入的 tensor 列表长度和顺序跟 world size 一致,否则数据就错位了。这个错位不会报错,只会让你的模型精度神秘地崩掉,排查起来非常痛苦。

4.3 Gather 与 AllGather 的区别与选择

Gather是 AllGather 的单点版本:所有数据最终只汇聚到一个节点,其他节点拿不到完整结果。它对应的应用场景是日志聚合、 checkpoint 保存前的数据收集。

实际训练中绝大多数场景需要的是 AllGather 而不是 Gather,因为所有节点在下一轮计算时仍然需要完整数据。只有在你需要把结果集中到主节点做特殊处理时,Gather 才有意义。

选择标准我总结成一个问题:这个数据聚合完之后,是只有主节点用,还是所有节点都要用?前者用 Gather,后者用 AllGather。平时训练代码里我看大家几乎只接触过all_gathergather出现得少,这很正常,不必觉得是自己漏了什么。

5. 最高阶的全交换:All2All

5.1 All2All 的逻辑视图与工作方式

All2All是 8 个原语里最复杂也最灵活的一个。它的逻辑视图是:每个节点把自己的数据切分成 N 份(N 是节点数),然后把第 i 份发给节点 i,同时每个节点把从其他节点收到的数据拼接起来。

简单说,Scatter 是单点分发,AllGather 是全局汇聚,而 All2All 是每个节点同时充当发送方和接收方,数据在节点间做全排列交换。生活化类比是换座位:每个人把自己的东西分出一部分给每个人,同时从每个人那里收到一部分。

通信量上,All2All 的总通信量是N(N-1) × M',其中 M' 是每个节点发给另一个节点的数据量。这也就意味着 All2All 的通信量随节点规模的平方增长。这也是为什么 MoE 模型在专家数量很多时,通信会成为绝对的瓶颈。

5.2 All2All 的典型应用场景

All2All 最经典的训练场景是 MoE(Mixture of Experts,混合专家模型)中的专家并行。MoE 模型里,不同的 token 会被路由到不同的专家网络,每个专家放在不同的 GPU 上。token 对应的 embedding 需要被送到专家所在的 GPU 上,这本质上就是一个 All2All 操作。

另一个典型场景是序列并行中的 attention 计算。某些序列并行策略需要把 Q、K、V 的序列维度做切分,在注意力计算前后需要进行 All2All 来交换序列块。

我在实际项目里遇到过一个问题:All2All 在小规模集群上表现很好,扩到大规模后性能急剧下降。原因是 All2All 的通信量是平方级增长,网络很快就饱和了。这时候你需要做的是引入层次化 All2All,也就是先组内交换,再组间交换。比如 64 台机器,先按 8 台一组做组内 All2All,再把组间结果汇总。这个优化能显著降低单次通信的数据量,属于工程上非常实用的一招。

6. 从原语到框架:一次项目中的真实选型与部署

6.1 选型背后的考量

说了这么多理论,我来还原一个真实项目的选型过程。任务是训练一个 30B 参数的 MoE 模型,8 台机器,每台 8 张 A100,总共 64 卡。如果你是我,你怎么设计并行方案?

我的选择是:张量并行 + 数据并行 + 专家并行。张量并行解决单层参数放不下一张卡的问题,专家并行解决 MoE 专家路由分发问题,数据并行处理 batch 维度的扩展。这三者对应的通信原语分别是 AllReduce(张量并行的前向/反向)、All2All(专家路由)和 AllReduce(梯度同步)。

选型逻辑其实就一条:先看模型结构里哪些维度可以切,切完之后数据之间是什么依赖关系,依赖关系决定了通信原语。不是先选原语再想并行策略,而是反过来。

比如线性层参数是[输出维度, 输入维度],你可以沿输出维度切成 N 块,每张卡算完一部分输出,最后把输出拼起来——这就是 AllGather 场景。也可以沿输入维度切开,每张卡算完部分结果后把各部分相加——这就是 AllReduce 场景。模型结构决定一切,原语只是工具。

6.2 通信量计算示例

这里我给一个具体的通信量计算过程,方便你以后自己估算。

假设张量并行中,一个 Transformer 层的隐藏维度是 H = 8192,参数精度是 FP16(2 字节),并行度是 N = 8。

前向传播中,线性层输出需要做一次 AllReduce,通信量大约为2 × H × 2 字节 × (N-1)/N,简化计算大约是2 × 8192 × 2 × 0.875 ≈ 28 KB。单看这一层不大,但 Transformer 有几十层,每层有多个线性层,累计起来就是几百 MB 的通信量。

再看 All2All。假设 MoE 层有 E = 64 个专家分布在 8 张卡上,每张卡处理 B = 1024 个 token,每个 token 的隐藏维度是 H = 8192。路由到其他卡的 token 数量为 1024 × 63/64 ≈ 1008(实际会更复杂,因为路由不均衡)。按照每个 token 2 字节 × 8192 维计算,每张卡要发送的数据约为1008 × 2 × 8192 ≈ 16 MB。乘以 8 张卡就是约 128 MB 的单层通信量。注意,这只是一层 MoE,模型有几十层,通信量是非常可观的。

这就是为什么 MoE 训练对网络带宽要求极高:几百 MB 的通信如果跑在 25 Gbps 的网络上,单层就要传 40 多毫秒,几十层叠加就是秒级延迟。这也是为什么很多公司训练 MoE 模型必须用 400 Gbps 甚至更高带宽的 RDMA 网络。

6.3 实际部署中的注意事项

部署时我踩过几个坑,写出来给大家避雷。

第一,网卡绑定要确认。多机通信走的是网卡,但每台机器可能有多个网卡,NCCL 会自己选,但如果你的网卡性能和配置差异很大,NCCL 可能选到性能差的那张。方法是用nvidia-smi topo -m查看 GPU 和网卡的亲和性,确保 NCCL 用的是和 GPU 在同一 NUMA 节点上的网卡。

第二,共享内存和锁页内存。小规模单机多卡训练时,GPU 间通信会走 NVLink,同时也可能用到共享内存做 staging。不要手动去限制 NCCL 可以使用的共享内存大小,否则可能导致通信性能骤降。我遇到过有人为了“优化内存”设置了NCCL_SHM_DISABLE=1,结果多卡训练速度掉了 30% 以上。

第三,超时设置。NCCL 默认超时时间可能不够长,大模型 checkpoint 加载完之后的第一次 AllReduce,因为数据量巨大,容易触发超时。建议在代码里显式设置更长的超时时间:os.environ["NCCL_TIMEOUT"] = "3600",或者用torch.distributed.init_process_group(timeout=...)进行配置。

7. 疑难杂症与排查经验

7.1 常见问题速查表

我把这几年在集合通信上碰到的典型问题整理成一个速查表,方便你遇到问题时快速定位。

现象可能原因初步排查方法
训练直接卡死(hang)Send/Recv 顺序不匹配、AllReduce 个别进程未参与nsysncu看卡在哪个调用点,检查 rank 间调用顺序
多卡加速比很低通信未与计算重叠、网络走了慢路径nvidia-smi的 GPU 利用率是否周期性归零,检查 NCCL 日志中的算法选择
精度与单卡不一致AllGather 拼接顺序错位检查 tensor list 是否按 rank 顺序传入
显存 OOMAll2All 数据量超预期打印每卡发送和接收的数据尺寸,确认分块大小
网络带宽远低于预期网卡绑定问题、交换机拥塞ib_write_bw测试节点间带宽,检查拓扑亲和性
通信时快时慢多任务共享了同一张网卡或交换机确认集群隔离配置,避免和其他大流量任务混跑

7.2 排查思路:先用最小复现,再逐层放大

遇到集合通信问题,我习惯的排查路径是“最小复现法”。先在 2 台机器、每台 1 张卡的最小配置上跑通通信原语,确认代码逻辑没问题;再逐步增加到单机 8 卡,排除单机内拓扑问题;最后才扩展到多机。

这个思路看起来笨,但效率极高。因为多机环境变量多,一旦直接上多机排查问题,网络、网卡、交换机、NCCL 算法选择全搅在一起,很难定位根因。用最小配置先排除确定性 bug,再用增量方式复现非确定性问题,这是目前最稳妥的方法。

调试工具方面,我推荐优先看这几个:

  • torch.distributed.all_reduce的调试打印,确认每个 rank 参与情况
  • nsys profile追踪 GPU kernel 和通信调用的时间线
  • NCCL 的 debug 日志,设置NCCL_DEBUG=INFO,看它选择了什么算法、走哪条链路

7.3 一个真实的 All2All 性能排查案例

最后分享一个我印象很深的案例。有次训练一个 MoE 模型,发现 16 卡扩展到 32 卡,吞吐量只涨了 20%,远低于预期。我第一反应是网络瓶颈,但测下来带宽完全够。

后来仔细看时间线才发现,问题出在 All2All 的实现方式上。通信库默认做了全连接式 All2All,也就是每张卡直接和其他所有卡都建立通信链路,32 卡意味着 992 条链路。虽然单条带宽够,但这么多条并发链路在交换机上造成了严重的拥塞。

解决方案是改用层次化 All2All,先做节点内(8 卡)的小范围交换,再把结果跨节点传播。这个改动不到 50 行代码,但通信时间减少了 55%,整体训练吞吐量提升了接近 40%。

这个案例说明一个非常重要的道理:集合通信不是“原语选对了就行”,还要考虑数据规模和网络拓扑的匹配。

写在最后

做分布式训练这几年,我最大的体会是:框架帮你封装了 90% 的通信细节,但剩下的 10% 恰恰是决定训练效率的关键。你可以不手写 AllReduce,但不能不知道它背后的数据流和通信量;你可以不理解 NCCL 的源码,但不能不知道它选错拓扑时会发生什么。

熟悉这 8 个集合通信原语之后,再看 PyTorch 的 DDP、DeepSpeed 的 ZeRO、Megatron 的并行实现,你会有一种“原来如此”的清晰感。它们看似复杂,本质就是这些原语在不同策略下的排列组合。

最后再分享一个小习惯:每当要设计一个新的并行方案,我都会先画一张数据流图,把每个节点输入什么、输出什么、需要哪些原语标清楚。这张图比我写半天的文字说明都有用。这篇文章里讲的每个原语,你都可以用同样方式画成图,画着画着,分布式通信这层窗户纸就捅破了。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 7:37:08

AI算力爆发下的液冷技术解决方案与实战经验

1. AI算力爆发的电力困境2023年全球AI数据中心耗电量已相当于一个中等国家的年用电量。我在参与某大型语言模型训练项目时,单次实验就触发了机房电力警报——8组DGX A100满载运行时,瞬时功耗突破240kW,相当于300台家用空调同时启动。1.1 算力…

作者头像 李华
网站建设 2026/9/15 7:36:57

大道至简 - 基于Docker的Serverless探索之旅

近年来, 热门话题出现了一个全新的构建架构风格。新技术浪潮下不同的人, 会有不一样的解读。本文要对编程模型展开分析, 借助容器技术去打造一个最为简单的平台。简介随着移动互联网, 以及物联网和大数据应用迅猛发展, 人们对云计算的需求被极大促进。然而, 要让应用架构具备良…

作者头像 李华
网站建设 2026/9/15 7:35:07

Vue的基础原理和使用

一、Vue 1、前言 前端的基础知识参考:javaweb前端基础(HTML、CSS、Javascript、vue3、ajax/axios)-CSDN博客文章浏览阅读225次,点赞6次,收藏3次。本项目综合运用了三大核心技术,实现了一个完整的四段式页…

作者头像 李华
网站建设 2026/9/15 7:34:55

光伏CAD插件实战指南:从组件排布到施工图全流程

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/15 7:34:00

2026年AI编程实战地图:场景化工具链协同指南

1. 这不是工具清单,而是一份2026年AI编程生产力的实战地图“2026年AI编程工具大全,33个主流工具一次看懂”——看到这个标题,你脑子里浮现的可能是一页密密麻麻的软件名官网链接一句话介绍的表格。但我要坦白告诉你:那种清单&…

作者头像 李华
网站建设 2026/9/15 7:31:24

Hibernate 核心原理与架构详解

Hibernate 核心原理与架构详解 定位:Hibernate 架构分层、启动引导、持久化流程、代理原理、事务连接与类型系统 适用版本:Hibernate ORM 6.x(Jakarta Persistence 3.1) 目录 整体架构启动引导持久化流程代理生成原理连接与事务集…

作者头像 李华