已关闭
[Usage]: 训练时输出的显存占用与npu-smi info差值过大 #1264
snowcat12创建于  3月30日关闭于  5月7日
snowcat12
3月30日 创建

在提交新问题之前,请确保您已经在社区中搜索过相关问题,并使用了社区中提供的资源/工具后,仍未找到满意的解决方式。

环境信息

openeuler 22.03
910B
CANN 8.5.0
MindSpeed-LLM 2.3.0

使用场景及问题

训练时输出的显存信息为:

[Rank 0] (after 1 iterations) memory (MB) | allocated: 32351.6953125 | max allocated: 32351.703125 | reserved: 33172.0 | max reserved: 33172.0

但训练过程中使用npu-smi info查看显存占用,发现rank 0实际使用了53000MB显存,多出约三分之二:

| 0       0                 | 462034        | python                   | 53289    

其他rank都是类似的情况,这部分多出的显存分配在了哪一部分呢?为什么训练时的输出会偏低这么多呢?

欢迎加入社区,感谢您对社区的贡献 🎉!

likedislike
ascend-robotascend-robot成员
3月30日 添加了label:usage
snowcat12
3月30日 评论:

通过profile可以看到iter2开始时,随着前向计算进行,显存值仍在上升,iter2存储前向激活时需要额外的显存吗?iter1占用的显存不会被释放了吗?

likedislike
歪比巴卜
歪比巴卜成员
3月31日 评论:

您好,已收到您的反馈,使用npu-smi info查看到的显存占用除了包含allocated以外,还包含了os固定占用、CANN占用(通信、GE)和跨流开销等;另外,iter2优化器开始占用显存,显存有上升是正常现象,建议您进一步观察iter3之后的显存值是否平稳

likedislike
snowcat12
4月2日 评论:

您好,已收到您的反馈,使用npu-smi info查看到的显存占用除了包含allocated以外,还包含了os固定占用、CANN占用(通信、GE)和跨流开销等;另外,iter2优化器开始占用显存,显存有上升是正常现象,建议您进一步观察iter3之后的显存值是否平稳

@zhyebin01

感谢回复

iter3之后的显存值是平稳的,但megatron本来的iter2也是会额外占用显存去存前向激活吗,有点反直觉,这样一来前向激活不是占了两份空间吗

likedislike
snowcat12
4月3日 评论:

您好,已收到您的反馈,使用npu-smi info查看到的显存占用除了包含allocated以外,还包含了os固定占用、CANN占用(通信、GE)和跨流开销等;另外,iter2优化器开始占用显存,显存有上升是正常现象,建议您进一步观察iter3之后的显存值是否平稳

@zhyebin01

您提到iter2刚开始显存上升是优化器的占用,这部分难道不是前向激活的占用吗。profile里iter2每一层前向结束时间与显存上升时间几乎相同。

likedislike
歪比巴卜
歪比巴卜成员
4月3日 评论:

您好!关于您的疑问,回复如下:

iter3之后的显存值是平稳的,但megatron本来的iter2也是会额外占用显存去存前向激活吗,有点反直觉,这样一来前向激活不是占了两份空间吗

您提到iter2刚开始显存上升是优化器的占用,这部分难道不是前向激活的占用吗。profile里iter2每一层前向结束时间与显存上升时间几乎相同。

llm与megatron是相同的逻辑:第一步激活值在第一步反向传播中释放,您观察到的显存随前向计算上升,是当前步新的前向激活在累积。激活值在对应的计算完成以后会释放,不会占用两份空间。从iter2开始的显存增加,主要是由于优化器的延时分配:优化器在iter1末尾被初始化并分配空间,从iter2开始常驻显存,iter2的前向激活将在这个基础上逐渐累积。优化器状态分配后即常驻,到iter3时,显存结构已定型,后续显存平稳不再飙升,说明您的训练状态是健康的。

likedislike
snowcat12
4月4日 评论:

您好!关于您的疑问,回复如下:

iter3之后的显存值是平稳的,但megatron本来的iter2也是会额外占用显存去存前向激活吗,有点反直觉,这样一来前向激活不是占了两份空间吗

您提到iter2刚开始显存上升是优化器的占用,这部分难道不是前向激活的占用吗。profile里iter2每一层前向结束时间与显存上升时间几乎相同。

llm与megatron是相同的逻辑:第一步激活值在第一步反向传播中释放,您观察到的显存随前向计算上升,是当前步新的前向激活在累积。激活值在对应的计算完成以后会释放,不会占用两份空间。从iter2开始的显存增加,主要是由于优化器的延时分配:优化器在iter1末尾被初始化并分配空间,从iter2开始常驻显存,iter2的前向激活将在这个基础上逐渐累积。优化器状态分配后即常驻,到iter3时,显存结构已定型,后续显存平稳不再飙升,说明您的训练状态是健康的。

@zhyebin01

感谢回复,可能是我理解不当,还是有些问题。

您提到“激活值在对应的计算完成以后会释放,不会占用两份空间”,那为什么对iter2的前向激活存储时显存占用会上升呢,iter2不能使用原本iter1的激活占用的显存吗?

