qinghuan
← Transformer 与语言建模 / 全部笔记

单元 21 / Transformer 与语言建模

P0 · 必会主线L2 · 进阶

位置编码、RoPE 与 Transformer 模块

用向量旋转表示位置,推导两个位置之差(相对位移)怎样影响点积,再把归一化、注意力和前馈残差接成 Transformer 块。

本篇目录 · 按需要跳转

读完这篇,你能做什么

验证 RoPE 不改变向量长度、共同平移位置不改变相关点积,并用小 Transformer 块检查改动未来输入不会改变过去输出。

学习理由、准备要求与知识点

为什么学它

位置编号、旋转布局和缓存起始位置必须对得上;矩阵形状都正确,也可能因为这些约定不一致而算错。

先读这些内容

知识点清单

  • 位置编码与顺序信息
  • 二维旋转矩阵
  • RoPE频率与偶数维度
  • 相对位置点积恒等式
  • 相邻与半区配对布局
  • 多头维度与广播
  • PreNorm残差模块
  • KV缓存位置偏移
  • 长度外推边界

运行前先确认PyTorch 2.9.1+cpu、Java 17;源码对照Transformers v4.57.1 Llama。实验使用相邻二维配对RoPE与自编小模块,未加载Llama权重;真实源码使用前后半区配对布局。

01先弄懂它是什么

同样几个词,换个顺序可能就是另一个意思。只按内容匹配、又没有位置表示的自注意力,很难仅凭相同 token 的向量分清排列关系。位置编码用来补上顺序信息:有的方法把位置向量加到输入上,有的方法改变查询和键的比较方式。RoPE 采用后者,对位置 m 的查询和键按对应角度旋转,让注意力点积能带上两个位置相隔多远的信息。

可以先从平面上的旋转理解 RoPE。二维向量 (a,b) 旋转角度 theta 后,结果为 (a*cos(theta)-b*sin(theta), a*sin(theta)+b*cos(theta))。

旋转不改变自身的欧氏长度,但会改变它与另一个向量的夹角。RoPE 把偶数维的头向量拆成一对对二维向量,为第 i 对设置频率 omega_i,位置 m 对应角度 m*omega_i,让不同对以不同速度旋转,从而表达不同尺度的位置变化。

位置旋转只是 Transformer 块的一部分,块里还有归一化、注意力输出投影,以及在每个位置分别运行的前馈网络。可以把输入 [B,T,D] 读成“B 条序列,每条 T 个位置,每个位置 D 个数”。

拆成 H 个注意力头后,通常变成 [B,H,T,D/H],让每个头处理自己那一份表示;算完再拼接,并投影回 D 维,才能与原输入对应相加形成残差。这里的旋转通常施加在 Q 和 K 上,不能把旋转 V 当作所有实现的必选步骤,也不能把整块计算理解成转一次向量。

文中这几个词,先认一认

旋转频率
在某一对二维分量中,位置每前进一格,向量要转过多少角度。
相对位置
两个位置相隔多远,即差值 n-m,关注的是彼此距离,不是各自的绝对编号。
PreNorm模块
先对输入做归一化,再送入分支计算,最后把分支结果加回原来的残差路径。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

旋转矩阵满足R(m)转置乘R(n)=R(n-m),因此(R(m)q)点积(R(n)k)=q点积R(n-m)k。这里m和n实际代表乘以各自频率后的角度。若给所有位置同时加相同偏移,两侧相对差不变,纯旋转点积保持不变。这个恒等式是机制证据,但不意味着完整模型在所有位置平移后输出必然一致,其他位置处理和边界也可能变化。

常见基准频率形如omega_i=base^(-2i/d),d为参与旋转的维数,i从0到d/2-1。相邻配对与前后半区配对可以通过固定维度置换表达同类旋转,但不能在不调整配套权重和缓存的情况下随意切换。源码对照使用半区布局,完整Java和CPU机制实验使用相邻布局,特意将这条兼容性边界写清。

把上下文加长时,还要检查位置编号和训练中见过的长度。使用缓存生成新 token,位置应接着前缀的绝对位置编号;若每次又从零开始,新 Q 与旧 K 的旋转角度关系就会错。修改 base 或 RoPE 缩放会改变频率安排,但不会自动保证长文本效果。应在目标长度上测试检索、顺序和任务表现,并检查低精度计算是否把不同位置算得难以区分。

