这篇重点学什么
本篇重点:先核对矩阵各维的大小,再检查不同框架 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。
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"]按顺序看,每一步做了什么
- 逐项标出Q、K、V及得分形状。
- 计算缩放得分并沿key轴归一化。
- 明确True表示允许还是禁止,验证全屏蔽边界。
- 用未来扰动和梯度测试核对因果性。
03对照实现代码
PyTorch v2.9.1 · scaled_dot_product_attention;torch._C._nn.scaled_dot_product_attention
原创无dropout、单头参考模型,显式定义全屏蔽行为;不是FlashAttention或PyTorch融合内核源码。
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行为取决于参数,调用方要根据模块训练状态传入恰当概率。
不同设备与形状可能选不同内核;参考模型用于正确性对照,不据此推断显存或吞吐。
代码先调用谁,再调用谁
- 准备Q
/K/V和掩码 - SDPA参数处理
- 后端选择注意力实现
- 得分归一化与值汇总
- 反向传播
04动手写一个例子
本段仍从单个查询计算注意力,但示例规则有所不同:可以逐个指定哪些位置可见,V 中每个位置是一个数(标量),全部屏蔽时返回 0。入门篇只允许查看一段连续前缀,V 中每个位置是向量,没有可见位置时会拒绝计算。这些是两段示例各自的接口约定,不能当作所有框架的统一行为。之后还要学习各框架怎样解释掩码、dropout、梯度实验,以及下方的分块在线 softmax。
基础主读:Transformer 与注意力入门 ↗
复习已学内容:需要时展开讲解和代码
基础主读:Transformer 与注意力入门 ↗
带允许掩码的单查询稳定注意力
输入一个d维查询、S个d维键、S个标量值和S个允许标记,计算缩放点积权重及标量输出。全屏蔽明确返回0;非有限得分或错误形状拒绝。
- 输入 1
q全0,两个值10和20,均允许- 输出 1
15
- 输入 2
只允许第一个键- 输出 2
10
计算每个点积再除以sqrt(d),只在允许集合寻找最大值。
指数使用score-max,使最大指数为1,避免普通大得分直接exp溢出。
先计算归一化分母再求加权输出,全屏蔽在归一化之前单独处理。
为什么这样写是对的至少一个位置允许时,权重非负、屏蔽权重为零且总和约为一,输出位于允许值的凸包内。
点积扫描S个d维键,额外保存每个得分;标量汇总为O(S)。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
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掩码广播
- 全屏蔽行策略
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01代码推演
Q 为 [2,3]、K 为 [4,3]、V 为 [4,2]。自选小整数填写三者,手算第一行输出并标注缩放、归一化轴及中间形状。
- 提交内容
- 矩阵与逐步计算
- 怎样算完成
- key 轴定义一致
- 数值可独立核对
练到的知识点Q和K与V形状 · 点积缩放sqrt(dk) · 按key轴softmax
- 作业 02原理问答
一个批次包含长度 3 与 5 的序列。为因果限制和 padding 限制分别画可见关系,并解释迁移不同布尔掩码 API 时必须核对什么。
- 提交内容
- 可见矩阵及接口核对表
- 怎样算完成
- 位置及批次广播明确
- 布尔语义由契约指定
练到的知识点因果掩码 · padding掩码广播 · 布尔掩码API差异
- 作业 03代码推演
批次含两个长度分别为二和四的序列,自选小 QKV,分别写 padding 后与去掉 padding 后的每行归一化集合,比较应核验的输出坐标。
- 提交内容
- 集合表与坐标映射
- 怎样算完成
- 有效位置身份一致
- 归一化范围可逐行检查
练到的知识点按key轴softmax · padding掩码广播
综合应用
- 作业 04编程实践
在 CPU 上实现 [B=2,H=2,L=3,S=5] 的掩码注意力测试,包含至少一行全屏蔽;明确该行输出及反向的接口策略并执行校验。
- 提交内容
- 实现、契约与梯度测试
- 怎样算完成
- 全屏蔽策略自洽
- 广播覆盖多个头和样本
练到的知识点因果掩码 · padding掩码广播 · 全屏蔽行策略
- 作业 05排错实验
对固定 QKV 重复运行注意力,分别改变未来位置的值、切换模块 eval、改变显式 dropout 参数;记录随机性与因果性验证结果。
- 提交内容
- 对照运行与诊断报告
- 怎样算完成
- 单次只改变声明因素
- 随机与泄漏来源分开
练到的知识点因果掩码 · dropout参数与eval
- 作业 06面试场景
分类任务中的 padding query 仍产生输出。审阅“key 已屏蔽,所以训练损失天然忽略这些 query”的说法,并设计可以判定它的梯度实验。
- 提交内容
- 损失契约和实验记录
- 怎样算完成
- query 与 key 角色明确
- 直接损失与上下文影响分开
练到的知识点padding掩码广播 · 全屏蔽行策略
- 作业 07编程实践
在 CPU 上同时置换 K、V 及 key 掩码的顺序,核验注意力结果;再只置换其中一项,记录哪些契约被破坏。
- 提交内容
- 置换测试器及对照结果
- 怎样算完成
- 关联置换完整
- 比较包含非均匀分数
练到的知识点按key轴softmax · padding掩码广播
- 作业 08设计评审
某批次所有样本均无有效 key,下一层仍带偏置且损失照常统计。设计从注意力到损失的空数据契约,说明零输出策略能保证与不能保证什么。
- 提交内容
- 空数据流程及验收表
- 怎样算完成
- 模块输出和训练目标分开
- 后续偏置影响被审查
练到的知识点padding掩码广播 · 全屏蔽行策略
- 作业 09排错实验
比较用有限负常数与负无穷屏蔽分数的实现,在不同分数尺度和全屏蔽行上记录输出及梯度;自行设定 CPU 可表示的测试范围。
- 提交内容
- 尺度实验与策略说明
- 怎样算完成
- 有限值范围明确
- 异常与策略差异分别记录
练到的知识点按key轴softmax · 全屏蔽行策略
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
比较序列长度 32、64、128 下朴素注意力的中间张量数量与 CPU 时间,报告计时波动,并界定结果对 GPU 高效内核的证明范围。
- 提交内容
- 代价推导与基准记录
- 怎样算完成
- 二次项来源完整
- 未测设备不作性能结论
练到的知识点Q和K与V形状 · 二次注意力成本
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 反例构造
把全部 key 分成两组,各自计算注意力后简单平均,是否等于对全部 key 一次计算?构造反例,并讨论怎样描述两组有效证据量才足够比较。
- 提交内容
- 分组反例与信息需求分析
- 怎样算完成
- 分组与全集使用同一数据
- 不凭某次相等推出恒等
练到的知识点按key轴softmax · 全屏蔽行策略
- 作业 12思维拓展 · 条件变化
若改用按 query 轴归一化,相同形状仍然合法,这个算子还能表示相同信息混合机制吗?研究一个矩形 Q
/K案例的语义变化。- 提交内容
- 矩形算例及算子语义报告
- 怎样算完成
- 归一化对象明确
- 形状正确不替代语义正确
练到的知识点按key轴softmax · padding掩码广播
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
Attention Is All You Need (新标签页)↗
阅读缩放点积公式与因果Decoder约束。
- 深入参考文档英文
PyTorch2.9.1 SDPA源码接口 (新标签页)↗
核对布尔掩码、缩放和dropout语义。
- 深入参考文档英文
PyTorch2.9 MultiheadAttention (新标签页)↗
对照不同接口的布尔掩码方向。