Lesson 0004 · Phase 1 地基 · 约 45 分钟
RoPE:位置信息的打开方式
第三课的 attention 有一个被我们跳过的问题:它天生不知道词的顺序。这一课补上这块拼图——旋转位置编码(Rotary Position Embedding, RoPE),DeepSeek、GLM、Kimi 三家的共同选择。它也是日后长上下文争夺战的第一块多米诺骨牌。
一、位置盲的 attention:打乱顺序,输出原样
做个思想实验。第三课的打分是 score(i, j) = qi · kj——q、k 全部来自 token 内容的投影,公式里没有任何"第几个"的痕迹。现在把「猫追狗」的三个 token 顺序打乱成「狗追猫」:每个 token 的 q、k 值不变,只是排的位置换了,(L, L) 打分表的行和列同步对调,每一对 token 之间的注意力分数一模一样。
| 猫 | 追 | 狗 | |
|---|---|---|---|
| 猫(打乱前第 1 位) | — | 0.8 | 0.2 |
| 追 | 0.7 | — | 0.6 |
| 狗 | 0.2 | 0.6 | — |
打乱成「狗追猫」后,这张表只是行列对调:猫对追还是 0.8,狗对追还是 0.6。attention 层看两句话的感受完全相同——它分不清"谁追谁"。这就是置换不变性(permutation invariance):attention 的数学结构里没有位置这个变量。
二、核心思想:把"位置"变成"旋转角度"
RoPE 的一句话:让位置 m 的 q 向量在平面上旋转 m·θ 角度,位置 n 的 k 旋转 n·θ——这样两者的点积只取决于角度差 (m−n)·θ,也就是只取决于相对位置。
先看 2D:一根指针的思想实验
把 q 和 k 想象成平面上的两根箭头(从原点出发的向量)。旋转矩阵 R(m) 表示"逆时针转 m·θ 角度":
R(m) = | cos(m·θ) −sin(m·θ) |
| sin(m·θ) cos(m·θ) |
旋转后做点积(用转置写法):
⟨R(m)q, R(n)k⟩ = (R(m)q)ᵀ · (R(n)k)
= qᵀ · R(m)ᵀ · R(n) · k ← 两个旋转矩阵中间相乘
= qᵀ · R(n−m) · k ← 关键一步:R(m)ᵀR(n) = R(n−m)
R(m)ᵀR(n) = R(n−m):先逆转 m·θ 再正转 n·θ,净效果是转了 (n−m)·θ——转角直接相减。于是点积变成 q · R(n−m)·k:m 和 n 各自是多少消失了,只剩差值 n−m。绝对位置进,相对位置出。
数字实验(对照动画面板 D 的读数)
取演示角 θ = 25°/步,|q| = |k| = 1:
| 位置组合 | 角度差 (m−n)·θ | 点积 = cos(差) | 说明 |
|---|---|---|---|
| (m, n) = (3, 1) | 2 × 25° = 50° | 0.64 | 原局面 |
| (m, n) = (5, 3) | 2 × 25° = 50° | 0.64 | 整体平移 +2:点积纹丝不动 |
| (m, n) = (3, 3) | 0° | 1.00 | 同位置:方向全同,满配 |
前两行就是面板 D 的第 1、2 幕:整句 token 平移,两两注意力关系全部不变——模型学的天然是"相对位置"。这正是一个好的位置编码想要的性质:语义上"隔 2 个词"的关系,不管出现在句子开头还是结尾,应该被同等对待。
三、从 2D 到 128 维:把 128 维看成 64 根小箭头
先回答"怎么转":两两配对,每对是一根独立的小箭头
第二节的旋转矩阵 R(m) 一次只作用于一对数字——平面箭头 (x, y) 的两个坐标。而一个 head_dim = 128 的 q 向量有 128 个数。RoPE 的做法:把相邻的两个数配成一对——(x₀, x₁) 是第 0 根箭头、(x₂, x₃) 是第 1 根……一共 64 根独立的小箭头,每根在自己的小平面里旋转。
那么:64 根箭头都用同一个角速度行不行?行——相对位置的性质依然成立,但 64 根针完全同步转动就是信息冗余。RoPE 让每根箭头快慢不同:
θ_i = theta^(−2i/d),i = 0, 1, …, d/2−1(d = head_dim)
第 0 对(i=0): θ₀ = theta⁰ = 1 rad/步 ← 最快(秒针)
最后一对(i=63):θ₆₃ = theta^(−126/128) ← 最慢(时针),theta=10⁶ 时约 1.2×10⁻⁶ rad/步
迷你例子:head_dim = 4(只有 2 根箭头),把数字摊开
| 位置 m | 快钟角度 m·θ₀(= m rad) | 慢钟角度 m·θ₁(theta=10⁶ → θ₁=10⁻³ rad/步) |
|---|---|---|
| 0 | 0° | 0° |
| 1 | 57.3° | 0.057° |
| 2 | 114.6° | 0.115° |
| 3 | 171.9° | 0.172° |
快钟一步跨 57°——位置 1 和 2 一眼区分;慢钟三步才挪 0.17°——远距离处依然不乱。这就是"秒针 / 时针"分工的原形。
本节最重要的洞见:64 份相对性,相加
多频率会不会破坏第二节证明的"只看相对位置"?不会,而且原因很干净:整个 128 维的点积,等于 64 个小平面点积的总和——
q·k = q⁰·k⁰ + q¹·k¹ + … + q⁶³·k⁶³ (按对拆开求和)
每个平面各自是一个 2D RoPE,各自满足 ⟨Ri(m)qi, Ri(n)ki⟩ = qi·Ri(n−m)ki。64 份"只看差"相加,整体依然只看差——多频率不破坏相对性,反而是 64 份相对性的叠加。这就是为什么可以在每个平面上随意换频率。
为什么秒针和时针缺一不可
- 快钟:δ=1 就差 57°,近处一步见分晓;但 360° ÷ 57° ≈ 6 步就绕回一圈——6 步之外"角度相同",远处歧义(位置 6 和位置 0 在快钟眼里无法区分)。
- 慢钟:δ=1 只差 0.057°,近处几乎分辨不出;但绕一圈要 2π ÷ 0.001 ≈ 6283 步——几千步内永不重复,远处清晰。
- 合在一起:近处问快钟、远处问慢钟,64 根针从快到慢铺开,全部距离覆盖。而 theta(rope_theta)是整套时钟的基准——theta 越大所有针越慢,能覆盖的距离跨度越大(脚本
rope_decay.png的两条曲线)。
要点:第 1 幕看单对旋转的"绝对位置进、相对位置出";第 2 幕看整句平移后点积不变;第 3 幕三个小盘就是 64 根针里的三个代表——深色点越靠后(位置越大),快盘绕了几圈、慢盘才走一小段。(动画用 25°/步演示;真实模型从 ~57°/步 到 ~10⁻⁶ rad/步,跨度巨大——脚本 C1 时钟图是真实 theta 版本。)
四、三大模型的 config:数字全部对上(回收第一课)
第一课你亲手验证过三个模型的注意力机制,现在那些数字全部有了含义:
| 模型 | rope 配置(config 一手) | 含义 |
|---|---|---|
| GLM-4.5 | rope_theta = 1,000,000 partial_rotary_factor = 0.5 |
时钟基准极慢(分辨超长距离);每个 128 维头只旋转前 64 维,后 64 维原样通过——保留一部分"不带位置"的内容通道,这是实验上有效的折中 |
| DeepSeek-V3 | MLA + qk_rope_head_dim = 64 | MLA 把 K/V 压缩进缓存(第五课主角),RoPE 只施加在专门留出的 64 维"位置通道"上—— RoPE 和压缩缓存和平共处 |
| Kimi K2 | MLA + RoPE(同族方案) | 与 DeepSeek 同思路;第一课表格里三家的注意力差异,在 RoPE 这一层的具体落点不同 |
ROPE_INIT_FUNCTIONS 里躺着 linear / dynamic / yarn / longrope / llama3 一整排——config 里 rope_scaling 字段一出现,就是模型在告诉你"我动了时钟"。长上下文课正式展开。
五、动手:可自判卷的实现
conda activate py310_qwenpaw
cd <课程工作区目录>
python scripts\rope_from_scratch.py
脚本干四件事(无需写代码,但请逐行读懂 A 部分——注释里标了 shape 流动):
- A · 从零实现:cos/sin 表构造(inv_freq 幂函数)+ rotate_half + 前后两半配对旋转——与 HF 官方实现同款约定
- B · 四道判卷:B1 范数不变 → B2 相对性(平移不变)→ B3 与 transformers 5.5.4 官方
apply_rotary_pos_emb对拍 → B4 partial rope 复现 GLM 的 0.5(前半旋转、后半恒等) - C · 两张图:
rope_clocks.png(多频率时钟,真实 theta=10⁶)+rope_decay.png(相似度随距离衰减,两种 theta 对照) - D · 真实 config 直连:从 ModelScope 拉 GLM-4.5 config.json,rope_theta / partial_rotary_factor 与第一课你填的表格当场对账
rope_clocks.png:三个盘里,"秒针"盘多少步绕满一圈?"时针"盘呢?——用图里的"转 N 圈"数字回答。rope_decay.png:两条曲线在 δ 小时几乎重合、δ 大时才拉开——theta 改变的到底是"近处"还是"远处"的分辨能力?- B4 判卷输出的两个数:后半 0.0e+00、前半 5.6e+00——用这一对数字向自己解释 partial rope 在做什么。
- D 部分打印的 rope_theta 和 partial_rotary_factor:和第一课表格你填的格子一致吗?(不一致才是大新闻)
六、检索练习
七、本周深读任务(一手来源)
- 动手(20 分钟):跑通脚本,四道判卷全绿,完成第五节 4 个观察任务。
- 读源码(15 分钟,本地可达):打开
.../site-packages/transformers/models/llama/modeling_llama.py,搜rotate_half和apply_rotary_pos_emb——总共十几行。你现在应该能"预告"每一行:cos/sin 为什么前后各拼一份?rotate_half 的负号从哪来(90° 分量)? - 选读:RoFormer 原论文(arXiv: 2104.09864)§3.4——本网络若访问不了 arXiv,用镜像或搜标题。第 3.4 节的复数视角与本课的旋转矩阵视角是同一件事的两种写法。
读不懂 shape 流动、判卷出现 FAIL、时钟图有意外发现——直接问我。