在这里插入图片描述

第一次训练大模型时,你大概率不会关注随机数算子。

初始化权重,用torch.nn.init.kaiming_normal_;Dropout,用torch.nn.Dropout。你只管调API,至于底层用的什么随机数生成器(RNG),你不在乎。

直到你的模型在两张不同的卡上初始化出了不一样的权重,导致分布式训练崩掉。你才开始意识到:随机数,不是随便生成的。

随机数生成器是什么:从抛硬币说起

抛硬币,大家都知道,正面反面各50%概率。

但计算机没有"随机"这种东西。它所有的"随机数",都是伪随机——用一套确定的算法,从一个初始值(种子,seed)出发,生成一串看起来随机的数。

这套算法,就是随机数生成器(RNG)

打个比方:

  • 真随机:抛掷一枚真硬币,结果不可预测
  • 伪随机:按一本"随机密码本"读数,密码本一样,读出来的序列就一样

在深度学习里,我们用的都是伪随机。关键是:这个"密码本"(RNG算法)得够好——生成的数列要看起来够随机(统计特性好),且速度要快。

CANN的ops-rand:昇腾NPU的随机数专家

ops-rand是CANN的随机数生成算子库。它提供了一系列随机数生成算法,供昇腾NPU上的训练和推理使用。

常用的有这几个:

  • 均匀分布(Uniform):生成[a,b]区间内的均匀随机数,权重初始化常用
  • 正态分布(Normal):生成均值μ、方差σ²的正态随机数,Dropout、噪声注入常用
  • 伯努利分布(Bernoulli):生成0或1,Dropout的底层就是这个

这些看起来简单,但要在NPU上高效实现,有不少坑。

坑1:分布外的截断

你用过torch.randn吗?它生成的是标准正态分布(均值0,方差1)。

但如果你给torch.nn.init.normal_指定了一个非零均值、非1方差,PyTorch底层会先做标准正态分布,再做线性变换:output = mean + std * standard_normal()

ops-rand里的正态分布算子也是这么实现的。但有一个边界情况:如果std很小(比如1e-5),线性变换后的数值可能超出浮点数的有效表示范围,变成0或者inf。

这个坑在GPU上也存在,但GPU的cuRAND库处理了这个边界情况(做clipping)。ops-rand的早期版本没处理,导致极小的std下,权重初始化全变成0,模型训练不收敛。

现在的新版本已经修了,但如果你用的是CANN 8.0之前的版本,得注意这个坑。

坑2:多卡训练的随机数同步

这是最容易踩的坑,没有之一。

分布式训练时,你有8张卡,每张卡都要做权重初始化、Dropout。如果你不搞点特殊操作,每张卡用的随机数序列是不一样的(因为每张卡的RNG种子默认不一样)。

问题来了:权重初始化不一样,OK,这是你想要的(增加多样性)。但Dropout不一样,Dropout的目的是在训练时随机"关掉"一部分神经元,推理时恢复。如果每张卡的Dropout mask不一样,前向传播的激活值就对不齐,AllReduce通信时会算出来一个错误的结果。

解决这个问题的方法叫RNG状态同步:在每次Dropout前,把所有卡的RNG状态设成一样的。

在PyTorch + 昇腾NPU上,这个同步是自动做的(昇腾的PyTorch适配层帮你搞定了)。但如果你是自己写底层算子,调ops-rand的C++接口,得手动调aclrngSetSeed把所有卡的种子设成一样的。

踩坑提示:如果你的分布式训练loss不收敛,而且看起来是"有时收敛有时不收敛",先查一下是不是Dropout的RNG没同步。

坑3:数据类型和精度的坑

ops-rand支持f32和f16两种数据类型。f32的精度高,但速度慢一点;f16的速度快,但精度低。

大部分时候,用f32就够了。但如果你训练的是大模型(70B参数以上),为了省内存,你可能用f16做计算。这时候,随机数也得用f16生成,不然类型不匹配。

问题是:f16能表示的数值范围比f32小很多。如果你用f16生成正态分布(均值0,方差1),尾部那些绝对值大于5的数,f16表示不出来,会被截断成inf或者0。

实际影响是:你的权重初始化里,那些"极端"值(本该是±5σ以外的)全没了,初始化分布变得不够"标准"。

解决办法:大模型训练时,权重初始化用f32生成,再cast成f16;不要直接用f16生成。

ops-rand在CANN架构里的位置

ops-rand是第2层:昇腾计算服务层的AOL算子库的一部分。

它依赖底层的第5层:昇腾计算基础层提供的随机数种子管理、设备内存分配等基础能力。

上层调用来自:

  • PyTorch/MindSpore的初始化函数:调AscendCL接口,再调ops-rand
  • Dropout算子:直接调ops-rand的伯努利分布生成

所以,你平时不直接和ops-rand打交道,但你每次做权重初始化、每次跑Dropout,底层都有ops-rand的参与。

怎么正确用ops-rand的随机数

如果你是直接调PyTorch接口,基本不用操心,昇腾的PyTorch适配层已经帮你搞定了。

但如果你是算子开发者,或者你要做极致的性能调优,可能需要直接调ops-rand的C++接口。几点建议:

  1. 选对RNG算法。ops-rand支持多种RNG算法(Philox、MT19937等),Philox适合GPU/NPU的并行场景,MT19937适合CPU。在NPU上,默认用Philox。
  2. 注意种子管理。每张卡用不同的种子(权重初始化),但Dropout要用相同的种子(通过RNG状态同步)。
  3. 注意数据类型。f32够用时别用f16,除非你真的缺那点内存。
  4. 注意性能。ops-rand的随机数生成是可以在NPU上并行的,但你得保证你给的buffer足够大,让NPU能充分并行。

写在最后

随机数算子看起来不起眼,但它是大模型训练的"隐形支柱"。权重初始化、Dropout、噪声注入、数据增强,全离不开它。

ops-rand作为CANN的随机数算子库,在昇腾NPU上提供了高效、正确的随机数生成能力。但要用好它,你得理解伪随机的原理、多卡同步的机制、还有f16/f32的精度权衡。

如果你在昇腾上训练大模型,遇到"怎么有时收敛有时不收敛"的灵异事件,不妨查查随机数相关的部分。很可能是某个角落里,RNG没同步,或者数据类型用错了。

昇腾CANN的ops-rand仓库里还有不少细节,感兴趣可以去扒扒文档,能少踩不少坑。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