张量并行与序列并行

数据并行和 ZeRO 解决的是「模型状态」的冗余:参数、梯度、优化器状态在每张卡上各存一份太浪费,就把它们切开。但它们有一个共同的前提:每张卡仍然完整地跑一遍每一层的计算,于是每张卡都要为自己那份 micro-batch 存下这一层全部的中间激活。当模型的 hidden size 很大、序列很长时,单卡哪怕只放 1 条序列的激活也装不下,ZeRO 就帮不上忙了。

张量并行(Tensor Parallelism,TP)换了一个思路:不切 batch,而是把每一个矩阵乘法本身拆到多张卡上。这样参数、梯度、优化器状态和(大部分)激活都被切开了,而且不需要在用之前把参数拼回完整形状。代价是:通信从「每步一次」变成了「每层好几次」,而且就在计算的关键路径上。序列并行(Sequence Parallelism,SP)是 TP 的一个小补丁,把 TP 没切开的那部分激活也沿序列维切掉,通信量不变。

预备:记号与集合通信的代价

记号:

TP 用到的集合通信只有三种(推导见集合通信与分片矩阵乘法)。设一个张量总大小为 \(M\) 字节,在 \(t\) 张卡上用环形(ring)算法:

如果每张卡的出口带宽是 \(W\)(字节/秒),这些操作的耗时近似就是「每张卡发送的字节数 \(/W\)」。特别地,All-Reduce 的代价 = All-Gather + Reduce-Scatter 的代价。

矩阵乘法的两种切法

神经网络里的线性层写成 \(Y = XW\),其中 \(X\) 是 \([N, k]\) 的输入(\(N=b\cdot s\) 是 token 数),\(W\) 是 \([k, n]\) 的权重。TP 依赖的只是分块矩阵乘法的两个恒等式:

$$XW = X\begin{bmatrix}W_1 & W_2 & \cdots & W_t\end{bmatrix} = \begin{bmatrix}XW_1 & XW_2 & \cdots & XW_t\end{bmatrix}$$ $$XW = \begin{bmatrix}X_1 & X_2 & \cdots & X_t\end{bmatrix}\begin{bmatrix}W_1 \\ W_2 \\ \vdots \\ W_t\end{bmatrix} = \sum_{i=1}^{t} X_i W_i$$

第一个式子说:把 \(W\) 按列切成 \(t\) 块,每张卡拿完整的 \(X\) 和一块 \(W_i\)(形状 \([k, n/t]\)),各自算出输出的 \(n/t\) 列,彼此之间没有任何依赖。这叫列并行线性层(column-parallel linear)。它的输入需要每张卡都有一份完整的 \(X\),输出是沿最后一维切开的 \(Y_i\),形状 \([N, n/t]\)。如果下游需要完整的 \(Y\),就要做一次 All-Gather。

第二个式子说:把 \(W\) 按行切成 \(t\) 块(每块 \([k/t, n]\)),同时 \(X\) 也按最后一维切开(每块 \([N, k/t]\)),每张卡算出一个完整形状 \([N, n]\) 的矩阵 \(X_iW_i\)。但它只是部分和,真正的 \(Y\) 是所有卡的结果加起来,所以要做一次 All-Reduce(或者如果下游只需要一部分,做 Reduce-Scatter)。这叫行并行线性层(row-parallel linear)。

列并行:W 按列切 行并行:W 按行切,X 也切 X [N, k] 每卡完整 × W₁ W₂ [k, n/2] 各一块 = XW₁ XW₂ [N, n/2] 各一块 无需通信即可算出; 要完整 Y 才需 All-Gather X₁ X₂ [N, k/2] × W₁ W₂ [k/2, n] = X₁W₁ + X₂W₂ [N, n] 部分和 每卡得到完整形状的部分和; 需 All-Reduce(或 Reduce-Scatter) GPU 0 持有 GPU 1 持有
两张卡的例子。列并行切输出维,行并行切收缩维(contracting dimension)。切收缩维的乘法一定会留下要求和的部分和。

还有一种更紧凑的写法:给张量的维度加下标,表示这一维被切在哪个设备轴上(这里 \(Y\) 就是 TP 所在的那组卡),\(\cdot_k\) 表示沿 \(k\) 维收缩。列并行是 \(X[N, k]\cdot_k W[k, n_Y] \to Y[N, n_Y]\),没有切收缩维,所以不需要通信;行并行是 \(X[N, k_Y]\cdot_k W[k_Y, n]\to Y[N, n]\{U_Y\}\),切了收缩维,结果带着一个「未归约」标记 \(\{U_Y\}\),必须 Reduce 一次。这是判断「这一步要不要通信」的通用规则:只要收缩的那一维被切了,结果就是部分和。

