5.1 分布式训练范式:数据并行、张量并行与流水线并行 单张 A100 80GB 装不下 70B 模型(仅权重就要 140GB),更不用说训练时的优化器状态与激活值。当模型大到一张卡装不下时,「怎么把模型拆到多张卡上」就成了大模型训练的核心问题——三大并行范式给出了三种截然不同的拆法。 5.1.1 单卡的极限:为什么必须分布式 先算一笔账。训练一个 70B 参数的模型(用 Adam 优化器 + FP16 混合精度): 显存项 | 计算 | 大小 模型权重(FP16) | 70B × 2 字节 | 140 GB 梯度(FP16) | 70B × 2 字节 | 140 GB 优化器状态(Adam FP32 + 动量 + 方差) | 70B × 12 字节 | 840 GB 激活值(按
单张 A100 80GB 装不下 70B 模型(仅权重就要 140GB),更不用说训练时的优化器状态与激活值。当模型大到一张卡装不下时,「怎么把模型拆到多张卡上」就成了大模型训练的核心问题——三大并行范式给出了三种截然不同的拆法。
先算一笔账。训练一个 70B 参数的模型(用 Adam 优化器 + FP16 混合精度):
| 显存项 | 计算 | 大小 |
|---|---|---|
| 模型权重(FP16) | 70B × 2 字节 | 140 GB |
| 梯度(FP16) | 70B × 2 字节 | 140 GB |
| 优化器状态(Adam FP32 + 动量 + 方差) | 70B × 12 字节 | 840 GB |
| 激活值(按 batch 与序列长度) | 数十到数百 GB | 数十-数百 GB |
| 合计 | > 1 TB |
单张 A100 80GB 远远不够,单张 H100 80GB 也不够。即使不考虑训练(只做推理),70B 模型权重 140GB 也装不进单卡。这就是为什么大模型必须分布式——不是为了快,而是为了装得下。
分布式训练的核心问题是「怎么拆」。三大并行范式从不同维度拆:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 380" font-family="sans-serif" font-size="12"> <text x="450" y="22" text-anchor="middle" font-size="15" font-weight="bold">三大并行范式对比</text> <!-- 数据并行 --> <rect x="20" y="50" width="280" height="200" rx="8" fill="#dbeafe" stroke="#2563eb"/> <text x="160" y="75" text-anchor="middle" font-weight="bold" fill="#1e40af">数据并行 DP</text> <text x="160" y="95" text-anchor="middle" font-size="11">每卡:完整模型 + 部分数据</text> <g font-size="10"> <rect x="40" y="110" width="240" height="35" rx="4" fill="#fff" stroke="#2563eb"/> <text x="160" y="132" text-anchor="middle">GPU 0:模型副本 + Batch 1</text> <rect x="40" y="150" width="240" height="35" rx="4" fill="#fff" stroke="#2563eb"/> <text x="160" y="172" text-anchor="middle">GPU 1:模型副本 + Batch 2</text> <rect x="40" y="190" width="240" height="35" rx="4" fill="#fff" stroke="#2563eb"/> <text x="160" y="212" text-anchor="middle">GPU 2:模型副本 + Batch 3</text> </g> <text x="160" y="240" text-anchor="middle" font-size="10" fill="#16a34a">优势:简单;限制:模型要装得下单卡</text> <!-- 张量并行 --> <rect x="310" y="50" width="280" height="200" rx="8" fill="#fce7f3" stroke="#db2777"/> <text x="450" y="75" text-anchor="middle" font-weight="bold" fill="#9d174d">张量并行 TP</text> <text x="450" y="95" text-anchor="middle" font-size="11">每卡:每层的一部分 + 完整数据</text> <g font-size="10"> <rect x="330" y="110" width="240" height="35" rx="4" fill="#fff" stroke="#db2777"/> <text x="450" y="132" text-anchor="middle">GPU 0:Layer1 左半 + Layer2 左半</text> <rect x="330" y="150" width="240" height="35" rx="4" fill="#fff" stroke="#db2777"/> <text x="450" y="172" text-anchor="middle">GPU 1:Layer1 右半 + Layer2 右半</text> </g> <text x="450" y="200" text-anchor="middle" font-size="10" fill="#475569">每层计算时分两半,再 AllReduce 合并</text> <text x="450" y="225" text-anchor="middle" font-size="10" fill="#dc2626">要求:高带宽 NVLink,跨节点极慢</text> <text x="450" y="245" text-anchor="middle" font-size="10" fill="#16a34a">优势:装得下单层装不下的模型</text> <!-- 流水线并行 --> <rect x="600" y="50" width="280" height="200" rx="8" fill="#dcfce7" stroke="#16a34a"/> <text x="740" y="75" text-anchor="middle" font-weight="bold" fill="#15803d">流水线并行 PP</text> <text x="740" y="95" text-anchor="middle" font-size="11">每卡:连续的几层 + 部分数据</text> <g font-size="10"> <rect x="620" y="110" width="240" height="35" rx="4" fill="#fff" stroke="#16a34a"/> <text x="740" y="132" text-anchor="middle">GPU 0:Layer 1-10</text> <rect x="620" y="150" width="240" height="35" rx="4" fill="#fff" stroke="#16a34a"/> <text x="740" y="172" text-anchor="middle">GPU 1:Layer 11-20</text> <rect x="620" y="190" width="240" height="35" rx="4" fill="#fff" stroke="#16a34a"/> <text x="740" y="212" text-anchor="middle">GPU 2:Layer 21-30</text> </g> <text x="740" y="240" text-anchor="middle" font-size="10" fill="#16a34a">优势:跨节点友好;限制:流水线气泡</text> <!-- 3D 并行 --> <rect x="20" y="280" width="860" height="80" rx="8" fill="#fef9c3" stroke="#ca8a04"/> <text x="450" y="305" text-anchor="middle" font-weight="bold" fill="#854d0e">3D 并行:实际大模型训练同时用三者</text> <text x="450" y="328" text-anchor="middle" font-size="11">同节点内 8 卡做 TP(NVLink)→ 多节点做 PP(跨 IB)→ 节点组间做 DP(数据并行)</text> <text x="450" y="350" text-anchor="middle" font-size="11">例:405B 模型用 64 卡训练 → TP=8(每节点 8 卡)× PP=4(4 节点)× DP=2(2 组)</text> </svg>
数据并行(Data Parallelism, DP) 是最直观的并行方式:每张卡持有完整的模型副本,但处理不同的数据批次(batch)。前向后,各卡的梯度通过 AllReduce 聚合,保证所有卡的模型权重同步更新。
数据并行的流程:
数据并行的优势是实现简单、对模型结构无侵入,PyTorch 的 DDP(DistributedDataParallel)一行代码就能启用。它的限制是「模型要装得下单卡」——因为每张卡持有完整模型。所以纯数据并行只适合中小模型(如几 B 以下)。
💡 判读:数据并行的有效 batch size = 单卡 batch × GPU 数。当 GPU 数极大(如 1024 卡)时,全局 batch 可能大到影响训练收敛(BN 统计、学习率)。这是大模型训练用 LR Warmup、LARS/LAMB 等技巧的原因之一。
数据并行(以及后面的张量并行、ZeRO)都依赖 集合通信(Collective Communication) 原语。理解它们是理解分布式训练的基础。NVIDIA 的 NCCL(NVIDIA Collective Communications Library) 是事实标准实现。
AllReduce:所有节点把各自的数据做某种规约(如求和、求平均),结果分发给所有节点。最常用于数据并行中聚合梯度。
输入: GPU 0: [1, 2] GPU 1: [3, 4] GPU 2: [5, 6] AllReduce (SUM): 输出: GPU 0: [9, 12] GPU 1: [9, 12] GPU 2: [9, 12]
AllGather:每个节点收集所有节点的数据(不做规约),常用于 ZeRO-3 中收集完整参数。
输入: GPU 0: [1] GPU 1: [2] GPU 2: [3] AllGather: 输出: GPU 0: [1, 2, 3] GPU 1: [1, 2, 3] GPU 2: [1, 2, 3]
ReduceScatter:先 Reduce(规约)再 Scatter(分散),每个节点只得到规约结果的一部分,常用于 ZeRO 中分片梯度。
输入: GPU 0: [1, 2] GPU 1: [3, 4] GPU 2: [5, 6] ReduceScatter (SUM): 输出: GPU 0: [9] GPU 1: [12] GPU 2: [15] (注:实际按分片规则,这里示意)
| 原语 | 语义 | 通信量 | 典型用途 |
|---|---|---|---|
| AllReduce | 全规约 + 全分发 | O(N) × 数据量 | DP 聚合梯度 |
| AllGather | 全收集 | O(N) × 数据量 | ZeRO-3 收集参数 |
| ReduceScatter | 规约 + 分散 | O(N) × 数据量 / N | ZeRO 分片 |
| Broadcast | 一发多收 | 数据量 | 参数同步 |
朴素的 AllReduce 实现是「中心化」的——所有节点把数据发给一个 master 聚合再分发,通信量随节点数线性增长,master 成为瓶颈。
Ring AllReduce 把节点组织成环形,每个节点只与左右邻居通信,分两个阶段(Share-Reduce + AllGather)完成。其通信量与节点数几乎无关,只与数据量成正比,是分布式训练的事实标准算法。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 240" font-family="sans-serif" font-size="12"> <text x="360" y="22" text-anchor="middle" font-weight="bold">Ring AllReduce:环形通信</text> <!-- 环形节点 --> <circle cx="200" cy="120" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="200" y="125" text-anchor="middle">GPU 0</text> <circle cx="320" cy="60" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="320" y="65" text-anchor="middle">GPU 1</text> <circle cx="520" cy="60" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="520" y="65" text-anchor="middle">GPU 2</text> <circle cx="640" cy="120" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="640" y="125" text-anchor="middle">GPU 3</text> <circle cx="520" cy="180" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="520" y="185" text-anchor="middle">GPU 4</text> <circle cx="320" cy="180" r="28" fill="#dbeafe" stroke="#2563eb"/> <text x="320" y="185" text-anchor="middle">GPU 5</text> <!-- 环形连接箭头 --> <path d="M 220 105 L 300 75" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <path d="M 348 60 L 492 60" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <path d="M 540 75 L 620 105" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <path d="M 620 135 L 540 165" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <path d="M 492 180 L 348 180" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <path d="M 300 165 L 220 135" fill="none" stroke="#dc2626" stroke-width="2" marker-end="url(#arr6)"/> <text x="360" y="220" text-anchor="middle" font-size="11" fill="#475569">每个节点只与左右邻居通信 → 通信量与节点数无关</text> <defs> <marker id="arr6" markerWidth="10" markerHeight="10" refX="8" refY="3" orient="auto"> <path d="M0,0 L8,3 L0,6 z" fill="#dc2626"/> </marker> </defs> </svg>
Ring AllReduce 的通信量近似为 2 × (N-1)/N × 数据量,当 N 较大时趋近于 2 × 数据量,与节点数无关。这是为什么数据并行可以扩展到上千卡而通信不爆炸的根本原因。
张量并行(Tensor Parallelism, TP) 把模型的每一层(具体是矩阵乘法)拆到多张卡上。例如一个 Y = X × W 的矩阵乘法(X 是输入,W 是权重),可以按列拆 W:
W = [W_1 | W_2] (按列拆成两半) Y = X × W = [X × W_1 | X × W_2]
GPU 0 算 X × W_1,GPU 1 算 X × W_2,然后 AllReduce 合并。这样每张卡只需要一半的权重,且每层的计算并行化了。
张量并行的关键特点是:
Megatron-LM(NVIDIA 开源)是张量并行的代表实现,针对 Transformer 的注意力层与前馈层做了精细的切分。
⚠️ 关键约束:张量并行对网络极其敏感。一个 TP=8 的训练,如果 8 张卡在同一节点的 NVLink 组内,训练效率正常;如果分散到 2 个节点(每节点 4 卡),跨节点的张量并行通信会让训练慢 2-3 倍以上。这就是为什么张量并行度通常等于单节点 GPU 数(如 8),跨节点用流水线或数据并行。
流水线并行(Pipeline Parallelism, PP) 把模型的不同层分到不同卡。例如一个 30 层的模型,GPU 0 处理第 1-10 层,GPU 1 处理第 11-20 层,GPU 2 处理第 21-30 层。前向时数据从 GPU 0 流向 GPU 2,反向时梯度反向流动。
流水线并行的关键特点是:
流水线调度的核心是「怎么让所有 stage 都忙起来」。朴素流水线(先全部前向再全部反向)会有大量气泡。1F1B 调度让前向与反向交错,气泡最小化。更先进的 interleaved schedule 进一步减少气泡。
ZeRO(Zero Redundancy Optimizer) 是微软 DeepSpeed 提出的显存优化技术,它不是新的并行维度,而是消除数据并行中的显存冗余。
回顾数据并行:每张卡持有完整的(1)模型权重、(2)梯度、(3)优化器状态。这三者在 N 张卡上完全冗余复制了 N 份。ZeRO 的洞察是:这些不必都冗余,可以分片。
| ZeRO 阶段 | 切分对象 | 通信开销 | 显存节省 |
|---|---|---|---|
| ZeRO-1 | 优化器状态 | 与 DP 相同 | ~4x |
| ZeRO-2 | 优化器状态 + 梯度 | 与 DP 相同 | ~8x |
| ZeRO-3 | 优化器状态 + 梯度 + 权重 | 增加(需 AllGather) | ~N 倍 |
具体而言:
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 880 240" font-family="sans-serif" font-size="12"> <text x="440" y="22" text-anchor="middle" font-weight="bold">ZeRO 三阶段:逐步切分显存</text> <!-- DP 原始 --> <rect x="20" y="50" width="200" height="160" rx="6" fill="#fee2e2" stroke="#dc2626"/> <text x="120" y="75" text-anchor="middle" font-weight="bold" fill="#991b1b">朴素 DP</text> <text x="120" y="100" text-anchor="middle">每卡完整冗余</text> <text x="120" y="125" text-anchor="middle" font-size="11">权重 ✓</text> <text x="120" y="145" text-anchor="middle" font-size="11">梯度 ✓</text> <text x="120" y="165" text-anchor="middle" font-size="11">优化器 ✓</text> <text x="120" y="195" text-anchor="middle" font-size="10" fill="#dc2626">显存:1x</text> <!-- ZeRO-1 --> <rect x="240" y="50" width="200" height="160" rx="6" fill="#fef9c3" stroke="#ca8a04"/> <text x="340" y="75" text-anchor="middle" font-weight="bold" fill="#854d0e">ZeRO-1</text> <text x="340" y="100" text-anchor="middle">切分优化器状态</text> <text x="340" y="125" text-anchor="middle" font-size="11">权重 ✓</text> <text x="340" y="145" text-anchor="middle" font-size="11">梯度 ✓</text> <text x="340" y="165" text-anchor="middle" font-size="11" fill="#16a34a">优化器 ✗(分片)</text> <text x="340" y="195" text-anchor="middle" font-size="10" fill="#ca8a04">显存:~4x 节省</text> <!-- ZeRO-2 --> <rect x="460" y="50" width="200" height="160" rx="6" fill="#dbeafe" stroke="#2563eb"/> <text x="560" y="75" text-anchor="middle" font-weight="bold" fill="#1e40af">ZeRO-2</text> <text x="560" y="100" text-anchor="middle">+ 切分梯度</text> <text x="560" y="125" text-anchor="middle" font-size="11">权重 ✓</text> <text x="560" y="145" text-anchor="middle" font-size="11" fill="#16a34a">梯度 ✗(分片)</text> <text x="560" y="165" text-anchor="middle" font-size="11" fill="#16a34a">优化器 ✗(分片)</text> <text x="560" y="195" text-anchor="middle" font-size="10" fill="#2563eb">显存:~8x 节省</text> <!-- ZeRO-3 --> <rect x="680" y="50" width="180" height="160" rx="6" fill="#dcfce7" stroke="#16a34a"/> <text x="770" y="75" text-anchor="middle" font-weight="bold" fill="#15803d">ZeRO-3</text> <text x="770" y="100" text-anchor="middle">+ 切分权重</text> <text x="770" y="125" text-anchor="middle" font-size="11" fill="#16a34a">权重 ✗(分片)</text> <text x="770" y="145" text-anchor="middle" font-size="11" fill="#16a34a">梯度 ✗(分片)</text> <text x="770" y="165" text-anchor="middle" font-size="11" fill="#16a34a">优化器 ✗(分片)</text> <text x="770" y="195" text-anchor="middle" font-size="10" fill="#16a34a">显存:~Nx 节省</text> </svg>
💡 判读:ZeRO 的精髓是「用通信换显存」。ZeRO-1/2 几乎没有额外通信开销就大幅省显存,是性价比最高的选择;ZeRO-3 通信开销大但显存节省最极致,用于训练超大模型(如 100B+)。DeepSpeed 的 ZeRO 实现是开源大模型训练的事实标准。
实际大模型训练(GPT-3、Llama、PaLM 等)几乎都用 3D 并行——同时组合 DP + TP + PP。一个典型配方:
例如 64 卡训练:
加上 ZeRO-1/2 优化数据并行的显存,就能训 100B+ 的模型。
| 并行维度 | 典型取值 | 网络要求 | 作用 |
|---|---|---|---|
| TP | 单节点 GPU 数(如 8) | NVLink 必需 | 切分单层 |
| PP | 跨节点数 | IB/RoCE | 切分层段 |
| DP | 模型副本数 | 一般网络 | 数据并行 + ZeRO |
⚠️ 现实代价:3D 并行的调参极其复杂——TP/PP/DP 的组合、micro-batch 数量、ZeRO 阶段、激活重计算(activation checkpointing)等都要精调。一个糟糕的配置可能让训练慢 10 倍。这是为什么大模型训练团队都有专门的「系统工程师」负责调优。
两个最主流的大模型训练框架:
实际工业训练常用组合:
还有一个常用的显存优化技术:激活重计算(Activation Checkpointing / Gradient Checkpointing)。它的思想是:前向时不保存中间激活值(释放显存),反向需要时重新前向计算一遍。
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 720 160" font-family="sans-serif" font-size="12"> <text x="360" y="22" text-anchor="middle" font-weight="bold">激活重计算:用算力换显存</text> <rect x="20" y="50" width="320" height="90" rx="6" fill="#fee2e2" stroke="#dc2626"/> <text x="180" y="75" text-anchor="middle" font-weight="bold">前向(不存激活)</text> <text x="180" y="100" text-anchor="middle">释放中间激活</text> <text x="180" y="125" text-anchor="middle">显存:低</text> <rect x="380" y="50" width="320" height="90" rx="6" fill="#dbeafe" stroke="#2563eb"/> <text x="540" y="75" text-anchor="middle" font-weight="bold">反向(重算激活)</text> <text x="540" y="100" text-anchor="middle">重新前向计算激活</text> <text x="540" y="125" text-anchor="middle">算力:+30%</text> </svg>
激活重计算常与 ZeRO 结合使用——ZeRO 切权重/梯度/优化器,激活重计算切激活值,合起来覆盖训练显存的全部组成部分。
下一节《5.2 K8s 训练 Operator 与 Ray Train》将讲清如何在 K8s 上编排分布式训练。