RoPE 的几何直觉与代码实现
RoPE 的核心不是给每个维度加一个位置值,而是把隐藏维度两两成对,放进多个二维平面里旋转。
如果向量维度是 D,RoPE 会形成 D/2 个二维平面:
(x0, x1), (x2, x3), ..., (xD-2, xD-1)
每个平面都有自己的频率。低频平面旋转慢,更适合长距离关系;高频平面旋转快,更适合近距离区分。
几何性质
二维旋转改变方向,但不改变长度。因此 RoPE 把位置信息写进方向关系里,同时保留幅值信息。
这也是为什么它必须成对处理维度。只缩放单个维度不是旋转,只有 (x_even, x_odd) 联动才构成平面上的点。
代码形状
实现中常见的第一步是重排:
x = rearrange(x, "... (s r) -> ... s r", r=2)
形状从:
(B, H, S, 64)
变成:
(B, H, S, 32, 2)
最后的 2 就是每个二维平面的坐标。
接着:
x_even, x_odd = x.unbind(dim=-1)
x = torch.stack((-x_odd, x_even), dim=-1)
这个变换把 [a, b] 变成 [-b, a],对应二维平面中的 90 度旋转基向量。再和 sin/cos 组合,就得到任意角度旋转。
RoPE 的代码难点不是语法,而是始终记住:隐藏维度被组织成了很多独立的二维旋转平面。
知识补全:相对位置为什么会出现
RoPE 的一个重要性质是,两个 token 的 query/key 点积会自然包含相对位置差。直观地说,如果第 m 个位置旋转了 m * theta,第 n 个位置旋转了 n * theta,它们之间的角度差就是 (m - n) * theta。
因此模型不只是知道“我在第几个位置”,还可以通过旋转后的方向关系感知两个 token 的相对距离。
这也是 RoPE 相比绝对位置编码更适合长上下文扩展的原因之一。当然,长上下文扩展还需要处理频率外推问题,所以会出现 NTK-aware、YaRN 等方法。
学习检查清单
理解 RoPE 时,建议确认:
- 最后一维是否按偶数/奇数成对。
- 每一对是否对应一个二维平面。
- 旋转是否保持向量长度。
- 不同平面是否使用不同频率。
- 点积中如何体现相对位置。
- 长上下文扩展调的是位置、频率,还是缩放策略。
这些问题能避免把 RoPE 只记成一段 rotate_half 代码。