loss-scale被广泛用于混精训练中,扩大反向传播过程中的参数梯度计算。笔者进一步解读了Megatron-LM框架中的loss-scale设置到应用的完整过程,希望能加深理解。
1. loss-scale初始化
1.1 超参数初始化
Megatron中支持静态和动态两种loss scale设置方式:
- 静态scale (Constant Loss Scale),是指训练过程中用固定的 扩放因子进行scale,扩放因子的设置需要经验性调整超参。
- 动态scale (Dynamic Loss Scale),随着训练推进,每间隔一定迭代 或 满足一定触发条件,就对用于loss scale的因子 进行扩放 或 缩放。简言之:框架帮你动态地找最合适的scale超参,减少了超参数调整的复杂性,使得训练过程更加自动化。
参见 megatron/arguments.py
部分:
--loss-scale
:设置静态的loss-scale值,必须为2的正指数幂,缺省默认动态。
--initial-loss-scale
: 动态调整loss-scale时,需要设置的loss-scale初始值,缺省$2^{32}$。
—-min-loss-scale
:动态调整loss-scale时,loss-scale的最小值,缺省1.0。
--loss-scale-window
:动态调整loss-scale时,loss-scale的扩放周期,缺省1000 iters。
--hysteresis
:动态调整loss-scale时,loss-scale的缩放周期,或者称(缩放前)nan/if的容忍次数,缺省值是2。
此处注意:
- 在不设置任何loss-scale相关参数的情况下,在分布式优化器或者混合精度训练中,默认采用动态loss-scale,且从极高的loss-scale初值开始 不断向下寻找最合适的scale参数。
- 可以通过设置静态loss-scale值为1.0来避免混精训练中的缩放。(特殊情形下可用)
loss-scale
设置后,会优先采用静态loss-scale。
initial-loss-scale
动态loss-scale初值,笔者一般经验性地设置为 $32768 (2^{15})$ 或 $65536(2^{16})$ 。
1.2 类中初始化
上述开关值,在 megatron/optimizer/init.py
中被调用:
只有开启distributed_optimizer
或者 fp16/bf16(混精)
模式下,才会使用loss scale:
- 先检查是否使用静态scale。 (混精、分布式优化器都可采用)
- 其他情况下,一律采用动态scale。(只有混精训练可以启用)
2. scale值管理
上述loss scale相关参数,被用于初始化 grad_scaler
,最终传递给优化器。GradScaler的具体实现封装在 megatron/optimizer/grad_scaler.py
中:
2.1 MegatronGradScaler (Abstract)
上述的MegatronGradScaler是一个抽象基类,定义了基本的接口和一些初始化操作。
其中_scale
是一个protected变量(约定 只允许通过类方法来访问和修改),保存当前的scale值(也是运行时的_scale值 );scale
和inv_scale
属性,分别实现了取scale值和scale值倒数,用于对loss的缩放和还原。
update
、state_dict
和load_state_dict
三个抽象方法主要是为了兼容动态缩放。
2.2 Constant Grad Scaler
静态scale非常简单,不需要进行scale值的更新,通过--loss-scale
参数在类对象初始化方法(继承父类)中设置_scale
值。
2.3 Dynamic Grad Scaler
接下来我们来看动态缩放器,分别分析init、state_dict
和update
相关。
初始化部分没什么可以分析的,一系列assert和规范操作。我们重点来看下相关成员变量:
- 用户参数赋值:
initial_scale
:初始scale值,范围(0, +OO),由--initial-loss-scale
设定,默认$2^{32}$。
min_scale
:最小scale值,范围(0,init_scale],由--min-loss-scale
设定,默认是1.0。
growth_interval
:scale的扩放周期,范围(0, +OO),由--loss-scale-window
设定,默认1000迭代。
hysteresis
:scale的缩放周期,范围(0,max_value_INT), 由--hysteresis
设定,默认是2。
- hard-code赋值(1.2部分):
- growth_factor :scale值的扩放因子,范围(1.0, +OO),hard-code设定值2.0。
- backoff_factor:scale值的缩放因子,范围(0.0, 1.0),hard-code设定值0.5。
- 本类的protected :
_grow_tracker
:扩放周期计数器,初值设定为0。
_hysteresis_tracker
:缩放周期计数器,初值设定为self.hysteresis
。
state_dict部分:
scale_dict 需要额外保存 当前的_scale
值,扩放周期计数器(growth_tracker
)和缩放计数器(_hysteresis_tracker
)。
scale值动态更新(或者说搜索、自适应)过程:
_scale
值的自适应过程,分为缩放和扩放两种情况:
- 缩放:如果发现梯度中存在 inf或者nan,将扩放计数器
_growth_tracker
重置(清零),_hysteresis_tracker
减1,若_hysteresis_tracker
小于等于 0,则对_scale
本身进行缩放(减半)。这里之所以是小于等于,是因为触发了一次缩放后(变残血了),此后每遭遇一次inf或者nan,就进行一次缩放。直到扩放一次scale值,才会回满血。
- 扩放:在未发现inf或者nan时,累加扩放计数器
_growth_tracker
,当达到扩放周期时,_hysteresis_tracker
重置(回满血),进行一次_scale
的扩放(倍增)。
简而言之:
- 在上一次scale值动态调整后,当连续
growth_interval
(1000) iter内都没有出现nan或者inf时,倍增(x2)扩放一次scale,立刻重置缩放和扩放计数器。
- 在上一次scale值扩放调整后,(不连续)累积
_hysteresis
(2)次 出现nan或者inf,倍减(x0.5)缩放一次scale。 在上一次scale值缩放调整后,每出现一次nan或者inf,就进行一次scale倍减。
3. scale的应用
本节详进一步析下优化器内grad_scale如何发挥作用,我们参考megatron/optimizer/optimizer.py
,主要来看FP16混精模式下的优化器。
类继承关系:
3.1 对loss进行scale
MegatronOptimizer中实现了最简单扩放过程,就是前向计算完成,得到loss后,将其值乘上当前维护的_scale
值,接着依靠反向传播的链式反应,自动将扩放依次作用到对各个参数的梯度计算上。
在megatron/training.py的 train_step()
中,传入了优化器的scale_loss
函数。
pretrain → train → train_step → get_forward_backward_func
继续往下找,我们在megatron/core/pipeline_parallel/schedules.py
中找到了最终调用的grad_scaler
这一参数的是 backward_step()
函数。
forward_backward_func→get_forward_backward_func → forward_backward_no_pipelining→ backward_step
去看forward_step
不难发现,这里的output_tensor[0]
就是前向结束后计算得到的loss值。这和我们对loss-scale的认知是符合的:即使用scale值扩放过的loss进行反向计算。
3.2 unscale 操作
unscale故名思义,就是对loss的缩放过程。我们扩放的目的是为了避免反向计算过程中梯度过小而丢失的问题,但在最终更新参数时,应将梯度还原回去,还原过程定义在MixedPrecisionOptimizer
类中。
关于缩放还原,我们应该参考AMP的函数,由于笔者Foucus的平台不是CUDA,读者可自行研读CUDA的amp代码。
其本质是: 统计各参数的梯度值中存在的inf或者nan数量,并对梯度值进行还原。
(下面的内容可略过)在笔者工作的平台上,上述函数的调用关系是:
而上述函数又是实例化了对应数据类型的模版函数实现的,主核代码如下:
其中的args结构体定义如下:
包含了输入、输出和对应长度,以及scale倒数。
从核代码(third_party/swtensor/slave/amp_slave.cpp
)如下:
3.3 State dict
state_dict主要方便打印以及checkpoint载入。