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

单元 20 / Transformer 与语言建模

P0 · 必会主线L2 · 进阶

缩放点积注意力与掩码:亲手计算一次关注

先看一个位置怎样读取其他位置的信息,再从 QK 转置的形状推导缩放点积注意力,用因果与布尔掩码检查哪些连接被允许。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:先核对矩阵各维的大小,再检查不同框架 API 如何解释掩码、整行屏蔽时怎样处理以及 dropout 的行为;用梯度实验检查被屏蔽信息是否仍影响计算。

开始读正文 ↓
学过相近内容?查看哪些可以跳过

两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。

注意力

共同基础Q / K / V、缩放、掩码与 softmax 的基础直觉以 Agent 入门篇为主读,属于应用路线的选修模型背景。

另一条路线补充什么?

画出 Token 怎样变成注意力输出,说明每个位置能看到哪些上下文、为什么不能提前看到未来内容;按需选学,不要求训练模型。

读完这篇,你能做什么

手算一行注意力,运行 SDPA 对照,检查首个输出对未来 V(值向量)的梯度为零,并分清不同 API 的布尔掩码含义。

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

为什么学它

掩码方向、广播轴或全屏蔽行处理错了,即使程序能运行,也可能读到不该读取的位置或产生无效数值。

先读这些内容

知识点清单

  • Q和K与V形状
  • 点积缩放sqrt(dk)
  • 按key轴softmax
  • 因果掩码
  • padding掩码广播
  • 布尔掩码API差异
  • 全屏蔽行策略
  • dropout参数与eval
  • 二次注意力成本

运行前先确认PyTorch 2.9.1+cpu、Python 3.14、Java 17。先计算单头注意力;CPU参考实现与SDPA比较,未测试GPU融合内核性能。

01先弄懂它是什么

处理一个位置时,注意力要决定从其他位置各取多少信息。可以先想成“当前有一个查询,面前有一排可供读取的位置”:Q 是查询向量,K 用来与它比较,V 是最后要汇总的内容。一般有 L 个查询、S 个键,查询和键各有 dk 个分量,值有 dv 个分量,所以 Q、K、V 的形状分别是 [L,dk]、[S,dk]、[S,dv]。

Q 乘 K 转置得到 [L,S] 分数表,每行是一项查询对各个键的打分。每行做 softmax 得到权重,再乘 V,结果是 [L,dv]。这时得到的是值向量的加权汇总,还没有在词表中选出 token。

为什么注意力分数还要除以 sqrt(dk)?在查询和键的分量尺度合适时,维度 dk 越大,点积分数的方差也可能越大。分数差距过大,会让 softmax 的权重过于集中,也让梯度更难传递。除以 sqrt(dk) 是在调节分数尺度;它没有把每个向量的长度变成一,所以缩放点积注意力不能直接当作余弦相似度。

掩码是一张“这个位置能读谁”的允许表。例如按顺序预测下一词元时,位置 i 可以读自己及更早的输入,不能偷看后面的目标信息。为凑齐批次长度而补上的 padding 键,通常也不能被有效查询读取。这些限制应由程序在 softmax 前作用到分数上,不能指望训练自己学会。若先让所有位置分完权重,再把禁读位置清零而不重新归一化,剩下的权重之和会改变,输出大小也随之改变。

文中这几个词,先认一认

Query/Key/Value
Q 是当前查询的一组数,先与各位置的 K 算匹配分数,再把分数换成权重,用这些权重把对应的 V 加权相加。Q、K、V 通常由输入经过各自的线性变换得到,最终输出是 V 的加权汇总。
因果掩码
规定每个位置只能读取自己和允许的历史位置,防止训练时直接看到后面的目标词元;未来位置对应的注意力连接被屏蔽。
SDPA
PyTorch 提供的缩放点积注意力接口 scaled_dot_product_attention;实际采用哪种计算实现可能随设备变化。

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

02看看它怎么工作

单行稳定softmax先从允许位置得分中减去最大值,再取指数并归一化。被屏蔽位置的权重应为零。若整行没有允许位置,普通全负无穷softmax会出现未定义的归一化;教学算法明确返回零输出,避免依赖偶然NaN传播。实际模型更应检查为什么会出现该行,并确保损失也不把无意义padding查询当作有效预测。

换注意力接口时,先核对布尔掩码中 True 的含义:PyTorch SDPA 用它表示允许参与,MultiheadAttention 的 attn_mask 却用它表示禁止,因此同一张布尔矩阵不能直接互换。查询形状为 [B,H,L,dk] 时,掩码还要能按规则扩展到相应权重形状,也就是满足广播要求。归一化应沿键的 S 维进行,不能误用 head 或 query 所在维度。

