观文听傑

返回

上一篇把自回归推理改造成 KV Cache:历史 Key/Value 可以复用,新 token 只需用自己的 Query 读取缓存。但那里留下了一个不能靠缓存解决的问题:新 token 明明位于全局位置 P+tP+t,若每一步都把它当成位置 0,增量前向就不再等价于完整前向。

原因更基础:自注意力只比较内容,本身没有“第几个 token”的坐标。位置表示(Positional Representation)要补上的不是一个装饰性编号,而是让模型能够区分顺序、方向与距离。本文比较三种紧密相关的方法,并重点推导旋转位置编码(Rotary Position Embedding, RoPE)为何能把相对位移直接写进 Query–Key 点积。

01 没有位置时,注意力究竟看不见什么?#

设输入 XRN×L×DX\in\mathbb{R}^{N\times L\times D},忽略 mask 时:

Q=XWQ,K=XWK,V=XWVQ=XW_Q,\qquad K=XW_K,\qquad V=XW_V Attention(X)=softmax(QKd)V\operatorname{Attention}(X)=\operatorname{softmax}\left(\frac{QK^\top}{\sqrt{d}}\right)V

若用同一个置换矩阵 PRL×LP\in\mathbb{R}^{L\times L} 重排 token 行,则 Q,K,VQ,K,V 都被同样重排,结果满足:

Attention(PX)=PAttention(X)\operatorname{Attention}(PX)=P\operatorname{Attention}(X)

这叫置换等变(Permutation Equivariance):输入行换顺序,输出行只会跟着换顺序。模型能看到有哪些 token,却没有天然坐标说明谁先谁后。

输入 A: [狗, 咬, 人] ─► 只按内容投影 Q/K/V ─► 输出 [o狗, o咬, o人]
输入 B: [人, 咬, 狗] ─► 以同样方式重排行 ─► 输出 [o人, o咬, o狗]

没有位置表示时,模型没有额外信号区分:
“狗在咬之前” 与 “狗在咬之后”
text

因果 mask 能阻止位置 ii 读取未来 j>ij>i,但它只规定“能连哪条边”,不告诉模型两个可见 token 相隔 1 步还是 100 步。位置表示与 mask 解决的是不同问题。

02 位置信息可以注入哪一处?#

三种常见思路的关键差异,不是名字,而是位置进入数据流的位置:

绝对位置:
token embedding [N,L,D] + position [L,D]


             hidden [N,L,D] ─► Q/K/V projection ─► attention

RoPE:
hidden [N,L,D] ─► Q/K projection ─► 按位置旋转 Q、K ─► QKᵀ
                         └───────► V 通常不旋转 ─────► 加权和

相对偏置(边界比较):
QKᵀ [N,H,L,S] + bias(查询位置 - 键位置) ─► softmax
text
方法位置进入哪里是否学习位置参数直接表达什么
正弦绝对位置加到输入 hidden每个位置的多频率绝对坐标
学习式绝对位置加到输入 hidden训练数据自行塑造的离散位置向量
RoPE旋转每个头的 Q 与 K绝对角度产生的相对 Q–K 几何关系

这三种方法都不自动处理 padding,也不替代 causal mask。位置只提供坐标;可见性仍由 mask 决定。

03 正弦位置编码怎样把整数变成多尺度坐标?#

原始 Transformer 对偶数维 DD 定义多组频率:

ωi=100002i/D,i=0,1,,D/21\omega_i=10000^{-2i/D},\qquad i=0,1,\ldots,D/2-1

位置 pp 的第 ii 个二维坐标对为:

PEi(p)=[sin(pωi),cos(pωi)]PE_i(p)=[\sin(p\omega_i),\cos(p\omega_i)]

高频对随位置快速旋转,擅长区分近邻;低频对变化缓慢,提供更大尺度的坐标。把所有频率拼起来得到 PE(p)RDPE(p)\in\mathbb{R}^{D},再与 token embedding 相加:

