All-to-All Grouping
导言
all-to-all 要让每个参与者给其他参与者发送数据,为什么还要分 group?分组可以控制同时争用有限资源的请求,让通信更接近硬件能稳定处理的速度;但它是否能“防止阻塞”,取决于分的是什么,以及阻塞发生在哪一层。本文从四个 rank 的调度例子出发,解释限流、错峰、接收许可和拓扑分层,再区分 NCCL group 的并发推进语义。目标是看懂通信实现的设计动机,并知道怎样验证收益;文中的容量与规模例子均为推导,没有集群性能实测。
先看“全部通信”要求了什么¶
“既然最终都要发送,越早全部发出去,应该越快。”这个判断在网络、端点和软件进度引擎都有足够资源时完全合理。分组之所以有时更快,是因为真实系统未必能同时高效处理所有请求。
先固定通信语义。设有四个 rank,编号为 0、1、2、3,每个 rank 准备四块数据:一块给自己,其余三块分别给其他 rank。例如 rank 0 的输入是 [x00, x01, x02, x03];通信完成后,rank 2 收到 [x02, x12, x22, x32]。下标的第一位是来源,第二位是目的地。
All-to-all 规定最终的数据归属,没有要求所有传输必须同时发生。这来自 MPI 对块交换的定义;具体实现可以采用直接发送、分轮交换或其他等价算法。本文讨论常见的同一通信域内交换,自身那块可以本地复制,不需要走网络。MPI All-to-All 语义
这也给“分 group”划出第一条边界:
- 只把工作分批:四个 rank 最终仍交换全部目标数据,改变的是调度。
- 把通信域拆成
{0,1}和{2,3},各自独立做 all-to-all:原来跨集合的数据没有送到,已经改变任务。只有应用本来就允许独立通信,或者另有跨组阶段补齐数据,才成立。
后文先讨论第一种:总任务不变,怎样安排它更快完成。
分批为什么可能比齐发更快¶
活跃请求也要消耗资源¶
一条传输除了 payload,还可能占用请求描述符、网卡发送队列、接收缓存、软件轮询时间,以及负责搬运和处理完成事件的执行资源。具体占用哪些资源,取决于 MPI、NCCL 或自定义通信内核的实现。
因此,增加并发有两个阶段:资源尚未吃满时,它能填补等待空隙;资源已经饱和后,继续增加请求可能主要增加排队、调度开销和尾延迟。
用一个教学容量模型说明:接收端每个时间单位能处理两块数据,而这一单位突然来了六块,那么四块必须等待。提前把它们全部注入接收路径,不会把服务能力从两块提高到六块。如果额外队列又挤占共享缓存、触发回压或拖慢进度处理,总体完成时间还可能增加。反过来,如果排队没有额外代价,服务端始终满载,仅把等待从接收端搬到发送端,也不必然更快。
Open MPI 提供了可核查的工程例子。其 5.0.7 版本 Alltoall 算法既包含一次启动全部非阻塞收发的 linear,也包含只维持有限在途请求的 linear_sync;后者通过 coll_tuned_alltoall_max_requests 控制窗口。Open MPI 5.0.7 算法说明
这里的直接收益是给活跃请求数量设置上界。它不意味着 communicator 变小、已创建的 QP 被销毁,或者完整输入输出 buffer 随窗口同比缩小。能否减少内存,还取决于实现是否按窗口复用临时存储。
只限制发送端,还不够¶
假设每个 rank 一次只发一个目的地,看起来已经很克制。但如果 rank 1、2、3 都先发给 rank 0,rank 0 仍然同时收到三路流量。每个发送端的扇出小,不代表每个接收端的扇入小。多源同时汇入一个接收端,就是 incast。
错开目的地能进一步限制接收端竞争。下面是四个 rank 的循环平移调度;每个格子只写该源本轮的发送,接收由对应箭头确定。
| 轮次 | rank 0 发送 | rank 1 发送 | rank 2 发送 | rank 3 发送 |
|---|---|---|---|---|
| 1 | 0 → 1 | 1 → 2 | 2 → 3 | 3 → 0 |
| 2 | 0 → 2 | 1 → 3 | 2 → 0 | 3 → 1 |
| 3 | 0 → 3 | 1 → 0 | 2 → 1 | 3 → 2 |
每一轮,每个 rank 恰好发给一个其他 rank,也恰好从一个其他 rank 接收。三轮覆盖全部 12 条非自身有向传输;输入块保留到对应发送完成,接收块写入按来源划分的输出位置,不需要额外复制出三套完整输入。
Open MPI 5.0.7 的 ompi_coll_base_alltoall_intra_pairwise 使用相应的目标计算。下面仅摘录目的 rank 计算,省略的收发 buffer 地址、sendrecv 调用和错误处理可见原文件:
其中 rank 是当前编号,size 是通信域大小,step 是当前轮次。实现遍历 1..size,把自身复制放在最后;上表只展示非自身的三轮。固定版本源码,第 189–204 行
把单 rank 换成一组 rank,也能采用相同思路。例如 64 个 rank 分为四组,每组 16 个;每轮每个源组只面向一个目标组,并让组间配对形成置换。这样目标组这一轮只接收一个源组的流量,但组内每个接收 rank 仍可能面对 16 个源。组间错峰没有消除组内竞争。该例只是调度结构推导,不代表 16 是最优组宽。
以上保证都属于逻辑端点层面。多个不同目的地仍可能经过同一条 PCIe 上行链路、网卡或交换机出口,因此还要把 rank 编号映射回物理拓扑。网络路径选择和端点拥塞控制也有不同作用范围,不能只凭端点配对均匀就认定整网无拥塞。NVIDIA 对路径均衡与端点 incast 的区分
快慢不一致时,需要接收许可¶
表格把每轮画得整齐,真实 rank 的完成时间却不相同。如果快发送端直接进入下一轮,而慢接收端还在处理上一轮,原本错开的流量又可能叠到一起。
一种通用方法是 credit,接收许可:发送端只有拿到对应接收资源的许可,才发下一块;接收方在数据被消费、对应槽位能安全复用后返还许可。尚未获得许可的数据留在发送端,等待被限制在接收资源之外。
这个协议需要说清三个细节:
- 许可按什么计量:每条消息、每个 chunk、字节数,还是某个 group 的一轮工作;变长消息下,“两个请求”不等于固定字节数。
- 许可何时返还:发送方本地完成、数据对端可见、接收方消费完成,是不同事件。若许可保护接收槽位,必须等到它确实能复用。
- 许可覆盖谁:每个 peer 都单独允许两块,面对 32 个 peer 时仍可能积累 64 块。要限制共享接收资源,需要总额度或其他等价约束。
credit 自身增加信号、计数器和依赖;初始额度、完成通知的推进以及循环等待也必须设计正确。它是受控背压机制,不是“加上一个 flag 就绝不会挂住”的保证。
另一种办法是使用可补充的在途窗口。Open MPI 的 linear_sync 在有限数量的收发请求中等待任一请求完成,再补入同类型的新请求;它并不要求每批都执行全局 barrier。窗口上限分别作用于发送和接收,例如上限为四时,可以有四个发送请求加四个接收请求。这是本地请求窗口,不应直接称为接收方 credit 协议。两者都限制活跃工作,但约束的位置不同。linear_sync 源码说明及推进循环
这些机制能帮助理解仓库已有的 MoonEP Group Design:它用交互场景展示容量、错峰与许可,再映射到固定版本实现。具体的 AIV 分工、组员编号和完成协议仍应回到该文的源码证据,通用推导不能证明某个硬件上的最优参数。
“防止阻塞”需要分层理解¶
到这里,分组缓解拥塞的逻辑已经清楚:控制活跃工作,分散热点,并限制快发送者对慢接收者的冲击。但工程里说的“通信阻塞”,至少有以下几种不同情况。
| 现象 | 实际发生了什么 | 分组能帮助到哪里 |
|---|---|---|
| 拥塞与回压 | 请求到达速度超过链路或端点服务能力,队列增长,发送被减速或暂停 | 并发窗口、错峰和接收许可可能降低瞬时压力 |
| 队头阻塞 | 队首工作暂时不能推进,把本可执行的后续工作也挡住 | 需要独立队列、可绕过调度或更小粒度完成机制;仅编号分组不够 |
| 等待慢 rank | 后续阶段需要的数据尚未全部到齐 | 流水或独立完成可以缩小等待范围;最终需要的那份慢数据仍要等 |
| 死锁 | 存在循环等待,所有相关操作都无法继续 | 必须修复依赖和提交顺序;缩小 group 本身没有保证 |
| API 阻塞 | 调用尚未返回,或主机正在等待 GPU 完成 | 这是接口与完成语义,不能据此单独判断网络拥塞 |
队头阻塞尤其容易被误用。如果一个 FIFO 中,发给慢目标的数据挡住了后面发给快目标的数据,分开队列且调度器能继续服务其他队列,才有机会隔离影响。仅换一个 communicator 名字,底下仍共享同一个 FIFO,就没有自动隔离。
分组还可能增加等待:若实现规定“本组所有 peer 完成后才能开始下一组”,一个慢 peer 会挡住下一组中本可独立执行的工作。按完成事件补充窗口,可以减少这种整批等待;能否采用它,仍取决于匹配顺序、buffer 复用和应用的数据依赖。
网络层也存在这种传播:RoCE 的 PFC 按优先级暂停流量,共享该优先级的流可能一起受影响。应用层分组最多是减少触发拥塞的压力,不能直接等价成网卡队列或 PFC 优先级隔离。NVIDIA 关于 PFC 与队头阻塞的说明
NCCL group 为什么反而要合在一起¶
ncclGroupStart() / ncclGroupEnd() 包围一组通信调用时,group 表示把这些调用作为一批提交。它与“每轮只准少数 peer 通信”不是同一个抽象。
想象两个 rank 都在同一条各自的 CUDA stream 上先执行 send,再执行 recv。若 send 的完成依赖对方的 recv,而 recv 又排在本地尚未完成的 send 后面,就形成循环等待。这是依赖关系问题,与交换机有没有拥塞无关。
NCCL 的点对点文档明确要求:需要并发推进的调用应合入同一个 group。两端都可以按如下方式表达交换:
ncclGroupStart();
ncclSend(sendbuff, sendcount, sendtype, peer, comm, stream);
ncclRecv(recvbuff, recvcount, recvtype, peer, comm, stream);
ncclGroupEnd();
这是 NCCL 2.27 归档文档的 sendrecv 示例。peer 是对端,comm 是已有通信域,stream 是本地 CUDA stream;发送 buffer 由本 rank 产生,接收 buffer 由本 rank 分配,两端的发送与对应接收必须匹配计数和类型。group 让相关收发能够共同推进,并不创建一个更小的通信域,也不保证底层所有数据同时占用物理链路。NCCL 点对点通信文档
把等待关系看完整
分批时,不能把必须共同推进的操作切到互相等待的两个批次中。group 也不能补齐缺失的 peer、修复不匹配的消息,或消除用户在 CUDA event、stream 与其他 collective 之间构造的循环依赖。
NCCL group 还有两项相关用途:单线程管理多个 GPU 时,避免第一张卡的调用阻止其他卡的调用被提交;聚合多个操作,减少启动开销。这里的优化方向可能是合并更多调用,而限流的方向是减少同时在途的工作,两者可以在不同层次配合。NCCL Group Calls
也不能把 ncclGroupEnd() 返回理解为 GPU 通信数据已经全部完成。普通情形还需要适当的 stream 完成依赖;对非阻塞 communicator,若返回 ncclInProgress,甚至还需先确认后台提交完成,再进行相关 CUDA 操作。主机返回、提交完成和设备完成是不同边界。同一文档的 Nonblocking Group Operation
按拓扑分组还能改变数据路径¶
此前的分批保留同样的最终数据与直接传输关系。按节点、交换域或网络 rail 组织的分层通信,还可能改变数据经过哪里、在哪里聚合和在哪里复制。
设四台机器每台有八张 GPU。先按目标机器整理数据,跨机传送后再在机内分发,可以利用机内与机间链路的性能差异。但如果原来每个源给每个目标的内容都不同,仅把小消息拼成大消息,并不会自动减少跨机器边界必须送达的有效字节;它主要改变消息粒度、路径和资源使用,可能增加打包、转发和临时 buffer。
MoE 有一个不同的机会:同一 token 的 hidden 向量可能需要送给同一目标节点上的多个专家。假设三个专家都在那台机器上,朴素逐专家跨机发送需要三份 hidden;若路由元数据足够,跨机发送一份,到目标节点后再复制给三个专家,就能减少重复跨机 payload。这里减少的是同一份内容的跨机复制次数,不是 all-to-all 分组的一般保证。专家返回的结果通常不同,combine 是否能减少跨机字节,还需检查何处允许做局部归约。
DeepSeek-V3 技术报告 §3.2.2 给出具体实例:它限制每个 token 的目标节点数,先经 IB 发送到目标节点内相同局部编号的 GPU,再通过 NVLink 转发到持有目标专家的 GPU。报告同时描述了发送、转发和接收的 warp 分工。该证据支持路由、拓扑和通信内核共同设计的机制,不能用来证明任意分组都有同样收益,也不能直接迁移成 Ascend UDMA 的性能结论。DeepSeek-V3 Technical Report,v2,§3.2.2
拓扑分层还有一个常见反例:若所有跨机数据都压到一张代理 GPU 或一张网卡,原本可并行的带宽可能闲置,代理却成为瓶颈。分组映射应利用实际链路,不能只追求 rank 编号整齐。
现在可以把几个 group 放到同一张表里比较:
| group 的含义 | 改变的对象 | 主要收益条件 | 新成本或边界 |
|---|---|---|---|
| 独立通信域 | 参与者与数据交换范围 | 应用允许通信局部化 | 全局语义可能改变,跨域交换需另行补齐 |
| 调度分组、在途窗口 | 当前允许推进的 peer 或请求 | 全开并发引发资源压力 | 更多轮次;窗口过小会吃不满带宽 |
| 拓扑分层 | 传输路径、聚合与复制位置 | 能利用快链路、分散热点或去除重复跨域传输 | 打包、转发、额外存储与代理热点 |
| NCCL 调用 group | 一批提交和共同推进的调用 | 收发有并发推进要求,或启动开销显著 | 必须满足消息匹配、提交与完成依赖 |
怎样判断自己的分组有没有用¶
分组不能突破必要数据量与物理带宽决定的下界。在没有拥塞额外损失的理想系统里,如果一个接收端必须收到 64 MB、有效接收带宽为 16 GB/s,那么仅搬这些字节就至少需要约 4 ms;两者都按十进制计。分组改变不了这个算术,只可能避免实际时间被额外排队和控制开销进一步拉长,也可能因调度空隙而变慢。
对稠密的 64-rank 交换,如果每轮每个 rank 最多与四个非自身目的 rank 发送,完整覆盖 63 个目的 rank 至少需要 16 轮,最后一轮是三个。相比全开,这种安排降低同时活跃的逻辑传输数量,总计 64 × 63 条有向传输仍然存在。这只是均匀调度模型;真实 MoE 可能稀疏、变长,不能把轮数比例当成延迟或显存比例。
实际验证可以围绕以下步骤展开:
- 固定语义和工作量。固定硬件、拓扑、rank 映射、数据类型、发送量与实现版本。MoE 重放同一批路由记录,检查每个目标得到的数据和反向结果一致;只调度分组时,不能靠少发 token 获得“加速”。
- 分开并发与错峰。比较全开、同一并发窗口但目的地顺序相同、同一窗口且目的地错开;再单独评估 credit 或滑动窗口。这样才能区分减少请求数和消除接收热点的贡献。
- 扫描适当的窗口范围。例如 1、2、4、8、16 和全开,分别测试小消息、大消息、均匀流量与热点流量。窗口应足够填满传输管线,又不把有限资源推入低效区间;最优值需要测量。
- 测设备完成与整体关键路径。预热后测重复迭代,统计每轮最慢 rank 的完成时间,再对这些迭代求中位数和 P95/P99;同时记录吞吐和完整训练或推理步骤时间。仅测异步 API 的主机返回耗时会漏掉真正通信。
- 找对应的原因证据。结合硬件可用计数器看链路利用率、队列压力、ECN/PFC 或重传、发送提交开销、credit 等待、同步等待及计算重叠。PFC 只适用于相应网络,不能要求 NVLink 或其他互联也出现该指标。
Open MPI 5.0.7 的 Alltoall 提供了一个现成的实验入口:在确认 tuned 组件实际被选中后,用 coll_tuned_use_dynamic_rules=1、coll_tuned_alltoall_algorithm=4 选择 linear_sync,再扫描 coll_tuned_alltoall_max_requests。这些设置针对该版本对应算法,不能直接当作 NCCL、Alltoallv 或 MoonEP 的通用参数。Open MPI 调优文档
如果窗口减小后,队列压力和尾延迟下降、有效吞吐提高,才有依据认为原来的过量并发伤害了性能。如果吞吐下降而拥塞指标没有改善,应考虑原来并没有这个瓶颈。如果只有某个 rank 始终很慢,则还要检查路由倾斜、计算延迟和硬件异常;平均分组不会减少那个 rank 实际必须处理的数据。
回到最初的问题:all-to-all 分组的优势,是把请求的时间、目的地、数据路径与推进依赖组织成硬件能够高效执行的工作。它可以缓解拥塞、缩小部分等待的影响,也可以通过正确的 NCCL 调用分组避免特定死锁。但“分 group 就能防止通信阻塞”仍然太宽泛。判断一个实现时,最有用的追问是:它限制了哪项资源,哪条等待关系因此改变,代价又落在哪里?
参考资料¶
- MPI All-to-All 语义:用于界定最终数据布局,不代表某种网络执行顺序。
- Open MPI 5.0.7 调优文档与对应 Alltoall 源码:用于验证全开、pairwise 和有限请求窗口的区别。
- NCCL 2.27 点对点通信与Group Calls:用于验证共同推进、聚合提交和异步完成边界。
- NVIDIA 网络机制说明:仅用于路径拥塞、端点 incast 与 PFC 的机制区分,未采用其产品性能数字。
- DeepSeek-V3 Technical Report,arXiv v2:仅以 §3.2.2 支持 IB/NVLink 路由与通信协同设计,未把报告效果当作本文实测。
- MoonEP Group Design:仓库内的交互延伸阅读,包含固定版本 Ascend 移植实现的源码入口。