分布式 Transformer 深度解析:从张量形状到通信拓扑

49 分钟阅读

分布式 Transformer 深度解析:从张量形状到通信拓扑

前情说明:技术深度 D3。起点是 ailzhang 的可视化博客——她把 DP/TP/SP/CP/EP/VP 六种并行在 Transformer 各 block 里的张量形状和通信原语画得非常清爽,站在单张 GPU 视角看 sharding 的 dance。推荐先读她那篇看图理解直觉,我这篇是补深度:

  1. 每种并行的 tensor 形状分步演变 + 通信量在哪一步产生——ASCII 图逐层展开
  2. 通信原语底层怎么走——Ring AllReduce、Tree、Halving-Doubling、AllToAll
  3. FSDP / ZeRO 1/2/3 / Pipeline Parallel 这些 ailzhang 明确留给下一篇的方案
  4. 从训练侧扩到推理侧——TP/EP/PP 在推理里的差异

目标读者:已经知道 Transformer 基本结构、想搞懂"为什么大家都这么分卡"的工程师。所有图都是 ASCII,配合等宽字体阅读。


一、先建立坐标系:一次 forward 到底有多贵

在讨论"为什么要切"之前,先算明白"不切能不能行"。

以 Llama 3 70B 做锚点:

符号 含义 Llama 3 70B
B Global batch size 512 sequences
S Sequence length 8192
D Hidden dim 8192
F FFN dim 28672
L Layers 80
H Attention heads 64 (GQA: 8 KV heads)
V Vocab 128k

1.1 显存账(单卡装不下)

参数(bf16)        : 70B × 2 bytes = 140 GB
优化器状态(Adam fp32): 70B × 12 bytes = 840 GB   (m + v + master weight)
梯度(bf16)        : 70B × 2 bytes = 140 GB
激活值(80 层)     : ≈ 1-2 TB              (无 checkpointing)
─────────────────────────────────────────
合计               : > 2 TB

单张 H100 才 80 GB。分布式不是"要不要",是"必须切,问题是怎么切"。

1.2 三种约束的取舍

分布式训练所有设计都在平衡三件事:

       ┌─────────────┐
       │    显存     │  装不下 → 切参数/激活
       │  (MEMORY)   │
       └──────┬──────┘
              │
              │ 三角平衡
    ┌─────────┼─────────┐
    │                   │
┌───▼────┐         ┌────▼───┐
│  算力  │◄───────►│ 通信   │
│(COMPUTE│         │(COMM)  │
│ / MFU) │         │        │
└────────┘         └────────┘
 跑得慢 → 分 batch  卡间等 → 拓扑分层

核心原则:通信量大的并行走机内 NVLink(900 GB/s),通信量小的走机间 InfiniBand(100-400 GB/s)。理解这一句话,就理解了并行拓扑设计的一半。


二、通信原语:底层到底怎么走

ailzhang 的文章反复出现 AllReduce、ReduceScatter、AllGather,但没讲底层。这决定了每个 collective 的实际带宽和延迟——是理解一切并行的地基。

2.1 四个基本 collective

假设 4 张卡(GPU 0/1/2/3),每张持有一个 tensor(大小 M bytes)。看这张对照图:

初始状态:
  GPU 0: [A]     GPU 1: [B]     GPU 2: [C]     GPU 3: [D]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AllReduce(每卡都要完整 reduce 结果):
  GPU 0: [A+B+C+D]  GPU 1: [A+B+C+D]  GPU 2: [A+B+C+D]  GPU 3: [A+B+C+D]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ReduceScatter(reduce 后每卡只拿一片):
  假设每个 tensor 已经切成 4 块,A = [a0,a1,a2,a3] 等等
  GPU 0: [a0+b0+c0+d0]
  GPU 1: [a1+b1+c1+d1]
  GPU 2: [a2+b2+c2+d2]
  GPU 3: [a3+b3+c3+d3]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AllGather(每卡有一片,聚成完整):
  假设 GPU i 持有分片 s_i
  GPU 0: [s0, s1, s2, s3]  <── 拿到所有
  GPU 1: [s0, s1, s2, s3]
  GPU 2: [s0, s1, s2, s3]
  GPU 3: [s0, s1, s2, s3]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AllToAll(每卡切成 N 份,第 i 份发给卡 i):
  假设 GPU i 有 [i→0, i→1, i→2, i→3](4 份要发给 4 张卡)
  GPU 0: [0→0]  ─┐
  GPU 1: [1→0]  ─┼──► GPU 0 收到 [0→0, 1→0, 2→0, 3→0]
  GPU 2: [2→0]  ─┤    GPU 1 收到 [0→1, 1→1, 2→1, 3→1]
  GPU 3: [3→0]  ─┘    GPU 2/3 类似

数学恒等式(背下来):

    AllReduce  ==  ReduceScatter + AllGather
    ────────       ─────────────    ──────────
    每卡完整sum    每卡一片sum      每卡集齐所有

这是 FSDP / ZeRO / SP 能省显存的核心:AllReduce 拆成两步后,中间那一步是"每卡只有一片"——这一片可以直接用于分布式优化器更新,不需要每卡都拿到完整梯度。ZeRO 的每一级都是在这条恒等式的不同"截断"处做文章。

2.2 Ring AllReduce:为什么带宽不受卡数影响

朴素 AllReduce:所有卡把数据发给一张主卡,主卡加起来广播回去。看图:

朴素 AllReduce(差):
    GPU 0  GPU 1  GPU 2  GPU 3         ┌── 主卡瓶颈 ──┐
      │      │      │      │            │             │
      │      ▼      ▼      ▼            ▼             ▼
      └────► GPU 0 ◄────────┘         接收 3M         发送 3M
             (求和)                    带宽爆炸        带宽爆炸
                │
                └──► 广播回 3 张卡

  卡数 N 增大 → 主卡带宽 O(N) 线性下降,64 卡就跪

Ring AllReduce(NVIDIA NCCL 默认算法)的优雅之处:

Step 0: 每卡把自己的 tensor 切成 N 份(N=4)
  GPU 0: [a0][a1][a2][a3]
  GPU 1: [b0][b1][b2][b3]
  GPU 2: [c0][c1][c2][c3]
  GPU 3: [d0][d1][d2][d3]

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 1: ReduceScatter(N-1=3 步)

Step 1:  每卡把"编号 == 自己编号 - 1"的块发给下一张卡
  GPU 0 [a0]───► GPU 1 (收到 a0 + 累加到本地 b0 变成 a0+b0)
  GPU 1 [b1]───► GPU 2 (收到 b1 + 累加到本地 c1 变成 b1+c1)
  GPU 2 [c2]───► GPU 3 (收到 c2 + 累加到本地 d2 变成 c2+d2)
  GPU 3 [d3]───► GPU 0 (收到 d3 + 累加到本地 a3 变成 d3+a3)

Step 2: 每卡把"上一步刚累加过的块"再传下去(滚雪球)
  GPU 0 [d3+a3]───► GPU 1 变成 d3+a3+b3
  GPU 1 [a0+b0]───► GPU 2 变成 a0+b0+c0
  GPU 2 [b1+c1]───► GPU 3 变成 b1+c1+d1
  GPU 3 [c2+d2]───► GPU 0 变成 c2+d2+a2