Xp=Etoken(p)+PE(p)X_p=E_{token(p)}+PE(p)
位置 p ─► [快频率 sin/cos | 中频率 sin/cos | 慢频率 sin/cos] ─► PE(p) [D]
token id ───────────────────────────────► embedding [D]
                                                     │ 相加

                                              X_p [D]
text

为什么每个频率要成对使用正弦和余弦?因为平移 Δ\Delta 对每一对坐标都是固定旋转:

[sin((p+Δ)ω)cos((p+Δ)ω)]=[cos(Δω)sin(Δω)sin(Δω)cos(Δω)][sin(pω)cos(pω)]\begin{bmatrix} \sin((p+\Delta)\omega)\\ \cos((p+\Delta)\omega) \end{bmatrix} = \begin{bmatrix} \cos(\Delta\omega)&\sin(\Delta\omega)\\ -\sin(\Delta\omega)&\cos(\Delta\omega) \end{bmatrix} \begin{bmatrix} \sin(p\omega)\\ \cos(p\omega) \end{bmatrix}

对固定 Δ\Delta,右侧矩阵与绝对位置 pp 无关。线性层因此有机会从绝对坐标中恢复相对偏移,但这种关系只是“可被利用”,并没有像 RoPE 那样直接约束 Q–K 点积。

04 用一个频率手算绝对坐标与相对距离#

取单个频率 ω=π/2\omega=\pi/2

位置 pp[sin(pω),cos(pω)][\sin(p\omega),\cos(p\omega)]
0[0, 1]
1[1, 0]
2[0, -1]
3[-1, 0]

任意两个位置向量的点积为:

PEω(m)PEω(n)=cos((mn)ω)PE_\omega(m)^\top PE_\omega(n)=\cos((m-n)\omega)

所以位置 (0,1)(2,3) 虽然绝对编号不同,但位移都为 1,点积都等于 0。位置 (0,2)(1,3) 的位移都为 2,点积都等于 1-1

真实编码用许多频率,不同频率会给同一位移不同相位。模型不是从单个数字读取“距离”,而是从多尺度相位组合中学习位置关系。

05 不依赖框架封装,生成正弦位置#

下面函数返回 [L,D],偶数列放正弦、奇数列放余弦。它故意把频率和角度留在代码中,便于检查每个张量。

正弦表不需要梯度。生产模块若预先缓存它,可用 register_buffer 注册为 buffer:它不是可训练参数,但会跟随模块迁移设备;persistent=False 还可避免把可重建的大表写进 state_dict

06 学习式绝对位置改变了什么?#

学习式绝对位置(Learned Absolute Position Embedding)直接建立参数表 PRM×DP\in\mathbb{R}^{M\times D},其中 MM 是最大位置数。第 pp 行与 token embedding 相加:

PyTorch 2.13 的 nn.Embedding 接收任意形状的整数索引 (*),输出 (*, embedding_dim);其权重形状为 [num_embeddings, embedding_dim]。这里 token 表与 position 表都是查表,但行的语义完全不同。

学习式位置能让数据自行决定哪些位置应相似,却有一个硬边界:位置 pMp\ge M 根本没有参数行。直接增大 MM 后,新行也未受训练。正弦编码可以计算未见过的整数位置,但“函数有值”不等于模型能可靠外推到任意长度。

07 RoPE 为什么旋转 Q/K,而不是加到 hidden?#

RoPE 把每个注意力头的 Query 和 Key 沿最后一维两两分组。对第 ii 对维度、位置 pp,用二维旋转矩阵:

Ri(p)=[cos(pθi)sin(pθi)sin(pθi)cos(pθi)]R_i(p)= \begin{bmatrix} \cos(p\theta_i)&-\sin(p\theta_i)\\ \sin(p\theta_i)&\cos(p\theta_i) \end{bmatrix}

其中 θi=100002i/d\theta_i=10000^{-2i/d}dd 是每头宽度。对未加位置的内容向量 qp,ksRdq_p,k_s\in\mathbb{R}^{d}

