Transformer 解剖:从 Attention 到推理系统

第 4 章 位置编码:从 Sinusoidal 到 RoPE

作者 杨艺韬 · 6,380 字

第 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 嵌入序列 X=[x1,x2,,xT]RT×dmodelX = [x_1, x_2, \dots, x_T] \in \mathbb{R}^{T \times d_{\text{model}}}。它的输出是 Attention(XWQ,XWK,XWV)\text{Attention}(XW_Q, XW_K, XW_V)。整个计算用的是矩阵乘法和 softmax,没有任何运算依赖于 token 在序列中的位置索引 ii

形式化一下。设 PP 是任意一个置换矩阵(把行的顺序打乱),那么:

Attention(PXWQ,PXWK,PXWV)=PAttention(XWQ,XWK,XWV)\text{Attention}(PX W_Q, PX W_K, PX W_V) = P \cdot \text{Attention}(XW_Q, XW_K, XW_V)

这意味着:输入的位置 ii 调到位置 jj,输出的位置 ii 也跟着调到位置 jj,但内容(每个位置上的向量)一字不差。如果模型的下游任务(比如语言建模)只关心「这个位置应该是什么 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——给每个位置 pospos 算出一个 dmodeld_{\text{model}} 维向量,作为「位置向量」加到 token 嵌入上:

PE(pos,2i)=sin(pos100002i/dmodel)\text{PE}(pos, 2i) = \sin\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right) PE(pos,2i+1)=cos(pos100002i/dmodel)\text{PE}(pos, 2i+1) = \cos\left(\frac{pos}{10000^{2i/d_{\text{model}}}}\right)

其中 pospos 是位置(0,1,2,0, 1, 2, \dots),2i2i2i+12i+1 是向量的偶数维和奇数维(i=0,1,,dmodel/21i = 0, 1, \dots, d_{\text{model}}/2 - 1)。

公式直接看会觉得「这哪来的」。我们拆开看一下它的设计:

第一,每个位置 pospos 的位置编码 PEpos\text{PE}_{pos} 是一个 dmodeld_{\text{model}} 维向量,由若干对 (sin,cos)(\sin, \cos) 拼成。每一对来自不同的频率。

第二,频率从 11(最高频)一路降到 1/100001/10000(最低频)。具体地,第 ii 对的频率是 1/100002i/dmodel1/10000^{2i/d_{\text{model}}}

第三,最终的输入是「token 嵌入 + 位置编码」:

xi=xi+PEix_i' = x_i + \text{PE}_i

