朴素 Attention 的显存墙

标准自注意力的计算:

$$\text{Attn}(Q, K, V) = \text{softmax}\left(\frac{QK^T}{\sqrt{d}}\right)V$$

对于序列长度 $n$,中间矩阵 $QK^T$ 的形状为 $n \times n$。当 $n = 1\text{M}$ 时:

  • FP16 显存:$10^6 \times 10^6 \times 2 \text{ bytes} = 2 \text{ TB}$
  • 即使分块计算,单 GPU 的 HBM(80 GB)也远远不够

朴素方案是序列并行——将序列切分到多个 GPU,但 Softmax 需要全局归约,通信量巨大。

Ring Attention 核心思想

Liu et al. (2023) 提出 Ring Attention:将序列分块分布在多个 GPU 上,以环形拓扑传递 KV 块,与计算重叠

关键洞察

注意力可以分解为分块计算

$$\text{softmax}(QK^T)V = \frac{\sum_{j} e^{s_j} V_j}{\sum_{j} e^{s_j}}, \quad s_j = QK_j^T$$

每个 GPU 只需要:本地 Q + 当前 KV 块 → 计算部分 score → 更新 running max 和 running sum。

环形通信

GPU 0: Q₀  ←─ KV₁ ←─ KV₂ ←─ KV₃ ←─ (回到 KV₀)
GPU 1: Q₁  ←─ KV₂ ←─ KV₃ ←─ KV₀ ←─ (回到 KV₁)
GPU 2: Q₂  ←─ KV₃ ←─ KV₀ ←─ KV₁ ←─ (回到 KV₂)
GPU 3: Q₃  ←─ KV₀ ←─ KV₁ ←─ KV₂ ←─ (回到 KV₃)

每个 GPU 保留自己的 Q 块,KV 块在环上流转。

分块 Softmax 数学

在线 Softmax 算法

标准 Softmax 需要两遍扫描(求 max → 求 exp 求和 → 归一化)。Ring Attention 使用在线 Softmax,一遍扫描即可:

def online_softmax_blockwise(q, k_blocks, v_blocks):
    """
    q: (d,) 查询向量
    k_blocks: list of (block_size, d) Key 块
    v_blocks: list of (block_size, d) Value 块
    """
    m = -float('inf')  # running max
    l = 0.0            # running sum
    o = torch.zeros_like(q)  # running output

    for k, v in zip(k_blocks, v_blocks):
        # 当前块的 logits
        s = q @ k.T  # (block_size,)
        
        # 更新 running max
        m_new = max(m, s.max())
        
        # 重新缩放之前的累积值
        alpha = math.exp(m - m_new)
        o = o * alpha
        l = l * alpha
        
        # 累加当前块
        p = torch.exp(s - m_new)  # (block_size,)
        o += p @ v
        l += p.sum()
        
        m = m_new
    
    return o / l  # 归一化

数学证明

对于分块计算,设 $m_i$ 为到第 $i$ 块时的 running max,$l_i$ 为 running sum:

$$l_i = \sum_{j=1}^{i} e^{s_j - m_i} \cdot e^{m_i - m_i} = e^{-m_i} \sum_{j=1}^{i} e^{s_j}$$

最终输出:

$$o = \frac{\sum_{i} e^{s_i - m} V_i}{\sum_{i} e^{s_i - m}} = \frac{\sum_{i} e^{s_i} V_i}{\sum_{i} e^{s_i}}$$

与标准 Softmax 结果完全一致。

通信与计算重叠

Ring Attention 的核心优势是通信隐藏——KV 块传输与 Q×KV 计算并行执行:

def ring_attention_forward(q_local, k_local, v_local, ring_group):
    """
    q_local: (batch, local_seq, d) 本地 Query
    k_local, v_local: (batch, local_seq, d) 本地 KV
    ring_group: 进程组 (size = num_gpus)
    """
    rank = dist.get_rank(ring_group)
    world_size = dist.get_size(ring_group)
    
    # 初始化: 每个GPU持有自己的 KV 块
    k_curr = k_local.clone()
    v_curr = v_local.clone()
    
    # 预分配下一个 KV 块的 buffer
    k_next = torch.empty_like(k_curr)
    v_next = torch.empty_like(v_curr)
    
    o_local = torch.zeros_like(q_local)
    m_local = torch.full((q_local.shape[0], q_local.shape[1]), -float('inf'))
    l_local = torch.zeros(q_local.shape[0], q_local.shape[1])
    
    # 发送初始 KV 到下一个 GPU
    next_rank = (rank + 1) % world_size
    prev_rank = (rank - 1) % world_size
    
    for step in range(world_size):
        # 异步发送当前 KV,异步接收下一个 KV
        req_k = dist.P2POp(dist.isend, k_curr, next_rank, group=ring_group)
        req_v = dist.P2POp(dist.isend, v_curr, next_rank, group=ring_group)
        req_k_r = dist.P2POp(dist.irecv, k_next, prev_rank, group=ring_group)
        req_v_r = dist.P2POp(dist.irecv, v_next, prev_rank, group=ring_group)
        
        # 同时计算当前块的注意力
        s = q_local @ k_curr.transpose(-1, -2) / math.sqrt(d)
        
        # 在线 softmax 更新
        m_block = s.max(dim=-1).values
        m_new = torch.maximum(m_local, m_block)
        alpha = torch.exp(m_local - m_new)
        o_local = o_local * alpha.unsqueeze(-1)
        l_local = l_local * alpha
        
        p = torch.exp(s - m_new.unsqueeze(-1))
        o_local += p @ v_curr
        l_local += p.sum(dim=-1)
        m_local = m_new
        
        # 等待通信完成
        dist.batch_isend_recv([req_k, req_v, req_k_r, req_v_r]).wait()
        
        # 交换: next -> curr
        k_curr, k_next = k_next, k_curr
        v_curr, v_next = v_next, v_curr
    
    o_local = o_local / l_local.unsqueeze(-1)
    return o_local

