ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

aclnnFlatQuantV2 算子拆解:两步矩阵乘 + 动态量化,CANN 上怎么写一次讲清

aclnnFlatQuantV2 算子拆解:两步矩阵乘 + 动态量化,CANN 上怎么写一次讲清

aclnnFlatQuantV2 算子拆解:两步矩阵乘 + 动态量化,CANN 上怎么写一次讲清

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

aclnnFlatQuantV2是 CANN 量化算子里的一个融合操作:它把"两次小矩阵乘 + 动态量化"压进一次 kernel 完成,省掉中间结果的读写。做 LLM 权重/激活低比特压缩的开发者,在 Ascend 950PR/950DT 这类平台上替换掉"矩阵乘 + 单独量化"的算子链时,会直接用到它。产品支持上目前没有覆盖 A2/A3 系列,以 950 系列为主。

V1 还是 V2?三个场景直接判

  • 你要 pergroup 输出float4_e2m1(FP4,mxfp4 路线),又想自定义目标类型的最大值(比如取 8.0、10.0 而不是默认的 6.0)——只有 V2 能传dstTypeMax,选它。
  • 你走int4 / int32 的 pertoken路线(每行一个 FP32 缩放因子),两个版本行为一致,V1 完全够用。
  • 拿不准就直接用 V2、把dstTypeMax设成0.0:此时行为等价于 V1,之后想调量化范围也只需改一个标量参数,不必换接口。

一句话:新接入默认 V2,dstTypeMax=0.0是安全的保守姿势。

拆开算:两步矩阵乘,再量化

第 1 步:右乘 kroneckerP2。这一步本质是沿特征维(x 的尾轴 N)做一次小矩阵变换。P2 是N×N(N ≤ 256)的小方阵,代价可以忽略:

$$ x' = x @ \text{kroneckerP2} $$

第 2 步:左乘 kroneckerP1。P1 是M×M(M ≤ 256)的小方阵,沿 x 的第 1 维做同样的事:

$$ x'' = \text{kroneckerP1} @ x' $$

两步的目的,是把旋转/重排这类低比特量化前的小变换直接融进主数据流,全程不产生中间大张量。另外 pergroup 场景允许kroneckerP2传[0, 0],表示跳过右乘、只做左乘。

第 3 步:按模式量化——pertoken 或 pergroup。pertoken(按行缩放):沿 x'' 第 0 维逐行取最大绝对值,除以 $(7 / \text{clipRatio})$ 得到每行的缩放因子,再整行除过去:

$$ \text{quantScale}_k = \max_i |x''[k, i, :]| ; / ; (7 / \text{clipRatio}) $$

$$ \text{out} = x'' / \text{quantScale} $$

pergroup(按组缩放,对应 float4_e2m1):先把x''从[K, M, N]reshape 成[K, M*N],再按 groupSize=32 分组。组内缩放因子必须是 2 的整数次幂——这样才能塞进 ue8m0 这类只存指数位的格式(ue8m0:FP8 里只保留 8 位指数、没有尾数的特殊编码):

$$ \text{sharedExp} = \log_2\big(\text{reduceMax}(x_2, \text{groupSize}=32)\big) - \text{emax} $$

$$ \text{quantScale} = 2^{,\text{sharedExp}}, \qquad \text{out} = x_2 / \text{quantScale} $$

其中emax是组内元素绝对值的最大值。pergroup 场景的quantScale输出为[K, ceilDiv(M*N, 64), 2]、类型 FLOAT8_E8M0,缩放信息就打包在里面。

第 4 步:dstTypeMax 非零时,块缩放走"上取整指数"。这是 V2 的核心改动。目标类型的最大可表示值 $A_{\max}(DType)$ 不再固定取 6.0,而是由你传入的dstTypeMax(支持 6.0~12.0)指定。每个块先求最大绝对值:

$$ S^b_{fp32} = \frac{A_{\max}(D^b_{fp32})}{A_{\max}(DType)} $$

然后把 $S^b_{fp32}$ 拆成无偏指数 $E^b_{int}$ 与尾数 $M^b_{fixp}$;若它是正规数、$E^b_{int} < 254$ 且尾数非零,指数向上加 1 防溢出:

$$ E^b_{int} \leftarrow E^b_{int} + 1 \quad (\text{仅上述条件成立时}) $$

块缩放因子取 $S^b_{ue8m0} = 2^{E^b_{int}}$,换算因子 $R^b_{fp32} = 1 / S^b_{ue8m0}$,块内每个元素做 $d^i = DType(d^i_{fp32} \cdot R^b_{fp32})$。最终输出是"块缩放因子 + 块内量化数据"的组合。

两段式接口:先问 workspace,再执行

第一段接口完成入参校验、算出所需 workspace,并把整个计算流程打包成aclOpExecutor返回;第二段接口拿这个执行器挂到 stream 上真正跑起来。这就是所有 aclnn 算子的 FlatQuantV2 调用流程,原型如下:

aclnnStatus aclnnFlatQuantV2GetWorkspaceSize( const aclTensor *x, const aclTensor *kroneckerP1, const aclTensor *kroneckerP2, double clipRatio, double dstTypeMax, aclTensor *out, aclTensor *quantScale, uint64_t *workspaceSize, aclOpExecutor **executor); aclnnStatus aclnnFlatQuantV2( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream);

执行前记得:workspaceSize > 0才需要在 Device 侧aclrtMalloc;执行器里已经带了全部参数,第二段不需要再传任何张量。

参数速查

Tensor 入参 / 出参(数据格式均为 ND,全部支持非连续 Tensor;输入侧数据类型仅 FLOAT16、BFLOAT16):

参数方向数据类型shape 与约束
x输入FLOAT16、BFLOAT16[K, M, N];K ≤ 262144,M、N ≤ 256;N 的整除性取决于 out 类型(见下表)
kroneckerP1输入与x一致[M, M],M 与 x 第 2 维一致
kroneckerP2输入与x一致[N, N],N 与 x 第 3 维一致;pergroup 场景允许[0, 0](跳过右乘)
out输出INT4、INT32、FLOAT4_E2M1INT32:[K, M, N/8](x 的 N 须为 8 的倍数);INT4:与 x 相同(N 须为偶数);FLOAT4_E2M1:[K, M*N](N 须为偶数)
quantScale输出FLOAT32、FLOAT8_E8M0INT4/INT32 路线:[K];FLOAT4_E2M1 路线:[K, ceilDiv(M*N, 64), 2],类型为 FLOAT8_E8M0

标量参数:

参数取值含义
clipRatio(0, 1]量化裁剪比例,1 表示不裁剪
dstTypeMax0.0 或 6.0~12.00.0 = 用目标精度默认最大值;6.0~12.0 = 显式指定,仅 FLOAT4_E2M1 生效

一个能跑的完整示例

下面只标注关键路径:构造 Tensor → 两段调用 → 取回结果。完整可运行版(含全部错误处理宏与资源初始化)见仓库matmul/flat_quant/examples/test_aclnn_flat_quant_v2.cpp。