这个加法是逐元素加(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 论文给了一个数学论证:对任意固定的偏移 kkPEpos+k\text{PE}_{pos+k} 都可以表示为 PEpos\text{PE}_{pos} 的线性变换

具体推导:对一对 (sin,cos)(\sin, \cos),我们有:

sin(a+b)=sinacosb+cosasinb\sin(a + b) = \sin a \cos b + \cos a \sin b cos(a+b)=cosacosbsinasinb\cos(a + b) = \cos a \cos b - \sin a \sin b

也就是说:

(sin(ω(pos+k))cos(ω(pos+k)))=(cos(ωk)sin(ωk)sin(ωk)cos(ωk))(sin(ωpos)cos(ωpos))\begin{pmatrix} \sin(\omega(pos+k)) \\ \cos(\omega(pos+k)) \end{pmatrix} = \begin{pmatrix} \cos(\omega k) & \sin(\omega k) \\ -\sin(\omega k) & \cos(\omega k) \end{pmatrix} \begin{pmatrix} \sin(\omega \cdot pos) \\ \cos(\omega \cdot pos) \end{pmatrix}

这个右边的旋转矩阵只依赖偏移 kk,不依赖 pospos。这意味着:模型可以通过一次线性变换,从位置 pospos 的编码推出位置 pos+kpos+k 的编码——也就是说,「相对位置」信息被天然编码进去了。

而每个频率的周期不同:高频段(ω\omega 大)周期短,能区分相邻位置;低频段(ω\omega 小)周期长,能编码远距离。整个 dmodeld_{\text{model}} 维向量像是一个「多频率的位置时钟」,从最快的针走到最慢的针。

直觉上这设计很美,但在实战中 Sinusoidal 有几个问题:

  1. 加性叠加在低维分量上对 token 嵌入有干扰。位置编码和 token 嵌入直接相加,意味着同一个维度里既有 token 信息又有位置信息——它们必须共享有限的表示带宽。
  2. 对长距离的衰减不够好。点积注意力下,xi+PEix_i + \text{PE}_ixj+PEjx_j + \text{PE}_j 的内积包含 PEiPEj\text{PE}_i \cdot \text{PE}_j 这一项。从 sinusoidal 的性质可以推出,这个项随 ij|i-j| 增大确实会衰减,但衰减得不够快也不够规则——大约是按 cos\cos 振荡的方式衰减,长距离下偶尔会出现意外的高相关性。
  3. 外推不稳定。理论上 sinusoidal 可以外推到任意长度(公式里 pospos 没有上限),但实际训练时模型只见过 posTtrainpos \le T_{\text{train}} 的位置,从未见过更大的位置,于是在测试时用更长的序列时性能会显著下降。

4.3 第二版方案:Learned 位置嵌入

BERT 时代主流换成了 Learned Positional Embedding

self.pos_emb = nn.Embedding(max_seq_len, d_model)

每个位置 0,1,,Tmax10, 1, \dots, T_{\text{max}}-1 都对应一个可学习dmodeld_{\text{model}} 维向量,作为参数被训练。和 Sinusoidal 一样,最终是相加:

xi=xi+PosEmb[i]x_i' = x_i + \text{PosEmb}[i]

优点很直接:模型可以自由学到任何对当前数据最适合的位置表示——不再受限于人工设计的 sin/cos\sin/\cos 函数。

代价也很直接:没法外推。如果你训练时用 Tmax=512T_{\text{max}} = 512,那 pos_emb 这张表就只有 512 行,第 513 个位置根本没有对应的嵌入——模型直接坏掉。

BERT、GPT-1、GPT-2、ViT 都用了 Learned 位置嵌入。这个时代的模型上下文长度都被卡在 512 / 1024 / 2048——一是模型设计如此,二是即便你想把 1024 训成的模型推理时跑 4096,没有外推性质,模型也会失效。

4.4 第三版方案:相对位置编码

Sinusoidal 和 Learned 都属于绝对位置编码——给每个位置 ii 算一个嵌入,加到 token 上。但语言的本质规律往往是「相对的」——主谓关系是「主语在前」,介词短语是「依附于前面的名词」,疑问代词倾向于句首——这些规律关心的是两个 token 之间的距离和方向,而不是它们的绝对位置。

相对位置编码(Relative Position Encoding)的思路是:直接把『iijj 之间的相对距离 iji-j』作为一个特征注入到 attention 计算中,而不是给每个绝对位置打标签。

Shaw et al.(2018)的原始相对位置编码设计是这样的:在 attention 分数公式里加一项:

eij=(xiWQ)(xjWK)T+(xiWQ)aijKdke_{ij} = \frac{(x_i W_Q)(x_j W_K)^T + (x_i W_Q) \cdot a^K_{i-j}}{\sqrt{d_k}}

其中 aijKa^K_{i-j} 是一个对应「相对距离 iji-j」的可学习向量。这相当于在 attention 分数里加一个「位置先验」:根据 iijj 的相对距离调整它们的相关度。

T5 在此基础上做了简化,提出了 T5 bias:不再注入向量 aKa^K,而是直接在 attention 分数上加一个标量偏置:

eij=(xiWQ)(xjWK)Tdk+bije_{ij} = \frac{(x_i W_Q)(x_j W_K)^T}{\sqrt{d_k}} + b_{i-j}

bijb_{i-j} 是一个标量,对应每对相对距离的偏置。具体地,T5 把所有相对距离分桶(例如距离 0 一桶、1-2 一桶、3-4 一桶、5-7 一桶……),每个桶对应一个学习参数,外推到训练时未见的距离可以直接落到「最远的桶」里。

T5 bias 在外推性上比 Sinusoidal 好得多(论文里他们能从 512 训练长度外推到 1024 仍正常工作),但仍然存在两个限制:

  1. 每层都要查表——每一层 attention 都要算一次相对位置偏置,这是额外的开销。
  2. 分桶丢失精度——桶之间相对距离的差别被抹平了。

相对位置编码这条路启发了后来的 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 在 dkd_k 维空间里按位置 pospos 「旋转」一个角度,这样它们的内积就自动包含相对位置信息

我们一点点拆开。

二维情形的旋转

先看最简单的情况:dk=2d_k = 2,每个 query 和 key 都是二维向量。

定义旋转矩阵 R(θ)R(\theta)

R(θ)=(cosθsinθsinθcosθ)R(\theta) = \begin{pmatrix} \cos\theta & -\sin\theta \\ \sin\theta & \cos\theta \end{pmatrix}

这是平面上逆时针旋转 θ\theta 角度的标准矩阵。

RoPE 的做法:位置 mm 的 query 旋转 mθm\theta 角,位置 nn 的 key 旋转 nθn\theta。即:

qm=R(mθ)qm,kn=R(nθ)knq_m' = R(m\theta) q_m, \quad k_n' = R(n\theta) k_n

它们的内积变成:

qmTkn=qmTR(mθ)TR(nθ)kn=qmTR((nm)θ)knq_m'^T k_n' = q_m^T R(m\theta)^T R(n\theta) k_n = q_m^T R((n-m)\theta) k_n

这里用了一个旋转矩阵的性质:R(α)TR(β)=R(βα)R(\alpha)^T R(\beta) = R(\beta - \alpha)

这个公式的含义至关重要:旋转后的 query 和 key 的内积只依赖相对位置 nmn-m,不依赖绝对位置 mmnn。这正是相对位置编码的目标——而 RoPE 没有用任何额外的偏置项或者查表,只是对 query 和 key 做了一个旋转。

高维推广

dkd_k 维空间分成 dk/2d_k/2 个二维子空间(每对维度成一对),每对独立旋转,旋转角度由位置和频率共同决定:

θi=1100002i/dk,i=0,1,,dk/21\theta_i = \frac{1}{10000^{2i/d_k}}, \quad i = 0, 1, \dots, d_k/2 - 1

可以看到这个频率分布和 Sinusoidal 一样——从 111/100001/10000。位置 mm 的旋转角度是 (mθ0,mθ1,,mθdk/21)(m\theta_0, m\theta_1, \dots, m\theta_{d_k/2-1})。整个旋转矩阵是块对角的:

R(m)=(R(mθ0)R(mθ1)R(mθdk/21))R(m) = \begin{pmatrix} R(m\theta_0) & & \\ & R(m\theta_1) & \\ & & \ddots \\ & & & R(m\theta_{d_k/2-1}) \end{pmatrix}

每个 R(mθi)R(m\theta_i) 是 2×2 的旋转矩阵。

实现时不会真的构造这个 dk×dkd_k \times d_k 矩阵,而是用一个 O(dk)O(d_k) 的逐元素操作:

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 的关键性质

性质一:相对位置自然涌现qmknq_m' \cdot k_n' 只依赖 nmn-m。这等价于在 attention 里隐式地编码了「相对距离」,不需要任何额外的偏置项或查表。

性质二:长距离衰减。可以推出,qmkn|q_m' \cdot k_n'|nm|n-m| 增大而衰减——衰减规律比 Sinusoidal 更平滑、更可预测。这是 RoPE 在长距离场景下表现稳定的关键。

下面这张图把两种位置编码下「q·k 内积随相对距离的变化」画在一起(每条曲线对 80 个随机的 q、k 取平均):

位置编码下 q·k 内积随距离的衰减对比

可以看到 Sinusoidal 的内积剧烈振荡——长距离下偶尔出现的高相关纯粹是位置嵌入的噪声,模型很难分辨「真相关」和「巧合振荡」;RoPE 的内积曲线贴在 0 附近平稳收敛——内积本身只携带相对位置信息(绝对位置在旋转下被消掉),衰减又平滑可预测。这两条曲线的对比,就是 RoPE 在长上下文模型上能稳定外推到 32K / 128K 的工程根基。

性质三:与 Multi-Head 兼容。每个头独立做 RoPE 旋转。不同头有不同的 WQ,WKW_Q, W_K,旋转后的 query/key 各自携带相对位置信息,互不干扰。

性质四:可以外推(在一定范围内)。RoPE 训练时见过 pos[0,Ttrain)pos \in [0, T_{\text{train}}),但因为只是把 qqkk 按位置旋转,理论上 pos>Ttrainpos > T_{\text{train}} 的位置也可以「带入公式」。但实际外推效果有限——后面 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

Attention(RoPE(Q,pos),RoPE(K,pos),V)\text{Attention}(\text{RoPE}(Q, pos), \text{RoPE}(K, pos), V)

注意 V 不旋转。这一点很重要:V 携带的是「内容信息」,旋转 V 没有意义;只有 Q 和 K 在做相似度比较,相对位置信息只需要在它们之间体现。

每一层都要做一次 RoPE 旋转——因为每一层的 Q、K 都是新算的。计算开销很小(只是 O(Tdk)O(T \cdot d_k) 的逐元素操作),但是在所有层叠加起来仍然非零,因此推理工程里 RoPE 计算通常会被 fuse 进 attention kernel(Flash Attention 2/3 就这么做,第 18 章会讲)。

4.6 第五版方案:ALiBi

ALiBi(Attention with Linear Biases,Press et al., 2021)走了一条比 RoPE 更激进的路:完全不要位置嵌入,直接在 attention 分数上加一个线性偏置

eij=qikjdkmije_{ij} = \frac{q_i \cdot k_j}{\sqrt{d_k}} - m \cdot |i - j|

其中 mm 是一个固定的负斜率(不学习),不同头用不同的 mm,按几何级数排列:mh=28h/Hm_h = 2^{-8h/H}HH 是头数,hh 是头编号)。

