Transformer 解剖:从 Attention 到推理系统
第 4 章 位置编码:从 Sinusoidal 到 RoPE
第 4 章 位置编码:从 Sinusoidal 到 RoPE
第 2 章我们留了一个伏笔:Self-Attention 不感知位置。把同一组 token 重新排序,注意力权重和输出会跟着排序但不会改变值。这从工程上可以严格证明(参考 2.9 节的「排列等变性」),实际后果是——「animal eat fish」和「fish eat animal」在 Self-Attention 看来是同一回事。
但语言显然在乎顺序。要让模型理解「the cat sat on the mat」和「the mat sat on the cat」是不同的事实,必须从外部把位置信息注入进去。怎么注入——这就是位置编码(positional encoding)要解决的问题。
这件事看起来很小:不就是给每个位置加一个标记吗?但如果你跟着大模型这九年的演化看一遍,会发现位置编码是 Transformer 这个架构里改动最频繁、影响最深的子模块。原始论文用的 Sinusoidal 早就被淘汰,BERT 的 Learned 位置嵌入也用得少了,今天主流的 Llama / DeepSeek / Mistral 几乎全都用 RoPE,少数用 ALiBi。每一次改动,都是在解决一个具体的工程或者外推问题。
这一章我们沿着「为什么需要位置编码 → Sinusoidal → Learned → 相对位置 → RoPE → ALiBi → 长上下文外推」的路线走。读完之后你应该能:从论文里看到一个新的位置编码就立刻判断出它属于哪一类、解决了上一代的什么问题、外推性质如何。
4.1 为什么 Self-Attention 看不见位置
先把这件事讲透。
Self-Attention 的输入是 token 嵌入序列 。它的输出是 。整个计算用的是矩阵乘法和 softmax,没有任何运算依赖于 token 在序列中的位置索引 。
形式化一下。设 是任意一个置换矩阵(把行的顺序打乱),那么:
这意味着:输入的位置 调到位置 ,输出的位置 也跟着调到位置 ,但内容(每个位置上的向量)一字不差。如果模型的下游任务(比如语言建模)只关心「这个位置应该是什么 token」,它会得出和原始顺序完全一样的结论——根本分不清「the cat sat on the mat」和「the mat sat on the cat」。
直觉上想:Self-Attention 是「查找」——每个 token 根据内容去找其他相关的 token。但它没有「我和你之间隔了多少步」「你在我左边还是右边」这种位置概念。在自然语言里,「the woman saw the man with the telescope」这句话的歧义(who has the telescope)就高度依赖于 with 这个介词和它前后短语的相对位置——位置一改,整个句子的语义都变了。
所以位置编码要做的事是:给每个 token 的嵌入打上一个「位置烙印」,让 Self-Attention 在做相似度比较时,能间接感知到位置。
4.2 第一版方案:Sinusoidal 位置编码
原始 Transformer 论文里的方案叫 Sinusoidal Positional Encoding——给每个位置 算出一个 维向量,作为「位置向量」加到 token 嵌入上:
其中 是位置(), 和 是向量的偶数维和奇数维()。
公式直接看会觉得「这哪来的」。我们拆开看一下它的设计:
第一,每个位置 的位置编码 是一个 维向量,由若干对 拼成。每一对来自不同的频率。
第二,频率从 (最高频)一路降到 (最低频)。具体地,第 对的频率是 。
第三,最终的输入是「token 嵌入 + 位置编码」:
这个加法是逐元素加(element-wise),不是拼接。
flowchart LR
T["token 嵌入<br/>x_i"] --> ADD["+ 加法"]
P["位置编码<br/>PE_i"] --> ADD
ADD --> X["x_i' 进入 Self-Attention"]
subgraph howpe ["PE 怎么算"]
PI["位置 pos"] --> F1["sin(pos/10000^0)"]
PI --> F2["cos(pos/10000^0)"]
PI --> F3["sin(pos/10000^(2/d))"]
PI --> F4["cos(pos/10000^(2/d))"]
PI --> FX["...直到 sin/cos(pos/10000^1)"]
F1 --> CAT["拼成 d_model 维向量"]
F2 --> CAT
F3 --> CAT
F4 --> CAT
FX --> CAT
end
为什么用正弦余弦?这件事 Vaswani 论文给了一个数学论证:对任意固定的偏移 , 都可以表示为 的线性变换。
具体推导:对一对 ,我们有:
也就是说:
这个右边的旋转矩阵只依赖偏移 ,不依赖 。这意味着:模型可以通过一次线性变换,从位置 的编码推出位置 的编码——也就是说,「相对位置」信息被天然编码进去了。
而每个频率的周期不同:高频段( 大)周期短,能区分相邻位置;低频段( 小)周期长,能编码远距离。整个 维向量像是一个「多频率的位置时钟」,从最快的针走到最慢的针。
直觉上这设计很美,但在实战中 Sinusoidal 有几个问题:
- 加性叠加在低维分量上对 token 嵌入有干扰。位置编码和 token 嵌入直接相加,意味着同一个维度里既有 token 信息又有位置信息——它们必须共享有限的表示带宽。
- 对长距离的衰减不够好。点积注意力下, 与 的内积包含 这一项。从 sinusoidal 的性质可以推出,这个项随 增大确实会衰减,但衰减得不够快也不够规则——大约是按 振荡的方式衰减,长距离下偶尔会出现意外的高相关性。
- 外推不稳定。理论上 sinusoidal 可以外推到任意长度(公式里 没有上限),但实际训练时模型只见过 的位置,从未见过更大的位置,于是在测试时用更长的序列时性能会显著下降。
4.3 第二版方案:Learned 位置嵌入
BERT 时代主流换成了 Learned Positional Embedding:
self.pos_emb = nn.Embedding(max_seq_len, d_model)
每个位置 都对应一个可学习的 维向量,作为参数被训练。和 Sinusoidal 一样,最终是相加:
优点很直接:模型可以自由学到任何对当前数据最适合的位置表示——不再受限于人工设计的 函数。
代价也很直接:没法外推。如果你训练时用 ,那 pos_emb 这张表就只有 512 行,第 513 个位置根本没有对应的嵌入——模型直接坏掉。
BERT、GPT-1、GPT-2、ViT 都用了 Learned 位置嵌入。这个时代的模型上下文长度都被卡在 512 / 1024 / 2048——一是模型设计如此,二是即便你想把 1024 训成的模型推理时跑 4096,没有外推性质,模型也会失效。
4.4 第三版方案:相对位置编码
Sinusoidal 和 Learned 都属于绝对位置编码——给每个位置 算一个嵌入,加到 token 上。但语言的本质规律往往是「相对的」——主谓关系是「主语在前」,介词短语是「依附于前面的名词」,疑问代词倾向于句首——这些规律关心的是两个 token 之间的距离和方向,而不是它们的绝对位置。
相对位置编码(Relative Position Encoding)的思路是:直接把『 和 之间的相对距离 』作为一个特征注入到 attention 计算中,而不是给每个绝对位置打标签。
Shaw et al.(2018)的原始相对位置编码设计是这样的:在 attention 分数公式里加一项:
其中 是一个对应「相对距离 」的可学习向量。这相当于在 attention 分数里加一个「位置先验」:根据 与 的相对距离调整它们的相关度。
T5 在此基础上做了简化,提出了 T5 bias:不再注入向量 ,而是直接在 attention 分数上加一个标量偏置:
是一个标量,对应每对相对距离的偏置。具体地,T5 把所有相对距离分桶(例如距离 0 一桶、1-2 一桶、3-4 一桶、5-7 一桶……),每个桶对应一个学习参数,外推到训练时未见的距离可以直接落到「最远的桶」里。
T5 bias 在外推性上比 Sinusoidal 好得多(论文里他们能从 512 训练长度外推到 1024 仍正常工作),但仍然存在两个限制:
- 每层都要查表——每一层 attention 都要算一次相对位置偏置,这是额外的开销。
- 分桶丢失精度——桶之间相对距离的差别被抹平了。
相对位置编码这条路启发了后来的 RoPE 和 ALiBi——这两者都是「直接在 attention 里编码相对位置」的思路,但用了更优雅的数学形式。
4.5 第四版方案:RoPE(旋转位置编码)
RoPE(Rotary Position Embedding,旋转位置编码)由苏剑林(Su et al., 2021)在论文 RoFormer 里提出,今天几乎所有开源大模型都用它——Llama 1/2/3、Mistral、DeepSeek、Qwen、Yi、Baichuan、ChatGLM……
RoPE 的设计思路高度凝练,可以浓缩成一句话:把 query 和 key 在 维空间里按位置 「旋转」一个角度,这样它们的内积就自动包含相对位置信息。
我们一点点拆开。
二维情形的旋转
先看最简单的情况:,每个 query 和 key 都是二维向量。
定义旋转矩阵 :
这是平面上逆时针旋转 角度的标准矩阵。
RoPE 的做法:位置 的 query 旋转 角,位置 的 key 旋转 角。即:
它们的内积变成:
这里用了一个旋转矩阵的性质:。
这个公式的含义至关重要:旋转后的 query 和 key 的内积只依赖相对位置 ,不依赖绝对位置 或 。这正是相对位置编码的目标——而 RoPE 没有用任何额外的偏置项或者查表,只是对 query 和 key 做了一个旋转。
高维推广
把 维空间分成 个二维子空间(每对维度成一对),每对独立旋转,旋转角度由位置和频率共同决定:
可以看到这个频率分布和 Sinusoidal 一样——从 到 。位置 的旋转角度是 。整个旋转矩阵是块对角的:
每个 是 2×2 的旋转矩阵。
实现时不会真的构造这个 矩阵,而是用一个 的逐元素操作:
def rope_apply(x, pos):
# x: (T, d_k), pos: (T,)
half = d_k // 2
# 频率
inv_freq = 1.0 / (10000 ** (torch.arange(0, d_k, 2) / d_k)) # (d_k/2,)
# 角度: pos * inv_freq -> (T, d_k/2)
freqs = pos.unsqueeze(-1) * inv_freq.unsqueeze(0)
cos = freqs.cos().repeat_interleave(2, dim=-1) # (T, d_k)
sin = freqs.sin().repeat_interleave(2, dim=-1) # (T, d_k)
# 把 x 偶数和奇数维交错配对,旋转
x1, x2 = x[..., 0::2], x[..., 1::2]
rotated = torch.stack([-x2, x1], dim=-1).flatten(-2)
return x * cos + rotated * sin
20 行代码就把 RoPE 实现完了。它不引入任何新参数——相比 Learned 位置嵌入要存一张 (max_seq_len, d_model) 的大表,RoPE 的开销几乎为 0。
RoPE 的关键性质
性质一:相对位置自然涌现。 只依赖 。这等价于在 attention 里隐式地编码了「相对距离」,不需要任何额外的偏置项或查表。
性质二:长距离衰减。可以推出, 随 增大而衰减——衰减规律比 Sinusoidal 更平滑、更可预测。这是 RoPE 在长距离场景下表现稳定的关键。
下面这张图把两种位置编码下「q·k 内积随相对距离的变化」画在一起(每条曲线对 80 个随机的 q、k 取平均):

