【infra之路】05-分布式训练基础 — 为什么需要分布式 & 通信原语

为什么单卡不够?

先算一笔账:训练一个 7B 参数的模型(如 LLaMA-7B),用 FP16 精度。

复制代码
模型参数:    7B × 2 bytes (FP16) = 14 GB
梯度:        7B × 2 bytes = 14 GB
优化器状态:  7B × 12 bytes (Adam: FP32 参数副本 + 一阶动量 + 二阶动量) = 84 GB
激活值:      取决于 batch size 和 sequence length,通常 10-50 GB
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总计:        ~122 GB(最小配置)

一块 RTX 5060 Ti 只有 8 GB 显存。即使用 A100 80GB,一块也装不下完整的 7B 模型训练状态。13B 模型需要 ~200 GB,70B 模型需要 ~1 TB。

结论:大模型训练必须把模型/数据分散到多块 GPU 上。 这就引出了分布式训练的核心问题------如何切分,以及切分后 GPU 之间如何通信。


分布式训练的四种并行策略(全景)

在深入每一种之前,先看全局地图:

复制代码
                    ┌─────────────────────────────────┐
                    │         分布式训练策略            │
                    └───────────┬─────────────────────┘
            ┌───────────┬──────┴──────┬───────────┐
            ▼           ▼             ▼           ▼
        数据并行      张量并行       流水线并行    序列并行
        (DP/DDP)     (TP)          (PP)         (SP)
            │           │             │           │
     切分数据      切分模型层      切分模型层    切分序列长度
     复制模型      层内切分        层间切分      (长序列场景)
            │           │             │           │
     通信: AllReduce  通信: AllReduce  通信: P2P     通信: AllGather
     (梯度同步)    (前向/反向)    (激活值传递)   (序列维度)
            │           │             │           │
     适用: 模型     适用: 单层      适用: 模型    适用: 超长
     能放进单卡    太大需要切分    层数太多       序列(128K+)

这四种策略不是互斥的,实际大模型训练通常同时使用多种------这就是"3D 并行"(DP + TP + PP),后续课程会详细讲。


通信原语:分布式训练的基石

不管用哪种并行策略,GPU 之间都需要通信。这些通信操作叫集合通信原语(Collective Communication Primitives),由 NCCL 库实现。

1. Broadcast(广播)

一个 GPU 把数据发给所有其他 GPU。

复制代码
GPU 0: [data]  ──→  GPU 1: [data]
             ├──→  GPU 2: [data]
             └──→  GPU 3: [data]

用途:模型初始化时,GPU 0 的权重广播到所有 GPU

2. Reduce(归约)

所有 GPU 的数据聚合到一个 GPU 上。

复制代码
GPU 0: [a0]  ─┐
GPU 1: [a1]  ─┼──sum──→  GPU 0: [a0+a1+a2+a3]
GPU 2: [a2]  ─┤
GPU 3: [a3]  ─┘

用途:把各 GPU 的局部梯度汇总到一个 GPU

3. AllReduce(全归约)★ 最重要

所有 GPU 的数据聚合后,每个 GPU 都拿到完整结果。等价于 Reduce + Broadcast。

复制代码
GPU 0: [a0]  ─┐          GPU 0: [a0+a1+a2+a3]
GPU 1: [a1]  ─┼──sum──→  GPU 1: [a0+a1+a2+a3]
GPU 2: [a2]  ─┤          GPU 2: [a0+a1+a2+a3]
GPU 3: [a3]  ─┘          GPU 3: [a0+a1+a2+a3]

用途:DDP 中同步所有 GPU 的梯度(数据并行的核心操作)

4. AllGather(全收集)

每个 GPU 持有一部分数据,收集后每个 GPU 都拿到完整数据

复制代码
GPU 0: [a0]  ─┐          GPU 0: [a0, a1, a2, a3]
GPU 1: [a1]  ─┼──cat──→  GPU 1: [a0, a1, a2, a3]
GPU 2: [a2]  ─┤          GPU 2: [a0, a1, a2, a3]
GPU 3: [a3]  ─┘          GPU 3: [a0, a1, a2, a3]