直觉非常简单:距离越远,attention 分数被压制得越多。这是一种内置的「局部偏好」——模型默认认为相邻 token 比远距离 token 更相关,离得越远负偏置越大。

ALiBi 的优势:

  1. 零参数——和 RoPE 一样不引入额外参数。
  2. 强外推性——线性偏置随距离单调减小,在训练时未见的更远距离上也能保持合理的 attention 形状。Press 等人的实验显示 ALiBi 训练在 1024、外推到 16384 时困惑度基本不上升。
  3. 实现极简——一行 mask 加法。

ALiBi 的劣势:

  1. 过强的局部先验——线性减小的偏置让 attention 偏向局部,对那些真的需要长距离依赖的语言现象(比如长篇文章的指代)可能不友好。
  2. 不是真正的「相对位置」编码——它只编码了距离的大小,不编码方向(attention 矩阵会被加上 mij-m \cdot |i-j|,不区分 i>ji > j 还是 i<ji < j)。在带因果掩码的 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 也撑不住——朴素地把 pos=100000pos = 100000 喂进去,模型完全失效。

为什么?因为高频率的 θi\theta_i(如 i=0i = 0θ0=1\theta_0 = 1)对应的旋转周期非常短,pospos 太大时旋转角度已经绕了几千圈,远远超出训练时见过的角度范围。模型完全没有泛化到这个外推区域的能力。

