GateGeluQuant 算子深度解析:CANN ops-transformer 中 GeGLU 与 Per-Channel 量化融合 Kernel 的 Tiling 与实现原理
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
导读
GateGeluQuant 是 CANN ops-transformer 仓库(experimental/moe/gategelu_quant)中提供的一个面向大模型 FFN 层的 NPU 融合算子:它把 Gated GLU 结构中的 GELU 激活、门控逐元素乘法、激活值截断约束和 Per-Channel 动态量化(输出 INT8)四个操作融合进单个 AI Core Kernel,直接服务于 LLaMA、Qwen、DeepSeek 等大模型 W8A8 量化推理部署。本文以该算子官方 README 为骨架,结合仓库内 GATEGELU_QUANT2D_SBUF.cpp 源码、test_gategelu_quant.py 测试与 CMakeLists.txt 编译配置,完整讲解其数学原理、参数语义、Tiling 策略、Kernel 计算流水线、编译与精度验证方法,读完即可掌握该融合算子的设计与使用全貌。
一、算子概览与产品支持情况
GateGeluQuant 的定位是"GeGLU + Per-Channel Quantization"的融合算子:输入张量按列均分为 Gate 与 Value 两部分,对 Gate 部分施加 GELU 激活函数后与 Value 部分逐元素相乘,再经过可选的截断约束,最后乘以缩放因子量化输出为 INT8。
产品支持情况如下(引自 README.md):
| 产品 | 是否支持 |
|---|---|
| Atlas A2 训练系列产品 | 是 |
与之对应,该算子的编译配置(CMakeLists.txt)中显式指定了目标 SoC 版本为Ascend910B1、核类型为VecCore:
set_source_files_properties( ${GATEGELU_QUANT_NPU_SOURCES} PROPERTIES LANGUAGE CXX COMPILE_FLAGS "--cce-soc-version=Ascend910B1 --cce-soc-core-type=VecCore --cce-auto-sync -xcce" )这意味着该 Kernel 面向 Ascend 910B 系列(Atlas A2 训练系列)的向量核(VecCore)编写,使用 AscendC 编程模型实现。
二、数学原理:GeGLU + Per-Channel 量化
GLU(门控线性单元,Gated Linear Unit)将输入在最后一维拆成两支,一支经过激活函数后作为"门",另一支直接参与逐元素乘法。GateGeluQuant 中的"门"激活函数选用 GELU,因此得到 GeGLU。
设输入张量in的 shape 为(gbH, gbW),其中gbW = 2 × hidden_size,前半列([:, :W])为 Gate,后半列([:, W:])为 Value,且W = gbW / 2。算子整体数学公式如下:
$$intermediate = GELU(input[:, :W]) \odot input[:, W:]$$
$$if \ constrait: \ intermediate = Clamp(intermediate, -clampValue, clampValue)$$
$$output = Quantize(intermediate \times scale) \quad \in [-128, 127]$$
其中⊙表示逐元素乘法,Quantize表示四舍五入取整(round)并截断到 INT8 表示范围[-128, 127];scale是长度为gbW / 2的 Per-Channel 量化缩放因子,即输出张量的每一列(channel)对应一个独立的 float32 缩放系数,而非整个张量共用一个标量,这正是"Per-Channel"量化的含义。
注意:constrait为 true 时的截断约束施加在"乘以 scale 之前"的 FP32 GeGLU 中间结果上,其目的是在低精度量化前先行限制激活值幅值,避免后续放大后溢出 INT8 表示范围(详见下文 Kernel 计算流水线的步骤 4)。
三、参数说明
下表完整继承自 README.md,并补充了默认值、shape 关系等实操细节:
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
| in | 输入 | Gate 和 Value 拼接的输入张量,shape 为(gbH, gbW),其中gbW = 2 × hidden_size,前半部分为 Gate,后半部分为 Value | float16 | ND |
| scale | 输入 | Per-Channel 量化缩放因子,shape 为(gbW / 2, ) | float32 | ND |
| out | 输出 | GeGLU 计算并量化后的 INT8 结果,shape 为(gbH, gbW / 2) | int8_t | ND |
| gbH | 输入 | 输入张量的行数(token 数量) | int64_t | - |
| gbW | 输入 | 输入张量的列数(Gate + Value 拼接后的隐藏维度,必须为偶数) | int64_t | - |
| constrait | 输入 | 是否在量化前对 GeGLU 的 FP32 中间结果进行截断约束,默认为 false | bool | - |
| clampValue | 输入 | 截断约束的阈值,配合constrait使用,将值截断在[-clampValue, clampValue]内,默认为 128.0 | float32 | - |
| blockDim | 输入 | AI Core 的数量,如 Ascend910B 为 40 | int64_t | - |
| stream | 输入 | Device 端的 stream | AclrtStream | - |
几个值得注意的细节:
- 三个张量的 shape 存在严格关联:
in为(gbH, gbW),scale为(gbW / 2,),out为(gbH, gbW / 2),即列方向经量化后缩减一半,行数保持不变。 gbW必须为偶数,否则无法均分为 Gate / Value 两部分(源码Init中直接以bkW_ = gbW_ / 2作为输出列宽)。constrait/clampValue与源码成员constrait_/clampValue_的默认值(false / 128.0f)完全一致(见 GATEGELU_QUANT2D_SBUF.cpp),注意"约束"一词在文档与源码中的拼写均为constrait。blockDim传入后并非无条件生效:启动接口gateGeluQuant2dSBuf_lanuch中带有if (gbH < blockDim) { blockDim = gbH; }的保护逻辑(GATEGELU_QUANT2D_SBUF.cpp),即当 token 数(行数)少于 AI Core 数量时,实际并行核数会被裁剪为行数,避免空转核。
四、约束说明
- 输入张量的列数
gbW必须为偶数,以便均分为 Gate 和 Value 两部分。 - 数据类型约束:输入为 float16,Scale 为 float32,输出为 int8_t。
- 从实现角度还可以补充两条推导约束:其一,tile 宽度在 UB 容量计算后需对齐到 64 元素粒度(对应 128 字节对齐,见下文 Tiling 策略);其二,输出是 INT8 定点结果,因此在量化前以
RoundMode::CAST_RINT(四舍五入)取整,再以Mins/Maxs截断到[-128.0, 127.0],防止量化溢出。
五、应用价值:W8A8 量化推理中的 FFN 关键步骤
在 LLaMA、Qwen、DeepSeek 等大语言模型的 W8A8 量化推理部署中,GeGLU 激活及其输出量化是 FFN 层计算的核心步骤。若按朴素方式实现,这一路径需要:先写回 FP16/FP32 的 GeGLU 中间结果到 Global Memory,再启动独立的量化 Kernel 读入、缩放、取整后写回 INT8,中间会引入额外的显存带宽占用与 Kernel 启动开销。
GateGeluQuant 的价值在于把GELU 激活、门控乘法、激活值截断及 Per-Channel 动态量化四个操作融合为一个 Kernel,彻底消除了 FP16/FP32 中间结果的 Global Memory 写回与读入(中间结果始终停留在片上 UB 中,见下文 Compute 流水线),从而大幅降低显存带宽压力和 Kernel 启动开销,提升端到端推理性能(以上效果描述引自 README.md 的"价值/作用"章节,属于项目文档声明)。从结构上看,它位于 MoE / FFN 算子在experimental/moe目录下的算子族谱中,与该目录中的biasgategelu、moegategeluclamp等门控激活类算子互为参照(experimental/moe)。
六、设计方案(一):Tiling 策略
分核策略:按行(gbH)切分
算子按行维度进行分核,每个 Core 处理⌈gbH / blockNum⌉行数据(向上取整)。核心代码位于Init(GATEGELU_QUANT2D_SBUF.cpp):
bkLoop_ = (int64_t)(gbH_ / blockNum_); if (gbH_ % blockNum_ != 0) { bkLoop_ += 1; }每个 Core 负责的行索引并非连续区间,而是"按核号跨步"分布:第i轮处理的行号是i * blockNum_ + blockIdx_。行循环Process中通过if (i * blockNum_ + blockIdx_ < gbH_)判断当前行是否有效,从而跳过因向上取整带来的尾部多余迭代(GATEGELU_QUANT2D_SBUF.cpp)。这种跨步分核方式使相邻核尽量处理相邻行,配合 GELU 等逐行运算特性,可均衡各核负载。
分块策略:按列(bkW)在 UB 容量约束下切 Tile
在列方向上,输出宽度bkW_ = gbW / 2,每个 Core 在每行内按 UB 可用容量继续切分 Tile:
计算单元素占用:
Init中按"每个输出元素所需 Buffer"估算字节占用temp——Gate、Value 各 1 个 half(2 字节),1 个 float scale(4 字节),1 个 int8 输出(1 字节),对应源码(GATEGELU_QUANT2D_SBUF.cpp):int64_t temp = BUFFER_NUM * bkH_ * sizeof(half) * 2; // Gate + Value 输入 temp += BUFFER_NUM * 2 * sizeof(half); temp += BUFFER_NUM * sizeof(float); // scale temp += BUFFER_NUM * bkH_ * sizeof(int8_t); // 输出求最大 Tile 宽度:
tlMaxW_ = UB_MAX_BYTES / temp,其中UB_MAX_BYTES = 184 * 1024(184KB,见 GATEGELU_QUANT2D_SBUF.cpp),随后tlMaxW_ = tlMaxW_ / 64 * 64向下对齐到 64 元素,以满足 128 字节对齐要求(half 元素 2 字节,64 × 2 = 128)。确定实际 Tile 宽度
tlW_(GATEGELU_QUANT2D_SBUF.cpp):若整行宽度bkW_不超过tlMaxW_,则tlW_ = bkW_(单 Tile 处理整行);否则先计算 Tile 数量(向上取整),再用tlW_ = bkW_ / temp反推每个 Tile 的平均宽度,最后tlW_ = AlignUp(tlW_, 64)向上对齐到 64 元素。这种"先定 Tile 数、再反推宽度"的策略可保证 Tile 数量最少且各 Tile 尽量宽,减少循环开销。尾部 Tile 处理:
tlTailW_ = bkW_ % tlW_为尾部不足一个完整 Tile 的宽度;tlAlignTailW_ = AlignUp(tlTailW_, 64)为用于向量计算的对齐宽度(向量指令要求对齐),而实际有效宽度real_tlW仅用于搬入/搬出(DataCopyPad按实际字节数搬运)。tlLoop_ = ceil(bkW_ / tlW_)给出每行内的 Tile 循环次数。
七、设计方案(二):Kernel 侧设计
整个 Kernel 采用Init + Process两阶段结构,其中 Process 内又分为数据搬入(CopyIn)、计算(Compute)、数据搬出(CopyOut)三步,并使用单缓冲(BUFFER_NUM = 1)机制,即计算与搬入搬出不叠加流水(无 double buffer 双缓冲乒乓),简化了队列与同步管理。
初始化阶段(Init)
Init完成四类工作(GATEGELU_QUANT2D_SBUF.cpp):
- 分核参数:
bkLoop_(每个 Core 处理的行数)、blockIdx_(当前 Core 编号,取自GetBlockIdx()),并保存gbH_、gbW_、constrait_、clampValue_; - 分块参数:
bkW_ = gbW / 2,基于 UB 容量计算tlMaxW_,进而确定tlW_、tlTailW_、tlAlignTailW_、tlLoop_(见上文分块策略);此外还计算了按 32 对齐的整行宽度bkAlignW_作为地址对齐参考; - GM Tensor 映射:建立
inGm_(half)、scaleGm_(float)、outGm_(int8)三个 GlobalTensor,分别绑定到in、scale、out的 GM 地址并声明元素个数; - 队列初始化:VECIN 输入队列
inQueIn_(Gate 与 Value 合并存放,深度BUFFER_NUM)、inQueScale_(缩放因子,深度 1),VECOUT 输出队列outQueOut_(INT8 输出,深度BUFFER_NUM)。
计算流程(Process)
Process 外层按行循环(FOR i = 0 TO bkLoop_),内层处理尾部 Tile 与完整 Tile 两种路径(GATEGELU_QUANT2D_SBUF.cpp)。结合源码,各阶段细节如下:
CopyIn(数据搬入):从 GM 搬入拼接的 Gate 和 Value 数据到同一块本地内存,in_local[0]存 Gate,in_local[tlW_]存 Value——两次DataCopyPad的 GM 源地址相差bkW_个 half 元素(即从 Gate 起始列跳到 Value 起始列),行偏移为(i * blockNum_ + blockIdx_) * (bkW_ * 2);同时将当前 Tile 对应的 float32scale数据搬入inQueScale_队列(GATEGELU_QUANT2D_SBUF.cpp)。
Compute(高度融合的量化计算流水线),这是整个算子的核心,共 8 步,全部在 UB 内完成,中间结果不落 GM(GATEGELU_QUANT2D_SBUF.cpp):
Gelu(in_one, in_one):对 Gate 部分(in_one_local)原地计算 GELU 激活;Mul(in_two, in_one, in_two):Gate(已激活)与 Value(in_two_local)逐元素相乘,得到 GeGLU 结果;Cast(infloat_local, in_two, CAST_NONE):将 FP16 结果转为 FP32,为高精度量化计算做准备;- [可选约束]若
constrait_为 true,则用Mins(infloat_local, clampValue_)与Maxs(infloat_local, -clampValue_)将 FP32 结果截断在[-clampValue_, clampValue_]内; Mul(infloat_local, infloat_local, scale_local):乘以 Per-Channel 量化缩放因子(按列对应,逐元素广播);Cast(infloat_local, infloat_local, CAST_RINT):以四舍五入(round-to-nearest)模式将 FP32 取整为整数(仍以 FP32 格式存储);Mins/Maxs:将取整结果截断到[-128.0, 127.0]的 INT8 表示范围,防止溢出;Cast(in_one_local, infloat_local, CAST_NONE)与Cast(out_local, in_one_local, CAST_RINT):将结果从 FP32 经 FP16 中转,最终转为 INT8 输出。
需要说明的是,步骤 8 的"FP32 → FP16 → INT8"两级 Cast 是为了利用向量指令的数据通路特性完成最终定点化,属于实现层面的精度/指令权衡。
CopyOut(数据搬出):将 INT8 计算结果从 UB 搬回 GM,输出偏移量为offset = (i * blockNum_ + blockIdx_) * bkW_ + j * tlW_(GATEGELU_QUANT2D_SBUF.cpp),与输入行的跨步映射一一对应。
Kernel 入口与启动
Kernel 采用extern "C" __global__ __aicore__导出,参数为(gbH, gbW, in, scale, out, constrait, clampValue),通过gateGeluQuant2dSBuf_kernel<<<blockDim, nullptr, stream>>>启动(GATEGELU_QUANT2D_SBUF.cpp)。gategelu_quant_lanuch作为对外 C 接口返回启动状态,其中包含上文提到的gbH < blockDim时裁剪核数的保护。
八、PyTorch 侧接入方式
除了裸 Kernel 启动接口,仓库还提供了 PyTorch 算子接入封装gategelu_quant_npu(GATEGELU_QUANT2D_SBUF.cpp),其要点包括:
- 使用
TORCH_CHECK(torch_npu::utils::is_npu(...))校验in、scale、out三个张量均位于 NPU 设备上; - 从输入张量推导
gbH = inTensor.size(0)、gbW = inTensor.size(1); - 通过
c10_npu::getCurrentNPUStream()获取当前 NPU stream; - 以
at_npu::native::OpCommand::RunOpApi("GategeluQuant", acl_call)异步执行 Kernel; - 通过
TORCH_LIBRARY_IMPL(ascend_ops, PrivateUse1, m)注册自定义算子gategelu_quant,供torch.ops.ascend_ops.gategelu_quant(...)调用。
结合测试脚本 test_gategelu_quant.py 中的调用示例,NPU 侧最小调用方式为:
import torch import torch_npu import ascend_ops GBH, GBW, BLOCKDIM = 4, 64, 4 input_npu = torch.randn(GBH, GBW, dtype=torch.float16).npu() # (gbH, gbW) fp16 scale_npu = torch.ones(GBW // 2, dtype=torch.float32).npu() # (gbW/2,) fp32 out_npu = torch.empty(GBH, GBW // 2, dtype=torch.int8).npu() # (gbH, gbW/2) int8 torch.ops.ascend_ops.gategelu_quant(BLOCKDIM, input_npu, scale_npu, out_npu, False, 128.0) # 参数依次为: blockDim, in, scale, out, constrait, clampValue使用时需满足上文参数表的类型与 shape 约束,且三张量必须在 NPU 上;constrait=False表示关闭截断约束(测试即采用该配置)。
九、编译与精度验证
编译集成
该算子通过 CMakeLists.txt 以对象库形式加入构建:以file(GLOB ...)收集目录内全部.cpp,设置Ascend910B1/VecCore编译属性后创建gategelu_quant_objects对象库,并挂接COMMON_COMPILE_OPTIONS与COMMON_INCLUDE_DIRS。其所在目录由 experimental/moe/CMakeLists.txt 遍历各子目录的CMakeLists.txt后统一add_subdirectory引入,因此它是experimental/moe算子族整体构建链路上的一员。
测试脚本与精度标准
仓库提供了 CPU 参考实现对拍测试 test_gategelu_quant.py,验证流程如下:
- 测试配置:
GBH=4、GBW=64、BLOCKDIM=4,输入用torch.randn(...) * 10生成 fp16 随机数据,scale 用torch.ones(...) * 10构造,constrait=False; - CPU 参考实现
gategelu_quant_cpu:按列拆分in_one = input[:, :GBW//2]、in_two = input[:, GBW//2:],以 tanh 近似 GELU(0.5x(1+tanh(sqrt(2/π)(x+0.044715x³))))计算激活、门控乘法、乘 scale、截断到 INT8 范围后round取整,与算子公式逐一对齐; - 结果对比:同时统计绝对误差与基于 FP32 中间值的相对误差;
- 判定标准:最大绝对误差 ≤ 1(INT8 量化本身允许 1 个 LSB 的取整误差),最大相对误差 ≤ 0.01(1%),两者同时满足即判定
CPU comparison test PASSED。
该测试为验证算子数值正确性提供了可直接复现的对拍方法:替换 shape / scale 取值即可扩展覆盖不同分核(blockDim与gbH大小关系)与尾部 Tile(bkW_ % tlW_ != 0)路径。
十、小结
GateGeluQuant 是 ops-transformer 仓库中一个典型的"以访存优化为核心的融合算子"范例:按行跨步分核 + 按列 UB 容量分块的双层 Tiling 策略保证了多核并行度与片上存储的匹配;8 步在片内完成的融合量化流水线(GELU → 门控乘 → FP32 提升 → 可选截断 → 乘 scale → 取整 → INT8 范围截断 → 定点化)使 FP16/FP32 中间结果始终不落 Global Memory;而constrait/clampValue的运行时开关则为不同量化策略(是否预截断激活值)保留了灵活性。配合仓库内的 CPU 对拍测试与 Ascend910B1 编译配置,该算子可以作为在 CANN 生态中实现"GeGLU + Per-Channel 量化"融合 Kernel 的完整参考实现。
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考