土法炼钢兴趣小组的算法知识备份

【大模型基础设施工程】06:3D 并行深度——数据 / 张量 / 流水 / 序列 / ZeRO

文章导航

分类入口
architectureai-infra
标签入口
#llm#infra#parallelism#data-parallel#tensor-parallel#pipeline#sequence-parallel#zero#fsdp#megatron#dualpipe

目录

单卡 80GB HBM 最多装 70B FP16 的参数(不含梯度、优化器、激活)。 要训 DeepSeek-V3 这样的 671B MoE,必须把模型切成几千片,让几千张 GPU 像一台机器一样工作。 切分的艺术,就是并行的艺术。

一、为什么单卡再也装不下

1.0 开胃菜:一张时间线

年份 事件 规模/突破
2017 Transformer 原论文 65M 参数
2018 BERT-Large 340M
2019 Megatron-LM 发布 8B,奠定 TP
2019 ZeRO-1/2/3 DeepSpeed,DP 极致省显存
2020 GPT-3 175B PTD-P 3D 并行
2022 Megatron-Turing NLG 530B、BLOOM 176B 多集群合训
2023 GPT-4(推测 MoE)、Llama 2 70B MoE 工程化
2024 DeepSeek-V3 671B MoE + DualPipe 零气泡 + FP8
2025 Llama 3 405B、Qwen 2.5、GLM-4.5 10T+ tokens 成常态

每一次规模跨越都伴随并行策略的一次补刀。

1.1 显存的四项开销

训练一个 Transformer,GPU 显存的消耗大体分为四块:

组成 大小(以参数 N 为单位) 说明
参数(Weights) 2N(FP16) 前向、反向都要用
梯度(Gradients) 2N(FP16) 反向累积
优化器状态(Optimizer) 12N(Adam FP32 m+v+master) 混合精度下的主拷贝
激活(Activations) 与 batch、seq、depth 成正比 反向 recompute 时需要

以 Adam 混合精度训 70B 为例:参数 + 梯度 + 优化器 ≈ 2+2+12 = 16 × 70B = 1120 GB,再加激活——单卡 80GB 连塞一角都不够。

1.2 算力的两堵墙

除了显存,还有两堵看不见的墙:

所以并行化的目标不是”把模型切开能装下”就完事,而是在切分带来的通信代价计算能完成的比例(MFU)之间找平衡。工业界 Pre-train 的 MFU 目标:密集模型 40%+,MoE 30%+,能稳到 50% 算极优。

二、并行的五个维度

2.0 先认清四种集合通信原语

后面所有讨论都离不开这四种 NCCL 原语,先一次性说清楚:

AllReduce(x_i)      → 每个 rank 拿到 Σ x_i         ;流量 ≈ 2(N-1)/N × |x|
ReduceScatter(x_i)  → 每个 rank 拿到 Σ x_i 的 1/P 片;流量 ≈ (N-1)/N × |x|
AllGather(s_i)      → 每个 rank 拿到全部 s_i 拼接   ;流量 ≈ (N-1)/N × |s|×P
All-to-All(x_i[j])  → rank i 的第 j 片发给 rank j  ;流量 ≈ (N-1)/N × |x|

恒等式AllReduce = ReduceScatter + AllGather,流量减半——这正是 ZeRO-2/3 与 SP 的魔法来源。

2.1 DP:数据并行(Data Parallel)

最朴素:每张卡完整持有一份模型副本,把 global batch 切成 N 份,每卡前向 / 反向各一份,反向后对梯度做 AllReduce

GPU0: model | batch[0:B/N]  --\
GPU1: model | batch[B/N:2B/N] --> AllReduce(grad) --> update
GPU2: model | batch[2B/N:..] --/

2.2 TP:张量并行(Tensor Parallel)

把单个大矩阵乘法切到多卡上,由 Megatron-LM 定型。切法两种:

Megatron 的 MLP(Linear → GELU → Linear)采用”列切 + 行切”配对:

X --[col-split Linear]--> Y(split) --GELU--> Z(split) --[row-split Linear]--> AllReduce --> Out

Attention 的 QKV Linear 是列切(切 head 维度),Output Linear 是行切。一层 Transformer 有 2 次 AllReduce(forward)+ 2 次 AllReduce(backward,与 SP 结合可以换成 ReduceScatter/AllGather)。

2.3 PP:流水线并行(Pipeline Parallel)

