【LLM】大语言模型基础(二):RoPE
前言
现代大语言模型(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(dQK⊤)V
Q K ⊤ QK^\top QK⊤ 是 token 两两之间做内积。如果把输入序列的 token 顺序打乱(做一个置换 π \pi π),Q、K、V 的行也跟着同样打乱,输出也只是按同样方式打乱——注意力本身完全感知不到顺序(严格术语叫置换等变,permutation-equivariant:输出随输入同步打乱,而不是"输出完全不变"的置换不变)。
对纯注意力而言,“猫 吃 鱼” 和 “鱼 吃 猫” 是一样的(都是集合 {猫, 吃, 鱼})。这显然不行,语言和音频都强烈依赖顺序。所以必须额外往模型里灌位置信息。
2.2 位置编码和因果掩码
因果掩码限制的是"能看谁"(当前 token 不能看未来),它不直接提供"我在第几位"这种坐标。两者分工不同:位置编码给"我在哪、和你差多少",因果掩码给"我不能看未来"。(严格说两者并非完全独立:因果掩码让每个 token 可见的前缀长度不同,已有工作表明不加任何显式 PE 的 decoder-only 模型也能借此学到位置信息,如 NoPE、Haviv 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 的问题
-
外推破产(最致命):
wpe只有block_size行。训练 block_size=1024,想推理 2048,位置 1024~2047 在表里没有行,self.wpe(pos)越界。(正弦 PE 能算出但外推效果差。) -
编码的是绝对量,注意力真正想要的是相对量。"形容词修饰它右边第一个名词"这种规律,本质是相对距离 = +1,和这对词在句子第 3 位还是第 300 位无关。
-
加在输入,位置信号被逐层稀释。位置只在第 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{位置-位置}} qm⊤kn=内容-内容 xm⊤Wq⊤Wkxn+内容-绝对位置n xm⊤Wq⊤Wkpn+绝对位置m-内容 pm⊤Wq⊤Wkxn+位置-位置 pm⊤Wq⊤Wkpn
而 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⟩=q⊤R((n−m)θ)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θi−sinmθ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=10000−2i/d,i=0,1,…,2d−1
低维(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)=q⊤Rm⊤Rnk(1)
根据旋转矩阵的性质正交( R ⊤ = R − 1 R^\top = R^{-1} R⊤=R−1)、角度可加( 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) Rm⊤Rn=R(−mθ)R(nθ)=R((n−m)θ)
带回式(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⟩=q⊤R((n−m)θ)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) z⋅eimθ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=10000−2i/d=10000−i/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=10000−1/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θ000−sinmθ0cosmθ00000cosmθ1sinmθ100−sinmθ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 cos2≈−0.416, sin2≈0.909, cos0.02≈0.9998, sin0.02≈0.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.90900−0.909−0.41600000.99980.020000−0.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 cos5≈0.284, sin5≈−0.959, cos0.05≈0.99875, sin0.05≈0.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 q⊤R((n−m)θ)k=q⊤R(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} q⊤R(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 的 wpe 是 nn.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 =q⊤R((n−m)θ)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 旋转块。
八、常见问题
-
“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 eij∝xiWQ(xjWK+aijK)⊤);直接给注意力分数(logit)加可学习相对标量偏置的是 T5 的 relative position bias(固定斜率的版本是 ALiBi)。RoPE 则是旋转 Q/K,通过内积"自动"得到相对性,无参数、无需显式存 (i−j) 相对表,更省、更易外推。 -
“RoPE 没有参数,那位置信息存哪了?”
位置信息没有存储,它在每次前向时由确定性旋转计算。模型学的是"在给定相对几何下怎么用注意力",而不是"位置 m 长什么样"。 -
“旋转会不会改变向量长度、破坏 attention scale?”
不会。旋转矩阵正交, ∥ R m q ∥ = ∥ q ∥ \|R_m q\| = \|q\| ∥Rmq∥=∥q∥,模长不变、只改方向。 d \sqrt{d} d 缩放、softmax 不用改动。 -
“θ 里 10000 是必须的吗?”
是个超参(base)。它决定最低频维度的波长,影响外推上限。长上下文工作(NTK/YaRN)很多就是在调 base 或对频率分段缩放。
总结
RoPE = 按位置旋转 Q/K,使注意力分数只依赖相对距离,无参数、可外推。 这就是现代 LLM 能吃长上下文的基础。
更多推荐

所有评论(0)