训练营简介 2025年昇腾CANN训练营第二季,基于CANN开源开放全场景,推出0基础入门系列、码力全开特辑、开发者案例等专题课程,助力不同阶段开发者快速提升算子开发技能。获得Ascend C算子中级认证,即可领取精美证书,完成社区任务更有机会赢取华为手机,平板、开发板等大奖。

报名链接:https://www.hiascend.com/developer/activities/cann20252#cann-camp-2502-intro

前言

在上一期中,我们攻克了 RMSNorm。而在当今的大模型(LLM)推理界,除了追求算子逻辑的正确性,大家还在疯狂卷另一个指标:显存占用与推理速度

一个 70B 的 LLaMA 模型,如果用 FP16 加载,需要 140GB 显存,两张 80G 的卡才勉强塞下。但如果用 Int8 量化,显存直接减半,速度起飞。

昇腾 AI Core 的 Cube 单元其实是一个“偏科生”——它做 FP16 矩阵乘法很快,但做 Int8 矩阵乘法 更快(理论算力通常是 FP16 的两倍以上)。

本期番外篇,我们就来聊聊如何在 Ascend C 中驾驭这种“低精度”的快乐。

一、 核心原理:把高清图变像素画

量化(Quantization) 的本质,就是用更少的比特位来表示浮点数。 就好比把一张 4K 的高清照片(FP16),压缩成一张 8-bit 的像素画(Int8)。虽然细节丢了一点,但大体轮廓还在,而且体积小了很多。

数学公式通常是这样的:

$$Real\_Value \approx Scale \times (Quant\_Value - Zero\_Point)$$

在矩阵乘法中,我们通常把 $A \times B$ 变成:

$$(Scale_A \times Q_A) \times (Scale_B \times Q_B) = (Scale_A \times Scale_B) \times (Q_A \times Q_B)$$

AI Core 的 Cube 单元负责极其快速地算出 $Q_A \times Q_B$(整数乘法),得到一个 Int32 的结果。然后 Vector 单元负责把这个结果乘上 $Scale$,变回 FP16。

二、 Ascend C 实战:Int8 Matmul

在 Ascend C 中,开发 Int8 算子和 FP16 算子最大的区别在于 Matmul 对象的定义

2.1 定义 Int8 Matmul 类型

回忆一下,我们定义 Matmul 时用的是 half(FP16)。现在我们要换成 int8_t

#include "kernel_operator.h"
#include "lib/matmul_intf.h"

using namespace AscendC;

// 模板参数:<位置, 格式, 数据类型>
// 输入 A: int8_t
// 输入 B: int8_t
// 输出 C: int32_t (注意!Int8 乘加必须用 Int32 累加,否则会溢出)
// Bias: int32_t
typedef MatmulType<AscendC::TPosition::GM, CubeFormat::ND, int8_t> aType;
typedef MatmulType<AscendC::TPosition::GM, CubeFormat::ND, int8_t> bType;
typedef MatmulType<AscendC::TPosition::GM, CubeFormat::ND, int32_t> cType;
typedef MatmulType<AscendC::TPosition::GM, CubeFormat::ND, int32_t> biasType;

class KernelInt8Matmul {
public:
    __aicore__ inline void Init(...) {
        // ...
        // 设置 Tiling 等
        // 对于 Int8,L1 Buffer 能放下更多数据,stepM/N/K 可以更大
    }

    __aicore__ inline void Process() {
       mm.IterateAll(cGlobalInt32); // 计算结果直接写回 GM,是 Int32 格式
       // 或者使用分步接口,在 UB 中做反量化
    }

private:
    // 定义 Int8 版本的 Matmul 对象
    Matmul<aType, bType, cType, biasType> mm;
    GlobalTensor<int8_t> aGlobal;
    GlobalTensor<int8_t> bGlobal;
    GlobalTensor<int32_t> cGlobalInt32;
};

2.2 反量化 (Dequantization) 与 输出

Cube 算出来的结果是 int32_t,但后面的算子(如 Softmax, RMSNorm)通常需要 FP16。这就需要在 Vector 单元做反量化

通常我们不会直接把 Int32 写回 GM,而是在 UB 中拦截它。

__aicore__ inline void ProcessWithDequant() {
    // 迭代计算
    while (mm.Iterate()) {
        // 1. 获取 Int32 结果到 UB
        mm.GetResult(cLocInt32);

        // 2. 反量化计算 (Vector Unit)
        // Real = Quant * Scale
        // 需要申请 FP16 的空间
        
        // Step A: Int32 -> FP16
        // 直接 Cast 可能会有精度问题,通常建议 Int32 -> FP32 -> FP16
        Cast(cLocFp32, cLocInt32, RoundMode::CAST_NONE, tileLen);
        
        // Step B: 乘上量化系数 Scale (FP32)
        Muls(cLocFp32, cLocFp32, dequantScale, tileLen);
        
        // Step C: 转回 FP16
        Cast(cLocFp16, cLocFp32, RoundMode::CAST_NONE, tileLen);

        // 3. 搬运回 GM
        DataCopy(cGlobalFp16[offset], cLocFp16, tileLen);
        
        // ... 释放资源 ...
    }
}

三、 避坑指南:Int8 的“陷阱”

3.1 溢出风险

虽然 Int32 范围很大,但如果矩阵的 K 维非常大(比如 K=16384),累加是有可能溢出的。不过在深度学习常见场景下,Int32 足够安全。

3.2 精度崩塌

Int8 量化最大的问题是精度。如果 Scale 选得不好,或者每一层的 Outliers(离群值)太多,模型精度会掉得很厉害。 Ascend C 开发者的职责:保证算子的计算逻辑($Q_A \times Q_B$)是绝对准确的。至于 Scale 怎么算,那是算法工程师在量化校准(Calibration)阶段该操心的事。

3.3 格式转换

Int8 的底层数据排布(Fractal)比 FP16 更复杂。在使用 Matmul 高阶 API 时,尽量使用 CubeFormat::ND 让 API 自动处理,手动处理格式转换非常容易出错。

四、 总结

Int8 量化算子是通往高性能推理的必经之路。

  1. 收益:显存减半,算力翻倍(Cube Int8 性能强劲)。

  2. 实现:只需修改 MatmulType 的模板参数,代码结构与 FP16 基本一致。

  3. 后处理:别忘了在 Vector 单元把 Int32 结果反量化回 FP16。

掌握了这一招,你不仅能写出能用的算子,还能写出快到飞起的算子。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