前言

现代大语言模型(LLM)动辄支持 32K、128K 甚至上百万 token 的上下文,而它们训练时见过的长度往往远小于此。模型凭什么能处理比训练时更长的序列?这背后是位置编码、长上下文继续训练、高效注意力等多件事的合力,其中最基础的一块,是位置编码(Position Encoding)

GPT-2 / BERT(以及 nanoGPT 这类 GPT-2 复刻)用的是学习式绝对位置编码:一张 nn.Embedding(block_size, n_embd) 的查找表,给第 0、1、2…… 个位置各配一个向量,加到 token embedding 上。这套做法有个硬伤,最大长度被 block_size 限制住,超过这个长度模型无法输出位置向量。

LLaMA、Qwen、Mistral 等现代模型采用 RoPE(Rotary Position Embedding,旋转位置编码)

RoPE = 不把位置"加"进向量,而是按位置把 query/key 向量"旋转"一个角度;旋转角正比于位置索引,于是两个 token 的注意力内积只依赖它们的相对距离,且对任意长度都成立,天然相对、可外推、零参数。

本文从"朴素注意力需要位置信息"开始,到 RoPE 的数学表达,用真实数字举例验证,再读一份工业实现,说明为什么现代模型能处理长上下文。


一、先看结论:RoPE 是什么(五分钟极简版)

1.1 RoPE vs 学习式绝对 PE 差异表

