掩码是在 softmax 之前往注意力分数上加 ,把不该被 attend 的位置概率压成零——padding、因果、滑动窗口只是同一机制的不同先验。
统一视角:掩码即结构先验
注意力机制 的默认假设是”人人可见”:任意 query 与全部 key 计算相似度,softmax 归一化后得到一个满秩的注意力分布。但真实任务几乎从不需要这种全连接——批内的 <pad> 没有语义,自回归生成不能看未来,长序列不必看全局。
掩码就是把这些约束写进分数矩阵:
中允许的位置为 ,屏蔽的位置为 。因为 ,softmax 之后这些位置权重严格为 0,且不参与分母归一化——被屏蔽的 key 对输出没有任何贡献,梯度也不会回传过去。工程实现里常用 或 torch.finfo(dtype).min 代替 ,避免 fp16 下 inf - inf 产生 NaN。
关键在于:掩码是加性的、作用在 softmax 之前的。若在 softmax 之后置零,权重和不再为 1,等价于对输出做了未定义的缩放。
padding mask
批处理要求同一 batch 内序列等长,短序列右侧补 <pad>。这些位置的 embedding 是无意义的占位符,如果参与注意力,会分走真实 token 的概率质量——序列越短、pad 越多,语义被稀释得越厉害。
padding mask 由 attention_mask 布尔向量(1 表示真实 token,0 表示 pad)广播成矩阵,屏蔽的是列:任何 query 都不能 attend 到 pad 的 key。行方向通常不管——pad 位置的输出是垃圾,但后续 loss 计算会忽略它们。
需要注意的是全 pad 行:若某个 query 的所有 key 都被屏蔽,softmax 分母为 0,输出 NaN。因果掩码下第一行永远保留对角元,天然安全;但左 padding(推理常用)叠加因果掩码时可能出现整行屏蔽,需要额外兜底。
causal mask
因果掩码保证位置 的输出仅依赖 ,其形态是下三角:
0 & -\infty & -\infty & -\infty \\ 0 & 0 & -\infty & -\infty \\ 0 & 0 & 0 & -\infty \\ 0 & 0 & 0 & 0 \\ \end{bmatrix}$$ 这解决了自回归训练的核心矛盾:teacher forcing 一次性把整条目标序列喂进模型,并行计算所有位置的 loss,但每个位置又必须只看见它的前缀。下三角掩码让**一次前向等价于 $T$ 次因果预测**,训练效率与推理语义得以兼得。少了这层约束,模型会直接抄下一个 token,loss 迅速趋零而生成时完全失效。 核心逻辑:**过去可追溯,未来不可见**。 与 [[位置编码]] 对照看更清楚——两者都是让置换不变的自注意力适配序列建模的结构先验,一个注入顺序信息,一个切断未来信息。 ## 组合使用 LLM 训练中两类掩码同时生效,按位取交(允许集合的交集,等价于屏蔽集合的并集): ```python import torch def build_mask(attention_mask: torch.Tensor) -> torch.Tensor: """attention_mask: (B, T),1 为真实 token,0 为 pad。返回 (B, 1, T, T) 加性掩码。""" B, T = attention_mask.shape causal = torch.tril(torch.ones(T, T, dtype=torch.bool, device=attention_mask.device)) pad = attention_mask.bool()[:, None, :] # (B, 1, T) 屏蔽 pad 列 allow = causal[None] & pad # (B, T, T) 两者都允许才允许 additive = torch.zeros(B, T, T, device=attention_mask.device) return additive.masked_fill(~allow, torch.finfo(additive.dtype).min)[:, None] am = torch.tensor([[1, 1, 1, 1], [1, 1, 0, 0]]) # 第二条序列后两位是 pad mask = build_mask(am) print(mask.shape) # torch.Size([2, 1, 4, 4]) print(mask[1, 0] == 0) # 后两列全被屏蔽;第 1 行只剩 k1,第 2 行起可见 k1、k2 ``` 推理阶段的形态则完全不同。开启 KV Cache 后每步只有一个新 query,它天然只能看到缓存中已有的 key——**因果性由"未来的 key 还不存在"隐式保证,不需要显式掩码矩阵**。这也是 [[KV Cache]] 中 prefill 与 decode 两阶段掩码逻辑不对称的原因:prefill 用完整下三角,decode 用全 1 行向量。同理,[[FlashAttention]] 从不物化 $T\times T$ 掩码矩阵,而是在分块循环中跳过完全被屏蔽的 block,把因果性变成调度层面的剪枝。 ## 变体与延伸 | 掩码类型 | 屏蔽对象 | 典型场景 | |----------|----------|----------| | padding mask | 批内 `<pad>` 列 | 变长序列批训练 | | causal mask | 当前位置之后的全部 key | 自回归生成、Decoder-only LLM | | 无掩码(双向) | 不屏蔽 | Encoder 理解任务:分类、检索、BERT 式预训练 | | 滑动窗口 | 窗口外的历史 | 长上下文降本,Mistral、Longformer | | block / span mask | 段落或文档边界外 | 多文档打包训练、prefix-LM | 滑动窗口是因果掩码的进一步收紧——保留最近 $w$ 个 key,其余屏蔽,把复杂度从 $O(T^2)$ 降到 $O(Tw)$,也是 [[稀疏注意力]] 各类方案的共同起点。 值得单独一提的是 [[ALiBi]]:它给注意力分数加的不是 $\{0,-\infty\}$ 二值矩阵,而是与相对距离成正比的负偏置 $-m\cdot|i-j|$。这可以理解为**连续版掩码**——远处不是硬性禁止,而是按距离渐进衰减;硬掩码是它在斜率趋于无穷时的极限。同一套"加性偏置改写注意力分布"的接口,既能表达硬约束,也能表达软先验。