先跑通一个最小例子

CPU双精度RoPE性质测试,再运行一个单头PreNorm小块;不训练或复现预训练模型。

展开收起代码 · experiment.pypython · 44 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

experiment.py · python
import torch
from torch import nn
from torch.nn import functional as F
torch.manual_seed(21)
torch.set_num_threads(1)
def rope(x, positions, base=10000.):
    d = x.shape[-1]
    if d % 2 or d == 0:
        raise ValueError("even dimension required")
    freq = base ** (-torch.arange(0, d, 2, dtype=x.dtype) / d)
    angle = positions[..., None] * freq
    a, b = x[..., 0::2], x[..., 1::2]
    result = torch.stack((a*angle.cos()-b*angle.sin(), a*angle.sin()+b*angle.cos()), -1)
    return result.flatten(-2)  # 本实验明确采用相邻配对
q, k = torch.randn(4, dtype=torch.float64), torch.randn(4, dtype=torch.float64)
assert torch.allclose(rope(q, torch.tensor(3.)).norm(), q.norm())
left = (rope(q, torch.tensor(2.)) * rope(k, torch.tensor(5.))).sum()
right = (rope(q, torch.tensor(12.)) * rope(k, torch.tensor(15.))).sum()
assert torch.allclose(left, right, atol=1e-12)
assert torch.allclose(rope(q, torch.tensor(0.)), q)
class Block(nn.Module):
    def __init__(self):
        super().__init__()
        self.norm1, self.norm2 = nn.LayerNorm(4), nn.LayerNorm(4)
        self.qkv = nn.Linear(4, 12)
        self.out = nn.Linear(4, 4)
        self.ff = nn.Sequential(nn.Linear(4, 8), nn.GELU(), nn.Linear(8, 4))
    def forward(self, x):
        q, k, v = self.qkv(self.norm1(x)).chunk(3, -1)
        pos = torch.arange(x.shape[-2], dtype=x.dtype)
        q, k = rope(q, pos), rope(k, pos)
        attention = F.scaled_dot_product_attention(q, k, v, is_causal=True, dropout_p=0.)
        x = x + self.out(attention)
        return x + self.ff(self.norm2(x))  # 两个分支各自有残差
block = Block().double()
x = torch.randn(1, 5, 4, dtype=torch.float64, requires_grad=True)
out = block(x)
changed = x.detach().clone()
changed[:, 4] += 20
assert out.shape == x.shape
assert torch.allclose(out[:, :4], block(changed)[:, :4], atol=1e-12)
out.square().mean().backward()
assert torch.isfinite(x.grad).all()
print("6 RoPE and block checks passed")

核对运行结果旋转保持范数和相对点积;小块输出形状保持,未来扰动不影响过去,输入梯度有限。

正在加载流程图…

位置旋转嵌在注意力分支中,模块整体仍需维度和残差契约。
查看流程图源码(Mermaid)
flowchart TD
A["输入与位置编号"] --> B["归一化和QKV投影"]
B --> C["按位置旋转Q与K"]
C --> D["因果注意力与输出投影"]
D --> E["第一次残差相加"]
E --> F["归一化和前馈网络"]
F --> G["第二次残差相加"]

按顺序看,每一步做了什么

  1. 从二维旋转推导长度不变。
  2. 用旋转矩阵乘法验证相对位置公式。
  3. 明确真实模型的配对布局与位置偏移。
  4. 组合小模块并检查因果前向和有限梯度。

03对照实现代码

Llama对Q/K应用半区布局旋转 (新标签页)↗

Transformers v4.57.1 · LlamaRotaryEmbedding.forward;rotate_half;apply_rotary_pos_emb;LlamaAttention.forward

原创半区配对教学函数,用于解释真实源码布局;省略动态缩放、GQA、缓存及内核替换,不是完整Llama实现。