维度 学习式绝对 PE(nanoGPT wpe RoPE(LLaMA/Qwen)
注入方式 到输入 embedding 上 旋转 Q/K 向量(在 attention 内部)
编码内容 绝对位置 0,1,2,…(每个位置一个号码) 按绝对位置旋转,但内积只依赖相对距离 n−m
作用层 embedding 层,只作用一次 每一层 attention 的 Q/K 都旋转
参数量 有(block_size × d 的可学习查找表) 无参数(纯函数,确定性旋转)
外推能力 不能(超过 block_size 没有对应向量) 结构上支持(公式对任意 m 有效;直接外推有限,配合 PI/NTK/YaRN 可拉长,见第六节)
心智模型 给每个座位贴一张固定号码牌 给每个 token 转一个角度,模型看"角度差"

1.2 为什么能外推

学习式绝对 PE 是一张大小为 block_size 的查找表,位置 5000 在表里没有对应行,模型拿不到它的位置向量。RoPE 不查表,它是一个函数:位置 m 就把向量旋转 m·θ 这个角度,m 可以是任意整数,位置 5000 和位置 5 用的是同一套公式,只是转得多一点。更关键的是,RoPE 旋转后两个 token 的注意力分数只依赖相对距离 (n−m),而不依赖它们的绝对位置。训练时模型学到的是"相距 k 个位置该怎么注意",这个规律在更长的序列里依然成立。


二、self-attention 需要位置编码

2.1 注意力是"置换等变"的——感知不到顺序

注意力的核心公式:

A t t e n t i o n ( Q , K , V ) = s o f t m a x  ⁣ ( Q K ⊤ d ) V \mathrm{Attention}(Q,K,V) = \mathrm{softmax}\!\left(\frac{QK^\top}{\sqrt{d}}\right)V Attention(Q,K,V)=softmax(d QK)V

Q K ⊤ QK^\top QK 是 token 两两之间做内积。如果把输入序列的 token 顺序打乱(做一个置换 π \pi π),Q、K、V 的行也跟着同样打乱,输出也只是按同样方式打乱——注意力本身完全感知不到顺序(严格术语叫置换等变,permutation-equivariant:输出随输入同步打乱,而不是"输出完全不变"的置换不变)。

对纯注意力而言,“猫 吃 鱼” 和 “鱼 吃 猫” 是一样的(都是集合 {猫, 吃, 鱼})。这显然不行,语言和音频都强烈依赖顺序。所以必须额外往模型里灌位置信息。

2.2 位置编码和因果掩码

因果掩码限制的是"能看谁"(当前 token 不能看未来),它不直接提供"我在第几位"这种坐标。两者分工不同:位置编码给"我在哪、和你差多少",因果掩码给"我不能看未来"。(严格说两者并非完全独立:因果掩码让每个 token 可见的前缀长度不同,已有工作表明不加任何显式 PE 的 decoder-only 模型也能借此学到位置信息,如 NoPEHaviv et al. 2022;但显式位置编码仍是长上下文的主流做法。)


三、绝对位置编码的问题

3.1 正弦位置编码(原始 Transformer ,2017)

固定公式(无参数),给每个位置 pos 算一个 d 维向量:

P E ( p o s ,   2 i ) = sin ⁡  ⁣ ( p o s 10000 2 i / d ) , P E ( p o s ,   2 i + 1 ) = cos ⁡  ⁣ ( p o s 10000 2 i / d ) PE_{(pos,\,2i)} = \sin\!\left(\frac{pos}{10000^{2i/d}}\right),\qquad PE_{(pos,\,2i+1)} = \cos\!\left(\frac{pos}{10000^{2i/d}}\right) PE(pos,2i)=sin(100002i/dpos),PE(pos,2i+1)=cos(100002i/dpos)

然后到 token embedding 上: x p o s ′ = e m b ( t o k e n ) + P E p o s x'_{pos} = \mathrm{emb}(token) + PE_{pos} xpos=emb(token)+PEpos。不同维度 i 用不同频率( 10000 2 i / d 10000^{2i/d} 100002i/d 控制波长),低维转得快、高维转得慢——这个"多频率"思想 RoPE 继承了。

3.2 学习式绝对位置编码(GPT-2 / BERT / nanoGPT)

把"位置向量"也当成可学习参数,做一张查找表:

# nanoGPT model.py
self.wte = nn.Embedding(vocab_size, n_embd)    # token embedding
self.wpe = nn.Embedding(block_size, n_embd)    # ← 位置 embedding,共 block_size 行

# forward 里:
pos = torch.arange(0, t, dtype=torch.long, device=device)   # 0,1,...,t-1
tok_emb = self.wte(idx)        # (B, T, n_embd)
pos_emb = self.wpe(pos)        # (T, n_embd)  ← 第 pos 行就是该位置的向量
x = tok_emb + pos_emb          # ← 仍然是"加"

3.3 正弦和学习式的关系

它俩是同一个大类(绝对位置编码)下的两个成员。 “绝对位置编码"的定义是:给每个绝对位置配一个 d 维向量,加到 token embedding 上。差别只在"位置向量是算出来还是学出来”:

正弦位置编码 学习式绝对 PE(wpe
位置向量哪来 :固定 sin/cos 公式 nn.Embedding 查找表当参数训
有无参数 无参数 有(block_size × d 可学习表)
怎么用 到 embedding 到 embedding(完全一样)
编码内容 绝对位置 绝对位置
代表模型 原始 Transformer(2017) BERT、GPT-2、nanoGPT

看中间两行:注入方式(加)、编码内容(绝对)完全一致,唯一差别是位置向量算还是学。2017 原论文两种都试了,说效果"几乎一致",最终选正弦——给出的理由正是"可能允许外推到比训练更长的序列"(may allow the model to extrapolate to sequence lengths longer than the ones encountered during training);BERT/GPT-2 则改用了学习式。

一个关键区分:正弦是公式,理论上位置 20000 也"能算出"一个向量,不会像查找表那样越界;但**“能算 ≠ 效果好”**,模型训练时只见过 [0, block_size) 这段,超长时注意力质量极速下降。

3.4 绝对 PE 的问题

  1. 外推破产(最致命)wpe 只有 block_size 行。训练 block_size=1024,想推理 2048,位置 1024~2047 在表里没有行self.wpe(pos) 越界。(正弦 PE 能算出但外推效果差。)

  2. 编码的是绝对量,注意力真正想要的是相对量。"形容词修饰它右边第一个名词"这种规律,本质是相对距离 = +1,和这对词在句子第 3 位还是第 300 位无关。

  3. 加在输入,位置信号被逐层稀释。位置只在第 0 层注入一次,越深的层离它越远,信号越糊。

3.5 为什么注意力偏爱相对量

  • 泛化能力强: 语言里绝大多数依赖只跟"相距多远"有关,跟"在第几位"无关。同一条"相距 1"的规律,绝对 PE 得为 ( 3 , 4 ) , ( 4 , 5 ) , … , ( 300 , 301 ) (3,4),(4,5),\dots,(300,301) (3,4),(4,5),,(300,301) 每一对分别学一遍;相对位置学一次通用到所有位置

  • 可外推: 训练只见过位置 0~1023。若绝对 PE,位置 2000 是全新的、没训过;若按相对距离存,"相距 5"在位置 2000 附近和位置 5 附近是同一个模式,可复用。

  • 注意力的决策天然是相对的: 注意力要回答的问题几乎都是"我该关注我前面第几个 token",真正需要绝对坐标的场景(比如"我是不是句首")极少。既然位置决策几乎全是相对的,把相对性直接嵌入结构,比让模型从绝对量里自己反推更高效、更稳。

正弦/学习式都是"加性 + 绝对",相对性只能靠学、且会稀释。RoPE 用"乘性旋转 + 结构恒等的相对性"更加合理地解决注意力机制的 PE 问题。

正弦 PE 是 x m ′ = x m + p m x'_m = x_m + p_m xm=xm+pm p m p_m pm 是位置 m m m 的位置向量),于是 query/key 是 q m = W q ( x m + p m ) q_m = W_q(x_m + p_m) qm=Wq(xm+pm) k n = W k ( x n + p n ) k_n = W_k(x_n + p_n) kn=Wk(xn+pn)。注意力分数展开成 4 项

q m ⊤ k n = x m ⊤ W q ⊤ W k x n ⏟ 内容-内容 + x m ⊤ W q ⊤ W k   p n ⏟ 内容-绝对位置 n + p m ⊤ W q ⊤ W k x n ⏟ 绝对位置 m -内容 + p m ⊤ W q ⊤ W k   p n ⏟ 位置-位置 q_m^\top k_n = \underbrace{x_m^\top W_q^\top W_k x_n}_{\text{内容-内容}} + \underbrace{x_m^\top W_q^\top W_k\, p_n}_{\text{内容-绝对位置}n} + \underbrace{p_m^\top W_q^\top W_k x_n}_{\text{绝对位置}m\text{-内容}} + \underbrace{p_m^\top W_q^\top W_k\, p_n}_{\text{位置-位置}} qmkn=内容-内容 xmWqWkxn+内容-绝对位置n xmWqWkpn+绝对位置m-内容 pmWqWkxn+位置-位置 pmWqWkpn

而 2、3 项分别单独含绝对位置 n n n、绝对位置 m m m。分数的结构里天然带着 m、n 的绝对身份,不会自动化简成"只依赖 n−m"。对比 RoPE(第四节会证): ⟨ R m q , R n k ⟩ = q ⊤ R ( ( n − m ) θ ) k \langle R_m q, R_n k\rangle = q^\top R((n-m)\theta) k Rmq,Rnk=qR((nm)θ)k,只依赖 n−m。


四、RoPE 的数学表达

4.1 旋转矩阵

RoPE 在 attention 内部把位置 m 的 query 向量,按角度 m θ m\theta mθ 旋转;把位置 n 的 key 向量,按角度 n θ n\theta nθ 旋转。

4.2 最小单元:2 维旋转 + 多频率

把 d 维向量两两配对,每一对 ( x 2 i , x 2 i + 1 ) (x_{2i}, x_{2i+1}) (x2i,x2i+1) 看成 2D 平面上的一个点。对位置 m,用旋转矩阵转一个角度 m θ i m\theta_i mθi

R ( m θ i ) = ( cos ⁡ m θ i − sin ⁡ m θ i sin ⁡ m θ i cos ⁡ m θ i ) R(m\theta_i) = \begin{pmatrix} \cos m\theta_i & -\sin m\theta_i \\[2pt] \sin m\theta_i & \cos m\theta_i \end{pmatrix} R(mθi)=(cosmθisinmθisinmθicosmθi)

不同的维度对 i 用不同的频率(沿用正弦 PE 的设定)

θ i = 10000 − 2 i / d , i = 0 , 1 , … , d 2 − 1 \theta_i = 10000^{-2i/d},\qquad i = 0,1,\dots,\tfrac{d}{2}-1 θi=100002i/d,i=0,1,,2d1

低维(i 小)θ 大、转得快;高维(i 大)θ 小、转得慢。不同频率覆盖从局部到全局的不同尺度的相对距离。

整个 d 维向量的旋转,就是把 d/2 个这样的 2×2 旋转块拼成一个分块对角矩阵 R m R_m Rm(只依赖位置 m):

      ┌ R(mθ₀)              ┐
R_m = │        R(mθ₁)       │   ← d/2 个 2×2 旋转块沿对角线摆放
      │              ⋱      │      块与块之间全是 0(互不干扰)
      └                R(mθ_{d/2-1}) ┘

4.3 旋转后内积只依赖相对位置

这是 RoPE 的重要性质。

设位置 m 的 query 是 q q q,位置 n 的 key 是 k k k。RoPE 后它们变成 R m q R_m q Rmq R n k R_n k Rnk。注意力算内积

⟨ R m q ,    R n k ⟩ = ( R m q ) ⊤ ( R n k ) = q ⊤ R m ⊤ R n   k (1) \langle R_m q,\; R_n k \rangle = (R_m q)^\top (R_n k) = q^\top R_m^\top R_n\, k \tag1 Rmq,Rnk=(Rmq)(Rnk)=qRmRnk(1)

根据旋转矩阵的性质正交 R ⊤ = R − 1 R^\top = R^{-1} R=R1)、角度可加 R ( α ) R ( β ) = R ( α + β ) R(\alpha)R(\beta)=R(\alpha+\beta) R(α)R(β)=R(α+β))。于是

