RoPE:旋转位置编码与相对位置#
本页内容5 个部分
查看概览图
按绝对位置旋转 q / k,
点乘后只剩相对位置
RoPE 不把位置向量加到 embedding 上,而是把 head 内每两维当成一个平面,按 m·θᵢ 旋转 q 和 k。
RoPE 的最小单位是一个平面
-
旋转矩阵
R(a) = [[cos a, −sin a], [sin a, cos a]] -
R(a)[x, y] = [x cos a − y sin a, x sin a + y cos a] -
垂直方向
rotate_half([x, y]) = [−y, x]
= R((n−m)θ)
点乘后绝对位置自己消掉
= qm⊤ R((n − m)θ) kn —— 只剩位置差 n − m
RoPE 先按绝对位置旋转 q/k,但 q/k 点乘后只剩相对位置差。
q_rot = q * cos + rotate_half(q) * sink_rot = k * cos + rotate_half(k) * sin
角度单位是弧度,`angle_i = m · θ_i` 可以超过 2π —— cos / sin 本来就是周期函数,转过一圈继续算即可。每个二维对有自己的旋转速度,这才是「多尺度」的来源。
RoPE,全称 Rotary Position Embedding,中文常叫旋转位置编码。
它的作用是:让 Transformer 的 attention 在计算 q · k 时感知 token 之间的相对位置。
一句话版本:
RoPE 不是把位置向量加到 token embedding 上,而是按位置旋转每个 attention head 里的
q和k,让它们点乘时自然带上相对距离信息。
1. 位置编码的问题与计算对象#
1.1 为什么需要位置编码#
Transformer 的 self-attention 本身主要做这件事:
score = q · k
也就是一个 token 的 query 和另一个 token 的 key 做点乘,得到 attention 分数。
如果没有位置信息,模型很难区分:
我 爱 你
你 爱 我
因为 attention 本身对顺序不敏感。于是我们需要告诉模型:
这个 token 在第几个位置
这个 token 和另一个 token 隔了多远
RoPE 的做法不是直接贴一个“第几个位置”的标签,而是把位置变成旋转角度,作用到 q 和 k 上。
1.2 Multi-Head Attention 里的 q 是什么#
假设模型有:
hidden_size = 32
num_heads = 4
head_dim = 8
对某个 token,它的 hidden state 是:
h: 32 维
经过线性层得到:
q = h Wq
k = h Wk
v = h Wv
这里的 q 总体还是 32 维,但在 multi-head attention 里会被拆成 4 个 head:
q = [
head0 的 8 维,
head1 的 8 维,
head2 的 8 维,
head3 的 8 维
]
真实张量形状通常类似:
Q shape = [batch, seq_len, num_heads, head_dim]
比如:
Q shape = [2, 5, 4, 8]
含义是:
2 个样本
每个样本 5 个 token
每个 token 有 4 个 head
每个 head 的 q 是 8 维
RoPE 操作的是最后一维:
head_dim = 8
也就是说,对于每个:
batch b
token position m
head h
拿出:
Q[b, m, h, :] = [q0, q1, q2, q3, q4, q5, q6, q7]
然后在这个 head 自己的 8 维空间里做旋转。
1.3 为什么是每个 head 自己做 RoPE#
因为 multi-head attention 本来就是每个 head 独立算 attention。
不是这样算:
score = q_32dim · k_32dim
而是这样:
head0_score = q_head0 · k_head0
head1_score = q_head1 · k_head1
head2_score = q_head2 · k_head2
head3_score = q_head3 · k_head3
每个 head 都得到自己的一张 attention map。
所以 RoPE 也跟着 attention 的结构走:
head0_score = rope(q_head0, m) · rope(k_head0, n)
head1_score = rope(q_head1, m) · rope(k_head1, n)
head2_score = rope(q_head2, m) · rope(k_head2, n)
head3_score = rope(q_head3, m) · rope(k_head3, n)
这里:
m = query token 的位置
n = key token 的位置
RoPE 的本质是在 attention score 里加入相对位置旋转;实现上是先旋转每个 head 内部的 q/k,再用普通点乘计算 attention。
2. 二维旋转与实现布局#
2.1 二维旋转公式#
RoPE 的最小单位是二维旋转。
给一个二维向量:
[x, y]
旋转角度 a 后得到:
[
x cos(a) - y sin(a),
x sin(a) + y cos(a)
]
这就是标准二维旋转矩阵:
R(a) = [
[cos(a), -sin(a)],
[sin(a), cos(a)]
]
所以:
R(a) [x, y]
= [
x cos(a) - y sin(a),
x sin(a) + y cos(a)
]
2.2 q 里的 x 和 y 是哪部分#
假设一个 head 的 query 是 8 维:
q_head = [q0, q1, q2, q3, q4, q5, q6, q7]
最直观的配对方式是:
(q0, q1)
(q2, q3)
(q4, q5)
(q6, q7)
每一对是一个二维向量。
对第 i 对来说:
x = q_{2i}
y = q_{2i+1}
比如:
第 0 对: [q0, q1] => x = q0, y = q1
第 1 对: [q2, q3] => x = q2, y = q3
第 2 对: [q4, q5] => x = q4, y = q5
第 3 对: [q6, q7] => x = q6, y = q7
注意:有些实现不是相邻配对,而是前半部分和后半部分配对,例如:
[q0, q4]
[q1, q5]
[q2, q6]
[q3, q7]
这两种布局都把维度两两配对后旋转,但要相应置换 Q/K 投影权重和频率排布才等价。加载既有 checkpoint 时不能直接替换配对方式。
2.3 rope(q_head, m) 怎么算#
假设:
head_dim = 8
q_head = [q0, q1, q2, q3, q4, q5, q6, q7]
RoPE 会给每个二维对一个频率:
theta_i = 10000^(-2i / head_dim)
其中:
i = 0, 1, 2, 3
因为 head_dim = 8,所以有 4 个二维对。
于是:
theta_0 = 10000^(-0/8) = 1
theta_1 = 10000^(-2/8)
theta_2 = 10000^(-4/8)
theta_3 = 10000^(-6/8)
如果 token 的位置是 m,第 i 对的旋转角度是:
angle_i = m * theta_i
所以:
(q0, q1) 用 angle_0 = m * theta_0 旋转
(q2, q3) 用 angle_1 = m * theta_1 旋转
(q4, q5) 用 angle_2 = m * theta_2 旋转
(q6, q7) 用 angle_3 = m * theta_3 旋转
完整展开就是:
rope(q_head, m)
= [
q0 cos(m theta_0) - q1 sin(m theta_0),
q0 sin(m theta_0) + q1 cos(m theta_0),
q2 cos(m theta_1) - q3 sin(m theta_1),
q2 sin(m theta_1) + q3 cos(m theta_1),
q4 cos(m theta_2) - q5 sin(m theta_2),
q4 sin(m theta_2) + q5 cos(m theta_2),
q6 cos(m theta_3) - q7 sin(m theta_3),
q6 sin(m theta_3) + q7 cos(m theta_3)
]
伪代码:
from math import cos, sin
def rope(q, m):
if len(q) % 2:
raise ValueError("RoPE head dimension must be even")
# q shape: [head_dim]
# example: head_dim = 8
out = []
head_dim = len(q)
for i in range(head_dim // 2):
x = q[2 * i]
y = q[2 * i + 1]
theta = 10000 ** (-2 * i / head_dim)
angle = m * theta
x_rot = x * cos(angle) - y * sin(angle)
y_rot = x * sin(angle) + y * cos(angle)
out.extend([x_rot, y_rot])
return out
2.4 rotate_half 为什么等价于旋转#
很多代码会写:
q_rot = q * cos + rotate_half(q) * sin
k_rot = k * cos + rotate_half(k) * sin
对单个二维向量:
q_pair = [x, y]
定义:
rotate_half([x, y]) = [-y, x]
那么:
q * cos(a) + rotate_half(q) * sin(a)
就是:
[x, y] * cos(a) + [-y, x] * sin(a)
展开:
[
x cos(a),
y cos(a)
]
+
[
-y sin(a),
x sin(a)
]
得到:
[
x cos(a) - y sin(a),
y cos(a) + x sin(a)
]
也就是:
[
x cos(a) - y sin(a),
x sin(a) + y cos(a)
]
这正是二维旋转公式。
所以:
rotate_half(q)
不是单独完成旋转,它只是提供旋转公式里的垂直方向。
真正的旋转是:
原方向 * cos + 垂直方向 * sin
3. 相对位置与多频率机制#
3.1 为什么 RoPE 能表示相对位置#
普通 attention:
score = q · k
RoPE attention:
score_{m,n} = rope(q_m, m) · rope(k_n, n)
其中:
m = query 的位置
n = key 的位置
写成旋转矩阵:
rope(q_m, m) = R(m theta) q_m
rope(k_n, n) = R(n theta) k_n
所以:
score_{m,n}
= (R(m theta) q_m)^T (R(n theta) k_n)
因为旋转矩阵有性质:
R(a)^T = R(-a)
R(a) R(b) = R(a + b)
所以:
(R(m theta) q_m)^T (R(n theta) k_n)
= q_m^T R(m theta)^T R(n theta) k_n
= q_m^T R(-m theta) R(n theta) k_n
= q_m^T R((n - m) theta) k_n
注意最后只剩:
n - m
也就是相对位置。
这就是 RoPE 最关键的数学点:
分别按绝对位置旋转 q/k
等价于:
在 q · k 的时候加入相对位置差 n - m
3.2 RoPE 不是把 360 度切成很多份#
一个容易误解的点是:
是不是把 360 度切成 token_length * head_dim 份?
不是。
RoPE 不是切圆,而是每个二维对都有自己的旋转速度:
angle_i = m * theta_i
角度单位通常是弧度,不是度。
角度可以超过 2π,也就是可以超过 360 度。因为:
cos(angle)
sin(angle)
本来就是周期函数,超过一圈也可以继续算。
如果 head_dim = 8,频率大概可以直觉理解为:
theta_0 = 1
theta_1 = 0.1
theta_2 = 0.01
theta_3 = 0.001
那么位置 m = 100 时:
第 0 对角度: 100 * 1 = 100 rad
第 1 对角度: 100 * 0.1 = 10 rad
第 2 对角度: 100 * 0.01 = 1 rad
第 3 对角度: 100 * 0.001 = 0.1 rad
这些二维对像不同速度的指针:
第 0 对转得快,感知短距离
第 1 对慢一点
第 2 对更慢
第 3 对最慢,感知长距离
每个频率对应一个周期:
period_i = 2π / theta_i
例如:
theta_0 = 1 => 周期约 6.28 tokens
theta_1 = 0.1 => 周期约 62.8 tokens
theta_2 = 0.01 => 周期约 628 tokens
theta_3 = 0.001 => 周期约 6280 tokens
所以 RoPE 是:
不同维度对用不同速度旋转
不是:
把 360 度均匀切成 token_length * head_dim 份
3.3 每个维度对对应不同位置尺度#
你可以把一个 head 内部理解成有多把位置尺子。
如果:
q_head = [q0, q1, q2, q3, q4, q5, q6, q7]
那么:
(q0, q1) -> theta_0,最快,短尺度
(q2, q3) -> theta_1,慢一点
(q4, q5) -> theta_2,更慢
(q6, q7) -> theta_3,最慢,长尺度
所以是:
q0/q1 这一对是一个 scale
q2/q3 这一对是另一个 scale
q4/q5 这一对是另一个 scale
q6/q7 这一对是另一个 scale
不是单个维度一个 scale,而是一对维度共同组成一个二维旋转平面,共用同一个频率。
通常每个 head 都使用同一组 RoPE 频率:
theta_0, theta_1, theta_2, ...
但每个 head 的 q/k 内容不同,因为它们来自不同的投影参数。因此不同 head 可以学习用这些尺度关注不同模式。
4. 完整例子与回顾#
4.1 一个完整小例子#
假设:
head_dim = 4
q_head = [2, 3, 10, 20]
m = 1
有两对:
第 0 对: [2, 3]
第 1 对: [10, 20]
频率:
theta_0 = 10000^(-0/4) = 1
theta_1 = 10000^(-2/4) = 0.01
角度:
angle_0 = m * theta_0 = 1
angle_1 = m * theta_1 = 0.01
所以:
第 0 对 [2, 3] 旋转 1 rad
第 1 对 [10, 20] 旋转 0.01 rad
展开:
第 0 对:
[
2 cos(1) - 3 sin(1),
2 sin(1) + 3 cos(1)
]
第 1 对:
[
10 cos(0.01) - 20 sin(0.01),
10 sin(0.01) + 20 cos(0.01)
]
最后拼回:
rope(q_head, 1)
= [
2 cos(1) - 3 sin(1),
2 sin(1) + 3 cos(1),
10 cos(0.01) - 20 sin(0.01),
10 sin(0.01) + 20 cos(0.01)
]
4.2 最终总结#
RoPE 的计算流程:
输入 token hidden state
↓
线性层生成 Q/K/V
↓
Q/K reshape 成 [batch, seq_len, num_heads, head_dim]
↓
对每个 head 内部的 Q/K 做 RoPE
↓
每两个维度组成一个二维平面
↓
第 i 个二维平面使用 theta_i
↓
位置 m 的旋转角度是 m * theta_i
↓
旋转后的 Q/K 做普通 attention 点乘
↓
点乘结果自然包含相对位置 n - m
最核心的公式:
rope(q_m, m) · rope(k_n, n)
= q_m^T R((n - m) theta) k_n
最核心的直觉:
RoPE 先按绝对位置旋转 q/k,
但 q/k 点乘后只剩相对位置差。
最核心的实现:
q_rot = q * cos + rotate_half(q) * sin
k_rot = k * cos + rotate_half(k) * sin
其中:
rotate_half([x, y]) = [-y, x]
它对应二维旋转公式里的垂直方向。
原始资料:RoFormer。RoPE 使点积中的显式位置因子依赖相对位置;完整 attention score 仍取决于 Q/K 的内容。
延伸阅读#
- MLA:潜在表示压缩与解耦 RoPE:如何用压缩表示减少 KV Cache,并处理 RoPE 与矩阵吸收的冲突?