news 2026/9/18 22:36:44

GateGeluQuant 算子深度解析:CANN ops-transformer 中 GeGLU 与 Per-Channel 量化融合 Kernel 的 Tiling 与实现原理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
GateGeluQuant 算子深度解析:CANN ops-transformer 中 GeGLU 与 Per-Channel 量化融合 Kernel 的 Tiling 与实现原理

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,后半部分为 Valuefloat16ND
scale输入Per-Channel 量化缩放因子,shape 为(gbW / 2, )float32ND
out输出GeGLU 计算并量化后的 INT8 结果,shape 为(gbH, gbW / 2)int8_tND
gbH输入输入张量的行数(token 数量)int64_t-
gbW输入输入张量的列数(Gate + Value 拼接后的隐藏维度,必须为偶数)int64_t-
constrait输入是否在量化前对 GeGLU 的 FP32 中间结果进行截断约束,默认为 falsebool-
clampValue输入截断约束的阈值,配合constrait使用,将值截断在[-clampValue, clampValue]内,默认为 128.0float32-
blockDim输入AI Core 的数量,如 Ascend910B 为 40int64_t-
stream输入Device 端的 streamAclrtStream-

几个值得注意的细节:

  • 三个张量的 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目录下的算子族谱中,与该目录中的biasgategelumoegategeluclamp等门控激活类算子互为参照(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:

  1. 计算单元素占用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); // 输出
  2. 求最大 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)。

  3. 确定实际 Tile 宽度tlW_(GATEGELU_QUANT2D_SBUF.cpp):若整行宽度bkW_不超过tlMaxW_,则tlW_ = bkW_(单 Tile 处理整行);否则先计算 Tile 数量(向上取整),再用tlW_ = bkW_ / temp反推每个 Tile 的平均宽度,最后tlW_ = AlignUp(tlW_, 64)向上对齐到 64 元素。这种"先定 Tile 数、再反推宽度"的策略可保证 Tile 数量最少且各 Tile 尽量宽,减少循环开销。

  4. 尾部 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,分别绑定到inscaleout的 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):

  1. Gelu(in_one, in_one):对 Gate 部分(in_one_local)原地计算 GELU 激活;
  2. Mul(in_two, in_one, in_two):Gate(已激活)与 Value(in_two_local)逐元素相乘,得到 GeGLU 结果;
  3. Cast(infloat_local, in_two, CAST_NONE):将 FP16 结果转为 FP32,为高精度量化计算做准备;
  4. [可选约束]constrait_为 true,则用Mins(infloat_local, clampValue_)Maxs(infloat_local, -clampValue_)将 FP32 结果截断在[-clampValue_, clampValue_]内;
  5. Mul(infloat_local, infloat_local, scale_local):乘以 Per-Channel 量化缩放因子(按列对应,逐元素广播);
  6. Cast(infloat_local, infloat_local, CAST_RINT):以四舍五入(round-to-nearest)模式将 FP32 取整为整数(仍以 FP32 格式存储);
  7. Mins/Maxs:将取整结果截断到[-128.0, 127.0]的 INT8 表示范围,防止溢出;
  8. 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(...))校验inscaleout三个张量均位于 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_OPTIONSCOMMON_INCLUDE_DIRS。其所在目录由 experimental/moe/CMakeLists.txt 遍历各子目录的CMakeLists.txt后统一add_subdirectory引入,因此它是experimental/moe算子族整体构建链路上的一员。

测试脚本与精度标准

仓库提供了 CPU 参考实现对拍测试 test_gategelu_quant.py,验证流程如下:

  • 测试配置GBH=4GBW=64BLOCKDIM=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 取值即可扩展覆盖不同分核(blockDimgbH大小关系)与尾部 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),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/18 22:35:31

电商图片批量采集实战:DOM+内存双源提取方案

1. 这不是“爬虫教程”&#xff0c;而是一份电商图片批量采集的实战手记我第一次接到这个需求&#xff0c;是帮一个做跨境选品的朋友整理竞品图库。他每天要翻200个淘宝、京东、亚马逊、ASOS的商品页&#xff0c;手动右键保存主图、细节图、场景图、白底图……平均每个页面耗时…

作者头像 李华
网站建设 2026/9/18 22:34:56

无网也能写 AI 会议纪要:anarlog 离线模式完整指南

无网也能写 AI 会议纪要&#xff1a;anarlog 离线模式完整指南 【免费下载链接】anarlog Open source Granola AI Alternative 项目地址: https://gitcode.com/GitHub_Trending/hy/anarlog anarlog 的离线模式&#xff1a;一款开源 AI 会议笔记应用&#xff0c;监听你的…

作者头像 李华
网站建设 2026/9/18 22:29:29

代码审查实战:从原则到落地,构建高效Code Review流程

1. 代码审查到底在审什么&#xff1a;先想清楚这件事值不值得做代码审查&#xff08;Code Review&#xff09;这词儿&#xff0c;但凡是写代码的&#xff0c;基本都听过。有些人觉得它是形式主义&#xff0c;走个过场点个赞就完事&#xff1b;有些人觉得它是团队里最有价值的一…

作者头像 李华