反向传播时情况恰好对调。对 \(Y=XW\),有 \(\partial L/\partial X = (\partial L/\partial Y)\,W^\top\) 和 \(\partial L/\partial W = X^\top(\partial L/\partial Y)\)。

所以单独看:列并行「前向不通信、反向 All-Reduce 输入梯度」,行并行「前向 All-Reduce 输出、反向不通信」。Megatron 的 f 和 g 算子就来自这对互补关系。

MLP:先列后行,只通信一次

经典 GPT 的 MLP 是 \(Z=\mathrm{GeLU}(XA)\),\(Y = ZB\),\(A\) 是 \([h, F]\),\(B\) 是 \([F, h]\)。怎么切?

如果把 \(A\) 按行切,那么 \(XA\) 是部分和,而 GeLU 是非线性的:\(\mathrm{GeLU}(XA_1+XA_2)\ne \mathrm{GeLU}(XA_1)+\mathrm{GeLU}(XA_2)\)。所以 GeLU 之前必须先 All-Reduce,这是一个额外的同步点。

如果把 \(A\) 按列切,每张卡得到 \(XA_i\),形状 \([N, F/t]\),这是输出的一部分列,不是部分和。GeLU 是逐元素的,所以 \(\mathrm{GeLU}(XA)\) 的第 \(i\) 块列就是 \(\mathrm{GeLU}(XA_i)\),各卡独立算即可。现在每张卡手里正好是 \(Z\) 沿 \(F\) 维切开的一块 \(Z_i\),这恰好是行并行线性层需要的输入格式。于是 \(B\) 按行切,每张卡算 \(Z_iB_i\),形状 \([N, h]\),最后一次 All-Reduce 得到 \(Y=\sum_i Z_iB_i\)。

整个 MLP 前向只有一次 All-Reduce,反向也只有一次(在 MLP 的入口,对 \(dX\))。两个大矩阵都切成了 \(1/t\),中间激活 \([N, F]\) 也只存 \(1/t\)。这就是 Megatron-LM 论文的核心构造。

现代 LLM 多用 SwiGLU:\(Y = \big(\mathrm{SiLU}(XW_\text{gate})\odot XW_\text{up}\big)W_\text{down}\)。做法完全一样:gate 和 up 两个矩阵都按列切(同一个 \(F\) 维的同一段,通常把二者拼成一个 \([h, 2F/t]\) 的矩阵一次乘完),逐元素的 SiLU 和乘法在本地做,down 按行切。唯一要小心的是:拼接 gate/up 时要保证每张卡拿到的 gate 列和 up 列对应同一段 \(F\) 下标,否则逐元素乘就乘错了对象。

X f X·A₁ → GeLU [N, F/2] X·A₂ → GeLU [N, F/2] Z₁·B₁ [N, h] 部分和 Z₂·B₂ [N, h] 部分和 g Y f 前向:恒等 反向:All-Reduce g 前向:All-Reduce 反向:恒等 A 列并行(上 GPU 0,下 GPU 1) B 行并行 → Dropout / 残差
两路 TP 的 MLP。f 和 g 是一对共轭算子:一个前向恒等、反向 All-Reduce,另一个反过来。
同一个 MLP 的前向在各卡上的数据流。方块的竖边是 token 维 \(N\),横边是特征维;\(A\) 按列切成 \(A_i\)(\([h, F/t]\)),\(B\) 按行切成 \(B_i\)(\([F/t, h]\)),斜线方块是完整形状的部分和,All-Reduce 把它们加起来再发回每张卡。

反过来「先行后列」行不行?行,但更差:第一个矩阵按行切需要输入沿 \(h\) 切开,输出是部分和,必须在 GeLU 前 All-Reduce;第二个矩阵按列切,输出又是切开的,要拼回完整 \(Y\) 还要 All-Gather。两次通信,而且中间那次 All-Reduce 的张量是 \([N, F]\),比 \([N, h]\) 大好几倍。

注意力:按头切,以及 GQA 的约束