Step 3: 再传一次,每卡都持有一个完整 sum
  GPU 0: [_ , _ , a2+b2+c2+d2, _ ]  ← 完整 sum 的第 2 块
  GPU 1: [a0+b0+c0+d0, _ , _ , _ ]  ← 完整 sum 的第 0 块
  GPU 2: [_ , b1+c1+d1+a1, _ , _ ]  ← 完整 sum 的第 1 块
  GPU 3: [_ , _ , _ , d3+a3+b3+c3]  ← 完整 sum 的第 3 块

  ↑ ReduceScatter 完成!每卡持有一片完整 sum

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Phase 2: AllGather(再 N-1=3 步)

Step 4-6: 类似的环形传递,只不过这次不再累加,是把每卡的那片
         广播给其他卡。3 步后每卡都持有完整的 4 片。

总通信量:每卡收发 2(N-1)/N × M bytes ≈ 2M——和 N 无关!

关键洞察:
   卡数 N ↑         →  每卡收发量恒定 ≈ 2M
   卡数 N ↑         →  链路带宽利用率 ↑(所有链路都在工作)
   卡数 N ↑         →  步数 O(N),延迟 ↑

结论: Ring 适合"大消息 + 中等卡数",瓶颈从"带宽"转到"延迟"

局限:延迟 O(N),1000+ 卡时不再最优。这时切到 Tree 或 Halving-Doubling。

2.3 Tree AllReduce vs Halving-Doubling

Tree AllReduce(二叉树 reduce + broadcast):

  Reduce 阶段(log N 步):            Broadcast 阶段:

        [7]  root                      [7] root  (完整 sum)
        / \                             / \
      [3] [4]                        [3] [4]
      / \  / \                       / \  / \
    [1][2][3][4]                   [1][2][3][4]

  延迟: O(log N)  ← 卡多时更好
  带宽: M(一半 traffic)
  代价: 树的每一层链路利用率低(只有一半的卡在参与)
Halving-Doubling(NCCL 默认在大 N 场景):

Phase 1: 递归 Halving(log N 步)
  Step 1: 相邻两两配对,各交换一半数据并 reduce
    (0↔1) (2↔3) (4↔5) (6↔7)
    每对分别持有 "低一半 sum" 和 "高一半 sum"
  Step 2: 距离 2 的配对,再交换 sum 的一半
    (0↔2) (1↔3) (4↔6) (5↔7)
  Step 3: 距离 4 的配对
    ...

Phase 2: 递归 Doubling(log N 步)
  反过来 broadcast 分片

延迟: O(log N)  ← 和 Tree 一样好
带宽: 2M       ← 和 Ring 一样好
兼具两者优势,是"大 N + 大 msg"的最佳选择

面试锚点:Ring 适合大消息(GBs 级),Tree 适合小消息(KB 级)+ 大规模,Halving-Doubling 兼顾。NCCL 会根据 msg size × N 自动选,无需手动调参。

2.4 AllToAll:MoE 的通信之王

AllToAll 是 MoE 的核心 collective:每张卡把自己的数据切成 N 份,第 i 份发给第 i 张卡。看图:

初始状态(4 张卡,每张卡持有一段 token 序列,已经算好每个 token 要去哪个 expert):

  GPU 0: tokens [t0..t9]  路由决策: t0→E1, t1→E3, t2→E0, ...
  GPU 1: tokens [t10..t19] 路由决策: ...
  GPU 2: tokens [t20..t29] 路由决策: ...
  GPU 3: tokens [t30..t39] 路由决策: ...

Step 1: 每卡按目标 expert 做 permutation(本地重排 token)
  GPU 0: [→E0: t2,t5] [→E1: t0,t3] [→E2: t7,t9] [→E3: t1,t4,t6,t8]
  GPU 1: [→E0: t11]   [→E1: t14,t18] [→E2: t10,t15,t17] [→E3: t12,t19]
  GPU 2: [→E0: t23,t28] [→E1: t20] [→E2: t24,t27] [→E3: t21,t22,t25]
  GPU 3: [→E0: t32,t38] [→E1: t33] [→E2: t35] [→E3: t31,t34,t36]

Step 2: AllToAll 一次性交换(每对卡之间同时打开链路)
       ┌───────────────────────────────────────────┐
  GPU 0│──┬──►GPU 0[→E0]        (自己的第 0 段留下)│
       │  ├──►GPU 1[→E1]                           │
       │  ├──►GPU 2[→E2]                           │
       │  └──►GPU 3[→E3]                           │
       │                                            │
  GPU 1│──┬──►GPU 0[→E0]                           │
       │  ├──►GPU 1[→E1]                           │
       │  ├──►GPU 2[→E2]                           │
       │  └──►GPU 3[→E3]                           │
       │  ...                                       │
       │  N² 条链路同时活跃!                       │
       └───────────────────────────────────────────┘

Step 3: 交换完成后,每卡收到该卡上 expert 该处理的所有 token
  GPU 0(跑 E0): 收到 [t2,t5,t11,t23,t28,t32,t38]
  GPU 1(跑 E1): 收到 [t0,t3,t14,t18,t20,t33]
  GPU 2(跑 E2): 收到 [t7,t9,t10,t15,t17,t24,t27,t35]
  GPU 3(跑 E3): 收到 [t1,t4,t6,t8,t12,t19,t21,t22,t25,t31,t34,t36]

  ↑ 注意每卡收到的 token 数不同!这就是 MoE 负载不均衡问题

Step 4: 各 expert 本地跑 FFN
Step 5: 再一次 AllToAll,把结果 token 送回原位(combine)

通信量:每卡收发 M bytes(数据总量,不因 N 变化)。

为什么头疼:

  • 不能用 ring 优化——N² 条链路同时活跃,fabric 全打开
  • 延迟主导——路由必须完成才能算 FFN,不像 AllReduce 可以 overlap 到反向传播
  • 负载不均——每卡收到的 token 数不确定,拖尾严重

优化方案:

  • Hierarchical AllToAll:先机内做局部 AllToAll → 机间做一次 → 机内再一次
  • Fused kernel:permutation + AllToAll + GEMM 融合,隐藏延迟
  • Expert placement:频繁互相路由的 expert 放同一台机器
  • DualPipe(DeepSeek V3):把 AllToAll 塞进 pipeline 空闲气泡

三、六种并行 × 通信量对照表

这张表是这篇文章最实用的东西。以 Llama-3 70B、一步 forward+backward、GBS=512 为例:

并行 切什么 关键 collective 单步通信量(每卡) 建议网络层
DP Batch B AllReduce(gradients) 2 × params ≈ 280 GB/step 机间 IB(可 overlap)
TP Weights (heads/F) AllReduce(activations) × 4/layer 4 × L × B × S × D × 2 ≈ 21 TB/step 机内 NVLink(必须)
SP Activations (S) 在 element-wise op AllGather + ReduceScatter 和 TP 同数量级 机内 NVLink(配 TP 用)
CP Sequence in Attention Ring send/recv (KV) ~2 × L × B × S × D × 2 机内 NVLink
EP Experts AllToAll × 2/layer ~2 × L × B × S × D × 2(激活分数) 机内/机间都行,但机内更好
PP Layers Send/Recv(activations) 2 × B × S × D × (PP-1) 机间 IB(延迟不敏感)
VP Vocab AllReduce(loss stats) 极小(几 KB) 任意

关键规律(背下来):

  1. TP/SP 通信量最大(每层 4 次 collective),必须走 NVLink(机内)。跨机 TP = 灾难
  2. PP 通信量最小(只传激活),可以走 机间 IB。但有 pipeline bubble 问题
  3. DP 通信量中等,但可以 overlap 到 backward 里(NCCL 支持异步),实际不成为瓶颈
  4. EP 通信量大且是 AllToAll,是 MoE 的头号问题
  5. VP 通信量小,随便放