R m ⊤ R n = R ( − m θ )   R ( n θ ) = R ( ( n − m ) θ ) R_m^\top R_n = R(-m\theta)\,R(n\theta) = R\big((n-m)\theta\big) RmRn=R(mθ)R(nθ)=R((nm)θ)

带回式(1)

   ⟨ R m q ,    R n k ⟩ = q ⊤ R ( ( n − m ) θ )   k    \boxed{\;\langle R_m q,\; R_n k \rangle = q^\top R\big((n-m)\theta\big)\, k\;} Rmq,Rnk=qR((nm)θ)k

即旋转之后,query 和 key 的注意力分数只跟 (n − m) 相对距离有关,不含 m、n 的绝对值。

4.4 复数视角

2D 旋转 = 复数乘法。把一对实数 ( x 2 i , x 2 i + 1 ) (x_{2i},x_{2i+1}) (x2i,x2i+1) 看成复数 z = x 2 i + i   x 2 i + 1 z = x_{2i} + i\,x_{2i+1} z=x2i+ix2i+1,旋转 m θ i m\theta_i mθi 就是乘上 e   i   m θ i e^{\,i\,m\theta_i} eimθi

z ⋅ e   i m θ i = ( x 2 i + i   x 2 i + 1 ) ( cos ⁡ m θ i + i sin ⁡ m θ i ) z \cdot e^{\,i m\theta_i} = (x_{2i}+i\,x_{2i+1})(\cos m\theta_i + i\sin m\theta_i) zeimθi=(x2i+ix2i+1)(cosmθi+isinmθi)

