inference_mode与no_grad:冻结特征为何仍影响训练

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

冻结编码器,只训练后面的线性层时,优先考虑no_grad,而不是直接把整段替换成inference_mode。特征本身不需要梯度,不代表反向传播不需要读取特征值。两种模式的差别,会在“前半段不训练、后半段仍训练”的接口处显现。

文章目录
  1. 没有特征梯度,仍然要用特征算权重梯度
  2. no_grad与inference_mode的接口承诺不同
  3. 先确定边界,再选模式
  4. 九项标量检查验证的是依赖,不是框架报错
  5. 不要用“没报错”作为唯一验收
  6. eval、缓存与速度要另行检查
  7. 来源与补发说明

这篇回答一个具体问题:为什么冻结特征提取器之后,inference_mode仍可能影响下游训练?下面从一个两维线性头推导哪些值必须保留,再给出模式选择和验收方法。Read in English。

没有特征梯度,仍然要用特征算权重梯度

设编码器输出h=(2,−1),线性头权重w=(3,4),预测z=w·h=2,目标y=1,损失L=(z−y)²/2=0.5。我们只训练w,把h作为本轮固定输入。链式法则给出∂L/∂w=(z−y)h=(2,−1)。

这条公式没有要求计算∂L/∂h,却明确需要h的数值。若前向结束后把h扔掉,通常无法仅凭标量损失0.5还原两个权重的梯度:同一个损失可能来自不同特征和不同预测误差。反向需要哪些保存值,应由导数公式判断,而不是由“这个输入没有梯度”判断。

例如把特征改为(0,0),相同目标下损失也为0.5,但权重梯度是(0,0)。两个相同损失对应不同梯度,足以否定“只保留loss就够了”。这个反例不依赖神经网络规模,也不需要假设某个GPU内核如何实现。

固定特征 h=(2,−1)
预测 z=3×2+4×(−1)=2
损失 L=0.5;残差=1
头部梯度=残差×h=(2,−1)
原创依赖图:不计算特征梯度,仍需要特征数值来计算头部梯度。

no_grad与inference_mode的接口承诺不同

PyTorch 2.8的Autograd说明把no-grad产生的结果可供后续梯度模式使用,与inference模式的使用限制分开。后者适合不再与Autograd交互的计算;它减少额外跟踪,但不能据此推断输出可以无条件交给可微下游。

inference_mode接口文档进一步列出视图跟踪、版本计数与前向自动微分方面的差别。它不是一个自动判断后续生命周期的优化器。调用方需要知道输出会被谁使用,尤其要看下一段是否需要为反向保存它。

对上面的线性头,这个区别很实在:上游不求导只是需求之一,下游权重训练仍需要特征作为保存值。应把“停止上游建图”和“允许下游正常反向”同时写进接口要求。只观察特征的requires_grad=False,无法验证第二条已经满足。也不能把这种限制概括成“任何后续运算都会失败”:关键是实际运算是否需要参与反向记录和保存相关值,选项应以整条预期路径验收。

先确定边界,再选模式

冻结参数本身可以通过参数的requires_grad设置表达;是否额外关闭某段计算的梯度记录,需要结合输入依赖决定。不要把所有冻结模块都统一包进同一个上下文。

若编码器和后续处理都只服务于最终预测,输出只用于展示、排序或持久化,可以把这一完整生命周期作为候选推理区域;仍需在实际项目中验证返回对象是否会进入别的训练路径。若编码器冻结、线性头训练,no_grad通常更适合作为特征提取边界。

还有一种情形容易误判:编码器参数冻结,但输入前面接着一个可训练适配器,任务需要梯度穿过编码器回到适配器。此时不能因为编码器参数不更新,就把整个编码器调用放入no_grad。固定参数与切断对输入的导数是不同决定;后者可能切断适配器需要的训练信号。

因此模式选择至少问三个问题:谁的参数应更新,谁的输入需要导数,输出会不会被下一段反向读取。它们是数据流问题,不应被“训练”或“推理”这两个目录名替代。离线特征生成脚本的输出以后也可能用于训练,名称本身不是证据。

最终预测,输出不再进入反向 → 评估推理区域
冻结特征,后续头部训练 → 检查no_grad边界
输入侧适配器训练 → 保留穿过冻结模块的路径
每条路径 → 真实反向、梯度与更新验收
原创决策图:以输出消费者与导数需求确定边界,不按模块名字猜测。

九项标量检查验证的是依赖,不是框架报错

