AI 纪元 追踪人工智能最新进展,深度解读前沿技术

昇腾910C训练频繁OOM,问题可能出在显存碎片

一次1024卡昇腾910C训练中,单卡峰值占用82GB,却在第437步集体OOM。排查发现,14GB空闲显存被切成最大仅3GB的连续块。通过定期紧凑和内存池预分配,项目将单步时间从4.7秒降至3.8秒,MFU由31%升至42%。

0 次阅读 约 3148 字 6 分钟 AI 生成
Swiss cheese with holes too small for a key, symbolizing fragmented memory blocks preventing allocation despite available space.
Swiss cheese with holes too small for a key, symbolizing fragmented memory blocks preventing allocation despite available space.

1024卡昇腾910C集群训练千亿参数Dense模型时,我们碰到过一种很别扭的OOM:监控显示显存还有余量,Allocator却申请失败。

故障发生在第437个step。单卡理论显存为96GB HBM,训练配置记录的峰值占用是82GB,账面上还空着14GB。继续检查内存布局后才发现,最大连续空闲块已经不足3GB,无法容纳下一次AllReduce所需的4.2GB通信缓冲区。

显存没有真正用完,只是被切碎了。

14GB空闲显存为什么不能用

混合精度训练会让多种尺寸、不同生命周期的Tensor同时留在显存中。前向传播使用BF16,梯度累积和Adam优化器的m、v状态使用FP32。以千亿参数模型为例,BF16参数约占200GB,FP32优化器状态还要占用约800GB,需要分散到多卡保存。

动态分配过程中,BF16和FP32 Tensor交替进入显存。中间Tensor释放后会留下空洞,新申请的Tensor如果尺寸不匹配,就无法完整复用这些空间。空闲总量仍然很大,连续空间却越来越少。

动态图和长序列又会放大这个问题。项目使用8192 token序列,每个step的Tensor生命周期并不完全一致,alloc和free的顺序难以固定。大块激活值释放后,留下的空间很快会被多个小Tensor切开。

通信缓冲区也在参与切割。HCCS互联的卡间带宽为400GB/s,梯度AllReduce需要临时申请连续显存。缓冲区释放后,空洞可能夹在参数区和优化器状态区之间。训练运行得越久,显存布局越像一块被反复切过的奶酪。

腾讯云开发者社区的一篇对比文章列出的910C参数包括96GB HBM、1.8TB/s显存带宽和400GB/s HCCS带宽。该文同时给出H100的80GB HBM和3.35TB/s显存带宽。按照这组数据,910C容量更大,但数据搬运成本仍需谨慎控制。来源

总量监控抓不到碎片

ascend-dmi可以查看显存总量和已分配量,但这些数字不足以解释本次OOM。我们通过acl接口编写了一个扫描脚本,每100个step记录一次显存布局,重点观察两个指标:

  • 空闲显存总量;
  • 最大连续空闲块。

项目还使用“空闲总量÷最大连续块-1”记录碎片率。训练到第400步时,碎片率达到4.7:空闲显存共14GB,最大连续块只有3GB。下一次AllReduce需要至少4.2GB连续空间,申请自然失败。

这类故障只看“已使用多少GB”很容易误判。更有用的问题是:当前还能找到多大的完整空间?

定期整理:用Ascend C压紧显存布局

第一种处理方式是在固定间隔整理显存,把分散的空洞重新合并。难点不在搬运数据,而在移动之后仍要保证计算图中的引用有效。

项目使用Ascend C编写了一个显存紧凑算子,执行过程分为三步。

先标记仍在使用的Tensor。 通过CANN的aclGetTensorAddress接口遍历当前计算图,记录活跃Tensor的显存地址,避免在读写期间移动数据。

再扫描空洞。 从低地址向高地址检查显存布局,记录空洞的位置和大小,并计算存活Tensor向低地址移动时所需的偏移量。

最后搬运并更新引用。 整理安排在optimizer step结束、下一次forward开始之前。数据移动完成后,批量更新计算图中的Tensor地址。

这里不能只修改普通指针。CANN通过aclDataBuffer管理相关引用,直接换地址可能让后续算子读不到数据。项目在CANN 7.0.RC1中使用aclUpdateDataBufferAddr更新引用,确保计算图仍指向正确位置。

在1024卡集群上,每500个step执行一次紧凑操作后,碎片率从4.7降至0.8,最大连续空闲块从3GB恢复到12GB。单次整理耗时约1.2秒。

代价也很明确:整理期间,相关Tensor必须暂停读写,相当于额外插入一个全局同步点。集群规模继续增大时,尾部节点会拉长所有设备的等待时间。因此,这种方式更适合止损,不宜成为高频操作。

从源头减少alloc和free

第二种方案是不再把显存完全交给动态Allocator,而是在训练启动时预分配内存池。项目按Tensor用途和生命周期把显存分成三个区域:

  • 参数区:保存BF16和FP32数据,地址固定,训练期间不释放;
  • 激活区:保存BF16激活值,采用环形buffer复用;
  • 优化器区:保存FP32状态,与参数区对应。

激活区是控制碎片的关键。过去每层都会单独申请和释放激活值,现在预留一块能够容纳最大单层激活值的连续空间,各层轮流覆盖写入。整个训练过程中,这块显存不再反复alloc和free。

环形buffer会覆盖前面层的激活值,因此需要配合梯度检查点。项目每6个Transformer层设置一个检查点,只保留这些层的激活,其余部分在反向传播时重新计算。按这一配置,激活区大小降至原来的1/6。

优化器更新也做了合并。常规流程需要把梯度从BF16转换为FP32,与优化器状态计算,再写回BF16参数。项目通过Ascend C融合算子,在FP32域内连续完成梯度转换、动量更新和参数更新,最后只写回一次BF16结果,省掉两次中间拷贝。

上线后,项目记录到的碎片率一直低于0.3,不再需要定期执行显存紧凑。训练指标也随之变化:

  • 单步时间从4.7秒降至3.8秒,缩短19%;
  • MFU从31%升至42%;
  • 1024卡集群的step间差异系数从12%降至5%。

后一个变化很重要。分布式训练中,只要少数卡因内存分配或数据搬运变慢,其他卡就要在同步点等待。减少step间波动,收益不只体现在单卡显存上。

MFU到42%以后,瓶颈换了位置

显存碎片得到控制,并不意味着硬件算力已经充分释放。按照腾讯云开发者社区文章给出的数据,910C的FP16理论算力为800TFLOPS。MFU为42%时,对应的有效计算量约为336TFLOPS。

项目中的剩余损耗主要来自通信和软件栈。万卡AllReduce会持续考验HCCS带宽与HCCL集合通信效率;使用CANN 7.0.RC1和动态图模式时,图编译开销约占单步时间的6%。这些问题无法靠整理显存解决。

华为在2025年全联接大会上介绍了超节点互联技术。官方披露的另一个时间点是:华为云AI研发团队从2025年春节开始协同优化,到4月30日使Ascend 910B/910C的推理能力达到客户基本需求。来源

这段公开信息谈的是推理侧进展,不能直接当作训练优化结论。训练中的激活值、优化器状态和通信缓冲区有不同的生命周期,显存管理方式也要重新设计。

换到其他加速卡,先看两个条件

定期紧凑和内存池预分配并不限于910C,但迁移成本差别很大。

紧凑方案依赖框架提供Tensor地址更新能力。项目使用的关键接口是CANN 7.0.RC1中的aclUpdateDataBufferAddr。如果其他软件栈没有等价接口,就要在算子层自行维护引用,工程复杂度会明显增加。

内存池方案更容易迁移,但需要提前算清模型各层的显存需求。环形buffer必须覆盖最大单层激活值,梯度检查点间隔也要结合模型结构确定。融合算子则与芯片指令集直接相关,在910C上编写的Ascend C代码不能原样搬到其他加速卡。

遇到“监控还有显存,训练却OOM”时,不必立即加卡或缩小batch size。先记录空闲总量和最大连续空闲块。如果两者差距很大,问题可能不在容量,而在显存布局。

这次1024卡训练给出的教训很直接:96GB是容量上限,不等于随时都有96GB可按需分配。动态Tensor、通信缓冲区和混合精度状态反复进入同一片显存后,连续空间才是更值得盯住的指标。

相关阅读