按层切,不同 stage 放到不同 GPU 上。前向像流水一样:GPU0 算完第 1-8 层,把激活送到 GPU1 算 9-16 层……反向反着走。

经典问题是”气泡(bubble)“——头尾阶段的 GPU 必然有空闲。后文 §五 专门讲。

通信:stage 之间只发送 / 接收激活与激活梯度,是 P2P send/recv,数据量远小于 DP 的 AllReduce,跨节点友好。

2.4 SP:序列并行(Sequence / Context Parallel)

当 seq_len 达到 32K、128K 乃至 1M,激活B × S × H)本身就爆显存,单 TP 切不动了。SP 沿 序列维度 切分:

实操上:Ulysses 在 TP 已经切了 head 的情况下不好叠加,Ring/CP 正交性更好;DeepSeek-V3 和训练 128K+ 上下文的主流是 CP + TP + PP + EP + DP。

2.5 EP:专家并行(Expert Parallel,MoE 专用)

MoE 每层有几十到几百个 expert,但 token 只路由到 top-k 个。EP 做的就是把 expert 分散到 N 张卡:

token routing -> all-to-all dispatch -> local experts -> all-to-all combine

五个维度的关系可以用一张图表达:

EP:专家并行(Expert Parallel,MoE 专用)

三、ZeRO 与 FSDP:DP 的进化形态

DP 的痛点是”每张卡都完整保存参数 / 梯度 / 优化器”,冗余极大。ZeRO(Zero Redundancy Optimizer,DeepSpeed 2019)把这三份状态切分到 DP 各卡上。

3.1 ZeRO 三阶段

阶段 切分内容 单卡状态显存 额外通信
ZeRO-1 优化器状态 2N + 2N + 12N/Ndp 无(只在优化器 step 时处理)
ZeRO-2 +梯度 2N + 2N/Ndp + 12N/Ndp ReduceScatter 替 AllReduce
ZeRO-3 +参数 (2N + 2N + 12N)/Ndp 前向 / 反向前 AllGather 参数

ZeRO-3 的通信量 ≈ 标准 DDP 的 1.5 倍(AllGather + ReduceScatter + AllGather 反向),但显存降到 1/Ndp

3.2 FSDP(Fully Sharded Data Parallel)

PyTorch 原生实现的 ZeRO-3:

import torch
from torch.distributed.fsdp import FullyShardedDataParallel as FSDP
from torch.distributed.fsdp import ShardingStrategy

model = FSDP(
    model,
    sharding_strategy=ShardingStrategy.FULL_SHARD,  # ZeRO-3
    mixed_precision=mp_policy,
    device_id=torch.cuda.current_device(),
)

关键细节:

3.3 ZeRO-Offload / CPU / NVMe

工业预训练一般不用 Offload,SFT / LoRA 经常用。

3.4 ZeRO-R:激活与临时 buffer

ZeRO 家族里还有常被忽视的一档:

这些在纯 DeepSpeed 栈里是 ZeRO-R;Megatron / FSDP 都内化实现了类似机制。

3.5 FSDP vs DeepSpeed 实战差异

维度 FSDP2 DeepSpeed ZeRO-3
代码集成 纯 PyTorch,改动小 需要 deepspeed engine 包一层
torch.compile 逐步完善 兼容有限
CPU/NVMe Offload 有限 成熟(Infinity)
MoE 支持 需配合 Expert Parallel 手写 集成 MoE + EP
Checkpoint DCP(Distributed Checkpoint) 内置切片 + 合并工具
生态 TorchTitan / HF Accelerate HF Trainer / colossal / DS-Chat

选型不是二选一:国内头部团队常”Megatron 主干 + DeepSpeed 的优化器/Offload + FSDP2 做 SFT/RL”混用。

四、3D 并行组合与通信量估算

单用任何一种都不够,工业训练都是 TP × PP × DP (× SP × EP) 的笛卡儿积。

4.1 经典配置

场景 规模 TP PP DP SP/CP EP
Megatron 175B(8×A100 node×多节点) 175B dense 8 8 128 1 -
Llama 3 70B(官方) 70B dense 8 4 - 1 -
DeepSeek-V3 Pre-train 671B MoE 1(无 TP) 16 16 - 64
长上下文 SFT(128K) 13B 2 2 32 8 (CP) -

DeepSeek-V3 的一个设计亮点:放弃 TP,用 EP + PP + ZeRO-1 组合,因为 MoE 的 FFN 已被 expert 天然切分,不再需要 TP 切 MLP;这样避免了 TP 带来的每层 AllReduce,换来更大的 PP 窗口。