核心机制代码 · python
import torch
def half_layout_rope(x, position, base=10000.):
    # 教学片段前提:最后一维为正偶数,频率按每个二维旋转对生成
    half = x.shape[-1] // 2
    freq = base ** (-torch.arange(half, dtype=x.dtype) / half)
    angle = position[..., None] * freq  # 位置轴与频率轴形成角度表
    first, second = x[..., :half], x[..., half:]
    left = first * angle.cos() - second * angle.sin()
    right = second * angle.cos() + first * angle.sin()
    return torch.cat((left, right), -1)  # 前半与后半对应维度成对,而非相邻成对

真实LlamaRotaryEmbedding从配置选择RoPE初始化函数,生成频率与缩放因子;并不是所有配置都只用固定base。

forward在关闭autocast的范围内以float32计算位置与频率,再按输入dtype返回cos和sin。

rotate_half把向量前后两半组合成负后半与前半,apply_rotary_pos_emb按布局广播cos/sin后作用于Q和K。

注意力随后处理缓存与后端选择,位置逻辑必须与已缓存K的编码保持一致,不能仅验证单次无缓存前向。

代码先调用谁,再调用谁

  1. 配置生成inv_freq
  2. position_ids构造cos与sin
  3. Q/K投影并拆头
  4. apply_rotary_pos_emb
  5. 缓存更新与注意力

04动手写一个例子

相邻配对RoPE与相对位移不变量

对偶数维有限double向量按位置和频率做二维旋转。验证零位置、范数、逆旋转以及共同位置偏移下的点积一致;拒绝奇数维和非法base。

输入 1
x=[1,0],角度pi/2
输出 1
约[0,1]
输入 2
q在2、k在5;同时平移10
输出 2
两组旋转点积相等

每对分量应用二维旋转,频率按维度索引递减。

二维旋转正交,所以每对子向量的平方长度保持,总长度也保持。

点积恒等式可通过共同偏移检验,比只比几个固定输出更能发现符号错误。

为什么这样写是对的理想实数计算下旋转保持范数,R(-m)R(m)x=x;浮点比较采用有限容差。

时间 O(d)额外空间 O(d)

每两个分量执行常数次三角与乘加操作,返回一个同长数组。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
public class Main {
  static double[] rotate(double[] x,double position,double base){
    if(x.length==0||x.length%2!=0||!Double.isFinite(position)||!Double.isFinite(base)||base<=1)
      throw new IllegalArgumentException();
    double[] out=new double[x.length];
    for(int i=0;i<x.length;i+=2){
      if(!Double.isFinite(x[i])||!Double.isFinite(x[i+1]))throw new IllegalArgumentException();
      double angle=position*Math.pow(base,-(double)i/x.length); // 第i/2对使用独立频率
      double c=Math.cos(angle),s=Math.sin(angle);
      out[i]=x[i]*c-x[i+1]*s;out[i+1]=x[i]*s+x[i+1]*c; // 相邻二维正交旋转
    }
    return out; // 保留输入数组,便于比较旋转前后性质
  }
  static double dot(double[] a,double[] b){double sum=0;for(int i=0;i<a.length;i++)sum+=a[i]*b[i];return sum;}
  static void close(double a,double b){if(Math.abs(a-b)>1e-9)throw new AssertionError(a+" != "+b);}
  public static void main(String[] args){
    double[] q={1,2,3,4},k={-2,1,0,5};
    close(rotate(new double[]{1,0},Math.PI/2,10000)[1],1);
    close(dot(rotate(q,3,10000),rotate(q,3,10000)),dot(q,q));
    close(dot(rotate(q,2,10000),rotate(k,5,10000)),
          dot(rotate(q,12,10000),rotate(k,15,10000)));
    double[] inverse=rotate(rotate(q,7,10000),-7,10000);
    for(int i=0;i<q.length;i++)close(inverse[i],q[i]);
    close(rotate(q,0,10000)[2],q[2]);
    boolean bad=false;try{rotate(new double[]{1,2,3},1,10000);}catch(IllegalArgumentException e){bad=true;}
    if(!bad)throw new AssertionError();
    System.out.println("6 rotary invariants passed");
  }
}

这些特殊情况也要试一试

  • 奇数维不能完整配对,本题直接拒绝。
  • 极大位置可能降低三角函数参数精度,理论恒等式不等于无限精度实现。
  • 负位置只用于数学逆旋转测试,不表示训练数据采用负编号。
进阶选读 · 生产拓展:缓存解码的位置一致性

场景整段前向与逐token带KV缓存解码应给出一致结果。

