ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

vllm-ascend 算子深度解析:aclnnDequantSituQuant 融合 Dequant–Situ–Quant 的实现原理与调用实战

vllm-ascend 算子深度解析:aclnnDequantSituQuant 融合 Dequant–Situ–Quant 的实现原理与调用实战 人工智能大模型模型推理服务AscendCANN【免费下载链接】vllm-ascendCommunity maintained hardware plugin for vLLM on Huawei Ascend项目地址https://gitcode.com/gh_mirrors/vl/vllm-ascend点击查看免费下载aclnnDequantSituQuant是 vllm-ascend 仓库 csrc/moe/dequant_situ_quant 中提供的 Ascend 自定义算子将「反量化Dequant→ Situ 激活Situ→ 量化Quant」三段计算融合为单个 NPU 算子服务于 MoE 门控激活等 INT8 推理链路。本文以该算子官方文档 aclnnDequantSituQuant.md 为核心骨架结合算子定义、Tiling、Kernel 与 torch 适配层的源码实现完整讲解其计算公式、两段式 aclnn 接口、参数语义、约束条件、底层计算流水与 C 调用样例并梳理其在 vllm-ascend 量化推理路径中的实际接入方式。读完本文你可以直接复刻官方调用示例也能读懂该算子在 AI Core 上如何分阶段完成反量化、激活与量化。算子功能与计算公式接口功能该算子在 Situ 激活函数前后分别插入反量化dequant与量化quant操作实现输入x的 DequantSituQuant 一体化计算先用dequantScale与可选的dequantBias将量化输入x反量化为浮点对反量化结果执行 Situ 激活再用quantScale与可选的quantOffset将激活结果量化为 INT8 输出。计算链路dequantOut cast_to_float(x) × dequantScale dequantBias situOut Situ(dequantOut) β × tanh(gate / β) × sigmoid(gate) × up out Quant(situOut, quantScale, quantOffset)其中 Situ 激活的语义为gate 通道经 tanh 门控与 sigmoid 门控调制后再乘以 up 通道当activateLeft为true时gate取dequantOut的前半部分up取后半部分当activateLeft为false时gate取dequantOut的后半部分up取前半部分当linearBeta 0时up还会被进一步变换为linear_beta × tanh(up / linear_beta)。beta与linearBeta在算子定义中的默认值分别为4.0与25.0详见 op_host/dequant_situ_quant_def.cpp。产品支持情况该算子在以下产品上受支持产品是否支持Ascend 950PR / 950DT 系列产品×Atlas A3 系列产品√Atlas A2 系列产品√Atlas 200I/500 A2 推理产品×Atlas 推理系列产品×Atlas 训练系列产品×与产品支持表一致算子定义中注册的 AI Core 配置为ascend910b与ascend910_93两种 SoC见 op_host/dequant_situ_quant_def.cpp对应上述受支持的 A2 / A3 系列硬件。两段式接口与函数原型与 Ascend 上所有 aclnn 算子一致aclnnDequantSituQuant采用两段式接口调用范式第一阶段调用aclnnDequantSituQuantGetWorkspaceSize获取计算所需的 workspace 大小同时拿到封装了算子计算流程的执行器aclOpExecutor第二阶段调用aclnnDequantSituQuant在指定 stream 上实际执行计算。GetWorkspaceSize 原型aclnnStatus aclnnDequantSituQuantGetWorkspaceSize( const aclTensor *x, const aclTensor *dequantScale, const aclTensor *dequantBiasOptional, const aclTensor *quantScaleOptional, const aclTensor *quantOffsetOptional, float beta, float linearBeta, bool activateLeft, char *quantModeOptional, const aclTensor *yOut, const aclTensor *scaleOut, uint64_t *workspaceSize, aclOpExecutor **executor)执行接口原型aclnnStatus aclnnDequantSituQuant( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)参数说明下表为aclnnDequantSituQuantGetWorkspaceSize各参数的完整语义参数名输入/输出描述使用说明数据类型维度(shape)x输入输入待处理的数据shape 为 (N...,H)最后一维需要是 2 的倍数且 x 的维度必须大于 1 维。不支持空 Tensor。INT82-8dequantScale输入反量化 scaleshape 为 (H,) 或 (1,)。当 shape 为 (H,) 时取值 H 和 x 最后一维保持一致。FLOAT321dequantBiasOptional输入反量化 biasshape 为 (H,) 或 (1,)。可选参数支持传空指针。FLOAT321quantScaleOptional输入量化的 scale当quantModeOptional为static时shape 为 (H/2,) 或 (1,)当quantModeOptional为dynamic时shape 为 (H/2,)作为 smoothScale 使用。可选参数支持传空指针。FLOAT321quantOffsetOptional输入量化的 offsetshape 为 (H/2,) 或 (1,)。仅当quantModeOptional为static时有效。可选参数支持传空指针。FLOAT321beta输入Situ 激活的 beta 参数不能为 0。Float-linearBeta输入Situ 激活的 linear_beta 参数当值 ≤0 时不启用 linear_beta 变换。Float-activateLeft输入是否对输入的左半部分做 Situ 激活当值为 false 时对输入的右半部分做激活。Bool-quantModeOptional输入量化模式支持static和dynamic。String-yOut输出量化后的输出shape 为 (N...,H/2)。INT8-scaleOut输出动态量化的 scaleshape 为 (N,...)与 yOut 去除尾轴后的 shape 一致。FLOAT32-几点补充说明对应 Tiling 侧源码语义shape 校验Tiling 代码会严格校验dequantScale与bias的 shape 必须为inDimy或1、quantScale必须为outDimy或1不满足直接报错返回GRAPH_FAILED见 op_host/dequant_situ_quant_tiling.cpp量化模式与可选参数的关系static模式要求quantScale必须提供dynamic模式下quantScale可选若提供则充当 smoothScale在量化前对 Situ 输出做逐元素平滑缩放返回值aclnnStatus状态码成功返回ACL_SUCCESS失败返回对应的 aclnn 错误码。约束说明使用该算子时需满足以下约束x的最后一维必须是 2 的倍数且x的维数必须大于 1 维beta参数不能为 0否则 Tiling 阶段直接报错见 dequant_situ_quant_tiling.cpp当quantModeOptional为static时quantScaleOptional必须提供当quantModeOptional为dynamic时quantScaleOptional可选作为 smoothScale 使用。此外从 shape 推断与 Tiling 源码可以进一步确认INT8 路径要求x的维度大于 1 维且输出y的最后一维严格等于x最后一维的一半见 op_host/dequant_situ_quant_infershape.cpp输出scaleOut的元素总数必须等于行数inDimx。源码级实现剖析算子定义与默认属性图Graph层面的算子定义注册在 op_host/dequant_situ_quant_def.cpp其关键信息为输入x支持DT_INT32与DT_BF16两种 dtypetorch 适配层使用的路径可选输入包括weight_scale、activation_scale、bias、quant_scale、quant_offset、group_index输出为yINT8与scaleFLOAT32属性默认值beta 4.0、linear_beta 25.0、activate_left true、quant_mode dynamic。也就是说该算子在实际接入中存在两条底层实现分支面向 INT8 输入的通用反量化路径对应本文 aclnn 文档以及面向 INT32/BF16 输入的 MoE 路由路径K3 kernel。两者在 Tiling 侧通过x的 dtype 区分。INT8 路径Dequant → Situ → Quant 三级计算流水INT8 路径由 op_kernel/dequant_situ_quant.h 中的DequantSituQuantKernel实现其计算流程与文档公式一一对应Dequant 阶段ComputeDequant先把 INT8 数据Cast为 FP16再Cast为 FP32两次 Cast 保证精度随后若dequantScale为标量则用Muls乘标量、否则用Mul逐元素乘向量最后按需Adds/Add加上dequantBiasSitu 阶段ComputeSitu根据activateLeft确定gate与up分别取左/右半区先计算beta × tanh(gate / beta)再计算数值稳定的 sigmoid二者相乘后再乘up当linearBeta 0时对up套用linear_beta × tanh(up / linear_beta)变换Quant 阶段ComputeQuant按static或dynamic模式完成量化并写回 GM。数值稳定的 Situ 计算细节Kernel 中 sigmoid 采用sigmoid(gate) 1 / (1 exp(-gate))的数值稳定写法先对gate取负、求Exp、加 1 得到分母再执行除法。源码注释特别说明除法使用 Level 0 的Div而非Reciprocal以获得更高精度且分母 1.0 的数据块通过Duplicate生成后跨 repeat 复用见 dequant_situ_quant.h。tanh(gate / beta)则拆解为「乘1/beta→Tanh→ 乘beta」三步利用预取倒数避免除法指令。静态量化与动态量化静态量化staticquantScale必须提供。若quantScale为标量Kernel 会先取其倒数作为quantScaleVal_并用Muls完成缩放quantScale为 0 时兜底为 1.0随后按需Adds加quantOffset若为向量则用Div除以 scale、Add加 offset动态量化dynamic对 Situ 输出逐行求绝对值最大值absMax行 scale 取absMax / 127.0DYNAMIC_QUANT_FACTOR 1.0 / 127.0随后用1/scale缩放并写回每行的scaleOut。当列方向发生分 tile 时动态量化采用两遍扫描策略第一遍遍历该行的所有列 tile 汇总全局 absMax第二遍用全局 scale 重新计算 Situ 并量化输出见 dequant_situ_quant.h 与DynamicComputeRowScale/DynamicQuantizeAndOutput。量化输出统一经由CastFloatToInt8完成采用「FP32 → INT32CAST_RINT四舍五入到偶数→ FP16CAST_ROUND→ INT8CAST_TRUNC」的三级 Cast 链路以保证舍入语义见 dequant_situ_quant.h。INT32 / BF16 路径K3 Kernel 与 MoE 路由同一头文件中的DequantSituQuantK3Kernel面向 MoE 场景支持两种输入INT32 输入输入是 GMM 输出的 INT32 累加结果需要weight_scale每专家每通道、activation_scale每行与可选bias完成反量化x_f32 x_f32 × weight_scale × activation_scale biasBF16 输入输入本身已是预反量化结果isPreDequantized无需任何 scale 参数直接进入 Situ 计算。该路径还支持通过可选group_indexINT64 一维向量元素为各专家行数表达MoE 分组路由Kernel 按专家顺序切分行区间跳过死行dead tail按组逐行完成 Situ 计算与逐行动态量化见 dequant_situ_quant.h。此路径只支持dynamic量化不接收quant_scale/quant_offset与 Tiling 中的契约校验一致dequant_situ_quant_tiling.cpp。Tiling 策略与 workspaceTiling 逻辑位于 op_host/dequant_situ_quant_tiling.cpp要点包括数据切分INT8 路径在 UB 容量约束下计算单次可容纳的最大列数maxTileLen据此得到baseColLen行方向以单行为基准INT32/BF16 路径则整行放入 UB不做列切分核数分配usedCoreNum取行数与总 AI Core 数的最小值并限制在 64 以内当列数小于 1536 且行数小于 128 的小 shape 场景核数进一步限制为不超过 12MIN_CORE避免小任务多核开销workspaceINT32/BF16 路径 workspace 为 0INT8 路径默认申请 16 MiBUSER_WORKSPACE 16777216当动态量化且列方向发生分 tile 时额外追加usedCoreNum × outDimy × sizeof(float)字节用于跨 tile 的行级 scale 暂存见 dequant_situ_quant_tiling.cppTiling key 分类按 dtype、量化模式、scale 是否为标量、是否带 bias 组合出 8 种 INT8 变体与 2 种 INT32/BF16 变体实现编译产物按场景分派。完整调用示例官方文档给出了可直接参考的 C 调用样例编译与运行依赖 CANN 环境的 acl/acl.h 与 aclnnop 头文件。以下代码演示x [16, 64]、static 量化模式的完整两段式调用#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_dequant_situ_quant.h int64_t GetShapeSize(const std::vectorint64_t shape) { int64_t shapeSize 1; for (auto dim : shape) { shapeSize * dim; } return shapeSize; } template typename T int CreateAclTensor(const std::vectorT hostData, const std::vectorint64_t shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size GetShapeSize(shape) * sizeof(T); auto ret aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); if (ret ! ACL_SUCCESS) return ret; ret aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); if (ret ! ACL_SUCCESS) return ret; std::vectorint64_t strides(shape.size(), 1); for (int64_t i shape.size() - 2; i 0; i--) { strides[i] shape[i 1] * strides[i 1]; } *tensor aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1. Initialize device and stream int32_t deviceId 0; aclrtStream stream; auto ret aclInit(nullptr); ret aclrtSetDevice(deviceId); ret aclrtCreateStream(stream); // 2. Construct inputs: x[16, 64], static quant mode int64_t rowLen 16; int64_t inDimy 64; int64_t outDimy 32; double beta 1.0; double linearBeta 0.0; bool activateLeft false; std::vectorint64_t xShape {rowLen, inDimy}; std::vectorint64_t dequantScaleShape {inDimy}; std::vectorint64_t quantScaleShape {1}; std::vectorint64_t quantOffsetShape {1}; std::vectorint64_t yShape {rowLen, outDimy}; std::vectorint64_t scaleOutShape {rowLen}; auto xSize GetShapeSize(xShape); std::vectorint8_t xHostData(xSize); for (int64_t i 0; i xSize; i) { xHostData[i] static_castint8_t((i * 7 3) % 100); } std::vectorfloat dequantScaleHostData(inDimy, 0.1f); std::vectorfloat quantScaleHostData(1, 1.0f); std::vectorfloat quantOffsetHostData(1, 0.0f); std::vectorint8_t yHostData(GetShapeSize(yShape), 0); std::vectorfloat scaleOutHostData(GetShapeSize(scaleOutShape), 0.0f); void* xDeviceAddr nullptr; void* dsDeviceAddr nullptr; void* qsDeviceAddr nullptr; void* qoDeviceAddr nullptr; void* yDeviceAddr nullptr; void* scaleDeviceAddr nullptr; aclTensor* x nullptr; aclTensor* dequantScale nullptr; aclTensor* quantScale nullptr; aclTensor* quantOffset nullptr; aclTensor* y nullptr; aclTensor* scaleOut nullptr; CreateAclTensor(xHostData, xShape, xDeviceAddr, aclDataType::ACL_INT8, x); CreateAclTensor(dequantScaleHostData, dequantScaleShape, dsDeviceAddr, aclDataType::ACL_FLOAT, dequantScale); CreateAclTensor(quantScaleHostData, quantScaleShape, qsDeviceAddr, aclDataType::ACL_FLOAT, quantScale); CreateAclTensor(quantOffsetHostData, quantOffsetShape, qoDeviceAddr, aclDataType::ACL_FLOAT, quantOffset); CreateAclTensor(yHostData, yShape, yDeviceAddr, aclDataType::ACL_INT8, y); CreateAclTensor(scaleOutHostData, scaleOutShape, scaleDeviceAddr, aclDataType::ACL_FLOAT, scaleOut); // 3. Call aclnnDequantSituQuantGetWorkspaceSize uint64_t workspaceSize 0; aclOpExecutor* executor; ret aclnnDequantSituQuantGetWorkspaceSize(x, dequantScale, nullptr, quantScale, quantOffset, beta, linearBeta, activateLeft, const_castchar*(static), y, scaleOut, workspaceSize, executor); // 4. Allocate workspace and call aclnnDequantSituQuant void* workspaceAddr nullptr; if (workspaceSize 0) { aclrtMalloc(workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); } ret aclnnDequantSituQuant(workspaceAddr, workspaceSize, executor, stream); ret aclrtSynchronizeStream(stream); // 5. Copy output and cleanup std::vectorint8_t npuYResult(GetShapeSize(yShape), 0); aclrtMemcpy(npuYResult.data(), npuYResult.size() * sizeof(int8_t), yDeviceAddr, GetShapeSize(yShape) * sizeof(int8_t), ACL_MEMCPY_DEVICE_TO_HOST); aclDestroyTensor(x); aclDestroyTensor(dequantScale); aclDestroyTensor(quantScale); aclDestroyTensor(quantOffset); aclDestroyTensor(y); aclDestroyTensor(scaleOut); aclrtFree(xDeviceAddr); aclrtFree(dsDeviceAddr); aclrtFree(qsDeviceAddr); aclrtFree(qoDeviceAddr); aclrtFree(yDeviceAddr); aclrtFree(scaleDeviceAddr); if (workspaceSize 0) aclrtFree(workspaceAddr); aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }调用流程分步拆解初始化环境aclInit→aclrtSetDevice(deviceId)→aclrtCreateStream(stream)构造输入宿主侧生成xINT8填充(i*73) % 100、dequantScale全 0.1f、quantScale1.0f、quantOffset0.0f并通过CreateAclTensor模板函数完成「aclrtMalloc设备内存 →aclrtMemcpy拷入 →aclCreateTensor创建 ND 格式张量」注意示例在GetWorkspaceSize阶段对可选的dequantBias直接传入nullptr第一段调用aclnnDequantSituQuantGetWorkspaceSize(...)拿到workspaceSize与executor示例中activateLeft false表示对输入右半部分做 Situ 激活第二段调用若workspaceSize 0则aclrtMalloc申请 workspace随后aclnnDequantSituQuant(workspace, workspaceSize, executor, stream)执行计算并aclrtSynchronizeStream同步结果回收aclrtMemcpy将y拷回宿主最后依次销毁张量、释放设备内存与 workspace、销毁 stream、aclrtResetDevice与aclFinalize。在 vllm-ascend 中的集成Torch 适配层与 Python 调用除了 aclnn 原生接口该算子还通过 csrc/moe/dequant_situ_quant/dequant_situ_quant_torch_adpt.h 暴露为 Torch 自定义算子dequant_situ_quant。适配层将 7 个可选 Tensor 参数weight_scale、activation_scale、bias、quant_scale、quant_offset、group_index包装为c10::optional并把beta、linear_beta、activate_left、quant_mode以double/bool/string_view形式传入最终通过EXEC_NPU_CMD(aclnnDequantSituQuant, ...)走通两段式 aclnn 调用返回{y, scale}两个输出张量。适配层还做了两个前置校验与兜底要求x必须为二维[rows, width]输出y分配为[rows, width/2]的 INT8 空张量、scale分配为[rows]的 FP32 空张量并在x行数为 0 时直接返回空结果。Python 侧的实际调用点包括vllm_ascend/quantization/methods/w4a8/w4a8.py在 W4A8 量化路径中通过torch.ops._C_ascend.dequant_situ_quant(...)对门控激活输出执行 Dequant-Situ-Quant 融合得到量化后的hidden_states与swiglu_out_scalevllm_ascend/ops/fused_moe/shared_experts.py共享专家shared experts推理路径中同样调用该算子完成 Situ 激活与动态量化。这两处调用印证了算子在 vllm-ascend 中的定位在 INT8 量化后的 MoE/SwiGLU 门控计算链路上用单个 NPU 算子替代「反量化 → 激活 → 再量化」的多次内存往返同时通过动态量化输出每行 scale供下游矩阵乘算子直接复用。小结aclnnDequantSituQuant是一个典型的「计算与量化融合」的 Ascend 自定义算子它把 Situ 激活前后的反量化与量化全部收进单个 Kernel在 AI Core 上以 Dequant → Situ → Quant 三级流水完成 INT8 输入到 INT8 输出的闭环并支持 static/dynamic 两种量化模式、标量/向量 scale、activateLeft 切分方向与 linearBeta 变换等灵活配置。本文从官方文档出发结合 op_host 与 op_kernel 的源码给出了参数语义、约束条件、底层计算细节与完整调用示例并说明了它在 vllm-ascend W4A8 量化推理与 MoE 共享专家路径中的实际接入方式。对于需要在 Atlas A2/A3 系列硬件上落地 INT8 门控激活融合算子的开发者可直接以本文示例为模板按需替换 shape、量化模式与激活方向参数进行验证。赞分享人工智能大模型模型推理服务AscendCANN【免费下载链接】vllm-ascendCommunity maintained hardware plugin for vLLM on Huawei Ascend项目地址https://gitcode.com/gh_mirrors/vl/vllm-ascend点击查看免费下载相关推荐Minecraft区块管理终极指南使用MCA Selector轻松清理和优化世界存档Minecraft区块管理终极指南使用MCA Selector轻松清理和优化世界存档 你是否曾经因为Minecraft世界存档过大而烦恼或者想要删除某些不需人工智能算子库大模型深度学习CANNAscendCANN ops-nn 算子深度解析AddRmsNormDynamicQuantV2 融合算子原理、参数与调用实战CANN ops nn 算子深度解析AddRmsNormDynamicQuantV2 融合算子原理、参数与调用实战 AddRmsNormDynamicQuan人工智能算子库深度学习CANNAscendCANN ops-transformer 量化算子深度解析GroupedMatmulSwigluQuantV2 融合算子原理与 aclnn 调用实战CANN ops transformer 量化算子深度解析GroupedMatmulSwigluQuantV2 融合算子原理与 aclnn 调用实战 导读 本算子库人工智能大模型深度学习CANNAscend上一篇终极指南5个简单步骤让老旧Mac免费升级最新macOS系统下一篇12个惊艳技巧用开源工具打造个性化城市艺术地图创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED READING

延伸阅读

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