SDPA按dropout_p实参执行dropout,即使外部模块已eval也应显式传0。因果实验修改未来位置的V,较早位置输出必须保持不变;再只对第一个位置输出求和反向,未来V的梯度应为零。这比仅检查输出形状更有力,因为错误掩码往往仍能产生正确形状和下降的训练损失。

先跑通一个最小例子

CPU双精度比较手算与PyTorch SDPA,固定随机种子并关闭dropout。

experiment.py · python
import math
import torch
from torch.nn import functional as F
torch.manual_seed(20)
torch.set_num_threads(1)
q = torch.randn(1, 1, 4, 2, dtype=torch.float64, requires_grad=True)
k = torch.randn(1, 1, 4, 2, dtype=torch.float64, requires_grad=True)
v = torch.randn(1, 1, 4, 3, dtype=torch.float64, requires_grad=True)
allowed = torch.ones(4, 4, dtype=torch.bool).tril()  # SDPA的True表示允许
scores = q @ k.transpose(-2, -1) / math.sqrt(2)
weights = scores.masked_fill(~allowed, float("-inf")).softmax(-1)
reference = weights @ v
actual = F.scaled_dot_product_attention(q, k, v, attn_mask=allowed, dropout_p=0.)
assert actual.shape == (1, 1, 4, 3)
assert torch.allclose(actual, reference, atol=1e-12)
assert torch.allclose(weights.sum(-1), torch.ones(1, 1, 4, dtype=torch.float64))
changed = v.detach().clone()
changed[..., 3, :] += 1000
other = F.scaled_dot_product_attention(q, k, changed, attn_mask=allowed, dropout_p=0.)
assert torch.allclose(actual[..., :3, :], other[..., :3, :])  # 未来值不能影响过去输出
actual[..., 0, :].sum().backward()
assert torch.equal(v.grad[..., 1:, :], torch.zeros_like(v.grad[..., 1:, :]))
assert torch.isfinite(q.grad).all()
blocked = F.scaled_dot_product_attention(q.detach(), k.detach(), v.detach(),
    attn_mask=torch.zeros(4, 4, dtype=torch.bool), dropout_p=0.)
assert torch.equal(blocked, torch.zeros_like(blocked))  # 固定CPU基线验证全屏蔽结果
print("7 attention mask checks passed")

核对运行结果手算与SDPA一致,未来扰动不影响过去,未来值梯度为零;全屏蔽行为仅在本基线断言。

正在加载流程图…

掩码在归一化之前定义可见范围,输出仍是值空间向量。
查看流程图源码(Mermaid)
flowchart LR
Q["Q L乘dk"] --> D["QK转置除sqrt(dk)"]
K["K S乘dk"] --> D
D --> M["应用允许位置掩码"]
M --> S["沿key轴稳定softmax"]
S --> V["加权求和 V"]
V --> O["输出 L乘dv"]

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

  1. 逐项标出Q、K、V及得分形状。
  2. 计算缩放得分并沿key轴归一化。
  3. 明确True表示允许还是禁止,验证全屏蔽边界。
  4. 用未来扰动和梯度测试核对因果性。

03对照实现代码

SDPA接口公式与后端分派边界 (新标签页)↗

PyTorch v2.9.1 · scaled_dot_product_attention;torch._C._nn.scaled_dot_product_attention

原创无dropout、单头参考模型,显式定义全屏蔽行为;不是FlashAttention或PyTorch融合内核源码。

核心机制代码 · python
import math
import torch
def reference_attention(q, k, v, allowed):
    scores = q @ k.transpose(-2, -1) / math.sqrt(q.shape[-1])
    empty = ~allowed.any(-1, keepdim=True)  # 识别没有合法键的行
    scores = scores.masked_fill(~allowed, float("-inf"))
    scores = torch.where(empty, torch.zeros_like(scores), scores)  # 避免全负无穷归一化
    weights = scores.softmax(-1).masked_fill(~allowed, 0.)  # 屏蔽位置严格为零
    return weights @ v

functional.py将接口绑定到C++入口并附上等价公式说明,不表示实际运行必然使用Python矩阵步骤。

缩放默认为查询末维的平方根倒数,布尔掩码被解释为允许位置,浮点掩码则作为得分偏置。

接口中的dropout行为取决于参数,调用方要根据模块训练状态传入恰当概率。