这决定了千卡训练的拓扑:

                    3D 并行经典拓扑
    ┌─────────────────────────────────────────────────┐
    │                                                  │
    │   ┌──────────────┐  ┌──────────────┐            │
    │   │ 机器 A       │  │ 机器 B       │            │
    │   │ ┌──┬──┐      │  │ ┌──┬──┐     │            │
    │   │ │G0│G1│      │  │ │G0│G1│     │            │
    │   │ ├──┼──┤ NVL  │  │ ├──┼──┤ NVL │  ← TP 组   │
    │   │ │G2│G3│      │  │ │G2│G3│     │    机内    │
    │   │ ├──┼──┤      │  │ ├──┼──┤     │            │
    │   │ │G4│G5│      │  │ │G4│G5│     │            │
    │   │ ├──┼──┤      │  │ ├──┼──┤     │            │
    │   │ │G6│G7│      │  │ │G6│G7│     │            │
    │   │ └──┴──┘      │  │ └──┴──┘     │            │
    │   └──────┬───────┘  └──────┬──────┘            │
    │          │                  │                    │
    │          └──── IB ──────────┘   ← PP 组          │
    │             (机间)              (跨机)           │
    │                                                  │
    │        ┌────────────────────┐                    │
    │        │  ...多个 PP 组     │   ← DP 组          │
    │        │  AllReduce gradient│    (跨 pod)        │
    │        └────────────────────┘                    │
    └─────────────────────────────────────────────────┘

    典型: 8 卡机内 TP=8, PP 跨机, DP 跨 pod

四、Tensor Parallel:Column + Row 的免费配对

现在开始一种一种把并行讲透。第一个也是最核心的:TP。

4.1 Column Parallel(切输出维度)

对权重 W ∈ [D, F],按列切成 W = [W_0, W_1, W_2, W_3](假设 N=4),每卡持有 W_i ∈ [D, F/4]。

              W ∈ [D, F]                        Y = X · W ∈ [B*S, F]
     ┌──────┬──────┬──────┬──────┐
     │      │      │      │      │
     │ W_0  │ W_1  │ W_2  │ W_3  │           ┌─────┬─────┬─────┬─────┐
  D  │      │      │      │      │    ─►     │ Y_0 │ Y_1 │ Y_2 │ Y_3 │
     │      │      │      │      │           └─────┴─────┴─────┴─────┘
     │      │      │      │      │            B*S       F/4 each
     └──────┴──────┴──────┴──────┘
       F/4   F/4    F/4    F/4

切分方式:
  GPU 0 持有 W_0  →  Y_0 = X · W_0  ∈ [B*S, F/4]
  GPU 1 持有 W_1  →  Y_1 = X · W_1  ∈ [B*S, F/4]
  GPU 2 持有 W_2  →  Y_2 = X · W_2  ∈ [B*S, F/4]
  GPU 3 持有 W_3  →  Y_3 = X · W_3  ∈ [B*S, F/4]

输入要求:  X 必须每卡都有("复制"状态)
输出状态:  Y 是分片的(每卡持有 F/4 那一片),不需要通信

核心:输入是复制的、输出是分片的——结果自然分片,正好适合接下一个 Row Parallel。

4.2 Row Parallel(切输入维度)

对权重 W ∈ [F, D],按行切成 W = [W_0; W_1; W_2; W_3],每卡持有 W_i ∈ [F/4, D]。

      X ∈ [B*S, F]                W ∈ [F, D]
    分片输入                      ┌──────────┐
    ┌─────┬─────┬─────┬─────┐    │   W_0    │  F/4
    │ X_0 │ X_1 │ X_2 │ X_3 │    ├──────────┤
    └─────┴─────┴─────┴─────┘    │   W_1    │  F/4
       F/4 each                   ├──────────┤
                                  │   W_2    │  F/4
                                  ├──────────┤
                                  │   W_3    │  F/4
                                  └──────────┘
                                       D

  每卡本地算:
    GPU 0: Y_0 = X_0 · W_0  ∈ [B*S, D]  (是"部分和",不是最终结果)
    GPU 1: Y_1 = X_1 · W_1  ∈ [B*S, D]
    GPU 2: Y_2 = X_2 · W_2  ∈ [B*S, D]
    GPU 3: Y_3 = X_3 · W_3  ∈ [B*S, D]

  最终结果:
    Y = Y_0 + Y_1 + Y_2 + Y_3  ← 需要 AllReduce!

核心:输入是分片的(正好接 Column Parallel 的输出)、输出是部分和——必须 AllReduce 才能得到完整结果。

4.3 Column + Row 配对:一层 MLP 只需要一次 AllReduce

        [B, S, D]                       [B, S, D]
     "复制" 输入                       "复制" 输出
        每卡都有                          每卡都有
           │                                 ▲
           ▼                                 │
    ┌─────────────┐                   ┌──────┴──────┐
    │  W_up       │                   │  W_down     │
    │ Column      │                   │ Row Parallel│
    │ Parallel    │                   │             │
    │ [D → F]     │                   │ [F → D]     │
    └──────┬──────┘                   └──────▲──────┘
           │                                 │
           ▼                                 │
    [B, S, F/4]                       [B, S, D]  (部分和)
    "分片" 状态                       每卡有一个,需 AllReduce
    每卡 F/4                                 ▲
           │                                 │
           │     ┌────────────┐             │
           └────►│ activation │─────────────┘
                 │   (SiLU)   │
                 └────────────┘
                 每卡独立算,无通信

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  整个 MLP 只有一次 AllReduce (发生在 W_down 之后)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

为什么 Column + Row 是"免费"的:中间那个 [B, S, F/4] 的分片状态天然衔接——从 Column 出来就是分片,直接送进 Row 不需要额外通信。如果反过来 Row + Column,中间就需要一次 AllGather + 一次 AllReduce,通信翻倍。

Attention 也是同一套配对:

                 Attention TP 分卡示意(N=4)

  输入 X ∈ [B, S, D]  (复制)
                │
                ▼
        ┌───────────────┐
        │  W_qkv        │  Column Parallel
        │  [D → 3*D]    │  按 head 切
        │               │
        │ 切成 4 份:    │
        │  head 0-15  │ head 16-31 │ head 32-47 │ head 48-63
        │  ──────────┼─────────────┼────────────┼────────────
        │  GPU 0     │  GPU 1      │  GPU 2     │ GPU 3
        └───────┬───────┘
                ▼
      每卡持有 (Q, K, V) ∈ [B, S, D/4]
                │
                ▼
        ┌───────────────┐
        │  Attention    │
        │  本地计算!    │  每卡 16 个 heads 独立算
        │  无通信       │
        └───────┬───────┘
                ▼
      每卡持有 O_local ∈ [B, S, D/4]
                │
                ▼
        ┌───────────────┐
        │  W_o          │  Row Parallel
        │  [D → D]      │  按 head 切输入
        │               │
        │  Y = O · W_o  │  每卡本地算,得到 [B, S, D] (部分和)
        └───────┬───────┘
                │
                ▼ AllReduce
      Y = sum(Y_local)  ∈ [B, S, D]  (完整,复制状态)

一层 Transformer 总共 2 次 AllReduce(MLP 一次 + Attention 一次)。Forward 2 次,Backward 又 2 次,一步 4 次。这就是前面通信量表的来源。

4.4 为什么 TP 必须机内

用 Llama 70B 算一笔账,一次 AllReduce 传的是 [B, S, D] 大小的激活:

单次 AllReduce 数据量 = B × S × D × 2 bytes (bf16)
                    = 1 × 8192 × 8192 × 2
                    = 128 MB

每层 4 次 (forward 2 + backward 2)
80 层  = 80 × 4 × 128 MB = 40 GB/step

延迟对比:
  NVLink Gen4 (H100): 900 GB/s  →  40 GB / 900 = 44 ms
  InfiniBand HDR:      50 GB/s  →  40 GB / 50  = 800 ms

  差 18 倍!  跨机 TP 会让通信占 50%+ 时间

结论:TP 只能机内。这也是为什么一台 8×H100 机器就是 TP=8 的天然上限。


五、Sequence Parallel:把 LayerNorm 也切了

TP 存在一个浪费:**元素级操作(LayerNorm、Dropout、Residual)**在 TP 下每卡都在算完整 [B, S, D]——没有并行加速,纯浪费显存和算力。

5.1 SP 的核心 insight

element-wise op 沿 sequence 维度切开是独立的(不像 attention 需要跨 sequence 交互)。所以可以按 S 切。

           TP-only(浪费):                   TP + SP(省显存):

  ┌─────────────────────────┐           ┌─────────────────────────┐
  │ 前一个 Attention 块      │           │ 前一个 Attention 块      │
  │ 输出: [B, S, D] (复制)   │           │ 输出: [B, S/4, D] (分片) │
  └──────────┬──────────────┘           └──────────┬──────────────┘
             │                                     │
             ▼ (无通信,已复制)                     ▼ AllGather(sp)
                                                   拉齐 sequence
  ┌─────────────────────────┐           ┌─────────────────────────┐
  │ LayerNorm               │           │ LayerNorm               │
  │ 每卡算完整 [B, S, D]    │           │ 每卡算 [B, S/4, D]      │
  │ 4 张卡在重复干活!       │  ◄──────  │ 4 张卡真正并行           │
  └──────────┬──────────────┘           │ (但先要 AllGather!)      │
             │                          └──────────┬──────────────┘
             ▼                                     ▼
      进入 Attention TP 区                  进入 Attention TP 区

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  之后 Attention 输出 → AllReduce → LayerNorm → ...
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

看起来 SP 多了一次 AllGather,通信不是更多了吗?

5.2 关键魔法:把 AllReduce 拆成两半

用第二节的恒等式:

AllReduce = ReduceScatter + AllGather

原来的 TP:

Attention 输出 (部分和) ──[AllReduce]──► LayerNorm 输入 (复制,完整 S)
                            ↑
                     一次 collective

改成 TP+SP:

Attention 输出 (部分和) ──[ReduceScatter over S]──► LayerNorm 输入 (完整 sum, S/4 分片)
                                                              │
                                                              ▼
                                                     LayerNorm (本地算 [B, S/4, D])
                                                              │
                                                              ▼
                                             ──[AllGather over S]──►  下一个 TP 区输入
                                                              (完整 S, 复制)

通信量完全没变! AllReduce 拆成 ReduceScatter + AllGather 两半,中间插入了本地 LayerNorm——LayerNorm 的激活值只需要存 [B, S/4, D] 而不是 [B, S, D]。

5.3 SP 的收益

一层 Transformer 的激活值显存(在 element-wise op 处):

  纯 TP:  4 × B × S × D × 2 bytes   (LayerNorm × 2 + Dropout × 2)
  TP+SP:  4 × B × S/N × D × 2 bytes

N=4 时: 显存下降到 1/4 (仅 element-wise 部分)

整层激活值构成(不 checkpointing):
  ┌──────────────────────────────────────────┐
  │ Attention: QKV 中间, softmax matrix       │  ← FlashAttention 已优化
  │ ────────────────────────────────────────  │
  │ FFN: [B,S,F] 中间态 (超大!)                │  ← TP 已解决
  │ ────────────────────────────────────────  │
  │ element-wise: LayerNorm, Dropout, Residual│  ← SP 解决这块!
  └──────────────────────────────────────────┘

面试锚点:SP 通信量和 TP 一样,但显存节省显著。Megatron-LM 现代实现里 TP 和 SP 几乎总是一起用。


六、Context Parallel:把 Attention 的序列也切了

长上下文(32K+)时,Attention 的 [B, S, S] 矩阵会爆显存。CP 把 sequence 维度切开,让 attention 也分布式做。

6.1 为什么 CP 不能简单切 S

Attention 有个诅咒:每个 token 需要 attend 所有其他 token。如果每卡只有一段 sequence,单卡本地算 Q·K^T 只能拿到 attention matrix 的一小块。

序列切 4 段(CP=4),每卡只有 S/4 的 Q,K,V:

  完整 Attention Matrix (S × S):
        Q_0 ── K_0
        Q_1 ── K_1
        Q_2 ── K_2      ← 我们需要每个 Q_i 和所有 K_j 交互
        Q_3 ── K_3      ← 但每卡只有一段 K!

  ┌───────────────────────────────┐
  │ Q_0 K_0 │ Q_0 K_1 │ Q_0 K_2 │ Q_0 K_3│
  │─────────┼─────────┼─────────┼────────│
  │ Q_1 K_0 │ Q_1 K_1 │ Q_1 K_2 │ Q_1 K_3│
  │─────────┼─────────┼─────────┼────────│
  │ Q_2 K_0 │ Q_2 K_1 │ Q_2 K_2 │ Q_2 K_3│
  │─────────┼─────────┼─────────┼────────│
  │ Q_3 K_0 │ Q_3 K_1 │ Q_3 K_2 │ Q_3 K_3│
  └───────────────────────────────┘
      ▲
      本地能算的只有对角块!其他块的 K/V 都在别的卡上

6.2 Ring Attention: 让 K,V 循环转一圈

核心想法:Q 留在原地,让 K,V 沿环形拓扑传递,每转一圈每张卡都见过所有 K,V。

Ring Attention 时序图(4 张卡):

  时间 →

  卡 0:  ┌──────┬──────┬──────┬──────┐
         │ Q₀·K₀│ Q₀·K₃│ Q₀·K₂│ Q₀·K₁│  ← Q_0 本地不动,轮流用 K_0,K_3,K_2,K_1
         └──────┴──────┴──────┴──────┘
         同时: K,V 从上一张卡收进来,发给下一张卡

  卡 1:  ┌──────┬──────┬──────┬──────┐
         │ Q₁·K₁│ Q₁·K₀│ Q₁·K₃│ Q₁·K₂│
         └──────┴──────┴──────┴──────┘

  卡 2:  ┌──────┬──────┬──────┬──────┐
         │ Q₂·K₂│ Q₂·K₁│ Q₂·K₀│ Q₂·K₃│
         └──────┴──────┴──────┴──────┘

  卡 3:  ┌──────┬──────┬──────┬──────┐
         │ Q₃·K₃│ Q₃·K₂│ Q₃·K₁│ Q₃·K₀│
         └──────┴──────┴──────┴──────┘

  每一步:
    - 用当前手上的 K,V 和本地 Q 算 attention 一块
    - 同时把当前 K,V 传给下一张卡(异步)
    - N 步后每张卡都用完了所有的 K,V

  关键: 通信和计算完全 overlap(send/recv 是异步的)
        只要一步计算时间 >= 一次 K,V 传输时间,通信就被完全隐藏

6.3 online softmax 让分块 attention 数学正确