如果iter1的激活对应的显存已经被完全释放,有一个显存占用下降的过程,所以iter2存激活要重新分配显存,那iter2的激活被释放的时候显存是否也应该出现大幅下降呢?但iter2之后的显存占用是完全平稳的。

likedislike
HANHU1CHEN成员
5月6日 评论:

你好,这个观察和前面的解释其实不矛盾,关于你的问题:

  1. "iter2 前向时显存为什么还会上升?iter2 不能复用 iter1 激活的空间吗?"

可以复用,但新增上升的那部分不是激活本身,而是优化器状态 + 临时 workspace。时间线大致是:

iter1 forward:分配激活 A1(峰值出现在 forward 末尾);
iter1 backward:A1 随反向逐层释放,同时分配梯度 G;
iter1 optimizer.step():首次访问 master weights / m / v 时才真正分配优化器状态 O(distributed optimizer + lazy init 的典型行为),这一步显存会跳一档;
iter2 forward:在 "权重 + 梯度 + 优化器状态 O" 的基础上,重新累积新的激活 A2。

你看到的 iter2 前向期间的上升曲线,底座抬高的部分是 O,叠加在上面的爬升才是 A2 的累积。A2 占用的空间是可以复用 A1 释放出来的 block 的,并不会是两份。

  1. "如果 iter1 激活释放过,为什么没看到一次显存下降?"

这一点取决于你用什么工具看显存:

  • torch_npu使用的是 caching allocator:tensor 释放时只是把 block 还回 allocator 的 pool,device 侧并不会立刻归还,留着给后面复用;
  • npu-smi info 看到的是 reserved 高点 + allocator 外占用,本质上只涨不退,到达稳态后就平稳;你不会看到 iter1 backward 期间激活释放对应的"下降"。

真正能看到"上升 → 下降 → 上升"完整曲线的,是 torch_npu.npu.memory_allocated()(对应实际占用,不含缓存);如果用 memory_reserved() 或 npu-smi,看到的就是单调上升到稳态。所以你 profile 看到的"iter2 上升、之后平稳"是预期行为:上升 = 优化器状态首次分配 + 新激活在已 reserve 的池里继续要新 block 把池抬到稳态高点;之后所有 iter 的激活分配/释放都在这个池子里循环,device 视角自然就稳定了。

如果想验证这个解释,可以在每个 iter 前后打印 torch_npu.npu.memory_allocated() 和 torch_npu.npu.memory_reserved() 对比看:allocated 会在 forward 末峰值、backward 末回落,呈周期性;reserved 则单调上升到稳态后不变。这两条曲线放一起,前面的所有现象就都能对上了。

likedislike
HHANHU1CHEN成员
5月6日 添加了label:pending
HHANHU1CHEN成员
5月6日 将 HANHU1CHEN 设为负责人
snowcat12
5月7日 评论:

你好,这个观察和前面的解释其实不矛盾,关于你的问题:

  1. "iter2 前向时显存为什么还会上升?iter2 不能复用 iter1 激活的空间吗?"

可以复用,但新增上升的那部分不是激活本身,而是优化器状态 + 临时 workspace。时间线大致是:

iter1 forward:分配激活 A1(峰值出现在 forward 末尾);
iter1 backward:A1 随反向逐层释放,同时分配梯度 G;
iter1 optimizer.step():首次访问 master weights / m / v 时才真正分配优化器状态 O(distributed optimizer + lazy init 的典型行为),这一步显存会跳一档;
iter2 forward:在 "权重 + 梯度 + 优化器状态 O" 的基础上,重新累积新的激活 A2。

你看到的 iter2 前向期间的上升曲线,底座抬高的部分是 O,叠加在上面的爬升才是 A2 的累积。A2 占用的空间是可以复用 A1 释放出来的 block 的,并不会是两份。

  1. "如果 iter1 激活释放过,为什么没看到一次显存下降?"

这一点取决于你用什么工具看显存:

  • torch_npu使用的是 caching allocator:tensor 释放时只是把 block 还回 allocator 的 pool,device 侧并不会立刻归还,留着给后面复用;
  • npu-smi info 看到的是 reserved 高点 + allocator 外占用,本质上只涨不退,到达稳态后就平稳;你不会看到 iter1 backward 期间激活释放对应的"下降"。

真正能看到"上升 → 下降 → 上升"完整曲线的,是 torch_npu.npu.memory_allocated()(对应实际占用,不含缓存);如果用 memory_reserved() 或 npu-smi,看到的就是单调上升到稳态。所以你 profile 看到的"iter2 上升、之后平稳"是预期行为:上升 = 优化器状态首次分配 + 新激活在已 reserve 的池里继续要新 block 把池抬到稳态高点;之后所有 iter 的激活分配/释放都在这个池子里循环,device 视角自然就稳定了。

如果想验证这个解释,可以在每个 iter 前后打印 torch_npu.npu.memory_allocated() 和 torch_npu.npu.memory_reserved() 对比看:allocated 会在 forward 末峰值、backward 末回落,呈周期性;reserved 则单调上升到稳态后不变。这两条曲线放一起,前面的所有现象就都能对上了。

@HANHU1CHEN

完全理解了,非常感谢

likedislike
Ssnowcat12
5月7日 issue状态由 TODO 改变为 DONE
Ssnowcat12
5月7日 关闭了 issue