4.2 Group 布局与通信域

TP=8, PP=4, DP=16 共 512 卡为例,NCCL 里会创建三类 ProcessGroup:

rank = tp_rank + tp_size * (dp_rank + dp_size * pp_rank)

TP group:同一 (pp_rank, dp_rank) 的 8 张卡 → 放 node 内 NVLink
PP group:同一 (tp_rank, dp_rank) 的 4 张卡 → 跨节点,但通信量小
DP group:同一 (tp_rank, pp_rank) 的 16 张卡 → 跨节点 AllReduce

Rail-optimized 拓扑ibN 网卡与 GPUn 一一配对,DP AllReduce 走自己的 rail,避免 cross-rail。这是 NCCL 2.18+ NCCL_CROSS_NIC=0 的默认行为。

4.3 通信量粗算(每 step)

记:a = batch × seq × hiddenL 层,N 参数。

维度 数据量 说明
DP AllReduce 2N(FP16) 一次
TP AllReduce 4L × a 每层 4 次(fwd 2 + bwd 2)
PP P2P 2L/PP × a stage 边界激活 + 梯度
SP(与 TP 合并) 4L × a(总量不变,换成 RS/AG) 峰值激活 ↓ TP 倍
EP all-to-all 2 × topk × a × (1−1/EP) MoE 层才算

经验规则:若 DP AllReduce 超过 20% 单步时间,考虑升 ZeRO 或砍 DP;若 TP AllReduce 跑出节点,直接掉 MFU 30%+。

4.4 Rank 布局的可视化

TP=4, PP=2, DP=2,共 16 GPU 为例:

                 PP stage 0              PP stage 1
            ┌──────────────┐       ┌──────────────┐
DP group 0  │ R0 R1 R2 R3  │──PP──▶│ R8 R9 R10 R11│
            │ ◀── TP ────▶ │       │ ◀── TP ────▶ │
            └──────┬───────┘       └──────┬───────┘
                   │ DP AllReduce         │ DP AllReduce
            ┌──────┴───────┐       ┌──────┴───────┐
DP group 1  │ R4 R5 R6 R7  │──PP──▶│R12 R13 R14 R15│
            └──────────────┘       └──────────────┘

Megatron 按 tp × dp × pp 的顺序把 rank 扁平化,这也是”TP 放同 node”自然成立的原因——同一个 node 上的 8 个 rank 天然落入同一 TP 组。

五、流水线气泡:从 GPipe 到 DualPipe

5.1 气泡公式

设 PP 度 P,micro-batch 数 M

5.2 时序对比(Mermaid)

gantt
    title 1F1B vs Zero Bubble vs DualPipe(P=4,单位 = micro-batch 步)
    dateFormat  X
    axisFormat  %s

    section 1F1B
    GPU0 F1 :a1, 0, 1
    GPU0 F2 :a2, after a1, 1
    GPU0 F3 :a3, after a2, 1
    GPU0 F4 :a4, after a3, 1
    GPU0 B1 :b1, after a4, 1
    GPU0 B2 :b2, after b1, 1
    GPU0 B3 :b3, after b2, 1
    GPU0 B4 :b4, after b3, 1

    section ZeroBubble
    GPU0 F1 :c1, 0, 1
    GPU0 F2 :c2, after c1, 1
    GPU0 B1 :c3, after c2, 1
    GPU0 W1 :c4, after c3, 1
    GPU0 F3 :c5, after c4, 1
    GPU0 B2 :c6, after c5, 1

    section DualPipe
    GPU0 F1+ :d1, 0, 1
    GPU0 F2+B1- :d2, after d1, 1
    GPU0 F3+B2- :d3, after d2, 1
    GPU0 F4+B3- :d4, after d3, 1
    GPU0 B4- :d5, after d4, 1

5.3 Zero Bubble 关键:B / W 拆分

反向传播中:

W 往后推,中间空档让其他 micro-batch 的 F/B 填进来。

5.4 DualPipe 的双向 + 重叠

DualPipe 核心思想:

  1. 双向调度:把模型分成两份对称流水,一份从头到尾、一份从尾到头,两股数据流反向流动,共享 GPU 计算单元。
  2. 细粒度 chunk:将 attention / MLP / all-to-all dispatch / all-to-all combine 切为 4 块,按 ATTN → MLP → combine → dispatch 重排,让反向计算遮蔽前向 all-to-all
  3. 不需要 TP,天然适配 EP-heavy MoE。

