12,900
社区成员
发帖
与我相关
我的任务
分享在昇腾(Ascend)NPU上训练大模型或高分辨率图像模型时,我们常会遇到一个尴尬的问题:想要增加Batch Size以稳定收敛,但NPU显存(HBM)却报警了(OOM)。
除了增加更大显存的硬件,软件层面最有效的解决方案就是梯度累积(Gradient Accumulation)。它的核心思想是将一个大的Batch拆分成多个Micro-Batch依次计算,累积梯度后再更新参数。
本文将跳过基础API,直接带你深入MindSpore的底层Cell机制,手动实现一个支持梯度累积的训练封装。
在MindSpore的Graph模式下,直接写Python循环累积梯度是行不通的(因为会被编译成静态图)。我们需要自定义 TrainOneStepCell。
核心逻辑:
Forward & Backward:计算当前Micro-Batch的Loss和梯度。
Accumulate:将当前梯度加到累积变量(Parameter)中。
Update:当达到累积步数(Accumulation Steps)时,应用优化器更新权重,并清零累积变量。
下面的代码演示了如何封装一个通用的梯度累积训练步。
import mindspore as ms
from mindspore import nn, ops, Tensor, Parameter
from mindspore.common import dtype as mstype
class TrainOneStepWithAccumulation(nn.Cell):
"""
支持梯度累积的自定义训练步封装
network: 前向网络
optimizer: 优化器
accumulate_step: 累积步数 (例如 4)
sens: Loss缩放系数 (用于混合精度)
"""
def __init__(self, network, optimizer, accumulate_step, sens=1.0):
super(TrainOneStepWithAccumulation, self).__init__()
self.network = network
self.optimizer = optimizer
self.accumulate_step = accumulate_step
self.weights = self.optimizer.parameters
# 定义梯度计算函数
self.grad = ops.GradOperation(get_by_list=True, sens_param=True)
self.sens = sens
# 创建用于存储累积梯度的Parameter
# 注意:必须初始化为0,且不参与优化器更新
self.accumulated_grads = self.weights.clone(prefix="acc_grad", init='zeros')
# 内部计数器
self.counter = Parameter(Tensor(0, mstype.int32), name="accumulate_counter")
# 算子定义
self.hyper_map = ops.HyperMap()
self.partial = ops.Partial()
self.assign_add = ops.AssignAdd()
self.reset_acc = ops.Assign()
def construct(self, data, label):
# 1. 计算当前Micro-Batch的梯度
weights = self.weights
loss = self.network(data, label)
# 构造sens tensor用于反向传播
sens = ops.Fill()(ops.DType()(loss), ops.Shape()(loss), self.sens)
grads = self.grad(self.network, weights)(data, label, sens)
# 2. 累积梯度 (grads / accumulate_step)
# 我们通常在累积时平均,或者在Loss计算时平均,这里选择直接累积
success = self.hyper_map(ops.partial(self.assign_add), self.accumulated_grads, grads)
# 3. 计数器 +1
loss = ops.depend(loss, success)
current_step = self.assign_add(self.counter, Tensor(1, mstype.int32))
# 4. 判断是否达到累积步数
if current_step % self.accumulate_step == 0:
# 达到累积步数:
# a. 使用累积的梯度更新权重
self.optimizer(self.accumulated_grads)
# b. 清零累积梯度
zeros = ops.ZerosLike()(self.accumulated_grads) # 这里需配合HyperMap使用,简化示意
# 实际清零逻辑:
self.hyper_map(ops.partial(self.reset_acc), self.accumulated_grads, self.weights.clone(init='zeros'))
# c. 重置计数器(可选,防止溢出)
# self.reset_acc(self.counter, Tensor(0, mstype.int32))
return loss
注意:上述代码为了通过静态图编译,需要严格遵守MindSpore的语法规范。在实际工程中,还需要处理sens的动态调整(Loss Scale),这在AMP(混合精度)模式下尤为重要。MindSpore高阶API boost模块中也提供了相关实验性特性,但在定制化场景下,手动实现Cell是最可控的。
在使用了梯度累积后,训练过程中的global_step概念会发生变化。在保存Checkpoint时,需要注意以下两点:
1、异步保存: 在Ascend上,IO操作(写磁盘)如果不异步进行,会严重阻塞计算流水线。
# 必须配置 async_save=True
config_ck = ms.CheckpointConfig(save_checkpoint_steps=1000,
keep_checkpoint_max=5,
async_save=True)
2、断点续训的陷阱: 加载模型时,如果使用了梯度累积,必须保证加载的优化器状态(Optimizer State)与当前累积步的状态一致。简单的 load_checkpoint可能只加载了权重。
建议:在生产环境中,始终将 epoch、cur_step等元数据作为独立的Parameter保存到ckpt中,以便恢复训练时能精准对齐。
在昇腾算力平台上,显存不应成为制约模型深度的瓶颈。
如果你的模型因为Batch Size太小而无法收敛(BN层震荡),梯度累积是必选方案。
通过继承 nn.Cell自定义训练步,虽然代码量稍大,但能让你完全掌控 NPU 的计算逻辑,实现如 Gradient Clipping(梯度裁剪)等更高级的操作。
希望这个硬核技巧能帮大家在昇腾上跑起更大的模型!