不同设备与形状可能选不同内核;参考模型用于正确性对照,不据此推断显存或吞吐。

代码先调用谁,再调用谁

  1. 准备Q/K/V和掩码
  2. SDPA参数处理
  3. 后端选择注意力实现
  4. 得分归一化与值汇总
  5. 反向传播

04动手写一个例子

本段仍从单个查询计算注意力,但示例规则有所不同:可以逐个指定哪些位置可见,V 中每个位置是一个数(标量),全部屏蔽时返回 0。入门篇只允许查看一段连续前缀,V 中每个位置是向量,没有可见位置时会拒绝计算。这些是两段示例各自的接口约定,不能当作所有框架的统一行为。之后还要学习各框架怎样解释掩码、dropout、梯度实验,以及下方的分块在线 softmax。

复习已学内容:需要时展开讲解和代码

带允许掩码的单查询稳定注意力

输入一个d维查询、S个d维键、S个标量值和S个允许标记,计算缩放点积权重及标量输出。全屏蔽明确返回0;非有限得分或错误形状拒绝。

输入 1
q全0,两个值10和20,均允许
输出 1
15
输入 2
只允许第一个键
输出 2
10

计算每个点积再除以sqrt(d),只在允许集合寻找最大值。

指数使用score-max,使最大指数为1,避免普通大得分直接exp溢出。

先计算归一化分母再求加权输出,全屏蔽在归一化之前单独处理。

为什么这样写是对的至少一个位置允许时,权重非负、屏蔽权重为零且总和约为一,输出位于允许值的凸包内。

时间 O(Sd)额外空间 O(S)

点积扫描S个d维键,额外保存每个得分;标量汇总为O(S)。

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

展开收起代码 · Main.java · 完整可运行程序Java · 41 行

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

Main.java · 完整可运行程序 · Java
public class Main {
  static double attention(double[] q,double[][] keys,double[] values,boolean[] allowed){
    if(q.length==0||keys.length==0||keys.length!=values.length||values.length!=allowed.length)
      throw new IllegalArgumentException();
    for(double x:q)if(!Double.isFinite(x))throw new IllegalArgumentException();
    double[] scores=new double[keys.length];
    double maximum=Double.NEGATIVE_INFINITY;
    for(int i=0;i<keys.length;i++){
      if(keys[i].length!=q.length||!Double.isFinite(values[i]))throw new IllegalArgumentException();
      for(int d=0;d<q.length;d++){
        if(!Double.isFinite(keys[i][d]))throw new IllegalArgumentException();
        scores[i]+=q[d]*keys[i][d];
      }
      scores[i]/=Math.sqrt(q.length); // 缩放依赖查询键维度
      if(!Double.isFinite(scores[i]))throw new ArithmeticException("score range");
      if(allowed[i])maximum=Math.max(maximum,scores[i]);
    }
    if(maximum==Double.NEGATIVE_INFINITY)return 0; // 显式定义全屏蔽行
    double sum=0;
    for(int i=0;i<scores.length;i++){
      scores[i]=allowed[i]?Math.exp(scores[i]-maximum):0;sum+=scores[i];
    }
    double output=0;
    for(int i=0;i<scores.length;i++)output+=(scores[i]/sum)*values[i]; // 先归一化再加权
    return output;
  }
  static void check(boolean v){if(!v)throw new AssertionError();}
  static void close(double x,double y){check(Math.abs(x-y)<1e-10);}
  public static void main(String[] args){
    double[][] k={{1,0},{0,1}};double[] v={10,20};
    close(attention(new double[]{0,0},k,v,new boolean[]{true,true}),15);
    close(attention(new double[]{1,0},k,v,new boolean[]{true,false}),10);
    close(attention(new double[]{1,0},k,v,new boolean[]{false,false}),0);
    close(attention(new double[]{1000,0},k,v,new boolean[]{true,true}),10);
    close(attention(new double[]{1,0},k,new double[]{10,999},new boolean[]{true,false}),10);
    boolean bad=false;
    try{attention(new double[0],k,v,new boolean[]{true,true});}catch(IllegalArgumentException e){bad=true;}
    check(bad);
    System.out.println("6 stable attention checks passed");
  }
}

这些特殊情况也要试一试

  • 全屏蔽输出零是本算法的明确契约。
  • 减最大值保护指数步骤,不会自动修复已经溢出的点积。
  • 非方形查询键长度的因果对齐需按实际缓存布局设计。
进阶选读 · 生产拓展:分块在线softmax