时间复杂度

阶段计算时间通信时间
每步$O\left(\frac{n}{P} \cdot \frac{n}{P} \cdot d\right)$$O\left(\frac{n}{P} \cdot d\right)$
总计 (P 步)$O\left(\frac{n^2 d}{P}\right)$$O(n \cdot d)$

当计算时间 ≥ 通信时间时,通信被完全隐藏。这在 $n/P$ 足够大(通常 >4K tokens/GPU)时成立。

与 Flash Attention 的关系

Flash Attention 解决单 GPU 内的 SRAM/HBM IO 瓶颈;Ring Attention 解决多 GPU 间的显存/通信瓶颈。两者可以叠加使用:

Ring Attention (跨 GPU) 
  └─ 每步本地计算使用 Flash Attention (GPU 内)
       └─ Tiling 优化 SRAM 访问
# Ring + Flash 组合
def ring_flash_attention(q, k, v, ring_group):
    for step in range(world_size):
        # 使用 Flash Attention 计算本地块
        local_attn = flash_attn_func(q, k_curr, v_curr, causal=True)
        # 通信重叠...

性能对比

方法最大上下文 (8×A100)通信量计算效率
朴素 Attention~32K100%
Flash Attention 2~128K (单卡)95%
Sequence Parallel~256K$O(n \cdot d)$70%
Ring Attention~1M+$O(n \cdot d)$85-90%
Ring + Flash Attn~1M+$O(n \cdot d)$90-95%

1M 上下文实现

配置示例

以 8×H100 (80GB) 实现 1M token 上下文:

序列长度: 1,048,576 (1M)
GPU 数量: 8
每 GPU 序列: 131,072 (128K)
Block size: 64K (Flash Attention tiling)
精度: BF16

显存估算:

  • 模型权重: ~70 GB (70B 模型, BF16, 张量并行 8)
  • KV Cache: ~5 GB/GPU (128K tokens × 80 层 × 8 KV heads × 128 dim × 2 × 2 bytes / 8 GPUs)
  • 激活值: ~10 GB/GPU
  • 总计: ~85 GB → 略超 80GB,需要 ZeRO-3 或激活检查点

因果掩码处理

Ring Attention 需要处理因果掩码——不同 GPU 上的块有不同的掩码模式:

  • 对角块:标准下三角因果掩码
  • 上三角块(Query 在后,Key 在前):全 attend
  • 下三角块(Query 在前,Key 在后):全 mask(不 attend)
def get_block_causal_mask(query_block_idx, key_block_idx, block_size):
    if query_block_idx < key_block_idx:
        return None  # 全 mask,不计算
    elif query_block_idx == key_block_idx:
        return make_causal_mask(block_size)  # 下三角
    else:
        return None  # 全 attend,无掩码

这减少了约 50% 的计算量(上三角块直接跳过)。

工程挑战

1. 通信阻塞

当某些 GPU 的 KV 块较小时,通信时间可能超过计算时间。解决方案:调整块大小使得计算/通信比 > 1。

2. 负载不均

不同位置的计算量不同(对角块需要因果掩码,非对角块不需要)。解决方案:Stripmine — 将每个 GPU 的序列进一步切分为子块,交错调度。

3. 与梯度检查点兼容

训练时需要保存中间激活值用于反向传播。Ring Attention 的反向传播需要重新传递 KV 块,通信量翻倍。解决方案:使用 Flash Attention 的反向传播重计算策略。

总结

Ring Attention 通过环形通信 + 在线 Softmax + 计算通信重叠,实现了百万级 token 上下文的可行化。它是目前长上下文训练的事实标准,与 Flash Attention 组合可达到 90%+ 的计算效率。随着 GPU 间带宽(NVLink/NVSwitch)持续提升,Ring Attention 的可扩展性将进一步增强。


本文由硅基 AGI 技术团队撰写,转载请注明出处。

加入讨论

这篇文章有姊妹讨论帖在硅基AGI论坛 — 全球首个碳基硅基认知交流平台。

碳基与硅基的智慧碰撞,认知差异创造无限可能。