工业界提出了几种处理这个问题的方法。

Position Interpolation (PI)

Chen et al.(Meta,2023)提出的方法:把超出训练长度的 pospos 线性压缩回训练长度

具体地,如果训练时上下文长度是 LtrainL_{\text{train}}、要外推到 LevalL_{\text{eval}},那么把 pospos 替换成 posLtrain/Levalpos \cdot L_{\text{train}} / L_{\text{eval}}。这样位置 pos=Levalpos = L_{\text{eval}} 在 RoPE 公式里被处理成 pos=Ltrainpos = L_{\text{train}}——回到了训练见过的范围。

PI 简单粗暴但出奇有效:在 Llama-1-7B 上从 2048 外推到 32768 的实验里,只需要在长样本上微调几百步,模型就能在 32K 上下文里正常工作。

代价是:短距离精度被压扁pos=1pos = 1 现在被处理成 pos=1/16pos = 1/16,相当于把高频段的位置区分能力弱化了。

NTK-Aware Scaling

NTK(Neural Tangent Kernel)启发的方法:只对低频段插值,高频段保持不动

直觉:高频段编码的是局部(短距离)信息,模型在短距离上已经有训练数据;低频段编码的是远距离信息,模型才需要外推支持。所以只把「低频段的位置」拉伸开,高频段不动——这样既扩展了感受野,又保住了局部精度。

