1. 为什么长上下文重要?

4K 上下文只能容纳约 3000 字的文档。要处理整本书、代码仓库或长对话,需要 32K-1M 的上下文窗口。但直接训练长上下文模型面临三大挑战:

  1. 训练成本:Attention 复杂度 $O(n^2)$,32K 上下文的计算量是 4K 的 64 倍
  2. 位置编码外推:训练时未见过的位置编码效果不可控
  3. 显存占用:KV Cache 随序列长度线性增长

2. 位置编码外推

2.1 Position Interpolation (PI)

最简单的方法:将位置索引线性缩放到训练范围内。

$$ m’ = m \cdot \frac{L_{train}}{L_{target}}


4K 训练 → 8K 推理时,位置 $m$ 缩放为 $m \times 0.5$。

**问题:** 高频分量被等比缩放,局部注意力模式被破坏。短距离关系(如相邻 Token)的编码精度下降。

### 2.2 NTK-aware Interpolation

NTK(Neural Tangent Kernel)理论指出,高频分量需要保留,低频分量可以缩放。修改 RoPE 的 base:

$$
b' = b \cdot s^{d/(d-2)}

其中 $s = L_{target} / L_{train}$ 是扩展倍数,$d$ 是 Head 维度。

效果: 高频部分(局部注意力)几乎不变,低频部分(长距离)被扩展。4K→16K 无需微调即可使用。

2.3 NTK-by-parts

将频率维度分为三段:

  • 频率高于 $\lambda_{high}$(波长远小于扩展后长度):不插值
  • 频率低于 $\lambda_{low}$(波长远大于扩展后长度):线性插值
  • 中间:平滑过渡
def ntk_by_parts_rope(head_dim, orig_len, target_len, alpha=1, beta=32):
    """NTK-by-parts 位置编码"""
    s = target_len / orig_len
    base = 10000.0
    
    freqs = 1.0 / (base ** (torch.arange(0, head_dim, 2).float() / head_dim))
    
    # 计算波长
    wavelengths = 2 * math.pi / freqs
    
    # 临界波长
    lambda_high = orig_len
    lambda_low = orig_len / (2 * math.pi) * beta
    
    # 分段缩放
    scale = torch.ones_like(freqs)
    for i in range(len(freqs)):
        wl = wavelengths[i]
        if wl < lambda_low:
            scale[i] = 1.0  # 高频不缩放
        elif wl > lambda_high:
            scale[i] = 1.0 / s  # 低频完全插值
        else:
            # 平滑过渡
            t = (lambda_high - wl) / (lambda_high - lambda_low)
            scale[i] = 1.0 - (1 - 1.0/s) * t
    
    freqs = freqs * scale
    return freqs

2.4 YaRN

YaRN 在 NTK-by-parts 基础上增加 Attention 温度缩放:

$$ t = 0.1 \cdot \ln(s) + 1, \quad \text{logits}’ = \frac{\text{logits}}{t}


温度缩放补偿了长上下文中 Attention 分布变得过尖的问题。YaRN 是目前最有效的 RoPE 外推方案之一。

### 2.5 LongRoPE

LongRoPE(Microsoft, 2024)将 RoPE 的频率缩放从均匀分组推广为**逐维度优化**:

- 不再按频率高低分区,而是为每个 2D 旋转平面独立搜索缩放因子
- 使用进化算法搜索最优缩放方案
- 可扩展到 2M 上下文

## 3. 动态 NTK

推理时根据当前序列长度动态调整 RoPE base:

$$
b'(l) = b \cdot \left(\frac{l}{L_{train}}\right)^{d/(d-2)}
$$

其中 $l$ 是当前序列长度。在短序列时保持原始编码,在长序列时自动缩放。

