Chapter 07

张量并行 TP 与列并行 / 行并行的实现

📌 源码引用基于 megatron/core/tensor_parallel/ · commit 432d76b2b8bf 。

7.1 为什么需要张量并行

看一个具体数字:Llama-3 70B 的一层 FFN 包含 $W_1 \in \mathbb{R}^{8192 \times 28672}$,仅这一个矩阵 fp16 就是 $8192\times 28672\times 2 = 470$ MB;fp32 master + Adam $m,v$ 加起来 $\times 5 = 2.35$ GB。整层带来的状态接近 5 GB。70B 模型 80 层,纯 DDP 是绝无可能

解决办法之一是把这个大矩阵沿一个维度切到不同 GPU 上,每张卡只算自己负责的"竖条"或"横条",最后 all-reduce 拼起来。这就是 Megatron 的张量并行(Tensor Parallelism, TP)。

7.2 列并行 vs 行并行:两种切法

Megatron 把所有 nn.Linear 替换成两种特殊版本:

名字切 $W$ 的哪一维输入输出通信
ColumnParallelLinear列方向(output_dim)完整复制切分反向 all-reduce 梯度
RowParallelLinear 行方向(input_dim)切分完整(需 all-reduce)前向 all-reduce 激活

列并行(ColumnParallelLinear)

设输入 $X \in \mathbb{R}^{B \times H}$,权重 $W \in \mathbb{R}^{H \times K}$,普通线性是 $Y = XW$。

把 $W$ 沿列切成 $W = [W_1, W_2, \dots, W_T]$,每个 $W_i \in \mathbb{R}^{H \times K/T}$ 放在 GPU $i$ 上:

$$ Y_i = X W_i,\qquad Y = [Y_1, Y_2, \dots, Y_T] $$

每张卡得到 $Y$ 的一片,不需要通信。反向时需要 all-reduce 输入梯度 $\partial L/\partial X$。

行并行(RowParallelLinear)

把 $W$ 沿行切成 $W = \begin{bmatrix}W_1\\W_2\\\vdots\\W_T\end{bmatrix}$,每个 $W_i \in \mathbb{R}^{H/T \times K}$。输入 $X = [X_1, X_2, \dots, X_T]$(沿 hidden 维切):

$$ Y_i = X_i W_i,\qquad Y = \sum_{i=1}^{T} Y_i $$

每张卡算出部分和,前向需要 all-reduce。反向时输入梯度不需要通信。

7.3 Megatron 怎么"两次切"组合成 attention / FFN

Megatron 的核心 trick:把 attention / FFN 拆成 "Col → Row" 的组合,把两次通信合并成一次 all-reduce

FFN(两层 Linear + 激活)