q~p=R(p)qp,k~s=R(s)ks\widetilde q_p=R(p)q_p,\qquad \widetilde k_s=R(s)k_s

注意力分数变为:

q~pk~s=qpR(p)R(s)ks=qpR(sp)ks\widetilde q_p^\top\widetilde k_s =q_p^\top R(p)^\top R(s)k_s =q_p^\top R(s-p)k_s

最后一式只通过 sps-p 依赖位置。若把 Query 与 Key 同时平移 Δ\Delta

R((s+Δ)(p+Δ))=R(sp)R((s+\Delta)-(p+\Delta))=R(s-p)

点积不变。这就是 RoPE 的核心:每个 Q/K 仍按绝对位置旋转,但二者相乘时显式出现相对位移。

content q_p [d] ─► R(p) ─► rotated q_p ─┐
                                         ├─ dot / sqrt(d) ─► score(p,s)
content k_s [d] ─► R(s) ─► rotated k_s ─┘

共同平移:p→p+Δ, s→s+Δ
相对位移:(s+Δ)-(p+Δ)=s-p ─► score 的位置部分不变
text

Value 通常不旋转,因为 RoPE 的目标是改变“Query 与哪个 Key 相似”,而不是直接把位置混进被加权取回的内容。

08 用二维向量手算一次旋转点积#

只取一对维度,令 q=k=[1,0]q=k=[1,0],角频率 θ=π/2\theta=\pi/2

查询在 p=1p=1,Key 在 s=3s=3

R(1)q=[0,1],R(3)k=[0,1]R(1)q=[0,1],\qquad R(3)k=[0,-1]

旋转后点积为 1-1。用相对形式计算:

qR(sp)k=[1,0]R(2)[1,0]=1q^\top R(s-p)k=[1,0]^\top R(2)[1,0]=-1

再把二者都向右平移 4 个位置,得到 p=5,s=7p=5,s=7。相对位移仍是 2,所以点积仍为 1-1

注意,这不意味着 RoPE 分数只由距离决定。完整表达式仍含内容向量 qp,ksq_p,k_s;RoPE 只是让位置对点积的作用具有相对结构。

09 用 PyTorch 写出透明 RoPE 与注意力数据流#

下面实现接受 Query/Key x [N,H,L,d] 和位置 position_ids [L][N,L]。角度先以 float32 计算,再把正余弦转换到输入 dtype,避免在长位置上过早用低精度累计角度误差。

PyTorch 2.13 当前的 scaled_dot_product_attention 接收 Query [N,...,H_q,L,E]、Key [N,...,H,S,E]、Value [N,...,H,S,E_v]。RoPE 不改变这些形状,只在调用 SDPA 前旋转 Q/K;SDPA 本身不会替你添加 RoPE。

教学实现每次重建频率和角度,便于观察但有额外开销。生产实现通常缓存一定长度的 cos/sin 表,在需要更长位置时扩展,并针对设备、dtype 与编译策略管理缓存生命周期。

10 RoPE 与 KV Cache 怎样保持等价?#

完整因果前向中,第 pp 个 Key 应先用 R(p)R(p) 旋转。增量解码时,缓存已有长度为 past_length,新 token 的位置必须从这里开始:

prefill 位置: 0 1 2 3 ─► 各自旋转 K ─► cache length 4
decode 新位置: 4       ─► R(4) 旋转 q4,k4 ─► append k4
decode 新位置: 5       ─► R(5) 旋转 q5,k5 ─► append k5

错误做法 A:每步新 token 都使用 R(0)
错误做法 B:cache 中历史 K 在每步又旋转一次
text

测试时应关闭 dropout,比较完整 causal forward 与“prefill + 逐 token cache”的所有位置输出。只比最终 argmax 可能掩盖小但系统性的错位。

11 左 padding、拼接与打包为何最容易错?#