实现上是把 RoPE 的 base(10000)改成一个更大的数 α\alpha,使得高频段的相位变化几乎不变,低频段的相位变化被压缩。

YaRN

Peng et al.(2023)提出的 YaRN(Yet another RoPE extensioN method)综合了 PI 和 NTK 的思想,按频率分段处理:

YaRN 在 Llama-2-7B 上能从 4K 外推到 128K,质量损失非常小。这是当前大多数开源长上下文模型的标准做法。

Llama 3 的 base 调整

更直接的工程做法:训练时就把 RoPE 的 base 调大。Llama 1/2 的 RoPE base 是 10000;Llama 3 直接改成 500000——这相当于把所有频率乘以 10000/500000=0.0210000/500000 = 0.02,让最低频段的周期变得超长,自然就支持更长的上下文。

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)?

细节三:因果掩码和位置编码是两件事。因果掩码是「不能看未来」(Decoder 自回归约束),位置编码是「让模型知道每个 token 的位置」(信息注入)。两者独立。Encoder(无因果掩码)也需要位置编码;带因果掩码的 Decoder-only 模型也需要位置编码。

细节四:ALiBi 和因果掩码可以叠加。ALiBi 加 mij-m|i-j| 偏置,因果掩码把 i<ji < j 的位置设成 -\infty;两者在同一个分数矩阵里相加,互不冲突。

细节五:embedding 共享与位置编码无关。「输入嵌入和输出投影是否共享权重」(常见做法是共享,称 weight tying)是另一个独立的设计选项,不和位置编码绑定。

4.10 选位置编码的工程经验

如果你在为一个新模型设计位置编码,下面是一些经验法则:

  1. 没特别理由就用 RoPE。开源生态、工具链、长上下文外推方案都已经围绕 RoPE 收敛——选它能最大化复用现成的优化(Flash Attention、vLLM 的 RoPE 实现都是默认假设 RoPE)。

  2. 训练时把 base 设大一点。原始 RoPE 的 base=10000 适合短上下文;如果你计划支持 32K 以上,建议训练时就把 base 设到 500000 甚至更大,避免后续做插值。

  3. ALiBi 适合外推压力大的场景。如果你的训练资源有限、需要在小训练长度上做大外推(比如训练长度 1K,希望推理 16K),ALiBi 比 RoPE 简单很多。

  4. Encoder-only 模型仍可以用 Learned。BERT 类模型的下游任务大多在固定长度(≤ 512)上做,不需要外推。Learned 的灵活性反而是优势。

  5. 绝对不要混用。同一个模型里只用一种位置编码——混用会破坏模型的内部一致性,几乎一定会让训练崩盘。

本章小结

  1. Self-Attention 不感知位置——这是 Transformer 的固有性质,必须从外部注入位置信息。
  2. Sinusoidal 是原始设计——多频率的 sin/cos 拼成位置向量,加到 token 嵌入上。优雅但外推不稳定。
  3. Learned 是 BERT/GPT 时代的实用方案——直接学位置嵌入,灵活但完全不能外推。
  4. 相对位置编码(T5 bias)解决了「方向不变性」——模型关心的是相对距离而非绝对位置。
  5. RoPE 是 2023 之后的统治者——通过对 Q、K 旋转 mθm\theta 的方式,让内积自动包含相对位置信息。零参数,每层做,与 Multi-Head 兼容。
  6. ALiBi 是另一种零参数方案——直接在 attention 分数上加线性偏置,外推性极强但局部偏好过强。
  7. 长上下文外推需要专门处理:Position Interpolation、NTK-Aware、YaRN,或者训练时直接把 RoPE base 调大。
  8. 主流大模型几乎全部用 RoPE——这条工程收敛在 2023 之后已经基本完成。

我们到这里把 Self-Attention 真正讲完了——第 2 章是「裸内核」、第 3 章是「多头扩展」、第 4 章是「位置注入」。但 Transformer 不只是 Attention:它还有 FFN、LayerNorm、Residual——这些「配套零件」每一个都不可省。下一章我们解剖 Transformer Block 的完整结构,看清这些零件互相之间的咬合关系。

延伸阅读