<aside> 🎯
并行策略回答的是一个很基础的问题:模型的不同部分,怎么切到多张卡上。注意力、MoE 专家、层、请求、序列都可以用不同的方式切,而每一种切法,都会换来一种固定的集合通信。这一页我们就把「切法 → 通信原语 → 通信量」这条链一直手推到底。
</aside>
术语与口径表(全页公式统一使用,推理阶段没有反向传播、不同步梯度,所有公式只算前向)
| 符号 | 含义 | 备注 / 典型值 |
|---|---|---|
| B | Batch Size,一次前向里的请求数 | decode 时等于并发请求数 |
| S | Sequence Length,本次前向处理的序列长度 | prefill 是几千到几十万;decode 恒为 1 |
| B·S | 本次前向的 token 总数 | 所有通信量都正比于它,是最该先估的量 |
| H | Num Heads,注意力头数 | TP 切的就是这个维度,所以要求 N 整除 H |
| D | Hidden Dim per Head,每个头的维度 | 常见 128 |
| H·D | 模型隐藏层总维度 | DeepSeek-V3 是 7168,Llama-3-70B 是 8192 |
| I | FFN 中间层扩展维度 | 稠密模型约 4H·D 或 SwiGLU 的 8/3 H·D;DeepSeek expert 是 2048 |
| N | 参与当前这种并行的 GPU 数(Rank Count) | 注意每种并行的 N 可以不同:tp_size、ep_size、pp_size 各算各的 |
| K | MoE 的 Top-K 路由系数 | DeepSeek-V3 是 8;EP 通信量正比于 K |
| M | 某次通信里单卡手上要参与通信的那份数据大小 | AllReduce 时 M 是完整隐藏状态;All-to-All 时 M 是本地那份 token |
| Φ | 单卡在单个 Transformer layer 里传输的数据元素个数 | 本页所有通信量的统一单位;换成字节再乘 dtype 宽度(BF16 是 2,FP8 是 1) |
所有 Φ 公式里的系数(那些 2、4 和 (N-1)/N)都不是并行策略自己带的,而是它用的通信原语带的。原语层面的推导放在最后一节「补充知识:Ring 式集合通信」,第一次读可以先记结论,回头再看怎么来的。
| 参数 | 作用 | 默认 | 面试要点 |
|---|---|---|---|
--tensor-parallel-size / --tp-size / --tp |
张量并行:把每层的矩阵切到多卡 | 1 | 每层两次 AllReduce,对互联带宽敏感;多机时跨节点 TP 会很慢 |
--data-parallel-size / --dp-size |
数据并行:整个模型复制多份,各算各的请求 | 1 | 推理期零通信;官方建议用 SGLang Model Gateway(SMG)而不是内置 DP |
--pipeline-parallel-size / --pp-size |
流水并行:按层切成多段 | 1 | 只在段边界传一次激活,通信量最小,适合跨节点;代价是流水线气泡 |
--pp-max-micro-batch-size、--pp-async-batch-depth、--enable-dynamic-chunking |
PP 的微批次、异步深度、动态分块 | None / 0 / False | 动态 chunk 的目的是让每个 chunk 执行时间一致,减少流水线气泡 |
--enable-dp-attention |
只对注意力部分做数据并行,其余仍用 TP/EP | False | 必须同时设 --dp-size 大于 1,且 tp_size 能被 dp_size 整除 |
--ep-size / --ep(参见 MoE 文档) |
专家并行:把 MoE 专家权重分布到多卡 | — | DeepEP、Mooncake、NIXL-EP等 all-to-all 后端目前只支持 ep_size 等于 tp_size |
--moe-data-parallel-size / --moe-dp-size |
MoE 部分的数据并行度 | 1 | 与 DPA 组合时理解注意力与 MoE 两套并行域 |
--attention-context-parallel-size / --attn-cp-size |
注意力上下文并行(prefill 侧的 CP/SP) | 1 | 长上下文场景,把序列维度切开;对应 Ulysses / Ring Attention 那一类方案 |
--dcp-size / --decode-context-parallel-size |
Decode Context Parallel,专为 MLA 模型设计 | 1 | 配套 --dcp-comm-backend(ag_rs、a2a、fi_a2a)的通信选择 |
--nnodes --node-rank --dist-init-addr |
多机部署参数 | 1 / 0 / None | 每台机器都启动一个进程,指定同一个初始化地址;遇到死锁可尝试 --disable-cuda-graph |
--load-balance-method |
内置 DP 的负载均衡策略 | auto | 可选 auto、round_robin、follow_bootstrap_room、total_requests、total_tokens;total_tokens 只能在 DP attention 下使用 |
--enable-prefill-delayer |
DPA 下延迟 prefill 以减少空闲 | False | 配套 --prefill-delayer-max-delay-passes(默认 30)等,解决各 DP rank 步调不一致的等待问题 |
解决什么问题:单卡放不下稠密模型的权重(Llama-3-70B 光权重 BF16 就是 140 GB),以及小 batch 下想压低单请求延迟——多卡分摊同一份权重的读取,decode 一步的时间就短。
原理(考点:为什么一定是「列切 + 行切」这个顺序):Megatron 的规则只有一条——把两个连续的矩阵乘配成「前一个按列切、后一个按行切」,中间不通信,唯一一次通信推到配对的末尾。这条规则能成立,靠的是三件事咬合在一起。
第一,列切之后不需要通信。Y = X A 里把 A 按列切成 A1、A2,每卡算出的 Y1 = X A1 是 Y 的完整列块——每个元素都已经算完,不缺任何一个加法项,因为矩阵乘在输出的列方向上完全独立。缺的只是「别的列」,而别的列在别的卡手里,此刻并不需要。
第二,行切恰好只要前一步切出来的那部分。Z = Y B 里把 B 按行切成 B1、B2,展开就是 Z = Y1 B1 + Y2 B2。第 i 张卡要算 Yi Bi,它需要的输入正好是自己上一步算出的 Yi。前一步切出来的列块,就是后一步想要的输入,两步的切分方向天生咬合,所以中间那一刀不需要任何数据交换。
第三,代价被推到末尾。Yi Bi 的形状是完整的,但只含求和式里的一项,是部分和;把 N 张卡的部分和相加才是 Z,所以这里必须有一次 AllReduce,而且只需要一次。中间的逐元素算子(SwiGLU、GELU、缩放)不跨列混合,可以跟着列块原地走,不打断这条链;一旦中间插进一个要跨整个 hidden 维求统计量的算子(LayerNorm 那类),链条就断了,必须先把数据聚合起来——这也是 LayerNorm 和残差在标准 TP 里被每卡各存一份的原因(第 6 节的 Megatron-SP 就是来收拾这份冗余的)。
落到实际的 Transformer 层里:注意力块里 QKV 投影按列切,权重从 [H·D, 3H·D] 变成每卡 [H·D, (3H/N)·D],每卡拿到若干个完整的头,可以独立算局部 attention,不需要通信;输出投影 W_O 按行切,从 [H·D, H·D] 变成每卡 [(H/N)·D, H·D],每卡算出的是 [B, S, H·D] 的部分和,必须相加才对,于是第一次 AllReduce 出现在这里。FFN 同理:up / gate 按列切成 [H·D, I/N],down 按行切成 [I/N, H·D],down 之后又是部分和,第二次 AllReduce。所以「每层两次 AllReduce」不是实现细节,而是「一层里有两个行切矩阵」的直接结果。
flowchart LR
X["输入 [B, S, H·D]<br>每卡一份完整副本"] --> QKV["QKV 列切<br>每卡拿 H/N 个头"]
QKV --> ATT["各卡独立算局部 attention<br>无通信"]
ATT --> O["W_O 行切<br>输出是部分和"]
O --> AR1["AllReduce #1"]
AR1 --> UP["up/gate 列切"]
UP --> ACT["SwiGLU 逐元素<br>无通信"]
ACT --> DOWN["down 行切<br>输出是部分和"]
DOWN --> AR2["AllReduce #2"]
两种切法的语义差别值得单独记住,面试里问「为什么这里要 AllReduce」其实问的就是这张表:
| 切法 | 切权重的哪个维 | 单卡输出 | 输出语义 | 末尾要不要通信 |
|---|---|---|---|---|
| 列切(column / 输出维) | 输出维,如 QKV 的 3H·D、FFN 的 I | 形状变窄,输出维只剩 1/N | 完整的一部分列,每个元素都算完了 | 不要(后继行切矩阵正好只要这部分) |
| 行切(row / 输入维) | 输入维,如 W_O 的 H·D、FFN down 的 I | 形状不变,回到完整 hidden | 部分和,只含求和式里的一项 | 要一次 AllReduce |
把顺序反过来会怎样(这是验证自己是不是真懂的好问题):如果先行切再列切,第一步的输出已经是形状完整的部分和,而第二步的列切需要一份正确的完整输入,所以不得不在中间先 AllReduce 一次;而末尾列切的输出又是分片的,下一层要完整 hidden,还得再 AllGather 一次。同一层从一次通信变成两次,而且中间那次卡在关键路径上、前后都不能重叠。所以配对顺序不是风格问题,反了直接翻倍通信。
这条规则延伸出四个常考的角落: