SDPA Dropout:eval后为何还随机

黎 浩然/ 16 10 月, 2026/ 大语言模型/LARGELANGUAGEMODEL/LLM, 教程/TUTORIAL, 计算机/COMPUTER/ 0 comments

自定义注意力层切到eval后,同一输入仍然产生变化,先看传给scaled_dot_product_attention的dropout_p。这个函数按传入概率执行Dropout,不会因为外层模型调用了eval,就自动把一个写死的非零概率改成零。no_grad和inference_mode也不能替你完成这一步。

文章目录
  1. 三种控制不要混成一个开关
  2. 为什么nn.Dropout的经验会误导
  3. 从两个权重推导:期望相同不代表输出相同
  4. 穷举验证,不用随机数碰运气
  5. Dropout后的权重和为什么不一定为1
  6. 固定随机种子为什么可能把问题藏起来
  7. 关闭Dropout后,如何继续缩小差异来源
  8. 把推理配置写到可以核对的地方
  9. 参考资料

排查重点是实际调用参数,而不是再多套一层“推理模式”。下面用两个注意力权重的精确枚举,解释为什么随机结果均值正确,单次输出却仍然会变。Read in English。

三种控制不要混成一个开关

PyTorch 2.14的SDPA参考明确提醒:该函数依据dropout_p应用Dropout,评估时要显式传0.0。自定义模块通常在调用处使用self.p if self.training else 0.0,把模块状态转成函数参数。

Autograd说明则将eval与no-grad、inference mode区分开。eval影响依赖训练状态的模块行为;后两者控制梯度跟踪及相关开销。是否记录梯度,与是否在前向中抽取随机掩码,是不同问题。

所以先检查两件事:所属模块是否进入预期模式,SDPA最终收到的概率是多少。仅打印model.training,并不能证明每条自定义调用路径都传了零。

1
模块模式
eval改变self.training
2
梯度记录
no_grad / inference_mode
3
SDPA参数
显式传dropout_p
4
输出比较
再检查输入、精度与后端
原创控制路径图:模块模式与梯度记录不是同一个开关;调用SDPA时仍需确定实际概率。

为什么nn.Dropout的经验会误导

习惯使用nn.Dropout时,调用eval之后该模块成为恒等映射,这与模块文档一致。但在自定义forward里直接调用函数,必须遵守函数自己的参数语义。不能从模块的便捷行为推导出所有带Dropout的函数都会读取父模块状态。

这类问题常出现在把手写注意力替换为SDPA时:保留了配置中的dropout概率,却漏掉训练状态分支。优化前后的计算路径看似相同,推理路径却额外保留了随机掩码。修复应落在调用边界,而不是修改权重文件或把训练配置永久清零。

如果需要训练时保留正则化、评估时关闭,那么保存配置p并在调用时选择有效概率即可。把p永久改为零,虽然可能让推理稳定,却也会悄悄改变后续训练的意图。

从两个权重推导:期望相同不代表输出相同

固定softmax后的权重a₁=a₂=1/2,对应标量值v₁=2、v₂=4。不使用Dropout时,输出y=Σaᵢvᵢ=3。这里Q、K和值都固定,不讨论生成采样,也不引入模型训练。

取丢弃概率p=1/2,每个位置独立保留。令mᵢ为0或1,保留概率1-p;倒置Dropout使用ãᵢ=aᵢmᵢ/(1-p)。在0≤p<1的前提下,E[ãᵢ]=aᵢ,对固定值做线性加权时,E[ỹ]=y。这里没有处理p=1的全丢弃边界,不能让分母直接变成零。

四种掩码00、10、01、11等概率,输出依次为0、2、4、6。它们平均为3,却没有一个等于3。独立掩码下还有Var(ỹ)=p/(1-p)·Σ(aᵢvᵢ)²;本例等于1²+2²=5,与枚举一致。这个小例子直接说明:期望保持,并不意味着每次前向等于关闭Dropout的前向,更不意味着多次输出必须相同。

00 → 0

10 → 2

01 → 4

11 → 6

原创精确枚举图:p=1/2,四种掩码各概率1/4,输出为0、2、4、6;均值为3,方差为5。不是模型实测。

穷举验证,不用随机数碰运气

下面只使用Python标准库,以分数做精确计算,遍历全部四种掩码;它验证的是数学构造。当前环境没有安装PyTorch,因此没有运行SDPA、神经网络或GPU内核,API行为依据官方文档。代码最后的effective_p是纯Python策略示例,并不是调用真实模块后的状态检测。

# Exact arithmetic for a teaching example; not a PyTorch execution.
from fractions import Fraction as F
from itertools import product

