Attention 本身只看 token 内容,不天然知道 token 的位置。
如果两个 token 的内容向量相同,普通 attention 的点积也可能相同。模型需要某种方式知道:
这个 token 在第几个位置; 两个 token 相隔多远; 哪个 token 在前,哪个 token 在后。
RoPE,也就是 Rotary Position Embedding,是当前 LLM 中非常常见的位置编码方法。
MiniMind 也使用 RoPE。
1. 为什么需要位置编码
Transformer 的 self-attention 本质是集合式计算。
如果没有位置编码,下面两句话在某些层面会很难区分:
猫追狗 狗追猫
词一样,但顺序不同,语义完全不同。
所以模型必须引入位置信息。
早期 Transformer 使用绝对位置编码:
token embedding + position embedding
RoPE 的思路不同:它不直接把位置向量加到 token embedding 上,而是在 Attention 里的 Q/K 上做旋转。
2. RoPE 做在哪里
MiniMind 的 Attention 里,Q/K 经过投影和 norm 后,会进入:
xq, xk = apply_rotary_pos_emb(xq, xk, position_embeddings)
也就是说 RoPE 作用在:
Q 和 K
不是作用在 V,也不是直接作用在 hidden state。
原因是 Attention score 来自:
Q @ K^T
位置关系最需要影响的是“匹配关系”,也就是谁应该关注谁。
V 更像内容本身,不一定需要被位置旋转。
3. 二维旋转的基本公式
先看一个二维向量:
[a, b]
旋转角度 θ 后变成:
[a cosθ - b sinθ, a sinθ + b cosθ]
矩阵写法是:
[ cosθ -sinθ ] [a] [ sinθ cosθ ] [b]
RoPE 就是把高维向量两两分组,在每个二维平面里做旋转。
例如 head_dim=96,会分成:
(0, 1) (2, 3) (4, 5) ...
每一组用不同频率。
4. rotate_half 在做什么
代码里常见:
x * cos + rotate_half(x) * sin
其中 rotate_half 做的是二维旋转里的:
[a, b] -> [-b, a]
于是:
x * cos = [a cos, b cos] rotate_half*x = [-b sin, a sin]
相加后:
[a cos - b sin, b cos + a sin]
也就是标准二维旋转:
[a cos - b sin, a sin + b cos]
注意:rotate_half 作用在 hidden/head_dim 维度上,不作用在 token 长度维度上。
如果 Q 的形状是:
[batch, seq_len, heads, head_dim]
比如:
[1, 6, 8, 96]
那么 rotate_half 操作的是最后的 96,不是 6。
它不会改变 token 顺序。
5. 为什么 RoPE 天然支持相对位置
这是 RoPE 最关键的数学点。
假设 query 在位置 m,key 在位置 n。
RoPE 分别把它们旋转:
q_m = R(mθ) q k_n = R(nθ) k
Attention score 是:
score(m, n) = (R(mθ)q)^T (R(nθ)k)
二维展开后会出现:
cos((m - n)θ) sin((m - n)θ)
核心三角恒等式是:
cosA cosB + sinA sinB = cos(A - B) sinA cosB - cosA sinB = sin(A - B)
令:
A = mθ B = nθ
就得到:
A - B = (m - n)θ
也就是说,两个绝对位置分别旋转后做点积,结果里自然出现了相对距离 m - n。
这就是 RoPE 强大的地方。
6. 一个简单例子
假设:
q = [1, 0] k = [1, 0]
不用 RoPE:
q^T k = 1
不管两个 token 相隔多远,score 都是 1。
使用 RoPE 后:
R(mθ)q = [cos(mθ), sin(mθ)] R(nθ)k = [cos(nθ), sin(nθ)]
点积:
cos(mθ)cos(nθ) + sin(mθ)sin(nθ) = cos((m - n)θ)
如果 m 和 n 很近,某些频率下相关性可能高。
如果距离变大,相关性会随相位变化而改变。
于是 Attention score 不再只是内容相似度,还包含了相对位置关系。
7. 为什么有很多频率
真实模型里 head_dim 不止 2 维。
RoPE 会给不同二维分组不同频率:
低频维度:变化慢,适合长距离关系。 高频维度:变化快,适合短距离关系。
这有点像 Fourier 特征。
不同维度用不同频率编码位置变化,模型可以组合出丰富的位置感知能力。
8. 为什么长文本还需要 YaRN / NTK scaling
原始 RoPE 对训练长度附近的上下文表现很好。
但如果训练时只见过:
4k context
推理时直接拉到:
32k / 128k
旋转角度可能进入训练时没见过的范围,相位变化会变得不稳定。
所以后来出现了:
NTK-aware scaling YaRN LongRoPE
这些方法本质上都是调整 RoPE 的频率或缩放方式,让长上下文外推更稳定。
MiniMind 也支持推理时的 RoPE scaling。
9. 和代码形状对应
调试时你看到:
position_embeddings: [6, 96]
这里:
6 = 输入 token 长度 96 = head_dim
因为 RoPE 是对每个 token 位置、每个 head_dim 维度准备 cos/sin。
如果输入长度是 6,每个 head 维度是 96,就需要:
每个位置一组 96 维的旋转参数。
这和 Q/K 的最后一维对齐。
10. 小结
RoPE 可以用一句话概括:
在 Q/K 空间做位置相关旋转,让 Attention score 天然携带相对位置信息。
它的核心不是“把 sin/cos 加到 embedding 上”,而是:
把位置变成旋转角度; 让 Q/K 点积通过三角恒等式出现 m - n。
理解了这一点,再看 apply_rotary_pos_emb、rotate_half、position_embeddings 的形状,就会清楚很多。