做法固定无dropout条件,对同一序列比较两种执行的每步logits;记录缓存长度、position_ids与旋转布局,测试非零起始偏移和padding。

复杂度校验参考整段注意力通常O(T²d),逐步缓存解码总注意力工作也随历史长度累计。

好处与代价缓存减少重复投影,但引入状态、位置和内存管理,不能只测试第一步。

05项目里怎么用

复现架构时先建立参数和张量表:归一化类型、epsilon、MLP激活、是否门控、头数、KV头数和位置布局都可能不同。把两个都叫Transformer的模块互换并不能保证权重兼容。

位置相关计算应单独测试低精度误差。源码选择float32生成三角量有明确数值目的,但最终计算还受输入dtype影响。修改频率或上下文上限后,需要检查训练分布之外的位置与任务表现。

比较PreNorm和PostNorm时同时控制优化器、初始化和训练预算,否则无法把差异归因于归一化位置。小块有限梯度只是基本正确性检查,模型效果还要由可靠验证集和消融实验判断。

容易踩的坑

  • 把半区旋转和相邻旋转直接混用预训练权重。
  • 缓存解码的新token位置每次从零开始。
  • 认为RoPE相对形式保证任意长度外推。
  • 只检查维度而不检查因果输出。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
绝对位置向量概念和实现直观位置范围与外推需处理固定长度或明确位置表
RoPE点积自然表达相对位移布局与缓存契约严格相应设计的注意力模型
调整RoPE频率可探索更长位置范围需额外训练和评估验证明确长上下文研究

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01二维旋转为什么保持长度?

展开参考答案与推导

旋转矩阵正交,转置乘自身为单位矩阵。

02RoPE点积为何依赖相对位置?

展开参考答案与推导

两侧旋转合成R(n-m),共同偏移在矩阵乘法中抵消。

03残差相加需要什么形状条件?

展开参考答案与推导

两支必须具有兼容且语义一致的形状,通常都回到[B,T,D]。

换个情况,还会处理吗

01无缓存正常,缓存解码异常,先查什么?

展开参考答案与推导

位置偏移、缓存K是否已旋转、重复旋转和掩码对齐。

02换为相邻配对后仍能加载权重,是否等价?

展开参考答案与推导

不一定,张量形状一致不代表维度语义一致,需要对应权重置换或原布局。

03增大最大位置后模型能输出,说明外推成功吗?

展开参考答案与推导

只说明可执行,需长上下文任务与数值测试。

写代码试一试

01[1,0]旋转pi/2得到什么?

输入 / 条件
单对向量
输出 / 预期
[0,1]

提示 cos为0,sin为1。

展开参考答案与推导

第一分量0,第二分量1,浮点近似允许小误差。

02位置2和5都加10,点积如何变化?

输入 / 条件
相同频率规则
输出 / 预期
保持

提示 相对差仍为3。

展开参考答案与推导

利用R(m)转置R(n)=R(n-m)。

03怎样测试旋转符号是否写反?

输入 / 条件
先旋转7再旋转-7
输出 / 预期
还原输入

提示 同时检查固定pi/2例子。

展开参考答案与推导

逆旋转和已知方向样例结合,避免两处同错相互抵消。

读完之后,独立完成

课后作业:自己做一遍

本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

验证 RoPE 不改变向量长度、共同平移位置不改变相关点积,并用小 Transformer 块检查改动未来输入不会改变过去输出。

本篇重点加练