weights, values = [F(1,2),F(1,2)], [F(2),F(4)]
p = F(1,2)
base = sum(a*v for a,v in zip(weights,values))
outputs, sums = [], []
for keep in product([0,1],repeat=2):
    dropped = [a*m/(1-p) for a,m in zip(weights,keep)]
    outputs.append(sum(a*v for a,v in zip(dropped,values)))
    sums.append(sum(dropped))
mean = sum(outputs)/len(outputs)  # all four masks equally likely here
variance = sum((x-mean)**2 for x in outputs)/len(outputs)
assert base == 3
assert sorted(outputs) == [0,2,4,6]
assert mean == base
assert variance == 5
assert sorted(sums) == [0,1,1,2]
assert sum(sums)/len(sums) == 1
# Explicit evaluation policy: p must become zero at the call boundary.
def effective_p(training, configured):
    return configured if training else F(0)
assert effective_p(False,p)==0
assert effective_p(True,p)==p
print("8 exact checks passed; mean=3, variance=5")

本地执行通过八项断言,包括基准输出、全部可能输出、均值、方差、权重和的可能值及均值,以及训练和评估两条有效概率分支。四种掩码等概率只适用于这里p=1/2的设置;不能把同一个简单平均公式直接套到其他p。

Dropout后的权重和为什么不一定为1

softmax输出的每一行和为1,不代表随后Dropout得到的每一行仍然和为1。本例的权重和可以是0、1、1、2,只有期望为1。保留项乘1/(1-p)是期望补偿,不是重新归一化。

如果调试时为了“修正概率分布”,把Dropout后的非零权重再次除以行和,就改变了原运算;遇到全零行还要另外处理除零。不要把注意力权重在不同处理阶段的性质混在一起,再用错误的不变量判断实现有Bug。

同样,E[ỹ]=y只针对这里固定值的线性加权阶段。输出再经过非线性、归一化或其他随机运算后,一般不能把期望移进函数,声称整个网络的平均预测等于关闭Dropout的预测。

固定随机种子为什么可能把问题藏起来

如果每次调用前重置相同随机状态,错误保留的Dropout也可能复现相同掩码,从而让两次输出看起来一致。这个现象不能证明推理路径没有Dropout,只说明特定随机过程被重复。

反过来,只设置一次种子也不要求连续两次调用使用同一掩码,因为随机状态会被消耗。把“两次输出相等”当作唯一断言既可能漏报,也可能受数据构造影响:某些值或对称结构会让不同掩码得到相同输出。

更直接的测试是检查评估调用确实传入0.0,再对固定输入比较预期结果。训练路径则单独验证非零概率仍被保留。参数流向与数学反例结合,比反复更换种子更容易定位问题。

关闭Dropout后,如何继续缩小差异来源

把比较分成层级:先固定同一份Q、K、V与mask,排除输入预处理;再核对有效概率和dtype;最后比较同一后端下的局部输出。不要一开始就比较完整生成文本,采样与累积误差会把局部原因混在一起。

关闭Dropout也不等于跨设备、后端和精度逐位一致。官方SDPA参考说明不同实现存在数值差异;合理容差应来自实际精度与任务要求。若切换内核后出现微小偏差,应与这里非零概率造成的随机变化分开分析。

昨天的KV cache因果掩码文章解决“哪些位置能看见”的问题。本篇解决“被允许的权重是否又被随机丢弃”。两类问题可以同时存在,修掉其中一项并不自动验证另一项。

把推理配置写到可以核对的地方

最小的工程改动通常是:保留训练配置,评估路径显式选择零概率,并在调用处留下可检查的有效配置。回归测试分别覆盖训练与评估,避免未来重构把self.training分支再次丢掉。

记录框架版本、输入形状、dtype、有效dropout概率、mask设置与所选后端。输出不同先比较这些条件;只有条件对应,数值对照才有解释力。本文不提供未执行的模型性能或稳定性测评。

如果这个推导帮你分清了“没有梯度”和“没有随机丢弃”,或缩短了一次推理差异排查,欢迎通过文末支持区自愿支持。

参考资料

依据PyTorch 2.14 SDPA参考、Dropout模块参考和Autograd机制说明,于2026年10月16日核对。两幅图与精确枚举示例为原创;本地验证限于标准库数学构造。

支持

如果文中的解释或示例帮你解决了问题、节省了时间,欢迎自愿打赏支持本站。

微信支持二维码,点击查看大图
微信
支付宝支持二维码,点击查看大图
支付宝
Buy Me a Coffee,点击支持本站
Buy Me a Coffee

二维码可点击放大。更多支持方式见支持页面。

Share this Post

Leave a Comment

您的邮箱地址不会被公开。 必填项已用 * 标注

*
*