代价:显存需要保存 2 份参数副本(因为两个方向同时在算),适合 MoE 这种参数相对 sparse 的结构;Dense 大模型用 DualPipe 不划算。

六、通信与计算 overlap

MFU 上 50% 的关键:让每一次集合通信都藏到 GEMM 后面。

6.1 NCCL stream 与 CUDA graph

# PyTorch 内部约定:通信走单独的 NCCL stream
comm_stream = torch.cuda.Stream()
with torch.cuda.stream(comm_stream):
    dist.all_reduce(grad, async_op=True)
# 计算继续在默认 stream

DDP 的 bucket 机制:梯度按桶(默认 25MB)触发 AllReduce,反向还没结束、前面桶的 AllReduce 已经在跑。

6.2 Chunk / micro-batch

6.3 DualPipe 的 all-to-all 重叠

MoE 的 all-to-all 最痛苦。DualPipe 把 dispatch / combine 拆成 4 个 warp group,与本 chunk 的反向 compute 用不同 SM:

Compute SMs:  | attn_bwd | mlp_bwd |
Comm SMs:     |          dispatch(next) / combine(prev) |

这是 DeepSeek-V3 能在 H800(NVLink 带宽被砍一半)上做到 MFU 40%+ 的秘密武器。

七、激活重计算与梯度累积

7.1 Activation Checkpointing

反向用到的中间激活占用极大(B × S × H × L × 12 级别)。checkpoint:前向时只存 block 入口激活,反向时重新跑一次前向。换显存与 1/3 额外算力。

7.2 梯度累积

for step, batch in enumerate(loader):
    loss = model(batch) / accum_steps
    loss.backward()                       # 累在 .grad 上
    if (step + 1) % accum_steps == 0:
        optimizer.step()
        optimizer.zero_grad()

梯度累积把有效 batch size 放大而不增显存,但会降低 PP / DP AllReduce 的 overlap 机会(因为最后一次累积后必须同步)。Megatron 的做法是把 global_batch = DP × micro_batch × accum,accum 等于 PP micro-batch 数。

7.3 激活显存粗算

一个 Transformer Block,FlashAttention 2/3 下保存的激活量大约是:

act ≈ B × S × H × (34 / TP)  bytes(BF16,含 norm / residual / proj 输出)

L 层全保留:L × B × S × H × 34 / TP。以 L=96, B=2, S=4096, H=12288, TP=8 估算: 96 × 2 × 4096 × 12288 × 34 / 8 ≈ 41 GB / micro-batch,1F1B 再 × P = 几百 GB,不开 recompute 铁定 OOM。

selective recompute 后约 10 / TP,激活减到 12GB/micro-batch,完全可控。

7.4 与 CPU/GPU 传输的激活 offload

对超长序列(256K+),就算 recompute 仍然很痛。可以把 block 前向输出 offload 到 CPU,反向时异步拷回:

act = act.to("cpu", non_blocking=True)        # 前向尾部
...                                           # 其他 block 继续算
act = act.to("cuda", non_blocking=True)       # 反向前预取

PyTorch 的 torch.utils.checkpoint + offload_to_cpu=True、Megatron 的 --cpu-offloading 自动处理。代价是 PCIe 带宽(64GB/s for Gen5 ×16)会成为新瓶颈,典型折算 MFU 降 5–10%。

八、负载均衡的工程痛点

8.1 MoE 路由抖动

MoE 训练最头疼:某些 expert 成”网红”,其他 expert 饥饿。解决:

8.2 PP 切分不均

每层 TFLOPS 不同:embedding、LayerNorm、FinalHead 比中间层轻。静态均分会让 stage0、stage_last 变慢成木桶短板。

8.3 动态 batch / seq

长短样本混批时,seq=4K 和 seq=128K 同 batch 等于短样本陪跑。做法:

8.4 PP 切分的动态规划伪码

给每一层一个 profile 耗时 t[i],要切成 P 段使 max(Σt) 最小:

