Skip to content
Published on

Ring Attention 论文解析:在分布式环境中实现无限上下文窗口训练

分享
Authors
Ring Attention

引言

Transformer 架构中的 Self-Attention 是计算序列内所有 token 对之间关系的强大机制,但存在一个根本性的局限:相对于序列长度 LL,其内存与计算复杂度均为 O(L2)O(L^2)。在单块 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 矩阵整体载入内存,一次性计算注意力分数矩阵 S=QKT/dkS = QK^T / \sqrt{d_k}。由于该分数矩阵的大小是 L×LL \times L,序列长度一旦增加,内存占用就会呈二次增长。以 fp16 计算,16K token 时该分数矩阵约为 512MB,而在 128K token 时会暴增至约 32GB。这意味着,即便撇开模型权重或优化器状态不谈,仅注意力计算本身就会消耗设备内存的相当一部分。尤其是在训练时,由于反向传播需要保存注意力分数,内存负担比推理时高 2-3 倍。

分块划分策略

BPT 的核心,是把整体注意力计算切分为独立的分块单元,同时保证最终结果在数学上与原始的精确(exact)注意力完全一致。把序列切成大小为 BB 的块之后,对于 Query 块 QiQ_i,可以先计算它与所有 Key-Value 块 (Kj,Vj)(K_j, V_j) 的部分注意力,再把这些结果精确地累加起来。

在这个累加过程中,关键在于 在线 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 块 QiQ_i 的注意力结果之后,立刻对该结果执行 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 的分块注意力计算分布到多台设备上。假设有 NN 台设备以逻辑环的形式连接。把整个序列切成 NN 个 chunk,分配给每台设备。设备 ii 持有序列第 ii 个 chunk 所对应的 Query 块 QiQ_i、Key 块 KiK_i、Value 块 ViV_i

算法的核心步骤如下。

  1. 初始状态:各设备 ii 先对自己本地的 KV 块 (Ki,Vi)(K_i, V_i) 计算部分注意力。
  2. KV 循环:每台设备把当前持有的 KV 块发送给环上的下一台设备,同时从上一台设备接收新的 KV 块。
  3. 计算-通信重叠:在收发进行的同时,对当前持有的 KV 块执行注意力计算。只要注意力计算所需时间大于等于通信时间,通信开销就会被完全隐藏。
  4. 重复:完成 N1N-1 轮循环后,每台设备的 QiQ_i 就已经参照过整个序列的全部 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 的效率在通信时间不超过计算时间时达到最大化。用公式表示这一条件如下。

对于块大小 BB、模型维度 dd,块注意力的计算量为 O(B2d)O(B^2 \cdot d) FLOPs。而一对 KV 块的通信量为 2Bd2 \cdot B \cdot d 个元素(Key 和 Value 各一份)。设设备间带宽为 β\beta(bytes/s)、计算吞吐量为 γ\gamma(FLOPs/s),重叠条件如下。

2Bdsizeof(dtype)β2B2dγ\frac{2 \cdot B \cdot d \cdot \text{sizeof(dtype)}}{\beta} \leq \frac{2 \cdot B^2 \cdot d}{\gamma}

整理后,块大小的下限为:

Bγsizeof(dtype)βB \geq \frac{\gamma \cdot \text{sizeof(dtype)}}{\beta}

在 A100 GPU 上使用 NVLink(600 GB/s)并执行 bf16 运算(312 TFLOPS)时,B312×1012×2/(600×109)1024B \geq 312 \times 10^{12} \times 2 / (600 \times 10^9) \approx 1024,也就是说块大小达到 1024 token 以上,通信就会被完全隐藏。若使用节点间 InfiniBand(400 Gbps = 50 GB/s),则 B312×1012×2/(50×109)12,480B \geq 312 \times 10^{12} \times 2 / (50 \times 10^9) \approx 12,480,需要把块大小设置得大得多。

Causal Masking 的处理

在自回归(autoregressive)模型中,屏蔽对未来 token 的注意力的因果掩码(causal masking)是必不可少的。Ring Attention 在处理这一点时有一项重要优化:如果设备 ii 收到的 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% 的计算量。原因在于,虽然总共 NN 台设备中每台设备都要循环处理 NN 个 KV 块,但由于 causal masking,约一半的块可以被完全跳过。但需要注意的是,这种节省效果也会带来设备间的计算不均衡。负责序列前段的设备几乎要处理全部 KV 块,而负责后段的设备则会跳过大部分块,导致空闲时间变长。

架构设计细节

Ring 通信模式

Ring Attention 的通信模式运作方式如下。以 N=4N=4 台设备的环境为例说明。

步骤 0(初始状态)

  • Device 0:持有 Q0Q_0(K0,V0)(K_0, V_0) -> 对 Q0Q_0(K0,V0)(K_0, V_0) 计算部分注意力
  • Device 1:持有 Q1Q_1(K1,V1)(K_1, V_1) -> 对 Q1Q_1(K1,V1)(K_1, V_1) 计算部分注意力
  • Device 2:持有 Q2Q_2(K2,V2)(K_2, V_2) -> 对 Q2Q_2(K2,V2)(K_2, V_2) 计算部分注意力
  • Device 3:持有 Q3Q_3(K3,V3)(K_3, V_3) -> 对 Q3Q_3(K3,V3)(K_3, V_3) 计算部分注意力