```python
class DynamicNTKScaling:
    def __init__(self, base=10000.0, train_len=4096, head_dim=128):
        self.base = base
        self.train_len = train_len
        self.head_dim = head_dim
    
    def get_scaling_factor(self, seq_len):
        if seq_len <= self.train_len:
            return 1.0
        s = seq_len / self.train_len
        return s ** (self.head_dim / (self.head_dim - 2))
    
    def get_base(self, seq_len):
        return self.base * self.get_scaling_factor(seq_len)

4. 分块注意力

4.1 Sliding Window Attention (SWA)

每个 Token 只关注固定大小的局部窗口:

$$ \text{Attention}(q_i, K, V) = \text{softmax}(q_i K_{[i-w, i]}^T) V_{[i-w, i]} $$

窗口大小 $w$ 通常为 4096。复杂度从 $O(n^2)$ 降到 $O(nw)$。

Mistral 使用 SWA + 全局注意力交替:每隔几层放一个全局注意力层,其余层用 SWA。

4.2 StreamingLLM

StreamingLLM 的关键发现:Attention 的"注意力 sinks"现象——第一个和最后几个 Token 获得不合理的超高注意力分数。

方法:保留初始几个 Token + 滑动窗口,丢弃中间部分:

[Attention Sinks] ... [Sliding Window]
  Token 0-3           Token (i-w) to i

效果:可以处理无限长度序列(流式),但丢失了中间信息。

4.3 Landmark Attention

将序列分为固定大小的块,每块用一个"地标"Token 代表。Attention 在块级别进行:

  • 块内:标准 Self-Attention
  • 块间:通过地标 Token 传递信息

复杂度从 $O(n^2)$ 降为 $O(n \cdot \sqrt{n})$。

5. Ring Attention

5.1 核心思想

Ring Attention(Liu et al., 2023)将长序列分布到多个 GPU 上,GPU 组成环形拓扑:

  1. 每个 GPU 持有一段序列的 Q, K, V
  2. K, V 在 GPU 间环形传递
  3. 每经过一个 GPU,计算当前 Q 与该 GPU 的 K, V 的部分 Attention

5.2 计算-通信重叠

时间线:
GPU 0: 计算 Q0×K0 | 发送 K0,V0 → GPU1 | 计算 Q0×K1 | 发送 K1,V1 → GPU2 | ...
GPU 1: 计算 Q1×K1 | 发送 K1,V1 → GPU2 | 计算 Q1×K2 | 发送 K2,V2 → GPU3 | ...
GPU 2: 计算 Q2×K2 | 发送 K2,V2 → GPU3 | 计算 Q2×K3 | 发送 K3,V3 → GPU0 | ...
GPU 3: 计算 Q3×K3 | 发送 K3,V3 → GPU0 | 计算 Q3×K0 | 发送 K0,V0 → GPU1 | ...

通信(K, V 传输)与计算(部分 Attention)并行,理论上可以扩展到无限长度(只要 GPU 足够)。

5.3 实现要点

def ring_attention_forward(Q, K, V, comm):
    """简化版 Ring Attention"""
    rank = comm.get_rank()
    world_size = comm.get_size()
    
    # 本地 Q/K/V
    local_q, local_k, local_v = Q, K, V
    
    # 存储 K/V 的缓冲区
    k_buffer, v_buffer = local_k.clone(), local_v.clone()
    
    output = torch.zeros_like(local_q)
    
    for step in range(world_size):
        # 计算当前 Q 对 K_buffer, V_buffer 的部分 Attention
        block_output = compute_block_attention(local_q, k_buffer, v_buffer)
        output += block_output
        
        if step < world_size - 1:
            # 异步发送 K,V 到下一个 GPU
            send_req = comm.isend(k_buffer, dst=(rank+1) % world_size)
            # 接收前一个 GPU 的 K,V
            recv_k = torch.empty_like(k_buffer)
            comm.recv(recv_k, src=(rank-1) % world_size)
            send_req.wait()
            k_buffer = recv_k
            # V 同理
    
    return output

5.4 Flash Ring Attention

结合 Flash Attention 的 tiling 技巧,Ring Attention 可以在不实例化完整 $n \times n$ Attention 矩阵的情况下完成长序列计算。实测在 8×A100 上可以处理 1M 上下文。

6. KV Cache 优化

长上下文的另一个瓶颈是 KV Cache 显存。以 LLaMA-2 70B 为例:

上下文长度KV Cache 大小 (FP16)
4K10.5 GB
32K84 GB
128K336 GB
1M2.6 TB

6.1 量化 KV Cache

将 KV Cache 从 FP16 量化为 INT8 或 INT4:

  • INT8: 显存减半,精度损失 < 1%
  • INT4: 显存减 75%,需要特殊处理精度

6.2 PagedAttention (vLLM)

借鉴操作系统的虚拟内存管理,将 KV Cache 分为固定大小的 Page:

  • 不需要为最大长度预分配连续内存
  • 动态分配/释放 Page,减少碎片
  • 支持共享前缀(多个请求共享同一 system prompt 的 KV Cache)

7. 效果对比

方法最大长度需要微调质量保持工程复杂度
直接外推1.5x
PI4x少量
NTK-aware8x
YaRN32x少量
LongRoPE500x少量
Ring Attention无限*
SWA + 全局无限

*受 GPU 数量限制

8. 实践建议

  1. 4K→32K:YaRN 微调是最性价比方案
  2. 32K→128K:YaRN + Ring Attention(多 GPU)
  3. 128K+:LongRoPE + PagedAttention + 量化 KV Cache
  4. 流式场景:StreamingLLM + SWA
  5. 微调数据:长文本微调需要包含长文档的 QA 数据,否则模型有"长文本窗口"但不会利用
  6. 评估:除了 perplexity,用 Needle in a Haystack 测试验证模型是否真的能检索到长上下文中的信息

加入讨论

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

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