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、通信缓冲区和混合精度状态反复进入同一片显存后,连续空间才是更值得盯住的指标。