位置 id 不能总由一条共享的 arange(L) 机械生成。

设两条序列左侧补齐:

token:      [PAD, PAD, 我, 爱, ML]
valid:      [  0,   0, 1,  1,  1]
语义位置:   [  -,   -, 0,  1,  2]

token:      [你, 也, 爱, ML, 吗]
valid:      [ 1, 1,  1,  1,  1]
语义位置:   [ 0, 1,  2,  3,  4]
text

若直接给两个样本都用 [0,1,2,3,4],第一条真实 token 从位置 2 开始。某些训练方案允许这种绝对偏移,某些模型则要求首个真实 token 从 0 开始;训练和推理必须一致。可按有效 mask 生成每行位置:

valid = torch.tensor([
    [False, False, True, True, True],
    [True, True, True, True, True],
])
position_ids = valid.long().cumsum(dim=-1) - 1
position_ids = position_ids.clamp_min(0)
# [[0,0,0,1,2],
#  [0,1,2,3,4]]
python

padding 位置最终仍要由 attention mask 禁止作为 Key,损失也要忽略其标签。这里把 padding 的 position id 暂设 0,只是避免非法索引,不会自动让 padding 无效。

文档拼接、检索片段、滑动窗口和 packed sequence 还要明确:位置是全局连续、每段重置,还是保留原文坐标。没有唯一答案,但训练、缓存、mask 与推理服务必须使用同一契约。

12 一条可执行的调试路径#

  1. 先做无位置对照。 关闭位置表示和 dropout,同时重排输入与输出,验证自注意力近似满足置换等变,确认实验对象正确。
  2. 打印三份张量。 token ids、valid mask、position ids 必须分别检查;不要从形状相同推断语义相同。
  3. 做共同平移测试。 固定同一组内容 Q/K,把查询与键位置同时加 Δ\Delta;RoPE 点积应在数值容差内保持一致。
  4. 做相对位移测试。 只移动 Key 位置,确认点积确实随 sps-p 改变,而不是 RoPE 函数根本没接到注意力前。
  5. 验证偶奇配对。 最后一维必须按 (0,1),(2,3),... 配对;错误地把前半维与后半维配对会得到另一种旋转布局,权重不能混用。
  6. 比较完整前向与 KV Cache。 检查 past_length、新 position id 和已旋转 Key 的追加轴,逐位置 assert_close
  7. 测试左右 padding。 同一真实序列换一种 padding 方式后,若模型契约要求语义位置从 0 开始,则真实 token 输出应保持接近。
  8. 测试边界长度。 学习式绝对位置在 L=max_lengthL=max_length+1 应有明确行为,不要等线上才遇到越界。
  9. 用 float32 计算长位置角度。 若误差随位置增长,先排查低精度角度、错误缩放和缓存重复旋转,再怀疑注意力内核。

13 最常见的“形状正确,位置却错了”#

  • 只加 token embedding。 attention 能运行,却没有顺序坐标。
  • 以为 causal mask 已经编码距离。 它只阻止看未来,不区分相隔多远。
  • token id 当 position id。 重复词得到重复位置,语义完全颠倒。
  • 学习式 position 表越界后临时取模。 不报错,但远位置被映射成错误的旧坐标。
  • RoPE 在 hidden 上旋转一次。 标准 RoPE 作用于每层每头的 Q/K,不能用一次输入旋转替代。
  • 旋转了 Q 却没旋转 K。 相对位移恒等式不成立。
  • 连 Value 一起旋转并误称标准 RoPE。 这改变了被取回内容的坐标系,是不同设计。
  • 训练右 padding、推理左 padding 却仍共享 arange(L) 真实 token 的位置发生整体偏移。
  • KV Cache 每步从 0 开始。 新 token 的 RoPE 角度错误,完整与增量前向失配。
  • 缓存未旋转 K,读取时按当前步统一旋转历史 K。 历史 token 的各自位置被抹掉。
  • 把“公式能算更长位置”当成可靠长上下文。 模型可能从未学过那些相位、距离或注意力模式。