def split_pp(t, P):
    L = len(t)
    # dp[i][p]: 前 i 层切 p 段的最小瓶颈耗时
    INF = float("inf")
    dp = [[INF] * (P + 1) for _ in range(L + 1)]
    cut = [[0] * (P + 1) for _ in range(L + 1)]
    dp[0][0] = 0
    prefix = [0]
    for x in t:
        prefix.append(prefix[-1] + x)
    for i in range(1, L + 1):
        for p in range(1, P + 1):
            for j in range(p - 1, i):
                seg = prefix[i] - prefix[j]
                val = max(dp[j][p - 1], seg)
                if val < dp[i][p]:
                    dp[i][p] = val
                    cut[i][p] = j
    # 回溯 cut 得到每 stage 的层区间
    return dp[L][P], cut

生产上(Megatron-Core PipelinePartitioner,Colossal-AI PipelineGPTPolicy)用这种思路自动均衡。

九、通信优化细节

9.1 FP8 AllReduce / FP8 训练

NVIDIA Transformer Engine(TE)把 GEMM 输入做动态 scaling 转到 FP8(E4M3 / E5M2),AllReduce 本身一般仍走 BF16 / FP16(Hopper 的 NCCL 现已支持 FP8 collective,但数值稳定性需谨慎)。

DeepSeek-V3 自研了一套 E4M3-only 的 FP8 混合精度,对 GEMM 使用 FP8,对累加 / 权重更新保持 FP32,在 H800 上把算力吃干。

9.2 NCCL 拓扑感知

Megatron 启动脚本常见:

export NCCL_IB_HCA=mlx5
export NCCL_IB_GID_INDEX=3
export NCCL_SOCKET_IFNAME=eth0
export NCCL_ALGO=Tree
export NCCL_CROSS_NIC=0
export NCCL_IB_SL=1

9.3 ZeRO Offload / CPU / NVMe

真显存不够时的兜底。一般组合:

十、实战栈

10.1 Megatron-LM(NVIDIA)

10.2 DeepSpeed(微软)

10.3 FSDP2 / HSDP(PyTorch 官方)

10.4 Colossal-AI / TorchTitan / veScale / MindSpeed

10.5 DualPipe(DeepSeek 2024 开源)

DeepSeek 在 v3 后开源了 DualPipe 调度器(github.com/deepseek-ai/DualPipe),以及 EPLB(Expert Parallelism Load Balancer)和 profile-data。对做 MoE 预训练的团队是一手材料。

十一、代码示例:PyTorch FSDP2 + TP 最小样例

下面给出一个可运行骨架(需要 PyTorch 2.4+,实际训练请结合 TorchTitan)。

import os
import torch
import torch.nn as nn
import torch.distributed as dist
from torch.distributed.device_mesh import init_device_mesh
from torch.distributed.tensor.parallel import (
    ColwiseParallel,
    RowwiseParallel,
    parallelize_module,
)
from torch.distributed._composable.fsdp import fully_shard


class MLP(nn.Module):
    def __init__(self, d):
        super().__init__()
        self.w1 = nn.Linear(d, 4 * d, bias=False)
        self.w2 = nn.Linear(4 * d, d, bias=False)

    def forward(self, x):
        return self.w2(torch.nn.functional.gelu(self.w1(x)))


class Block(nn.Module):
    def __init__(self, d):
        super().__init__()
        self.attn = nn.MultiheadAttention(d, 8, batch_first=True)
        self.mlp = MLP(d)
        self.n1 = nn.LayerNorm(d)
        self.n2 = nn.LayerNorm(d)

    def forward(self, x):
        x = x + self.attn(self.n1(x), self.n1(x), self.n1(x), need_weights=False)[0]
        x = x + self.mlp(self.n2(x))
        return x


def build_model(num_layers=12, d=1024):
    return nn.Sequential(*[Block(d) for _ in range(num_layers)])


def main():
    dist.init_process_group("nccl")
    world = dist.get_world_size()
    rank = dist.get_rank()
    torch.cuda.set_device(rank % torch.cuda.device_count())

    # 2D mesh:行是 DP(FSDP),列是 TP
    tp_size = 2
    dp_size = world // tp_size
    mesh = init_device_mesh(
        "cuda",
        mesh_shape=(dp_size, tp_size),
        mesh_dim_names=("dp", "tp"),
    )

    model = build_model().cuda()

    # === TP:把每个 Block 的 MLP 列切 + 行切 ===
    tp_mesh = mesh["tp"]
    for block in model:
        parallelize_module(
            block.mlp,
            tp_mesh,
            {
                "w1": ColwiseParallel(),
                "w2": RowwiseParallel(),
            },
        )

    # === FSDP2:按 Block 粒度做 ZeRO-3 切分 ===
    dp_mesh = mesh["dp"]
    for block in model:
        fully_shard(block, mesh=dp_mesh)
    fully_shard(model, mesh=dp_mesh)

    model = torch.compile(model)

    opt = torch.optim.AdamW(model.parameters(), lr=1e-4)
    x = torch.randn(4, 256, 1024, device="cuda")

    for step in range(10):
        loss = model(x).square().mean()
        loss.backward()
        opt.step()
        opt.zero_grad()
        if rank == 0:
            print(f"step {step} loss {loss.item():.4f}")


