Skip to content

20. NCCL and AllReduce Basics | NCCL 与 AllReduce 基础

难度: Medium | 环境: CPU-first | 标签: 并行通信, NCCL, AllReduce | 目标人群: 通信机制入门者

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)


本节导读

多卡训练扩不动,很多时候不是算子不够快,而是同步点太重。梯度要不要聚合、聚合走哪条链路、等待会不会把 step time 重新拉长,这些问题一旦没想清楚,GPU 数量上去之后也可能只是在放大通信成本。

这一页在整个教程的纵向主线里属于 Part 01 的多卡通信基础页,优先服务 监督微调路线 的分布式训练判断,也给后续并行与系统优化页建立 collective 前置。学完这里,后面再看 26 / 27 / 79 以及多卡训练相关项目页时,你会更容易把“多卡同步”拆成更具体的判断:同步发生在哪里、频率高不高、通信有没有压过计算;如果这里没学明白,后面很容易把扩展效率问题都归因到“卡不够快”,而忽略 collective 本身已经成了 step time 的主瓶颈。按专题归类,这一页主要属于 通信与并行专题,也和 监督微调路线 的训练工程判断直接相连。

关键词: NCCL, AllReduce, DP


前置阅读

导语: 先把通信和并行层级对齐,再看 AllReduce 与 NCCL 会更顺;如果你正在走 监督微调路线,这里会直接服务多卡训练同步、effective batch 和扩展效率判断。

相关阅读

导语: 把 AllReduce 放回 ZeRO、流水线并行和训练性能分析里看,更容易把通信原语和实际扩展效果连起来。


Q1:NCCL 在分布式训练里到底解决什么问题?

点击展开查看解析

NCCL 解决的是多 GPU 之间高效通信的问题。

在分布式训练里,单卡算完还不够,参数、梯度和状态都要在多卡之间同步。如果通信效率太低,GPU 就会在等待数据时空转,扩展效果会很差。

NCCL 的作用,就是给这些通信原语提供高效实现,让多卡之间的同步尽量接近硬件能力上限。它本质上不是“额外功能”,而是分布式训练能否真正扩展的基础设施。

### Q1小验证:为什么多卡离不开通信库

先记住:多卡不是把计算复制几份就结束了。

python
def step_time(compute_ms, sync_ms, tasks):
    # 多卡场景下,除了计算,还要付出同步成本。
    return compute_ms + sync_ms * max(tasks - 1, 0)

for tasks in [1, 2, 8]:
    print(tasks, 'GPUs ->', step_time(10, 3, tasks), 'ms/step')

Q2:AllReduce 为什么是最核心的通信原语之一?

点击展开查看解析

AllReduce 的核心是“每张卡都有一份数据,最后大家要得到同一个聚合结果”。

这在数据并行训练里特别常见,因为每张卡都会计算局部梯度,最后需要把梯度做求和或平均,再同步回所有设备。这个过程如果效率低,就会拖慢整条训练链。

AllReduce 之所以重要,是因为它几乎代表了“多卡协同”的基础动作。很多通信优化、拓扑设计和同步策略,本质上都在围绕它展开。

### Q2小验证:为什么梯度同步离不开 AllReduce

先把“局部结果 -> 全局一致”这件事想清楚。

python
def allreduce_mean(values):
    total = sum(values)
    return [total / len(values)] * len(values)

gradients = [0.8, 1.2, 1.0, 1.4]
print('before:', gradients)
print('after :', allreduce_mean(gradients))

Q3:为什么通信带宽和拓扑会直接影响训练扩展效果?

点击展开查看解析

通信不是只有“能不能通”的问题,更重要的是“通得快不快”。

如果带宽低、拓扑绕路多、同步链路长,那么多卡之间的通信时间就会迅速放大,最后训练速度可能并不会随着 GPU 数量线性提升。

所以判断并行方案时,不能只看算力堆了多少卡,还要看通信是否会成为主瓶颈。拓扑、带宽、同步原语和调度策略共同决定了最终的扩展效率。

### Q3小验证:为什么拓扑会影响速度

先判断是带宽问题,还是路由和同步问题。

python
def comm_time_ms(size_mb, bandwidth_gbps):
    # 粗略估算:MB -> Mb,再除以 Gbps。
    return size_mb * 8 / bandwidth_gbps

payload_mb = 256
cases = {'nvlink': 900, 'pcie': 64}
for name, bw in cases.items():
    print(name, '->', round(comm_time_ms(payload_mb, bw), 2), 'ms')
print('slowdown:', round(comm_time_ms(payload_mb, 64) / comm_time_ms(payload_mb, 900), 1), 'x')

Q4:Ring AllReduce 为什么常被用来解释 AllReduce 的实现思路?

点击展开查看解析

Ring AllReduce 的关键点,不是“名字里有 ring”,而是它把一次全量聚合拆成了两个更可控的阶段:

  1. Reduce-Scatter:先沿着环把各卡的数据逐步归约,并把结果切成若干块分散到各卡;
  2. All-Gather:再沿着环把这些局部归约结果重新拼回完整结果。

这样做的好处是:每张卡在每一轮只发送和接收一小块数据,通信负载更均匀,不需要某一张卡一次性承担全部传输压力。

所以 NCCL 之所以重要,不只是因为它“能做 AllReduce”,而是因为它能把这些通信原语落成对拓扑更友好的实现。对于训练来说,真正的瓶颈常常不是有没有通信,而是通信是否能被拆成适合硬件链路的节奏。

### Q4小验证:AllReduce 为什么常拆成两段
python
def ring_rounds(world_size):
    # Ring AllReduce 通常可拆成两个阶段,每阶段有 world_size - 1 轮。
    return 2 * (world_size - 1)

for n in [2, 4, 8]:
    print(n, 'ranks ->', ring_rounds(n), 'rounds')
print('phases:', ['reduce-scatter', 'all-gather'])

⚠️ 常见误区

  • NCCL 不是模型结构的一部分,但它会直接决定多卡训练是否顺畅。
  • AllReduce 不是唯一通信原语,但它是最常见、最关键的同步动作之一。
  • 多卡扩展不是“卡越多越快”,通信常常会限制实际收益。
  • 先看通信原语,再看拓扑和带宽,通常更容易定位分布式瓶颈。

Released under the MIT License.