AMP梯度裁剪:为什么先unscale再clip

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

使用GradScaler时,裁剪阈值通常是针对还原后的梯度定义的。先裁剪缩放梯度、再让step还原,会把这个阈值也除以缩放因子。代码可以照常运行,实际更新却比预期更小。

文章目录
  1. 先分清三个不同的数
  2. 顺序错误,相当于改了阈值
  3. 在PyTorch里把还原放在哪里
  4. 十二项本地检查复现差异
  5. 累积未结束时,还原会混进两套单位
  6. 裁剪的集合和日志也要一致
  7. 不要让裁剪掩盖非有限值
  8. 依据与验证范围

例如原梯度为(3,4)、范数阈值为1、缩放因子为8:先还原再裁剪得到(0.6,0.8);反过来得到(0.075,0.1),最终范数只有0.125。这个简单反例足以定位顺序问题,不需要先跑完整模型。Read in English。

先分清三个不同的数

损失缩放因子S、裁剪阈值M和学习率η,分别控制不同环节。若在一次反向传播中把损失乘以相对参数固定的正数S,在理想算术下,得到的梯度也乘以S。还原操作将它除回去,目的是恢复原目标的梯度,而非把优化目标改成另一个权重。

梯度范数裁剪则有意改变过大的梯度。对有限向量g和正阈值M,定义Cₘ(g)=g·min(1,M/‖g‖₂),零向量保持零。它保留方向,并把超过阈值的向量缩到边界;这与逐元素把每个分量夹在某区间不同。

学习率发生在后续优化器更新中。只在普通无动量SGD下,更新量可直接写成−ηCₘ(g)。对AdamW这类带状态的优化器,不应把梯度范数与最终参数变化的范数简单画等号。

顺序错误,相当于改了阈值

把未还原的Sg按同一个M裁剪,再除以S,得到Cₘ(Sg)/S=g·min(1,M/(S‖g‖₂)),也就是以M/S为阈值的裁剪。正确顺序得到Cₘ(g)。两者不一定相等;S=1、零梯度或两边都未触发裁剪时可以相同。

反例中‖g‖₂=5。正确路径先将(24,32)除以8,再乘1/5,得到(0.6,0.8)。错误路径先将范数40裁到1,得到(0.6,0.8),接着又除以8,变成(0.075,0.1)。中间数值相同,所在阶段却完全不同。

如果S会随训练变化,错误顺序还会让有效阈值M/S随之变化。此时看到梯度突然更小,不能只靠调整学习率解释;先核对日志记录的是哪一个阶段的梯度。上述等式假设S>0,不要求它一定大于1。

蓝色:先还原再裁剪;棕色:先裁剪再还原。

S=1

S=8

S=64

原创理想算术对照:原梯度范数5、阈值1。正确顺序最终范数为1;错误顺序分别为1、0.125、0.015625。不是训练曲线。

在PyTorch里把还原放在哪里

PyTorch 2.14 AMP示例要求在修改或检查.grad前先还原;同一个优化器的一次step之间只调用一次unscale_,并等该步的梯度累积完成。已显式还原后,scaler.step知道不再重复还原。

因此,操作位置是所有本次backward完成之后、裁剪之前,然后才是scaler.step和scaler.update。使用多个优化器时,还要明确每个参数属于谁;还原某个优化器的梯度,不代表其他优化器中的参数也已经处理。

不要直接把“AMP”理解成所有数据类型都必须启用同样的缩放流程。这里讨论的是明确使用GradScaler的训练路径。首先确认你的实际配置和框架版本,再把这个顺序映射进去。

1. 缩放后的反向传播
2. 完成本次梯度累积
3. 还原梯度 unscale
4. 裁剪 → step → update
原创更新流程:同一有效批次内保持统一缩放,累积完成后再还原与裁剪。

十二项本地检查复现差异

以下标准库代码实现上面的理想L2裁剪公式,只处理这个有限向量教学场景。它不是PyTorch实现的逐位复刻;实际库可能包含数值保护、设备分支和有限精度误差。没有在这里运行GPU或训练模型。

