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