RoPE 完全拆解:旋转位置编码的原理、实现与长度外推

7596 字
38 分钟
RoPE 完全拆解:旋转位置编码的原理、实现与长度外推

背景:为什么 Transformer 必须显式编码位置#

自注意力(self-attention)有一个经常被忽略的性质:它是排列不变的(permutation invariant)。把输入序列 x1,x2,,xnx_1, x_2, \dots, x_n 打乱顺序重新排列,输出中每个位置的向量只是跟着重新排列,计算过程本身完全不感知顺序——因为注意力做的就是”每个 token 跟其他所有 token 做内积再加权求和”,而求和与顺序无关。

这对语言模型是致命的。句子”我不爱你”和”你不爱我”的词完全相同,只有顺序不同,语义却相反。如果模型看不见顺序,这两句话对它毫无区别。因此 Transformer 必须显式把位置信息注入到表示中,这就是位置编码(positional encoding)存在的意义。

围绕”怎么注入位置”这个问题,2021 年之前的主流方案可以分成两类:

绝对位置编码(absolute positional encoding):把位置当作一个附加的”标签”加到 token 上。

  • 可学习嵌入(learned embedding,BERT 使用):训练一个位置向量表,位置 mm 对应一个可学习向量 pmp_m,加到 token embedding 上。简单直接,但有一个硬伤——训练时最多见过 512 个位置(BERT 的 max length),推理时序列一旦超过 512 就无向量可用。
  • 三角位置编码(sinusoidal,原始 Transformer 使用):用固定频率的正弦、余弦函数生成位置向量,理论上可以推广到任意长度。但它把位置信息”加”进 embedding 后,经过多层注意力、MLP 的叠加,位置信号会被逐层稀释、与内容信号纠缠,模型很难提取出干净的”相对距离”信息。

相对位置编码(relative positional encoding):不编码”第 m 个位置”,而是编码”两个 token 相隔多远”。

  • T5 relative bias:在注意力得分上加一个只依赖 mnm-n 的可学习偏置。
  • Transformer-XL / Shaw et al.:在注意力公式里显式加入相对位置项。
  • ALiBi:在注意力得分上线性减去 mn|m-n| 倍的斜率。

相对位置编码信息更精准,但代价是:要么修改注意力公式的结构(增加实现复杂度、破坏与 FlashAttention 等优化 kernel 的兼容性),要么额外引入可学习参数。

RoPE(Rotary Position Embedding,旋转位置编码)于 2021 年由苏剑林团队提出(RoFormer 论文,NeurIPS 2021),它给出了一条不同的路线:用旋转矩阵把绝对位置”乘”进 query 和 key 的向量里,让内积结果天然只依赖相对位置。形式上它像绝对位置编码(实现简单,不碰注意力公式),性质上却是相对位置编码(内积只依赖位置差)。这个”绝对的形式、相对的本质”让它成为 LLaMA、Mistral、Qwen、DeepSeek 等几乎所有主流开源模型的标准配置——你在前面读过的 GQAMLADeepSeek-V4 混合注意力 里,RoPE 都是地基。

这篇文章从数学原理、工程实现到长度外推,把 RoPE 拆到最底层。

核心思想:把”位置差”变成”旋转角差”#

目标函数:内积只依赖相对位置#

先放下具体形式,想想我们希望位置编码满足什么条件。设 fq(xm,m)f_q(x_m, m) 是把位置 mm 编码进 query 向量的函数(xmx_m 是第 mm 个 token 的向量),fk(xn,n)f_k(x_n, n) 同理。注意力得分的核心是内积 fq(xm,m),fk(xn,n)\langle f_q(x_m, m), f_k(x_n, n) \rangle。我们希望这个内积只依赖相对位置 mnm-n,而不依赖 mmnn 各自是多少,即存在某个函数 gg 使得:

fq(xm,m),fk(xn,n)=g(xm,xn,mn)\langle f_q(x_m, m),\, f_k(x_n, n) \rangle = g(x_m, x_n, m - n)

翻译成人话:无论第 5 个 token 和第 9 个 token,还是第 1005 个和第 1009 个 token,只要相对距离都是 4,注意力得分就应该一样(平移不变性)。这是 RoPE 全部设计的出发点。

二维情形:完整的求解过程#

论文先考虑最简单的 d=2d=2 情形,用复数把二维向量看作平面上的点。这一步值得完整走一遍,因为它揭示了”为什么答案必须是旋转”。

把二维向量 (a,b)(a, b) 写成复数 a+bia + bi。两个二维向量的内积等于复数乘积的实部:

q,k=Re[qk]\langle q, k \rangle = \operatorname{Re}[\, q \cdot k^* \,]