场景长序列不希望显式保存完整L乘S权重矩阵。

做法分块维护每行最大值m、指数和l及加权和,遇到更大最大值时重新缩放历史累积,再结合反向重计算;对照参考输出验证。

复杂度精确稠密注意力算术量仍通常为O(LSd),可降低中间矩阵显存,不是把注意力复杂度自动变线性。

好处与代价融合与分块减少访存,却增加实现和数值误差验证难度。

05项目里怎么用

先建立小张量参考和因果测试,再启用融合内核。不同后端在浮点求和顺序上有差异,容差比较通常比逐位比较更适合;容差应根据精度和任务设定,不能用宽松阈值掩盖掩码错误。

补齐位置有两件不同的事要处理。第一,别人算注意力时不要读取 padding 键,避免把填充值当资料。第二,轮到 padding 查询所在的位置时,不要把它当成有效的预测题来算训练损失。例如一句话后面补了三个空位,既要阻止正文读取这些空位,也要避免把这三个空位的“答题成绩”放进平均损失。两处限制作用不同,只做其中一个通常不够。

注意力图可以帮助调试,但权重高不等于因果解释成立。模型还有投影、残差和多层变换,研究结论应结合干预实验。计算成本也要区分得分矩阵、线性投影和KV缓存,不能只引用一个序列平方就替代实际测量。

容易踩的坑

  • 混用SDPA与MultiheadAttention布尔掩码方向。
  • 沿query轴而非key轴softmax。
  • eval时仍给SDPA传非零dropout_p。
  • 屏蔽全行后直接softmax得到NaN。

几种做法,怎么选

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

方案优势代价与限制适用条件
显式参考注意力形状与权重易检查中间矩阵较大教学和小规模正确性验证
融合SDPA可减少访存并提升吞吐依赖后端与形状限制验证正确后实际训练
稀疏或局部注意力减少可见连接数量改变模型可访问的信息任务允许局部依赖

06检验是否读懂了

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

先用自己的话解释

01Q[L,dk]与K[S,dk]得分形状是什么?

展开参考答案与推导

[L,S],每个查询对应全部键。

02为何除以sqrt(dk)?

展开参考答案与推导

在相应分量尺度假设下控制点积方差,避免维度增大导致softmax过尖。

03SDPA中布尔True表示什么?

展开参考答案与推导

允许参加注意力;MultiheadAttention的布尔attn_mask采用相反禁止语义。

换个情况,还会处理吗

01模型训练损失异常低,掩码可能有什么问题?

展开参考答案与推导

可能泄漏未来标签。修改未来token并检查过去输出是否变化。

02eval输出仍随机,应查什么?

展开参考答案与推导

检查是否直接调用SDPA且仍传非零dropout_p,或其他随机操作未关闭。

03全部键被屏蔽,应该如何处理?

展开参考答案与推导

定义明确行为并检查数据原因,同时避免将该查询计入有效损失。

写代码试一试

01两个相同得分对应值10和20,输出多少?

输入 / 条件
两个键均允许
输出 / 预期
15

提示 权重各一半。

展开参考答案与推导

softmax相同得分均分概率。

02屏蔽第二个值后把它从20改成999,输出应变吗?

输入 / 条件
只允许第一个键
输出 / 预期
不变

提示 屏蔽权重为零。

展开参考答案与推导

这是检查掩码确实生效的直接干预。

03为何exp(score-max)更稳定?

输入 / 条件
score=[1000,1000]
输出 / 预期
指数变为[1,1]

提示 softmax对统一平移不变。

展开参考答案与推导

避免直接exp(1000)溢出,归一化结果仍各为0.5。

读完之后,独立完成

课后作业:自己做一遍

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

这次练什么

手算一行注意力,运行 SDPA 对照,检查首个输出对未来 V(值向量)的梯度为零,并分清不同 API 的布尔掩码含义。

本篇重点加练

归一化轴、padding 广播和全屏蔽策略会直接改变输出与梯度,需加强不同输入表示的等价检查。

  • 按key轴softmax
  • padding掩码广播
  • 全屏蔽行策略

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

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