可以看到 Sinusoidal 的内积剧烈振荡——长距离下偶尔出现的高相关纯粹是位置嵌入的噪声,模型很难分辨「真相关」和「巧合振荡」;RoPE 的内积曲线贴在 0 附近平稳收敛——内积本身只携带相对位置信息(绝对位置在旋转下被消掉),衰减又平滑可预测。这两条曲线的对比,就是 RoPE 在长上下文模型上能稳定外推到 32K / 128K 的工程根基。
性质三:与 Multi-Head 兼容。每个头独立做 RoPE 旋转。不同头有不同的 ,旋转后的 query/key 各自携带相对位置信息,互不干扰。
性质四:可以外推(在一定范围内)。RoPE 训练时见过 ,但因为只是把 和 按位置旋转,理论上 的位置也可以「带入公式」。但实际外推效果有限——后面 4.7 节会讲为什么以及怎么解决。
flowchart LR Q["q_m"] --> ROT_Q["旋转 m 角度<br/>q_m' = R(m) q_m"] K["k_n"] --> ROT_K["旋转 n 角度<br/>k_n' = R(n) k_n"] ROT_Q --> DOT[内积 q_m' · k_n'] ROT_K --> DOT DOT --> RES["= q_m^T R(n-m) k_n<br/>只依赖相对位置 n-m"]
RoPE 在哪里应用
RoPE 不是「把 token 嵌入加上位置编码」——它只作用于 attention 内部的 Q 和 K:
注意 V 不旋转。这一点很重要:V 携带的是「内容信息」,旋转 V 没有意义;只有 Q 和 K 在做相似度比较,相对位置信息只需要在它们之间体现。
每一层都要做一次 RoPE 旋转——因为每一层的 Q、K 都是新算的。计算开销很小(只是 的逐元素操作),但是在所有层叠加起来仍然非零,因此推理工程里 RoPE 计算通常会被 fuse 进 attention kernel(Flash Attention 2/3 就这么做,第 18 章会讲)。
4.6 第五版方案:ALiBi
ALiBi(Attention with Linear Biases,Press et al., 2021)走了一条比 RoPE 更激进的路:完全不要位置嵌入,直接在 attention 分数上加一个线性偏置:
其中 是一个固定的负斜率(不学习),不同头用不同的 ,按几何级数排列:( 是头数, 是头编号)。
直觉非常简单:距离越远,attention 分数被压制得越多。这是一种内置的「局部偏好」——模型默认认为相邻 token 比远距离 token 更相关,离得越远负偏置越大。
ALiBi 的优势:
- 零参数——和 RoPE 一样不引入额外参数。
- 强外推性——线性偏置随距离单调减小,在训练时未见的更远距离上也能保持合理的 attention 形状。Press 等人的实验显示 ALiBi 训练在 1024、外推到 16384 时困惑度基本不上升。
- 实现极简——一行 mask 加法。
ALiBi 的劣势:
- 过强的局部先验——线性减小的偏置让 attention 偏向局部,对那些真的需要长距离依赖的语言现象(比如长篇文章的指代)可能不友好。
- 不是真正的「相对位置」编码——它只编码了距离的大小,不编码方向(attention 矩阵会被加上 ,不区分 还是 )。在带因果掩码的 Decoder 里没问题(只能看左边),但在 Encoder 里会损失方向信息。
ALiBi 主要应用在 MosaicML MPT、BLOOM 等模型上,但在 Llama 系列没采用——Meta 的工程师在 Llama 1 报告里测试过 ALiBi 和 RoPE 在 7B 规模下的对比,结果表明 RoPE 在长上下文外推上更稳定、综合表现更好,于是 Llama 系列一路坚持 RoPE。
4.7 长上下文外推:Position Interpolation 与 NTK
到 2023 年,「上下文长度」成为大模型竞赛的核心战场。GPT-4 把上下文从 8K 扩到 32K,再扩到 128K;Claude 一路推到 200K;Gemini 推到 1M。这背后位置编码扮演了关键角色——你想从训练时的 4K 直接扩到 128K,RoPE 也撑不住——朴素地把 喂进去,模型完全失效。
为什么?因为高频率的 (如 时 )对应的旋转周期非常短, 太大时旋转角度已经绕了几千圈,远远超出训练时见过的角度范围。模型完全没有泛化到这个外推区域的能力。
工业界提出了几种处理这个问题的方法。
Position Interpolation (PI)
Chen et al.(Meta,2023)提出的方法:把超出训练长度的 线性压缩回训练长度。
具体地,如果训练时上下文长度是 、要外推到 ,那么把 替换成 。这样位置 在 RoPE 公式里被处理成 ——回到了训练见过的范围。
PI 简单粗暴但出奇有效:在 Llama-1-7B 上从 2048 外推到 32768 的实验里,只需要在长样本上微调几百步,模型就能在 32K 上下文里正常工作。
代价是:短距离精度被压扁。 现在被处理成 ,相当于把高频段的位置区分能力弱化了。
NTK-Aware Scaling
NTK(Neural Tangent Kernel)启发的方法:只对低频段插值,高频段保持不动。
直觉:高频段编码的是局部(短距离)信息,模型在短距离上已经有训练数据;低频段编码的是远距离信息,模型才需要外推支持。所以只把「低频段的位置」拉伸开,高频段不动——这样既扩展了感受野,又保住了局部精度。
实现上是把 RoPE 的 base(10000)改成一个更大的数 ,使得高频段的相位变化几乎不变,低频段的相位变化被压缩。
YaRN
Peng et al.(2023)提出的 YaRN(Yet another RoPE extensioN method)综合了 PI 和 NTK 的思想,按频率分段处理:
- 极高频段:完全不缩放(保持局部精度)
- 中频段:NTK 插值(兼顾)
- 极低频段:PI 缩放(保证外推)
YaRN 在 Llama-2-7B 上能从 4K 外推到 128K,质量损失非常小。这是当前大多数开源长上下文模型的标准做法。
Llama 3 的 base 调整
更直接的工程做法:训练时就把 RoPE 的 base 调大。Llama 1/2 的 RoPE base 是 10000;Llama 3 直接改成 500000——这相当于把所有频率乘以 ,让最低频段的周期变得超长,自然就支持更长的上下文。
flowchart TB A[原始 RoPE base=10000<br/>训练时见过 0-2048] --> B[直接外推到 8192<br/>失效] A --> PI[Position Interpolation<br/>把 pos 压缩回训练范围] A --> NTK[NTK-Aware<br/>只拉伸低频段] A --> YARN[YaRN<br/>分频段处理] A --> RETRAIN[改大 base 重训<br/>Llama-3 用 500000] PI --> WORK[8K-32K 外推] NTK --> WORK YARN --> WORK2[长达 128K] RETRAIN --> WORK3[原生支持长上下文]
第 13 章「长上下文之战」会从更宏观的工程视角讲所有这些方法的取舍。位置编码这一章你只需要理解:RoPE 不是「学完就能任意外推」的银弹,需要配合特定的工程改造才能支撑超长上下文。
4.8 主流模型的位置编码对照表
把主流模型的选择放一起看:
| 模型 | 位置编码 | 训练长度 | 备注 |
|---|---|---|---|
| 原始 Transformer | Sinusoidal | 短 | 已淘汰 |
| BERT | Learned | 512 | 不能外推 |
| GPT-1/2/3 | Learned | 1024 / 2048 | 外推差 |
| T5 | Relative bias | 512 | 分桶外推 |
| Llama 1 | RoPE (base=10000) | 2048 | 主流起点 |
| Llama 2 | RoPE (base=10000) + 微调 | 4096 | PI 微调到 32K |
| Llama 3 | RoPE (base=500000) | 8192 | 原生支持长上下文 |
| Llama 3.1 | RoPE + YaRN-style 改进 | 128K | 长上下文标杆 |
| Mistral | RoPE | 8192 | + 滑动窗口 |
| MPT / BLOOM | ALiBi | 2048+ | 外推友好 |
| Falcon | RoPE | 2048 | |
| Qwen / DeepSeek / Yi | RoPE (各自调 base) | 4K-128K | RoPE + 工程化外推 |
可以看到 2023 年之后开源大模型几乎一边倒选 RoPE,少数选 ALiBi,几乎没有再用 Sinusoidal 或 Learned 的。这条收敛是工程上「跑赢的设计」自然胜出的结果。
4.9 一些容易混淆的细节
第 4 章这个话题里有几个细节经常被搞错,列在这里:
细节一:RoPE 旋转的是 Q 和 K,不是 V。V 携带的是内容,旋转无意义。
细节二:位置编码是每一层都要做(RoPE / ALiBi),还是只在输入层做(Sinusoidal / Learned)?
- Sinusoidal / Learned 是「输入端注入」——只在输入嵌入里加一次。
- RoPE / ALiBi 是「每层都做」——每一层的 Q、K 都要重新旋转或加偏置。
细节三:因果掩码和位置编码是两件事。因果掩码是「不能看未来」(Decoder 自回归约束),位置编码是「让模型知道每个 token 的位置」(信息注入)。两者独立。Encoder(无因果掩码)也需要位置编码;带因果掩码的 Decoder-only 模型也需要位置编码。
细节四:ALiBi 和因果掩码可以叠加。ALiBi 加 偏置,因果掩码把 的位置设成 ;两者在同一个分数矩阵里相加,互不冲突。
细节五:embedding 共享与位置编码无关。「输入嵌入和输出投影是否共享权重」(常见做法是共享,称 weight tying)是另一个独立的设计选项,不和位置编码绑定。
4.10 选位置编码的工程经验
如果你在为一个新模型设计位置编码,下面是一些经验法则:
-
没特别理由就用 RoPE。开源生态、工具链、长上下文外推方案都已经围绕 RoPE 收敛——选它能最大化复用现成的优化(Flash Attention、vLLM 的 RoPE 实现都是默认假设 RoPE)。
-
训练时把 base 设大一点。原始 RoPE 的 base=10000 适合短上下文;如果你计划支持 32K 以上,建议训练时就把 base 设到 500000 甚至更大,避免后续做插值。
-
ALiBi 适合外推压力大的场景。如果你的训练资源有限、需要在小训练长度上做大外推(比如训练长度 1K,希望推理 16K),ALiBi 比 RoPE 简单很多。
-
Encoder-only 模型仍可以用 Learned。BERT 类模型的下游任务大多在固定长度(≤ 512)上做,不需要外推。Learned 的灵活性反而是优势。
-
绝对不要混用。同一个模型里只用一种位置编码——混用会破坏模型的内部一致性,几乎一定会让训练崩盘。
本章小结
- Self-Attention 不感知位置——这是 Transformer 的固有性质,必须从外部注入位置信息。
- Sinusoidal 是原始设计——多频率的 sin/cos 拼成位置向量,加到 token 嵌入上。优雅但外推不稳定。
- Learned 是 BERT/GPT 时代的实用方案——直接学位置嵌入,灵活但完全不能外推。
- 相对位置编码(T5 bias)解决了「方向不变性」——模型关心的是相对距离而非绝对位置。
- RoPE 是 2023 之后的统治者——通过对 Q、K 旋转 的方式,让内积自动包含相对位置信息。零参数,每层做,与 Multi-Head 兼容。
- ALiBi 是另一种零参数方案——直接在 attention 分数上加线性偏置,外推性极强但局部偏好过强。
- 长上下文外推需要专门处理:Position Interpolation、NTK-Aware、YaRN,或者训练时直接把 RoPE base 调大。
- 主流大模型几乎全部用 RoPE——这条工程收敛在 2023 之后已经基本完成。
我们到这里把 Self-Attention 真正讲完了——第 2 章是「裸内核」、第 3 章是「多头扩展」、第 4 章是「位置注入」。但 Transformer 不只是 Attention:它还有 FFN、LayerNorm、Residual——这些「配套零件」每一个都不可省。下一章我们解剖 Transformer Block 的完整结构,看清这些零件互相之间的咬合关系。
延伸阅读
- Vaswani et al., Attention Is All You Need, NeurIPS 2017,3.5 节是 Sinusoidal 位置编码。
- Devlin et al., BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding, NAACL 2019——Learned 位置嵌入的代表。
- Shaw et al., Self-Attention with Relative Position Representations, NAACL 2018——相对位置编码奠基。
- Raffel et al., Exploring the Limits of Transfer Learning with a Unified Text-to-Text Transformer, JMLR 2020——T5 论文,相对位置 bias 的简化设计。
- Su et al., RoFormer: Enhanced Transformer with Rotary Position Embedding, 2021. arXiv:2104.09864——RoPE 原始论文。中文读者可参考苏剑林的博客《让研究人员绞尽脑汁的 Transformer 位置编码》。
- Press et al., Train Short, Test Long: Attention with Linear Biases Enables Input Length Extrapolation, ICLR 2022——ALiBi 论文。
- Chen et al., Extending Context Window of Large Language Models via Positional Interpolation, 2023. arXiv:2306.15595——Position Interpolation。
- Peng et al., YaRN: Efficient Context Window Extension of Large Language Models, 2023. arXiv:2309.00071——YaRN。