if __name__ == "__main__":
    main()

启动:

torchrun --nproc_per_node=8 fsdp_tp_demo.py

11.1 扩展到 PP

PyTorch 2.4+ 提供 torch.distributed.pipelining(PP),基本接口:

from torch.distributed.pipelining import pipeline, SplitPoint, ScheduleGPipe, Schedule1F1B

stage_mod = pipeline(model, mb_args=(x,),
                     split_spec={"layer4": SplitPoint.BEGINNING})
schedule = Schedule1F1B(stage_mod, n_microbatches=8, loss_fn=loss_fn)
schedule.step(x, target=y)

TorchTitan 里有 TP + PP + FSDP2 的完整 llama3 demo,可以直接参考。

11.2 Megatron-LM 启动片段

作为对比,Megatron 的启动参数:

torchrun --nproc_per_node=8 --nnodes=128 \
  pretrain_gpt.py \
  --tensor-model-parallel-size 8 \
  --pipeline-model-parallel-size 16 \
  --num-layers 96 --hidden-size 12288 --num-attention-heads 96 \
  --seq-length 2048 --max-position-embeddings 2048 \
  --micro-batch-size 2 --global-batch-size 1024 \
  --sequence-parallel \
  --use-distributed-optimizer \
  --num-layers-per-virtual-pipeline-stage 4 \
  --recompute-activations --recompute-granularity selective \
  --bf16 --use-flash-attn \
  --transformer-impl transformer_engine \
  --fp8-format hybrid --fp8-amax-history-len 1024 --fp8-amax-compute-algo max

逐项意义:

11.3 DeepSpeed 配置片段

{
  "train_micro_batch_size_per_gpu": 2,
  "gradient_accumulation_steps": 16,
  "zero_optimization": {
    "stage": 3,
    "overlap_comm": true,
    "contiguous_gradients": true,
    "reduce_bucket_size": 5e8,
    "stage3_prefetch_bucket_size": 5e8,
    "stage3_param_persistence_threshold": 1e6,
    "offload_optimizer": {"device": "cpu", "pin_memory": true}
  },
  "bf16": {"enabled": true},
  "activation_checkpointing": {
    "partition_activations": true,
    "contiguous_memory_optimization": true
  }
}

十二、工程经验:选型与调参

12.1 决策树:到底用哪种并行?

模型能塞进单卡(含优化器)?
├─ 是 → DDP / FSDP1 即可
└─ 否 → 看参数规模
         ├─ < 20B:FSDP2(或 ZeRO-3) + Activation Ckpt,单节点搞定
         ├─ 20B–200B dense:TP(node 内 ≤ 8) + PP(跨节点) + DP + SP
         ├─ > 200B dense:TP + PP + DP + SP + Interleaved 或 Zero Bubble
         └─ MoE:EP(node 内或 node-limited) + PP + DP (+ 小 TP 或 无 TP) + DualPipe

并结合序列长度:

seq ≤ 8K    : 不需要 SP
seq 8K–32K  : Megatron-SP
seq ≥ 64K   : Context Parallel / Ring Attention
seq ≥ 256K  : CP + Activation Offload + FlashAttention

12.2 Batch size 怎么定

12.3 MFU 目标

场景 MFU 参考线
A100/H100 dense pretrain 45–55%
H800 dense(砍 NVLink) 35–45%
H800 MoE DualPipe 35–42%
长上下文(CP > 1) 30–40%
国产 910B / MX 25–40%(和算子库成熟度强相关)

MFU 30% 以下,先查:TP 是否跨节点;PP 气泡是否过大;ZeRO-3 是否 AllGather 打爆 IB;FlashAttention 版本;FP8 是否真开。