朴素的分块会遇到 softmax 的问题——softmax 的分母需要看到所有值才能算。FlashAttention 用的 online softmax 解决了这个:每次算一块就更新 running max 和 running sum,最后归一化。

online softmax 状态维护(每张卡本地):

  m: running max (每个 Q 位置一个)
  l: running sum-of-exp (每个 Q 位置一个)
  O: running output (每个 Q 位置一个 D 维向量)

  每收到一块 K,V:
    1. 算 S_new = Q · K^T
    2. 更新 m_new = max(m_old, max(S_new))
    3. 用新旧 max 差重新 rescale l 和 O
    4. 累加 exp(S_new - m_new) 到 l
    5. 累加 exp(S_new - m_new) · V 到 O

  N 步后:
    最终 O / l 就是正确的 attention 输出

Ring Attention = Ring 传 K,V + online softmax + 通信/计算 overlap——这就是 Gemini 1M context 背后的技术。


七、FSDP / ZeRO:ailzhang 留的坑填一下

FSDP 是最重要的"训练侧显存优化"。ailzhang 没画的这一块,我详细讲。

7.1 ZeRO 三级递进

核心 insight:DP 下每张卡都持有完整参数+梯度+优化器状态是巨大浪费——DP 组内的 N 张卡都在存同样的东西,只是各自算不同的 batch。

纯 DP 每卡的显存构成(Adam 优化器,bf16 训练):

  ┌─────────────────────────────────────────────┐ 每卡都有
  │ 参数 (bf16)     : params × 2 bytes           │ 完整一份
  │ 梯度 (bf16)     : params × 2 bytes           │ ← 重复!
  │ Adam m (fp32)   : params × 4 bytes           │
  │ Adam v (fp32)   : params × 4 bytes           │
  │ master (fp32)   : params × 4 bytes           │
  │ ────────────────────────────────────────────│
  │ 合计            : 16 × params                │
  └─────────────────────────────────────────────┘

  70B 模型 = 70B × 16 = 1.1 TB/卡  ← OOM!

ZeRO 分三级切开这些"重复存"的东西:

ZeRO-1: 切优化器状态

  参数     : 每卡都有 (复制)         2 GB (对应 1B params)
  梯度     : 每卡都有 (复制)         2 GB
  优化器   : 按 N 切! 每卡 1/N        12/N GB  ← 省了!

  N=4: 每卡 (2+2+3) = 7 GB (vs 原来 16 GB)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ZeRO-2: 优化器 + 梯度

  参数     : 每卡都有 (复制)         2 GB
  梯度     : 按 N 切! 每卡 1/N        2/N GB
  优化器   : 按 N 切! 每卡 1/N        12/N GB

  N=4: 每卡 (2 + 0.5 + 3) = 5.5 GB

  实现: AllReduce grad → 改成 ReduceScatter grad
        (通信量不变,但每卡只存 1/N 梯度)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

ZeRO-3 / FSDP: 三样都切

  参数     : 按 N 切! 每卡 1/N        2/N GB
  梯度     : 按 N 切! 每卡 1/N        2/N GB
  优化器   : 按 N 切! 每卡 1/N        12/N GB

  N=4: 每卡 (0.5 + 0.5 + 3) = 4 GB

  代价: forward/backward 时需要临时 AllGather 参数
        通信量 ~1.5x DP

7.2 FSDP 的运行时时间线

FSDP 最难理解的是运行时——每次前向/反向传播都要 AllGather + discard 参数。看时间线:

                    FSDP forward pass 时间线(3 层示意)

  时间 →

  layer 0:                    ┌─ AllGather 参数 ─┐
                              │   ┌── compute ──┐│
                              │   │   forward   ││
                              │   └─────────────┘│
                              │   ┌ discard 参数┘
                              │
  layer 1:                    │  ┌─ AllGather 参数 ─┐  ← 与 layer0 compute overlap!
                              │  │   ┌── compute ──┐│
                              │  │   │   forward   ││
                              │  │   └─────────────┘│
                              │  │   ┌ discard 参数┘

  layer 2:                        ┌─ AllGather 参数 ─┐
                                  │   ┌── compute ──┐│
                                  │   │   forward   ││
                                  │   └─────────────┘│

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  overlap 是灵魂: 当前 layer 计算时,下一层的 AllGather 并行进行
  只要 compute >= 通信,通信被完全隐藏
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

                    FSDP backward pass 时间线

  layer 2 backward:  ┌─AllGather─┐ ┌─backward─┐ ┌─ReduceScatter grad─┐
                                      │                │
  layer 1 backward:                   ├─AllGather─┐    │  ┌─backward─┐ ...
                                                       │  │
                                                       └────► 拉下一层参数
  layer 0 backward:                                          ...

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  backward 每层做三件事:
    1. AllGather 参数 (临时拉完整,和 forward 时形状一样)
    2. 计算 backward
    3. ReduceScatter 梯度 (分散到各卡)
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

通信量分析:

一次完整 iteration (forward + backward + step):

  FSDP:
    forward:  L × AllGather(params/N)  ≈ 一次 params
    backward: L × AllGather(params/N)  ≈ 一次 params
              L × ReduceScatter(grad/N) ≈ 一次 params
    ─────────────────────────────────
    合计: 3 × params

  纯 DP:
    backward: AllReduce(gradients) = 2 × params (Ring AllReduce 系数)
    ─────────────────────────────────
    合计: 2 × params

  FSDP 通信量 = 1.5x DP,但显存降到 params/N

面试锚点:ailzhang 说 FSDP 不影响 local tensor shape——从 "compute 时看到什么" 的视角是对的(AllGather 后确实看到完整参数)。但从 "存什么" 的视角,FSDP 每张卡只存 1/N 参数。两个视角都对,不要混淆。

7.3 FSDP × TP 怎么搭:HSDP

现代大模型训练常用 HSDP (Hybrid Sharded Data Parallel):

                      HSDP 拓扑示意

  ┌────────────────────┐    ┌────────────────────┐
  │   机器 A            │    │   机器 B            │
  │  ┌───┬───┬───┬───┐ │    │  ┌───┬───┬───┬───┐ │
  │  │G0 │G1 │G2 │G3 │ │    │  │G0 │G1 │G2 │G3 │ │
  │  ├───┼───┼───┼───┤ │    │  ├───┼───┼───┼───┤ │
  │  │G4 │G5 │G6 │G7 │ │    │  │G4 │G5 │G6 │G7 │ │
  │  └───┴───┴───┴───┘ │    │  └───┴───┴───┴───┘ │
  │  NVLink (机内)     │    │  NVLink (机内)     │
  │  ┌──────────────┐  │    │  ┌──────────────┐  │
  │  │ TP=8         │  │    │  │ TP=8         │  │
  │  │ 每层参数在   │  │    │  │ 每层参数在   │  │
  │  │ 8 卡间切分   │  │    │  │ 8 卡间切分   │  │
  │  └──────────────┘  │    │  └──────────────┘  │
  └──────────┬─────────┘    └─────────┬──────────┘
             │                        │
             └────── IB (机间) ───────┘
                    FSDP 跨机 shard
                    参数在机器间切分
                    (每机器只存 1/M 参数副本)

  两层 shard 结构:
    - 机内: TP=8 (Megatron 方式,按 head/F 切权重)
    - 机间: FSDP=M (按 DP 方式,跨机 shard 参数)

  总参数副本数 = 1 份 (完全分散)
  单卡负担 = params / (TP × FSDP)