相对位置关系、缓存偏移和长度外推不能互相替代,重点检验位置契约变更后的表现。

  • 相对位置点积恒等式
  • KV缓存位置偏移
  • 长度外推边界

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01代码推演

    给定四维向量 [1,2,3,4],位置为 0、1、3。为相邻配对和半区配对分别写出旋转作用的坐标对及完整变换定义。

    提交内容
    坐标布局图与公式
    怎样算完成
    • 频率和维度对应明确
    • 布局不能暗中替换

    练到的知识点二维旋转矩阵 · RoPE频率与偶数维度 · 相邻与半区配对布局

  2. 作业 02原理问答

    对两个二维向量推导同时平移位置时点积的变化规律,并列出该结论成立所需的旋转频率条件。

    提交内容
    代数论证与条件表
    怎样算完成
    • 相对位置定义一致
    • 条件不被省略

    练到的知识点位置编码与顺序信息 · 相对位置点积恒等式

  3. 作业 03原理问答

    序列位置允许跳号而非从零连续增长。对位置 [0,2,7] 声明 Q/K 的绝对位置,分析缓存索引与数学位置应如何对应。

    提交内容
    位置契约及点积关系
    怎样算完成
    • 数组偏移与位置 ID 分开
    • 相对位移可核对

    练到的知识点相对位置点积恒等式 · KV缓存位置偏移

  4. 作业 04代码推演

    固定一组 RoPE 频率,把所有位置乘二而非加常数,推导各对位置点积表达式发生何种变化;用四维小向量核对。

    提交内容
    符号推导及数值记录
    怎样算完成
    • 平移与缩放分别讨论
    • 频率未被暗中更改

    练到的知识点相对位置点积恒等式 · 长度外推边界

综合应用

  1. 作业 05编程实践

    实现 [B=2,H=2,T=5,D=4] 的 CPU RoPE,比较整段处理与切成 3+2 两块处理;测试显式位置偏移及广播。

    提交内容
    实现和分块等价测试
    怎样算完成
    • 位置来源可追踪
    • 多头维度不混用

    练到的知识点RoPE频率与偶数维度 · 多头维度与广播 · KV缓存位置偏移

  2. 作业 06排错实验

    在小型 PreNorm 注意力块中分别注入错误旋转布局、遗漏缓存位置偏移、残差维度错误,记录哪类问题能被形状检查捕获。

    提交内容
    故障注入与诊断表
    怎样算完成
    • 错误类型分别定位
    • 静默错误有数值测试

    练到的知识点相邻与半区配对布局 · PreNorm残差模块 · KV缓存位置偏移

  3. 作业 07面试场景

    模型只在长度不超过 32 的合成序列训练,却要评估长度 128。设计位置外推实验,区分能运行、能保留规律与能完成任务。

    提交内容
    训练及评价协议
    怎样算完成
    • 长度分布清楚
    • 任务指标不由接口成功代替

    练到的知识点位置编码与顺序信息 · 长度外推边界

  4. 作业 08编程实践

    在 CPU 上实现长度六的滑动窗口读取,只保留最近三项缓存;比较保留原位置编号和重新从零编号两种策略下对应查询的注意力。

    提交内容
    滑窗实验与位置日志
    怎样算完成
    • 比较使用相同历史内容
    • 位置调整范围明确

    练到的知识点相对位置点积恒等式 · KV缓存位置偏移

  5. 作业 09排错实验

    设相同 token 前缀但不同位置起点,故意复用同一份已旋转缓存;记录缓存身份缺少位置条件时的差异,加入兼容性测试。

    提交内容
    错误复用案例及检查器
    怎样算完成
    • 权重及 token 固定
    • 缓存条件与结果关联清楚

    练到的知识点KV缓存位置偏移 · 长度外推边界

进阶挑战 · 掌握前置后选做

  1. 作业 10设计评审

    评审更换 RoPE 频率尺度的实验方案,要求冻结其他模块并报告短序列能力变化;设计至少一个检查位置顺序的合成任务。

    提交内容
    受控方案与任务生成器
    怎样算完成
    • 干预因素单一
    • 短长序列结果同时报告

    练到的知识点相对位置点积恒等式 · PreNorm残差模块 · 长度外推边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 11思维拓展 · 反例构造

    “旋转保持向量长度,所以任意长位置上任务能力不变”是否成立?构造论证缺口,并设计可区分几何不变量与任务泛化的小实验。

    提交内容
    反驳条件与验证方案
    怎样算完成
    • 几何与预测性能分开
    • 实验有保留长度段

    练到的知识点相对位置点积恒等式 · 长度外推边界

  2. 作业 12思维拓展 · 开放探究

    在有限精度下研究很大位置 ID 的旋转误差:自行规定 CPU 双精度可测试范围,比较相同相对位移在不同绝对起点的数值一致性。

    提交内容
    范围说明、误差表及限制
    怎样算完成
    • 参考计算或不变量独立
    • 数值误差不直接称模型退化

    练到的知识点KV缓存位置偏移 · 长度外推边界

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