多头注意力天然有一个互相独立的维度:头。第 \(j\) 个头只用 \(Q\)、\(K\)、\(V\) 里属于它的那 \(d_h\) 列,算出自己的 \([N, d_h]\) 输出,和别的头没有关系,直到输出投影 \(W_O\) 把所有头拼起来再乘一个 \([h, h]\) 矩阵。于是:

  1. \(W_Q, W_K, W_V\) 按列切,并且切的边界对齐头的边界:每张卡拿 \(n_h/t\) 个完整的 query 头、\(n_{kv}/t\) 个完整的 KV 头。这是列并行,前向不通信。
  2. RoPE 是在每个头内部对 \(d_h\) 维做旋转,本地做;\(\mathrm{softmax}(QK^\top/\sqrt{d_h})V\) 对每个头独立,本地做(FlashAttention 照常用)。
  3. 拼起来的注意力输出在每张卡上是 \([N, h/t]\),正好是沿 \(h\) 维切开的一块,所以 \(W_O\) 按行切(\([h/t, h]\)),得到部分和,再 All-Reduce。

结构和 MLP 一模一样:入口一个 f,出口一个 g。所以一层 Transformer(注意力 + MLP)前向共有 2 次 All-Reduce,反向也是 2 次。

几个实现细节:

GQA:KV 头比 query 头少

GQA(Grouped-Query Attention)里 \(n_{kv}\lt n_h\),每 \(n_h/n_{kv}\) 个 query 头共享一组 K/V。切的时候要让一组 query 头和它们共享的那个 KV 头在同一张卡上:

Llama 3 系列的 8B / 70B / 405B 都是 8 个 KV 头,所以 TP=8 正好每卡一个 KV 头。Llama-3 70B(\(h=8192\),\(n_h=64\),\(n_{kv}=8\),\(d_h=128\),\(F=28672\),\(V=128256\))在 TP=8 下,每张卡上的权重形状如下:

权重完整形状切法每卡形状(t=8)
\(W_{QKV}\)[8192, (64+16)·128] = [8192, 10240]列(按头)[8192, (8+2)·128] = [8192, 1280]
\(W_O\)[8192, 8192]行[1024, 8192]
\(W_\text{gate}, W_\text{up}\)[8192, 28672] ×2列[8192, 3584] ×2
\(W_\text{down}\)[28672, 8192]行[3584, 8192]
RMSNorm 权重[8192] ×2复制[8192] ×2
Embedding / LM head[128256, 8192]词表维[16032, 8192]

每层每卡的参数从 8.56 亿降到约 1.07 亿。整个模型约 706 亿参数,每卡约 88 亿。

f 和 g:前向与反向到底发生什么

Megatron-LM 把上面的通信封装成两个 autograd 算子:

它们是一对共轭(conjugate):一个在前向通信,另一个在反向通信。你在别处也会看到 g 被写成 \(f^*\) 或 \(\bar f\),指的是同一个算子。用 PyTorch 写出来只有几行:

class CopyToTP(torch.autograd.Function):        # f
    @staticmethod
    def forward(ctx, x):  return x
    @staticmethod
    def backward(ctx, dx):
        dist.all_reduce(dx, group=tp_group); return dx

class ReduceFromTP(torch.autograd.Function):    # g
    @staticmethod
    def forward(ctx, x):
        dist.all_reduce(x, group=tp_group); return x
    @staticmethod
    def backward(ctx, dy): return dy

为什么 f 的反向必须 All-Reduce?f 的输出 \(X\) 被送进各卡的列并行层,每张卡只沿着自己那几列反传,所以每张卡算出的 \(dX\) 只包含「通过自己那部分列」的梯度贡献。\(X\) 真正的梯度是所有路径之和。如果不加起来,各卡的 \(dX\) 不一样,再往前传到 LayerNorm、残差、embedding 就会分叉,各卡上「同一份」复制参数的梯度也会不一样。

复制区域里的 LayerNorm 和 Dropout

在纯 TP 里,g 之后到下一个 f 之前(残差相加、Dropout、LayerNorm/RMSNorm)是「复制区域」:每张卡对完全相同的完整张量做相同的计算。Megatron 的选择是让每张卡都重复算一遍,而不是一张卡算完再广播,因为这些操作很便宜。

纯 TP 的问题也在这里:复制区域里的激活是完整的 \([b, s, h]\),每张卡都存一份,没有被 \(t\) 除。序列并行要解决的就是这部分。

词表并行:embedding 与交叉熵

词表很大(Llama 3 是 128256),embedding 表 \([V, h]\) 和输出层(LM head)\([h, V]\) 加起来是一笔不小的参数:Llama-3 8B 的两张表合计约 10.5 亿参数,占全模型的 13%;1B 量级的小模型里这个比例能超过 1/4。而且 logits \([b, s, V]\) 是整个网络里最大的单个激活。所以它们也要切,切在词表维上。

输入 embedding

