把 Llama-3 70B 的训练序列拉到 128K,即便开了 TP=8 加序列并行,每张卡每层仍要存约 4.2 GiB 激活,80 层合计约 335 GiB。张量并行加序列并行把一层里的激活要么沿 hidden 维切、要么沿序列维切,最大激活降到了 \(bsh/t\)。但它有一个边界:TP 区域内部,每张卡上的张量仍然是完整的序列长度,比如 \([b, s, F/t]\);而 TP 度又被 NVLink 域卡在 8 左右。序列一长(128K、1M),这点切分就不够了。
上下文并行(Context Parallelism,CP)的做法是:把序列切开,并且在整个模型里都保持切开,包括注意力。MLP、LayerNorm 这些逐 token 的操作不受影响;唯一的麻烦是注意力,因为每个 query 要看到(因果情况下:之前的)所有 key 和 value。
激活随序列长度增长
训练时,反向传播要用到前向的中间结果,所以每一层都要存下一批张量。对 GPT 结构的一层逐项记账(bf16,不做重计算,\(a\) 为头数),每层要存的字节数是:
$$m_\text{act}^{\text{层}} = sbh\left(34 + 5\frac{as}{h}\right)$$第一项正比于 \(s\),来自线性层输入、LayerNorm 输入、MLP 中间激活等;第二项正比于 \(s^2\),来自注意力分数矩阵 \(QK^\top\)、softmax 输出和它的 dropout mask,每个头都是一个 \(s\times s\) 的矩阵。拿 Llama 系列的尺寸代进去算一下:序列只有一两千 token 时,激活和模型状态比起来可以忽略;到 2K–4K 以后激活开始占大头,而且因为有 \(s^2\) 项,增长得越来越快。
FlashAttention 不存 \(s\times s\) 的矩阵,只存 \(Q,K,V,O\) 和每行的 log-sum-exp,反向时分块重算分数矩阵。这样第二项就没了,激活变成和 \(s\) 线性增长。线性听起来还好,但数字仍然很大。按 Llama 结构(RMSNorm、GQA、SwiGLU、FlashAttention)记账,每层每条序列存 \(12sbh + 4sb\,n_{kv}d_h + 6sbF\) 字节(推导见张量并行的显存账)。拿 Llama-3 70B(\(h=8192\),\(F=28672\),8 个 KV 头)在 \(s=128\text{K}\)、TP=8 + SP 下算一下:
- 每层:\(s\,(12h + 4n_{kv}d_h + 6F)/t\),代入得 131072 × 34304 字节 ≈ 4.19 GiB,80 层合计约 335 GiB。
- 就算做全层重计算,每层只存层输入 \(2sbh/t\),80 层也要 20 GiB;反向重算某一层时还要临时放下这一层的 4.19 GiB;fp32 logits \(4sbV/t\) 再加 7.8 GiB。
而模型状态本身(706 亿参数,每卡 1/8 加上 ZeRO 切分)也要占几十 GB。这说明:长序列下,重计算最多只能把激活压到「和 \(s\) 线性、系数是 \(2h\cdot L\)」的水平,而且全层重计算要把每层的前向再跑一遍,前向约占一步训练计算量的 1/3,所以总计算多出约 1/3。剩下唯一的办法是把 \(s\) 本身分到更多卡上。
上下文并行的基本想法
CP 把输入序列沿 \(s\) 维切成 \(N\) 段(\(N\) 是 CP 度),第 \(i\) 张卡只处理第 \(i\) 段 token 的所有层。和 SP 的区别是:SP 只在 LayerNorm/Dropout 这些区域切序列,进入 TP 区域时要 All-Gather 回完整序列;CP 则从 embedding 到 loss 一直切着。
- 逐 token 的模块(embedding、RMSNorm、MLP、QKV 投影、输出投影、LM head、交叉熵):每个 token 的计算只依赖它自己,所以每张卡用完整的权重处理自己的 \(s/N\) 个 token,前向不需要任何通信。这些模块的激活自然都缩小到 \(1/N\)。
- 权重是复制的。CP 不切权重,每张卡都有完整的(或 TP 切开的那一份)参数。每张卡用不同的 token 算出不同的梯度,所以反向结束后要在 CP 组内 All-Reduce 梯度,和数据并行完全一样。实践中 CP 组和 DP 组合在一起做一次梯度同步(梯度 All-Reduce 的组是 DP×CP)。
- 注意力是唯一的例外:第 \(i\) 段的 query 需要和第 \(1..i\) 段(因果)或所有段(双向)的 key/value 交互。
- loss:每张卡算出自己那段 token 的 loss 之和,归一化时要用全局 token 数(所有 CP rank 的有效 token 加起来),否则 padding 或不同长度会让平均值偏掉。
所以 CP 的全部难点就是:怎么在 query、key、value 都分散在不同卡上的情况下,精确地算出注意力。
分块注意力:online softmax 与 log-sum-exp 合并
对一个 query 向量 \(q\)(省略头的下标),注意力输出是
$$o = \sum_{j} \frac{e^{x_j}}{\sum_{k} e^{x_k}}\, v_j,\qquad x_j = \frac{q\cdot k_j}{\sqrt{d_h}}$$softmax 的分母要对所有 key 求和,看起来必须先拿到全部 key 才能开始。但其实可以先对一部分 key 算一个「局部结果」,再把局部结果合并。设 key 被分成两块 \(\mathcal{B}_1, \mathcal{B}_2\)。对每一块定义:
$$m_b = \max_{j\in\mathcal{B}_b} x_j,\qquad \ell_b = \sum_{j\in\mathcal{B}_b} e^{x_j - m_b},\qquad o_b = \frac{1}{\ell_b}\sum_{j\in\mathcal{B}_b} e^{x_j-m_b}\,v_j$$\(o_b\) 就是「假如只有这一块 key」时的注意力输出,\(m_b\) 是为了数值稳定减掉的最大值。把 \(m_b\) 和 \(\ell_b\) 合成一个数更方便:
$$\mathrm{lse}_b = \log\sum_{j\in\mathcal{B}_b} e^{x_j} = m_b + \log \ell_b$$这就是这一块的 log-sum-exp。现在推导完整结果。完整的分母是
$$\sum_{j} e^{x_j} = \sum_{j\in\mathcal{B}_1} e^{x_j} + \sum_{j\in\mathcal{B}_2} e^{x_j} = e^{\mathrm{lse}_1} + e^{\mathrm{lse}_2}$$所以全局 \(\mathrm{lse} = \log\left(e^{\mathrm{lse}_1}+e^{\mathrm{lse}_2}\right)\)。完整的分子是
$$\sum_j e^{x_j} v_j = \sum_{j\in\mathcal{B}_1} e^{x_j} v_j + \sum_{j\in\mathcal{B}_2} e^{x_j} v_j = e^{\mathrm{lse}_1} o_1 + e^{\mathrm{lse}_2} o_2$$(因为 \(o_b = \sum_{j\in\mathcal{B}_b} e^{x_j}v_j / e^{\mathrm{lse}_b}\)。)两式相除:
$$\boxed{\;o = e^{\mathrm{lse}_1-\mathrm{lse}}\,o_1 + e^{\mathrm{lse}_2-\mathrm{lse}}\,o_2,\qquad \mathrm{lse}=\log\left(e^{\mathrm{lse}_1}+e^{\mathrm{lse}_2}\right)\;}$$两个权重 \(e^{\mathrm{lse}_b - \mathrm{lse}}\) 之和恰好是 1,可以理解成「这一块 key 在总 softmax 质量中所占的比例」。实际计算时用稳定的写法:\(\mathrm{lse} = M + \log(e^{\mathrm{lse}_1-M}+e^{\mathrm{lse}_2-M})\),\(M=\max(\mathrm{lse}_1,\mathrm{lse}_2)\),所有指数都不超过 0。几个性质让它适合分布式:
- 可交换、可结合:多块就反复两两合并,顺序无关。Ring Attention 正是建立在这一点上:一个 query 块对一组 KV 块的注意力可以按任意顺序计算,只要正确合并每块的统计量。
- 每个 query 只需要多存一个标量(lse),而不是 \(s\) 个分数。
- 完全被掩码的块:所有 \(x_j=-\infty\),\(\mathrm{lse}_b = -\infty\),权重为 0,等价于跳过。所以因果掩码下上三角的整块可以直接不算。
FlashAttention 在单卡内部用的是同一个恒等式(在 SRAM 里按块扫过 K/V,边扫边更新 \(m\)、\(\ell\) 和未归一化的输出),只是把合并写成了增量形式。CP 把它搬到了卡与卡之间:每张卡用 FlashAttention 算出本地 Q 对某个 KV 块的 \((o_b, \mathrm{lse}_b)\),再和之前的累积结果合并。
反向同样只需要 lse。对一个 key \(j\),softmax 概率是 \(p_j = e^{x_j - \mathrm{lse}}\),用全局的 lse 就能在任何一块上单独重算出正确的 \(p_j\),然后算 \(dv_j = \sum_q p_j\, do\)、\(dx_j = p_j(do\cdot v_j - D)\),其中 \(D = do\cdot o\) 是每个 query 一个标量(也是全局的)。所以前向结束时每张卡把本地 query 的全局 \(o\) 和 lse 存下来,反向时再把 KV 块转一圈,每一步都能独立算出这一块对 \(dQ\)(累加在本地)以及对 \(dK, dV\) 的贡献。
Ring Attention
有了合并规则,算法就很自然了。\(N\) 张卡排成一个环,卡 \(i\) 一开始持有第 \(i\) 段的 \(Q_i, K_i, V_i\)。一共 \(N\) 步,每一步每张卡做三件事:
- 把手里当前的 K/V 块异步发给下一张卡(最后一步不用发);
- 用本地的 \(Q_i\) 和手里当前的 K/V 块算局部注意力 \((o, \mathrm{lse})\),合并进累积结果;
- 等上一张卡发来的 K/V 块到达,作为下一步的「当前块」。
第 \(r\) 步(从 0 开始),卡 \(i\) 手里的是卡 \((i-r)\bmod N\) 原来的 K/V。\(N\) 步之后,每个 \(Q_i\) 都见过了全部 K/V,Q 和输出 O 自始至终没离开过本卡。
def ring_attention_fwd(q, k, v, rank, N): # q,k,v: 本卡的 [s/N, heads, d]
out, lse = None, None
for r in range(N):
if r < N - 1:
nxt = async_sendrecv(k, v, to=(rank+1) % N, frm=(rank-1) % N)
src = (rank - r) % N # 当前 k,v 原属于哪张卡
o_b, lse_b = flash_attn(q, k, v, mask=causal_mask(rank, src))
out, lse = merge(out, lse, o_b, lse_b) # 上一节的合并公式
if r < N - 1:
k, v = nxt.wait()
return out, lse
显存
每张卡在注意力里只需要:本地 Q(和累积的 O、lse)、当前计算用的 K/V 块、正在接收的下一个 K/V 块(双缓冲)。数一下一共是 6 个块:Q、当前的 K 和 V、正在接收的 K 和 V、输出 O。显存只和块大小线性相关,与总序列长度无关;序列再长,只要加卡让块大小不变,每卡的注意力显存就不变。
能否把通信藏住
每一步的计算是一个 \(c\times c\) 的注意力块(\(c = s/N\) 是每卡的 token 数),通信是一个 K/V 块。设单卡算力 \(C\),单向带宽 \(W\),按一个 micro-batch 算:
$$T_\text{math} = \frac{4\,b\,c^2 h}{C}\quad(QK^\top \text{ 和 } PV \text{ 各 } 2bc^2h),\qquad T_\text{comm} = \frac{2\cdot 2\,b\,c\,n_{kv}d_h}{W}\quad(K,V\text{,bf16})$$要 \(T_\text{math}\ge T_\text{comm}\):
$$c \;\ge\; \frac{C}{W}\cdot\frac{n_{kv}d_h}{h} = \frac{C}{W}\cdot\frac{n_{kv}}{n_h}$$多头注意力时(\(n_{kv}=n_h\))条件就是:每卡的块大小 \(c\) 要大于「算力 / 带宽」\(C/W\)。几个数:H100 节点内 \(C/W\approx 990\times10^{12}/450\times10^9\approx 2200\),MHA 需要每卡至少约 2200 个 token;GQA 8/64(Llama-3 70B)只要约 275。跨节点每卡约 50 GB/s,\(C/W\approx 19800\),MHA 需要约 2 万 token,GQA 8/64 约 2500。因果掩码会让每步有用计算大约减半,要求再翻倍。这也解释了为什么 GQA 让 CP 变得容易:K/V 比 Q 小得多,要搬的东西少了 \(n_h/n_{kv}\) 倍。反向时每步要计算的量约为前向的 2 倍多,而要传的除了 K/V 还有累积的 \(dK, dV\)(也绕环走),通信也大约翻倍,比例差不多。
因果掩码带来的负载不均
语言模型用因果注意力:token \(p\) 只能看 \(\le p\) 的 key。把 \(s\times s\) 的分数矩阵画出来,只有下三角(含对角线)需要算。如果按顺序切(卡 \(i\) 拿第 \(i\) 段连续的 token),在 Ring Attention 的第 \(r\) 步,卡 \(i\) 处理的是 \(Q_i\) 和 \(K_{(i-r)\bmod N}\):
- 第 0 步:每张卡都在算对角块,下三角一半,大家一样忙。
- 第 \(r\ge 1\) 步:若 \(i\ge r\),KV 来自更早的段,整块都要算(\(c^2\));若 \(i\lt r\),KV 来自更晚的段,整块被掩掉,这张卡无事可做。
每一步的耗时取决于最慢的那张卡,所以第 1 到 \(N-1\) 步每步都要 \(c^2\) 的时间。总耗时约 \(c^2/2 + (N-1)c^2\),而总的有用工作量约 \(N^2c^2/2\),平摊到 \(N\) 张卡是 \(Nc^2/2\)。效率
$$\eta_\text{naive} \approx \frac{Nc^2/2}{(N-\tfrac12)c^2} = \frac{N}{2N-1}\;\to\;\frac12$$\(N=4\) 时约 57%,\(N=8\) 时约 53%:差不多一半的时间在空等。把注意力矩阵按卡上色就能直观看到:卡 0 持有最前面的 token,第 0 步就算完了自己的全部行,之后一直闲着;编号越大的卡,要算的块越多,最后一张卡每一步都满载。问题不在于谁先算完,而在于每一步都要等最忙的那张卡。
Zig-zag 与 Striped:重新分配 token
解决办法是不按连续段分配 token,让每张卡都拿到一些「早」的 token 和一些「晚」的 token。
Zig-zag
把序列切成 \(2N\) 个块 \(C_0, \dots, C_{2N-1}\)(每块 \(c/2\) 个 token),卡 \(i\) 拿 \(C_i\) 和 \(C_{2N-1-i}\):卡 0 拿第一块和最后一块,卡 \(N-1\) 拿中间两块。Llama 3 的 CP 正是这样切的:「把输入序列切成 \(2\times\text{CP}\) 块,第 \(i\) 个 CP rank 拿第 \(i\) 块和第 \(2\times\text{CP}-1-i\) 块」。
验证一下为什么平衡。卡 \(i\) 的 query 块是 \(\{i,\;2N-1-i\}\),它在某一步拿到的 KV 来自卡 \(j\),KV 块是 \(\{j,\;2N-1-j\}\)。四个(query 块,key 块)组合:
| key 块 \(j\)(早) | key 块 \(2N-1-j\)(晚) | |
|---|---|---|
| query 块 \(i\)(早) | \(j\lt i\):全算;\(j=i\):对角;\(j\gt i\):跳过 | \(2N-1-j\ge N\gt i\):总是跳过 |
| query 块 \(2N-1-i\)(晚) | \(j\le N-1\lt 2N-1-i\):总是全算 | \(j\gt i\):全算;\(j=i\):对角;\(j\lt i\):跳过 |
当 \(j\ne i\)(第 1 到 \(N-1\) 步)时,恰好有两个组合要全算、两个跳过,工作量是 \(2\times(c/2)^2 = c^2/2\),每张卡、每一步都一样。当 \(j=i\)(第 0 步)时,一个全块加两个对角半块,也约是 \(c^2/2\)。总耗时约 \(Nc^2/2\),正好等于理想值。相比朴素切法快了约 \(2-1/N\) 倍。
表格还揭示了一个实现上的巧合:\(j\lt i\) 时要算的两个组合都用 key 块 \(j\),即「全部 Q × KV 的前一半」;\(j\gt i\) 时都用 query 块 \(2N-1-i\),即「Q 的后一半 × 全部 KV」。两种情况都是没有掩码的矩形,直接调一次不带 causal 的 FlashAttention 即可,只有第 0 步需要带掩码。开源实现 ring-flash-attention 的 zigzag 版本就是这样做的。
Striped
Striped Attention(Brandon 等人)走得更彻底:token \(p\) 分给卡 \(p \bmod N\),每张卡拿到均匀散布在整条序列上的一组 token。这样每一步每张卡面对的掩码都近似是一个「上三角掉一半」的块,各卡工作量只差对角线上的那几个元素。实测中,Striped 在 8 张 A100 上把 256K 序列的端到端训练吞吐提高到 Ring Attention 的最多 1.45 倍,在 16 块 TPUv4、786K 序列上是 1.65 倍;当块大小和设备数趋于无穷时,理论加速趋近 2 倍。和 zig-zag 相比,Striped 每一步的掩码都不是全满或全空的矩形,实现上需要一个带偏移的因果掩码。
重排 token 的代价
注意力对 token 顺序是「置换等变」的:只要掩码按 token 在原序列里的位置来判断,重排不影响结果。所以 zig-zag 或 striped 只需要在模型入口重排一次 token,整个前向和反向都保持这个布局。但要同时重排三样东西:输入 token id、位置编码用的 position id(RoPE 必须用原始位置),以及计算 loss 用的目标 token。逐 token 的模块本来就不关心顺序。
演示:Ring Attention 逐步执行
左边是 \(T\) 个 token 的因果注意力矩阵(行是 query,列是 key,只画下三角),上方和左侧色带的深浅表示每个 token 分给了哪张卡(越深编号越大)。当前这一步正在计算的格子用实心橙色,之前步骤算过的用浅紫色。右边是每张卡这一步的工作量(按格子数)、空等的部分,以及累计的效率。这里假设掩码可以按元素精确跳过;真实的 kernel 按 tile 跳过,对角块的收益会打折扣。
切到「朴素连续」,按几下「下一步」:从第 1 步开始,编号小的卡整块空闲,编号大的卡整块满载。换成 Zig-zag,每一步每张卡的工作量都相同。Striped 下各卡只差几个对角元素。把 \(N\) 调到 8,朴素切法的效率接近 53%。
All-Gather 式 CP
Ring 不是唯一的实现。另一种做法是:每层注意力之前,在 CP 组内对 K 和 V 做一次 All-Gather,每张卡拿到完整序列的 K/V,然后用本地的 Q 算注意力;反向时对 \(dK,dV\) 做 Reduce-Scatter。两种实现各有取舍:
- All-Gather 实现:一步完成,简单;但需要临时放下全部 K/V。
- 环形实现:每次只多存一个块,通信分散在 \(N\) 步里并与计算重叠;代价是多步通信带来的额外延迟和更复杂的调度。
Llama 3 选的是 All-Gather 式(同样配合 \(2\times\text{CP}\) 块的负载均衡切法),并给了两个理由:一是更容易支持各种注意力掩码,比如文档掩码(一条序列里拼了多篇文档,跨文档不可见),环形实现要在每一步为不同的 KV 块计算正确的掩码;二是 GQA 让 K/V 比 Q 小得多,而注意力的计算是 \(O(S^2)\)、All-Gather 是 \(O(S)\),暴露在关键路径上的通信可以忽略。
完整 K/V 的大小:每层 \(2\cdot 2\,b\,s\,n_{kv}d_h\) 字节(TP 下再除以 \(t\)),对 Llama-3 70B、\(s=128\text{K}\) 是 512 MiB,TP=8 后 64 MiB。这是一层的临时缓冲,算完就可以释放(反向重算时再 gather),所以完全可以接受。对 MHA 模型或者 KV 头很多的模型,这个账就不那么好看了。
DeepSpeed-Ulysses:按头的 All-to-All
换一个角度:注意力在序列维上纠缠,但在头维上完全独立。DeepSpeed-Ulysses 利用这一点在注意力前后各转置一次切分维度:
- 注意力之外,和 CP 一样,每张卡持有 \(s/P\) 个 token 的完整隐藏向量(\(P\) 是序列并行度),逐 token 的模块本地计算。
- QKV 投影后,每张卡有 \([s/P,\; n_h,\; d_h]\) 的 Q(K/V 类似)。做一次 All-to-All:把头维切成 \(P\) 份发给不同的卡,同时收集别的卡的序列段。结束后每张卡有 \([s,\; n_h/P,\; d_h]\):完整序列、部分头。
- 每张卡对自己的 \(n_h/P\) 个头算普通的(单卡)注意力,可以直接用 FlashAttention,掩码怎么复杂都行。
- 输出 \([s, n_h/P, d_h]\) 再做一次 All-to-All,换回 \([s/P, n_h, d_h]\),接输出投影。
通信量。All-to-All 里每张卡把本地张量的 \((P-1)/P\) 发出去。在节点内 NVSwitch、节点间 fat-tree 这样的网络上,总大小为 \(M\) 的 All-to-All 在 \(P\) 张卡上每条链路只传 \(M/P\)。QKV 的总大小是 \(3Nh\)、输出是 \(Nh\)(\(N\) 为序列长度),每条链路共 \(4Nh/P\),所以序列长度和卡数同比例增长时,每条链路的通信量不变。对比 Megatron 的 SP:它的 All-Gather / Reduce-Scatter 每卡通信量是 \(O(N)\),不随 \(P\) 下降。
上面的 \(3Nh\) 是 MHA。对 GQA,K/V 只有 \(n_{kv}d_h\) 宽,把每卡发送的字节数(bf16,单层前向)写准确一点:
$$\text{Ulysses:}\;\frac{P-1}{P}\cdot 2b\frac{s}{P}\,(2h + 2n_{kv}d_h)\qquad \text{Ring / All-Gather:}\;\frac{P-1}{P}\cdot 2b\,s\cdot 2n_{kv}d_h$$两者之比是 \(\frac{h+n_{kv}d_h}{P\cdot n_{kv}d_h}\)。MHA 下是 \(2/P\),Ulysses 在 \(P\ge 3\) 时就更省;但 GQA 8/64 下是 \(9/P\),要 \(P\gt 9\) Ulysses 才更省。也就是说 GQA 越激进,环形或 All-Gather 式 CP 越占便宜:它们只搬 K/V,而 Ulysses 还要搬 Q 和 O。
限制。
- 并行度受头数约束:\(P\) 要整除 \(n_h\);对 GQA,\(P\gt n_{kv}\) 时 K/V 头不够分,只能复制,通信量随之增大。和 TP 叠加时更紧:TP 已经按头切了 \(t\) 份,剩下给 Ulysses 的只有 \(n_{kv}/t\)。Llama 3 这类 8 个 KV 头、TP=8 的配置,几乎没有给 Ulysses 留空间。另外,注意力阶段每张卡要放下完整序列长度的 Q/K/V(只是头少了),序列极长时这本身就是负担。
- All-to-All 是阻塞的,不像 ring 那样自然地与注意力计算重叠;跨节点的 All-to-All 对网络拓扑也更敏感。
- 好处是注意力 kernel 完全不用改:每张卡看到的是完整序列上的普通注意力,任意掩码、稀疏注意力都能直接用。
两者也可以叠加:USP(Unified Sequence Parallelism,Fang 与 Zhao)把序列并行组拆成两维,一维做 Ulysses、另一维做 Ring,比如节点内用 Ulysses 的 All-to-All,节点间用 Ring 的点对点传递。
通信量对比与选择
| Ring Attention | All-Gather CP | Ulysses | |
|---|---|---|---|
| 注意力时每卡持有 | 本地 Q + 1–2 个 KV 块 | 本地 Q + 完整 K/V | 完整序列的 \(n_h/P\) 个头 |
| 每卡前向发送(单层) | \(\frac{P-1}{P}\cdot 4bs\,n_{kv}d_h\) | 同左 | \(\frac{P-1}{P}\cdot\frac{4bs}{P}(h+n_{kv}d_h)\) |
| 通信方式 | 相邻点对点,\(P-1\) 步 | 一次集合通信 | 两次 All-to-All |
| 与计算重叠 | 天然重叠(块够大时) | 暴露在关键路径上 | 暴露在关键路径上 |
| 并行度上限 | 无硬上限(受 \(s/P\) 够不够大约束) | 同左 | \(\le n_h\)(GQA 时实际 \(\le n_{kv}\)) |
| 复杂掩码 | 每步要算对应掩码 | 容易 | 容易 |
| 因果负载均衡 | 需要 zig-zag / striped | 需要 \(2\times\)CP 切法 | 不需要(每卡都是完整序列) |
大致的选择:KV 头少(GQA/MQA)且需要文档掩码,All-Gather 式最省事;序列极长、跨节点、想把通信完全藏住,用 Ring + zig-zag;头多、节点内带宽高、想复用现成的注意力 kernel,Ulysses 或 Ulysses + Ring 的混合。
演示:激活显存 vs 序列长度
看每卡显存随序列长度怎么增长,以及 CP 能把能训练的最长序列推到多远。激活按 Llama 结构记账(FlashAttention,TP + SP)。CP 下所有激活(包括 logits)都按 \(s/N\) 计;模型状态假设优化器状态用 ZeRO-1 在 DP×CP 上切(DP 取 8)。全重计算时,每层只存层输入,再加上反向时临时重算一层的激活。真实框架里还有通信缓冲、显存碎片、CUDA context 等开销,实际能放下的长度会再短一些。
| CP 度 | 模型状态 / 卡 | 能放下的最长序列(2 的幂) | 该长度下 GPU 总数(TP×CP) |
|---|
试试:70B、TP=8、80 GB、无重计算时,CP=1 只能放下 8K,每翻倍一次 CP 能放下的长度也翻倍,CP=8 到 64K。打开全重计算,CP=1 就能到 64K、CP=8 到 1M,但要付出约 1/3 的额外前向计算。405B 的模型状态本身就很大,留给激活的空间少,必须靠 CP 才能做长上下文。
和其他并行组合
- CP × TP。两者切的是不同的维度(CP 切 \(s\),TP 切头和 \(F\)),可以直接叠加:一个 \(t\times N\) 的网格,TP 组内是 SP/TP 的那套通信,CP 组内是注意力里的 K/V 交换。CP 组的 K/V 只是本 TP rank 的那几个头,所以 CP 的通信量也被 \(t\) 除了。Llama 3 把维度顺序排成 [TP, CP, PP, DP]:TP 在最内层(同一台服务器),CP 其次。
- CP × DP。CP 组内权重复制,梯度要同步,所以梯度 All-Reduce / Reduce-Scatter 在 DP×CP 的大组上进行;ZeRO 的分片也可以在这个大组上做。从优化器的角度看,CP 就是「每个样本被拆给了几张卡」的数据并行。
- CP × PP。流水线各级之间传的激活也是 \([b, s/N, h]\),变小了,没有冲突。
- 变长与打包。把多篇文档打包进一条长序列时,CP 的切分是对打包后的整条序列做的;掩码按文档边界计算。All-Gather 式实现在这里最省心,这也是 Llama 3 给出的理由之一。
TP+SP 解决「单层太大」,CP 解决「序列太长」,二者在 NVLink 域内外的位置不同;「模型太深、一个节点放不下」则要靠流水线并行。
参考与延伸阅读
- Nanotron 团队,The Ultra-Scale Playbook:Context Parallelism(Ring Attention、Zig-Zag Ring Attention、All-Gather 与 All-to-All 两种实现,以及按卡上色的注意力矩阵示意图)以及 Memory usage in transformers(激活显存公式与重计算)。
- Liu, Zaharia, Abbeel, Ring Attention with Blockwise Transformers for Near-Infinite Context(环形算法、块大小与带宽的条件、每卡 6 个块的显存分析),2023。
- Brandon et al., Striped Attention: Faster Ring Attention for Causal Transformers(A100 / TPUv4 上的加速实测),2023。
- Jacobs et al., DeepSpeed Ulysses: System Optimizations for Enabling Training of Extreme Long Sequence Transformer Models(按链路的 All-to-All 通信分析),2023。
- Fang, Zhao, USP: A Unified Sequence Parallelism Approach for Long Context Generative AI,2024。
- Llama Team, The Llama 3 Herd of Models,第 3.3.2 节(Context parallelism for long sequences),2024。
- Korthikanti et al., Reducing Activation Recomputation in Large Transformer Models(激活显存记账),2022。
- zhuzilin, ring-flash-attention(ring / zigzag / stripe 三种切法的开源实现,想看它们在代码里的差别可以直接读它)。