以下代码可直接使用Python标准库执行。它算出线性头的预测、残差和解析梯度,再用中心差分分别检查两个权重。步长0.1的一步更新把损失从0.5降到0.125,最后用零特征构造相同损失、不同梯度的反例。

# Scalar dependency check, not an implementation of PyTorch grad modes.
from math import isclose
h=(2.,-1.); w=(3.,4.); target=1.
def loss(weights,features=h):
    prediction=sum(a*b for a,b in zip(weights,features))
    return (prediction-target)**2/2
prediction=sum(a*b for a,b in zip(w,h))
residual=prediction-target
grad=tuple(residual*x for x in h)
assert prediction == 2.
assert residual == 1.
assert grad == (2.,-1.)
for j in range(2):
    plus=list(w); minus=list(w)
    plus[j]+=1e-5; minus[j]-=1e-5
    numerical=(loss(plus)-loss(minus))/(2e-5)
    assert isclose(numerical,grad[j],rel_tol=1e-8)
assert loss(w)==0.5
new_w=tuple(a-0.1*g for a,g in zip(w,grad))
assert isclose(loss(new_w),0.125)
# No gradient for features does not make their values irrelevant.
assert loss(w,(0.,0.))==0.5
assert (residual*0.,residual*0.) != grad
print('9 scalar dependency checks passed')

这九个断言验证数学依赖关系。当前执行环境没有PyTorch,未运行inference tensor、模型反向或设备测试,因而没有把某条预期错误文本写成实测结果。模式语义来自版本化官方文档,标量数值来自实际执行,两类证据各自支持不同论断。

有限差分只扰动w,保持h和y固定;若每次扰动顺便重算另一个依赖w的特征,就已经换了函数。检查冻结特征的头部训练时,应先把这种数学前提固定下来,再检查框架是否实现了相同计算。

另一个常见边界错误,是把头部前向也留在no_grad上下文内。这样不只是冻结特征提取,连本来要训练的头部计算也不再按预期记录。把区域画在函数调用层级上:编码器调用结束后退出上下文,头部、损失和反向位于正常梯度区域;再用真实测试确认项目的装饰器与嵌套上下文没有扩大范围。

不要用“没报错”作为唯一验收

项目中的最小验收应覆盖一次真实的头部前向、损失、反向和更新,而不是只运行编码器并检查输出形状。确认头部预期参数获得有限梯度,再用可解释的小例子核对梯度值或数值差分;同时确认冻结编码器的参数没有收到本不该出现的梯度。

对需要穿过冻结模块训练输入侧适配器的路径,应额外检查适配器参数的梯度。只检查编码器参数没有梯度,恰好会漏掉整条信号被错误切断的情况。还要明确哪些梯度应为零、哪些应不存在,不能把所有零梯度都当作成功。

若代码中已有inference_mode,先缩小上下文边界或改为no_grad,验证完整训练路径。不要仅为消除错误就到处复制张量;复制会改变存储成本,放在什么模式内也有语义。应先建立一条正确、可解释的基线,再对复制和缓存策略做有条件的优化。

eval、缓存与速度要另行检查

模型的eval状态解决依赖训练状态的模块行为,不能代替梯度模式选择。此前的SDPA Dropout排查说明了自定义调用参数也要显式核对。这里的新问题是输出能否跨越到训练区域,两项检查应各自有验收条件。

同样,no_grad并不自动把特征保存成独立快照。若生产者后续复用缓冲区,应另外检查存储所有权;可继续读detach与clone的存储区别。把图关系、模块状态和存储生命周期分开,能避免用一种模式修补另一种问题。

性能比较应固定输入、设备、精度、模块状态和完整调用范围,分别测量所需指标。没有测量就不能承诺inference_mode一定更快多少;在还要复制输出、跨阶段保存或训练的管线里,更不能只计编码器一段耗时。这里提供的是选择依据,不是未经运行的吞吐成绩。

如果两维推导与接口验收方法帮你定位了冻结模型后的梯度问题,欢迎通过文末自愿支持。你可以直接复用的是“先列出反向需要哪些值,再决定推理边界”的排查顺序。

来源与补发说明

Inference mode API · Autograd mechanics · no_grad API

依据原计划时间之前已有的PyTorch 2.8文档核对,Autograd说明更新于2025年6月16日。图示和标量示例为原创;实际执行范围仅为标准库数值检查。本次补发保留2026年10月21日14:00北京时间文章日期,实际公开时间另行记录。

支持

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

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

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

Share this Post

Leave a Comment

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

*
*