第 \(i\) 张卡持有词表下标 \([iV/t, (i+1)V/t)\) 这一段行。查表时,每张卡对每个 token 判断:在自己的范围内就查出那一行,不在就输出全零向量。这样每张卡得到一个 \([b, s, h]\) 的张量,每个 token 只有一张卡给出了非零值。All-Reduce(求和)之后,每张卡都拿到完整的 embedding 输出。从矩阵角度看,查表等于 one-hot 矩阵 \([N, V]\) 乘 embedding 表 \([V, h]\),切的是收缩维 \(V\),所以它是一个行并行层,出口跟一个 g。

LM head 与交叉熵

输出层 \(W_\text{out}\) \([h, V]\) 按列(词表维)切,是列并行:每张卡算出 logits 的一段 \(z^{(i)}\in\mathbb{R}^{N\times V/t}\)。朴素做法是 All-Gather 出完整 logits 再算 softmax,但这要传 \(b\cdot s\cdot V\) 个元素,非常大。Megatron 的做法是把交叉熵融合进来,只传标量。对一个 token,设目标词是 \(y\):

$$\ell = -z_y + \log\sum_{v=1}^{V} e^{z_v} = -z_y + m + \log\sum_{v} e^{z_v - m},\quad m=\max_v z_v$$

每一项都可以分到各卡上算:

  1. 每张卡算本地最大值 \(m_i=\max_{v\in\text{本卡}} z_v\),All-Reduce(MAX) 得到全局 \(m\)。(减最大值只是为了数值稳定,不影响结果。)
  2. 每张卡算 \(S_i = \sum_{v\in\text{本卡}} e^{z_v-m}\),All-Reduce(SUM) 得到 \(S\)。
  3. 目标词 \(y\) 只落在一张卡的范围内,那张卡取出 \(z_y\),其他卡给 0,All-Reduce(SUM) 得到 \(z_y\)。
  4. \(\ell = \log S + m - z_y\),每张卡都能算出同样的 loss。

三次 All-Reduce 的张量都是 \([b, s]\),相比 \([b, s, V]\) 小了 \(V\) 倍。反向也是本地的:\(\partial\ell/\partial z_v = \mathrm{softmax}(z)_v - \mathbb{1}[v=y]\),每张卡用全局的 \(m\) 和 \(S\) 就能算出自己那段的 softmax,再在自己负责的目标位置减 1。然后列并行的反向(f)把 \(dX\) All-Reduce。

还有两个小细节。第一,词表大小要能被 \(t\) 整除,而且每卡的那一段最好是 128 的倍数,GEMM 才高效:Megatron-LM 论文把 GPT-2 的 50257 补到了 51200(\(128\times 8 = 1024\) 的倍数)。补出来的行永远不会作为目标出现,相当于浪费一点点算力。第二,如果输入和输出 embedding 共享权重(tied),两边的切法要一致,梯度在同一张卡上相加即可。

序列并行:把剩下的激活也切开

纯 TP 的激活里有多少没被切开?对 GPT 结构的一层逐项记账(bf16,不做重计算),每层要存的激活字节数是:

$$\text{无并行:}\; sbh\left(34 + 5\frac{a s}{h}\right)\qquad \text{TP:}\; sbh\left(10 + \frac{24}{t} + 5\frac{a s}{h t}\right)$$

这里 \(a\) 是头数。\(5as/h\) 那项来自 \(QK^\top\)、softmax、dropout 这些 \(s^2\) 大小的张量(用了 FlashAttention 就不存了)。关键是那个 \(10\):它来自两个 LayerNorm 的输入(各 \(2sbh\))、两个 dropout mask(各 \(sbh\))以及注意力和 MLP 入口线性层的输入(各 \(2sbh\)),这些都在复制区域里,不随 \(t\) 缩小。\(t=8\) 时,\(24/8=3\),复制部分 10 反而是大头。

观察:复制区域里的操作(LayerNorm、Dropout、残差相加)对每个 token 都是独立的。LayerNorm 要在 \(h\) 维上算均值方差,所以不能沿 \(h\) 切;但沿序列维 \(s\) 切完全没问题:

$$\mathrm{LayerNorm}(x) = \gamma\odot\frac{x-\mu}{\sqrt{\sigma^2+\epsilon}}+\beta,\quad \mu,\sigma^2 \text{ 只在单个 token 的 } h \text{ 维上计算}$$

于是序列并行:在复制区域里,第 \(i\) 张卡只保存和处理 \([b, s/t, h]\) 这一段 token。问题变成两个区域之间怎么转换:

注意这里的 \(g\) 是一个新算子,和纯 TP 里那个「前向 All-Reduce」的 g 不是一回事。可以这样记:SP 把纯 TP 入口的 f 换成了 \(g\)(All-Gather),把出口的 g 换成了 \(\bar g\)(Reduce-Scatter);两者仍然是一对共轭,一个的前向是另一个的反向。

