CANN ops-transformer 算子解析:aclnnMoeInitRoutingV3 两段式接口、路由重排与全量化模式实战
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
本文是 CANN ops-transformer 仓库中moe_init_routing_v3算子的深度技术指南。该算子负责 MoE(Mixture of Experts,混合专家)模型的 routing 计算,将 Gating 网络产出的专家索引(expertIdx)转换为可直接送入专家计算的扩展特征(expandedXOut)与索引映射(expandedRowIdxOut),并支持不量化、静态量化、动态量化及多种 FP8/FP4/INT4/HIFLOAT8 量化输出。阅读本文后,你将掌握该算子的两段式 aclnn 接口用法、全部 18 种 quantMode 量化语义、Dropless/DropPad/Active 三种输出模式、按产品的支持差异与性能模板准入条件,并能参照完整示例代码在 NPU 上独立跑通该算子。
算子定位:MoE 推理链路中的路由核心
在 MoE 大模型中,一个 token 并不会被所有专家处理,而是由 Gating 网络(例如aclnnMoeGatingTopKSoftmaxV2)为每个 token 选出 Top-K 个专家,并给出权重。aclnnMoeInitRoutingV3正是承接这一结果、完成"路由"动作的算子:它根据 aclnnMoeGatingTopKSoftmaxV2 计算出的 expertIdx 结果,对 token 特征做重排(Gather/Scatter)与量化,输出专家侧可直接消费的expandedXOut及配套索引,是整个 MoE 前向链路上承上启下的关键算子。
该算子的产品支持情况如下(见 README 与 接口文档):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 不支持 |
相对 V2 的功能变更
aclnnMoeInitRoutingV3是针对 V2 接口 aclnnMoeInitRoutingV2 的演进版本,核心变更可以归纳为三点:
- 量化能力增强:新增动态与静态量化功能,支持
expandedXOut的 INT8 量化输出;在 Ascend 950 系列上进一步扩展出 MXFP8(quantMode=2/3)、FP8 PerGroup(4/5)、HIF8(6/7/8)、MXFP4(9)、FP8 PerBlock(11/12)、INT4 动态量化(13)、FP8 PerGroup + Amax(14/15)、MXFP8 RoundScale+Amax(16/17)等多种量化模式。 - 输出重定义:删除输出
expertTokensBeforeCapacityOut,新增输出expertTokensCountOrCumsumOut,并新增activeExpertRangeOptional参数,支持筛选有效范围内的 expertId(从 README 的功能说明可见)。 - 属性重定义:原有
expertTokensBeforeCapacityFlag(bool)和expertTokensCountOrCumsumFlag(int)被重新定义为expertsTokensNumFlag(bool)与expertTokensNumType(int),通过DropPadMode、expertsTokensNumFlag、expertTokensNumType三者组合,统一了 cumsum / count / key_value 三种直方图输出格式,对应关系如下:
| DropPadMode | expertsTokensNumFlag | expertTokensNumType | 输出格式说明 |
|---|---|---|---|
| 0 | true | 0 | cumsum 模式,expertTokensCountOrCumsumOut表示按排序后各专家处理 token 的计数前缀和直方图 |
| 0 | true | 1 | count 模式,expertTokensCountOrCumsumOut表示按排序后各专家处理 token 的单独计数直方图 |
| 0 | true | 2 | key_value 模式,输出 shape 为[expert_num, 2],表示每个专家和该专家处理非零 token 数量的累计值 |
| 1 | true | 1 | 输出模式为 count 模式 |
| 不开启 | false | 不开启 | 不输出expertTokensCountOrCumsumOut |
工作原理:排序、索引映射与量化公式
算子的计算过程在 接口文档 中有完整的数学描述,整体分为六步:
第 1 步:对 expertIdx 做 key-value 排序。输入专家索引expertIdx(shape 为(NUM_ROWS, K))按专家 id 排序,得到排序后的sortedExpertIdx与对应的原始位置序号sortedRowIdx:
$$sortedExpertIdx,\ sortedRowIdx=keyValueSort(expertIdx,rowIdx)$$
第 2 步:以sortedRowIdx做位置映射得到expandedRowIdxOut。具体输出哪种索引由属性rowIdxType决定:
rowIdxType=1:输出 scatter 索引,$expandedRowIdxOut[i]=sortedRowIdx[i]$;rowIdxType=0:输出 gather 索引,$expandedRowIdxOut[sortedRowIdx[i]]=i$。
第 3 步:统计直方图。对排序后的sortedExpertIdx统计各专家 token 数,得到expertTokensCountOrCumsumOutOptional:
$$expertTokensCountOrCumsumOutOptional[i]=Histogram(sortedExpertIdx)$$
第 4 步:量化计算(quantMode ≠ -1 时)。静态量化与动态量化的公式如下:
- 静态量化:$quantResult=round((x*scaleOptional)+offsetOptional)$;
- 动态量化(不输入 scale):$dynamicQuantScaleOutOptional = row_max(abs(x)) / 127$,$quantResult = round(x / dynamicQuantScaleOutOptional)$;
- 动态量化(输入 scale):$dynamicQuantScaleOutOptional = row_max(abs(x * scaleOptional)) / 127$,$quantResult = round(x / dynamicQuantScaleOutOptional)$;
- 当
quantMode=13(INT4 动态量化)时,对称量化范围变为 [-8, 7],scale 计算中的分母为 7,量化结果沿 H 维每两个 INT4 值打包为 1 个字节(因此要求 H 为偶数)。
第 5 步:按索引搬运 token 得到expandedXOut。若活跃 expert 范围为全专家范围(即activeExpertRangeOptional覆盖全部专家),则按 Scatter 索引搬运;否则按 Gather 索引搬运:
- Scatter 搬运:$expandedXOut[i]=x[scatterRowIdx[i] // K]$(量化场景为 $expandedXOut[i]=quantResult[scatterRowIdx[i] // K]$);
- Gather 搬运:$expandedXOut[gatherRowIdx[i]]=x[i // K]$(量化场景为 $expandedXOut[gatherRowIdx[i]]=quantResult[i // K]$)。
在dropPadMode=1时,还会将每个专家需要处理的 token 个数对齐为expertCapacity个:超过容量的 token 被 Drop,不足的用 0 填充(Pad)。
第 6 步:计算有效元素数量availableIdxNum。即expertIdx中落在activeExpertRangeOptional=[expertStart, expertEnd)范围内元素的个数;-1表示无效专家,不参与路由计算:
$$availableIdxNum = |{x\in expertIdx| expert_start \le x < expert_end }|$$
两段式接口与函数原型
与 CANN aclnn 系列算子一致,aclnnMoeInitRoutingV3采用两段式接口(详见 两段式接口说明):必须先调用aclnnMoeInitRoutingV3GetWorkspaceSize完成入参校验、计算 workspace 大小并生成包含算子计算流程的执行器,再调用aclnnMoeInitRoutingV3真正执行计算。
aclnnStatus aclnnMoeInitRoutingV3GetWorkspaceSize( const aclTensor *x, const aclTensor *expertIdx, const aclTensor *scaleOptional, const aclTensor *offsetOptional, int64_t activeNum, int64_t expertCapacity, int64_t expertNum, int64_t dropPadMode, int64_t expertTokensNumType, bool expertTokensNumFlag, int64_t quantMode, const aclIntArray *activeExpertRangeOptional, int64_t rowIdxType, const aclTensor *expandedXOut, const aclTensor *expandedRowIdxOut, const aclTensor *expertTokensCountOrCumsumOut, const aclTensor *expandedScaleOut, uint64_t *workspaceSize, aclOpExecutor **executor)aclnnStatus aclnnMoeInitRoutingV3( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)接口头文件在 op_host/op_api/aclnn_moe_init_routing_v3.h 中声明(extern "C"导出,头文件位于aclnnop/aclnn_moe_init_routing_v3.h),实现位于 op_host/op_api/aclnn_moe_init_routing_v3.cpp。
参数详解
输入参数(Tensor)
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | shape |
|---|---|---|---|---|---|
| x | 输入 | MoE 的 token 特征输入 | shape 为(NUM_ROWS, H);quantMode=9/13 的 MXFP4/INT4 动态量化场景,以及 quantMode=-1 且 x 为 FLOAT4_E2M1 的非量化透传场景,H 要求为偶数 | 见下文量化模式表格 | 2 |
| expertIdx | 输入 | 每行特征对应的 K 个处理专家,元素专家 id 不能超过专家数,-1 表示无效专家(该位置不参与路由,会被过滤) | shape 为(NUM_ROWS, K) | INT32 | 2 |
| scaleOptional | 输入(可选) | 用于计算量化结果的参数 | 非量化场景可选;静态量化(quantMode=0)必须输入且 shape 为[1,];动态量化(quantMode=1)可选,输入时要求 2D(expertEnd-expertStart, H);INT4 动态量化(quantMode=13)可选,输入时 shape 为(1, H)表示按 H 维广播的 smooth scale;MXFP8/HIF8 直转/FP8 PerGroup/MXFP4/FP8 PerBlock/MXFP8 RoundScale+Amax 场景不输入;HIF8 PERTENSOR(quantMode=7)必须输入 shape[1,] | FLOAT32、FLOAT8_E8M0 | 1-3 |
| offsetOptional | 输入(可选) | 用于计算 quant 结果的偏移值 | 非量化、动态量化(quantMode=1)、MXFP8/HIF8/MXFP4/FP8 PerGroup/FP8 PerBlock/INT4/MXFP8 RoundScale+Amax 场景均不输入;静态量化(quantMode=0)必须输入,shape 为[1,] | FLOAT32 | - |
属性参数(int64_t / bool / aclIntArray)
| 参数名 | 类型 | 描述与取值范围 |
|---|---|---|
| activeNum | INT64 | 总的最大处理 row 数。A2/A3:dropPadMode=0 时支持 ≥-1;-1/0 表示不限制,>0 时最多处理min(activeNum, NUM_ROWS*K)行。Ascend 950:不用于限制处理行数,仅接受 -1、0 或NUM_ROWS*K |
| expertCapacity | INT64 | 每个专家能处理的 token 数。Dropless 场景不使用;DropPad 场景必须校验且取值范围为(0, NUM_ROWS] |
| expertNum | INT64 | 专家数。key_value 模式(expertTokensNumType=2)时范围为 [1, 5120],其它模式 [1, 10240] |
| dropPadMode | INT64 | 0 表示 Dropless 场景;1 表示 DropPad 场景 |
| expertTokensNumType | INT64 | 0=cumsum 模式;1=count 模式;2=key_value 模式 |
| expertTokensNumFlag | BOOL | 是否输出 expertTokensCountOrCumsumOut,false/true |
| quantMode | INT64 | 量化场景,取值 -1、0、1、2、3、4、5、6、7、8、9、11、12、13、14、15、16、17(详见量化模式小节) |
| activeExpertRangeOptional | aclIntArray | 活跃 expert 范围[expertStart, expertEnd),左闭右开,要求 ≥0 且 expertEnd ≤ expertNum;DropPad 场景下必须为[0, expertNum] |
| rowIdxType | INT64 | 0=gather 类型索引;1=scatter 类型索引 |
输出参数(Tensor)
| 参数名 | 描述与 shape |
|---|---|
| expandedXOut | 根据 expertIdx 扩展过的特征。Dropless:[NUM_ROWS*K, H];Active:[min(activeNum, NUM_ROWS*K), H];DropPad:3D[expertNum, expertCapacity, H]。数据类型随量化模式变化 |
| expandedRowIdxOut | expandedXOut 与 x 的索引映射,shape(NUM_ROWS*K,),前 availableIdxNum 个元素有效;rowIdxType=0 时无效数据填 -1,rowIdxType=1 时无效数据未初始化。INT32 |
| expertTokensCountOrCumsumOut | 各专家处理 token 数量的统计或累加值。type=0/1 时 shape 为[expertEnd-expertStart];type=2 时 shape 为[expertNum, 2]。INT64 |
| expandedScaleOut | 量化过程中 scale 的中间值。非量化 + 输入 scale 时 shape[NUM_ROWS*K](FLOAT32),x 为 FP8/FP4 时 shape[NUM_ROWS*K, CeilDiv(H,64), 2](FLOAT8_E8M0);MXFP8(2/3/16/17)时[NUM_ROWS*K, M],M=CeilAlign(CeilDiv(H,32),2)(FLOAT8_E8M0);HIF8 PERTOKEN 时[NUM_ROWS*K](FLOAT32);MXFP4 时[NUM_ROWS*K, CeilDiv(H,64), 2](FLOAT8_E8M0);FP8 PerGroup(4/5/14/15)时[NUM_ROWS*K, CeilDiv(H,128)](FLOAT32);FP8 PerBlock(11/12)时[NUM_ROWS*K, CeilDiv(H,256), 2](FLOAT32);DropPad 场景为[expertNum*expertCapacity](FLOAT32) |
参数校验在 op_host/moe_init_routing_v3_infershape.cpp 中实现,例如:validQuantModes集合定义了全部 18 个合法 quantMode(第 82-99 行);CheckScaleShapeForStaticQuant要求静态量化 scale 为 1D 且 shape 为 1;CheckScaleShapeForDynamicQuant要求动态量化 scale 为 2D(activeExpertRange, H);GetAndCheckAttrRowIdxType在 DropPad 模式下强制rowIdxType=0;CalculateOutputDims中activeNum==0||-1时取n*k,否则取min(activeNum, n*k)。这些源码级约束与文档描述一一对应。
量化模式(quantMode)全解
量化模式是 V3 相比 V2 最核心的增强,expandedXOut的输出数据类型随 quantMode 变化:
| quantMode | 场景说明 | expandedXOut 数据类型 |
|---|---|---|
| -1 | 不量化(透传),x 支持 FLOAT16/BFLOAT16/FLOAT32/INT8/HIFLOAT8/FLOAT4_E2M1/FLOAT8_E4M3FN/FLOAT8_E5M2 | 同 x |
| 0 | 静态量化,必须输入 scale/offset | INT8 |
| 1 | 动态量化(INT8),scale 可选 | INT8 |
| 2 / 3 | MXFP8 量化 | FLOAT8_E5M2 / FLOAT8_E4M3FN |
| 4 / 5 | FP8 PerGroup(GroupSize=128,RoundScale) | FLOAT8_E5M2 / FLOAT8_E4M3FN |
| 6 | HIF8 直转量化 | HIFLOAT8 |
| 7 | HIF8 PERTENSOR 量化(必须输入 scale) | HIFLOAT8 |
| 8 | HIF8 PERTOKEN 量化 | HIFLOAT8 |
| 9 | MXFP4 量化 | FLOAT4_E2M1 |
| 11 / 12 | FP8 PerBlock(BlockSize=128),expandedScaleOut 为 FLOAT32 三维布局 | FLOAT8_E5M2 / FLOAT8_E4M3FN |
| 13 | INT4 动态量化(x 为 FLOAT32/BFLOAT16) | INT4 |
| 14 / 15 | FP8 PerGroup(GroupSize=128,RoundScale+Amax) | FLOAT8_E5M2 / FLOAT8_E4M3FN |
| 16 / 17 | MXFP8 RoundScale+Amax | FLOAT8_E5M2 / FLOAT8_E4M3FN |
各量化模式下 x 的数据类型约束:quantMode=0/1 支持 FLOAT16/BFLOAT16/FLOAT32;quantMode=2、3、4、5、6、7、8、9、11、12、14、15、16、17 支持 FLOAT16/BFLOAT16;quantMode=13 支持 FLOAT32/BFLOAT16。这些 dtype 推导逻辑在 infershape 的DetermineOutputDtypes中实现,同时在 op_host/moe_init_routing_v3_def.cpp 的OpAICoreConfig(ascend950 配置)中注册了对应 25 组输入/输出数据类型组合。
产品差异:Atlas A2/A3 系列仅支持 -1、0、1,且 quantMode=-1 时 x 仅支持 FLOAT16/BFLOAT16/FLOAT32/INT8;其余全部量化模式仅 Ascend 950PR/950DT 支持。Ascend 950 上expertTokensNumFlag仅支持 true。
约束说明与性能模板
确定性计算
aclnnMoeInitRoutingV3为默认确定性实现,保证相同输入多次运行的输出一致。
空 Tensor 处理
- A2/A3:
NUM_ROWS=0时进入空 Tensor 处理路径; - Ascend 950:
NUM_ROWS=0或K=0时没有路由元素,进入空 Tensor 处理路径,专家计数为 0,输出 shape 仍需满足相应模式约束;NUM_ROWS*K>0且H=0时仍走正常路由流程,生成expandedRowIdxOut和expertTokensCountOrCumsumOut。
输入值域
不支持输入为 inf/-inf/nan;activeExpertRangeOptional需满足expertStart < expertEnd、expertStart ≥ 0、expertEnd ≤ 10240(infershape 中GetAndCheckAttrActiveExpertRange的校验)。
性能模板准入条件
A2/A3 产品上,算子根据输入特征自动选择性能模板,不满足条件时退化为通用模板:
| 性能模板类型 | 准入条件 |
|---|---|
| 低时延性能模板 | x/expertIdx/scaleOptional shape 分别为 (1, 7168)/(1, 8)/(256, 7168);x 为 BFLOAT16;属性:activeExpertRangeOptional=[0, 256]、quantMode=1、expertTokensNumType=2、expertNum=256 |
| 大batch性能模板 | NUM_ROWS ∈ [384, 8192],K=8;属性:expertNum=256、expertEnd-expertStart≤32、quantMode=-1、rowIdxType=1、expertTokensNumType=1 |
| 全载性能模板 | dropPadMode=0;Ascend 950 非量化场景还支持 dropPadMode=1(此时 quantMode=-1、rowIdxType=0、activeExpertRange=[0, expertNum])。该模板在输入 shape 较小时将搬入、排序、计算放入同一个 kernel 内完成,避免多核同步开销 |
README 中还补充了 950 上的计数排序性能模板:FullLoad 模板要求 x 为 BFLOAT16/FLOAT16/FLOAT32/INT8、expertNum≤1024、expertEnd-expertStart≤32、quantMode=-1、dropPadMode=0;CutOrigin 模板要求 expertNum≤1024、expertEnd-expertStart≤128、quantMode 为 -1 或 0、NUM_ROWS*H*x.dtype >= 1.5*totalUbSize、NUM_ROWS*K ≥ 8192。性能模板的实际调度由 op_host/moe_init_routing_v3_tiling.cpp 及 op_host/moe_init_routing_v3_tiling_arch35.cpp 完成,tiling 数据字段(coreNum、expertStart/expertEnd、quantMode、dropPadMode、各子任务分核参数等)定义在 op_host/moe_init_routing_v3_tiling.h 中;op_host/moe_init_routing_v3_tiling_base.cpp 负责在 Tiling 准备阶段获取 AIV 核数与 UB 大小(TilingPrepareForMoeInitRountingV3),为分核决策提供硬件信息。
950 DropPad 模式特殊约束
Ascend 950 上 dropPadMode=1 时:rowIdxType 仅支持 0(gather 索引);activeExpertRangeOptional 必须为[0, expertNum];expertTokensNumType 仅支持 1(count 模式);quantMode 仅支持 -1(非量化),且 x 数据类型仅支持 FLOAT16/BFLOAT16/FLOAT32/INT8/HIFLOAT8。
返回码与错误处理
两段式接口均返回aclnnStatus状态码,具体参见 aclnn 返回码说明。第一段接口(GetWorkspaceSize)完成入参校验,典型错误如下:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 计算输入和计算输出是空指针 |
| ACLNN_ERR_PARAM_NULLPTR | 161002 | 输入和输出的数据类型不在支持的范围内 |
| ACLNN_ERR_INNER_TILING_ERROR | 561002 | 输入、输出 Tensor 的 shape 不在支持的范围内,或输入的属性不在支持的范围内 |
完整调用示例
下面是在单卡上跑通aclnnMoeInitRoutingV3的完整 C++ 示例(该示例也以 examples/test_aclnn_moe_init_routing_v3.cpp 形式存在于仓库中),编译与运行的整体流程可参考 编译与运行样例:
#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_moe_init_routing_v3.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 shape_size = 1; for (auto i : shape) { shape_size *= i; } return shape_size; } 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初始化 int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出 std::vector<int64_t> xShape = {3, 2}; std::vector<int64_t> expertIdxShape = {3, 4}; std::vector<int64_t> scaleShape = {3}; std::vector<int64_t> offsetShape = {1}; std::vector<int64_t> expandedXOutShape = {12, 2}; std::vector<int64_t> expandedRowIdxOutShape = {12}; std::vector<int64_t> expertTokensCountOrCumsumOutOptionalShape = {4}; std::vector<int64_t> expandedScaleOutOptionalShape = {12}; std::vector<int64_t> activeExpertRangeArray = {0, 4}; void *xDeviceAddr = nullptr; void *expertIdxDeviceAddr = nullptr; void *scaleDeviceAddr = nullptr; void *offsetDeviceAddr = nullptr; void *expandedXOutDeviceAddr = nullptr; void *expandedRowIdxOutDeviceAddr = nullptr; void *expertTokensCountOrCumsumOutOptionalDeviceAddr = nullptr; void *expandedScaleOutOptionalDeviceAddr = nullptr; aclTensor *x = nullptr; aclTensor *expertIdx = nullptr; aclTensor *scale = nullptr; aclTensor *offset = nullptr; int64_t activeNum = 12; int64_t expertCapacity = 0; int64_t expertNum = 256; int64_t dropPadMode = 0; int64_t expertTokensNumType = 1; bool expertTokensNumFlag = true; int64_t quantMode = -1; aclIntArray *activeExpertRange = aclCreateIntArray(activeExpertRangeArray.data(), activeExpertRangeArray.size()); int64_t rowIdxType = 1; aclTensor *expandedXOut = nullptr; aclTensor *expandedRowIdxOut = nullptr; aclTensor *expertTokensCountOrCumsumOutOptional = nullptr; aclTensor *expandedScaleOutOptional = nullptr; std::vector<float> xHostData = {0.1, 0.1, 0.2, 0.2, 0.3, 0.3}; std::vector<int> expertIdxHostData = {1, 2, 0, 3, 0, 2, 1, 3, 0, 1, 3, 2}; std::vector<float> scaleHostData = {0.3423, 0.1652, 0.2652}; std::vector<float> offsetHostData = {1.8369}; std::vector<float> expandedXOutHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::vector<int> expandedRowIdxOutHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; std::vector<int64_t> expertTokensCountOrCumsumOutOptionalHostData = {0, 0, 0, 0}; std::vector<float> expandedScaleOutOptionalHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; // 创建输入 aclTensor ret = CreateAclTensor(xHostData, xShape, &xDeviceAddr, aclDataType::ACL_FLOAT, &x); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(expertIdxHostData, expertIdxShape, &expertIdxDeviceAddr, aclDataType::ACL_INT32, &expertIdx); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(scaleHostData, scaleShape, &scaleDeviceAddr, aclDataType::ACL_FLOAT, &scale); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(offsetHostData, offsetShape, &offsetDeviceAddr, aclDataType::ACL_FLOAT, &offset); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建输出 aclTensor ret = CreateAclTensor(expandedXOutHostData, expandedXOutShape, &expandedXOutDeviceAddr, aclDataType::ACL_FLOAT, &expandedXOut); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(expandedRowIdxOutHostData, expandedRowIdxOutShape, &expandedRowIdxOutDeviceAddr, aclDataType::ACL_INT32, &expandedRowIdxOut); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(expertTokensCountOrCumsumOutOptionalHostData, expertTokensCountOrCumsumOutOptionalShape, &expertTokensCountOrCumsumOutOptionalDeviceAddr, aclDataType::ACL_INT64, &expertTokensCountOrCumsumOutOptional); CHECK_RET(ret == ACL_SUCCESS, return ret); ret = CreateAclTensor(expandedScaleOutOptionalHostData, expandedScaleOutOptionalShape, &expandedScaleOutOptionalDeviceAddr, aclDataType::ACL_FLOAT, &expandedScaleOutOptional); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用第一段接口,获取workspaceSize和executor uint64_t workspaceSize = 0; aclOpExecutor *executor; ret = aclnnMoeInitRoutingV3GetWorkspaceSize( x, expertIdx, scale, offset, activeNum, expertCapacity, expertNum, dropPadMode, expertTokensNumType, expertTokensNumFlag, quantMode, activeExpertRange, rowIdxType, expandedXOut, expandedRowIdxOut, expertTokensCountOrCumsumOutOptional, expandedScaleOutOptional, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMoeInitRoutingV3GetWorkspaceSize 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); } // 4. 调用第二段接口执行计算 ret = aclnnMoeInitRoutingV3(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnMoeInitRoutingV3 failed. ERROR: %d\n", ret); return ret); // 5. 同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 6. 将device侧结果拷贝回host并打印 auto expandedXSize = GetShapeSize(expandedXOutShape); std::vector<float> expandedXData(expandedXSize, 0); ret = aclrtMemcpy(expandedXData.data(), expandedXData.size() * sizeof(expandedXData[0]), expandedXOutDeviceAddr, expandedXSize * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < expandedXSize; i++) { LOG_PRINT("expandedXData[%ld] is: %f\n", i, expandedXData[i]); } auto expandedRowIdxSize = GetShapeSize(expandedRowIdxOutShape); std::vector<int> expandedRowIdxData(expandedRowIdxSize, 0); ret = aclrtMemcpy(expandedRowIdxData.data(), expandedRowIdxData.size() * sizeof(expandedRowIdxData[0]), expandedRowIdxOutDeviceAddr, expandedRowIdxSize * sizeof(int32_t), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < expandedRowIdxSize; i++) { LOG_PRINT("expandedRowIdxData[%ld] is: %d\n", i, expandedRowIdxData[i]); } // 7. 释放aclTensor和device资源 aclDestroyTensor(x); aclDestroyTensor(expertIdx); aclDestroyTensor(scale); aclDestroyTensor(offset); aclDestroyTensor(expandedXOut); aclDestroyTensor(expandedRowIdxOut); aclDestroyTensor(expertTokensCountOrCumsumOutOptional); aclDestroyTensor(expandedScaleOutOptional); aclrtFree(xDeviceAddr); aclrtFree(expertIdxDeviceAddr); aclrtFree(scaleDeviceAddr); aclrtFree(offsetDeviceAddr); aclrtFree(expandedXOutDeviceAddr); aclrtFree(expandedRowIdxOutDeviceAddr); aclrtFree(expertTokensCountOrCumsumOutOptionalDeviceAddr); aclrtFree(expandedScaleOutOptionalDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例中xShape={3,2}、expertIdxShape={3,4},即 3 行 token、每行选 4 个专家(NUM_ROWS=3,K=4),非量化模式(quantMode=-1)下输出expandedXOutshape 为[12, 2]。若需要验证 950 上的量化路径,可将quantMode调整为对应取值,并同步替换expandedXOut/expandedScaleOut的数据类型与 shape。
源码结构导读
moe_init_routing_v3目录按 CANN 算子标准布局组织(详见 目录清单 与 接口文档):
- 接口层op_host/op_api/:
aclnn_moe_init_routing_v3.h/.cpp暴露 aclnn 两段式 API,moe_init_routing_v3.h/.cpp为算子内部 API 封装; - 算子注册op_host/moe_init_routing_v3_def.cpp:通过
OpDef声明输入/输出/属性及ascend910b、ascend910_93、ascend950三个 AICore 配置; - shape/dtype 推导op_host/moe_init_routing_v3_infershape.cpp:全部属性与 shape 校验、输出 shape/dtype 推导;
- tilingop_host/moe_init_routing_v3_tiling*.cpp/h:分核、分块与性能模板调度;
- kernelop_kernel/:按场景拆分为 sort(单核/多核/合并排序)、gather(含 droppad、mxfp4/mxfp8、hif8、fp8 group/perblock 量化)、full_load、expert_tokens_count、mrgsort 等子模块,arch35 子目录为 950 平台实现;
- 产品配置op_host/config/:ascend910_93、ascend910b、ascend950 三套
binary.json与simplified_key.ini; - 测试tests/:
ut/op_host/op_api/test_aclnn_moe_init_routing_v3.cpp覆盖两段式接口的参数校验与执行路径,ut/op_host/下还有 tiling 与 infershape 单测,st/下含 aclnn 的算子级系统测试用例与golden.py数据生成脚本;另有 tests/ut/op_kernel/ 的 kernel 级测试(gen_data.py/gen_tiling.py生成输入与 tiling 数据)。
小结
aclnnMoeInitRoutingV3是 CANN ops-transformer 中面向 MoE 路由场景的高阶算子:它以两段式 aclnn 接口承接 Gating 结果,通过排序 + 直方图统计 + Gather/Scatter 搬运完成 token 到专家的路由分发,并在搬运的同时完成从透传到 INT8/FP8/FP4/INT4/HIFLOAT8 的多种量化,支持 Dropless、Active、DropPad 三种输出布局。理解其参数语义、量化模式与产品差异,是将其正确接入自有 MoE 推理/训练链路、并利用性能模板获得更优执行效率的前提。
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考