其中 kk^*kk 的共轭复数(实部不变、虚部取负)。再假设 fq(xm,m)f_q(x_m, m) 可以写成极坐标形式 Rq(xm,m)eiΘq(xm,m)R_q(x_m, m) e^{i\Theta_q(x_m, m)},其中 RqR_q 是模长、Θq\Theta_q 是幅角,fkf_k 同理。把它们代进目标等式,注意到位置 m=nm = n 时两个 token 位置相同,应有 g(xm,xm,0)=qkg(x_m, x_m, 0) = \|q\|\|k\|(此时退化成普通内积),可以得到两条关键结论:

第一,模长与位置无关。 对比两边模长:

Rq(xm,m)Rk(xn,n)=Rg(xm,xn,mn)R_q(x_m, m) \cdot R_k(x_n, n) = R_g(x_m, x_n, m - n)

m=nm = n 时右边是 Rg(xm,xn,0)=qkR_g(x_m, x_n, 0) = \|q\| \|k\|,与 mm 无关;要让两边恒等,只能让 RqR_qRkR_k 与位置无关,即模长不变。直观上也说得通:位置编码不该改变”这个 token 内容有多大”,只该改变”它的朝向”。

第二,幅角按等差增长。 对比两边幅角,令 m=nm = n 得幅角差与内容无关、只与位置有关。记公共的幅角函数为 Θf(x,m)\Theta_f(x, m),则对任意 token xx

Θf(x,m)=ϕ(m)+θx\Theta_f(x, m) = \phi(m) + \theta_x

其中 θx\theta_x 是 token 自身方向(m=0m=0 时的幅角)。再把 n=m+1n = m+1 代入,可以得到 ϕ(m+1)ϕ(m)\phi(m+1) - \phi(m) 是与 mm 无关的常数——换句话说,每前进一个位置,幅角转过的角度恒定,所以 ϕ(m)\phi(m) 必须是等差序列:

ϕ(m)=mθ+γ\phi(m) = m\theta + \gamma

γ=0\gamma = 0,并记 q=fq(xm,0)=Wqxmq = f_q(x_m, 0) = W_q x_mk=fk(xn,0)=Wkxnk = f_k(x_n, 0) = W_k x_nWq,WkW_q, W_k 是 query/key 投影矩阵,位置 0 时函数退化为普通投影),就得到最终的答案:

fq(xm,m)=(Wqxm)eimθfk(xn,n)=(Wkxn)einθ\begin{aligned} f_q(x_m, m) &= (W_q x_m)\, e^{im\theta} \\ f_k(x_n, n) &= (W_k x_n)\, e^{in\theta} \end{aligned}

其中 θR\theta \in \mathbb{R} 是预设的非零常数。写成矩阵形式,复数乘法 eimθe^{im\theta} 就是二维平面上的旋转矩阵:

Rm=(cosmθsinmθsinmθcosmθ)R_m = \begin{pmatrix} \cos m\theta & -\sin m\theta \\ \sin m\theta & \cos m\theta \end{pmatrix}

“旋转”这个名字的由来就在这里:给向量乘上 eimθe^{im\theta},就是把它在平面上逆时针旋转 mθm\theta 弧度。位置 mm 越大,转过的角度越多。

论文里的图 1 直观展示了这个过程——左边是原始 query/key 向量 (x1,x2)(x_1, x_2),右边是旋转 mθ1m\theta_1 之后的位置编码向量:

RoFormer 论文图 1:RoPE 的旋转实现示意——原始向量 (x1, x2) 逆时针旋转 m·θ1 得到位置编码后的向量
RoFormer 论文图 1:RoPE 的旋转实现示意——原始向量 (x1, x2) 逆时针旋转 m·θ1 得到位置编码后的向量

(图源:RoFormer 论文 Figure 1。图中 θ1\theta_1 是第一个频率,mθ1m\theta_1 是位置 mm 对应的旋转角,位置 1、2、5 等分别转过不同角度。)

为什么内积自动变成相对位置#

现在把 fqf_qfkf_k 代入内积,关键的一步来了——两个旋转矩阵相乘,角度直接相减:

fq(xm,m),fk(xn,n)=(RmWqxm)(RnWkxn)=(Wqxm)RmRn(Wkxn)=(Wqxm)Rnm(Wkxn)\begin{aligned} \langle f_q(x_m, m), f_k(x_n, n) \rangle &= (R_m\, W_q x_m)^\top (R_n\, W_k x_n) \\ &= (W_q x_m)^\top R_m^\top R_n\, (W_k x_n) \\ &= (W_q x_m)^\top R_{n-m}\, (W_k x_n) \end{aligned}

最后一步用到了旋转矩阵的两个性质:Rm=RmR_m^\top = R_{-m}(转置等于逆,因为旋转矩阵是正交矩阵),以及 RmRn=RnmR_{-m} R_n = R_{n-m}(旋转可以叠加)。于是位置信息在注意力得分里只剩下 nmn - m 一个量,两个 token 相隔 5 步还是相隔 500 步,取决于旋转角之差,而不是各自的位置。这正是上一节目标等式要求的形式。