基础必做

  1. 作业 01代码推演

    Q 为 [2,3]、K 为 [4,3]、V 为 [4,2]。自选小整数填写三者,手算第一行输出并标注缩放、归一化轴及中间形状。

    提交内容
    矩阵与逐步计算
    怎样算完成
    • key 轴定义一致
    • 数值可独立核对

    练到的知识点Q和K与V形状 · 点积缩放sqrt(dk) · 按key轴softmax

  2. 作业 02原理问答

    一个批次包含长度 3 与 5 的序列。为因果限制和 padding 限制分别画可见关系,并解释迁移不同布尔掩码 API 时必须核对什么。

    提交内容
    可见矩阵及接口核对表
    怎样算完成
    • 位置及批次广播明确
    • 布尔语义由契约指定

    练到的知识点因果掩码 · padding掩码广播 · 布尔掩码API差异

  3. 作业 03代码推演

    批次含两个长度分别为二和四的序列,自选小 QKV,分别写 padding 后与去掉 padding 后的每行归一化集合,比较应核验的输出坐标。

    提交内容
    集合表与坐标映射
    怎样算完成
    • 有效位置身份一致
    • 归一化范围可逐行检查

    练到的知识点按key轴softmax · padding掩码广播

综合应用

  1. 作业 04编程实践

    在 CPU 上实现 [B=2,H=2,L=3,S=5] 的掩码注意力测试,包含至少一行全屏蔽;明确该行输出及反向的接口策略并执行校验。

    提交内容
    实现、契约与梯度测试
    怎样算完成
    • 全屏蔽策略自洽
    • 广播覆盖多个头和样本

    练到的知识点因果掩码 · padding掩码广播 · 全屏蔽行策略

  2. 作业 05排错实验

    对固定 QKV 重复运行注意力,分别改变未来位置的值、切换模块 eval、改变显式 dropout 参数;记录随机性与因果性验证结果。

    提交内容
    对照运行与诊断报告
    怎样算完成
    • 单次只改变声明因素
    • 随机与泄漏来源分开

    练到的知识点因果掩码 · dropout参数与eval

  3. 作业 06面试场景

    分类任务中的 padding query 仍产生输出。审阅“key 已屏蔽,所以训练损失天然忽略这些 query”的说法,并设计可以判定它的梯度实验。

    提交内容
    损失契约和实验记录
    怎样算完成
    • query 与 key 角色明确
    • 直接损失与上下文影响分开

    练到的知识点padding掩码广播 · 全屏蔽行策略

  4. 作业 07编程实践

    在 CPU 上同时置换 K、V 及 key 掩码的顺序,核验注意力结果;再只置换其中一项,记录哪些契约被破坏。

    提交内容
    置换测试器及对照结果
    怎样算完成
    • 关联置换完整
    • 比较包含非均匀分数

    练到的知识点按key轴softmax · padding掩码广播

  5. 作业 08设计评审

    某批次所有样本均无有效 key,下一层仍带偏置且损失照常统计。设计从注意力到损失的空数据契约,说明零输出策略能保证与不能保证什么。

    提交内容
    空数据流程及验收表
    怎样算完成
    • 模块输出和训练目标分开
    • 后续偏置影响被审查

    练到的知识点padding掩码广播 · 全屏蔽行策略

  6. 作业 09排错实验

    比较用有限负常数与负无穷屏蔽分数的实现,在不同分数尺度和全屏蔽行上记录输出及梯度;自行设定 CPU 可表示的测试范围。

    提交内容
    尺度实验与策略说明
    怎样算完成
    • 有限值范围明确
    • 异常与策略差异分别记录

    练到的知识点按key轴softmax · 全屏蔽行策略

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

  1. 作业 10设计评审

    比较序列长度 32、64、128 下朴素注意力的中间张量数量与 CPU 时间,报告计时波动,并界定结果对 GPU 高效内核的证明范围。

    提交内容
    代价推导与基准记录
    怎样算完成
    • 二次项来源完整
    • 未测设备不作性能结论

    练到的知识点Q和K与V形状 · 二次注意力成本

思维拓展

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

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

    把全部 key 分成两组,各自计算注意力后简单平均,是否等于对全部 key 一次计算?构造反例,并讨论怎样描述两组有效证据量才足够比较。

    提交内容
    分组反例与信息需求分析
    怎样算完成
    • 分组与全集使用同一数据
    • 不凭某次相等推出恒等

    练到的知识点按key轴softmax · 全屏蔽行策略

  2. 作业 12思维拓展 · 条件变化

    若改用按 query 轴归一化,相同形状仍然合法,这个算子还能表示相同信息混合机制吗?研究一个矩形 Q/K 案例的语义变化。

    提交内容
    矩形算例及算子语义报告
    怎样算完成
    • 归一化对象明确
    • 形状正确不替代语义正确

    练到的知识点按key轴softmax · padding掩码广播

完成后,再检验一次

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

继续阅读

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

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