展开后实部/虚部正好就是 4.2 旋转矩阵的两行。所以工程实现里把向量 view_as_complex 表示成复数,直接乘 e i m θ e^{im\theta} eimθ


五、举个例子

d = 4 d / 2 = 2 d/2 = 2 d/2=2 个旋转块)。

5.1 定频率、拼分块对角矩阵

θ i = 10000 − 2 i / d = 10000 − i / 2 \theta_i = 10000^{-2i/d} = 10000^{-i/2} θi=100002i/d=10000i/2

θ 0 = 10000 0 = 1 , θ 1 = 10000 − 1 / 2 = 1 100 = 0.01 \theta_0 = 10000^{0} = 1,\qquad \theta_1 = 10000^{-1/2} = \tfrac{1}{100} = 0.01 θ0=100000=1,θ1=100001/2=1001=0.01

把 4 维向量 x = ( x 0 , x 1 , x 2 , x 3 ) ⊤ x=(x_0,x_1,x_2,x_3)^\top x=(x0,x1,x2,x3) 两两配对: ( x 0 , x 1 ) (x_0,x_1) (x0,x1) 归第 0 块(用 θ 0 \theta_0 θ0), ( x 2 , x 3 ) (x_2,x_3) (x2,x3) 归第 1 块(用 θ 1 \theta_1 θ1)。位置 m 的旋转矩阵

R m = ( cos ⁡ m θ 0 − sin ⁡ m θ 0 0 0 sin ⁡ m θ 0 cos ⁡ m θ 0 0 0 0 0 cos ⁡ m θ 1 − sin ⁡ m θ 1 0 0 sin ⁡ m θ 1 cos ⁡ m θ 1 ) R_m= \begin{pmatrix} \cos m\theta_0 & -\sin m\theta_0 & 0 & 0\\ \sin m\theta_0 & \cos m\theta_0 & 0 & 0\\ 0 & 0 & \cos m\theta_1 & -\sin m\theta_1\\ 0 & 0 & \sin m\theta_1 & \cos m\theta_1 \end{pmatrix} Rm= cosmθ0sinmθ000sinmθ0cosmθ00000cosmθ1sinmθ100sinmθ1cosmθ1

代入 m = 2 m=2 m=2:角度 m θ 0 = 2 m\theta_0 = 2 mθ0=2 rad, m θ 1 = 0.02 m\theta_1 = 0.02 mθ1=0.02 rad。查值 cos ⁡ 2 ≈ − 0.416 ,   sin ⁡ 2 ≈ 0.909 ,   cos ⁡ 0.02 ≈ 0.9998 ,   sin ⁡ 0.02 ≈ 0.0200 \cos2\approx-0.416,\ \sin2\approx0.909,\ \cos0.02\approx0.9998,\ \sin0.02\approx0.0200 cos20.416, sin20.909, cos0.020.9998, sin0.020.0200

R 2 = ( − 0.416 − 0.909 0 0 0.909 − 0.416 0 0 0 0 0.9998 − 0.0200 0 0 0.0200 0.9998 ) R_2= \begin{pmatrix} -0.416 & -0.909 & 0 & 0\\ 0.909 & -0.416 & 0 & 0\\ 0 & 0 & 0.9998 & -0.0200\\ 0 & 0 & 0.0200 & 0.9998 \end{pmatrix} R2= 0.4160.909000.9090.41600000.99980.0200000.02000.9998

x = ( 1 , 0 , 1 , 0 ) ⊤ x=(1,0,1,0)^\top x=(1,0,1,0) 作用后,

R 2   x = ( − 0.416 ,   0.909 ,   0.9998 ,   0.0200 ) ⊤ R_2\,x=(-0.416,\ 0.909,\ 0.9998,\ 0.0200)^\top R2x=(0.416, 0.909, 0.9998, 0.0200)

每个 2D 子空间独立转自己的角度。高频块( θ 0 = 1 \theta_0=1 θ0=1)转了 2 rad(一大截,方向大变);低频块( θ 1 = 0.01 \theta_1=0.01 θ1=0.01)只转了 0.02 rad(几乎没动)。不同频率覆盖从"局部近距离"到"全局远距离"。

5.2 q→R_m q、k→R_n k,只依赖相对距离

设 query 在位置 m=2 q = ( 1 , 0 , 1 , 0 ) ⊤ q=(1,0,1,0)^\top q=(1,0,1,0),key 在位置 n=5 k = ( 0 , 1 , 1 , 0 ) ⊤ k=(0,1,1,0)^\top k=(0,1,1,0)。RoPE 就是各自乘上自己位置的旋转矩阵。