这个推导值得停下来体会一下:整个过程没有”设计”编码公式,而是先写下”内积只依赖相对位置”这个需求,然后通过复数运算一步步逼出了唯一合理的解——旋转。苏剑林在博客里总结为”通过绝对位置编码的方式实现相对位置编码”,一句话点破了它的双重身份。

高维推广:分块旋转矩阵与频率序列#

二维情形的解很漂亮,但实际的 hidden size 通常是 128、256、4096 这样的偶数维。把 dd 维空间拆成 d/2d/2 个互相独立的二维子空间,每个子空间里做旋转,就得到分块对角矩阵形式的旋转矩阵:

RΘ,md=(cosmθ1sinmθ1sinmθ1cosmθ1cosmθ2sinmθ2sinmθ2cosmθ2cosmθd/2sinmθd/2sinmθd/2cosmθd/2)R_{\Theta, m}^d = \begin{pmatrix} \cos m\theta_1 & -\sin m\theta_1 & & & & \\ \sin m\theta_1 & \cos m\theta_1 & & & & \\ & & \cos m\theta_2 & -\sin m\theta_2 & & \\ & & \sin m\theta_2 & \cos m\theta_2 & & \\ & & & & \ddots & \\ & & & & \cos m\theta_{d/2} & -\sin m\theta_{d/2} \\ & & & & \sin m\theta_{d/2} & \cos m\theta_{d/2} \end{pmatrix}

每个二维块负责一对维度,使用自己的角频率 θi\theta_i。频率序列的取值是:

θi=100002i/d,i=0,1,,d/21\theta_i = 10000^{-2i/d}, \quad i = 0, 1, \dots, d/2 - 1

这是一个几何级数:从 θ0=1\theta_0 = 1(最”慢”,转一圈要 2π2\pi 个位置)到 θd/21=10000(d2)/d\theta_{d/2-1} = 10000^{-(d-2)/d}(最”快”)。为什么要用几何级数而不是统一一个频率?有两个原因:

  1. 呼应原始 Transformer 的 sinusoidal 编码:Vaswani 等人 2017 年的三角位置编码用的正是 100002i/d10000^{-2i/d} 这个几何级数频率,实验证明它比均匀频率效果好。RoPE 沿用了这个被验证过的设计。
  2. 频率分工:慢频率(小 θ\theta)的旋转周期长,能区分”很远”的位置,负责长距离依赖;快频率(大 θ\theta)的旋转周期短,对相邻位置敏感,负责局部信息。多尺度频率让不同维度各司其职——这个特性在后面讲长度外推时会反复出现。

旋转矩阵还有两个对工程至关重要的性质:

  • 正交性RΘ,mdR_{\Theta,m}^d 是正交矩阵(RR=IR^\top R = I),转置就是逆,所以 Rnmd=(RΘ,md)RΘ,ndR_{n-m}^d = (R_{\Theta,m}^d)^\top R_{\Theta,n}^d 成立,数值上非常稳定,不会像普通矩阵连乘那样积累误差。
  • 保范性:旋转不改变向量长度 Rx=x\|R x\| = \|x\|。这意味着位置编码不会扰动注意力 softmax 的分母——qk/dq^\top k / \sqrt{d} 的尺度完全由内容决定,位置只改变方向。论文还利用这一点把 RoPE 推广到了线性注意力上(旋转可以放进 kernel 里而不破坏归一化,这是加法型位置编码做不到的)。

另外回答一个常见疑问:为什么只旋转 Q 和 K,不旋转 V? 因为位置信息需要进入的是注意力得分 qkq^\top k(决定”谁和谁有关”),而 V 只负责”提供内容”。旋转是等距变换,把 V 全部旋转一遍不改变加权和的结果,只会白费算力,所以 V 保持原样。KV cache 里也只需要对 K 施加位置、V 直接存原值。

远程衰减:为什么远处的 token 相关性天然变弱#

自然语言里有个经验规律:距离越远的词,语义关联通常越弱。位置编码如果能”内置”这个偏置,模型学起来会轻松很多。RoPE 恰好做到了这一点——论文证明了在 θi=100002i/d\theta_i = 10000^{-2i/d} 的设置下,注意力得分的大小随相对距离增大而衰减,这个性质叫 long-term decay(远程衰减)。

先看怎么把 qkq^\top k 写成复数求和。把 q=Wqxmq = W_q x_mk=Wkxnk = W_k x_n 按维度两两配对,每个内积项就是一对复数的乘积:

(RΘ,mq)(RΘ,nk)=Re[i=0d/21q[2i:2i+1]k[2i:2i+1]ei(mn)θi](R_{\Theta,m} q)^\top (R_{\Theta,n} k) = \operatorname{Re} \left[ \sum_{i=0}^{d/2-1} q_{[2i:2i+1]} \, k_{[2i:2i+1]}^* \, e^{i(m-n)\theta_i} \right]