步骤 1:各设备把 KV 发送给下一台设备

  • Device 0:收到 (K3,V3)(K_3, V_3) -> 对 Q0Q_0(K3,V3)(K_3, V_3) 计算部分注意力,并与此前结果累加
  • Device 1:收到 (K0,V0)(K_0, V_0) -> 对 Q1Q_1(K0,V0)(K_0, V_0) 计算部分注意力
  • Device 2:收到 (K1,V1)(K_1, V_1) -> 对 Q2Q_2(K1,V1)(K_1, V_1) 计算部分注意力
  • Device 3:收到 (K2,V2)(K_2, V_2) -> 对 Q3Q_3(K2,V2)(K_2, V_2) 计算部分注意力

步骤 2 与 3 也按同样的模式进行,总共 N=4N=4 步之后,每台设备的 Query 都已参照过整个序列的全部 KV。

这一模式的重要特性在于,每一步所有设备都同时恰好发送一个 KV 块、接收一个 KV 块。因此整体带宽利用是均衡的,也不会像 AllReduce 那样出现网络瓶颈。环形拓扑是分布式系统中最简单、同时带宽利用效率也很高的通信模式之一,AllReduce 算法所依赖的 Ring-AllReduce 也运用了同样的原理。

还有一点很重要:每台设备需要处理的通信轮数恰好是 N1N-1。这是一个仅由设备数量决定的常数,与整体序列长度或块大小无关。由于每轮交换的数据量是固定的,整体通信时间是可预测的,这使得计算-通信重叠的调度变得容易。

内存占用分析

分析 Ring Attention 中每台设备的内存占用,情况如下。

  • Q 块B×dB \times d(常数,本地块)
  • KV 块(当前处理中)2×B×d2 \times B \times d(Key 和 Value 各一份)
  • KV 接收缓冲区2×B×d2 \times B \times d(用于异步接收的双缓冲)
  • 在线 Softmax 累积变量B×dv+B×1+B×1B \times d_v + B \times 1 + B \times 1(acc、sum_exp、max_score)

总内存占用为 O(B×d)O(B \times d),相对于整体序列长度 L=N×BL = N \times B,即为 O(L/N×d)O(L/N \times d)。相比单设备下的 O(L2)O(L^2) 或 FlashAttention 下的 O(L)O(L),这一占用会随设备数 NN 成比例下降。

若把模型参数与优化器状态一并纳入实际内存预算考虑,以 A100 80GB 为例,一个 7B 模型可用于注意力计算的内存约为 20-30GB。以块大小 B=8192B=8192、模型维度 d=4096d=4096、bf16 计算,KV 缓冲区内存约为 4×8192×4096×2=5124 \times 8192 \times 4096 \times 2 = 512MB 量级,完全在单设备内存限度内可以处理。

这项内存分析说明了为什么 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 中保存每个块的注意力分数矩阵,就需要 O(N×B2)O(N \times B^2) 的内存,节省内存的效果会因此被抵消。取而代之的做法是,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台7B32x A100 80GB1,000,000+ token32x(相较此前)
TPUv4-10243B1024 个 TPUv4 芯片16,000,000 token512x(相较此前)
A100 8台7B8x A100 80GB262,144 token8x

值得注意的是,"相较此前"的基准是内存高效 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 token0-2%
PCIe Gen564 GB/s~9,750 token5-15%
InfiniBand HDR50 GB/s~12,480 token10-25%
Ethernet 100G12.5 GB/s~49,920 token30-60%

在节点内 NVLink 环境中,只要块大小达到 1024 以上,通信就几乎可以被完全隐藏。但在节点间通信中,需要把块大小设置得大得多,在 Ethernet 环境下,实际上很难实现高效的 Ring Attention。这一结果表明,在制定 Ring Attention 的部署策略时,网络拓扑是一个决定性的设计变量。在云环境中搭建 GPU 集群时,需要慎重选择节点间带宽规格,如果可能,应优先考虑基于 NVLink 或 NVSwitch 的单节点多 GPU 配置。

比较分析:Ring Attention vs Sequence Parallelism vs Tensor Parallelism

比较分布式环境下处理长上下文的三种主要并行化策略。

特性Ring AttentionSequence Parallelism (DeepSpeed-Ulysses)Tensor Parallelism
划分对象序列维度(整体注意力)序列维度(基于注意力头)模型维度(权重划分)
通信模式P2P Ring (Send/Recv)All-to-AllAllReduce
通信量O(Bd)O(B \cdot d) per stepO(Ld/N)O(L \cdot d / N) per layerO(Bd)O(B \cdot d) per layer
注意力精度Exact(无近似)Exact(无近似)Exact(无近似)
最大并行度不受设备数量限制受注意力头数量限制受注意力头数量限制
GQA/MQA 兼容性完全兼容有限(头数不足)有限
通信-计算重叠可行(核心设计)不可行(同步 All-to-All)不可行(同步 AllReduce)
节点间扩展性满足块大小条件时表现良好依赖 All-to-All 带宽依赖 AllReduce 带宽
实现复杂度中等
内存效率极高(O(Bd)O(B \cdot d)高(O(L/Nd)O(L/N \cdot d)与模型规模成正比

核心差异点分析

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.ppermuteppermute 是 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 已经超越学术贡献,正在成为实质性的行业标准。

未来研究方向

  1. 自适应块大小:根据网络带宽与计算负载,在运行时动态调整块大小的技术
  2. Sparse Ring Attention:不循环全部 KV 块,而是基于 Top-k 选择性交换重要块的方法
  3. 异步流水线:把多个 Transformer 层的 Ring Attention 以流水线方式重叠,最大化整体吞吐量的技术
  4. 异构硬件优化:在 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,预计将在大规模语言模型的发展中,进一步巩固其作为核心基础设施技术的地位。

参考资料