- 引言
- 前置研究:Blockwise Parallel Transformer
- Ring Attention 核心算法
- 架构设计细节
- Backward Pass 与梯度计算
- 性能基准分析
- 比较分析:Ring Attention vs Sequence Parallelism vs Tensor Parallelism
- 基于 JAX 的官方实现分析
- 实战应用:长上下文训练流水线
- 局限性、失败案例,以及尚待解决的问题
- 最新进展与未来展望
- 结语
- 参考资料

引言
Transformer 架构中的 Self-Attention 是计算序列内所有 token 对之间关系的强大机制,但存在一个根本性的局限:相对于序列长度 ,其内存与计算复杂度均为 。在单块 GPU 内存约为 80GB(A100)或 141GB(H200)的现实条件下,在单一设备上处理数百万 token 规模的上下文几乎是不可能的。
为解决这一问题,业界提出了 FlashAttention、Sparse Attention、Linear Attention 等多种方案。FlashAttention 通过 IO-awareness 将单设备内的内存效率发挥到极致,但依然受限于单设备 HBM 容量这一物理上限。而 Sparse Attention 或 Linear Attention 则通过近似(approximation)降低复杂度,但代价是必须放弃精确的注意力计算。
2023 年 10 月,UC Berkeley 的 Hao Liu、Matei Zaharia、Pieter Abbeel 发表的论文 Ring Attention with Blockwise Transformers for Near-Infinite Context 从一个完全不同的角度切入了这个问题。他们提出了一种方法:在完全不损害注意力计算精度的前提下,将序列分布到多台设备上,并让通信与计算完美重叠。核心思路是把设备连接成一个逻辑上的环(ring)拓扑,在环中循环传递 Key-Value 块,同时执行 Blockwise Parallel Transformer 的分块注意力计算。
通过这种方式,上下文长度会随设备数量线性扩展。用 32 台 A100 GPU,可以把 7B 模型的上下文扩展到 100 万 token 以上;在 TPUv4-1024 上,据报告一个 3B 模型可以处理到 1,600 万 token。这篇被 ICLR 2024 接收的论文,从根本上改变了分布式环境下长上下文训练的范式。
本文将全面剖析 Ring Attention 论文的理论基础——Blockwise Parallel Transformer,Ring Attention 的核心算法,分布式通信设计,PyTorch/JAX 实现细节,基准测试分析,与其他并行化策略的比较,以及实战应用中出现的局限与失败案例。
前置研究:Blockwise Parallel Transformer
要理解 Ring Attention,首先要理解同一批作者此前发表的 Blockwise Parallel Transformer(BPT)。可以把 BPT 看作 Ring Attention 的单设备版本,它为分块注意力计算提供了数学上的正当性。没有这项前置研究,Ring Attention 的分布式扩展就不可能实现,两篇论文属于同一条研究脉络。
标准 Self-Attention 的内存问题
在标准 Self-Attention 中,需要把 Query、Key、Value 矩阵整体载入内存,一次性计算注意力分数矩阵 。由于该分数矩阵的大小是 ,序列长度一旦增加,内存占用就会呈二次增长。以 fp16 计算,16K token 时该分数矩阵约为 512MB,而在 128K token 时会暴增至约 32GB。这意味着,即便撇开模型权重或优化器状态不谈,仅注意力计算本身就会消耗设备内存的相当一部分。尤其是在训练时,由于反向传播需要保存注意力分数,内存负担比推理时高 2-3 倍。
分块划分策略
BPT 的核心,是把整体注意力计算切分为独立的分块单元,同时保证最终结果在数学上与原始的精确(exact)注意力完全一致。把序列切成大小为 的块之后,对于 Query 块 ,可以先计算它与所有 Key-Value 块 的部分注意力,再把这些结果精确地累加起来。
在这个累加过程中,关键在于 在线 Softmax(Online Softmax) 技术。这项也被 FlashAttention 采用的技术,使得无需一次性看到全部注意力分数,也能按块逐步计算出精确的 softmax 结果。
import torch
import torch.nn.functional as F
def blockwise_attention(Q: torch.Tensor, K: torch.Tensor, V: torch.Tensor,
block_size: int) -> torch.Tensor:
"""按块计算注意力(基于在线 Softmax)。
不一次性计算完整的注意力分数矩阵,
而是按块逐步累积出精确结果。
Args:
Q: Query 张量 [batch, seq_len, d_k]
K: Key 张量 [batch, seq_len, d_k]
V: Value 张量 [batch, seq_len, d_v]
block_size: 块大小
Returns:
Output 张量 [batch, seq_len, d_v]
"""
batch, seq_len, d_k = Q.shape
d_v = V.shape[-1]
scale = d_k ** -0.5
num_blocks = seq_len // block_size
output = torch.zeros(batch, seq_len, d_v, device=Q.device, dtype=Q.dtype)
for i in range(num_blocks):
q_block = Q[:, i * block_size:(i + 1) * block_size, :] # [B, block_size, d_k]
# 用于在线 Softmax 的累积变量
max_score = torch.full((batch, block_size, 1), float('-inf'), device=Q.device)
sum_exp = torch.zeros(batch, block_size, 1, device=Q.device)
acc = torch.zeros(batch, block_size, d_v, device=Q.device)
for j in range(num_blocks):
k_block = K[:, j * block_size:(j + 1) * block_size, :]
v_block = V[:, j * block_size:(j + 1) * block_size, :]
# 计算部分注意力分数
scores = torch.bmm(q_block, k_block.transpose(-2, -1)) * scale # [B, bs, bs]
# 在线 Softmax 更新
new_max = torch.maximum(max_score, scores.max(dim=-1, keepdim=True).values)
correction = torch.exp(max_score - new_max)
new_exp = torch.exp(scores - new_max)
# 修正此前的累积值并纳入新块
sum_exp = sum_exp * correction + new_exp.sum(dim=-1, keepdim=True)
acc = acc * correction + torch.bmm(new_exp, v_block)
max_score = new_max
# 最终归一化
output[:, i * block_size:(i + 1) * block_size, :] = acc / sum_exp
return output
上面代码中的关键是 correction 项。当新块的最大分数大于此前的最大分数时,就会把此前累积的指数和与加权和,按新的尺度重新修正。这个修正过程,在数学上保证了与对整个序列一次性计算的精确 softmax 完全相同的结果。
BPT 的 Feedforward 融合
BPT 不止于分块计算注意力,还把 Feedforward Network(FFN)的计算也按块融合进来。也就是说,在求出 Query 块 的注意力结果之后,立刻对该结果执行 FFN,完成该块的最终输出。这样一来,无需把整个注意力输出保存在内存中,就可以按块完成到 FFN 为止的处理并记录结果。
class BlockwiseParallelTransformerLayer(torch.nn.Module):
"""BPT 层:按块融合处理注意力与 FFN。"""
def __init__(self, d_model: int, n_heads: int, d_ff: int):
super().__init__()
self.d_model = d_model
self.n_heads = n_heads
self.d_k = d_model // n_heads
self.W_q = torch.nn.Linear(d_model, d_model)
self.W_k = torch.nn.Linear(d_model, d_model)
self.W_v = torch.nn.Linear(d_model, d_model)
self.W_o = torch.nn.Linear(d_model, d_model)
self.ffn = torch.nn.Sequential(
torch.nn.Linear(d_model, d_ff),
torch.nn.GELU(),
torch.nn.Linear(d_ff, d_model),
)
self.norm1 = torch.nn.LayerNorm(d_model)
self.norm2 = torch.nn.LayerNorm(d_model)
def forward(self, x: torch.Tensor, block_size: int = 1024) -> torch.Tensor:
batch, seq_len, _ = x.shape
num_blocks = seq_len // block_size
Q = self.W_q(x)
K = self.W_k(x)
V = self.W_v(x)
output = torch.zeros_like(x)
for i in range(num_blocks):
start, end = i * block_size, (i + 1) * block_size
q_block = Q[:, start:end, :]
# 按块计算注意力(在线 Softmax)
attn_out = self._blockwise_attn(q_block, K, V, block_size)
attn_out = self.W_o(attn_out)
# 残差连接 + 层归一化
block_input = x[:, start:end, :]
normed = self.norm1(block_input + attn_out)
# 立即应用 FFN(节省内存的关键)
ffn_out = self.ffn(normed)
output[:, start:end, :] = self.norm2(normed + ffn_out)
return output
这个结构的内存占用,由块大小决定,而非整个序列长度。无论序列是 100 万 token 还是 1000 万 token,每个时刻内存中保留的都只是当前处理中的块和正在循环的 KV 块。这正是 BPT 相比现有内存高效 Transformer 能够处理长达 32 倍上下文的根本原因。
BPT 的另一项重要贡献,是通过重新编排注意力与 FFN 的计算顺序来优化内存访问模式。传统 Transformer 是先对整个序列完成注意力计算,再应用 FFN;而 BPT 则对每个块连续处理注意力与 FFN。这样一来,中间结果的内存生命周期被限制在块大小之内,峰值内存占用大幅下降。GPU 的 SRAM(L1/L2 缓存)与 HBM 之间的数据搬运也被最小化,从而提升 IO 效率。
Ring Attention 核心算法
环形拓扑与 KV 循环
Ring Attention 把 BPT 的分块注意力计算分布到多台设备上。假设有 台设备以逻辑环的形式连接。把整个序列切成 个 chunk,分配给每台设备。设备 持有序列第 个 chunk 所对应的 Query 块 、Key 块 、Value 块 。
算法的核心步骤如下。
- 初始状态:各设备 先对自己本地的 KV 块 计算部分注意力。
- KV 循环:每台设备把当前持有的 KV 块发送给环上的下一台设备,同时从上一台设备接收新的 KV 块。
- 计算-通信重叠:在收发进行的同时,对当前持有的 KV 块执行注意力计算。只要注意力计算所需时间大于等于通信时间,通信开销就会被完全隐藏。
- 重复:完成 轮循环后,每台设备的 就已经参照过整个序列的全部 KV 块,从而得到精确的 Full Attention 结果。
下面是用 PyTorch 分布式通信原语实现的 Ring Attention 核心循环的伪代码。
import torch
import torch.distributed as dist
def ring_attention_forward(
Q_local: torch.Tensor, # 本设备的 Query 块 [batch, chunk_len, d_k]
K_local: torch.Tensor, # 本设备的 Key 块
V_local: torch.Tensor, # 本设备的 Value 块
rank: int, # 当前设备的 rank
world_size: int, # 设备总数
scale: float, # 缩放因子 1/sqrt(d_k)
) -> torch.Tensor:
"""Ring Attention 前向传播。
在环形拓扑中循环传递 KV 块,按块累积计算注意力。
以异步方式重叠通信与计算,隐藏通信开销。
"""
batch, chunk_len, d_k = Q_local.shape
d_v = V_local.shape[-1]
# 在线 Softmax 累积变量
max_score = torch.full((batch, chunk_len, 1), float('-inf'), device=Q_local.device)
sum_exp = torch.zeros(batch, chunk_len, 1, device=Q_local.device)
acc = torch.zeros(batch, chunk_len, d_v, device=Q_local.device)
# 当前待处理的 KV 块(初始为本地块)
kv_current = (K_local.clone(), V_local.clone())
# 接收缓冲区
kv_recv = (torch.empty_like(K_local), torch.empty_like(V_local))
# 计算环上的邻居
send_to = (rank + 1) % world_size
recv_from = (rank - 1) % world_size
for step in range(world_size):
K_block, V_block = kv_current
# 非最后一步时启动异步通信
if step < world_size - 1:
send_ops = [
dist.isend(K_block, dst=send_to),
dist.isend(V_block, dst=send_to),
]
recv_ops = [
dist.irecv(kv_recv[0], src=recv_from),
dist.irecv(kv_recv[1], src=recv_from),
]
# 对当前 KV 块计算部分注意力(与通信同时进行)
scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale
# 在线 Softmax 更新
block_max = scores.max(dim=-1, keepdim=True).values
new_max = torch.maximum(max_score, block_max)
correction = torch.exp(max_score - new_max)
new_exp = torch.exp(scores - new_max)
sum_exp = sum_exp * correction + new_exp.sum(dim=-1, keepdim=True)
acc = acc * correction + torch.bmm(new_exp, V_block)
max_score = new_max
# 等待通信完成后替换缓冲区
if step < world_size - 1:
for op in send_ops + recv_ops:
op.wait()
kv_current = (kv_recv[0].clone(), kv_recv[1].clone())
# 最终归一化
output = acc / sum_exp
return output
计算-通信重叠条件
Ring Attention 的效率在通信时间不超过计算时间时达到最大化。用公式表示这一条件如下。
对于块大小 、模型维度 ,块注意力的计算量为 FLOPs。而一对 KV 块的通信量为 个元素(Key 和 Value 各一份)。设设备间带宽为 (bytes/s)、计算吞吐量为 (FLOPs/s),重叠条件如下。
整理后,块大小的下限为:
在 A100 GPU 上使用 NVLink(600 GB/s)并执行 bf16 运算(312 TFLOPS)时,,也就是说块大小达到 1024 token 以上,通信就会被完全隐藏。若使用节点间 InfiniBand(400 Gbps = 50 GB/s),则 ,需要把块大小设置得大得多。
Causal Masking 的处理
在自回归(autoregressive)模型中,屏蔽对未来 token 的注意力的因果掩码(causal masking)是必不可少的。Ring Attention 在处理这一点时有一项重要优化:如果设备 收到的 KV 块在原始序列中的位置比自己的 Query 块更靠后,那么该整块都会被掩盖,因此可以完全跳过计算。
def ring_attention_causal_step(
Q_local: torch.Tensor,
K_block: torch.Tensor,
V_block: torch.Tensor,
q_block_idx: int, # Query 块在原始序列中的索引
kv_block_idx: int, # 当前 KV 块在原始序列中的索引
block_size: int,
scale: float,
) -> tuple:
"""应用了 causal masking 的 Ring Attention 单步计算。
依据 KV 块的位置决定进行完整计算、部分掩码,还是完全跳过。
"""
if kv_block_idx > q_block_idx:
# KV 块的位置晚于 Query -> 完全跳过
return None, None, None
scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale
if kv_block_idx == q_block_idx:
# 仅在同一块内部应用部分 causal masking
chunk_len = Q_local.shape[1]
causal_mask = torch.triu(
torch.ones(chunk_len, chunk_len, device=Q_local.device, dtype=torch.bool),
diagonal=1
)
scores = scores.masked_fill(causal_mask.unsqueeze(0), float('-inf'))
# kv_block_idx < q_block_idx:无需掩码(均为过去 token)
block_max = scores.max(dim=-1, keepdim=True).values
exp_scores = torch.exp(scores - block_max)
block_sum = exp_scores.sum(dim=-1, keepdim=True)
block_out = torch.bmm(exp_scores, V_block)
return block_out, block_max, block_sum
这项优化在 causal 场景下平均可节省约 50% 的计算量。原因在于,虽然总共 台设备中每台设备都要循环处理 个 KV 块,但由于 causal masking,约一半的块可以被完全跳过。但需要注意的是,这种节省效果也会带来设备间的计算不均衡。负责序列前段的设备几乎要处理全部 KV 块,而负责后段的设备则会跳过大部分块,导致空闲时间变长。
架构设计细节
Ring 通信模式
Ring Attention 的通信模式运作方式如下。以 台设备的环境为例说明。
步骤 0(初始状态):
- Device 0:持有 、 -> 对 与 计算部分注意力
- Device 1:持有 、 -> 对 与 计算部分注意力
- Device 2:持有 、 -> 对 与 计算部分注意力
- Device 3:持有 、 -> 对 与 计算部分注意力
步骤 1:各设备把 KV 发送给下一台设备
- Device 0:收到 -> 对 与 计算部分注意力,并与此前结果累加
- Device 1:收到 -> 对 与 计算部分注意力
- Device 2:收到 -> 对 与 计算部分注意力
- Device 3:收到 -> 对 与 计算部分注意力
步骤 2 与 3 也按同样的模式进行,总共 步之后,每台设备的 Query 都已参照过整个序列的全部 KV。
这一模式的重要特性在于,每一步所有设备都同时恰好发送一个 KV 块、接收一个 KV 块。因此整体带宽利用是均衡的,也不会像 AllReduce 那样出现网络瓶颈。环形拓扑是分布式系统中最简单、同时带宽利用效率也很高的通信模式之一,AllReduce 算法所依赖的 Ring-AllReduce 也运用了同样的原理。
还有一点很重要:每台设备需要处理的通信轮数恰好是 。这是一个仅由设备数量决定的常数,与整体序列长度或块大小无关。由于每轮交换的数据量是固定的,整体通信时间是可预测的,这使得计算-通信重叠的调度变得容易。
内存占用分析
分析 Ring Attention 中每台设备的内存占用,情况如下。
- Q 块:(常数,本地块)
- KV 块(当前处理中):(Key 和 Value 各一份)
- KV 接收缓冲区:(用于异步接收的双缓冲)
- 在线 Softmax 累积变量:(acc、sum_exp、max_score)
总内存占用为 ,相对于整体序列长度 ,即为 。相比单设备下的 或 FlashAttention 下的 ,这一占用会随设备数 成比例下降。
若把模型参数与优化器状态一并纳入实际内存预算考虑,以 A100 80GB 为例,一个 7B 模型可用于注意力计算的内存约为 20-30GB。以块大小 、模型维度 、bf16 计算,KV 缓冲区内存约为 MB 量级,完全在单设备内存限度内可以处理。
这项内存分析说明了为什么 Ring Attention 与单设备内存高效技术(FlashAttention)是互补的。在每台设备内部应用 FlashAttention 的分块与重计算策略,把 HBM 占用降到最低;而在设备之间则应用 Ring Attention 的分布式循环策略,分配整个序列。两种技术结合,可以同时克服单设备的物理内存上限与单设备的计算处理上限。
Backward Pass 与梯度计算
反向传播中的 KV 再循环
Ring Attention 的反向传播过程中,同样适用与 Forward Pass 相同的环形循环模式。利用 Forward 阶段保存下来的 softmax 统计量(max_score、sum_exp),可以精确计算每个块的梯度。
反向传播时需要注意 KV 块的循环方向。Forward 阶段把 KV 沿正向(rank -> rank+1)循环,Backward 阶段也按同样顺序再次循环 KV 块,同时计算梯度。在这个过程中,dK 与 dV 的梯度先在各设备上分别计算出一部分,之后需要累加回该 KV 块原本所属的设备。
def ring_attention_backward(
dO_local: torch.Tensor, # 输出梯度 [batch, chunk_len, d_v]
Q_local: torch.Tensor, # 保存下来的 Query
K_local: torch.Tensor, # 本地 Key
V_local: torch.Tensor, # 本地 Value
O_local: torch.Tensor, # Forward 输出
lse_local: torch.Tensor, # log-sum-exp(在线 Softmax 统计量)
rank: int,
world_size: int,
scale: float,
) -> tuple:
"""Ring Attention 反向传播。
以与 Forward 相同的 KV 循环模式计算 dQ、dK、dV。
dK、dV 会累加回原本所属的设备。
"""
batch, chunk_len, d_k = Q_local.shape
d_v = V_local.shape[-1]
dQ = torch.zeros_like(Q_local)
dK_local = torch.zeros_like(K_local)
dV_local = torch.zeros_like(V_local)
# 预先计算 D 向量:rowsum(dO * O)
D = (dO_local * O_local).sum(dim=-1, keepdim=True) # [batch, chunk_len, 1]
kv_current = (K_local.clone(), V_local.clone())
send_to = (rank + 1) % world_size
recv_from = (rank - 1) % world_size
for step in range(world_size):
K_block, V_block = kv_current
# 重新计算注意力分数(由于使用了检查点,Forward 阶段未保存)
scores = torch.bmm(Q_local, K_block.transpose(-2, -1)) * scale
P = torch.exp(scores - lse_local) # 归一化后的注意力权重
# 计算梯度
dV_block = torch.bmm(P.transpose(-2, -1), dO_local)
dP = torch.bmm(dO_local, V_block.transpose(-2, -1))
dS = P * (dP - D) * scale
dQ += torch.bmm(dS, K_block)
dK_block = torch.bmm(dS.transpose(-2, -1), Q_local)
# 把 dK、dV 发送回原本所属的设备
source_rank = (rank - step) % world_size
if source_rank == rank:
dK_local += dK_block
dV_local += dV_block
else:
dist.reduce(dK_block, dst=source_rank, op=dist.ReduceOp.SUM)
dist.reduce(dV_block, dst=source_rank, op=dist.ReduceOp.SUM)
# 为下一步循环 KV
if step < world_size - 1:
kv_recv = (torch.empty_like(K_block), torch.empty_like(V_block))
send_ops = [dist.isend(K_block, dst=send_to), dist.isend(V_block, dst=send_to)]
recv_ops = [dist.irecv(kv_recv[0], src=recv_from), dist.irecv(kv_recv[1], src=recv_from)]
for op in send_ops + recv_ops:
op.wait()
kv_current = kv_recv
return dQ, dK_local, dV_local
检查点策略
为了在 Ring Attention 中实现内存高效的训练,Gradient Checkpointing(重计算,rematerialization)是必不可少的。如果在 Forward Pass 中保存每个块的注意力分数矩阵,就需要 的内存,节省内存的效果会因此被抵消。取而代之的做法是,Forward 阶段只保存在线 Softmax 的统计值(max_score、log-sum-exp),在 Backward 阶段重新计算注意力分数。这与 FlashAttention 采用的策略相同。
检查点的代价是计算量的增加。由于 Backward 阶段要重新计算注意力分数,整体 FLOPs 会增加约 33%。但由于节省内存的效果要大得多,在长上下文训练中这一权衡是完全值得的。在实务中,通常会用 PyTorch 的 torch.utils.checkpoint 或 JAX 的 jax.checkpoint,以 Transformer 层为单位应用检查点。对于 Ring Attention,也可以按每个环循环步骤应用检查点,从而对内存与计算量做更精细的权衡控制。
性能基准分析
论文报告结果
Ring Attention 论文中报告的核心基准测试结果整理如下。
| 设置 | 模型规模 | 设备 | 达到的上下文长度 | 扩展倍数 |
|---|---|---|---|---|
| A100 32台 | 7B | 32x A100 80GB | 1,000,000+ token | 32x(相较此前) |
| TPUv4-1024 | 3B | 1024 个 TPUv4 芯片 | 16,000,000 token | 512x(相较此前) |
| A100 8台 | 7B | 8x A100 80GB | 262,144 token | 8x |
值得注意的是,"相较此前"的基准是内存高效 Transformer(如 FlashAttention 等)。在 Ring Attention 之前,单设备使用 FlashAttention 就已经能处理约 32K-64K token,而 Ring Attention 把这个数字按设备数量做了精确的比例扩展。这种线性扩展特性并非只是理论上可能,而是在真实硬件上测得的结果,这一点具有重要意义。设备数量每增加一分,可处理的上下文长度就按比例增长,这让基础设施投入与性能之间的预测变得容易。
后续研究基准
在 Ring Attention 概念基础上扩展的后续研究成果也值得关注。RingX(2024)在 Frontier 超级计算机上使用 4,096 个 GPU,把 Llama3 8B 模型训练到 100 万 token 的上下文,同时达到了 38% 的 Model FLOPs Utilization(MFU)。这是长上下文训练中报告过的最高水平的训练效率之一。
在 Meta 的 Context Parallelism 研究中,Llama3 405B 模型的 100 万 token 预填充(prefill)在 77 秒内完成,达到了 93% 的并行化效率和 63% 的 FLOPS 利用率。128K 上下文的预填充仅需 3.8 秒。
通信开销实测
分析论文所强调的"零开销通信"在何种条件下真正能够实现,结果如下。
| 连接类型 | 带宽 | 最小块大小(bf16, d=4096) | 实测开销 |
|---|---|---|---|
| NVLink(节点内) | 600 GB/s | ~1,024 token | 0-2% |
| PCIe Gen5 | 64 GB/s | ~9,750 token | 5-15% |
| InfiniBand HDR | 50 GB/s | ~12,480 token | 10-25% |
| Ethernet 100G | 12.5 GB/s | ~49,920 token | 30-60% |
在节点内 NVLink 环境中,只要块大小达到 1024 以上,通信就几乎可以被完全隐藏。但在节点间通信中,需要把块大小设置得大得多,在 Ethernet 环境下,实际上很难实现高效的 Ring Attention。这一结果表明,在制定 Ring Attention 的部署策略时,网络拓扑是一个决定性的设计变量。在云环境中搭建 GPU 集群时,需要慎重选择节点间带宽规格,如果可能,应优先考虑基于 NVLink 或 NVSwitch 的单节点多 GPU 配置。
比较分析:Ring Attention vs Sequence Parallelism vs Tensor Parallelism
比较分布式环境下处理长上下文的三种主要并行化策略。
| 特性 | Ring Attention | Sequence Parallelism (DeepSpeed-Ulysses) | Tensor Parallelism |
|---|---|---|---|
| 划分对象 | 序列维度(整体注意力) | 序列维度(基于注意力头) | 模型维度(权重划分) |
| 通信模式 | P2P Ring (Send/Recv) | All-to-All | AllReduce |
| 通信量 | per step | per layer | per layer |
| 注意力精度 | Exact(无近似) | Exact(无近似) | Exact(无近似) |
| 最大并行度 | 不受设备数量限制 | 受注意力头数量限制 | 受注意力头数量限制 |
| GQA/MQA 兼容性 | 完全兼容 | 有限(头数不足) | 有限 |
| 通信-计算重叠 | 可行(核心设计) | 不可行(同步 All-to-All) | 不可行(同步 AllReduce) |
| 节点间扩展性 | 满足块大小条件时表现良好 | 依赖 All-to-All 带宽 | 依赖 AllReduce 带宽 |
| 实现复杂度 | 高 | 中等 | 低 |
| 内存效率 | 极高() | 高() | 与模型规模成正比 |
核心差异点分析
Ring Attention 的优势。 最大的差异点在于不受注意力头数量的限制。DeepSpeed-Ulysses 只能把序列划分为注意力头数量那么多份,因此在 GQA(Grouped Query Attention)中,如果 Key-Value 头只有 8 个,最多也只能实现 8-way 并行。Ring Attention 没有这一限制。
DeepSpeed-Ulysses 的优势。 在节点内环境中,All-to-All 通信效率非常高,因此在头数足够的情况下,其吞吐量会高于 Ring Attention。在基于 NVSwitch 的系统上,All-to-All 通信有时比 P2P Send/Recv 更高效。
混合方案。 近期研究提出了结合两种方法的 USP(Unified Sequence Parallelism)。这是一种 2D 序列并行化策略:节点内使用 Ulysses 的 All-to-All,节点间使用 Ring Attention 的 P2P 循环。
import torch.distributed as dist
def hybrid_ulysses_ring_attention(
Q: torch.Tensor,
K: torch.Tensor,
V: torch.Tensor,
intra_node_group: dist.ProcessGroup, # 节点内组(Ulysses)
inter_node_group: dist.ProcessGroup, # 节点间组(Ring)
n_heads: int,
) -> torch.Tensor:
"""USP:Ulysses + Ring Attention 混合序列并行化。
节点内用基于 All-to-All 的 Ulysses 划分头维度,
节点间用 Ring Attention 划分序列维度。
"""
intra_size = dist.get_world_size(intra_node_group)
inter_size = dist.get_world_size(inter_node_group)
intra_rank = dist.get_rank(intra_node_group)
inter_rank = dist.get_rank(inter_node_group)
# Step 1:Ulysses All-to-All(节点内)
# 序列维度划分 -> 重排为头维度划分
Q_heads = all_to_all_reshape(Q, intra_node_group, split_dim='seq', gather_dim='head')
K_heads = all_to_all_reshape(K, intra_node_group, split_dim='seq', gather_dim='head')
V_heads = all_to_all_reshape(V, intra_node_group, split_dim='seq', gather_dim='head')
# Step 2:Ring Attention(节点间)
# 各节点上的设备针对各自的头执行 Ring Attention
output_heads = ring_attention_forward(
Q_heads, K_heads, V_heads,
rank=inter_rank,
world_size=inter_size,
scale=(Q.shape[-1]) ** -0.5,
)
# Step 3:反向 All-to-All(节点内)
# 头维度划分 -> 还原为序列维度划分
output = all_to_all_reshape(output_heads, intra_node_group, split_dim='head', gather_dim='seq')
return output
基于 JAX 的官方实现分析
Ring Attention 的官方实现基于 JAX/Flax,核心的分布式通信使用了 jax.lax.ppermute。ppermute 是 JAX 的一种 collective operation,能够按设备间的置换(permutation)同时交换数据。这使得环形拓扑的循环通信可以用单次函数调用来实现。
# 基于 JAX 的 Ring Attention 核心实现(参考官方代码)
import jax
import jax.numpy as jnp
from jax import lax
def ring_attention_jax(
q: jnp.ndarray, # [batch, chunk_len, n_heads, d_k]
k: jnp.ndarray, # [batch, chunk_len, n_heads, d_k]
v: jnp.ndarray, # [batch, chunk_len, n_heads, d_v]
axis_name: str, # pmap 轴名称
scale: float,
causal: bool = True,
block_size: int = 1024,
) -> jnp.ndarray:
"""在 JAX pmap 环境下实现 Ring Attention。
使用 lax.ppermute 让 KV 块在环上循环。
"""
axis_size = lax.psum(1, axis_name)
axis_index = lax.axis_index(axis_name)
def scan_fn(carry, step):
acc, max_score, sum_exp, k_block, v_block = carry
# 当前 KV 块在原始序列中的索引
kv_idx = (axis_index - step) % axis_size
# Causal masking 检查
if causal:
should_compute = kv_idx <= axis_index
else:
should_compute = True
# 计算部分注意力
scores = jnp.einsum('bqhd,bkhd->bqhk', q, k_block) * scale
if causal and kv_idx == axis_index:
# 同一块:应用对角线 causal mask
chunk_len = q.shape[1]
mask = jnp.triu(jnp.ones((chunk_len, chunk_len)), k=1).astype(bool)
scores = jnp.where(mask[None, :, None, :], -1e9, scores)
# 在线 Softmax 更新
new_max = jnp.maximum(max_score, scores.max(axis=-1, keepdims=True))
correction = jnp.exp(max_score - new_max)
new_exp = jnp.exp(scores - new_max)
sum_exp = jnp.where(should_compute, sum_exp * correction + new_exp.sum(axis=-1, keepdims=True), sum_exp)
acc = jnp.where(should_compute, acc * correction + jnp.einsum('bqhk,bkhd->bqhd', new_exp, v_block), acc)
max_score = jnp.where(should_compute, new_max, max_score)
# 把 KV 块循环传给环上的下一台设备
# ppermute:按 (src, dst) 对交换数据
perm = [(i, (i + 1) % axis_size) for i in range(axis_size)]
k_block = lax.ppermute(k_block, axis_name, perm=perm)
v_block = lax.ppermute(v_block, axis_name, perm=perm)
return (acc, max_score, sum_exp, k_block, v_block), None
# 初始化
batch, chunk_len, n_heads, d_v = v.shape
init_acc = jnp.zeros((batch, chunk_len, n_heads, d_v))
init_max = jnp.full((batch, chunk_len, n_heads, 1), -1e9)
init_sum = jnp.zeros((batch, chunk_len, n_heads, 1))
init_carry = (init_acc, init_max, init_sum, k, v)
(acc, max_score, sum_exp, _, _), _ = lax.scan(scan_fn, init_carry, jnp.arange(axis_size))
return acc / sum_exp
JAX 实现的核心优势在于,lax.ppermute 会被 XLA 编译器转换为硬件层面优化过的 P2P 通信。在 TPU 环境中,数据通过 ICI(Inter-Chip Interconnect)以极低延迟交换,这正是 Ring Attention 在 TPU 上表现出特别高效率的原因。
实战应用:长上下文训练流水线
Progressive Context Extension
在使用 Ring Attention 的实战训练中,通常不会从一开始就以最大上下文长度进行训练。渐进式上下文扩展(Progressive Context Extension)策略在训练稳定性与效率两方面都表现更优。
import torch
from dataclasses import dataclass
from typing import List
@dataclass
class ContextSchedule:
"""渐进式上下文扩展的调度设置。"""
context_lengths: List[int] # 各阶段的上下文长度
warmup_steps: List[int] # 各阶段的训练步数
rope_theta_values: List[float] # 各阶段的 RoPE theta 值
def get_config(self, global_step: int) -> dict:
cumulative = 0
for i, steps in enumerate(self.warmup_steps):
cumulative += steps
if global_step < cumulative:
return {
'context_length': self.context_lengths[i],
'rope_theta': self.rope_theta_values[i],
'stage': i,
}
return {
'context_length': self.context_lengths[-1],
'rope_theta': self.rope_theta_values[-1],
'stage': len(self.context_lengths) - 1,
}
# 4K -> 16K -> 64K -> 256K -> 1M 渐进式扩展示例
schedule = ContextSchedule(
context_lengths=[4096, 16384, 65536, 262144, 1048576],
warmup_steps=[1000, 800, 600, 400, 200],
rope_theta_values=[10000, 50000, 500000, 5000000, 50000000],
)
# 在训练循环中使用
for step in range(3000):
config = schedule.get_config(step)
ctx_len = config['context_length']
n_devices = torch.cuda.device_count()
chunk_per_device = ctx_len // n_devices
print(f"Step {step}: context={ctx_len}, "
f"chunk/device={chunk_per_device}, "
f"RoPE theta={config['rope_theta']:.0f}, "
f"stage={config['stage']}")
这一渐进式扩展策略中,同步调整 RoPE(Rotary Position Embedding)的 theta 值也很重要。上下文长度增加时,需要拓宽位置编码的频率带宽,才能准确表达远距离的位置关系。YaRN、LongRoPE 等技术正是为此目的而使用的。
长文本数据预处理与 chunk 分配
在 Ring Attention 训练中,数据预处理不止于简单的分词,还包括把多份文档连接成一条长序列,并插入合适的边界标记这一过程。
from typing import List, Optional
import torch
class LongContextDataCollator:
"""用于 Ring Attention 训练的长文本数据 Collator。
把多份文档连接起来构成目标序列长度,
并做填充使其能均匀分配给 N 台设备。
"""
def __init__(
self,
tokenizer,
target_seq_len: int,
world_size: int,
doc_separator_id: int = 2, # </s> 或 <|endoftext|>
):
self.tokenizer = tokenizer
self.target_seq_len = target_seq_len
self.world_size = world_size
self.doc_separator_id = doc_separator_id
self.chunk_size = target_seq_len // world_size
def __call__(self, documents: List[str]) -> dict:
# 对文档分词并用分隔符连接
all_tokens = []
doc_boundaries = []
for doc in documents:
tokens = self.tokenizer.encode(doc, add_special_tokens=False)
doc_boundaries.append(len(all_tokens))
all_tokens.extend(tokens)
all_tokens.append(self.doc_separator_id)
# 按目标长度截断或填充
if len(all_tokens) > self.target_seq_len:
all_tokens = all_tokens[:self.target_seq_len]
elif len(all_tokens) < self.target_seq_len:
pad_len = self.target_seq_len - len(all_tokens)
all_tokens.extend([self.tokenizer.pad_token_id] * pad_len)
# 确认能否被 world_size 整除
assert len(all_tokens) % self.world_size == 0, (
f"시퀀스 길이 {len(all_tokens)}가 "
f"world_size {self.world_size}로 나눠지지 않습니다."
)
input_ids = torch.tensor(all_tokens, dtype=torch.long)
# 生成分配给各设备的 chunk 索引
chunks = input_ids.view(self.world_size, self.chunk_size)
return {
'input_ids': input_ids,
'chunks': chunks,
'doc_boundaries': doc_boundaries,
}
局限性、失败案例,以及尚待解决的问题
尽管 Ring Attention 是一个强大的解决方案,但在实战应用中也报告了多种局限与失败案例。准确理解这些问题,对于生产环境部署是必不可少的。
局限 1:节点间通信瓶颈
如前文分析,Ring Attention 所谓"零开销"的说法,只在节点内高带宽互联(NVLink、NVSwitch)下才成立。在节点间通信(InfiniBand、Ethernet)中,需要把块大小设置得非常大,这与单设备的内存限制相冲突。
有报告指出,在实际云环境(AWS、GCP)中扩展到 2 个节点以上时,观测到了 10-30% 的通信开销。尤其是在非均质(heterogeneous)网络拓扑中,最慢的链路会成为整体性能的瓶颈。
局限 2:Causal Masking 下的负载不均衡
在 Causal Attention 中,负责序列前段的设备(索引较低)需要对大部分 KV 块计算注意力,而负责后段的设备(索引较高)则会跳过许多块。这导致设备间的计算负载出现不均衡。
举例来说,在 8 台设备中,Device 0 要处理全部 8 个 KV 块,而 Device 7 只处理 1 个块(自身),跳过其余 7 个块。虽然平均下来有 50% 的块被跳过,整体计算量因此减少,但会出现 Device 0 让其他设备等待的同步瓶颈。
为缓解这一问题,有人提出了 Striped Attention 模式。它不再按顺序切分序列,而是以交织(interleaving)方式分配,使各设备的计算负载趋于均衡。
局限 3:小批量下的低效
当批量大小非常小时(例如批量大小为 1),Ring Attention 的 GPU 利用率会大幅下降。在长上下文训练中,由于内存限制往往不得不缩小批量,这时 GPU 的 CUDA 核心得不到充分利用,MFU 可能跌至 20% 以下。
局限 4:调试与可复现性的困难
基于分布式异步通信的 Ring Attention 极难调试。通信顺序的细微差异、浮点运算的非确定性、设备间同步错误等,都可能导致训练不稳定,而这些问题的复现与追踪都非常困难。
局限 5:静态内存分配与可变长度序列
Ring Attention 的高效实现假设所有 chunk 大小相同。而在实际训练数据中,文档长度差异很大,对短文档做过度填充会造成计算浪费。要构成一条 100 万 token 的序列,往往需要连接数百份短文档,此时文档边界处的注意力处理也需要额外考虑。
失败案例:NaN/Inf 发散
在线 Softmax 的实现中可能出现数值稳定性问题。尤其是在 bf16 训练中,修正因子 exp(old_max - new_max) 可能因数值过大而溢出,或者当 max_score 随 KV 块顺序剧烈波动时,NaN 可能会传播开来。为防止这种情况,需要用 fp32 进行 softmax 累积计算,或对 max_score 的波动幅度做裁剪。
在实际生产环境中报告过的另一个失败案例,是由异步通信同步错误导致的静默数据损坏。当某台设备的发送出现延迟,接收设备用上一轮遗留的数据计算注意力时,不会出现明确的错误,只会表现为训练损失停滞或模型质量下降。这类问题可以通过在每轮通信完成后显式插入 barrier 验证,以及定期做校验和检查来预防。
局限 6:推理时 KV Cache 的分布式管理
Ring Attention 不仅用于训练,也用于推理,但在推理时会产生 KV Cache 管理这一额外的复杂性。在自回归生成中,每个解码步骤都需要访问此前所有 token 的 KV Cache,因此分布式 KV Cache 之间的通信在每次生成 token 时都是必需的。在预填充(prefill)阶段,Ring Attention 表现出很高的效率,但在解码阶段,由于每次只生成一个 token,计算相对通信的比例变得不利,效率会急剧下降。为解决这一问题,业界正在研究把预填充与解码分离的 Disaggregated Serving 架构。
最新进展与未来展望
World Model on Million-Length Video
Ring Attention 的作者 Hao Liu 在后续研究中利用 Ring Attention 训练出了一个处理超过 100 万 token 的视频-语言多模态模型。该研究采用从 4K 开始逐步扩展到 1M 的策略,成功训练出了能够同时处理长视频与文本的模型。这项研究在视觉-语言领域也证明了 Ring Attention 的实用性。
LASP(Linear Attention Sequence Parallelism)
2025 年提出的 LASP,是专为 Linear Attention 模型设计的序列并行化技术。它采用与 Ring Attention 类似的 P2P 通信模式,但利用 Linear Attention 的核技巧(kernel trick)进一步降低了通信量。在 128 个 GPU 上处理超过 400 万 token 的序列,证明了相比 Ring Attention,可以用相同资源处理长 8 倍的序列。
Context Parallelism 的行业标准化
在 NVIDIA 的 Megatron-LM、Meta 的 Llama 训练基础设施、Google 的 Gemini 训练流水线等主要行业框架中,基于 Ring Attention 的 Context Parallelism 正在被采纳为标准功能。这表明 Ring Attention 已经超越学术贡献,正在成为实质性的行业标准。
未来研究方向
- 自适应块大小:根据网络带宽与计算负载,在运行时动态调整块大小的技术
- Sparse Ring Attention:不循环全部 KV 块,而是基于 Top-k 选择性交换重要块的方法
- 异步流水线:把多个 Transformer 层的 Ring Attention 以流水线方式重叠,最大化整体吞吐量的技术
- 异构硬件优化:在 GPU-TPU 混合集群、CPU offloading 等异构环境中优化 Ring Attention
结语
以上我们从理论基础到实战应用,全面梳理了 Ring Attention。Ring Attention 是在分布式环境中克服 Transformer 上下文长度限制的一种优雅解法。它把 Blockwise Parallel Transformer 的分块注意力计算分布到多台设备上,并结合环形拓扑下的 KV 循环与计算-通信重叠这一核心思路,在完全不损害注意力精度的前提下,让上下文长度随设备数量成比例扩展。
然而在实战应用中,节点间通信瓶颈、causal masking 的负载不均衡、数值稳定性问题等各种挑战依然存在。认清这些局限,并恰当运用 USP 混合策略、Striped Attention、Progressive Context Extension 等补充技术,是成功落地的关键。
目前 Ring Attention 已以 Context Parallelism 之名被整合进主要行业框架,成为支持 100 万 token 以上上下文的新一代 LLM 训练的核心基础设施。未来随着自适应块大小、Sparse Ring Attention 等技术的发展,将会实现更高效、更具扩展性的长上下文处理。位于分布式系统设计与注意力机制优化交汇点上的 Ring Attention,预计将在大规模语言模型的发展中,进一步巩固其作为核心基础设施技术的地位。
参考资料
- Ring Attention with Blockwise Transformers for Near-Infinite Context (arXiv) - Hao Liu、Matei Zaharia、Pieter Abbeel。ICLR 2024 论文原文。
- Ring Attention 官方 GitHub 仓库 - 基于 JAX/Flax 的官方实现代码。
- ring-attention-pytorch (lucidrains) - 基于 PyTorch 的 Ring Attention 实现。
- World Model on Million-Length Video And Language With Blockwise RingAttention - 利用 Ring Attention 训练 100 万 token 视频-语言模型的研究。
- Ultra-Long Sequence Parallelism: Ulysses + Ring-Attention Technical Principles - Ulysses 与 Ring Attention 混合方案的技术解读。
- USP: Unified Sequence Parallel (GitHub) - 结合 Ulysses 与 Ring Attention 的统一序列并行化实现。
- Context Parallelism for Scalable Million-Token Inference - Meta 的 Context Parallelism 推理优化研究。
- GPU MODE Lecture 13: Ring Attention - 从 GPU 实现与 CUDA kernel 角度解读 Ring Attention。
- Blockwise Parallel Transformer (GitHub) - BPT 的 PyTorch/JAX 实现。
- Scaling LLM Inference: Innovations in Tensor Parallelism, Context Parallelism, and Expert Parallelism (Meta Engineering) - Meta 的大规模 LLM 推理并行化策略研究。
현재 단락 (1/465)
Transformer 架构中的 Self-Attention 是计算序列内所有 token 对之间关系的强大机制,但存在一个根本性的局限:相对于序列长度 $L$,其内存与计算复杂度均为 $O(L^2...