SP 区域:沿 s 切 TP 区域:沿 h / F / 头切 SP 区域 X 分片 [b,s/t,h] LayerNorm [b,s/t,h] g AG / 反向 RS 列并行 → [b,s,F/t] 激活 行并行 [b,s,h] 部分和 ḡ RS / 反向 AG 残差/Dropout [b,s/t,h] 任何时刻,每张卡上的激活要么沿 s 切,要么沿 h(或 F、头)切:最大激活从 bsh 降到 bsh/t
TP+SP 下 MLP 子层的数据流(注意力子层同理)。All-Reduce 被拆成了 Reduce-Scatter 和 All-Gather,分别放在出口和下一个入口。
同一个 MLP 在 TP+SP 下的前向。进来时每卡只有一段 token(横条),All-Gather 沿序列维拼回完整的 \(X\);中间和纯 TP 完全一样;出口的 Reduce-Scatter 把每张卡部分和的第 \(q\) 段都送到 GPU \(q\) 相加,每卡又只剩一段。

按区域把激活形状列成表(\(h\) 列、\(s\) 列分别表示这一维是完整还是被切开):

区域纯 TPTP + SP
进入 TP(列并行之前)h 完整,s 完整(f:前向无操作)h 完整,s 由 All-Gather 拼回完整
TP 区域内部h 切开,s 完整h 切开,s 完整
离开 TP(行并行之后)h 完整,s 完整(All-Reduce 求和)h 完整,s 由 Reduce-Scatter 切开
SP / 复制区域h 完整,s 完整(每卡复制)h 完整,s 切开
输入 embedding(词表并行)All-Reduce 后 h、s 都完整Reduce-Scatter 后 s 切开

通信量不变

纯 TP 每层前向 2 次 All-Reduce;TP+SP 每层前向 2 次 All-Gather + 2 次 Reduce-Scatter。操作次数翻倍,但环形 All-Reduce 本来就等于一次 Reduce-Scatter 加一次 All-Gather,所以字节数完全一样:每层前向每卡发送 \(4\cdot\frac{t-1}{t}\cdot 2bsh\) 字节。反向是对偶的,同样不变。实际跑起来,SP 还让一层的前向略快一点:在一个 22B GPT 模型上,单层前向从 7.7 ms 降到 7.2 ms,因为 LayerNorm 和 Dropout 只处理 \(1/t\) 的数据。这个收益被另一件事抵消了一部分:同样的数据量,RS + AG 两次调用比一次 All-Reduce 要慢一些。

SP 带来的两个新细节

显存

有了 SP,每层激活里不再有任何复制项:

$$\text{TP+SP:}\; \frac{sbh}{t}\left(34 + 5\frac{a s}{h}\right)$$

每个张量都被 \(t\) 除。再配合选择性重计算(只重算注意力里那些 \(s^2\) 大小、FLOPs 很少的部分;FlashAttention 本身就是这样做的),每层降到 \(34\,sbh/t\)。在 22B 到 1T 参数的 GPT 模型上,序列并行和选择性重计算各自把激活显存减少约一半,合起来约 5 倍。

演示:一层 Transformer 里的 TP / TP+SP

一层 Llama 风格的 Transformer(RMSNorm → 注意力 → 残差 → RMSNorm → SwiGLU MLP → 残差),拆成一步一步的算子。每个小方块代表一张卡上的激活张量:方块的竖边是序列维 \(s\),横边是当前的特征维(\(h\)、\(F\) 或头);实心部分是这张卡实际持有的那块,斜线表示「完整形状但只是部分和」。通信步骤用橙色标出,并给出每张卡要发送的字节数(环形算法)。

