位置编码:RoPE 直觉 → 推导 → 实现
一句话定位:注意力打分天生不认识先后顺序,位置编码负责把顺序补进去——RoPE 用「旋转」代替「发号牌」,让模型天然只关心「隔多远」,也是长上下文扩展的主战场。本文走一遍直觉 → 推导 → 实现与外推难题。前置:Attention 与 Transformer——先知道 QKV 打分与因果掩码。
先看一个不认识顺序的模型会怎样
把「猫追狗」三个词交给注意力:每个 token 两两比对打分——可打分用的是向量点积,交换两个 token 的位置,参与比对的还是那几个向量,分数一模一样。在打分环节,注意力只认内容不认座位:「猫追狗」和「狗追猫」算出的分数毫无差别。
但顺序就是语义:追与被追、因果与修饰,全靠词序定夺。顺序信息必须从外面补给模型。补法看似简单——给每个位置发一个固定「号牌」,第 1 位发 1 号、第 2 位发 2 号——可模型就此记死了「1025 号长什么样」,文本一长、号码超纲,模型面对没见过的号牌就开始胡言。
什么样的位置编码,既能让模型懂顺序,又天然只关心「隔多远」,还能往外延伸?
RoPE(Rotary Position Embedding,旋转位置编码)的答案:不发号牌(不给每个位置发固定编号),而是让每个 token 转一个与位置成正比的角度——两个 token 相互打分时,看到的只是彼此的夹角差,而夹角差只取决于位置差。先看它在架构里的位置:
图:RoPE 全景——①在 QKV 变换后、打分前,按各自位置旋转 Q 与 K(V 不动);②打分天然只看位置差;③之后与前篇注意力完全一致
渐进拆解:从发号牌到转指针
RoPE 像钟表指针。问「8 点与 11 点的时针差多少」,答案永远是 90°——不用管是今天还是去年、上午还是下午。旋转天生把「绝对时刻」折进「相对夹角」:两根指针一比,日期信息消失,只剩时间差。模型要的正是这个:打分只看「隔多远」。
谱系:三种补顺序的思路
补顺序的方案按代际演进,三代思路对照如下:
| 代际 | 代表 | 机制 | 代价 |
|---|---|---|---|
| 第一代 · 绝对位置编码 | 原版 Transformer | 给每个位置配一段正弦波纹加到 token 向量上,或干脆让模型学一套专属向量——前者固定、后者随训练定形 | 「号牌焊死」:模型只认训练时见过的号码段,位置数是模型结构的一部分,改不了 |
| 第二代 · 相对位置编码 | ALiBi(线性偏置,按距离线性罚分) | 不再报绝对位置,打分时改按「隔多远」加分或扣分 | 距离感好,但位置信息变成一套外挂规则,与注意力本体分离 |
| 第三代 · RoPE | RoPE | 每个 token 独立转身,不需要看别人在哪——形式上是绝对的 | 效果上是相对的:打分只依赖位置差 |
RoPE 走的第三条路,妙处正是「形式绝对、效果相对」——两种好处一起拿:既没有第一代的「号牌焊死」,也没有第二代的外挂规则。
推导:什么样的函数有这个性质
想要「打分只依赖位置差」,就是要找变换 f,使 f(q, m)·f(k, n) 只含 m−n。二维向量时答案只有一个:旋转。把 q 乘复数 e^{imθ}(即转过 mθ 角)、k 转过 nθ 角,点积展开后 mθ 与 −nθ 恰好合并成 (m−n)θ——三角恒等式保证,这不是巧合是设计。高维向量则拆成两两一组:每组独立转,第 i 组的角速度 θᵢ 随维号递减(θᵢ = base^(−2i/d),base 即各模型配置里的 rope_theta)。
各维波长:同一套编码里的快慢针
角速度不同,每组「指针」转一圈走过的 token 数(波长)就不同——这组实测数字(base=10000、8 维教学算例;真实模型上百维,末维波长更长,结论只强不弱)后面会反复用到:
图:各维波长条形图——组号越靠后针越慢;base 从 1 万调到 100 万(Qwen2-7B 官方配置),末维波长从 6283 拉到约 20 万 token,这是长上下文的地基
实现与缓存
RoPE 的介入点只有一个:QKV 变换之后、打分之前,Q 与 K 各自旋转,V 不动(位置不该掺进被加权的内容)。频率表由每头的维度决定,各头共用同一套 θᵢ,向量内容不同、转速相同。工程上没有任何旋转矩阵真的被乘出来——每个位置的转角固定,cos 与 sin 提前算好存成表,逐 token 查表做乘加即可,代价可忽略。这套「查表 + 逐组乘加」让 RoPE 与注意力的键值缓存天然兼容:缓存里存的 K 已经带着各自的转角,新 token 只需转自己的。
外推与插值:长上下文的桥
快慢针的代价藏在波长里:base=10000 时末维波长 6283,若训练长度只有 4096,模型在这一维见过的最大相位(相位即这一维指针已经转过的角度)差只有 2π×4096/6283 ≈ 4.10 弧度(rad,约 235°)——超过 4096 的位置差,给出的都是它没读过的刻度。直接把位置标号往外续(外推),模型就在「没见过的角度区」里打分,质量骤降。修桥的思路按代际演进:
- 位置插值(PI):把位置标号整体除以倍数 s,强行把超界的相位差压回训练区间——8192 的位置差除以 2 变 4096,相位差恰好落回 4.10 rad 的边界;代价是全盘刻度变密,需短暂微调
- NTK-aware(名字来自「神经正切核」理论,不必深究):干脆调大 base 把针整体调慢(就是上一节的 rope_theta 从 1 万到 100 万),让远处刻度天然落在熟悉区,几乎免微调
- NTK-by-parts:按波长分频段区别对待(秒针不动、分针插值)
- YaRN:在此之上再修正注意力温度,是当前主流——DeepSeek-V3 等模型直接内建 YaRN;Qwen 系则早早把 rope_theta 提到 100 万、把长度预算留给预训练
一条演进线:从「压标号」到「调时钟」,再到「分频段+修温度」:
图:长上下文扩展五级阶梯——从直接外推到 YaRN,思路从「压标号」演进到「调时钟、分频段、修温度」;现成模型扩窗先查它的 rope_scaling 配置
大白话:模型天生分不清先后;RoPE 不给词发号码牌,让每个词按站位转过转盘上的一个角度——两个词对看,只剩「差几个身位」。想让转盘延伸得更远,就把针调慢(调 base)或把刻度压密(插值)。
技术定义:RoPE 出自 RoFormer 论文(2021):在注意力打分前对 Q、K 按位置做复数旋转,旋转不改变向量长度,点积只依赖位置差(三角恒等式保证);工程上预计算各位置 cos/sin 表、逐组乘加,V 不旋转。
动手:亲手验证「只看位置差」
纯 Python 可跑——核心断言:旋转后打分只依赖位置差,与绝对位置无关:
import math, random
random.seed(7)
def rot(v, m, base, d): # 把向量 v 旋转 m 步的角度
out = []
for i in range(0, d, 2): # 两两一组:i=0,2,4,6,对应组号 i/2
th = m * base ** (-i / d) # 组号 g 的角速度 θ_g = base^(-2g/d)
c, s = math.cos(th), math.sin(th)
x, y = v[i], v[i+1]
out += [x*c - y*s, x*s + y*c]
return out
d, base = 8, 10000
q = [random.gauss(0, 1) for _ in range(d)]
k = [random.gauss(0, 1) for _ in range(d)]
dot = lambda a, b: sum(x*y for x, y in zip(a, b))
print(f"{dot(rot(q, 10, base, d), rot(k, 7, base, d)):.6f}") # 位置差 3
print(f"{dot(rot(q, 100000, base, d), rot(k, 99997, base, d)):.6f}") # 同为位置差 3
实测输出两行完全相同:2.043693——绝对位置差了四个数量级,打分纹丝不动(差在浮点末位)。再补一个「加号牌」对照——号牌拼在向量尾部再打分,拼进来的分量会跟内容相乘,绝对位置就漏进了分数:
def additive_dot(m, n, d, base): # 加性对照:号牌拼在尾部
qa = q + [math.sin(m * base ** (-i / d)) for i in range(0, d, 2)]
ka = k + [math.sin(n * base ** (-i / d)) for i in range(0, d, 2)]
return sum(x * y for x, y in zip(qa, ka))
print(f"{additive_dot(10, 7, d, base):.6f}") # 位置差同为 3
print(f"{additive_dot(100000, 99997, d, base):.6f}")
实测输出 2.047734 与 2.790205——同样的位置差 3,换个「楼层」读数就漂移。这就是「旋转优于加性」的数学实感。
边界与取舍
外推失败的根因,与位置编码有什么关系
根因不在注意力,在「刻度没学全」:低频维的波长长于训练长度,训练时只见过它一段相位(4096 长度只覆盖末维 235°),超出训练长度的位置差会把这些维带进没见过的相位区——模型读的是没学过的读数。所以扩上下文的第一问题永远是「位置编码是什么、怎么扩展」,第二问题才是显存与算力(那是长上下文篇的主题)。
RoPE 不是唯一解
ALiBi 式线性偏置实现更简、外推行为平缓,对不需要精确远距结构的场景够用;绝对位置编码在教学场景仍然好懂。选型看三问:要不要扩窗、要不要精确的相对结构、生态与框架支持哪种。
坏味道预警:把「上下文窗口」当「记忆力」
窗口是位置编码与显存共同划定的硬边界,不是模型「记住了」多少——窗口外的内容一个字都不在。另外 base 调大再配长文本微调是成套动作,只调 base 不补训练,长距离任务照样翻车;扩窗之后必须用长文档问答实测,别只看配置参数。交出长任务前,先过快速自检两问:
- 这模型的 rope_scaling 在哪一级?
- 我给它灌过的最长文本,比它的原生训练长度长多少?
两问答不上来,就先别把长任务交出去。
自测
合上文章先自己答,再展开对照:
RoPE 为什么用旋转而不是加性编码?
核心是「打分只依赖位置差」这一性质能否成立。旋转满足:位置 m 的 Q 与位置 n 的 K 各自转过 mθ、nθ 角后,点积只含 (m−n)θ——绝对位置在比对中恰好相消,且每个 token 独立旋转、不需看别人位置,实现也便宜(查 cos/sin 表)。加性编码把位置分量拼进或加进向量,打分时交叉项带着绝对位置——实测同样的位置差 3,位置 (10,7) 打分 2.047734、(100000,99997) 打分 2.790205:模型换个「楼层」读数就漂移,外推必然吃瘪。旋转把「绝对时刻」折成「相对夹角」,正中语言建模「关心相对距离」的需求。
长上下文外推失败的根因和位置编码有什么关系?
直接外推失败,根因是位置编码的「刻度没学全」:RoPE 各维波长不同,低频维波长长于训练长度时(base=10000 末维 6283 token,训练 4096 只见过约 235° 相位),超界的位置差会让模型读到没学过的相位,质量骤降。修法都在位置编码上做文章:位置插值把标号除以 s 压回训练区(需微调);NTK-aware 调大 base 把针调慢(几乎免微调);NTK-by-parts 按波长分频段;YaRN 再加注意力温度修正,是当前主流(如 DeepSeek-V3 内建 YaRN;Qwen 系靠大 base 预训练)。Llama 3 则直接用 base=500000 预训练。扩窗先查模型的 rope_scaling 配置在哪一级,再谈显存算力。
参考与延伸
- Su 等《RoFormer: Enhanced Transformer with Rotary Position Embedding》(2021):RoPE 的原始提出 — arxiv.org/abs/2104.09864
- Chen 等《Extending Context Window of Large Language Models via Positional Interpolation》(2023):位置插值 PI — arxiv.org/abs/2306.15595
- Peng 等《YaRN: Efficient Context Window Extension of LLMs》(2023):分频段插值与温度修正 — arxiv.org/abs/2309.00071
- Men 等《Base of RoPE Bounds Context Length》(NeurIPS 2024):base 框住上下文上限的理论 — arxiv.org/abs/2405.14591
- Press 等《Train Short, Test Long》(2022):ALiBi 线性偏置 — arxiv.org/abs/2108.12409
- Qwen2-7B 官方配置:rope_theta=1000000 的一手实证 — huggingface.co/Qwen/Qwen2-7B
下一站:长上下文——位置编码的桥搭好了,真正的墙在显存与注意力计算:稀疏注意力怎么「少看」。