09. 位置编码:RoPE 与长上下文
前几篇把一层里的零件拆完了:注意力让“申请”去看“年假”,FFN往向量里加知识,残差和归一化让 32 层能稳定地叠起来。这些零件里有一个缺口:注意力打分只比较两个向量的内容,不知道它们相隔多远、谁在前谁在后。这一篇回答模型怎样知道词序,Llama-3.1-8B 用的 RoPE(旋转位置编码)具体怎样转、rope_theta 控制什么,以及 8192 训练出来的位置怎样拉到 131072。读完你应该能看懂 config.json 里 rope_theta、rope_scaling 这几个字段,判断给 Qwen 打开 YaRN 会带来什么变化,也能说清 Llama 4 为什么有几层干脆不用位置编码。
打乱词序,注意力算出的结果不变
先做一个思想实验。把“年假怎么申请”换成“申请怎么年假”,两句话的 token 一样,只是顺序不同。在没有任何位置信息的注意力里,“申请”的 Q 和“年假”的 K 点积多少,只取决于这两个向量本身;把序列打乱,每对 token 之间的分数一个都不变,每个 token 最终得到的输出也不变,只是跟着换了位置。数学上这叫置换等变:输入怎么排,输出就跟着怎么排,内容上没有区别。
对语言来说这显然不行。“小林批准了经理的年假”和“经理批准了小林的年假”意思相反。模型必须在某个地方拿到“第几个位置”的信息,这件事叫位置编码(positional encoding)。
严格说,因果掩码已经泄露了一点顺序:第 5 个位置能看见 5 个 token,第 2 个位置只能看见 2 个,模型可以从“我能看到多少东西”间接推出自己的大致位置。后面讲 NoPE 时会用到这一点。但只靠它,模型很难精确区分“隔 3 个词”和“隔 4 个词”,所以主流模型都显式加了位置信息。
位置信息加在哪里:输入、分数,还是 q 和 k
位置信息可以放进三个地方,历史上的方案也大致按这个顺序演进。
加在输入上(绝对位置)。 2017 年的原始 Transformer 给每个位置算一个固定的正弦/余弦向量,直接加到词嵌入上;GPT-2 和 BERT 改成可学习的位置表,GPT-2 有 1024 行,BERT 有 512 行,每个位置查一行加上去。问题是表有多长,模型就只能处理多长:GPT-2 的第 1025 个位置根本没有对应的那一行。
加在分数上(相对位置)。 注意力分数真正关心的是“两个词隔多远”。T5 给每个头学一组偏置,按距离分进 32 个桶(近处分得细,远处分得粗,最远到 128),直接加到注意力分数上。ALiBi 更简单,不学参数,每个头按距离线性扣分:距离为 d 就减去 m·d,8 个头的斜率 m 是 1/2、1/4……1/256。ALiBi 的论文标题就是“训练短、测试长”,它在超出训练长度时比较稳,BLOOM、MPT 用过它。
转动 q 和 k(RoPE)。 2021 年的 RoFormer 提出另一种做法:不加任何东西,而是在算点积之前,把 q 和 k 按各自的位置旋转一个角度。下一节会看到,旋转之后的点积自然只取决于相对距离。Llama、Qwen、Mistral、DeepSeek、gpt-oss 等主流开源模型都用 RoPE 或它的变体。
| 方案 | 位置放在哪 | 有没有长度上限 | 典型模型 |
|---|---|---|---|
| 正弦绝对位置 | 加到输入向量 | 公式上没有,但超出训练长度效果差 | 原始 Transformer |
| 学习式绝对位置 | 查表加到输入向量 | 表的行数就是上限 | GPT-2(1024)、BERT(512) |
| T5 相对偏置 | 加到注意力分数 | 远距离并入同一个桶 | T5 |
| ALiBi | 按距离线性扣分 | 无硬上限,外推较稳 | BLOOM、MPT |
| RoPE | 旋转 q 和 k | 无硬上限,但超出训练长度会失效,要靠缩放 | Llama、Qwen、DeepSeek 等 |
RoPE:每个位置转一个角度
先在二维里手算一遍。假设“申请”的查询向量是 q = (1, 0),“年假”的键向量是 k = (0, 1),两者垂直,不旋转时点积为 0。规定每往后一个位置,向量逆时针转 θ = 0.5 弧度。“年假”在位置 1,“申请”在位置 3:
text
旋转:R(α)·(x, y) = (x·cos α − y·sin α, x·sin α + y·cos α)
q₃ = R(3×0.5)·(1, 0) = (cos 1.5, sin 1.5) = (0.0707, 0.9975)
k₁ = R(1×0.5)·(0, 1) = (−sin 0.5, cos 0.5) = (−0.4794, 0.8776)
q₃ · k₁ = 0.0707×(−0.4794) + 0.9975×0.8776 = −0.0339 + 0.8754 = 0.8415现在把两个词整体往后挪两格,“年假”在 3、“申请”在 5。q 转了 2.5 弧度,k 转了 1.5 弧度,两个向量一起多转了 1 弧度,它们之间的夹角没有变,点积还是 0.8415。
图里两个圆盘的向量朝向完全不同,但橙色夹角一样大。右边的表是脚本算的结果:位置差为 2 的三行点积相同,位置差为 0 或 −2 的结果不同。正负号不同说明 RoPE 还能区分“在前”和“在后”。把维度换成真实的 128、rope_theta 换成 500000、q 和 k 换成随机向量,(10, 7)、(1003, 1000)、(100003, 100000) 三组位置的点积都是 4.422094,结论不变。
背后的原因只有一行:旋转 mθ 再和旋转 nθ 的向量做点积,等于原向量在相对角度 (m − n)θ 下的点积。
text
(R(mθ)·q) · (R(nθ)·k) = q · R((n − m)θ)·k这就是 RoPE 的全部关键:位置是绝对地加进去的(每个 token 按自己的位置转),起作用的却只有相对距离。 它还有两个工程上的好处。第一,旋转不改变向量长度,不会像加法那样干扰内容本身的大小。第二,RoPE 只作用在 q 和 k 上,v 不动;推理时 KV Cache 里存的是已经转好的 k,新 token 只需转自己的 q 和 k,历史 K 不用重算(KV Cache 的机制见推理系列第 03 篇)。
128 维怎样转:64 个频率与 rope_theta
二维只有一个转速。Llama-3.1-8B 每个头的 q、k 是 128 维,RoPE 把它分成 64 对,每对当作一个二维平面,各用自己的转速:
text
θᵢ = rope_theta^(−2i / d) i = 0, 1, …, 63;d = head_dim = 128;rope_theta = 500000
第 i 对在位置 m 的转角 = m · θᵢ
波长 λᵢ = 2π / θᵢ 转一整圈要经过多少个位置
i = 0:θ = 1 λ ≈ 6.3
i = 16:θ ≈ 0.0376 λ ≈ 167
i = 32:θ ≈ 0.00141 λ ≈ 4443
i = 48: λ ≈ 118143
i = 63: λ ≈ 2559196第 0 对每个位置转 1 弧度,6 个多位置就转一圈,它分辨“紧挨着”和“隔两个词”很灵敏,但隔 100 个词和隔 106 个词在它看来差不多。第 63 对要两百多万个位置才转一圈,近处几乎不动,专门表示“隔了几千、几万个 token”。这和钟表的秒针、分针、时针是一个道理:几组不同转速合起来,才能在很大的范围内既分得清近处又分得清远处。
图里蓝点是原始波长,在对数刻度上是一条直线,这就是公式里指数的效果。绿圈是下一节要讲的 llama3 缩放,先看两条红色虚线:原始训练长度 8192 大约落在第 35 对,扩展后的 131072 落在第 48 对附近。
rope_theta 就是这个公式的底数,决定最慢的那几对有多慢。Llama 2 用 10000,最长波长约 54410;Llama 3 提到 500000,最长约 256 万;Qwen2.5 和 Qwen3-8B 用 1000000,最长约 506 万。Llama 3 的技术报告说明,提高底数是为了更好地支持长上下文。底数越大,低频对转得越慢,长距离越不容易“转过一圈又回到原点”,但相邻位置在低频对上的角度差也越小,分辨率被摊薄,所以它不是越大越好,而是和训练长度一起选。
在 Transformers 的 modeling_llama.py 里,这部分代码很短。LlamaRotaryEmbedding 先按上面的公式算出 64 个频率,apply_rotary_pos_emb 对 q、k 做旋转:
python
# 简化自 LlamaRotaryEmbedding.forward 与 apply_rotary_pos_emb
inv_freq = 1.0 / (rope_theta ** (torch.arange(0, 128, 2) / 128)) # [64]
freqs = position_ids[:, None] * inv_freq[None, :] # [seq, 64]
emb = torch.cat([freqs, freqs], dim=-1) # [seq, 128]
cos, sin = emb.cos(), emb.sin()
def rotate_half(x): # 前 64 维与后 64 维配对
x1, x2 = x[..., :64], x[..., 64:]
return torch.cat([-x2, x1], dim=-1)
q = q * cos + rotate_half(q) * sin # q: [batch, 32 头, seq, 128]
k = k * cos + rotate_half(k) * sin # k: [batch, 8 头, seq, 128]注意 Transformers 的 Llama 实现把第 j 维和第 j+64 维配成一对,而 Llama 4 的实现用复数乘法,把相邻两维配成一对。两种排法在数学上等价,但权重是按各自的排法训练的,转换模型格式时排错了,模型不会报错,只会胡言乱语。
超出训练长度时会发生什么
RoPE 公式本身没有长度上限,位置 20 万也能算出角度。问题在于模型没见过。Llama 3 主训练用 8192 长度的序列,第 40 对的波长约 22911,在 8192 个位置内只转了三分之一圈多一点;如果直接拿 10 万长度的输入去推理,这一对会转到训练时从没出现过的角度,注意力分数变成模型无法解释的数值。表现通常是:超过某个长度后困惑度突然上升,回答开始重复、跑题或者胡编,“大海捞针”测试(在长文里埋一句话再提问)找不到针。
资料助手想把整本员工手册(假设 6 万 token)塞进 Llama 2(训练长度 4096)时就会遇到这个问题。判断方法是看位置相关的指标,而不是只看能否运行:按位置分段统计困惑度,或者在不同深度埋针测试。能把长文本塞进去不出错,只说明显存够;模型能不能用好这么长的上下文,要另外测,第 12 篇会专门区分“能读”和“读得好”。
把 8192 拉到 131072:插值、NTK、YaRN 与 Llama 3 的分频缩放
既然问题是“转到了没见过的角度”,思路就是想办法让长序列的角度落回训练见过的范围。几种方法的区别在于对 64 个频率对一视同仁,还是区别对待。
位置插值(Position Interpolation,PI)。 2023 年 Meta 的论文把位置 m 换成 m × L/L′(L 是训练长度,L′ 是目标长度),等于所有频率除以同一个倍数 s = L′/L。这样最大转角回到训练范围内,LLaMA 只需约 1000 步微调就能扩到 32768。代价是高频对也被放慢了 s 倍:原来相邻两个 token 在第 0 对上差 1 弧度,插值 4 倍后只差 0.25 弧度,模型分辨近处词序的能力变差。
NTK-aware 缩放。 社区随后提出不改位置、改底数:把 rope_theta 换成 theta × s^(d/(d−2))。以底数 10000、d = 128、s = 4 为例,新底数约 40890;代入公式会发现第 0 对的频率完全不变,第 63 对正好慢了 4 倍,中间各对按指数逐渐过渡。高频保住了,低频被拉长了。Transformers 里的 dynamic 类型是它的动态版本:只有输入长度超过 max_position_embeddings 时才按当前长度计算新底数。
YaRN。 2023 年的 YaRN 把“区别对待”做得更细:先看每一对在原训练长度内转了多少圈。转了 32 圈以上的(高频)完全不动;转不到 1 圈的(低频)按插值除以 s;中间的用线性斜坡混合两者。它还额外把注意力分数放大一点,补偿拉长后分布变平。用 Transformers 的函数代入 Qwen3-8B 的参数(底数 1000000、d = 128、factor 4、原始长度 32768)实算:第 0—23 对不变,第 24—39 对缩放 1.05—3.40 倍,第 40—63 对除以 4;attention_factor 为 0.1·ln 4 + 1 ≈ 1.139,乘在 cos 和 sin 上,q、k 各乘一次,分数整体放大约 1.30 倍。
Llama 3 的 llama3 缩放。 Llama-3.1-8B 的 config.json 写着:
json
"rope_scaling": {
"rope_type": "llama3", "factor": 8.0,
"low_freq_factor": 1.0, "high_freq_factor": 4.0,
"original_max_position_embeddings": 8192
}Transformers 的 modeling_rope_utils.py::_compute_llama3_parameters 用它们算出两条分界线:波长短于 8192 ÷ 4 = 2048 的对是高频,原样保留;波长长于 8192 ÷ 1 = 8192 的对是低频,频率除以 8;介于两者之间的按 smooth = (8192 / 波长 − 1) / (4 − 1) 在“除以 8”和“不变”之间插值。代入 64 对的实际波长,结果是 29 对不变、29 对除以 8、第 29—34 这 6 对分别缩放 1.21、1.55、2.03、2.69、3.68、5.26 倍,就是上图里绿圈的形状。和 YaRN 相比,它的分界按波长和原训练长度直接划定,不改注意力分数的大小(attention_factor 恒为 1)。
这些缩放公式只负责让角度落回熟悉的范围,模型还要在长文本上继续训练才能真正用好长上下文。Llama 3 的技术报告写明,长上下文是在主训练之后分六个阶段从 8K 逐步拉到 128K,共约 800B token,每个阶段都要确认短文本成绩恢复、当前长度下大海捞针全部答对(训练过程见训练系列第 07 篇)。所以 Llama 3.1 的 131072 是“缩放公式 + 长文本继续训练”的结果,不是改一行配置就白得的。
Qwen 的做法是把 YaRN 留给使用者开关。Qwen3-8B 的 config.json 没有 rope_scaling,max_position_embeddings 是 40960;模型卡说明原生支持 32768,需要更长时在配置里加 {"rope_type": "yarn", "factor": 4.0, "original_max_position_embeddings": 32768},官方验证过到 131072。模型卡同时提醒两件事:主流推理框架实现的是静态 YaRN,开启后不论输入多短都按 factor 4 缩放,可能拖累短文本的表现,所以只在确实需要长上下文时开;如果实际文本只到 65536,factor 设 2 更合适。资料助手平时问答都在几千 token 以内,不该为了偶尔的长文档全局打开 YaRN,更稳妥的做法是给长文档任务单独部署一份开启 YaRN 的服务,并用自己的长文本评测确认效果。
NoPE 与 Llama 4 的 iRoPE
另一条路是部分层干脆不加位置编码,叫 NoPE(No Positional Encoding)。2023 年的一项研究在小规模因果解码器上发现,不加任何位置编码的模型也能学会词序,靠的正是开头说的因果掩码:每个位置可见的 token 数不同,模型可以从中推出位置;而且没有显式位置,也就不存在“没见过的位置”,在部分长度泛化任务上表现更好。这个结论来自小规模模型和合成任务上的实验,能否直接推广到几百亿参数的模型,要看具体模型的验证。
Llama 4 把两种层交替使用,Meta 称之为 iRoPE(i 指 interleaved,交替)。从 Llama 4 Scout 的 config.json 可以直接读出层模式:no_rope_layers 共 48 项,每第 4 项是 0,即第 3、7、11……47 层不用 RoPE;attention_chunk_size 为 8192。Transformers 的 modeling_llama4.py 和配置类据此把 RoPE 层设成分块注意力,NoPE 层设成全局注意力。Maverick 的配置有相同的 NoPE 层模式。
分块注意力和滑动窗口不同:序列被切成一段段 8192 长的块,每个 token 只看自己所在块里的前文(图左下,第 5 个 token 看不到前 4 个)。这样 RoPE 层里的相对距离永远不超过 8191,正好落在 original_max_position_embeddings 8192 之内,不会出现没见过的角度。跨块的信息全靠每 4 层一次的 NoPE 全局层传递。NoPE 层还有一个补偿:Meta 博客提到推理时对注意力做温度缩放,源码里是查询向量乘以 log1p(floor((位置+1) / 8192)) × 0.1 + 1,位置 0—8190 为 1,8191 起为 1.069,约 100 万处为 1.486,用来抵消长序列里注意力被摊平的问题。Scout 的配置里 RoPE 层另外还带了 factor 16 的 llama3 缩放,Maverick 没有。
Meta 公布的 Scout 上下文是 1000 万 token,预训练和后训练都用 256K 长度。从 256K 到 1000 万靠的是这套结构的外推能力,这么长的上下文实际效果怎样,需要按自己的任务测。
RoPE 还有几种变体会在后面出现:DeepSeek 的 MLA 把 RoPE 单独放在一小部分维度上,以便压缩 KV(第 17 篇);多模态模型的 M-RoPE 把旋转维度分给时间、高、宽三个方向(第 14 篇);有些模型只对 head_dim 的一部分做旋转,config.json 里对应 partial_rotary_factor。
读配置时看哪几个字段
拿到一个新模型,和位置相关的字段可以按这个顺序看:rope_theta 决定最慢的频率;max_position_embeddings 是配置声明的最大长度,但它可能已经包含了缩放后的长度(Llama 3.1 的 131072),也可能小于模型卡宣称的上下文(Qwen3-8B 的 40960);rope_scaling 或新版的 rope_parameters 里的 rope_type 告诉你用了哪种缩放,original_max_position_embeddings 是真正训练过的基础长度;最后看有没有 no_rope_layers、layer_types、sliding_window 这类字段,判断是否有层不用 RoPE 或只看局部。字段的逐项解读见第 19 篇。
注意力本身只比较内容,词序要靠位置编码补上;RoPE 把 q、k 按位置旋转,使点积只取决于相对距离,64 对不同转速分别负责近处和远处,rope_theta 决定最慢的那几对。超出训练长度时低频对会转到没见过的角度,插值、NTK、YaRN 和 Llama 3 的分频缩放都是把角度拉回熟悉范围,区别在于是否保护高频;Llama 4 则让部分层不用位置、其余层只在 8192 块内用 RoPE。位置信息加进去以后,32 层算完,最后一个位置的向量还要变回文字,下一篇看输出层与解码:从 logits 到文字。