q ′ = R 2 q q' = R_2 q q=R2q 上面算过。算 k ′ = R 5 k k' = R_5 k k=R5k 5 θ 0 = 5 5\theta_0=5 5θ0=5 rad, 5 θ 1 = 0.05 5\theta_1=0.05 5θ1=0.05 rad,查值 cos ⁡ 5 ≈ 0.284 ,   sin ⁡ 5 ≈ − 0.959 ,   cos ⁡ 0.05 ≈ 0.99875 ,   sin ⁡ 0.05 ≈ 0.04998 \cos5\approx0.284,\ \sin5\approx-0.959,\ \cos0.05\approx0.99875,\ \sin0.05\approx0.04998 cos50.284, sin50.959, cos0.050.99875, sin0.050.04998

k ′ = R 5   k = ( 0.959 ,   0.284 ,   0.99875 ,   0.04998 ) ⊤ k' = R_5\,k = (0.959,\ 0.284,\ 0.99875,\ 0.04998)^\top k=R5k=(0.959, 0.284, 0.99875, 0.04998)

直接算旋转后的注意力分数:

⟨ R 2 q , R 5 k ⟩ = ( − 0.416 ) ( 0.959 ) + ( 0.909 ) ( 0.284 ) + ( 0.9998 ) ( 0.99875 ) + ( 0.0200 ) ( 0.04998 ) = − 0.399 + 0.258 + 0.9986 + 0.0010 = 0.8586 \begin{aligned} \langle R_2 q, R_5 k\rangle &= (-0.416)(0.959) + (0.909)(0.284) + (0.9998)(0.99875) + (0.0200)(0.04998)\\ &= -0.399 + 0.258 + 0.9986 + 0.0010 = \boxed{0.8586} \end{aligned} R2q,R5k=(0.416)(0.959)+(0.909)(0.284)+(0.9998)(0.99875)+(0.0200)(0.04998)=0.399+0.258+0.9986+0.0010=0.8586

验证定理:它应该等于 q ⊤ R ( ( n − m ) θ )   k = q ⊤ R ( 3 θ )   k q^\top R((n-m)\theta)\,k = q^\top R(3\theta)\,k qR((nm)θ)k=qR(3θ)k(相对距离 n−m=3,用原始 q、k)。分块算,第 0 块转 3 rad,第 1 块转 0.03 rad

  • 第 0 块: R ( 3 ) ( 0 , 1 ) ⊤ = ( − sin ⁡ 3 , cos ⁡ 3 ) = ( − 0.141 , − 0.990 ) R(3)(0,1)^\top=(-\sin3,\cos3)=(-0.141,-0.990) R(3)(0,1)=(sin3,cos3)=(0.141,0.990),内积 ( 1 , 0 ) ⋅ ( − 0.141 , − 0.990 ) = − 0.141 (1,0)\cdot(-0.141,-0.990)=-0.141 (1,0)(0.141,0.990)=0.141
  • 第 1 块: R ( 0.03 ) ( 1 , 0 ) ⊤ = ( cos ⁡ 0.03 , sin ⁡ 0.03 ) = ( 0.99955 , 0.030 ) R(0.03)(1,0)^\top=(\cos0.03,\sin0.03)=(0.99955,0.030) R(0.03)(1,0)=(cos0.03,sin0.03)=(0.99955,0.030),内积 ( 1 , 0 ) ⋅ ( 0.99955 , 0.030 ) = 0.99955 (1,0)\cdot(0.99955,0.030)=0.99955 (1,0)(0.99955,0.030)=0.99955

q ⊤ R ( 3 θ )   k = − 0.141 + 0.99955 = 0.8586 q^\top R(3\theta)\,k = -0.141 + 0.99955 = \boxed{0.8586} qR(3θ)k=0.141+0.99955=0.8586

再补一个验证:把 q、k 的绝对位置同时平移——q 挪到位置 12、k 挪到位置 15(相对距离仍是 3),重算 ⟨ R 12   q ,   R 15   k ⟩ \langle R_{12}\,q,\ R_{15}\,k\rangle R12q, R15k,结果依然是 0.8586。绝对位置一起变、相对距离不变 → 分数不变,这就是"平移不变"性。


六、为什么 RoPE 能外推

有两层原因:

6.1 第一,RoPE 是函数,不是查找表 → 不会越界

学习式绝对 PE 的 wpenn.Embedding(block_size, n_embd)位置 m 必须 < block_size,否则没有对应行。RoPE 里位置 m 只是旋转角 m θ i m\theta_i mθi 里的一个乘数,m 取任何整数都能算

6.2 第二,分数只依赖相对距离,训过的相对模式可复用

更深的原因来自 4.3 中注意力分数 = q ⊤ R ( ( n − m ) θ ) k = q^\top R((n-m)\theta) k =qR((nm)θ)k,只看相对距离 (n−m)。训练时即便序列只有 1024 长,模型已经把"相对距离 = 1、2、…、k 该怎么注意"学进了权重。推理时序列变成 4096,只要 query 关心的还是它附近的 token(相对距离落在训练见过的范围内),这套规律照样适用。

6.3 外推不是无限的

当序列远超训练长度,低频维度(θ 小、转得慢的那些)的旋转角会进入训练从未见过的区域:高频维度在训练长度内早已转过许多个完整周期,角度 mod 2π 的所有相位训练中都见过,外推时没有新东西;而低频维度在训练长度内只扫过一小段弧(比如 θ ≈ 1 / 10000 \theta\approx 1/10000 θ1/10000 时 1024 个位置只转约 0.1 rad),序列一拉长才第一次遇到全新角度,分数分布随之漂移,性能仍会下降。于是衍生一些外推增强技术,都是在"不破坏已学到的相对几何"的前提下,把 RoPE 的有效角度范围温和地拉长。

方法 一句话 适用
Position Interpolation (PI) 把位置索引线性压缩进训练长度 简单、需少量微调
NTK-aware scaling 调大 base:高频近似不动、低频被压回已见角度范围 常用,部分免微调
YaRN 按频率分波段处理(NTK-by-parts)+ 温度修正 32K~128K 长上下文主流

七、RoPE 实现

下面是 LLaMA 官方风格实现,也是绝大多数现代实现的母版。三个函数。

7.1 预计算旋转因子 freqs_cis

import torch

def precompute_freqs_cis(dim: int, end: int, theta: float = 10000.0):
    # dim = 每个 head 的维度;end = 最大序列长度(预生成这么多位置的旋转因子)
    # 1) 每个维度对 i 的频率 θ_i = theta^{-2i/dim},共 dim/2 个
    freqs = 1.0 / (theta ** (torch.arange(0, dim, 2).float() / dim))   # (dim/2,)
    # 2) 位置 t = 0,1,...,end-1
    t = torch.arange(end)                                             # (end,)
    # 3) 外积:freqs[pos, i] = pos * θ_i  ← 位置 pos 在第 i 对维度上要转的角度
    freqs = torch.outer(t, freqs).float()                             # (end, dim/2)
    # 4) 转成复数 e^{i·角度} = cos + i·sin(cis 的含义),模长为 1
    freqs_cis = torch.polar(torch.ones_like(freqs), freqs)            # (end, dim/2) complex
    return freqs_cis

做 3 点:① θ_i 多频率(和正弦 PE 同款);② outer(t, freqs) 把"位置 × 频率"组成旋转角矩阵;③ polar(1, angle) 造出单位复数 e i   a n g l e e^{i\,angle} eiangle,模长恒为 1 保证只旋转不缩放。

7.2 把旋转应用到 Q/K:apply_rotary_emb

def apply_rotary_emb(xq, xk, freqs_cis):
    # xq, xk: (B, T, n_head, head_dim)
    # 1) 把最后一维两两配对,看成复数:(..., head_dim) -> (..., head_dim/2) complex
    xq_ = torch.view_as_complex(xq.float().reshape(*xq.shape[:-1], -1, 2))
    xk_ = torch.view_as_complex(xk.float().reshape(*xk.shape[:-1], -1, 2))
    # 2) freqs_cis 形状 (T, head_dim/2),广播到 (1, T, 1, head_dim/2)
    freqs_cis = freqs_cis[None, :, None, :]
    # 3) 复数乘法 = 旋转,再 view 回实数并展平回 head_dim
    xq_out = torch.view_as_real(xq_ * freqs_cis).flatten(3)
    xk_out = torch.view_as_real(xk_ * freqs_cis).flatten(3)
    return xq_out.type_as(xq), xk_out.type_as(xk)

