位置编码演进

绝对位置编码

原始 Transformer 使用可学习的绝对位置编码:$E = E_{\text{token}} + E_{\text{pos}}$,其中 $E_{\text{pos}}$ 是可学习参数。

问题:无法外推到训练时未见过的长度。

相对位置编码

T5/ALBERT 使用相对位置偏置:$\text{Attn}(Q_i, K_j) = \frac{Q_i K_j^T}{\sqrt{d}} + b_{i-j}$

问题:需要额外偏置项,且对窗口外的位置需要截断。

旋转位置编码(RoPE)

RoPE(Su et al., 2021)的核心思想:通过旋转矩阵将绝对位置编码转化为相对位置编码——对位置 $m$ 的 Query 和位置 $n$ 的 Key,注意力分数只依赖 $m - n$。

RoPE 数学原理

二维情形

对于二维向量 $q = (q_0, q_1)$,在位置 $m$ 处旋转角度 $m\theta$:

$$q_m = R_m q = \begin{pmatrix} \cos m\theta & -\sin m\theta \ \sin m\theta & \cos m\theta \end{pmatrix} \begin{pmatrix} q_0 \ q_1 \end{pmatrix}$$

注意力分数:

$$\langle q_m, k_n \rangle = q^T R_m^T R_n k = q^T R_{n-m} k$$

关键性质:内积只依赖相对位置 $n - m$,且 $R_{n-m}$ 是旋转矩阵。

高维推广

将 $d$ 维向量分为 $d/2$ 个二维对,每对使用不同频率 $\theta_i$:

$$\theta_i = 10000^{-2i/d}, \quad i = 0, 1, \ldots, d/2 - 1$$

旋转矩阵(块对角形式):

$$R_m = \text{diag}\left(R(m\theta_0), R(m\theta_1), \ldots, R(m\theta_{d/2-1})\right)$$

实现形式

实际实现不构造稀疏旋转矩阵,而是用元素级乘法:

def apply_rope(q, positions, theta_base=10000.0):
    """
    q: (batch, seq_len, num_heads, head_dim)
    positions: (batch, seq_len) 位置索引
    """
    d = q.shape[-1]
    half = d // 2
    
    # 计算频率
    freqs = 1.0 / (theta_base ** (torch.arange(0, half).float() / half))
    # (batch, seq_len, half)
    angles = positions.unsqueeze(-1) * freqs  # (batch, seq_len, half)
    
    cos = angles.cos()  # (batch, seq_len, half)
    sin = angles.sin()  # (batch, seq_len, half)
    
    # 扩展到 full dim: [cos_0, cos_1, ..., cos_{d/2-1}, cos_0, ...]
    cos = torch.cat([cos, cos], dim=-1)  # (batch, seq_len, d)
    sin = torch.cat([sin, sin], dim=-1)
    
    # 旋转: [q0, q1, ..., q_{d/2}, q_{d/2+1}, ...]
    # → [q0*cos0 - q1*sin0, q1*cos0 + q0*sin0, ...]
    q_rot = q * cos + rotate_half(q) * sin
    return q_rot

def rotate_half(x):
    """将后半部分取负并交换: [a, b] → [-b, a]"""
    half = x.shape[-1] // 2
    return torch.cat([-x[..., half:], x[..., :half]], dim=-1)

为什么 RoPE 有效?

  1. 相对位置编码:$q_m^T k_n$ 只依赖 $m - n$
  2. 远程衰减:高频分量 $\theta_0$ 使得相近位置区分度高,低频分量 $\theta_{d/2-1}$ 捕捉长程关系
  3. 可外推:理论上对任意位置 $m$ 都可计算旋转,不限于训练范围

长度外推问题

问题定义

模型在长度 $L_{\text{train}}$ 上训练,推理时使用 $L_{\text{test}} > L_{\text{train}}$。直接外推会导致性能急剧下降。

原因分析

训练时模型只见过 $m\theta_i \in [0, L_{\text{train}} \cdot \theta_i]$。测试时 $m > L_{\text{train}}$,某些 $\cos(m\theta_i)$ 的值超出训练分布。

特别是高频分量($\theta_i$ 大)在长序列中周期变化快,外推时产生训练中未见的组合。

位置插值(PI)

最简单的方法:将测试位置线性缩放到训练范围:

$$m’ = m \cdot \frac{L_{\text{train}}}{L_{\text{test}}}$$

$$q’m = R{m’} q$$

优点:简单,少量微调即可工作。缺点:均匀缩放降低了短距离的分辨率。

NTK-aware 缩放

核心思想

不同频率需要不同的缩放策略:

  • 高频分量:不需要缩放(短距离信息保持精确)
  • 低频分量:需要缩放(扩展长距离范围)

修改 base 频率:

$$\theta_i’ = \left(\text{base} \cdot s^{d/(d-2i)}\right)^{-2i/d}$$

其中 $s = L_{\text{test}} / L_{\text{train}}$ 为缩放因子。

def ntk_aware_freqs(d, base=10000.0, scale=4.0):
    """NTK-aware 频率缩放"""
    half = d // 2
    # 原始频率
    freqs = 1.0 / (base ** (torch.arange(0, half).float() / half))
    # NTK 缩放: base -> base * scale^(d/(d-2))
    scaled_base = base * (scale ** (d / (d - 2)))
    freqs_scaled = 1.0 / (scaled_base ** (torch.arange(0, half).float() / half))
    return freqs_scaled

