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 代码。
Source Log Coverage
The excerpts below are generated from Renyuan_Log.md and preserve the original tables, code fences, ASCII diagrams, commands, links, and explanations with source line numbers.
| Source | Lines | Title |
|---|---|---|
| 2026-04-16 | 2130-2132 | 空白日期占位 |
| 2026-04-17 | 2133-2255 | RoPE 几何与代码实现 |
| 2026-04-29 | 2475-2577 | RoPE 频率、NTK-aware 与 YaRN |
Source Log: 2026-04-16
Source lines: Renyuan_Log.md:2130-2132
2130 |# 2026-04-16
2131 |
2132 |
Source Log: 2026-04-17
Source lines: Renyuan_Log.md:2133-2255
2133 |# 2026-04-17
2134 |
2135 |## 知识学习
2136 |
2137 |### RoPE
2138 |
2139 |- 今天把 RoPE 的几何直觉重新想清楚了:一个 `D` 维向量会被拆成 `D/2` 个二维平面,每个平面都以原点 `(0, 0)` 为旋转中心。
2140 |- 第 `k` 个平面由 `(x_{2k}, x_{2k+1})` 组成,位置编码的本质就是把这一对坐标绕原点旋转角度 `theta_k`。
2141 |- 不同平面之间是正交、互不干涉的:
2142 | - 平面 0 只影响 `(x0, x1)`
2143 | - 平面 1 只影响 `(x2, x3)`
2144 | - 各平面独立旋转,不会互相混入
2145 |- 它们唯一的系统性联系是频率分布:
2146 | - 低频平面旋转慢,更偏向捕捉长距离关系
2147 | - 高频平面旋转快,更偏向捕捉短距离细节
2148 |
2149 |### 维度与旋转
2150 |
2151 |- 另一个关键理解是:RoPE 不是“单维缩放”,而是二维成对旋转。
2152 |- 在第 `k` 个平面中:
2153 | - `x_{2k}` 是横坐标
2154 | - `x_{2k+1}` 是纵坐标
2155 | - 它们共同组成平面上的点 `P`
2156 |- 当 token 位于第 `m` 个位置时,这个点会被旋转 `m * theta_k`。
2157 |- 旋转后的核心性质:
2158 | - 方向改变
2159 | - 向量长度不变
2160 | - 因而保留了幅值信息,同时把位置信息写进方向关系里
2161 |- 这也解释了为什么必须“成对旋转”:
2162 | 只动一个维度会更像缩放;只有 `(x_{2k}, x_{2k+1})` 联动,才是真正的圆周旋转。
2163 |
2164 |对应代码理解:
2165 |
2166 |```python
2167 |x = rearrange(x, '... (s r) -> ... s r', r=2)
2168 |
2169 |[
2170 | [x0, x1], # 第 1 个平面的坐标
2171 | [x2, x3], # 第 2 个平面的坐标
2172 | ...
2173 | [x62, x63] # 第 32 个平面的坐标
2174 |]
2175 |```
2176 |
2177 |这段代码本质上就是把一维向量按两维一组重排,显式变成“多个二维旋转平面”。
2178 |
2179 |
2180 |
2181 |### 代码实现详解
2182 |
2183 |``` RoPE
2184 | def rotate_tensor(self, x: torch.Tensor) -> torch.Tensor:
2185 | '''
2186 | create a rotated tensor (x_2k, x_2k+1) -> (-x_2k+1, x_2k)
2187 | '''
2188 | # 先把最后一维按两两一组重排:
2189 | # (..., Dh) -> (..., Dh/2, 2)
2190 | # 最后那个长度为 2 的维度分别存放偶数位和奇数位。
2191 | x = rearrange(x, '... (s r) -> ... s r', r=2)
2192 |
2193 | # 拆出每一对中的偶数位和奇数位。
2194 | x_even, x_odd = x.unbind(dim=-1)
2195 |
2196 | # 完成二维平面旋转中的“正交向量”构造:
2197 | # (x_even, x_odd) -> (-x_odd, x_even)
2198 | x = torch.stack((-x_odd, x_even), dim=-1)
2199 |
2200 | # 再还原回原始最后一维的布局,方便和输入逐元素相乘。
2201 | return rearrange(x, '... s r -> ... (s r)')
2202 |```
2203 |
2204 |1. 核心算子:从“一排”到“一对” (rearrange)
2205 |在进行旋转前,必须将平铺的隐藏维度 Dh 进行两两分组。
2206 |
2207 |代码:x = rearrange(x, '... (s r) -> ... s r', r=2)
2208 |
2209 |形状流:(..., 64) -> (..., 32, 2)
2210 |
2211 |意义:物理上确立了 32 个平面。最后一维的 2 代表每个平面内的坐标点 (x_even, x_odd)。
2212 |
2213 |2. 拆解与重组:实现 90° 垂直旋转
2214 |RoPE 的旋转公式中,关键在于构造 rotate_half(x)。其内部逻辑如下:
2215 |
2216 |A. 拆分 (unbind)
2217 |操作:x_even, x_odd = x.unbind(dim=-1)
2218 |
2219 |维度变化:
2220 |
2221 |x (原变量): 保持 (..., 32, 2) 不变。
2222 |
2223 |x_even / x_odd (新变量): 变为 (..., 32)。最后那个 2 被拆掉了。
2224 |
2225 |直观理解:像是把一叠双层卡片拆成了“上层”和“下层”两堆。
2226 |
2227 |B. 取反与配对 (stack)
2228 |操作:x_rotated = torch.stack((-x_odd, x_even), dim=-1)
2229 |
2230 |逻辑:这里的 stack 会新建一个维度,将 -x_odd 和 x_even 按位置重新配对。
2231 |
2232 |变换结果:[a, b] -> [-b, a]。
2233 |
2234 |几何意义:这在二维平面上对应一个标准的逆时针 90° 旋转。
2235 |
2236 |3. 全程形状流动图 (Shape Flow)
2237 |这是理解 RoPE 变换最清晰的视角:
2238 |
2239 |原始输入:(B, H, S, 64)
2240 |—— 64 个特征平铺。
2241 |
2242 |分组 (rearrange):(B, H, S, 32, 2)
2243 |—— 形成 32 个平面坐标系。
2244 |
2245 |提取 (unbind):x_even: (B, H, S, 32) | x_odd: (B, H, S, 32)
2246 |—— 坐标分量拆分。
2247 |
2248 |旋转 (stack):(B, H, S, 32, 2)
2249 |—— 得到 [-x_odd, x_even] 组合。
2250 |
2251 |还原 (rearrange):(B, H, S, 64)
2252 |—— 旋转后的向量重新进入后续点积计算。
2253 |
2254 |
2255 |
Source Log: 2026-04-29
Source lines: Renyuan_Log.md:2475-2577
2475 |# 2026-04-29
2476 |
2477 |## RoPE
2478 |
2479 |$n$ 是动力源: 随着 $n$ 的增加(即 Token 越来越靠后),旋转的角度 $n\theta_i$ 就会越来越大。
2480 |
2481 |
2482 |
2483 |刚才不是说越大,频率越低,角度越小吗
2484 |
2485 |这是个非常棒的观察!这里其实涉及两个不同的变量在“赛跑”,你的直觉敏锐地捕捉到了它们的矛盾点。
2486 |
2487 |我们要区分 位置 n 和 频率 θ
2488 |i
2489 |
2490 | 对最终角度的贡献:
2491 |
2492 |1. 核心公式:角度 = 位置 × 频率
2493 |旋转的角度公式是:
2494 |
2495 |Angle=n×θ
2496 |i
2497 |
2498 |
2499 |这里有两个变量:
2500 |
2501 |n (位置索引):指 Token 是第几个(0,1,2,…)。
2502 |
2503 |θ
2504 |i
2505 |
2506 | (频率):指某个维度旋转的“步长”(由 inv_freq 决定)。
2507 |
2508 |2. 回答你的疑问:为什么你觉得“矛盾”?
2509 |你说的“越大越小”是指维度 i 越大,频率 θ
2510 |i
2511 |
2512 | 越小:
2513 |
2514 |在 高维度(i 很大时),频率 θ
2515 |i
2516 |
2517 | 极其微小(比如 0.0001)。
2518 |
2519 |这意味着在这个维度上,位置 n 每增加 1,角度只增加一点点。
2520 |
2521 |但我说的“越大越大”是指位置 n 增加时,角度会变大:
2522 |
2523 |即使频率 θ
2524 |i
2525 |
2526 | 很小(比如 0.0001),当位置 n 从 1 变成 10000 时,角度也会从 0.0001 变成 1.0。
2527 |
2528 |3. 用“时钟”类比(解决你的困惑)
2529 |想象一个时钟,里面有秒针、分针和时针。它们代表了不同的维度 i。
2530 |
2531 |频率 θ
2532 |i
2533 |
2534 | (由维度决定):
2535 |
2536 |秒针:频率最高(转得最快)。
2537 |
2538 |时针:频率最低(转得最慢,也就是你说的“维度越大,频率越低”)。
2539 |
2540 |位置 n(动力源):
2541 |
2542 |n 就是流逝的时间。
2543 |
2544 |随着时间 n 的流逝,所有的针都在往前转,角度都在变大。
2545 |
2546 |重点来了:
2547 |
2548 |当 n 增加时(时间流逝),秒针转得飞快,很快就转满一圈回到原点(容易产生你之前担心的“碰撞”)。
2549 |
2550 |当 n 增加时,时针转得极慢。虽然它的角度也在变大,但它转得很稳,能区分非常遥远的时间点。
2551 |
2552 |4. 总结:到底谁大谁小?
2553 |变量增加 对频率的影响 对角度 (nθ
2554 |i
2555 |
2556 | ) 的影响 物理意义
2557 |维度 i 变大 变小 变小 旋转步长变短(从秒针变成时针)
2558 |位置 n 变大 无关 变大 随着 Token 靠后,总旋转量累积
2559 |你的困惑点在于: 你把“维度带来的频率衰减”和“位置带来的角度累加”搞混了。
2560 |
2561 |维度决定了这根针转得有多快。
2562 |
2563 |位置决定了这根针转了多久。
2564 |
2565 |所以,在同一个维度下,Token 越靠后(n 越大),角度肯定越大;但在同一个位置下,维度越高,旋转的角度反而越小。
2566 |
2567 |
2568 |你现在能理解为什么 NTK-aware Scaling 很有用了:
2569 |当我们想把上下文从 2k 扩展到 8k 时,如果我们直接把所有维度都“调慢”(位置内插),那么原本就很快的高频维度变慢了,会导致模型在短距离内的分辨精度下降(模型变糊了)。
2570 |
2571 |所以聪明的做法是:保持高频维度不动,只把那些本来就很慢的低频维度变得更慢。 这样既保住了“近视”的清晰度,又获得了“远视”的广度。
2572 |
2573 |现在的逻辑是不是彻底闭环了?低频维度在短距离确实“废”,但它们是为长跑而生的。
2574 |
2575 |
2576 |
2577 |