【昇腾CANN训练营·进阶篇】上帝的骰子:在 AI Core 上实现高性能随机数生成与 Dropout 算子
摘要:2025年昇腾CANN训练营第二季提供从0基础到进阶的算子开发课程,完成认证可获得证书及奖品。本文重点解析AI芯片中随机数生成的工程难题,介绍Philox算法如何在确定性硬件上实现并行伪随机数生成,并以Dropout算子开发为例,展示AscendC实现过程,包括核心逻辑、代码实现及性能优化技巧。文章最后预告下期将探讨零代码算子融合技术。(149字)
训练营简介
2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。
报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

前言
爱因斯坦曾说:“上帝不掷骰子。” 但在 AI 的世界里,“掷骰子” 是灵魂所在。
如果没有随机性:
-
训练:模型容易过拟合(Dropout 失效)。
-
生成:ChatGPT 的回答将永远一模一样(Temperature 失效)。
然而,计算机是确定性的机器。如何在 AI Core 这种追求极致确定性的 SIMD 硬件上,并行地、高性能地生成“伪随机数(PRNG)”,是一个硬核的工程问题。
本期文章,我们将揭秘昇腾的随机数生成机制(Philox 算法),并实战开发一个 Dropout 算子。
一、 核心图解:有序中的混乱
Dropout 的过程就像是电流在传输过程中,随机地“断路”。

二、 核心算法:Philox 的魔法
在并行计算中,我们不能用传统的 rand()(线性同余法),因为它不仅串行,而且在多核并发时状态难以同步。
AI 芯片界公认的标准是 Philox 算法。 它不需要维护一个巨大的状态池,而是基于 Counter-based(基于计数器) 的逻辑:
$$\text{Rand}(Seed, Offset) = \text{Hash}(Seed, Offset)$$
-
Seed (种子):全局唯一,决定了整次运行的随机序列。
-
Offset (偏移量):每个核、每个数据元素都有唯一的 ID。
只要给了 Seed 和 Offset,任意时刻、任意位置的随机数都是可计算且确定的,这完美契合了 AI Core 的并行架构。
三、 代码实战:Ascend C 实现 Dropout
Dropout 的逻辑很简单:
-
生成一个与输入 Shape 相同的随机向量 $R$(0~1 均匀分布)。
-
生成掩码 $Mask = (R < \text{keep\_prob})$。
-
输出 $Y = X \times Mask \times \frac{1}{\text{keep\_prob}}$。
3.1 Kernel 类定义
我们需要传入 seed 和 offset(通常由 Host 侧维护并累加)。
class KernelDropout {
public:
__aicore__ inline void Init(GM_ADDR x, GM_ADDR y, uint64_t seed, uint64_t offset, ...) {
// ... Init ...
// 保存随机数种子
this->seed = seed;
this->offset = offset;
}
// ...
};
3.2 核心计算逻辑:生成随机 Mask
Ascend C 提供了 DropOutDoMask 等高阶指令,但为了理解原理,我们这里演示如何用更基础的指令组合实现(或者使用 Stateless 随机接口)。
假设我们使用 SetVectorMask 配合随机数指令(视具体芯片能力,部分芯片支持直接生成 Mask)。
通用实现路径:
__aicore__ inline void Compute(int32_t i) {
LocalTensor<half> xLoc = inQueueX.DeQue<half>();
LocalTensor<half> yLoc = outQueueY.AllocTensor<half>();
// 1. 生成随机数 (0~1 Uniform)
// 这里的 tileLength 决定了我们需要多少个随机数
// 每次生成后,offset 需要自增,保证不重复
// 假设有 vector_rand(dst, seed, offset_tensor) 指令
LocalTensor<half> randLoc = tmpQueue.AllocTensor<half>();
// 这是一个示意性 API,实际开发需查阅 "StatelessRandom" 相关接口
// 或者使用 DropOutDoMask 生成比特位掩码
// GenerateRandom(randLoc, this->seed, this->offset + i * tileLength);
// 2. 生成 Keep Mask
// mask = rand < keep_prob
// 这一步生成的是 0/1 的数值掩码 (half)
// Duplicate(probLoc, keep_prob, tileLength);
// Compare(maskLoc, randLoc, probLoc, CMP_LT);
// Select(maskLoc, maskLoc, 1.0, 0.0); // 转换为 0.0 或 1.0 的数值
// 3. Apply Dropout & Scale
// y = x * mask * (1 / keep_prob)
// scale = 1 / keep_prob
// y = x * mask
// Mul(yLoc, xLoc, maskLoc, tileLength);
// y = y * scale
// Muls(yLoc, yLoc, scale, tileLength);
// ------------------------------------------
// 高性能捷径:DropOutDoMask (生成 Bit Mask)
// ------------------------------------------
// 上面的方法太慢,需要生成完整的 FP16 随机数。
// Ascend C 提供了专门生成 Bit Mask 的指令,配合 MTE 的 Mask 搬运或 Select 指令。
LocalTensor<uint8_t> bitMask = tmpQueue.AllocTensor<uint8_t>();
// 生成比特掩码:1 代表保留,0 代表丢弃
// DropOutDoMask(bitMask, input, keep_prob, count, seed, ...);
// 利用 Bit Mask 选择数据
// 这通常需要特殊的 Vector 指令支持通过 BitMask 进行 Select
// 或者将 Bit Mask 还原为 Byte Mask/Half Mask
// 假设我们已经有了处理好的 mask
// ... 计算逻辑 ...
outQueueY.EnQue(yLoc);
inQueueX.FreeTensor(xLoc);
}
3.3 进阶:FlashDropout
在 FlashAttention 中,Dropout 是在 Softmax 之后做的。 由于 Softmax 的结果是在 UB 里的,我们不想把 $N \times N$ 的 Attention Matrix 写回 HBM 再做 Dropout。
On-the-fly Dropout: 我们在计算完 Softmax 后,直接在 UB 里生成随机数进行 Drop,然后立刻与 $V$ 做乘法。 这就要求随机数生成必须极快,不能成为流水线瓶颈。通常利用 AI Core 的特殊寄存器或专用电路来实现 Philox 计算。
四、 总结
随机数算子是 AI Core 确定性海洋中的一座孤岛。
-
算法:理解 Philox 算法的无状态特性(Seed + Offset)。
-
实现:区分 数值掩码 (Value Mask) 和 比特掩码 (Bit Mask)。前者通用但占内存,后者高效但需要专用指令。
-
场景:Dropout 不仅用于训练,在推理的 Speculative Decoding(投机采样)中也越来越重要。
掌握了随机数,你就赋予了模型“创造力”。
下期预告 在攻克了各种复杂的算子后,我们可能会遇到一种情况:算子逻辑太简单了,简单到不值得写一个 Kernel。比如 y = x + 1,或者 y = x * 2。 如果有一串这样的小算子,启动 Kernel 的开销比计算还大。 有没有办法把它们“捏”在一起,不用写 C++ 代码就能融合? 下一期,我们将探讨 【昇腾CANN训练营·进阶篇】零代码融合:利用 DSL (Domain Specific Language) 快速构建 Element-wise 融合算子。
更多推荐




所有评论(0)