用途:ZeRO-3 中临时收集完整参数用于前向计算

5. ReduceScatter(归约分散)

先做 Reduce,再把结果的不同部分分散到不同 GPU。等价于 Reduce + Scatter。

复制代码
GPU 0: [a0, b0, c0, d0]  ─┐          GPU 0: [a0+a1+a2+a3]
GPU 1: [a1, b1, c1, d1]  ─┼──sum──→  GPU 1: [b0+b1+b2+b3]
GPU 2: [a2, b2, c2, d2]  ─┤  +split  GPU 2: [c0+c1+c2+c3]
GPU 3: [a3, b3, c3, d3]  ─┘          GPU 3: [d0+d1+d2+d3]

用途:ZeRO 中分片梯度的核心操作

6. Scatter(分散)

一个 GPU 把数据的不同部分发给不同 GPU。

复制代码
GPU 0: [a0,a1,a2,a3]  ──→  GPU 0: [a0]
                      ├──→  GPU 1: [a1]
                      ├──→  GPU 2: [a2]
                      └──→  GPU 3: [a3]

用途:分发数据/参数片段

7. AllToAll(全交换)

每个 GPU 给每个其他 GPU 发送不同的数据块。最复杂的通信模式。

复制代码
GPU 0: [→0, →1, →2, →3]     GPU 0: [←0, ←0, ←0, ←0]
GPU 1: [→0, →1, →2, →3]  →  GPU 1: [←1, ←1, ←1, ←1]
GPU 2: [→0, →1, →2, →3]     GPU 2: [←2, ←2, ←2, ←2]
GPU 3: [→0, →1, →2, →3]     GPU 3: [←3, ←3, ←3, ←3]

用途:MoE(Mixture of Experts)中 token 路由到不同 expert

通信成本:不同原语的带宽消耗

这是理解分布式训练性能的关键。假设 N 个 GPU,每个 GPU 发送 M 字节数据:

原语 通信量(理论下界) 说明
Broadcast M × (N-1) 1 对 N-1
Reduce M × (N-1) N-1 对 1
AllReduce 2M × (N-1) Ring 算法:ReduceScatter + AllGather
AllGather M × (N-1) 每个 GPU 发 M 字节
ReduceScatter M × (N-1) 每个 GPU 发 M 字节
AllToAll M × N × (N-1)/N = M × (N-1) 全交换

AllReduce 的通信量最大(2M × (N-1)),而它恰恰是数据并行(DDP)的核心操作。这就是为什么数据并行在 GPU 数量增多时通信开销急剧增加。

Ring AllReduce 算法

实际中 AllReduce 不会让一个 GPU 收集所有数据再广播(那样这个 GPU 会成为瓶颈)。而是用 Ring 算法

复制代码
4 个 GPU 组成一个环:GPU 0 → GPU 1 → GPU 2 → GPU 3 → GPU 0

把数据切成 4 块: [d0, d1, d2, d3]

Phase 1: ReduceScatter(N-1 = 3 步)
  第 1 步: 每个 GPU 把自己的 d_i 发给下一个 GPU,同时接收前一个的 d_j,做 reduce
  第 2 步: 把 reduce 后的结果继续传递
  第 3 步: 每个 GPU 最终持有 1/4 的完整 reduce 结果

Phase 2: AllGather(N-1 = 3 步)
  把 Phase 1 的结果沿着环传递,每个 GPU 收集完整的 reduce 结果

总通信量: 2 × M × (N-1)/N ≈ 2M(N 很大时趋近 2M,与 GPU 数量无关!)

这就是 NCCL 实现的高效 AllReduce。注意通信量在 N 很大时趋近 2M,不随 GPU 数量线性增长------这是 Ring AllReduce 的核心优势。


NCCL:NVIDIA 集合通信库