取舍:

  • FSDP-only(组内组间都 shard):显存最省,但跨机通信压力大
  • HSDP(组内 TP,组间 FSDP):现代 H100 集群主流,机内 TP 高带宽 + 机间 FSDP 分显存
  • ZeRO-2 只切梯度/优化器:显存节省不如 FSDP,但通信压力小

八、Pipeline Parallel 和它的 Bubble

PP 的核心 insight:把 L 层切成 K 段,每段放一台机器,forward 时数据流过 K 台机器。

8.1 朴素 PP 的 bubble 问题

                朴素 PP 时间线(4 段,一次一个 batch)

  时间 →  ═════════════════════════════════════════

  GPU 0: ┃F0┃                              ┃B0┃
  GPU 1:     ┃F1┃                      ┃B1┃
  GPU 2:         ┃F2┃              ┃B2┃
  GPU 3:             ┃F3┃      ┃B3┃

           ├──stage 内 forward──┤├─backward-┤
           GPU 大部分时间在等!

  bubble 比例 = (K-1)/K = 3/4 = 75% 空闲!

8.2 GPipe → 1F1B → Interleaved 1F1B

GPipe(Google, 2018):把 batch 切成 M 个 micro-batch,像流水线一样喂:

                GPipe 时间线(K=4, M=8)

  时间 →

  GPU 0: F0 F1 F2 F3 F4 F5 F6 F7                  B7 B6 B5 B4 B3 B2 B1 B0
  GPU 1:    F0 F1 F2 F3 F4 F5 F6 F7             B7 B6 B5 B4 B3 B2 B1 B0
  GPU 2:       F0 F1 F2 F3 F4 F5 F6 F7       B7 B6 B5 B4 B3 B2 B1 B0
  GPU 3:          F0 F1 F2 F3 F4 F5 F6 F7 B7 B6 B5 B4 B3 B2 B1 B0

  bubble 比例 = (K-1)/(K+M-1) = 3/11 ≈ 27%
  M=32 时  = 3/35 ≈ 9%
  M=64 时  = 3/67 ≈ 5%

  问题: 所有 forward 完再做 backward → 激活值要存 M 次!
        M=64 时激活值显存爆炸

1F1B (One Forward One Backward, PipeDream):交替做 forward 和 backward,激活值只存 K 次:

                1F1B 时间线(K=4, M=8)

  时间 →

  GPU 0: F0 F1 F2 F3 B0 F4 B1 F5 B2 F6 B3 F7 B4 B5 B6 B7
  GPU 1:    F0 F1 F2 F3 B0 F4 B1 F5 B2 F6 B3 F7 B4 B5 B6 B7
  GPU 2:       F0 F1 F2 F3 B0 F4 B1 F5 B2 F6 B3 F7 B4 B5 B6 B7
  GPU 3:          F0 F1 F2 F3 B0 F4 B1 F5 B2 F6 B3 F7 B4 B5 B6 B7

  bubble 比例 = 和 GPipe 相同,但激活值显存降到 O(K) 不是 O(M)!

  1F1B 稳态: 每张卡在 F(i) 和 B(i-K+1) 之间交替
             一旦 backward 开始,forward 一步就 backward 一步

Interleaved 1F1B:每张卡持有多个不连续的 layer chunk(如 GPU 0 持有 L0-L10 和 L40-L50)。让流水线更细,bubble 进一步降低。代价是通信次数翻倍。

8.3 DeepSeek V3 的 DualPipe

DualPipe 同时跑两条 pipeline,一条正向一条反向,让它们的通信互相 overlap:

                DualPipe 概念示意(简化)

  时间 →

  GPU 0: F(正向 batch A) │ B(反向 batch B) │ F(正向 A) │ B(反向 B)
                            ↑                           ↑
                       两条 pipeline 的通信在此处 overlap

  GPU 1: 类似
  GPU 2: 类似
  GPU 3: 类似

  精妙之处:
    - 一条 pipeline 的 backward 通信 (send activations)
      与另一条的 forward compute overlap
    - MoE 的 AllToAll 通信也塞进 pipeline 空闲气泡里
    - bubble 几乎归零

  代价: 显存翻倍 (两条 pipeline 同时存激活)

面试锚点:Pipeline bubble 无法完全消除的根源是依赖关系——第 k 层的 forward 必须等第 k-1 层输出,backward 必须等第 k+1 层梯度传回。1F1B/DualPipe 都是通过精巧调度让 GPU 不空闲,但第一个 micro-batch 传到最后一层前,前面几张卡必然空闲(warm-up bubble)。


九、MoE Expert Parallel 深度展开

MoE 的 EP 是当代最复杂的并行。用 8 个 expert × 4 张卡(EP=4)为例:

9.1 Expert 在卡上的分布

                Expert Parallel 布局(8 experts, EP=4)

  GPU 0: E0, E4          ← 每卡持有 2 个 expert
  GPU 1: E1, E5
  GPU 2: E2, E6
  GPU 3: E3, E7

  每个 expert 是一个独立的 FFN 权重(W_up + W_down + W_gate)
  Llama-3-70B 那种 dense 模型没有,DeepSeek/Mixtral/Qwen3 才有

9.2 MoE forward 完整流程

                MoE Forward 6 步走

  Step 1: Router 给每个 token 打分(本地计算,无通信)
  ─────────────────────────────────────────────────────
    GPU 0: 拿到 tokens [t0..t99]
    经过 router MLP → 每个 token 的 8 个 expert 的分数
    Top-2 路由: t0 → E1,E3; t1 → E0,E5; ...


  Step 2: Local Permutation(本地重排 token)
  ─────────────────────────────────────────────────────
    按目标 expert 分组:
    GPU 0 本地重排后:
      Bucket_E0: [t1, t5, t9, ...]        ← 目标 E0 (在 GPU 0 上,本地)
      Bucket_E1: [t0, t3, t7, ...]        ← 目标 E1 (在 GPU 1 上,要发)
      Bucket_E2: [t2, t8, ...]
      Bucket_E3: [t0, t11, ...]
      Bucket_E4: [...]                    ← 目标 E4 (在 GPU 0 上,本地)
      Bucket_E5: [...]
      Bucket_E6: [...]
      Bucket_E7: [...]


  Step 3: Dispatch AllToAll(第一次通信!)
  ─────────────────────────────────────────────────────
        ┌─────────────────────────────────────┐
    GPU 0│──[Bucket_E1]──►GPU 1                │
        │──[Bucket_E2]──►GPU 2                │
        │──[Bucket_E3]──►GPU 3                │
        │──[Bucket_E0,E4]─保留本地             │
        │──[Bucket_E5]──►GPU 1                │
        │──[Bucket_E6]──►GPU 2                │
        │──[Bucket_E7]──►GPU 3                │
        │                                     │
    ... 每张卡同理,N² 条链路同时打开          │
        └─────────────────────────────────────┘

    通信后每张卡收到该卡 expert 该处理的所有 token:
      GPU 0 (跑 E0, E4): [来自所有卡的 E0 tokens] + [来自所有卡的 E4 tokens]
      GPU 1 (跑 E1, E5): 类似


  Step 4: Expert 计算(本地,无通信)
  ─────────────────────────────────────────────────────
    每张卡的每个 expert 独立算 FFN
    GPU 0:
      E0_output = FFN_E0(E0_tokens)
      E4_output = FFN_E4(E4_tokens)

    ⚠️ 每卡收到的 token 数不同 → 负载不均衡,拖尾严重


  Step 5: Combine AllToAll(第二次通信!)
  ─────────────────────────────────────────────────────
    把 expert 输出送回原来的 token 位置
    和 Step 3 完全对称,但方向反了


  Step 6: Un-permutation + 加权组合
  ─────────────────────────────────────────────────────
    每个 token 收到 2 个 expert 的输出(top-2)
    按 router 打分加权组合:
      out = w_E1 · out_E1 + w_E3 · out_E3