# Ideal finite-vector model: no framework epsilon, GPU, or optimizer.
from math import hypot, isclose

def mul(v, k):
    return tuple(x*k for x in v)

def clip(v, bound):
    n = hypot(*v)
    return v if n == 0 or n <= bound else mul(v, bound/n)

g, bound, scale = (3.0, 4.0), 1.0, 8.0
right = clip(mul(mul(g, scale), 1/scale), bound)
wrong = mul(clip(mul(g, scale), bound), 1/scale)
assert hypot(*g) == 5.0
assert all(isclose(x,y) for x,y in zip(right, (0.6,0.8)))
assert all(isclose(x,y) for x,y in zip(wrong, (0.075,0.1)))
assert isclose(hypot(*right), 1.0)
assert isclose(hypot(*wrong), 0.125)
assert clip((0.0,0.0), bound) == (0.0,0.0)
for s in (1.0,8.0,64.0):
    good = clip(mul(mul(g,s),1/s),bound)
    bad = mul(clip(mul(g,s),bound),1/s)
    assert isclose(hypot(*good),1.0)
    assert isclose(hypot(*bad),1/s)
print("12 clipping checks passed")

十二项检查覆盖原范数、两条路径的分量及范数、零向量,以及S取1、8、64时的结果。范数对照图直接来自这些教学数值,不表示训练收敛、模型质量或吞吐变化。

累积未结束时,还原会混进两套单位

设两个微批次贡献为g₁和g₂,统一缩放时累积得到Sg₁+Sg₂,最后一次性除以S即可。若中途还原第一份,再把下一份缩放梯度加进去,会得到g₁+Sg₂;这已经不是统一比例下的总和。

更一般地,若两份贡献分别乘S₁和S₂,单纯把总和除以一个数,不能对任意g₁、g₂同时恢复正确总梯度。补救不应是猜一个“平均scale”,而应修正累积窗口的处理边界。

上午的梯度累积与loss归一化讨论每份贡献的权重,这里讨论数值缩放的单位。先确保各微批次对应同一个目标,再在统一缩放下累积;两个问题需要分别正确,不能用裁剪掩盖目标权重错误。

裁剪的集合和日志也要一致

clip_grad_norm_参考说明,它对指定参数的梯度计算整体范数、原地修改梯度,并返回裁剪前的总范数。这个返回值不是裁剪后的范数。如果日志显示大于阈值,先确认记录的含义,不要马上判断裁剪没有执行。

分别对每层使用同一阈值,与对整个参数集合统一裁剪,也不是同一约束。例如两组梯度各有范数1,分别裁到1后,拼接总范数仍可能达到√2。记录阈值时应同时记录参数集合和norm_type。

需要定位异常时,区分缩放后、还原后、裁剪前与裁剪后四个观察点。记录所用S、更新边界和被处理的参数集合,可以帮助解释数值;仅留下一个名为grad_norm的曲线,往往不足以复原当时发生了什么。

不要让裁剪掩盖非有限值

本文公式与代码以有限向量为前提。NaN或Inf不是普通“大梯度”,不能用这个示例推断它们被裁剪后就安全了。出现非有限值时,应追查损失、输入、运算与缩放状态,而不是先把阈值改得更小。

实际训练还可能包含分布式归约、分片参数或框架封装的裁剪接口。这里的单进程向量不证明那些路径的全局范数实现正确;需要核对相应框架的更新顺序和参数可见范围。

调试时可以先固定一个小的有限梯度,比较每个观察点的预期数值;确认单位、集合和顺序一致后,再恢复完整训练条件。若这组反例帮你排清了AMP裁剪中的顺序问题,欢迎通过文末支持区自愿支持。

依据与验证范围

于2026年10月17日核对PyTorch 2.14 AMP示例与梯度范数裁剪参考。这是一篇工程原理解读,不是新版本发布新闻。两幅图、公式推导与有限向量代码为原创教学内容,十二项检查已本地执行;未测量真实模型或硬件表现。

支持

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

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

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

Share this Post

Leave a Comment

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

*
*