NCCL(NVIDIA Collective Communications Library)是上述所有通信原语的底层实现。它会自动探测 GPU 之间的互联拓扑,选择最优的通信算法。

复制代码
应用层:    PyTorch Distributed / DeepSpeed / Megatron-LM
              │
              ▼
通信层:    NCCL (AllReduce, AllGather, ReduceScatter...)
              │
              ▼
硬件层:    NVLink (机内 GPU 互联, ~900 GB/s)
           InfiniBand / RoCE (机间互联, ~400 Gbps)
           PCIe (最慢, ~64 GB/s)

不同互联的带宽对比

互联方式 单向带宽 适用场景
NVLink(H100) 900 GB/s 同一节点内 GPU 通信(最快)
InfiniBand HDR 200 Gb/s (~25 GB/s) 跨节点通信
InfiniBand NDR 400 Gb/s (~50 GB/s) 跨节点通信(新一代)
PCIe Gen5 ~64 GB/s 没有 NVLink 时的备选

关键洞察 :机内通信(NVLink)比机间通信(InfiniBand)快 18-36 倍。这就是为什么 Tensor Parallelism(通信量最大的并行策略)通常只在同一台机器内使用,而 Data Parallelism 可以跨机器。


PyTorch Distributed 基本使用

在进入具体并行策略之前,先了解 PyTorch 分布式的基础 API:

python 复制代码
import torch
import torch.distributed as dist

# 1. 初始化分布式环境
dist.init_process_group(backend='nccl')  # 使用 NCCL 后端

# 2. 获取当前 GPU 信息
rank = dist.get_rank()           # 当前 GPU 编号 (0, 1, 2, ...)
world_size = dist.get_world_size()  # 总 GPU 数量
local_rank = int(os.environ['LOCAL_RANK'])  # 本节点内的 GPU 编号

# 3. 设置当前设备
torch.cuda.set_device(local_rank)

# 4. 使用通信原语
tensor = torch.ones(1000).cuda()
dist.all_reduce(tensor, op=dist.ReduceOp.SUM)  # AllReduce
# 现在每个 GPU 上的 tensor 都是所有 GPU 的 sum

# 5. 清理
dist.destroy_process_group()

启动方式(torchrun,替代旧的 torch.distributed.launch):

bash 复制代码
# 单机 4 卡
torchrun --nproc_per_node=4 train.py

# 多机(2 台机器,每台 4 卡,共 8 卡)
# 在 node 0 上:
torchrun --nproc_per_node=4 --nnodes=2 --node_rank=0 \
  --master_addr=10.0.0.1 --master_port=29500 train.py
# 在 node 1 上:
torchrun --nproc_per_node=4 --nnodes=2 --node_rank=1 \
  --master_addr=10.0.0.1 --master_port=29500 train.py

本课小结

概念 要点
为什么需要分布式 单卡显存装不下模型参数 + 优化器状态 + 梯度
四种并行策略 DP(切分数据)、TP(层内切分)、PP(层间切分)、SP(序列切分)
核心通信原语 AllReduce(最常用)、AllGather、ReduceScatter
Ring AllReduce 通信量 ≈ 2M,不随 GPU 数量线性增长
通信带宽 NVLink (~900 GB/s) >> InfiniBand (~50 GB/s) >> PCIe (~64 GB/s)
NCCL 集合通信库,自动选择最优通信算法

自检

  1. AllReduce 和 Reduce + Broadcast 的区别是什么?(答:结果一样,但 Ring AllReduce 的通信量是 2M,而先 Reduce 到 1 个 GPU 再 Broadcast 需要 M(N-1) + M(N-1) = 2M(N-1),Ring 算法更优)
  2. 为什么 Tensor Parallelism 通常只在机内使用?(答:TP 每层前向/反向都需要 AllReduce,通信量巨大。NVLink 带宽 900 GB/s 远大于 InfiniBand 50 GB/s,跨机会成为瓶颈)
  3. 训练 7B FP16 模型至少需要多少显存?(答:~122 GB,一块 A100 80GB 不够)