一层 MoE 有 2 次 AllToAll。80 层就是 160 次——这就是为什么 MoE 训练 AllToAll 是绝对瓶颈。

9.3 EP × TP 组合:万亿参数模型的典型拓扑

DeepSeek V3 (671B, MoE) 的实际配置:

                  DeepSeek V3 训练拓扑(简化)

  ┌─────────────────────────────────────────────────┐
  │  Cluster: N 台机器,每台 8 卡                     │
  │                                                  │
  │  一层 Transformer 的 forward:                    │
  │                                                  │
  │  ┌──────────────┐                                │
  │  │  Attention   │  ← 用 TP 切 head              │
  │  │  TP=1 or 2   │    (Llama 用 8,DeepSeek 少)   │
  │  └──────┬───────┘                                │
  │         │                                        │
  │  ┌──────▼───────┐                                │
  │  │    Router    │  ← 本地计算                    │
  │  └──────┬───────┘                                │
  │         │                                        │
  │  ┌──────▼───────┐                                │
  │  │   MoE FFN    │  ← EP=64 (跨机!)              │
  │  │   256 experts│    每台机器持有一部分 experts │
  │  │              │    AllToAll 走 IB              │
  │  └──────────────┘                                │
  │                                                  │
  │  额外:                                           │
  │    - PP=16 (Pipeline 分层)                       │
  │    - DualPipe (让 AllToAll 塞进 PP 气泡)         │
  │    - FSDP 组内 shard 参数                        │
  └─────────────────────────────────────────────────┘

十、推理侧并行:TP/EP/PP 的差异

前面全在讲训练。推理侧我的主场,补关键差异。

10.1 推理和训练的核心差异

                训练 vs 推理 关键差异

  维度         │ 训练              │ 推理
  ─────────────┼───────────────────┼──────────────────
  主导阶段     │ forward + backward│ 只有 forward
  Batch 大小   │ GBS 512-2048 稳定 │ 动态 batch,变化剧烈
  关键指标     │ Throughput (MFU)  │ Latency (TTFT + TPOT)
  通信容忍度   │ 高(可 overlap)  │ 低(延迟敏感)

结论:推理下通信开销更难被隐藏(decode 阶段计算量小,compute 遮不住通信)。

10.2 推理下的 TP:为什么止步 TP=8

              训练 TP AllReduce (可 overlap):

  layer i forward compute ┌───────────────────┐
                          │                   │
                          └──────┬────────────┘
  AllReduce               ┌────▼─────┐
                          └──────┬───┘
  layer i+1 fwd compute        ┌─▼─────────────┐  ← 与 AllReduce overlap
                               └───────────────┘
                          总时间 = max(compute, comm)

  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
              推理 TP AllReduce (无法 overlap):

  decode step (只算 1 token 的 forward):
    layer i compute  ┌──┐            ← 极短(1 token 太少)
                      └┬─┘
    AllReduce         ┌▼──────────┐  ← 通信主导!
                      └───────────┘
    layer i+1 compute            ┌──┐
                                  └──┘

  总时间 = compute + comm  ← 通信不能藏!
  • Decode 阶段每步只算 1 token,compute 时间极短,AllReduce 通信占大头
  • 推理 TP 通常最多 TP=8(一台机器内 NVLink)——TP=16 跨机会大幅掉 TPOT
  • MQA/GQA 帮了大忙——KV heads 少,TP 对 KV 的复制开销小

10.3 推理下的 EP:DP Attention + EP FFN

MoE 推理最有意思的是 Attention 走 DP、FFN 走 EP 的组合:

                  DP Attention + EP FFN 布局

  8 张卡,MoE 32 experts

  ┌──────────────────────────────────────────────────┐
  │  各卡完全独立处理不同请求(DP Attention)           │
  │                                                    │
  │  GPU 0: [请求 A] → Attention 本地  → 拿到 hidden_A│
  │  GPU 1: [请求 B] → Attention 本地  → 拿到 hidden_B│
  │  GPU 2: [请求 C] → Attention 本地  → 拿到 hidden_C│
  │  GPU 3: [请求 D] → Attention 本地  → 拿到 hidden_D│
  │  ...    (每卡独立,无跨卡通信!)                   │
  │                                                    │
  │  ↓ 到 MoE FFN 时                                   │
  │                                                    │
  │  8 张卡共享 32 experts (每卡 4 个 expert)         │
  │  用 AllToAll 路由 hidden state                     │
  │                                                    │
  │  GPU 0: 持有 E0-E3                                 │
  │  GPU 1: 持有 E4-E7                                 │
  │  ...                                               │
  │                                                    │
  │  AllToAll → 每张卡收到给自己 expert 的 tokens     │
  │             (来自所有请求)                          │
  │  本地 FFN → AllToAll 送回原位置                    │
  └──────────────────────────────────────────────────┘

  为什么这个组合好:
    - Attention 完全无跨卡通信 (每请求独立)
    - 只有 FFN 层需要 AllToAll,通信频次减半
    - 每个请求的 KV Cache 完全在自己卡上,不用分享
    - 天然支持 Prefix Cache (KV 就地存,不跨卡)

10.4 推理独有的并行:DP 副本 + Prefix Cache

推理有个训练没有的维度——多副本 DP 服务:

                  推理集群拓扑

                       ┌──────────┐
                       │  Router  │  按 prompt hash 路由
                       └────┬─────┘
                            │
             ┌──────────────┼──────────────┐
             │              │              │
        ┌────▼───┐    ┌─────▼──┐    ┌────▼────┐
        │副本 1  │    │副本 2  │    │副本 3   │
        │        │    │        │    │         │
        │TP=8    │    │TP=8    │    │TP=8     │
        │Prefix  │    │Prefix  │    │Prefix   │
        │Cache   │    │Cache   │    │Cache    │
        └────────┘    └────────┘    └─────────┘

  路由策略: 同一 prompt 前缀 → 同一副本
            利用副本的 Prefix Cache (KV Cache 复用)

  好处:
    - 无梯度同步 (推理没有梯度)
    - 副本之间完全独立
    - 前缀命中率高 (system prompt 共享率 >90%)

这是推理独有的并行拓扑——vLLM 三年演进史 里详细讲过。

10.5 为什么推理很少用 SP/CP

  • SP 为省 LayerNorm 激活显存,推理没有 backward 不需要存激活
  • CP 为训练超长序列,推理长序列走 FlashAttention + PagedAttention 就够了

Pipeline Parallel 推理里极少用——bubble 在流式服务里消除不掉,只在"单机装不下的超大模型"时不得不 PP。


十一、选型决策树