核心是第 3 步的 xq_ * freqs_cis一次复数逐元素乘,就把每一对维度旋转了对应角度。对照 4.4,复数乘 e i m θ e^{im\theta} eimθ 就是 2D 旋转。

注:这里"最后一维相邻两两配对 ( x 0 , x 1 ) , ( x 2 , x 3 ) , … (x_0,x_1),(x_2,x_3),\dots (x0,x1),(x2,x3),"是 LLaMA/Meta 官方布局;HuggingFace transformers 的实现用的是"前半/后半配对"( x i x_i xi x i + d / 2 x_{i+d/2} xi+d/2 配对,即 rotate_half 写法)。两者数学等价,但权重布局不兼容。

7.3 在 attention 里的调用位置

class Attention(nn.Module):
    def forward(self, x, freqs_cis):
        B, T, C = x.shape
        q, k, v = self.wq(x), self.wk(x), self.wv(x)          # 常规 QKV 投影
        q = q.view(B, T, self.n_head, self.head_dim)
        k = k.view(B, T, self.n_head, self.head_dim)
        v = v.view(B, T, self.n_head, self.head_dim)
        # ★ 唯一的新增:在算注意力分数之前,旋转 Q 和 K(V 不转!)
        q, k = apply_rotary_emb(q, k, freqs_cis[:T])
        # 之后照常:scores = q @ k^T / sqrt(d) -> +causal mask -> softmax -> @ v
        ...

相比用 wpe 的 nanoGPT,差异只有两处:① embedding 处删掉 wpe(不再加绝对 PE);② attention 里算分数,对 Q/K 各做一次 apply_rotary_emb

7.4 三个实现细节

  • 为什么只转 Q/K 不转 V? 位置关系体现在注意力分数 Q K ⊤ QK^\top QK 里;V 是被分数加权的"内容载荷",旋转它反而破坏内容语义。
  • 为什么每层都要转? RoPE 在每层 attention 的 Q/K 上重做(每层都重新投影出新的 Q/K),位置信号不会像加性 PE 那样越深越糊。
  • head_dim 必须偶数:因为要两两配对成复数 / 2D 旋转块。

八、常见问题

  1. “RoPE 是相对位置编码,和 Shaw 2018 那种相对 PE 一样吗?”
    不一样。Shaw 式相对 PE 是把可学习的相对位置向量加进 K(和 V)的表示里再做内积( e i j ∝ x i W Q   ( x j W K + a i j K ) ⊤ e_{ij}\propto x_iW^Q\,(x_jW^K + a^K_{ij})^\top eijxiWQ(xjWK+aijK));直接给注意力分数(logit)加可学习相对标量偏置的是 T5 的 relative position bias(固定斜率的版本是 ALiBi)。RoPE 则是旋转 Q/K,通过内积"自动"得到相对性,无参数、无需显式存 (i−j) 相对表,更省、更易外推。

  2. “RoPE 没有参数,那位置信息存哪了?”
    位置信息没有存储,它在每次前向时由确定性旋转计算。模型学的是"在给定相对几何下怎么用注意力",而不是"位置 m 长什么样"。

  3. “旋转会不会改变向量长度、破坏 attention scale?”
    不会。旋转矩阵正交, ∥ R m q ∥ = ∥ q ∥ \|R_m q\| = \|q\| Rmq=q,模长不变、只改方向。 d \sqrt{d} d 缩放、softmax 不用改动。

  4. “θ 里 10000 是必须的吗?”
    是个超参(base)。它决定最低频维度的波长,影响外推上限。长上下文工作(NTK/YaRN)很多就是在调 base 或对频率分段缩放。


总结

RoPE = 按位置旋转 Q/K,使注意力分数只依赖相对距离,无参数、可外推。 这就是现代 LLM 能吃长上下文的基础。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