Lesson 0004 · Phase 1 地基 · 约 45 分钟

RoPE:位置信息的打开方式

读完本课你将能:证明 attention 是"位置盲"的,解释 RoPE 的旋转恒等式,读懂 config 里的 rope_theta / partial_rotary_factor / rope_scaling

第三课的 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.80.2
0.70.6
0.20.6

打乱成「狗追猫」后,这张表只是行列对调:猫对追还是 0.8,狗对追还是 0.6。attention 层看两句话的感受完全相同——它分不清"谁追谁"。这就是置换不变性(permutation invariance):attention 的数学结构里没有位置这个变量。

所以位置必须从外面注入 两条经典路线:① 输入端加——GPT-2 的做法,在进入模型前给每个位置的嵌入向量加上一个"位置向量"(可学习参数表);② attention 内部打——在算 q·k 的时候现场注入位置差。RoPE 是路线②的冠军方案:不占输入端、天然表达相对距离、对 KV Cache 友好(第四节展开)。

二、核心思想:把"位置"变成"旋转角度"

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)·km 和 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)1.00同位置:方向全同,满配

前两行就是面板 D 的第 1、2 幕:整句 token 平移,两两注意力关系全部不变——模型学的天然是"相对位置"。这正是一个好的位置编码想要的性质:语义上"隔 2 个词"的关系,不管出现在句子开头还是结尾,应该被同等对待。

顺带白送的两条性质 ① 范数不变:旋转是正交变换,||R(m)q|| = ||q||——只拧方向、不拉伸长度,q、k 携带的内容信息不被破坏(脚本 B1 判卷)。② V 不需要转:V 是被加权平均的"内容",点积打分只用 q·k,所以 RoPE 只碰 Q、K、不碰 V——这直接带来 KV Cache 友好(第五节)。

三、从 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
157.3°0.057°
2114.6°0.115°
3171.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)ki64 份"只看差"相加,整体依然只看差——多频率不破坏相对性,反而是 64 份相对性的叠加。这就是为什么可以在每个平面上随意换频率。

为什么秒针和时针缺一不可

要点:第 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_scaling 训练时位置只见过 0 … 32767(比如),推理时用户硬喂 100000 个 token——时钟上这些"没转过的地方"落点失控,质量崩掉。修法家族统称 rope_scaling:把角度整体放缓(Linear)、用 NTK 插值调频率、或 YaRN 按频率分段处理。transformers 的 ROPE_INIT_FUNCTIONS 里躺着 linear / dynamic / yarn / longrope / llama3 一整排——config 里 rope_scaling 字段一出现,就是模型在告诉你"我动了时钟"。长上下文课正式展开。

五、动手:可自判卷的实现

conda activate py310_qwenpaw
cd <课程工作区目录>
python scripts\rope_from_scratch.py

脚本干四件事(无需写代码,但请逐行读懂 A 部分——注释里标了 shape 流动):

  1. A · 从零实现:cos/sin 表构造(inv_freq 幂函数)+ rotate_half + 前后两半配对旋转——与 HF 官方实现同款约定
  2. B · 四道判卷:B1 范数不变 → B2 相对性(平移不变)→ B3 与 transformers 5.5.4 官方 apply_rotary_pos_emb 对拍 → B4 partial rope 复现 GLM 的 0.5(前半旋转、后半恒等)
  3. C · 两张图rope_clocks.png(多频率时钟,真实 theta=10⁶)+ rope_decay.png(相似度随距离衰减,两种 theta 对照)
  4. D · 真实 config 直连:从 ModelScope 拉 GLM-4.5 config.json,rope_theta / partial_rotary_factor 与第一课你填的表格当场对账
观察任务(带结果回来)
  1. rope_clocks.png:三个盘里,"秒针"盘多少步绕满一圈?"时针"盘呢?——用图里的"转 N 圈"数字回答。
  2. rope_decay.png:两条曲线在 δ 小时几乎重合、δ 大时才拉开——theta 改变的到底是"近处"还是"远处"的分辨能力?
  3. B4 判卷输出的两个数:后半 0.0e+00、前半 5.6e+00——用这一对数字向自己解释 partial rope 在做什么。
  4. D 部分打印的 rope_theta 和 partial_rotary_factor:和第一课表格你填的格子一致吗?(不一致才是大新闻)

六、检索练习

七、本周深读任务(一手来源)

本周必做
  1. 动手(20 分钟):跑通脚本,四道判卷全绿,完成第五节 4 个观察任务。
  2. 读源码(15 分钟,本地可达):打开 .../site-packages/transformers/models/llama/modeling_llama.py,搜 rotate_halfapply_rotary_pos_emb——总共十几行。你现在应该能"预告"每一行:cos/sin 为什么前后各拼一份?rotate_half 的负号从哪来(90° 分量)?
  3. 选读:RoFormer 原论文(arXiv: 2104.09864)§3.4——本网络若访问不了 arXiv,用镜像或搜标题。第 3.4 节的复数视角与本课的旋转矩阵视角是同一件事的两种写法。

读不懂 shape 流动、判卷出现 FAIL、时钟图有意外发现——直接问我