flowchart LR X[X, 完整复制] --> CP1["ColumnParallel(W1)
每卡得 Y1_i = X·W1_i"] CP1 --> Act[GeLU/SwiGLU
无通信,逐元素] Act --> RP2["RowParallel(W2)
每卡得 Y2_i = Y1_i·W2_i"] RP2 --> AR["All-reduce
Σ Y2_i = Y"] AR --> Y[Y, 完整复制]

整个 FFN 只在末尾做一次 all-reduce,不是两次。这就是论文里说的 "MLP requires only one all-reduce per layer"。

Attention(QKV + Output Projection)

同样的思想:

$$ \underbrace{\mathrm{QKV} = X \cdot W_{qkv}}_{\text{ColumnParallel}} \;\rightarrow\; \mathrm{Attn}(Q,K,V) \;\rightarrow\; \underbrace{\mathrm{Out} = Y \cdot W_o}_{\text{RowParallel + all-reduce}} $$

QKV 矩阵被合并成一个 column parallel linear,attention 内部每张卡算自己负责的 heads,最后 output projection 是行并行 + all-reduce 拼起来。

为什么这样切 attention head 之间天然解耦(每个 head 算自己的 softmax),所以"按 head 切"和"按列切 QKV"是同一件事。num_attention_heads 必须能被 TP 整除

7.4 通信量计算

每个 transformer block 的 TP 通信量:

$$ C_\text{block} = 2 \cdot (B \cdot S \cdot H) \cdot \text{bytes} \quad (\text{2 个 all-reduce: attention + FFN}) $$

$B$ batch, $S$ seq length, $H$ hidden size。Llama-70B 单层每张卡每步通信量约 $2 \cdot 4 \cdot 4096 \cdot 8192 \cdot 2\,\text{bytes} \approx 540$ MB(bf16)。整 80 层就是 43 GB/step。这就是为什么 TP 必须配 NVLink——PCIe 跨节点带宽不够,性能会塌方。

7.5 显存收益

设 TP 度 $T$,单个 transformer block 参数从 $P_\text{block}$ 降到 $P_\text{block}/T$。但要注意:

7.6 关键源码地图

文件角色
megatron/core/tensor_parallel/layers.py ColumnParallelLinearRowParallelLinear 主实现
megatron/core/tensor_parallel/mappings.py 核心通信原语:copy_to_tensor_model_parallel_regionreduce_from_tensor_model_parallel_regiongather_from_tensor_model_parallel_regionscatter_to_tensor_model_parallel_region
megatron/core/tensor_parallel/cross_entropy.pyvocab parallel cross entropy(输出层也切 vocab)
megatron/core/tensor_parallel/random.py 跨卡同步随机种子(dropout 一致性)
megatron/core/tensor_parallel/data.py 跨 TP rank 的数据广播

7.7 mappings.py 里的 4 个通信原语

这是整个 TP 的"灵魂 4 函数",每个都有自己的前向 / 反向定义,因为反向时通信方向反过来:

函数前向反向
copy_to_tp_region 恒等(identity)all-reduce
reduce_from_tp_region all-reduce恒等
scatter_to_tp_region scatterall-gather
gather_from_tp_region all-gatherscatter

读 Megatron TP 源码的正确顺序

  1. 先读 mappings.py,把 4 个原语吃透;
  2. 再读 layers.py,看 ColumnParallelLinear / RowParallelLinear 怎么调这 4 个;
  3. 最后看模型层(如 megatron/core/models/gpt/gpt_layer_specs.py),看怎么把 ColumnParallel / RowParallel 拼成 attention + FFN。

7.8 vocab parallel cross-entropy

输出层 logits 维度 = vocab_size,对 Llama-3 是 128K。完整 logits 矩阵在每张卡上是 $B \cdot S \cdot 128000 \cdot 4\,\text{bytes}$,很容易 OOM。Megatron 的招:

  1. 把 output embedding 沿 vocab 维切到不同 TP rank(每卡持有 $V/T$ 个 vocab);
  2. 每卡只算自己负责那段 vocab 的 logits 与 loss
  3. cross_entropy.py 里特殊的 VocabParallelCrossEntropy,跨卡 max 找全局 max(数值稳定),跨卡 sum 找 exp sum,然后算 NLL。

这一步把 output layer 的激活内存也降了 $T$ 倍。千亿模型 + 大词表必须开这个。Megatron 中默认就走这条路径。

7.9 dropout 一致性问题

TP 把 attention 切到 $T$ 张卡上,但每张卡都要做同样的 dropout —— 如果 mask 不一致,反向梯度就错。tensor_parallel/random.py 维护两套 RNG:

这是 TP 实现里最容易踩坑的地方,新人改 dropout 行为时经常忘记切 RNG context。

7.10 调 TP 的经验法则

情况TP 建议
单机 8 卡 TP=1(DP=8)或 TP=2
单机 8 卡 + 模型偏大 TP=4 或 TP=8
多节点 TP 不要跨节点,TP $\leq$ 单节点 GPU 数
有 NVLink TP=8 是甜点(NVLink 内带宽够)
无 NVLink(PCIe) TP $\leq$ 2,超过性能塌方
MoE 模型 TP=2 + EP 配合
num_heads = $h$ TP 必须能整除 $h$(或 num_query_groups)

7.11 与第三方框架的关系

框架TP 实现来源
Megatron-LM 自家
Megatron-Core 同上,抽出来给外部用
Megatron-DeepSpeed 用 Megatron 的 TP + DeepSpeed 的 ZeRO
NeMo Megatron-Core
ColossalAI 自家实现(思路相同)
TorchTitan PyTorch DTensor 原生
verl 训练时调 Megatron 或 FSDP;推理时 vLLM 自家 TP

7.12 源码定位:四个核心 TP 类

所有 TP 实现集中在 megatron/core/tensor_parallel/layers.py(54KB),打开它直奔以下四个类:

类 / 函数位置用途
_initialize_affine_weight_gpu layers.py:136 weight 初始化:fan-in scaling + 跨 TP 一致性
VocabParallelEmbedding layers.py:197 词表沿 vocab 维切;forward 后 all-reduce
ColumnParallelLinear layers.py:770 沿列切(Y = XA, A 列切);QKV / FFN.up / out_proj_input
RowParallelLinear layers.py:1134 沿行切(Y = XA, A 行切);FFN.down / attn.output

四个原语在 mappings.pycopy_to / gather_from / reduce_from / scatter_to_tensor_model_parallel_region。配合 autograd 反向自动反演(copy 反 → reduce, gather 反 → scatter, reduce 反 → copy, scatter 反 → gather)。

7.13 MLA:Multi-head Latent Attention(DeepSeek 风格)

2025-Q3 加入。megatron/core/transformer/multi_latent_attention.py:1437 实现,arguments.py:3150 注册 CLI。要点:

7.14 TransformerLayer:TP 怎么进到一个 layer 里

megatron/core/transformer/transformer_layer.py:291TransformerLayer

7.15 这章你需要带走什么