aclnnDequantRopeQuantKvcache 算子深度解析:NPU 上融合反量化、RoPE 旋转位置编码与量化 KV Cache 写入的实战指南
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
导读
aclnnDequantRopeQuantKvcache是 CANN ops-transformer 算子库中面向大模型推理场景的高阶融合算子:它将反量化(Dequant)→ QKV 切分 → 旋转位置编码(RoPE)→ 量化(Quant)→ KV Cache 写入更新五个步骤融合为一次 NPU 算子调用,避免了多个中间张量的 Device↔Host 往返与多次 Kernel 启动开销。阅读本文后,你将掌握该算子的完整计算流程、两段式 aclnn 接口的每个参数语义与取值约束、两种 KV Cache 更新模式(contiguous / page)的差异,并能够依据仓库中的完整调用示例与源码实现,在自己的推理管线中正确接入该算子。
本算子位于仓库的 posembedding/dequant_rope_quant_kvcache 目录,完整源码可参考该目录下的 op_host 与 op_kernel 实现。
产品支持情况
根据算子官方文档与目录内 README.md 说明,当前支持情况如下:
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | √ |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | √ |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | √ |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | × |
| Atlas 训练系列产品 | × |
| Kirin X90 处理器系列产品 | √ |
| Kirin 9030 处理器系列产品 | √ |
需要特别说明两点:
- Kirin 平台不支持 BFLOAT16(README 明确标注),因此在 Kirin X90 / Kirin 9030 上使用本算子时,x 仅支持 FLOAT16 / INT32 两种类型。这一差异在算子注册源码中也有体现:dequant_rope_quant_kvcache_def.cpp 为 Kirin 平台单独定义了
XDtypeListKirin/cosDtypeListKirin等数据类型列表,其中不含DT_BF16,且 AICore 配置(kirinx90、kirin9030)通过GetKirinCoreConfig()统一挂载。 - 在 dequant_rope_quant_kvcache_def.cpp 中可以看到,算子通过
this->AICore().AddConfig("ascend910b")、AddConfig("ascend910_93")、AddConfig("ascend950")完成 AICore 配置注册,与上表支持的产品一一对应。
算子功能与计算流程
功能总览
算子对输入张量x执行如下流水线:
- Dequant(可选):对输入
x进行反量化,恢复高精度浮点表示; - 切分(Split):按属性
sizeSplits给出的长度对尾轴(dim=-1)进行切分,得到q、k、vOut三段; - RoPE(旋转位置编码):对
q、k应用基于cos、sin的旋转位置编码,生成qOut和kOut; - Quant(量化):对
kOut与vOut分别使用scaleK/offsetK、scaleV/offsetV进行静态量化,输出 INT8 数据; - KV Cache 更新:根据
indices指定的 token 位置信息,将量化后的 k、v 写入kCacheRef与vCacheRef。
从计算路径看,该算子本质上是把 LLM 推理中"权重反量化 + QKV 投影结果切分 + 位置编码 + KV 量化缓存"这段高频热点串成一条端到端的单算子流水,适合与 PagedAttention 类推理框架配合使用。
计算步骤公式
算子的计算过程可形式化为以下五步:
第 1 步:反量化(可选)
$$ dequantX = Dequant(x, weightScaleOptional, activationScaleOptional, biasOptional) $$
第 2 步:尾轴切分
$$ q, k, vOut = SplitTensor(dequantX, dim=-1, sizeSplits) $$
第 3 步:旋转位置编码
$$ qOut, kOut = ApplyRotaryPosEmb(q, k, cos, sin) $$
第 4 步:静态量化
$$ quantK = Quant(kOut, scaleK, offsetKOptional) $$
$$ quantV = Quant(vOut, scaleV, offsetVOptional) $$
第 5 步:KV Cache 更新(两种模式见下文)
两种 KV Cache 更新模式
模式一:cacheModeOptional = "contiguous"(默认)
连续式缓存,按 batch 维度逐位置写入:
$$ kCacheRef[i][indices[i]] = quantK[i] $$
$$ vCacheRef[i][indices[i]] = quantV[i] $$
模式二:cacheModeOptional = "page"
分页式缓存(Paged KV Cache),先将 4 维 cache 张量展平成[总页数, 页内行数, 列数]的视图,再按下标写入:
$$ kCacheRefView = kCacheRef.view(-1, kCacheRef[-2], kCacheRef[-1]) $$
$$ vCacheRefView = vCacheRef.view(-1, vCacheRef[-2], vCacheRef[-1]) $$
$$ kCacheRefView[indices[i]] = quantK[i] $$
$$ vCacheRefView[indices[i]] = quantV[i] $$
两种模式的选择会直接影响indices的 shape 语义(见参数表与约束说明),并且 tiling 阶段会通过CheckPaCacheMode()(见 dequant_rope_quant_kvcache_tiling.cpp)识别是否为 page 模式,进而把batch展开为B*S、seqlen置 1 参与任务划分。
两段式接口与函数原型
该算子采用 CANN 算子库标准的两段式接口(详见仓库文档 两段式接口说明):必须先调用aclnnDequantRopeQuantKvcacheGetWorkspaceSize接口获取入参校验结果与所需 workspace 大小,再调用aclnnDequantRopeQuantKvcache接口执行计算。
第一段接口原型:
aclnnStatus aclnnDequantRopeQuantKvcacheGetWorkspaceSize( const aclTensor *x, const aclTensor *cos, const aclTensor *sin, aclTensor *kCacheRef, aclTensor *vCacheRef, const aclTensor *indices, const aclTensor *scaleK, const aclTensor *scaleV, const aclTensor *offsetKOptional, const aclTensor *offsetVOptional, const aclTensor *weightScaleOptional, const aclTensor *activationScaleOptional, const aclTensor *biasOptional, const aclIntArray *sizeSplits, char *quantModeOptional, char *layoutOptional, bool kvOutput, char *cacheModeOptional, const aclTensor *qOut, const aclTensor *kOut, const aclTensor *vOut, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型:
aclnnStatus aclnnDequantRopeQuantKvcache( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)第一段接口完成入参校验与 workspace 大小计算;第二段接口在指定stream上真正下发执行。两段接口均返回aclnnStatus状态码,具体取值参见仓库文档 aclnn 返回码说明。
参数详解
第一段接口参数表
下表完整列出aclnnDequantRopeQuantKvcacheGetWorkspaceSize的全部参数语义。其中"非连续 Tensor"标记为 √ 表示该输入支持非连续(带 stride)张量。
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| x | 输入 | 公式中用于切分的输入 x | shape 为[B, S, H]或[B, H],H=(Nq+Nkv+Nkv)*D。x 的尾轴小于等于 4096,且按 64 对齐 | FLOAT16、BFLOAT16、INT32 | ND | 2-3 | √ |
| cos | 输入 | 公式中用于位置编码的输入 cos | x 为 3 维时 shape 为[B, S, 1, D],x 为 2 维时 shape 为[B, D] | FLOAT16、BFLOAT16 | ND | 2,4 | √ |
| sin | 输入 | 公式中用于位置编码的输入 sin | x 为 3 维时 shape 为[B, S, 1, D],x 为 2 维时 shape 为[B, D] | 和 cos 保持一致 | ND | 2,4 | √ |
| kCacheRef | 输入 | 公式中用于缓存 k 的输入 kCacheRef | shape 为[C_1, C_2, Nkv, D] | INT8 | ND | 4 | √ |
| vCacheRef | 输入 | 公式中用于缓存 v 的输入 vCacheRef | shape 为[C_1, C_2, Nkv, D] | INT8 | ND | 4 | √ |
| indices | 输入 | 表示 Kvcache 的 token 位置信息的输入 indices | 当 cache_mode 为 page 且 x 为 3 维时 shape 为[B*S],否则 shape 为[B] | INT32 | ND | 1 | √ |
| scaleK | 输入 | 公式中的输入 scaleK,用于量化 k 的 scale 因子 | 元素个数为Nkv*D,推荐 shape 为[Nkv, D],兼容一维展平 shape[Nkv*D] | FLOAT | ND | ≥1 | √ |
| scaleV | 输入 | 公式中的输入 scaleV,用于量化 v 的 scale 因子 | 元素个数为Nkv*D,推荐 shape 为[Nkv, D],兼容一维展平 shape[Nkv*D] | FLOAT | ND | ≥1 | √ |
| offsetKOptional | 输入 | 公式中的输入 offsetKOptional,用于量化 k 的 offset 因子 | 元素个数为Nkv*D,推荐 shape 为[Nkv, D],兼容一维展平 shape[Nkv*D] | FLOAT | ND | ≥1 | √ |
| offsetVOptional | 输入 | 公式中的输入 offsetVOptional,用于量化 v 的 offset 因子 | 元素个数为Nkv*D,推荐 shape 为[Nkv, D],兼容一维展平 shape[Nkv*D] | FLOAT | ND | ≥1 | √ |
| weightScaleOptional | 输入 | 公式中的输入 weightScaleOptional,用于反量化的权重 scale 因子 | shape 为[H] | FLOAT | ND | 1 | √ |
| activationScaleOptional | 输入 | 公式中的输入 activationScaleOptional,用于反量化的激活 scale 因子 | x 为 3 维时 shape 为[B*S],x 为 2 维时 shape 为[B] | FLOAT | ND | 1 | √ |
| biasOptional | 输入 | 公式中的输入用于反量化的偏置 biasOptional | shape 为[H] | FLOAT、FLOAT16、INT32、BFLOAT16 | ND | 1 | √ |
| sizeSplits | 输入 | 表示输入的 qkv 进行切分的长度 | size 大小为 3,值为[Nq*D, Nkv*D, Nkv*D] | AclIntArray | - | - | - |
| quantModeOptional | 输入 | 表示支持的量化类型 | 目前仅传入"static" | CHAR | - | - | - |
| layoutOptional | 输入 | 表示支持的数据格式 | 目前仅支持"BSND" | CHAR | - | - | - |
| kvOutput | 输入 | Host 侧布尔值,表示是否输出 kOut 和 vOut | 为 true 时输出有效 shape 的 kOut 和 vOut;为 false 时 kOut 和 vOut 的 shape 为空 | BOOL | - | - | - |
| cacheModeOptional | 输入 | 表示 kCacheRef 的更新方式 | 目前仅支持"page"和"contiguous",默认为"contiguous" | CHAR | - | - | - |
| qOut | 输出 | 公式中经旋转位置编码后的 q | x 为 3 维时 shape 为[B, S, Nq, D],x 为 2 维时 shape 为[B, Nq, D]。数据类型与 cos、sin 保持一致 | FLOAT16、BFLOAT16 | ND | 3-4 | × |
| kOut | 输出 | 公式中经旋转位置编码后的 k | kvOutput 为 true 时,x 为 3 维时 shape 为[B, S, Nkv, D],x 为 2 维时 shape 为[B, Nkv, D];kvOutput 为 false 时 shape 为空。数据类型与 cos、sin 保持一致 | FLOAT16、BFLOAT16 | ND | 1-4 | × |
| vOut | 输出 | 公式中切分得到的 v | kvOutput 为 true 时,x 为 3 维时 shape 为[B, S, Nkv, D],x 为 2 维时 shape 为[B, Nkv, D];kvOutput 为 false 时 shape 为空。数据类型与 cos、sin 保持一致 | FLOAT16、BFLOAT16 | ND | 1-4 | × |
| workspaceSize | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
| executor | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
参数语义的源码佐证
上述参数定义可以在算子注册源码 dequant_rope_quant_kvcache_def.cpp 中得到印证:
x、cos、sin、k_cache、v_cache、indices、scale_k、scale_v为REQUIRED(必选)输入;offset_k、offset_v、weight_scale、activation_scale、bias为OPTIONAL(可选)输入;- 所有输入均声明了
AutoContiguous(),与参数表中"非连续 Tensor"的支持情况一致; - 属性侧:
size_splits为 REQUIRED 的ListInt,quant_mode默认"static"、layout默认"BSND"、kv_output默认false、cache_mode默认"contiguous",与文档描述完全对应; - 输入/输出均限定
FORMAT_ND格式,即文档参数表中的"数据格式 ND"。
第二段接口参数表
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口aclnnDequantRopeQuantKvcacheGetWorkspaceSize获取 |
| executor | 输入 | op 执行器,包含了算子计算流程 |
| stream | 输入 | 指定执行任务的 Stream |
返回值与错误码
两段接口均返回aclnnStatus状态码。第一段接口完成入参校验,出现以下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 输入和输出的 Tensor 是空指针 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 输入和输出的数据类型不在支持的范围内 |
完整返回码语义参见仓库文档 aclnn 返回码说明。
约束说明
使用该算子时必须满足以下约束:
- 确定性计算:
aclnnDequantRopeQuantKvcache默认确定性实现。 - contiguous 模式下的 indices 取值:
kCacheRef的第 0 维大于等于 x 的第 0 维。x 为 3 维时,indices 数据值大于等于 0 且小于等于kCacheRef的第 1 维减 x 的第 1 维;x 为 2 维时,indices 数据值大于等于 0 且小于等于kCacheRef的第 1 维减 1。 - page 模式下的 indices 取值:indices 数据值大于等于 0,小于
kCacheRef的第 0 维 × 第 1 维,且不重复。 - 非 INT32 输入:输入 x 不为 INT32 时,x、cos、sin 与输出 qOut、kOut、vOut 的数据类型保持一致,此时
activationScaleOptional、weightScaleOptional、biasOptional不生效。 - INT32 输入(反量化路径):输入 x 为 INT32 时,cos、sin 与输出 qOut、kOut、vOut 的数据类型保持一致,此时
weightScaleOptional必选,activationScaleOptional、biasOptional可选(biasOptional不需要与其他输入类型一致)。 - 尾轴限制:x 的尾轴小于等于 4096,且按 64 对齐。
- Kirin 平台:Kirin X90 / Kirin 9030 处理器系列产品不支持 BFLOAT16。
这些约束在 tiling 阶段会被进一步强制校验。例如 dequant_rope_quant_kvcache_tiling.cpp 中会检查sizeSplits长度必须为 3、k 与 v 的切分长度必须相等、hiddenSize必须为 16 的倍数、qHiddenSize/vHiddenSize必须为hiddenSize的整数倍(由此推出 Nq、Nkv 必须为整数)、x 的尾轴必须等于三段切分长度之和等;同时 tiling 还会校验 scale/offset 的元素个数必须等于Nkv*D(quantShapeSize),与参数表要求一致。
调用示例
仓库在 examples/test_aclnn_dequant_rope_quant_kvcache.cpp 中提供了可直接参考的完整示例,其调用流程与本算子文档中的示例代码一致,编译和执行过程请参考仓库文档 编译与运行样例。核心代码如下:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_dequant_rope_quant_kvcache.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shapeSize = 1; for (auto i : shape) { shapeSize *= i; } return shapeSize; } void PrintOutResult(std::vector<int64_t> &shape, void** deviceAddr) { auto size = GetShapeSize(shape); std::vector<int8_t> resultData(size, 0); auto ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), *deviceAddr, size * sizeof(resultData[0]), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return); for (int64_t i = 0; i < size; i++) { LOG_PRINT("mean result[%ld] is: %d\n", i, resultData[i]); } } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. 固定写法,device/stream初始化,参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口定义构造 int64_t shapeB = 1; // batch int64_t shapeS = 1; // seqlen int64_t shapeNq = 2; // query 头数 int64_t shapeNkv = 1; // kv 头数 int64_t shapeD = 32; // 每头维度 int64_t shapeH = shapeD * (shapeNq + shapeNkv + shapeNkv); // 尾轴长度 = (Nq+Nkv+Nkv)*D std::vector<int64_t> inputShape = {shapeB, shapeS, shapeH}; std::vector<int64_t> cosShape = {shapeB, shapeS, 1, shapeD}; std::vector<int64_t> sinShape = {shapeB, shapeS, 1, shapeD}; std::vector<int64_t> kcacheShape = {shapeB, shapeH, 1, shapeD}; std::vector<int64_t> vcacheShape = {shapeB, shapeH, 1, shapeD}; std::vector<int64_t> indicesShape = {shapeB}; std::vector<int64_t> kscaleShape = {shapeNkv, shapeD}; std::vector<int64_t> vscaleShape = {shapeNkv, shapeD}; std::vector<int64_t> koffsetShape = {shapeNkv, shapeD}; std::vector<int64_t> voffsetShape = {shapeNkv, shapeD}; std::vector<int64_t> weightShape = {shapeH}; std::vector<int64_t> activationShape = {shapeB * shapeS}; std::vector<int64_t> biasShape = {shapeH}; // 以 INT32 输入触发反量化路径,需提供 weightScale/activationScale/bias std::vector<int32_t> inputHostData(shapeB * shapeS * shapeH, 1); std::vector<int16_t> cosHostData(shapeB * shapeS * shapeD, 1); std::vector<int16_t> sinHostData(shapeB * shapeS * shapeD, 1); std::vector<int8_t> kcacheHostData(shapeB * shapeH * shapeD, 6); std::vector<int8_t> vcacheHostData(shapeB * shapeH * shapeD, 6); std::vector<int32_t> indicesHostData(shapeB, 0); std::vector<float> kscaleHostData(shapeNkv * shapeD, 2); std::vector<float> vscaleHostData(shapeNkv * shapeD, 2); std::vector<float> koffsetHostData(shapeNkv * shapeD, 2); std::vector<float> voffsetHostData(shapeNkv * shapeD, 2); std::vector<float> weightHostData(shapeH, 2); std::vector<float> activationHostData(shapeB * shapeS, 2); std::vector<float> biasHostData(shapeH, 2); // (省略:逐个调用 CreateAclTensor 创建各输入/输出的 aclTensor 与 device 内存) std::vector<int64_t> splitData = {shapeNq * shapeD, shapeNkv * shapeD, shapeNkv * shapeD}; aclIntArray *sizeSplits = aclCreateIntArray(splitData.data(), splitData.size()); char quantMode[] = "static"; char layout[] = "BSND"; char cacheMode[] = "contiguous"; // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnDequantRopeQuantKvcache第一段接口 ret = aclnnDequantRopeQuantKvcacheGetWorkspaceSize(input, cos, sin, kcache, vcache, indices, kscale, vscale, koffset, voffset, weight, activation, bias, sizeSplits, quantMode, layout, true, cacheMode, q, k, v, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnDequantRopeQuantKvcacheGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret); } // 调用aclnnDequantRopeQuantKvcache第二段接口 ret = aclnnDequantRopeQuantKvcache(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnDequantRopeQuantKvcache failed. ERROR: %d\n", ret); return ret); // 4. 固定写法,同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 5. 获取输出的值,将device侧内存上的结果拷贝至host侧 PrintOutResult(kcacheShape, &kcacheDeviceAddr); PrintOutResult(vcacheShape, &vcacheDeviceAddr); // 6. 释放aclTensor和aclIntArray // 7. 释放device资源(aclrtFree / aclrtDestroyStream / aclrtResetDevice / aclFinalize) return 0; }上述示例中sizeSplits = {Nq*D, Nkv*D, Nkv*D}与参数表要求一致。需要注意,示例中indices全部为 0 且cacheMode = "contiguous",因此量化后的 k/v 会被写入每一条 batch 对应 cache 的第 0 个 token 位置;若要在真实推理中做增量写入,需按 token 位置更新indices。
源码级原理深挖
算子定义与数据类型注册
dequant_rope_quant_kvcache_def.cpp 中通过OP_ADD(DequantRopeQuantKvcache)完成算子注册。该文件用 16 组 dtype 组合(FLOAT16 / INT32 / BF16 三种 x 类型分别与 cos/sin、bias、scale、cache 类型的组合)精确刻画了文档约束中"数据类型保持一致"的规则,例如:
x为 FLOAT16 时,cos/sin必须为 FLOAT16(XDtypeList与cosDtypeList前 4 组一一对应);x为 INT32 时,bias可以是 FLOAT16 / BF16 / INT32 / FLOAT 四种(biasDtypeList中对应条目),即约束第 5 条"biasOptional 不需要与其他输入类型一致";k_cache/v_cache固定为 INT8,indices固定为 INT32,scale/offset/weight_scale/activation_scale固定为 FLOAT。
Shape 推导逻辑
dequant_rope_quant_kvcache_infershape.cpp 中InferShapeForDequantRopeQuantKvcache负责输出 shape 推导:
- 要求 x 为 2 维或 3 维,cache 为 4 维;
- 从
size_splits[0]与 cache 的隐藏维度推导qHead = size_splits[0] / hiddenSize; - x 为 3 维时,
qOut为[B, S, Nq, D],kOut/vOut为[B, S, Nkv, D];x 为 2 维时(seqlen 视为 1),qOut为[B, Nq, D],kOut/vOut为[B, Nkv, D]; - 当
kv_output为 false 时,kOut/vOut的第 0 维被置为 0(shape 为空),这与参数表中"kvOutput 为 false 时 shape 为空"完全对应; InferDataTypeForDequantRopeQuantKvcache将 q/k/v 的输出类型设置为与cos一致(印证"数据类型与 cos、sin 保持一致")。
Tiling 策略与 workspace
dequant_rope_quant_kvcache_tiling.cpp 中的TilingDequantRopeQuantKvcache负责在 Host 侧完成任务切分:
- 任务量建模:
taskNum = batch * seqlen,并按 AIV 核数(GetCoreNumAiv())与 UB 容量(GetCoreMemSize)计算blockFactor(前核块因子)与tailCoreBlockFactor(尾核块因子),实现多核负载均衡; - UB 单次承载量:
OnceUBMaxS由 UB 剩余空间除以单次处理所需的 buffer 总量(q/k/v/cos/sin/indices 的缓冲区之和,全部按 32 字节BLOCK_SIZE对齐)计算得到,Kernel 按该值循环搬数; - Workspace:tiling 阶段统一申请
MINIMAL_WORKSPACE = 16MB的 workspace(见 dequant_rope_quant_kvcache_tiling.cpp),作为空 tensor 与 kernel 计算时的中间缓冲; - TilingKey:以 bias 的数据类型作为
SetTilingKey的取值(FLOAT=0 / FLOAT16=1 / INT32=2 / BF16=3),驱动 Kernel 侧模板实例化。
tiling 数据的字段定义位于 dequant_rope_quant_kvcache_tiling.h(qHeadNum、kvHeadNum、hiddenSize、OnceUBMaxS、isPA、ifKVout、hasBias、hasAS等),这些字段直接决定了 Kernel 内部的分支走向。
Kernel 实现要点
Kernel 侧入口为 dequant_rope_quant_kvcache.cpp,按 TilingKey 0/1/2/3 分别实例化RopeQuantKvcacheV2<DTYPE_X, bias类型, DTYPE_COS>模板,bias 类型对应 FLOAT / half / int32_t / bfloat16_t。核心类实现在 dequant_rope_quant_kvcache.h:
- 反量化(
dequantUb):对 INT32 输入先Cast到 float,再乘weight_scale(Mul),可选乘激活 scale(Muls)与加 bias(Add); - 切分搬数:通过
DataCopyPad与DataCopyExtParams以"块内 stride"方式从inputGm中带间隔地抽取 q、k、v 三段(见dataCopyParamsQ_/K_/V_的构造,srcStride恰好跳过其他两段),避免三块独立访存; - RoPE 计算:按
hiddenSize / 2拆分奇偶半段,执行k*cos ± 旋转(k)*sin的旋转位置编码,代码中对sin先乘以 -1(Muls),再通过两次Mul与一次Add完成标准 RoPE 公式; - 量化:对 kOut/vOut 先
Div除以 scale,可选Addoffset,再经Cast(CAST_RINT)到 INT16、转 half、最终Cast(CAST_NONE)到 INT8; - Cache 写入(
copyOutcache):page 模式下以index * kvHeadNum * hiddenSize计算页内偏移;contiguous 模式下以(bOffset + bIndex) * cacheSeqlen + index + sIndex计算连续偏移,随后DataCopy写回kCacheGm/vCacheGm; - 流水并行:通过
MTE2_S、V_MTE3、MTE3_MTE2、MTE2_MTE3等硬件事件(SetFlag/WaitFlag)对搬入(MTE2)、向量计算(V)、搬出(MTE3)三段流水做同步编排,降低访存延迟。
测试与验证
仓库为算子提供了多级测试保障:
- ST 测试:目录 tests/st/aclnnDequantRopeQuantKvcache 下的
atk_aclnnDequantRopeQuantKvcache.json定义了基于 ATK 的端到端用例:用例 0 覆盖 x 为[1, 2304]的 FLOAT16 输入(Nq*D=1536、Nkv*D=384、Nkv*D=384,即 2D 输入 +cacheMode=page);用例 1 覆盖 x 为[1, 3584]的 INT32 反量化路径(sizeSplits={1792, 896, 896},bias 为 BF16,cacheMode=page)。两个用例均携带backward: true,可用于精度对比基准(cv_fused_double_benchmark); - UT 测试:目录 tests/ut 下包含 op_host 层的
test_dequant_rope_quant_kvcache_infershape.cpp、test_dequant_rope_quant_kvcache_tiling.cpp,以及 op_kernel 层的test_dequant_rope_quant_kvcache.cpp,分别验证 shape 推导、tiling 数据与 Kernel 计算结果的正确性。
小结
aclnnDequantRopeQuantKvcache是 CANN ops-transformer 中把"反量化 → QKV 切分 → RoPE → 量化 → KV Cache 写入"五步融合的单算子实现。通过两段式 aclnn 接口,开发者可以在 Host 侧一次性完成参数校验、workspace 计算与执行器构建,随后在 Stream 上异步执行。本文完整梳理了其计算流程、两段式接口的全部参数语义、两种 cache 更新模式与 7 条使用约束,并结合仓库中的算子定义、shape 推导、tiling 策略与 Kernel 实现,揭示了底层的数据类型组合规则、多核任务划分与流水并行机制。对于正在自研或接入大模型推理框架、需要在 NPU 上高效维护量化 KV Cache 的开发者,该算子可作为 PagedAttention 场景中 KV 预处理环节的落地参考。
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考