14 三类方法会在哪里失败?#

方法主要优点主要边界
正弦绝对位置无位置参数,可生成任意整数位置间接要求模型从 hidden 中学习相对关系
学习式绝对位置灵活,能拟合训练数据的位置模式最大表长固定,未训练行没有可靠语义
RoPEQ–K 点积显式依赖相对位移超训练长度仍可能外推失效,角度与缓存要一致

三者都可能学到训练数据中的位置捷径:例如标签总出现在开头,模型可能依赖绝对位置而不是内容。随机裁剪、片段偏移或适合任务的数据增强能缓解部分问题,但不能取代正确的评估。

长上下文尤其容易出现“能运行但质量下降”:显存允许把序列从 2K 扩到 32K,不代表位置表示、注意力分布和训练数据支持这种距离。应按长度分桶报告损失、检索准确率和位置敏感测试,而不是只验证没有 OOM。

15 与相近方法的边界#

方法如何影响注意力本文是否展开
Shaw 相对位置表示将相对距离向量写进注意力关系仅作边界比较
T5 相对位置偏置按距离桶给 attention logits 加偏置
ALiBi按头给距离施加线性惩罚
RoPE旋转 Q/K,使点积含相对位移
Position Interpolation/Scaling改写 RoPE 位置或频率以适配更长上下文下一阶段

相对位置偏置直接修改 [N,H,L,S] 的分数矩阵;RoPE 修改 [N,H,L,d] 的 Q/K 后再计算分数。两者都能表达相对结构,但参数化、内存访问与外推行为不同,不能只因都含“相对位置”就视为同一种实现。

16 今天真正需要记住什么?#

  1. 自注意力本身对 token 排列置换等变;causal mask 只限制可见边,不能单独表达完整的位置与距离。
  2. 正弦与学习式绝对位置都先进入 hidden,再影响 Q/K/V;前者是固定多频率坐标,后者是有限长度参数表。
  3. RoPE 对每层每头的 Q/K 成对旋转,利用 R(p)R(s)=R(sp)R(p)^\top R(s)=R(s-p) 让注意力点积显式依赖相对位移。
  4. RoPE 通常不旋转 Value,也不改变 SDPA 张量形状;它发生在 Q/K 投影之后、点积之前。
  5. KV Cache 必须用全局 position id 旋转新 Q/K,并缓存已经按各自位置旋转的历史 Key。
  6. position ids、padding mask 与 token ids 是三份不同契约;左右 padding、拼接和打包时必须显式定义。

17 思考题与小练习#

  1. q=[1,2],k=[3,4]q=[1,2],k=[3,4]θ=π/3\theta=\pi/3。分别计算 (p=1,s=2)(p=5,s=6) 的 RoPE 点积,验证共同平移后不变;再将第二组改为 (p=5,s=7),观察相对位移变化。
  2. 修改 apply_rope,故意只旋转 Query、不旋转 Key。写一个共同平移测试并解释为什么会失败。
  3. 为长度 4 的完整 causal forward 与“前 2 token prefill + 后 2 token 逐步 decode”写等价测试。加入错误版本:每个 decode token 都使用 position 0,比较每个位置的最大绝对误差。

相关工作#

18 下一篇预告#

RoPE 的公式可以为任意位置生成角度,但模型在 2K 长度上训练,并不会自动在 32K 上可靠工作。下一篇将区分“位置函数可计算”与“模型真正会外推”,拆解 Position Interpolation、频率缩放以及长上下文评估为何必须按距离和位置分桶。

词相同但顺序不同,注意力怎样辨别位置?从正弦编码到 RoPE
https://zwjcode.cn/blog/transformer-positional-encoding-rope-relative-dot-product
作者
发布于 2026年9月8日
版权协议 CC BY-NC-SA 4.0
评论加载似乎遇到了问题,请尝试刷新页面。