12.4 常见翻车

  1. TP 跨节点:立刻 MFU 腰斩。规矩是 TP ≤ 节点 GPU 数。
  2. PP micro-batch 太小:气泡吃掉一切。M ≥ 4P 是最低线。
  3. Activation Ckpt 开太猛:浪费 30% 算力。用 selective + FlashAttention 后,full recompute 通常可以关掉。
  4. DP 维度巨大:几千路 DP 做 AllReduce 跨机网络会饱和,用 HSDP 分层 reduce。
  5. MoE expert 不够大 / EP 太粗experts_per_rank = 1 时 all-to-all 代价 > 计算;建议 2–4。
  6. CP + Ulysses 混用:路由和 head 维度冲突,debug 极难,选一个就行。
  7. 混合精度 NaN:FP8 scaling 窗口设太长 / loss scaler 没跟上。Transformer Engine 的 fp8_autocast 默认窗口 1 常常够用。

十三、深入:一个 175B 训练的完整账本

把上面所有维度放到一次真实训练上算一笔账。假设:

13.1 切分配置

TP=8, PP=16, DP=8

单卡参数:175B / (TP × PP) = 175B / 128 ≈ 1.37B,FP16 权重 2.74GB,加梯度 / 优化器:2.74 + 2.74 + 16.4 ≈ 22GB

激活(全存):L/PP × B × S × H × 12 bytes ≈ 6 × 2 × 2048 × 12288 × 12 ≈ 3.6GB / micro-batch(FlashAttention 下)。若 M=32 micro-batch,1F1B 峰值 P × 3.6GB ≈ 58GB——加权重已 80GB,必须开 selective recompute

13.2 通信预算

单步数据流:

13.3 时序估算

单 micro-batch 纯前向计算时间约 6 × N × B × S / (FLOPS × MFU) = 6 × 175e9 × 2 × 2048 / (989e12 × 0.5) ≈ 8.7s / step(全模型)。分到 PP 每 stage ≈ 540ms;B 与 F 比 ≈ 2:1,所以 1F1B 一个 micro-batch 单 stage 耗时约 1.6s。

气泡 (P-1)/M = 15/32 ≈ 47%,太高!方案:

生产上 Megatron 默认 Interleaved,DeepSeek V3 用 DualPipe。

13.4 成本曲线

1024 × H100 × 一年 ≈ $15–25M(含电 + 折旧),所以每 1% MFU 提升 = 每年省 15–25 万美金。这就是为什么所有大厂要自研调度器、死磕 FP8 和零气泡——算力涨 1% 都是真金白银。

十四、国产与异构

14.1 华为昇腾(910B/910C)

14.2 寒武纪、天数、沐曦、摩尔线程

14.3 AMD MI300X / MI325X

14.4 TPU v5p / Trillium

十五、调试与性能分析

15.1 工具

15.2 一个典型 MFU 排查流程

  1. tokens/sec/GPU,算 MFU。低于目标 5% 以上就要查。
  2. nsys 看一个 step:
    • GEMM 占比 < 50%?→ 算子 / 精度问题(没开 FP8、没用 FlashAttention)。
    • 通信 kernel 与 GEMM 同时在跑?→ overlap OK。否则看是不是 async_op=False、或 bucket 太小。
    • PP 气泡是否如预期?→ M 调大或换调度。
  3. 单独跑单卡 benchmark,对比理论峰值(989 TFLOPS × 0.7 为单 GEMM 经验线)。
  4. 把 TP 调成 1(只 DP + PP)看 MFU,隔离 TP 跨节点问题。

15.3 数值稳定性坑

15.4 真实世界的”诡异故障”案例

工业训练的大部分时间都在和一些”看不见的坑”做斗争。下面几例来自公开的故障复盘(OpenAI、Meta、DeepSeek、字节、百度等技术博客):

  1. 静默数据损坏(SDC):某 GPU 的 SM 偶发算错一个 bit,loss 缓慢偏离。定位靠 replay 两个不同 rank 的同批次数据比对。解决:周期性 checksum;故障卡打标,调度绕开。
  2. HBM ECC 错误累积:BF16 训到后期突然 NaN,日志查到 ECC 纠正计数暴增。换卡 + 重 load checkpoint。
  3. NCCL 死锁:某个 rank 因为磁盘满/dataset 读超时,集合通信全链卡住。方案:全 rank watchdog + 超时自动 dump stack。
  4. 慢节点(straggler):一张卡因散热问题降频 20%,PP/DP 全阻塞在 AllReduce。方案:周期性 ping 各 rank 的 step time,异常值剔除。
  5. NVLink 一条挂了:节点内 8 卡变 7 卡有效,TP 全锅。nvidia-smi nvlink -s 自动巡检。
  6. IB 抖动:丢包导致 NCCL 超时。方案:NCCL_IB_TIMEOUT/NCCL_IB_RETRY_CNT 调大 + 网络侧 BER 监控。
  7. MoE 路由爆炸:某 step top-1 命中 > 80% 集中到单 expert,capacity overflow 丢 token,loss 直接跳 10×。方案:auxiliary-free balancing + bias EMA。
  8. Checkpoint 写崩torch.save 单机写 1TB 权重写到一半掉线。方案:分片并行写 + 原子 rename + 异步(见第 10 篇)。