给一个可以照抄的选型流程:

                    分布式选型决策树

  开始
    │
    ▼
  ┌─────────────────────────┐
  │ 单卡能装下模型吗?        │
  └───┬─────────────────────┘
      │
   能 │       ┌── DP + ZeRO-1 起步(省显存不掉性能)
      ▼───────┤
      │       └── 数据够多就上,不需要复杂并行
   不能
      ▼
  ┌─────────────────────────┐
  │ 有 NVLink 机内互联吗?    │
  └───┬─────────────────────┘
      │
   有 │       ┌── 机内 TP=8 起步(Column+Row 配对)
      ▼───────┤
      │       └── 可能配上 SP 省激活值显存
   没有
      ▼───────── 只能纯 PP(少数不幸的场景)


  ┌─────────────────────────┐
  │ 加上 TP=8 后单机能装下吗?│
  └───┬─────────────────────┘
      │
   能 │       ┌── 机间 DP 或 FSDP
      ▼───────┤
      │       └── 现代主流: HSDP (机内 TP + 机间 FSDP)
   不能
      ▼───────── 加 PP,跨机 stage


  ┌─────────────────────────┐
  │ 是 MoE 吗?               │
  └───┬─────────────────────┘
      │
   是 ▼───────── 加 EP,机内先做,溢出再机间
      │        (DualPipe 让 AllToAll overlap PP 空闲)
      │
   否 ▼


  ┌─────────────────────────┐
  │ 序列超长(>32K)?          │
  └───┬─────────────────────┘
      │
   是 ▼───────── 加 SP + CP,和 TP 配合


  ┌─────────────────────────┐
  │ 显存还是不够?             │
  └───┬─────────────────────┘
      │
   是 ▼───────── 上 FSDP 或 ZeRO-3(代价 1.5x 通信)

典型 Llama-3 70B 训练:

  • 8 台 H100 = 64 卡
  • 每台 TP=8(NVLink)
  • 机间 FSDP=8(IB)
  • 无 PP,无 EP

典型 DeepSeek V3 671B (MoE) 训练:

  • 数千卡集群
  • TP=1(MoE expert 本身分卡)
  • EP=64(跨节点)
  • PP=16 + DualPipe
  • FSDP 组内 shard 参数

典型 kimi-k2 万亿参数推理(推测):

  • 单请求 TP=8 + EP=32(几十张卡服务一个请求)
  • 多副本 DP 复制
  • Prefix Cache + Continuous Batching

十二、面试锚点

Q1: TP 为什么必须走机内 NVLink?

A: TP 每层 4 次 AllReduce(fwd 2 + bwd 2),每次约 B×S×D 级别。Llama 70B、B=1、S=8192、D=8192,一次 AllReduce ≈ 128 MB,80 层就是 40 GB/step。跨机 IB(50 GB/s)跑要 800ms,机内 NVLink(900 GB/s)只要 44ms。差 18 倍。TP 只能机内。

Q2: FSDP 和 ZeRO-3 有什么区别?

A: 概念上是同一件事——都把参数、梯度、优化器状态全按 DP 组切开。工程实现不同:ZeRO-3 是 DeepSpeed 的实现,FSDP 是 PyTorch 官方实现,通信调度和 hook 机制略有差异。等价的,选实现看框架偏好。

Q3: AllReduce 为什么能拆成 ReduceScatter + AllGather?这有什么意义?

A: 数学上 AllReduce = "reduce 到每卡 + 广播"。ReduceScatter 是"reduce 完每卡只拿一片",AllGather 是"每卡有一片聚成完整",合起来等于 AllReduce。意义:中间那一步"每卡只有一片"可以直接用于分布式优化器更新——这就是 ZeRO-2/FSDP 省显存的原理。SP 也用同样的等价变换,把 AllReduce 拆开后中间插入 element-wise op。

Q4: MoE 的 AllToAll 为什么这么头疼?

A: 三个原因: (1) 通信量大——每卡把 token 分成 N 份发出去再收回来,总量 O(B×S×D); (2) 不能用 ring——N² 条链路同时活跃,fabric 压力极大; (3) 延迟主导——路由必须完成才能算 FFN,不像 AllReduce 可以 overlap 到反向传播。DeepSeek V3 用 DualPipe 把 AllToAll 塞进 PP 空闲气泡是目前最先进的方案。

Q5: Ring AllReduce 为什么带宽不受卡数影响?

A: Ring 把 tensor 切成 N 块,ReduceScatter + AllGather 两阶段各 N-1 步。每一步每张卡同时发一块给下一张、收一块从上一张——所有链路同时工作。总通信量 2(N-1)/N × M ≈ 2M,和 N 无关。瓶颈从"带宽"变成"延迟"(O(N) 步)。1000+ 卡时切换到 Tree 或 Halving-Doubling。

Q6: FSDP 通信量比 DP 大,为什么大家还用?

A: 显存节省 >> 通信代价。FSDP 通信 1.5x DP,但显存降到 1/N。70B+ 大模型不用 FSDP 根本装不下(16 卡训 70B,不 shard 单卡 30GB+ 直接 OOM)。FSDP 通信可以和 compute overlap,实际吞吐差距远小于 1.5x。关键要有 NVLink——通信慢了 overlap 藏不住,FSDP 会崩。

Q7: 3D 并行(TP × PP × DP)的拓扑怎么设计?

A: "通信量大的走机内"原则:(1) TP=8 in machine (NVLink); (2) PP=K across machines within a pod (IB,延迟不敏感); (3) DP replicate across pods (AllReduce 可 overlap)。举例:1024 卡训 70B,TP=8, PP=4, DP=32——一台机器一个 TP 组,4 台机器一个 PP 组,32 组做 DP。

Q8(进阶): Pipeline bubble 为什么无法完全消除?

A: 依赖关系——第 k 层的 forward 必须等第 k-1 层输出,backward 必须等第 k+1 层梯度传回。1F1B 通过交错 F 和 B 让 GPU 不空闲,但第一个 micro-batch 传到最后一层前,前面几张卡必然空闲(warm-up bubble)。DualPipe 用双向流水几乎消除,但代价显存翻倍。理论下界 K/(K+M),M 越大越好但激活值显存受限。

Q9(进阶): 推理和训练的并行策略最大差异是什么?

A: 三点: (1) 推理没有 backward,SP/CP 不需要; (2) 推理延迟敏感,TP 通信藏不住,一般不超过 TP=8; (3) 推理独有 DP 副本 + Prefix Cache 组合——多副本、按前缀 hash 分发、共享 KV Cache。Pipeline Parallel 训练常用推理少用,bubble 在流式推理里消除不掉。

Q10(进阶): 给你 1024 张 H100 训万亿参数 MoE,你会怎么切?

A: 参考 DeepSeek V3: (1) EP=64(跨节点专家并行,expert 数量决定); (2) PP=8-16(Pipeline 分层); (3) TP=1(MoE 本身分卡了,TP 反而增加通信); (4) 组内 FSDP shard 参数; (5) 跨组 DP replicate。关键优化: DualPipe 让 AllToAll 和 PP 计算 overlap; hierarchical AllToAll 优先机内路由。


一句话记住这一篇

分布式 Transformer 的所有设计,本质在平衡三个约束(显存/算力/通信)——把通信量大的并行(TP/SP/EP)放到机内 NVLink,通信量小的(DP/PP)走机间 IB。理解 AllReduce = ReduceScatter + AllGather 这个恒等式就理解了 ZeRO/FSDP/SP 一系列优化的核心。训练侧和推理侧的并行策略在拓扑决策上有本质差异,推理独有 DP 副本 + Prefix Cache 这一维度,训练独有 PP 的 bubble 问题。


配套阅读:ailzhang 的 Visualizing Parallelism in Transformer 是最好的入门可视化,JAX Scaling Book 的 Transformer Accounting 是最好的单卡张量形状参考。我这个博客还有几篇相关: MoE 推理完整机制、vLLM 三年演进史、现代 LLM 推理引擎剖析、FlashAttention 三代演进。

Zoe

Written by

Zoe

AI Infra Engineer · LLM Serving · GPU/RDMA · 造工具的偏执狂

评论