训练营简介
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 的逻辑很简单:

  1. 生成一个与输入 Shape 相同的随机向量 $R$(0~1 均匀分布)。

  2. 生成掩码 $Mask = (R < \text{keep\_prob})$。

  3. 输出 $Y = X \times Mask \times \frac{1}{\text{keep\_prob}}$。

3.1 Kernel 类定义

我们需要传入 seedoffset(通常由 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 确定性海洋中的一座孤岛。

  1. 算法:理解 Philox 算法的无状态特性(Seed + Offset)。

  2. 实现:区分 数值掩码 (Value Mask)比特掩码 (Bit Mask)。前者通用但占内存,后者高效但需要专用指令。

  3. 场景:Dropout 不仅用于训练,在推理的 Speculative Decoding(投机采样)中也越来越重要。

掌握了随机数,你就赋予了模型“创造力”。

下期预告 在攻克了各种复杂的算子后,我们可能会遇到一种情况:算子逻辑太简单了,简单到不值得写一个 Kernel。比如 y = x + 1,或者 y = x * 2。 如果有一串这样的小算子,启动 Kernel 的开销比计算还大。 有没有办法把它们“捏”在一起,不用写 C++ 代码就能融合? 下一期,我们将探讨 【昇腾CANN训练营·进阶篇】零代码融合:利用 DSL (Domain Specific Language) 快速构建 Element-wise 融合算子

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