这些坑没有哪一条能靠”看论文”避开,全是血泪。一套成熟训练系统的差异就体现在这里。

十六、FAQ:十个被频繁问到的问题

Q1:既然 ZeRO-3 能省到 1/N 显存,为什么还要 TP/PP? ZeRO-3 的代价是前 / 反向前要 AllGather 完整权重,跨节点带宽一被打爆,MFU 就崩。TP/PP 切的是”永久切”,权重常驻本地,通信量反而小。超 50B 后纯 ZeRO-3 不现实。

Q2:FSDP2 和 Megatron-LM 二选一怎么选? Python 代码可读性、快速试错、与 HF 生态打通 → FSDP2 / TorchTitan。 追求极限 MFU、成熟 3D 并行、FP8、稳定支持 671B 级 → Megatron-Core。

Q3:MoE 非得用 EP 吗? 小 MoE(< 8 expert)可以用 TP 切。但 expert 多起来(DeepSeek 256 个),TP 切不动,必须 EP。EP + all-to-all 的瓶颈正是当下的主战场。

Q4:PP 的 micro-batch 越多越好? 一定范围内是的。M ≥ 4P 气泡才可接受。但 M 太大会让激活存不下(1F1B 下激活 = P 份,不受 M 影响;GPipe 下 = M 份,会爆)。

Q5:ZeRO-Offload 到 CPU 真的能训 70B 吗? 能,但吞吐常是纯 GPU 方案的 1/5–1/3。更适合 SFT / LoRA / 消融实验。

Q6:TP 为什么不能跨节点? TP 每层 AllReduce 几百 MB,一步几十次。NVLink 900GB/s 和 IB 50GB/s 差一个数量级,跨节点 TP 的通信时间会盖住 GEMM。

Q7:DualPipe 是银弹吗? 不是。显存要多一份权重副本,Dense 大模型不划算;对 MoE 的 all-to-all 重叠收益大。对上下文极长(激活巨大)场景也未必适合。

Q8:Sequence Parallel 和 Context Parallel 有什么差别? Megatron 术语里,SP 特指与 TP 合体的版本(切非线性算子的 seq 维,通信量不变)。CP(Context Parallel)切真正的 attention 部分 seq 维,跟 Ring Attention 同类。两者通常合起来用。

Q9:FlashAttention 与并行怎么搭? FlashAttention 本身不引入额外通信,与 TP/PP/DP 正交;与 CP 搭配时用 Flash-Attn 的 ring 变体(FA3 已内置)。

Q10:万卡训练什么时候必须上? 当 training tokens > 10T、model > 300B、或你想 3–4 周内出 checkpoint。小于这个规模,千卡 + 更好的工程更划算。

十七、一页速查表

维度 通信 op 通信量级 合适的域
DP / DDP AllReduce O(N) 任意(跨节点)
ZeRO-1 AllReduce grad O(N) 任意
ZeRO-2 ReduceScatter + … O(N) 任意
ZeRO-3 / FSDP AllGather + RS 1.5×O(N) 节点内优先,跨节点用 HSDP
TP(Megatron) AllReduce O(a) × 层 节点内
TP + SP RS + AG 同上 节点内
PP P2P send/recv O(a) / stage 跨节点
Context Parallel Ring send/recv O(a) × ring 节点内或专属 ring
EP All-to-All × 2 O(a × topk) node-limited

通用口诀

十八、小结

并行不是把模型切开就完事,真正的工程在于:谁切、切到哪个通信域、与什么计算 overlap、在哪个阶段同步

下一篇我们拆解最具代表性的两套实现:Megatron-LM 与 DeepSpeed

参考资料


上一篇训练全景:Pre-train、SFT、RLHF、DPO、蒸馏 下一篇Megatron-LM 与 DeepSpeed

同主题继续阅读

把当前热点继续串成多页阅读,而不是停在单篇消费。


By .