#include "acl/acl.h" #include "aclnnop/aclnn_flat_quant_v2.h" int main() { // 固定写法:device/stream 初始化 int32_t deviceId = 0; aclrtStream stream; CHECK(aclInit(nullptr) == ACL_SUCCESS); CHECK(aclrtSetDevice(deviceId) == ACL_SUCCESS); CHECK(aclrtCreateStream(&stream) == ACL_SUCCESS); // 构造输入输出(INT32 示例:16x16x16 输入 -> 16x16x2 输出) std::vector<int64_t> xShape = {16, 16, 16}; std::vector<int64_t> p1Shape = {16, 16}; std::vector<int64_t> p2Shape = {16, 16}; std::vector<int64_t> outShape = {16, 16, 2}; std::vector<int64_t> scaleShape = {16}; void *xDev = nullptr, *p1Dev = nullptr, *p2Dev = nullptr; void *outDev = nullptr, *scaleDev = nullptr; aclTensor *x = nullptr, *p1 = nullptr, *p2 = nullptr; aclTensor *out = nullptr, *scale = nullptr; double clipRatio = 1.0; double dstTypeMax = 0.0; // 默认值:走目标精度默认最大值 // 填充数据、aclCreateTensor 创建各 Tensor(此处略) CHECK(CreateAclTensor(xShape, xDev, aclDataType::ACL_FLOAT16, &x) == ACL_SUCCESS); CHECK(CreateAclTensor(p1Shape, p1Dev, aclDataType::ACL_FLOAT16, &p1) == ACL_SUCCESS); CHECK(CreateAclTensor(p2Shape, p2Dev, aclDataType::ACL_FLOAT16, &p2) == ACL_SUCCESS); CHECK(CreateAclTensor(outShape, outDev, aclDataType::ACL_INT32, &out) == ACL_SUCCESS); CHECK(CreateAclTensor(scaleShape, scaleDev, aclDataType::ACL_FLOAT, &scale) == ACL_SUCCESS); // 第一段:校验 + 计算 workspace + 拿执行器 uint64_t workspaceSize = 0; aclOpExecutor *executor = nullptr; CHECK(aclnnFlatQuantV2GetWorkspaceSize( x, p1, p2, clipRatio, dstTypeMax, out, scale, &workspaceSize, &executor) == ACL_SUCCESS); void *workspaceAddr = nullptr; if (workspaceSize > 0) { CHECK(aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST) == ACL_SUCCESS); } // 第二段:执行 CHECK(aclnnFlatQuantV2(workspaceAddr, workspaceSize, executor, stream) == ACL_SUCCESS); CHECK(aclrtSynchronizeStream(stream) == ACL_SUCCESS); // 把 out / quantScale 拷回 host 查看(略) // 释放 Tensor、device 内存、workspace、stream,复位 device(略) return 0; }

容易踩的坑,集中说一遍

  1. 三个输入(x、kroneckerP1、kroneckerP2)和两个输出(out、quantScale)一个都不能是空指针,否则第一段直接返回 161001(ACLNN_ERR_PARAM_NULLPTR)。
  2. kroneckerP1/kroneckerP2的数据类型必须与x完全一致,都只能是 FLOAT16 或 BFLOAT16,不支持混用。
  3. out 的尾轴约束与类型绑定:INT32 要求 x 的 N 是 out 尾轴的 8 倍;INT4 要求 x 与 out 的 shape 完全一致;FLOAT4_E2M1 的 out 是[K, M*N]的二维。
  4. pergroup(float4_e2m1)场景下quantScale的 shape 是三维的[K, ceilDiv(M*N, 64), 2]、类型 FLOAT8_E8M0,别按[K]的 FP32 习惯去申请内存。
  5. dstTypeMax只在 FLOAT4_E2M1 输出下生效,合法取值是 0.0 或 6.0~12.0,其他值会被参数校验拒掉。
  6. [0, 0]的kroneckerP2(跳过右乘)仅限 pergroup 场景,pertoken/int4/int32 场景下 P2 必须和 x 的尾轴等维。
  7. 除空指针外的所有校验失败(shape、dtype、clipRatio越界等)统一返回 161002(ACLNN_ERR_PARAM_INVALID):先检查clipRatio ∈ (0, 1]、x 各维范围[1,262144]×[1,256]×[1,256],再逐条对上面几条。
  8. 该算子默认确定性实现;当前产品支持范围是 Ascend 950PR/950DT,A2/A3 系列暂不支持。

【免费下载链接】ops-nn本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-nn

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进