其中 q[2i:2i+1]q_{[2i:2i+1]} 表示 qq 的第 2i2i 和第 2i+12i+1 维组成的复数。整个求和就是 d/2d/2 个”旋转后的复数”相加,每个复数带着自己的相位 (mn)θi(m-n)\theta_i

问题是:为什么一堆相位不同的复数相加,和的模长会随 mnm-n 衰减? 论文用 Abel 变换(离散分部求和)给出了上界。记 hi=q[2i:2i+1]k[2i:2i+1]h_i = q_{[2i:2i+1]} k_{[2i:2i+1]}^*Sj=i=0j1ei(mn)θiS_j = \sum_{i=0}^{j-1} e^{i(m-n)\theta_i},则

i=0d/21hiei(mn)θi=i=0d/21Si+1(hi+1hi)\sum_{i=0}^{d/2-1} h_i e^{i(m-n)\theta_i} = -\sum_{i=0}^{d/2-1} S_{i+1} (h_{i+1} - h_i)

取模长并用三角不等式放缩:

i=0d/21hiei(mn)θimaxihi+1hii=0d/21Si+1\left| \sum_{i=0}^{d/2-1} h_i e^{i(m-n)\theta_i} \right| \le \max_i |h_{i+1} - h_i| \cdot \sum_{i=0}^{d/2-1} |S_{i+1}|

hih_i 是相邻维度对的乘积,它的一阶差分 maxihi+1hi\max_i |h_{i+1}-h_i| 是有界的;而关键的量是右边那个几何级数和 iSi\sum_i |S_i|。论文证明,当 θi=100002i/d\theta_i = 10000^{-2i/d} 时,SiS_i 的均值 1d/2i=1d/2Si\frac{1}{d/2} \sum_{i=1}^{d/2} |S_i| 随着 mn|m-n| 增大而单调衰减,衰减曲线的形状如论文图 2 所示:

RoFormer 论文图 2:远程衰减——相对距离增大时,注意力得分的相对上界随之衰减
RoFormer 论文图 2:远程衰减——相对距离增大时,注意力得分的相对上界随之衰减

(图源:RoFormer 论文 Figure 2。横轴是相对距离 mn|m-n|,纵轴是上界 1d/2i=1d/2Si\frac{1}{d/2}\sum_{i=1}^{d/2}|S_i| 的相对值,曲线随距离增大而下降。)

这个衰减的直觉其实很朴素:几何级数的频率 θi\theta_i 让”旋转角”按维度从慢到快排开。当 mn|m-n| 很大时,大部分高频项的相位 ei(mn)θie^{i(m-n)\theta_i} 已经转了好几圈,在复平面上指向四面八方、互相抵消;只有少数低频项还保持大致同向。抵消得越多,总和的模就越小。

但要注意两点:

  1. 远程衰减是”软”约束。它只说明内积上界随距离收缩,模型完全可以在训练中通过调节 hih_i(即学出来的 qqkk 分量)来放大某个特定距离的响应。它内置的是”先验倾向”,不是硬性限制。
  2. 衰减并非均匀。不同头、不同维度对距离的敏感度不同,模型可以学到”某些头专注局部、某些头专注远程”的分工,这正是多头注意力的灵活性所在。

工程实现:布局约定、PyTorch 代码与推理侧细节#

数学上”稀疏矩阵乘法”,工程上”逐元素乘加”#

直接拿分块对角矩阵 RΘ,mdR_{\Theta,m}^d 去乘向量是浪费的——矩阵里几乎全是零。论文给出的高效实现把旋转写成逐元素运算。对第 ii 对维度 (x2i,x2i+1)(x_{2i}, x_{2i+1})(这里用 0 起始下标):

(x2ix2i+1)=(cosmθisinmθisinmθicosmθi)(x2ix2i+1)\begin{pmatrix} x_{2i}' \\ x_{2i+1}' \end{pmatrix} = \begin{pmatrix} \cos m\theta_i & -\sin m\theta_i \\ \sin m\theta_i & \cos m\theta_i \end{pmatrix} \begin{pmatrix} x_{2i} \\ x_{2i+1} \end{pmatrix}

整个向量上就是:

RΘ,mdx=x(cosmθi 每维重复)+rotate_half(x)(sinmθi 每维重复)R_{\Theta,m}^d x = x \otimes (\cos m\theta_i \text{ 每维重复}) + \text{rotate\_half}(x) \otimes (\sin m\theta_i \text{ 每维重复})

其中 rotate_half\text{rotate\_half} 交换配对维度并给其中一个取负号。全部计算只有乘法和加法,没有矩阵乘法——两个长度为 dd 的向量逐元素乘,总共 O(d)O(d) 次运算。

