03|注意力的深入追问:位置、掩码、多头与反向传播
本章目录
本章预备知识
掩码规定哪些位置可见,位置编码帮助区分顺序,多头把表示分组处理;反向传播则追踪最终误差怎样沿原计算回传。它们解决不同问题,不能靠一张架构图一起猜。
本章各题列出所需基础。可先完成相应基础课,也可通过自检确认掌握程度。
进入深读前的小检查:先画3个历史位置后再加2个位置的可见表:新位置3能看0到3,新位置4能看0到4。随后再读非方阵mask与缓存偏移。
术语带虚线时可点击速查。公式中的字母先看定义;不要用猜缩写替代推导。
本章把看起来类似的维度和概念拆开:序列索引、逻辑位置、缓存槽位不是一回事;Q头数、KV头数也不是一回事。理解这些差别,才能解释“shape完全对,生成却错了”的故障。
沿着对象与状态理解本章
先在可见性表里选一个查询行,决定哪些Key位置允许参与;再看如何缩放和混合。多头改变表示的分组方式,位置旋转改变Q/K的比较,反向传播计算参数的影响,三者不是同一个步骤。
下方图解的固定输入在“输入与适用条件”中声明。它用于解释本章机制,不与本章其他独立例子的数值暗中接续。
Mask是可见性表,不是额外语义
怎样防止读到未来,或者把PAD读成内容?
把注意力的分数表看成一张“谁可以读谁”的座位表。行代表接收者,列代表提供者;禁止格子不能参与概率分母。因果限制、PAD限制可以同时存在,但一个负责时间顺序,一个负责有效内容,不能互相替代。
输入与适用条件:前3帧用3个有效位置;第4帧是明确的屏蔽数值对照;第5帧另设past=3、new=2展示缓存偏移。图内始终标出当前条件。
1 / 5 · 处理绝对位置0
窄屏可左右滑动图形,或点击“放大图形”。文字解释紧随图后。
位置不是数字大小比赛。因果规则只允许当前查询读取自己和更早的Key;后面的位置即使已经在训练输入数组中,也不能读。
允许Key位置 ≤ 0默认静止。用“下一步”看完解释再继续;播放到最后一步自动停止。键盘聚焦播放器后,可用左右键逐步查看、空格播放/暂停。图中颜色辅助区分,含义同时用文字注明。
为什么需要
防止训练偷看目标,并排除只为组批存在的填充内容。
怎样实现
先按实际位置和有效长度构造关系,再核对所用API布尔mask语义。
可能的代价与误解
不同API的True可能表示允许或禁止;全行无可见Key还需明确策略,不能直接做普通softmax。
如何判断是否做对
用能暴露未来泄漏的数据,比较正确掩码与故意写错偏移的结果。
Q009|因果mask、padding mask、分块解码mask到底如何构造?
核心回答
因果mask禁止读取未来位置;padding mask排除没有实际内容的补齐位置;分块解码还要把新块的局部索引映射到已有前缀后的全局位置。判断某个键可否被读取,应看逻辑位置和有效性,而不是只画一个左上角三角形。[TR][CACHE]
先看完整输入,再看缓存场景
一个4-Token序列,行是查询位置,列是键位置,1表示允许读取:
k0 k1 k2 k3
q0 1 0 0 0
q1 1 1 0 0
q2 1 1 1 0
q3 1 1 1 1
这里下三角是因为查询与键从相同的全局位置0开始。现在已有3个Token的缓存,新来2个Token。Q长度是2,K长度是5;新查询的全局位置是3与4,正确mask为:
old0 old1 old2 new3 new4
new3 1 1 1 1 0
new4 1 1 1 1 1
通式是key_position ≤ past_length + local_query_index。如果直接创建tril(ones(2,5)),第一行只会看old0,第二行只会看old0与old1,历史的大部分被错误遮住。它仍是一个合法的2×5矩阵,所以不会靠shape检查暴露问题。
再加入padding。两条序列长度分别为3和5,组批时短序列补齐。被补齐的键位置不应获得注意力权重;padding查询位置可能仍被算子计算,但不应作为有效答案或训练目标使用。左padding时,数组下标和真实Token位置更容易分离,位置编号应服从模型与实现约定,不能只用数组下标。
把因果、padding与loss mask画在同一个序列上
输入与条件。批内第二句是 [你,爱,PAD]。三个位置有效性为 [1,1,0];采用右补齐,真实Token位置0、1。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1查询位置0 | 因果限制只许看Key0 | 可见 [1,0,0]。 |
| 2查询位置1 | 可看Key0、1 | 可见 [1,1,0]。 |
| 3PAD查询位置2 | 其输出不用于这句话的预测/训练 | 实现可计算后丢弃,也可有显式跳过策略。 |
| 4训练标签 | 假设只评价前两个有效预测位置 | loss mask是 [1,1,0],它控制计分,不自动控制attention。 |
| 5有缓存时 | 3个历史+2个新Token | 新的注意力表是2×5,行位置为3、4,不是0、1。 |
把过程连起来。三个mask都可能长得像0/1表,但分别限制能读什么、哪些数据是真实输入、哪些位置参与评分。
边界与反例。0/1在这里是本例“可见/不可见”的说明符;具体库的布尔mask可能约定相反。
同一张“允许访问表”,不同接口的布尔值为什么相反
输入与条件。两个Token的因果可见关系是:第0行只能看第0列;第1行可看两列。先定义语义允许表[[真,假],[真,真]]。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1先写语义 | 真表示“本例希望允许访问” | 这个定义独立于任何API。 |
| 2传入scaled_dot_product_attention | 按该接口文档,布尔True表示参与注意力 | 直接传[[True,False],[True,True]]。 |
| 3传入MultiheadAttention的attn_mask | 该接口布尔True表示禁止访问 | 必须传反表[[False,True],[False,False]]。 |
| 4加性掩码表达 | 允许处加0,禁止处加−∞ | 本例为[[0,−∞],[0,0]];仍需核对接口接受的dtype、形状与设备。 |
把过程连起来。不要把另一篇代码里的True直接抄到新接口。先画语义表,再按文档转换,最后做未来Token扰动测试。
边界与反例。此处对照的是所列PyTorch 2.14接口文档。以后版本或其他库必须按其文档重新确认,不能把布尔约定当成通用标准。
追问:训练时一次输入整段答案,不就是偷看答案吗?
不是,只要每个位置的可见范围正确。位置i的隐藏状态只能读到x≤i,拿它预测x(i+1)。标签存在于batch里不等于前向可以读取它。多个位置的预测可同时计算,因为正确前缀已经由训练数据提供。若mask删掉,训练loss可能异常漂亮,却学习了不符合部署条件的任务。
追问:两份文档拼接训练,只放一个EOS就完全隔离了吗?
不一定。EOS是序列内容标记,不自动禁止注意力跨文档边界。若目标是严格隔离样本,需要分段attention mask,并同步处理位置和loss范围。若允许跨文档上下文,则可以有不同策略;关键是明确训练目标,不能把“出现分隔符”当成确定性隔离。
追问:如何写一个能抓住mask错误的测试?
固定权重、关闭dropout,先保存前缀输出;然后只改变未来Token,前缀对应logits不应变化。再比较整段、逐Token和多种分块推理的输出,三者应在合理浮点容差内一致。故意移除mask或重置位置,测试应失败;否则测试很可能没有覆盖目标错误。
Q010|RoPE如何让注意力知道位置?为什么缓存位置不能每轮从零开始?
核心回答
RoPE把Q/K每一对分量按位置相关角度旋转,使两位置旋转后点积包含相对位置差。它不是简单把位置整数加到Token ID上。缓存推理必须保持新旧Token的逻辑位置连续或符合指定位置策略,否则相对关系被改变。[ROPE]
从二维旋转推导相对位置
对二维向量x=[a,b],位置p与频率θ对应:
R(pθ) x = [a cos(pθ) − b sin(pθ), a sin(pθ) + b cos(pθ)]
Q在位置p、K在位置t时,点积为(R(pθ)q)ᵀ(R(tθ)k)。利用R(pθ)ᵀ=R(-pθ)与旋转的组合性质,得到qᵀR((t-p)θ)k。因此位置信息通过差t-p进入相关性。高维向量按分量对应用不同频率,提供多尺度位置变化。
这并不意味着注意力只依赖位置差:q与k本身仍由文本内容和前层表示决定。RoPE提供相对位置结构,内容匹配仍由学习得到的投影负责。也不能说它让模型对任意长度天然泛化;超过训练分布的相位组合仍可能带来退化。
假设前缀长度P=100,新Token本应在位置100。如果错误地把它的Q/K按位置0旋转,而旧K仍按0到99存储,那么模型读到的“距离”已不是真实距离。张量长度、dtype、缓存拼接轴都可能正确,但logits已经偏离整段前向。
相对位置保持与缓存错位:同一组旋转算两遍
输入与条件。用二维旋转R(θ),q=k=[1,0]。设位置m的角度是m×90°,只用于展示相对位置性质。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1比较位置0与1 | q0=[1,0],k1=[0,1] | 点积0。 |
| 2两者整体后移1位 | q1=[0,1],k2=[−1,0] | 点积仍为0。 |
| 3比较同位置 | q1与k1 | 点积1,说明相对差0与差1不同。 |
| 4缓存正确处理 | 新Token继续采用绝对位置2 | 位置不因为进入新调用而重置。 |
| 5重置反例 | 把位置2误当0 | k2误成[1,0],与位置0的比较从−1变1。 |
把过程连起来。固定频率旋转中共同平移不改变相对角度;错误重置位置则改变模型实际计算。
边界与反例。真实RoPE多坐标对、频率与扩展策略需按模型实现;示例不代表所有模型采用90°步长。
依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。
追问:可以把全部位置同时平移吗?
在理想纯RoPE点积中,同时给Q/K位置加同一偏移,会保留相对差。但完整模型可能还有其他位置相关机制、滑窗规则、特殊位置或缩放策略;已旋转的缓存也不能只改一个position counter就完成一致变换。数学上的整体平移性质不能直接当任意缓存截断实现的安全证明。
追问:长上下文扩展是怎么做的?
常见思路包括位置插值、频率缩放、特定频段调整和长序列继续训练。它们试图让超出原训练长度的位置模式更接近可学习范围,同时保留局部区分。局部精度与长距离覆盖可能发生取舍,因此需测检索、跨段组合、代码定位等真实任务,不只测“接口是否接受更多Token”。
追问:为什么通常旋转Q/K而不是V?
这种设计把相对位置信号主要放到读取权重的计算里,Value仍提供被混合的内容。不同架构可有其他方案;不能把这个实现选择扩展成数学上所有模型都必须遵守的定理。阅读源码时应检查缓存里保存的是旋转前还是旋转后的K,以及重用时是否重复旋转。
Q011|MHA、MQA、GQA到底少了什么?是不是把多头删掉了?
核心回答
MHA通常每个Q头有对应K/V头;MQA让多个Q头共享一组K/V;GQA在两者之间,让一组Q头共享一个K/V头。主要减少K/V投影输出和缓存量,不等于所有查询头变成同一注意力分布。[GQA][CACHE]
逐形状比较
设隐藏维度4096,查询头H=32,每头Dh=128。MHA有32个KV头;GQA假设只有8个KV头;MQA只有1个。忽略bias时:
| 对象 | MHA | GQA(8 KV头) | MQA |
|---|---|---|---|
| Wq输出宽度 | 4096 | 4096 | 4096 |
| Wk/Wv各自输出宽度 | 4096 | 1024 | 128 |
| 单层K缓存 | [B,32,T,128] |
[B,8,T,128] |
[B,1,T,128] |
| 每个Q头的注意力权重 | 各自计算 | 各自计算 | 各自计算 |
GQA中可以把Q头0—3分给KV头0、Q头4—7分给KV头1,以此类推。共享K不意味着Q相同;不同Q与同一K点积,仍可产生不同权重。共享V也不意味着输出相同,因为加权比例不同。
缓存空间从32组降为8组,理论上这部分减少为四分之一。但总显存还有权重、激活、工作空间和调度元数据,不会因此整机显存必然减少四倍。QKᵀ和AV仍需服务32个Q头;实际计算效率还依赖kernel如何避免重复加载共享KV。
保留4个查询头,但只缓存2组Key/Value
输入与条件。设B=1、T=3、查询头Hq=4、每头Dh=2。MHA有4个KV头,GQA设2个,MQA设1个。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1MHA | K和V各有 1×4×3×2=24 个数 | 两份共48数。 |
| 2GQA | K和V各有 1×2×3×2=12 个数 | 共24数;查询头仍4个。 |
| 3设分组 | Q头0、1读KV组0;Q头2、3读KV组1 | 每个Q有自己的查询向量,不是删掉两个Q头。 |
| 4MQA | 所有4个Q共用1组KV | 共12数,但每个Q算出的读取份额仍可不同。 |
| 5投影宽度 | 模型宽度8时,GQA的K投影可输出4维 | Q投影仍8维;最终残差宽度不随KV组数自动减半。 |
把过程连起来。GQA主要减少K/V投影与状态规模,不等于把注意力的查询头数一起删除。
边界与反例。这是常见均匀分组配置;具体模型与内核布局可不同,已有MHA权重不能不经适配直接当GQA使用。
依据与适用范围:[DERIVE]。示例输入和场景由本书明确设定。
4个Q头共享2个KV头,究竟怎样配对
输入与条件。设Q头数4、KV头数2,每头宽度2。采用连续分组:Q0/Q1用KV0,Q2/Q3用KV1。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1写清映射 | Q0→KV0,Q1→KV0,Q2→KV1,Q3→KV1 | 共享的是对应的K/V,而不是把四个Query平均。 |
| 2保持Query独立 | Q0=[1,0]、Q1=[0,1],同一键K0=[2,3] | 点积分别为2和3,即使共享键也可能得到不同注意力权重。 |
| 3计算本体 | 每Token的K和V各有2头×2数 | 共2×2×2=8个数;若4个KV头则16个数。 |
| 4还原输出 | 每个Q头各输出2个数,再按头拼接 | 得到8个数,之后由输出投影映射回模型的隐藏宽度。 |
把过程连起来。GQA减少KV存储,但并不把输出头数降成KV头数。要分别跟踪Q头、KV头和输出宽度。
边界与反例。组映射、头维度和输出投影依架构决定;本例明确采用连续分组,不讨论某一闭源模型的具体实现。
依据与适用范围:[SDPA]。示例输入和场景由本书明确设定。
追问:能否把一个训练好的MHA模型直接改配置成GQA?
不能只改头数。参数形状、权重组合和学习到的功能会变化。GQA论文研究了从多头检查点转换并继续训练的方式;这不是修改一个配置就获得等价模型。模型部署必须匹配权重与架构。
追问:为什么有的GQA代码还把KV重复成H个头?
这可能是为了兼容普通attention接口而做概念展开。若物理复制长缓存,会抵消内存收益;高效实现应通过映射、视图或专用kernel共享数据。阅读代码时要看实际内存分配与stride,而不是只看注释写了“共享”。
追问:MQA缓存最小,为什么不全部用MQA?
共享程度提高会改变表示容量与模型质量,硬件收益也受工作负载影响。GQA是在质量、训练成本与推理资源之间的一种折中。不存在不看模型与任务就永远最优的KV头数。
Q012|Attention如何反向传播?为什么自动求导正常不等于模型实现正确?
核心回答
反向传播把输出误差沿Value内容路径和Q/K权重路径传回。自动求导能忠实计算当前计算图的梯度,却不能知道你的mask、标签或维度是否表达了正确任务。必须把数学梯度检查和语义不变性测试分开。[TR]
沿计算图逐步求导
定义S=QKᵀ/√d、A=softmax(S)、O=AV,设上游梯度G=∂L/∂O。先对最后一步矩阵乘法:
dV = AᵀG
dA = GVᵀ
第一个式子表示某历史Value从所有读取它的查询位置累计误差;第二个式子衡量某个读取权重改变时损失如何变化。接着对softmax,某行概率a与上游g对应:
ds = a ⊙ (g − sum(a ⊙ g))
这里减的是整行加权和,不能省略。其结果各分量之和为0,与“给整行分数加常数不改变softmax”的性质一致。最后:
dQ = dS K / √d
dK = dSᵀ Q / √d
若Q/K/V由同一X投影而来,X梯度是三条支路分别乘投影转置后的和,再加上残差直接回传的梯度。多头的换轴、拼接在反向中要走对应的逆变换。
一个有用的诊断:只让第一位置输出参与损失,在因果mask下,未来K/V不应通过这一损失得到梯度。如果未来位置有非零依赖,可能mask或索引错误。这个测试关注任务语义,不只是数值求导。
对一个查询手算Attention的梯度,再用扰动检查
输入与条件。Q=[[0.4,−0.2]];K=[[1,0.5],[−0.5,1]];V=[[2,−1],[0.5,3]];两个Key都可见。上游梯度G=[[0.7,−0.3]],定义标量损失L=sum(O⊙G)。
| 步骤与对象 | 内部动作 | 中间结果 / 新状态 |
|---|---|---|
| 1前向 | 算缩放点积、softmax与混合 | P约 [0.621278,0.378722],O约 [1.431916,0.514889]。 |
| 2内容路径 | dV=PᵀG | 两行约 [0.434894,−0.186383]、[0.265106,−0.113617]。 |
| 3份额路径 | dP=GVᵀ | 得到 [1.7,−0.55]。 |
| 4传过softmax | dS=P⊙(dP−sum(P⊙dP)) | 约 [0.529406,−0.529406],和为0。 |
| 5传到Q/K | dQ=dS K/√2,dK=dSᵀ Q/√2 | dQ约 [0.561520,−0.187173]。 |
| 6数值检查 | 对Q/K/V的10个坐标逐个±1e−6 | 最大绝对差约1.90e−10,核验当前公式导数一致。 |
把过程连起来。梯度分成“改读取份额”和“改被读取内容”两条路径。只验证dV不能证明Q/K学习正确。
边界与反例。有限差分能核对本例前向的导数,不能单独证明标签或mask符合任务;因此还需独立的因果反例测试。
依据与适用范围:[SDPA]。示例输入和场景由本书明确设定。
追问:有限差分怎样检查梯度?
对某输入维度j,把它分别改为xj+ε和xj-ε,用[L(x+εej)-L(x-εej)]/(2ε)近似导数。小矩阵用float64更容易定位错误。ε太大会有截断误差,太小则舍入误差占主导;不能仅通过不断放宽容差让测试通过。
追问:有限差分通过,是否证明模型对了?
它证明解析或自动梯度与当前前向函数一致;如果前向错误地允许看未来,两者仍可能一致。还需要标签移动、因果性、整段与缓存一致性等测试。因此“梯度正确”和“目标正确”是不同证据。
追问:推理时为什么不需要反向传播?
普通部署生成使用固定参数,只计算条件分布并选择Token,不需要计算损失和更新权重。长推理或多次工具调用增加的是前向计算与输入信息,不自动变成梯度训练。特定在线学习系统可以额外更新参数,但必须显式设计,不能从“模型越来越了解当前任务”推断发生了训练。