PyTorch推理工程:03 train、eval、no_grad 与 inference_mode
PyTorch 推理工程(03):train、eval、no_grad 与 inference_mode
1. 本节定位
常见推理脚本同时包含 model.eval() 与 torch.inference_mode()(或 no_grad()):
1 | model.eval() |
二者作用于不同子系统:eval() 调整部分模块(如 BatchNorm、Dropout)的行为;inference_mode() / no_grad() 控制是否构建 autograd 图。仅设置其一通常不足以同时满足数值行为与开销两方面的预期,混用不当也会出现与视图、原地操作相关的报错。
2. 模块模式与 autograd 模式(分类图)
在展开讲之前,先把这 4 个东西的关系说清楚。
它们其实分属两个完全独立的系统:
1 | ┌─────────────────────────────────────────────────────┐ |
这两个维度互不干涉:
eval()不会关闭梯度记录no_grad()不会让 Dropout 停下来
两维相互独立:仅 eval() 不能关闭 autograd;仅 no_grad() 不能改变 Dropout/BatchNorm 等行为。典型推理路径需同时设置模块模式与 autograd 模式。
后文均围绕下列关系展开:
train()/eval()控制的是模块行为模式;no_grad()/inference_mode()控制的是 autograd 记录行为。两者是独立的,不能互相替代。
3. 背景:PyTorch 训练时在做什么额外的事
训练前向与计算图
训练分为前向与反向两阶段:前向计算 loss;反向沿计算图求梯度并更新参数。
在默认梯度模式下,前向过程会同步构建供 .backward() 使用的计算图:
具体来说,它会在内存里构建一张计算图(computation graph),记录:
- 每一步做了什么操作
- 每个中间结果从哪里来
- 这些信息是为了后面
.backward()时能顺着图求导
1 | 前向:x → Linear → ReLU → Linear → loss |
推理与 autograd
推理仅需 x → 模型 → y,不求梯度、不更新权重。若仍启用默认梯度模式,将承担构图带来的内存与开销;故推理路径中通常使用 torch.no_grad() 或 torch.inference_mode() 关闭记录。
1 | with torch.no_grad(): |
4. train() 和 eval():控制模块行为
train() / eval() 切换模块的训练/评估状态;对 Linear、ReLU、LayerNorm 等多数算子无行为差异。
只有少数层在两种模式下行为不同,主要是:
- Dropout
- BatchNorm
下面分别说说。
5. Dropout:train/eval 下行为差异
Dropout 的原理是:训练时随机"关掉"一部分神经元(把它们的输出置零),强迫模型不依赖特定的某几个神经元,起到正则化的效果。
但推理时不需要这个随机性:
1 | import torch |
注意:训练模式下非零位置的值是
2.0,不是1.0。这是 Dropout 的"缩放补偿":为了让期望值一致,存活的元素会除以(1-p),这里1/(1-0.5)=2。
结论:含 Dropout 的模型在推理前应调用 model.eval(),否则同一输入可能因随机掩码而得到不同输出。
6. BatchNorm:train/eval 下行为差异
BatchNorm 相对复杂,但推理岗位必须理解。
它做了什么
BatchNorm 对每个 batch 的输入做归一化:把均值调成 0,方差调成 1,然后用可学习的参数 γ(gamma,weight)和 β(beta,bias)还原尺度。
训练时和推理时的区别
| 阶段 | 用什么统计量归一化 | 会不会更新 running 统计量 |
|---|---|---|
| 训练 | 当前 batch 自己的均值/方差 | 会,每个 batch 都更新 |
| 推理 | 训练时积累的 running_mean / running_var |
不会 |
1 | import torch |
结论:如果推理时忘了 model.eval(),BatchNorm 会用当前 batch(可能只有 1 条数据)的统计量做归一化,而不是训练时学到的统计量,结果往往是错的。
这是"模型能跑但推理结果不对"的经典原因之一。
7. eval() 只影响模块行为,不影响梯度
这是最容易误解的点,直接用代码证明:
1 | import torch |
即使 model.eval() 了,输出依然有 requires_grad=True,说明 autograd 依然在工作。
因此仅调用 eval() 不足以关闭 autograd,仍需显式进入 no_grad 或 inference_mode:
1 | model.eval() |
8. torch.no_grad():关闭 autograd 记录
torch.no_grad() 是一个上下文管理器,进入这个块后,PyTorch 不再构建计算图:
1 | import torch |
在推理里的实际意义
模型参数默认 requires_grad=True。未进入 no_grad() / inference_mode() 时:
1 | model.eval() |
进入 no_grad() 后:
1 | model.eval() |
对于一个有很多层的大模型,
no_grad()带来的内存节省是相当可观的——因为不需要保存反向传播所需的中间激活值了。
9. torch.inference_mode():更适合纯推理的选择
inference_mode() 是 PyTorch 1.9 之后引入的,可以理解为"推理场景专用的 no_grad"。
1 | with torch.inference_mode(): |
它和 no_grad() 的共同点
- 都不构建 backward 计算图
- 都适合推理
- 都能减少计算和内存开销
它比 no_grad() 多做的事
PyTorch 内部维护着很多状态,帮助 autograd 工作,比如:
- version counter:追踪一个 Tensor 被 in-place 修改了几次(为了保证反向传播的正确性)
- grad_fn 记录:记录每个 Tensor 的计算来源
no_grad() 只是不构建计算图,但上面这些内部跟踪机制还在运行。
inference_mode() 则更激进:直接告诉 PyTorch,这里创建的 Tensor 是纯推理产物,所有 autograd 相关的跟踪机制都不需要。
所以 inference_mode() 通常比 no_grad() 省更多开销。
代价:更严格的限制
在 inference_mode() 里创建的 Tensor,离开这个上下文后,不能再参与需要 autograd 的计算:
1 | with torch.inference_mode(): |
而用 no_grad() 产生的 Tensor 则没有这个限制(虽然它本身没有 grad,但可以参与后续有 grad 的计算):
1 | with torch.no_grad(): |
10. 一张表:四种组合的效果对比
| 场景 | Dropout 行为 | BatchNorm 行为 | 梯度追踪 | 额外 autograd 开销 |
|---|---|---|---|---|
model.train() 默认 |
随机丢弃 | 用 batch 统计 | 是 | 是 |
model.eval() 默认 |
全部保留 | 用 running 统计 | 是 | 是 |
model.eval() + no_grad() |
全部保留 | 用 running 统计 | 否 | 部分节省 |
model.eval() + inference_mode() |
全部保留 | 用 running 统计 | 否 | 更省 |
推理时想要的是最后一行:Dropout 不丢弃、BatchNorm 用 running 统计、不追踪梯度、最小开销。
11. 标准推理代码的两个模板
模板 1:通用版(兼容性最好)
1 | model.eval() |
适合:
- 大多数推理代码
- 推理结果后面还可能参与其他计算
- 兼容性要求高、不想踩 inference_mode 的兼容坑
模板 2:纯推理版(性能最优)
1 | model.eval() |
适合:
- 纯前向推理,不需要后续梯度操作
- 追求更低延迟和更少内存
- 工程部署中的标准推理服务
12. 完整对比实验:自己跑一遍
建议把下面这个脚本跑一遍,把每个输出和原理对应起来:
1 | import torch |
预期输出(大致):
1 | 【场景1: train模式】 |
y6.is_inference()为True,说明这个 Tensor 被标记为"推理产物",PyTorch 知道不能把它拿回 autograd 计算图里用。
13. requires_grad 和模型参数的关系说明
初学者常见的困惑:我没有手动设 requires_grad,为什么输出有梯度跟踪?
原因:nn.Linear 等层的参数(weight、bias)默认 requires_grad=True。
若参与运算的参数 requires_grad=True,即使输入未要求梯度,输出仍可能接入 autograd 计算链。
1 | linear = nn.Linear(4, 2) |
因此即使输入 x 的 requires_grad 为假,只要参数需追踪,输出仍可携带梯度信息。
而加了 no_grad() / inference_mode() 后,这条追踪链就被整个切断了。
14. 推理脚本检查清单
审阅推理脚本时可依次核对:
-
[ ] 推理前有没有
model.eval()?
→ 没有的话,Dropout 会随机丢值,BatchNorm 用 batch 统计 -
[ ] 前向外面有没有
torch.no_grad()或torch.inference_mode()?
→ 没有的话,有不必要的计算图构建开销和内存占用 -
[ ] 两件事有没有都做,还是只做了其中一个?
→ 只写eval()不够;只写no_grad()也不够 -
[ ] 如果用了
inference_mode(),推理结果有没有拿去参与后续梯度计算?
→ 有的话要换成no_grad()
15. 常见误区
误区 1:eval() 等于关闭梯度
错。eval() 只切换模块的行为模式,不碰 autograd。
误区 2:no_grad() 会让 Dropout 停下来
错。no_grad() 只关 autograd,Dropout 的 train/eval 行为由模块模式决定,和 autograd 无关。
误区 3:推理只写 eval() 就完整了
不完整。还需要 no_grad() 或 inference_mode() 来关闭不必要的梯度追踪。
误区 4:inference_mode() 就是 no_grad() 的别名
不是。inference_mode() 更激进,额外关闭了更多 autograd 跟踪机制,产生的 Tensor 不能再参与 autograd 计算。
误区 5:模型不在训练,所以 grad 自然不会记录
否。默认梯度模式下前向会被 autograd 追踪,与是否处于训练业务逻辑无关。
16. 与推理模式相关的辨析
Q:model.eval() 做了什么?
A:把模块切换到评估模式,影响的是那些训练和推理行为不同的层(主要是 Dropout 和 BatchNorm)。不影响梯度记录。
Q:为什么 eval() 不能代替 torch.no_grad()?
A:两者控制的是完全不同的系统:eval() 控制模块行为,no_grad() 控制 autograd 记录。缺一不可。
Q:torch.no_grad() 的作用是什么?
A:让这一段计算不被记录到 backward 计算图,忽略不必要的梯度追踪开销,减少内存占用。
Q:torch.inference_mode() 和 torch.no_grad() 的区别是什么?
A:都不构建计算图;但 inference_mode() 更激进,还关闭了更多内部 autograd 跟踪机制(如 version counter),性能开销更小,但其中创建的 Tensor 不能再参与 autograd 计算。
Q:标准推理代码为什么要同时写 model.eval() 和 torch.inference_mode()?
A:前者让 Dropout 不丢弃、BatchNorm 用 running 统计;后者让推理过程不做任何梯度相关的额外工作。二者分属不同系统,必须都写。
17. 思考题
建议自己动手跑,先猜输出,再验证。
练习 1:验证 eval() 不会关闭梯度
1 | model = nn.Linear(4, 2) |
思考:为什么即使没有人设置
requires_grad=True,输出还是有梯度跟踪?
练习 2:验证 Dropout 受 train/eval 控制,不受 no_grad 控制
1 | drop = nn.Dropout(p=0.5) |
思考:哪种情况下 Dropout 起作用?这说明了什么?
练习 3:验证 no_grad() 切断梯度链
1 | linear = nn.Linear(4, 2) |
思考:linear.weight 的 requires_grad 变化了吗?为什么 y2 没有 grad?
练习 4:inference_mode 的 is_inference 标记
1 | model = nn.Linear(4, 2) |
思考:
inference_mode比no_grad多了哪些限制?为什么这些限制让它更快?
练习 5:写一个标准推理函数
1 | def infer(model, x, device="cpu"): |
进阶:让这个函数同时接受
device参数,自动把model和x移到指定设备。
18. 本节要点与自检
- 能清晰区分"模块行为模式"和"autograd 模式",知道它们是两个独立维度
- 知道 Dropout 和 BatchNorm 为什么在 train/eval 下行为不同
- 知道
eval()为什么不能代替no_grad() - 知道
no_grad()为什么不能代替eval() - 理解
inference_mode()比no_grad()多做了什么,以及对应的限制 - 能一眼看出一个推理脚本是否缺了关键步骤
- 能用标准模板正确写出纯推理函数
19. 小结
eval()负责让模型"像推理一样工作",no_grad()/inference_mode()负责让 PyTorch"像推理一样执行"。两件事独立,推理时都要做。