TP 块动画
模式 方向 TP 度 t
    本卡持有(切开的一块) 本卡持有完整副本 完整形状的部分和
    本层本方向:通信次数0
    累计每卡发送0
    当前最大激活(每卡)0

    可以试试:在纯 TP 下数前向的通信,是 2 次 All-Reduce;切到 TP+SP,变成 2 次 All-Gather + 2 次 Reduce-Scatter,走完一层后「累计每卡发送」和纯 TP 一样。再看「当前最大激活」:纯 TP 在复制区域里每卡要放完整的 \([b,s,h]\),TP+SP 里永远只有 \(1/t\)。反向模式下,TP+SP 多出两步「重新 All-Gather 输入」,那就是拿通信换显存的那次 All-Gather。

    精确的通信量

    把一层的通信加起来(每卡发送字节数,环形算法,bf16,\(M = 2bsh\) 是一个 \([b,s,h]\) 激活的字节数):

    前向反向每层合计
    纯 TP2 × AR = \(4\frac{t-1}{t}M\)2 × AR = \(4\frac{t-1}{t}M\)\(8\frac{t-1}{t}M\)
    TP + SP2 AG + 2 RS = \(4\frac{t-1}{t}M\)2 AG + 2 RS = \(4\frac{t-1}{t}M\)(+ 2 次重收集 AG \(2\frac{t-1}{t}M\))\(8\frac{t-1}{t}M\)(\(10\frac{t-1}{t}M\))

    算一个具体的例子:Llama-3 70B,\(b=1\),\(s=4096\),TP=8。\(M=2\times 4096\times 8192\) 字节 = 64 MiB。一次 All-Reduce 每卡发送 \(2\times\frac{7}{8}\times 64 = 112\) MiB,每层前向 + 反向 4 次就是 448 MiB,80 层合计 35 GiB,这只是一条 4K 序列。如果按 H100 NVLink 的 450 GB/s 算,每层约 1.04 ms。

    同一层的计算量:每层参数约 8.56 亿,前向 FLOPs 约 \(2\cdot bs\cdot 8.56\times10^8 \approx 7.0\) TFLOP,加上因果注意力的 \(2bs^2h\approx 0.27\) TFLOP,约 7.3 TFLOP;前向 + 反向约 3 倍,再除以 8 张卡,每卡 2.73 TFLOP,按 H100 的 990 TFLOP/s 峰值是 2.76 ms。通信 / 计算 ≈ 0.38。看起来通信不到计算的一半,但要注意三点:一是计算按 100% 峰值算,实际 MFU 在 40%–50% 时计算时间会长一倍,比例反而更好看,但 NVLink 实际也跑不满 450 GB/s:8×H100 上的 All-Reduce 在大消息下约 370 GB/s,58 MB 量级的消息(正好是 70B 一个 MLP 权重分片的大小)只有约 150 GB/s;二是 TP 的通信很难和计算重叠(下面会讲),所以这 38% 大部分是直接加在关键路径上的;三是 \(s\) 和 \(b\) 不影响这个比例(通信和计算都正比于 \(bs\),注意力项除外),它只取决于 \(h\)、\(F\) 和 \(t\)。

    再细看一个线性层:能不能把下一层的 All-Gather(每卡 \(\frac{t-1}{t}\cdot bsh\) 个元素)藏在当前 \([h,h]\) 线性层的计算(\(2bsh^2/t\) FLOPs)后面?两者的时间之比要满足

    $$\frac{t_\text{comm}}{t_\text{compute}} = \frac{t-1}{2h}\cdot\frac{\text{peak\_flops}}{\text{peak\_bw}} \le 1$$

    同样和 \(s\)、\(b\) 无关。对 H100(990 TFLOP/s,450 GB/s,按元素计算时带宽要再除以 2 字节)、\(h=8192\)、\(t=8\),左边约 \(\frac{7}{16384}\times\frac{990\times10^{12}}{225\times10^{9}}\approx 1.9\),也就是单个 \([h,h]\) 层的计算不足以盖住通信;MLP 的 \([h,F]\) 层更宽,情况好一些。

    显存账

    TP 切的东西有两类。

    模型状态:除了 RMSNorm 权重(每层 \(2h\) 个,可以忽略),所有参数都被切成 \(1/t\),梯度和优化器状态跟着参数走。混合精度 + Adam 下每个参数需要 bf16 权重 2 字节、bf16 梯度 2 字节、fp32 主权重 4 字节、Adam 一阶和二阶矩各 4 字节,共 16 字节。Llama-3 70B 是 706 亿参数 × 16 = 1129 GB,TP=8 后每卡 141 GB,仍然超过 80 GB,所以 70B 光靠 TP=8 不够,还要把优化器状态用 ZeRO 在数据并行维度上切(或者加流水线并行)。

    激活:按 Llama 结构(RMSNorm、GQA、SwiGLU、FlashAttention、无 dropout)重新记账,每层每条序列存下的张量(bf16):

    合计每层 \(12sbh + 4sb\,n_{kv}d_h + 6sbF\) 字节。其中两个 RMSNorm 输入和两个投影输入(\(8sbh\))在纯 TP 下是复制的,其余被 \(t\) 除;TP+SP 下全部被 \(t\) 除(投影输入只存本卡那段,反向时再 All-Gather)。全重计算时每层只存层输入 \(2sbh\)(SP 下也是 \(/t\))。最后的 logits 按 fp32 存,\(4sbV/t\)。不同框架具体存哪些张量略有差别(比如有的实现会在 SwiGLU 反向时重算 SiLU),但量级和随 \(t\) 的变化趋势是一样的。

    用这个模型,Llama-3 70B、\(s=4096\)、\(b=1\)、TP=8:纯 TP 每层 0.35 GiB,80 层 28 GiB;TP+SP 每层 0.13 GiB,80 层 10.5 GiB。SP 在这里省下了 17 GiB。

    为什么 TP 留在节点内

    TP 的通信有三个特点,让它对带宽和延迟都极其敏感:

    1. 在关键路径上。g 的 All-Reduce 不完成,下一个 LayerNorm 就没法开始;列并行层的输入不 All-Gather 完,矩阵乘法就没法开始。数据并行的梯度 All-Reduce 可以和反向传播的其他层重叠,ZeRO-3 的参数 All-Gather 可以提前预取,但 TP 的通信依赖于刚刚算出来的激活,能重叠的空间很小。
    2. 频繁。每层前向 2 次、反向 2 次(SP 下翻倍成 4+4 次更小的操作),一个 80 层的模型每个 micro-batch 有几百次集合通信。
    3. 通信量不随 \(t\) 缩小。每卡发送 \(\propto\frac{t-1}{t}M\),\(t\) 越大越接近 \(M\);而每卡的计算量是 \(\propto 1/t\)。所以 \(t\) 翻倍,计算减半,通信几乎不变,通信占比翻倍。

    第三点可以写成一个干净的上界。我们用一个只有两个矩阵的 MLP 来建模:前向计算 \(T_\text{math} = \frac{4BDF}{Y\cdot C}\)(\(B\) 是 token 数,\(Y\) 是 TP 度,\(C\) 是单卡算力),通信是一次 All-Gather 加一次 Reduce-Scatter,\(T_\text{comms}\approx\frac{2\cdot 2\cdot BD}{W}\)(bf16 两字节,\(W\) 是单卡带宽)。要计算受限,需要

    $$\frac{4BDF}{Y C} \gt \frac{4BD}{W}\quad\Longleftrightarrow\quad Y \lt \frac{F\cdot W}{C}$$

    \(B\) 和 \(D\) 都消掉了:TP 能做多大只取决于 MLP 宽度 \(F\) 和硬件的「算力 / 带宽」比 \(C/W\)。对 H100,节点内 \(C/W = 990\times10^{12}/450\times10^{9}\approx 2200\)。Llama-3 70B 的 \(F=28672\),节点内上界约 \(28672/2200 \approx 13\),而一个节点只有 8 张卡,所以就是 8。出了节点情况要糟得多:一个 8 卡节点对外总共只有 400 GB/s(8 张 400 Gbps 网卡),由 8 张卡共享,摊到每张卡约 50 GB/s,比 NVLink 慢近一个数量级。即便用分层的集合通信把整个节点的出口带宽都用上,\(C/W\) 也变成 \(990/400\approx 2475\),TP 在 \(Y \gt F/2475\) 时就会通信受限。所以 TP 通常限制在一个节点内,最多跨两个节点。

    在 H100 集群上实测(例如一个 3B 模型、序列长度 4096,扫 TP 度)也是这个形状:单卡吞吐随 TP 增大而下降,从 TP=8 到 TP=16(从 NVLink 变成跨节点的 EFA 网络)掉得最明显,到 TP=32 掉得更厉害;而另一方面,TP 越大,能放下的 batch 越大(激活被切得更碎)。这是一个算得快和放得下之间的取舍。

    Blackwell 一代的 GB200 NVL72 把 NVLink 域扩到 72 张卡(每卡 900 GB/s),「节点」这个边界就从 8 挪到了 72。上面的不等式没变,只是 \(W\) 在更大的范围内都很高。即便如此,第三点(通信占比随 \(t\) 线性增长)依然成立,所以 TP 度也不会无限增大。

    演示:显存与通信随 TP 度的变化

    选一个模型和训练配置,看每张卡的显存和「通信时间 / 计算时间」怎么随 TP 度变化。显存按上一节的记账模型;时间按:每层前向 + 反向每卡发送 \(8\frac{t-1}{t}M\) 字节(SP 时加上重收集的 \(2\frac{t-1}{t}M\)),除以带宽(\(t\) 不超过 NVLink 域时用 NVLink 带宽,否则用每卡跨节点带宽);计算按 \(3\times\)前向 FLOPs \(/t\) 除以峰值算力。带宽和算力都按标称峰值算,实际能跑到的更低,所以这里的比例用来看趋势,不是性能预测。

    每卡显存 / 通信计算比 vs TP 度
    序列并行 重计算 ZeRO
    每卡显存(GB)
    堆叠:■ 模型状态 / ■ 层激活 / ■ logits;虚线是 HBM 容量,超出纵轴的柱子顶端标出总值。点击选中一个 TP 度。
    每层通信时间 / 计算时间(理想峰值)
    对数纵轴。高于 1 意味着即使完美重叠,通信也会成为瓶颈;● NVLink 域内,● 跨节点。
    选中 TP 度8
    模型状态 / 卡
    激活 + logits / 卡
    每层每卡通信
    通信 / 计算
    上界 F·W/C

    几个值得拖一拖的现象:把 \(s\) 拉到 64K 以上,就算 TP=8 加 SP,激活也会撑爆显存,这时需要的是上下文并行,而不是更大的 TP;关掉 SP,TP 增大时激活几乎降不下去(复制项不随 \(t\) 缩小);8B 模型的 \(F=14336\),节点内 TP 上界只有约 6.5,所以小模型用 TP 往往得不偿失;切到 NVL72,TP=16、32 的柱子不再撞上跨节点的带宽墙,但比例仍然随 \(t\) 线性增长。

    把通信藏到计算后面

    TP 的通信难以重叠,但不是完全不能。常见的几种手段:

    这些都需要通信和计算在 GPU 上并发执行,会互相抢 SM 和显存带宽,所以实际收益通常小于理论值。

    TP 与 FSDP 混合:什么时候各用多少

    FSDP 移动权重,TP 移动激活。FSDP 每层要 All-Gather 一次权重(\(\propto DF\)),TP 每层要 All-Gather / Reduce-Scatter 一次激活(\(\propto BD\))。批次越小,激活越小,TP 越便宜;模型越宽,权重越大,FSDP 越贵。两者可以叠加在不同的设备轴上:\(X\) 个设备做 FSDP,\(Y\) 个做 TP,\(N = XY\):

    $$\mathrm{In}[B_X, D_Y]\cdot_D W_\text{in}[D_X, F_Y]\cdot_F W_\text{out}[F_Y, D_X]\to \mathrm{Out}[B_X, D_Y]$$

    好处是相互的:激活沿 batch 切在 \(X\) 上,所以 TP 的 All-Gather 小了 \(X\) 倍;权重沿 \(F\) 切在 \(Y\) 上,所以 FSDP 的 All-Gather 小了 \(Y\) 倍。只看前向,通信时间分别是

    $$T_\text{FSDP} = \frac{4DF}{Y\, W M_X},\qquad T_\text{TP} = \frac{4BD}{X\, W M_Y},\qquad T_\text{math} = \frac{4BDF}{N C}$$

    \(M_X\)、\(M_Y\) 是两个轴各占用了几个网络维度(TPU 的 3D 环面上两者之和约为 3;GPU 上可以理解为带宽倍数)。\(T_\text{FSDP}\) 随 \(X\) 增大,\(T_\text{TP}\) 随 \(X\) 减小,两者相等时总通信(取 max)最小:

    $$X_\text{opt} = \sqrt{\frac{B}{F}\frac{M_X}{M_Y}N}$$

    代回去,计算受限的条件变成

    $$\frac{B}{N} \gt \frac{\alpha^2}{M_X M_Y F},\qquad \alpha = C/W$$

    对比纯 FSDP 的条件 \(B/N \gt \alpha/M_X\):混合以后,每卡需要的 token 数从和 \(\alpha\) 成正比变成和 \(\alpha^2/F\) 成正比。代入一组数:TPU v5p 上 \(\alpha\approx 2550\),取 \(F=32768\)、\(M_XM_Y=2\),每芯片约 100 个 token 就够;纯 FSDP(三个网络维度全用上)需要约 850,混合后小了约 8 倍。具体例子:在一个 8960 芯片的 TPU v5p pod 上用 400 万 token 的 batch 训练 LLaMA-3 70B,每芯片只分到约 470 个 token,纯 FSDP 是通信受限的;改成约 2048 路 FSDP + 4 路 TP,就能保持计算受限。

    这件事在 GPU 上的含义是:当集群很大、全局 batch 被分到每卡只剩几千个 token 时(H100 上跨节点做纯 FSDP,按 \(C/W\approx 2475\) 需要每卡约 2500 个 token),加一点 TP 能把每卡需要的 batch 降下来,这是 TP 除了「省显存」之外的第二个用处。

    实践清单

    参考与延伸阅读