NTK 数学推导

RoPE 的有效波长为 $\lambda_i = 2\pi / \theta_i = 2\pi \cdot \text{base}^{2i/d}$。

NTK 的策略是让最低频分量(最长波长)扩展 $s$ 倍,最高频不变:

$$\lambda_i’ = \lambda_i \cdot s^{1 - 2i/d}$$

YaRN

YaRN(Peng et al., 2023)进一步改进 NTK,引入分段插值

  1. 将频率分为三个区间:高频(不插值)、中频(线性插值)、低频(NTK 缩放)
  2. 添加注意力温度缩放因子 $t$
def yarn_freqs(d, base=10000.0, original_len=4096, target_len=32768):
    """
    YaRN 频率计算
    """
    half = d // 2
    s = target_len / original_len
    
    # 计算每个频率的波长
    freqs = 1.0 / (base ** (torch.arange(0, half).float() / half))
    wavelengths = 2 * math.pi / freqs
    
    # 定义区间边界
    low_freq_wavelen = original_len
    high_freq_wavelen = original_len / (s ** 0.1)  # 经验值
    
    freqs_scaled = freqs.clone()
    for i in range(half):
        wl = wavelengths[i]
        if wl < high_freq_wavelen:
            # 高频: 不缩放
            pass
        elif wl > low_freq_wavelen:
            # 低频: 缩放 s 倍
            freqs_scaled[i] = freqs[i] / s
        else:
            # 中频: 线性插值
            alpha = (wl - high_freq_wavelen) / (low_freq_wavelen - high_freq_wavelen)
            freqs_scaled[i] = freqs[i] * (1 - alpha * (1 - 1/s))
    
    return freqs_scaled

def yarn_attention_scale(target_len, original_len):
    """YaRN 注意力温度缩放"""
    s = target_len / original_len
    if s <= 1:
        return 1.0
    return math.sqrt(1 + math.log(s) / math.log(original_len))

Longrope

Longrope(Microsoft, 2024)的关键创新:为不同维度搜索不同的缩放因子,而非使用统一公式。

def longrope_search(d, original_len, target_len, eval_fn):
    """
    Longrope: 搜索最优的每组频率缩放因子
    """
    half = d // 2
    freqs = 1.0 / (10000 ** (torch.arange(0, half).float() / half))
    
    # 搜索空间: 每个维度独立缩放
    best_lambdas = torch.ones(half)
    best_score = eval_fn(freqs)
    
    for i in range(half):
        for lam in np.linspace(0.5, 8.0, 50):
            candidate = best_lambdas.clone()
            candidate[i] = lam
            score = eval_fn(freqs * candidate)
            if score > best_score:
                best_score = score
                best_lambdas = candidate
    
    return freqs * best_lambdas

外推方法全面对比

方法 缩放策略 需微调 外推 8× 外推 32× 复杂度
直接外推 ❌ 严重降质 -
位置插值 (PI) 均匀线性 需~1000步 ✅ 良好 ⚠️ 下降
NTK-aware 频率自适应 否(少步更佳) ✅ 良好 ⚠️ 下降
YaRN 分段+温度 需~400步 ✅ 优秀 ✅ 良好
Longrope 逐维搜索 ✅ 优秀 ✅ 优秀

实测困惑度对比 (LLaMA-2 7B, 训练 4K)

外推到 直接 PI NTK YaRN Longrope
8K 5.42 5.31 5.28 5.25 5.23
16K 8.71 5.48 5.42 5.33 5.28
32K OOM 6.12 5.81 5.52 5.38
128K OOM 9.43 7.25 6.11 5.72

工程实现注意

1. 位置索引

不同框架的位置索引可能不同(从 0 或 1 开始),影响外推效果:

# LLaMA 系列: 从 0 开始
positions = torch.arange(0, seq_len)

# 某些实现: 第一个 token 位置为 0 但有 padding 偏移
positions = torch.arange(0, seq_len) + padding_offset

2. 与 Flash Attention 兼容

RoPE 需要在 Flash Attention kernel 内部应用,而非外部预处理:

# ✅ 正确: 在 Flash Attention kernel 内部应用 RoPE
from flash_attn import flash_attn_func
q_rot = apply_rope(q, positions)
k_rot = apply_rope(k, positions)
output = flash_attn_func(q_rot, k_rot, v, causal=True)

# ❌ 错误: 先做标准 attention 再应用 RoPE

3. 动态 NTK

推理时根据当前序列长度动态调整缩放因子:

def dynamic_ntk_scaling(seq_len, max_train_len=4096, base=10000.0):
    if seq_len <= max_train_len:
        return base
    scale = seq_len / max_train_len
    return base * (scale ** (d / (d - 2)))

总结

RoPE 通过旋转矩阵优雅地将绝对位置编码转化为相对位置编码,成为当前 LLM 的标准位置编码方案。长度外推方面,YaRN 和 Longrope 代表了 SOTA 方案,使 4K 训练的模型能够有效处理 128K+ 的上下文。选择建议:

  • 快速部署:NTK-aware(零微调)
  • 生产质量:YaRN(少量微调)
  • 极致外推:Longrope(搜索开销大但效果最好)

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

加入讨论

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

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