两种布局约定:interleaved 与 split-half#

“哪两个维度配成一对”有两种约定,这是阅读各种开源实现时最容易踩的坑:

约定配对方式代表实现
interleaved(交错)相邻维度配对 (x2i,x2i+1)(x_{2i}, x_{2i+1})GPT-J、HuggingFace 的 RoFormer 实现
split-half(半切)前半与后半配对 (xi,xi+d/2)(x_i, x_{i + d/2})GPT-NeoX、LLaMA、Mistral

两种约定数学上完全等价(都是同样的旋转,只是坐标顺序不同),但频率 θi\theta_i 的对应关系不同。split-half 的实现里,rotate_half 操作是”把后半部分取负后接到前半部分后面”:

def rotate_half(x):
"""split-half 约定下的配对旋转:交换两半并给后半取负。"""
x1 = x[..., : x.shape[-1] // 2] # 前半
x2 = x[..., x.shape[-1] // 2 :] # 后半
return torch.cat([-x2, x1], dim=-1)
def apply_rotary_pos_emb(x, cos, sin):
return x * cos + rotate_half(x) * sin

而 interleaved 约定下最自然的写法是直接把相邻对当复数乘:

def apply_rotary_pos_emb_interleaved(x, cos, sin):
# x: [..., d] 视为 [..., d/2, 2],用 view_as_complex 转成复数
x_complex = torch.view_as_complex(x.reshape(*x.shape[:-1], -1, 2))
# freqs_cis[m] = cos(mθ) + i·sin(mθ),预计算好的一张复数表
out = x_complex * freqs_cis
return torch.view_as_real(out).flatten(-2)

实际推理框架里几乎都预计算好 cos\cossin\sin 表:对每个位置 mm 和每个频率 θi\theta_i,预先算出 cosmθi\cos m\theta_isinmθi\sin m\theta_i,存成形状为 [max_seq_len, d/2] 的常量张量(复数版就是 freqs_cis),推理时只做查表 + 乘加。生成过程中序列长度逐步增长,但每个 token 的旋转角在它生成那一刻就固定了,之前算过的位置无需重算。

推理侧:decode 时的位置、KV cache 与融合 kernel#

在自回归解码中,RoPE 的具体行为值得单独理一遍:

  1. Prefill 阶段:对整段 prompt 的每个 token 施加各自的位置旋转,然后算注意力。此时位置 0 到 L1L-1cos/sin\cos/\sin 表都要用到。
  2. Decode 阶段:每次只生成 1 个新 token,它位于位置 m=m = 当前序列长度。只需要对这一 token 查一次表、做一次旋转;已有的 KV 全部不用动(它们的旋转角在写入时就固定了)。
  3. KV cache:K 旋转之后再写入缓存(因为注意力需要的是旋转后的 K),V 原样写入。KV cache 里存的是”已带位置信息”的 K——这也是为什么 KV cache 无法在请求间复用长度不同的前缀,位置信息已经烧进 K 里了(关于这一点,跨模型 KV 转移里有更深入的讨论)。
  4. 融合 kernel:现代推理框架不会单独跑一次 RoPE 再算注意力。vLLM 的 PagedAttention 系列 kernel 和 FlashAttention 的 rotary embedding 接口都把旋转融合进注意力 kernel 内部:加载 K 块时顺带完成旋转,省掉一次额外的显存读写。对带宽敏感的解码阶段,省这一次全量 K 的读写就是几个百分点的吞吐。
  5. 矩阵吸收:在 MLA 这类低秩注意力里,RoPE 因为打破矩阵乘法的结合律(旋转依赖位置、不能吸收进 WqW_q),必须”解耦”出来单独作用,代价是增加一点 KV cache 占用——MLA 完全拆解里讲过这个设计权衡。

长度外推:RoPE 的阿克琉斯之踵#

RoPE 把位置信息做得这么优雅,是不是就能无限外推了?不能。以固定长度训练(比如 4k token)的模型,在更长的序列上推理时,困惑度会灾难性上升——这不是 RoPE 特有的毛病,但 RoPE 模型的失效方式很有代表性。

为什么”相对位置编码”也会外推失败?问题出在旋转角度的分布上。模型训练时见过的是 0mn<40960 \le |m-n| < 4096 范围内的位置差;推理时序列长到 32k,mn|m-n| 第一次超出训练范围。此时高频维度(旋转最快的那些)的相位 ei(mn)θie^{i(m-n)\theta_i} 进入了模型从未见过的角度区间,注意力模式完全乱掉。

更精确的失效分析来自 YaRN 论文(Peng et al., 2023)提出的波长视角。每个维度 ii 的旋转周期是:

λi=2πθi=2π100002i/d\lambda_i = \frac{2\pi}{\theta_i} = 2\pi \cdot 10000^{2i/d}

维度 ii 越小,波长越短。LLaMA 模型(d=128d=128,base 10000)里,维度 0 的波长只有 2π6.32\pi \approx 6.3 个 token(每 6 个 token 就转一整圈),而维度 63 的波长是 2π×10000628322\pi \times 10000 \approx 62832 个 token。不同维度对”距离”的分辨率差了四个数量级:

YaRN 论文图 1:LLaMA 模型 RoPE 各维度的波长(对数刻度)——从约 6 个 token 到约 6 万个 token 跨越四个数量级
YaRN 论文图 1:LLaMA 模型 RoPE 各维度的波长(对数刻度)——从约 6 个 token 到约 6 万个 token 跨越四个数量级

(图源:YaRN 论文 Figure 1。横轴是维度编号,纵轴是波长,对数刻度下是一条直线,直观展示了几何级数频率造成的巨大差异。)

波长分析揭示了两件相反的事:

  • 短波长的维度(高频):它们对相邻 token 的分辨率至关重要——两个相邻位置在这些维度上转过的角度差很大,模型靠它们区分”紧挨着的词”。但正因为转得快,序列一超过训练长度,它们的相位就迅速偏离训练分布。
  • 长波长的维度(低频):它们转得慢,训练时在 4k 序列里根本没转过一圈,相位几乎没变过——模型实际上根本没学会使用它们。这些维度在超长序列上恰恰是最需要的。

所以”直接外推”(把位置索引 0..32767 原样喂进去)两头不讨好:高频维度偏离分布、低频维度从未被训练利用。要让 4k 模型支持 128k,必须改造位置编码本身。这催生了下面几个经典的扩展方法。

四大长度扩展方法:PI、NTK、YaRN 与生产实践#

Position Interpolation:把尺子压扁#

PI(Position Interpolation,Chen et al., Meta AI, 2023)的思路最直白:训练时模型只见过 4k 个位置,那就把 32k 的序列”压缩”回 4k 的范围内。做法是把位置索引除以缩放因子 s=L/Ls = L'/L

f(xm,m)=f(xm,m/s)f'(x_m, m) = f(x_m, m/s)

以 4k → 32k 为例,s=8s = 8,原本位置 0, 1, 2, …, 32767 变成 0, 0.125, 0.25, …, 4095.875。等价的说法是:把原始频率全部除以 8,让”尺子的刻度变密”,用原来的角度范围覆盖更长的距离。

PI 有扎实的理论支撑:论文证明了插值后的注意力得分上界比直接外推小约 600 倍(外推时高频维度的内积可以无界增长,插值时所有角度都被限制在训练范围内),因此微调更稳定。实验上,LLaMA 7B–65B 只需约 1000 步微调就能扩展到 32k,且 32k 内保持质量。

但 PI 的代价也很明显:局部分辨率损失。缩放 s=8s=8 后,相邻两个 token 的位置差从 1 变成 0.125,高频维度上”相邻 token 旋转角差”被压缩,模型区分紧邻 token 的能力下降——这正是 YaRN 论文指出的”PI 丢失高频信息”问题,缩放倍数越大越严重。

NTK-aware:改 base,而不是改位置#

NTK-aware(bloc97, 2023,名字借自神经正切核理论)换了个角度:不改位置索引,改频率序列的 base。把 base 从 bb 提高到

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

几何级数频率 b2i/db^{-2i/d} 被整体”拉伸”。拉伸的效果对每个维度不是均匀的:高频维度(ii 小,指数接近 0)几乎不动,保留了对相邻位置的精细分辨;低频维度(ii 接近 d/2d/2,指数接近 −1)被显著压低,旋转周期变长、能覆盖更远的距离。

比起 PI,NTK-aware 最大的优点是零微调即可用:直接把 base 换成新的值,模型在 4k 内的行为几乎不变,同时能把序列推远好几倍。它的局限在于:用单一标量 ss 缩放所有维度,缺少针对”哪些维度该插值、哪些该外推”的精细控制;而且最优 base 要靠经验试出来,训练/推理的 scale 必须固定。

YaRN:按波长分工 + 温度补偿#

YaRN(Peng et al., 2023,Nous Research + EleutherAI)把上面的分析推到极致,核心判断是:不同波长的维度应该区别对待——波长远小于训练上下文长度的维度(高频,负责局部)应该保持原样、继续外推;波长大于或接近上下文长度的维度(低频,负责远程)应该插值、压回训练范围;中间地带平滑过渡。

定义每个维度的”旋转圈数”比值 r(d)=L/λdr(d) = L/\lambda_dLL 是训练上下文长度,λd\lambda_d 是该维度波长),即”在这个维度上,训练长度的序列一共转过多少圈”。引入两个阈值 α\alphaβ\beta(LLaMA 家族的经验值是 α=1\alpha=1β=32\beta=32),用斜坡函数 γ(r)\gamma(r) 决定每个维度插值多少:

γ(r)={0r<α1r>βrαβα其他\gamma(r) = \begin{cases} 0 & r < \alpha \\ 1 & r > \beta \\ \dfrac{r-\alpha}{\beta-\alpha} & \text{其他} \end{cases}

然后逐维度混合”插值后的频率”和”原始频率”:

θi=(1γ(r(i)))θis+γ(r(i))θi\theta_i' = \left(1 - \gamma\big(r(i)\big)\right) \frac{\theta_i}{s} + \gamma\big(r(i)\big) \, \theta_i

r<αr < \alpha 的维度(转了超过 β\beta 圈……这里注意方向:rr 大说明波长、转的圈数多)完全外推(γ=1\gamma=1,用原始 θi\theta_i),r>βr > \beta 的维度完全插值(γ=0\gamma=0,用 θi/s\theta_i/s)。这就是 “NTK-by-parts”(按部分处理)的含义。

YaRN 的第二招是注意力温度缩放。直觉是:序列变长后,平均来看每个 query 面对的”近距离 token 密度”下降,softmax 的分布熵变大,模型学到的最佳注意力锐度不再匹配。补偿方式是给注意力得分乘一个温度 tt

softmax(qmkntd)\operatorname{softmax}\left( \frac{q_m^\top k_n}{t\sqrt{d}} \right)

LLaMA/Llama 2 上拟合出的经验公式是 1/t=0.1ln(s)+1\sqrt{1/t} = 0.1\ln(s) + 1ss 为缩放倍数)。工程上的巧妙之处在于:温度可以乘进预计算的复数旋转嵌入里(“长度缩放”技巧),不需要改任何注意力代码,推理和训练零额外开销。

实验上,YaRN 只需 400 步、约 1 亿 token(预训练数据的 0.1%) 微调,就能把 Llama 2 7B/13B 从 4k 扩展到 64k,再外推到 128k——训练量比 PI 少约 10 倍,步数少 2.5 倍。下图是 YaRN 论文中固定 s=8s=8 时各缩放方法在 896 篇 16k token 文档上的困惑度对比:

YaRN 论文图 4:固定缩放因子 s=8 时,各方法在 16k token 长文档上的困惑度对比——YaRN 全面低于 PI 与纯 NTK-aware
YaRN 论文图 4:固定缩放因子 s=8 时,各方法在 16k token 长文档上的困惑度对比——YaRN 全面低于 PI 与纯 NTK-aware

(图源:YaRN 论文 Figure 4。横轴是评估用的上下文长度,纵轴是困惑度;YaRN 在短程和长程都保持更低、更平坦的困惑度曲线,而 PI 在超长距离上明显退化。)

论文还给出了扩展到 128k 后、10 篇 128k 长文档上的滑窗困惑度(窗口 256)数据:

模型缩放微调步数8192163843276865536131072
Llama 2 7B4k × 164003.512.992.652.42>101
Llama 2 7B4k × 32400+2003.563.042.702.452.37
Llama 2 13B4k × 164003.252.792.502.29>101
Llama 2 13B4k × 32400+2003.292.832.532.312.24

(数据来源:YaRN 论文 Table 1。注意 s=16s=16 的模型只用 64k 上下文训练,在 128k 上困惑度直接崩到 >101;s=32s=32 的模型虽然也只训练了 64k 数据,却成功外推到了 128k——这正是”低频频段外推 + 高频频段插值”设计的直接收益。)

Dynamic NTK 与动态缩放#

上面所有方法都有一个隐含假设:缩放因子 ss 在整个推理过程中固定。但自回归解码中序列长度逐 token 增长,固定 ss 意味着”短的序列也被按最终长度缩放”,浪费了短序列上本可保留的原始分辨率。

Dynamic NTK(emozilla, 2023,后来也被引入 YaRN)的做法是每个 forward pass 按当前序列长度动态重算缩放因子:s=max(1,l/L)s = \max(1, l'/L),其中 ll' 是当前序列长度。序列从 1 长到 128k,缩放因子从 1 平滑涨到 32,每个瞬间模型看到的都是”恰好匹配当前长度”的频率配置。这是纯推理侧、零微调的实用方案——HuggingFace transformers 的 rope_scaling: {"type": "dynamic"}、llama.cpp 的 --rope-scaling 选项都内置支持,代价是序列长度跨过训练长度时需重算一次频率表(摊销后开销可忽略,因为频率表是预计算的)。

下图对比了不缩放(RoPE 直接外推)、Dynamic-PI、Dynamic-YaRN 在 Llama 2 长文档上的表现——不缩放的模型随距离增长迅速崩溃,动态方案则维持稳定:

YaRN 论文图 8:RoPE 直接外推与 Dynamic-PI、Dynamic-YaRN 在长文档上的对比——动态缩放的曲线平稳得多
YaRN 论文图 8:RoPE 直接外推与 Dynamic-PI、Dynamic-YaRN 在长文档上的对比——动态缩放的曲线平稳得多

(图源:YaRN 论文 Figure 8。横轴是文档中的 token 距离,纵轴是滑窗困惑度或相对困惑度变化;直接外推的 RoPE 在超出训练范围后困惑度急剧上升,两种动态方法保持平稳。)

生产模型里 RoPE 的配置一览#

把上面这些方法放到真实模型里对照,能清楚地看到各家怎么选:

  • LLaMA 1 / LLaMA 2 / Mistral:base 10000,训练 4k;LLaMA 2 后续用 PI 微调出 32k 版本,Mistral 靠 4k 训练 + 滑窗注意力凑出 32k 的”有效窗口”。
  • LLaMA 3:直接把 base 提高到 500000官方实现precompute_freqs_cis),预训练 8k,再经长上下文微调支持 128k(Llama 3 Herd of Models)。base 500000 意味着低频维度波长长达 2π×5×1053142\pi \times 5\times 10^5 \approx 314 万 token——模型从一开始就”准备好”了远程分辨能力,这是新一代模型原生长上下文的秘诀之一。
  • Qwen 系列:动态 NTK(zero-shot 外推,配合少量长文本微调)。
  • DeepSeek-V3:4k 预训练长度 + YaRN 缩放到 128k(技术报告里明确采用的方案)。

在推理框架里,这些配置通过 rope_scaling 参数暴露:HuggingFace transformers 支持 {"type": "linear", "factor": 16.0}(PI)、{"type": "dynamic"}(动态 NTK)与 {"type": "yarn", "factor": ...};vLLM、SGLang、llama.cpp 都能直接读取这份配置并在 kernel 里生效。对推理而言,缩放因子只影响预计算的 cos/sin\cos/\sin 表,不增加任何每 token 的额外开销——这也是 RoPE 系方案在工程上最大的优势:外推能力可以免费叠加在已有模型上。

小结#

把 RoPE 拆完,值得记住的四件事:

  1. 它是一次”求解”而非”设计”:写下”内积只依赖相对位置”的需求,复数运算自然逼出旋转矩阵这个答案。绝对位置的形式、相对位置的本质,是 LLaMA 系架构最优雅的细节之一。
  2. 几何级数频率负责一切:正交性保证数值稳定,保范性保证不扰动注意力尺度,多尺度频率天然给出远程衰减,也天然决定了”哪些维度能外推、哪些必须插值”。
  3. 工程上它几乎免费O(d)O(d) 的逐元素乘加、预计算查表、融合进注意力 kernel,decode 阶段每个新 token 只多几次乘加。
  4. 长度外推是它的软肋,也是它的舞台:PI、NTK、YaRN、动态缩放构成了一套”从零微调到零微调”的完整谱系,直接决定了今天 128k、1M 上下文模型怎么来的。

RoPE 也是理解后续注意力优化的支点:GQA 里的 KV 头共享、MLA 里的低秩压缩与 RoPE 解耦、DeepSeek-V4 混合注意力 里的多模态 M-RoPE,全都建立在今天这套旋转机制之上。理解了旋转,你就拿到了整个现代 LLM 注意力体系的钥匙。

参考资料#

  1. RoFormer: Enhanced Transformer with Rotary Position Embedding(NeurIPS 2021 原始论文)
  2. 苏剑林:Transformer 升级之路 2——博采众长的旋转式位置编码(RoPE 原始推导博客)
  3. Extending Context Window of Large Language Models via Positional Interpolation(PI 论文)
  4. YaRN: Efficient Context Window Extension of Large Language Models(YaRN 论文)
  5. The Llama 3 Herd of Models(LLaMA 3 长上下文与 base 频率说明)
  6. meta-llama/llama3 官方实现(precompute_freqs_cis 与 theta=500000)
  7. HuggingFace Transformers: RoFormer 模型文档
  8. NTK-aware Scaled RoPE(bloc97 提出 NTK-aware 的原始讨论帖)
  9. Dynamically Scaled RoPE further increases performance of long context LLaMA with zero fine-tuning(emozilla 提出的动态 NTK 缩放方法)

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

RoPE 完全拆解:旋转位置编码的原理、实现与长度外推
https://pinghaoyang.com.cn/aigc/posts/rope/
作者
平昊阳
发布于
2026-08-30
许可协议
CC BY-NC-SA 4.0

评论区

Profile Image of the Author
平昊阳
乘长风,破巨浪, 展鸿图于未央!
--
总访问量
--
访客数
公告
欢迎来到我的个人博客!欢迎关注交流吖!
更多相关公告,见
社交-留言」。
音乐
封面

音乐

暂未播放

0:000:00
暂无歌词
站点统计
文章
89
分类
18
标签
116
总字数
762,197
运行时长
0
最后活动
0 天前

文章目录