← 返回首页

昇腾算子地图:编排层、自研算子、CANN 三层逐层展开

一层前向的每一行,到了 NPU 上都是一个或一串算子——这些算子不是散沙,它们分三层住:编排层不出算子、插件自研层藏着 DSA 特化、CANN 层有两户人家。先看三层全景,再对照组件×楼层矩阵,然后逐层展开族谱,最后拿真实 profiling 数据认亲
读前须知:四个名词 30 秒(更多见文末术语速查)

1. 全景:三层算子楼

从基础已知出发:用 PyTorch 写一层前向(②),LN、attention、残差、MoE 四五行代码——每一行到了 NPU 上都要变成具体的算子。这些算子住在哪、谁管它们?答案是一栋三层楼(③的目录结构就是这栋楼的门牌):顶层是 vllm-ascend 的 Python 编排层,中间是插件自研的原生算子层,底层是 CANN 算子层。先看全景:

三层算子楼:每层算子聚集于啥、什么策略、什么特点 顶 · 编排层(vllm-ascend Python:attention/ 与 ops/ 分工) 聚集于:调度与备料——选路(配哪个引擎)、元数据、布局坐标;「不出算子,只出调用」 策略:数学全部下沉给楼下,自己只管"怎么调、按什么形状调" 特点:纯 Python——看得见、改得动、每次改不用重编译 代表调用点:fused_qkv_a_proj(nn.Linear)、ops/mla.py 的 custom op 注册 attention/ 管注意力怎么算;ops/ 管算子怎么接进昇腾——两回事,分开认 中 · 插件自研算子层(_C_ascend:源码在插件仓库 csrc/) 聚集于:DSA/MLA 特化与融合大算子——稀疏注意力、索引器、动态量化 策略:AscendC 内核源码写在插件仓库(csrc/*/op_host),按 CANN 自定义格式编译加载 特点:MIX_AIC 重量级——单次毫秒级,是 top 榜常客 代表:npu_sparse_attn_sharedkv、npu_vllm_quant_lightning_indexer、 npu_rms_norm_dynamic_quant、compressor 一族 底 · CANN 算子层(两户人家) 3a 标准算子库(torch_npu 暴露 / aclnn / 普通 matmul 自动到达):  聚集于通用数学——矩阵乘、归一化+rope+写 cache 融合、elementwise、通信 3b 自定义算子包(libcust_opapi.so,staticKey 那几个文件):  聚集于 MoE 大融合——DispatchFFNCombine 一家占实测窗口四成 特点:内核本体随 CANN 包版本走——实现变更要对版本账(staticKey 对比) 代表:MatMulV2、npu_kv_rmsnorm_rope_cache、AivKernel、DispatchFFNCombine 读法:三个楼层框=本文第 2/3/4 章逐层展开的顺序;一个组件的算子常散住多层——谁住哪格,见图 2 的组件×楼层矩阵。 三层的判断口诀(三看):torch_npu.npu_x 或普通 matmul → 层三 3a;torch.ops._C_ascend → 层二;纯 Python 流程 → 层一。
图 1 · 三层算子楼全景:编排层出调用、自研层出特化融合、CANN 层出本体——每层的聚集点、策略与特点

一栋楼三层的分工一句话:编排层决定"调谁、怎么调";自研层放"别处没有、必须自己写"的特化算子;CANN 层是"别人早就写好、拿来就用"的算子本体库。写下的一行 torch.matmul 不会经过顶两层的代码,但它最终执行的内核一定住在层三。

楼层不等于组件:没有任何一个模型组件只住一层——attention 的算子散在 3a(投影矩阵乘、KV 融合)与层二(稀疏注意力本体、索引器);MoE 散得更开(层二的路由 hash 算子、3a 的重排与激活、3b 的大融合)。图 2 的矩阵把这张多对多关系摆全:

组件 × 楼层矩阵:一个组件的算子散住多层(多对多) 组件 层一 · 编排 层二 · _C_ascend 层三 · 3a 标准库 层三 · 3b 包 不出算子 只出调用 四类组件的 调用序列 都在这层编排 LayerNorm (归一化) npu_rms_norm _dynamic_quant (Q 侧动态量化) npu_rms_norm (标准归一化) — attention (含投影选块) npu_sparse_attn _sharedkv(本体) lightning indexer MatMulV2(QKV/o_proj 投影) npu_kv_rmsnorm_rope_cache 索引器 torch_npu 份(选块) — 残差 h = h + x — ForeachAddList / AivKernel (elementwise 直接加) — MoE (路由专家合并) moe_gating_top_k_hash (hash 版路由) 共享专家量化族 torch.topk+gather(默认路由) npu_moe_token_permute npu_swiglu · 专家 GEMM DispatchFFN Combine(一体) 内核在本层 读法:多对多——attention 的算子散在 3a 与层二,MoE 散在层二、3a、3b;「—」=该层没有这个组件的常住算子;层一整列不出算子。 逐格依据(源码实测行号):npu_rms_norm=attention/sfa_v1.py:1818;dispatch_ffn_combine=ops/fused_moe/ moe_comm_method.py:485(torch.ops._C_ascend 下发);moe_gating_top_k_hash=router/fused_topk_router.py:194; 默认路由 torch.topk=fused_topk_router.py:57;npu_swiglu=ops/fused_moe/moe_mlp.py:95; npu_moe_token_permute/unpermute=token_dispatcher.py:592/706(torch_npu 下发); 共享专家量化族=shared_experts.py:326-389;占比与 trace 名口径=op_statistic.csv。
图 2 · 组件 × 楼层矩阵:attention 散住 3a 与层二、MoE 散住层二·3a·3b——多对多,层一整列不出算子

读矩阵的三点:一、多对多是常态——排查"attention 为什么慢"时,答案可能同时在 3a 的矩阵乘与层二的稀疏注意力,别只盯一层;二、层一整列空着不是漏画——编排层不出算子,四类组件的调用序列都由它编排,这正是它的工作方式;三、每格都有出处——图内下方列了源码文件与行号。DispatchFFNCombine 尤其特殊:调用走 _C_ascend 注册下发(moe_comm_method.py:485),内核本体却住在 3b 自定义算子包(staticKey 对比的对象)——两户联办。

下面逐层展开——每层先讲这一层的算子聚集于什么、什么策略,再看这一层的族谱。

2. 层一 · 编排层:不出算子,出调用

编排层怎么干活,拿 attention 一条调用链看最直观(②:用 PyTorch 实现推理时认识的那种前向)——这条链在 vllm-ascend 里被编排层一步步接手。这一层一个算子都不出:它出的是"调用与备料"——选哪个后端、元数据怎么建、张量按什么布局递下去。拿链上的五步看编排层每天在干什么:

层一 · 编排层:流水线五步里,它干了啥、楼下谁出算子 流水线步(attention 链) 编排层的活(纯 Python) 楼下谁出算子 步3 · Q/KV 低秩投影 fused_qkv_a_proj 一次矩阵乘出 Q 路和 KV latent 只是调 nn.Linear 编排层递张量、管权重, 矩阵乘不在自己手里 层三 3a MatMulV2 一族 (trace 里按形状认) 步4 · KV 侧融合 投影+norm+rope+写 cache 一枪出 组参数、递句柄 调 torch_npu 的融合接口, 融合本体不在自己手里 层三 3a npu_kv_rmsnorm_ rope_cache 选块 · indexer 长历史里选 top-2048 块 先挑后算 双家二选一 GLM(glm_moe_dsa)走 torch_npu 那份索引器 层三 3a 或层二 (lightning indexer 两个家各一份) 步8 · 稀疏注意力 QKᵀ/遮罩/softmax/·V 在选中块上算 调自研注册算子 torch.ops._C_ascend .npu_sparse_attn_sharedkv 层二 trace 里叫 SparseFlashAttention 步9 · o_proj 收口 输出投影交回层外壳 又是 nn.Linear 编排层管收口与归约 开关(归约权归谁) 层三 3a MatMulV2 一族 读法:五步里编排层(中列,蓝)只做调用与备料;右列=每步真正出算子的家。三列虚线=行行对应。
图 3 · 编排层展开:五步流水线里它只出调用——投影走 3a 矩阵乘、KV 融合走 3a、稀疏注意力走层二、索引器双家二选一

这一层的策略与特点:聚集于调度与备料,策略是"数学全部下沉"——所以它的改动(选路、开关、元数据)都是纯 Python,看得见、改得动、不用重编译;两个 PR 的改动落点也全在这一层(#18870 的选路窄门、#18961 的备料复用)。顺带把一件容易混的事拆开:attention/ 目录管"注意力怎么算"(编排注意力的调用链),ops/ 目录管"算子怎么接进昇腾"(custom op 注册与分发)——两回事,别合成一个框记。

3. 层二 · 插件自研算子层:_C_ascend 族谱

有些算子别处没有、必须自己写:DSA 的稀疏注意力语义、索引器的量化选块、Q 侧动态量化——CANN 标准库里没有现成对应物。vllm-ascend 把它们写在自己的 csrc/ 里,编译成 _C_ascend 扩展随插件安装。按功能分四族:

层二 · 插件自研算子(_C_ascend):按功能分四族 族A · 稀疏注意力族 npu_sparse_attn_sharedkv(DSA 主算子) npu_kv_quant_sparse_attn_sharedkv(A5 走这份) npu_sparse_attn_sharedkv_metadata(配套元数据) npu_sparse_flash_attention / score_prefill 一组 为什么自研:top-2048 稀疏语义标准库没有; trace 里叫 SparseFlashAttention(占实测 11.4%) 执行形态:MIX_AIC——Cube+Vector 混合大算子 选路由 device/device_op.py 按硬件代次分发 族B · 索引器族 npu_vllm_quant_lightning_indexer(量化选块) npu_vllm_quant_lightning_indexer_metadata npu_lightning_indexer(非量化的那份) 为什么自研:长历史里先挑相关块再算—— 索引器是 DSA"省算力"的源头 双家实例:torch_npu 也有一份同名能力; 代码按模型二选一——GLM(glm_moe_dsa)走 torch_npu 那份,其余走 _C_ascend 这份 族C · 归一化·旋转·量化族 npu_rms_norm_dynamic_quant(Q 侧动态量化) inplace_partial_rotary_mul(原地部分旋转) npu_gemma_rms_norm / npu_add_rms_norm_bias 为什么自研:动态量化+原地旋转省中间张量; 融合掉的都是"小而高频"的活 对应朴素写法:norm(x)、rope(q) 两行 高频小算子融合——省的是带宽与 launch 次数 名字带 inplace:直接改输入,不另开缓冲 族D · 融合特化族(AscendC 源码同仓库) compressor / compressor_metadata(DSA 压缩) fused_sparse_attention_overlap(计算通信重叠) indexer_compress_epilog_v2(索引压缩收尾) kv_compress_epilog(A5:量化+压缩+散射一体) 为什么自研:多步合一+与执行流协同, csrc/attention/*/op_host/ 有 AscendC 源码 (arch32/arch35 两版 tiling 按硬件适配) 读法:注册表全在 csrc/torch_binding.cpp(两个 TORCH_LIBRARY 块);这一层="源码在插件、执行靠 CANN"——按 CANN 自定义格式编译加载。 改这层的算子=改插件源码重编译;另有一部分 _C_ascend op 只是薄适配,转发到 CANN 的 aclnn 接口(内核仍在层三)。
图 4 · _C_ascend 族谱:稀疏注意力、索引器、归一化·旋转·量化、融合特化四族——每族"住着谁、为什么自研"

这一层的策略与特点:聚集于 DSA/MLA 特化与融合大算子;策略是"源码进仓库"——AscendC 内核写在 csrc/ 里、按 CANN 自定义算子格式编译加载,跟插件同版本发布;特点是重量级(MIX_AIC,单次毫秒级)且数量可控(四族几十个)。认它的口诀:代码里看到 torch.ops._C_ascend.xxx 就是这一层。

4. 层三 · CANN 算子层:两户人家,各有族谱

最底下一层住着两户人家:3a 标准算子库——torch_npu 暴露的 npu_* 接口、aclnn 前缀的标准算子、以及普通 matmul 自动到达的内核;3b 自定义算子包——libcust_opapi.so 那几个文件(Profiling 排查时对 staticKey 的对象),MoE 的大融合算子住这里。3a 内部再分族:

层三 · CANN 算子层:3a 标准库(内分五族)+ 3b 自定义算子包 3a · 标准算子库(torch_npu 暴露 / aclnn / 普通 matmul 自动到达) 矩阵乘族——普通 matmul 的家 MatMulV2(3.6%)· QuantBatchMatmulV3(12.2%,NZ 权重)· TransposeBatchMatMul(4.9%)· BatchMatMulV2 融合预处理族——把朴素写法的多行合到一枪 npu_kv_rmsnorm_rope_cache(投影+norm+rope+写 cache)· npu_quant_matmul(量化矩阵乘) elementwise 族——trace 里的聚合名 AivKernel(8.5%) Fill · IndexCheck · ForeachAddList(1.9%,残差/列表加)· AscendQuantV2(3.2%,W8A8 激活量化)· cast 一类 写回族——KV cache 的落笔 npu_scatter_nd_update_(按 slot 页表散写)· ScatterNdUpdate(trace 名)——每层每 chunk 写一次 通信族——AI_CPU 上执行(不走 AI Core) allgatherAicpuKernel(4.1%)· reduce_scatterAicpu(1.5%)——torch.distributed 集合通信下发 五族覆盖了窗口实测 top10 里的九席——普通一层前向的数学,几乎全落在 3a。 3b · 自定义算子包(libcust_opapi.so / libcust_opsproto_rt2.0.so,staticKey 对比的对象) MoE 通信融合族:DispatchFFNCombine——dispatch+专家 FFN+combine 一体, 实测一家占窗口总耗时 40.5%(12007 次);fused_mc2 开关的产物,拆开则变三个中算子 黑盒大算子:慢了没法在框架层再拆——只能对版本账(staticKey)或找算子团队 读法:上大框=3a 标准库(内分五族,每族带 GLM-5.1 prefill 窗口实测占比);下框=3b 自定义包(MoE 大融合)。 占比口径:op_statistic.csv 自带 Ratio 列;3a+3b 的 top10 合计约九成——剩余是长尾小算子。 3a 的入口三种:torch_npu.npu_xxx(显式)/aclnn 前缀(标准下发)/普通 torch.matmul(自动到达)。 3b 的入口:框架组参调用——版本实现变更看 staticKey(Profiling 文章的归因案例)。
图 5 · CANN 层两户族谱:3a 标准库内分五族(矩阵乘/融合预处理/elementwise/写回/通信),3b 自定义包住 MoE 大融合

这一层的策略与特点:3a 聚集于通用数学——朴素前向里的每一行几乎都能在这里找到对应内核,入口有三种(显式 torch_npu 调用、aclnn 标准下发、普通 matmul 自动到达);3b 聚集于 MoE 大融合——代价是黑盒。两户共同的特点:内核本体随 CANN 包版本走——这正是 Profiling 排查时对 staticKey 而不是对代码的根本原因。

5. 认亲:trace 里的名字对回代码

拿真实 profiling 数据(GLM-5.1,A3,prefill 窗口 op_statistic.csv)把 top10 逐个对回三层楼——这张表就是"拿到一份 trace 怎么读"的练习:

trace 名(op_statistic)代码里的名字 / 来源住哪层占比
DispatchFFNCombine同名(fused_mc2 开关选的融合形态)层三 · 3b 自定义包40.5%
QuantBatchMatmulV3量化矩阵乘下发(名字常带 WeightNz——NZ 权重布局)层三 · 3a 矩阵乘族12.2%
SparseFlashAttentiontorch.ops._C_ascend.npu_sparse_attn_sharedkv层二 · 稀疏注意力族11.4%
AivKernel聚合名:Fill / IndexCheck / foreach / cast 一类 vector 小算子层三 · 3a elementwise 族8.5%
TransposeBatchMatMul普通 torch.matmul 的转置形态层三 · 3a 矩阵乘族4.9%
allgatherAicpuKerneltorch.distributed 集合通信下发层三 · 3a 通信族(AI_CPU)4.1%
MatMulV2nn.Linear → torch.matmul层三 · 3a 矩阵乘族3.6%
AscendQuantV2W8A8 的激活量化下发层三 · 3a elementwise 族3.2%
ForeachAddList残差/列表加(torch foreach 下发)层三 · 3a elementwise 族1.9%
reduce_scatterAicputorch.distributed 集合通信下发层三 · 3a 通信族(AI_CPU)1.5%
三看口诀(判断一个算子住哪)
名字对不上怎么办(trace 名 ≠ 代码名是常态) 图融合会把多个小算子合成一个名字(AivKernel 就是这么来的);aten 层算子下发的 kernel 名也和 Python 名不同。对不上时用 shape + 调用次数去认:ForeachAddList 12008 次 ≈ DispatchFFNCombine 12007 次——成对出现的就在同一条执行路径上(MoE 的随从);ScatterNdUpdate 10611 次 == SparseFlashAttention 10611 次——KV 写入跟着 attention 走。别死磕名字。
同一算子两个家的实例 lightning indexer(DSA 选块的"闪电索引器")在 torch_npu(层三 3a)和 _C_ascend(层二)各有一份;代码按模型二选一——GLM(glm_moe_dsa)走 torch_npu 那份,其余走 _C_ascend 那份。所以"这个算子住哪"有时要加一句:住哪家还看跑的是什么模型。

6. 与我们排查问题的关系

7. 术语速查

术语本文语境下的含义
三层楼编排层(vllm-ascend Python)/ 插件自研算子层(_C_ascend)/ CANN 算子层(3a 标准库 + 3b 自定义包)
_C_ascendvllm-ascend 编译安装的原生算子扩展;注册表在 csrc/torch_binding.cpp,调用形如 torch.ops._C_ascend.xxx
torch_npuPyTorch 的昇腾适配层——npu_* 接口的绑定在此,内核本体在 CANN
3a / 3bCANN 标准算子库 / CANN 自定义算子包(libcust_opapi.so)——后者是 staticKey 对比的对象
MIX_AICCube+Vector 混合执行的大算子核心类型——融合算子多为它,top 榜重量级
AivKerneltrace 里 vector 小算子的聚合名(Fill/IndexCheck/foreach/cast 一类),看明细要回 kernel_details 按 Name+shape 拆
staticKey自定义算子包里标识 kernel 实现版本的键——两版本全 DIFF 即实现变更
NZ 布局矩阵单元友好的权重存放格式——量化矩阵乘名字里常带 WeightNz
AscendC写昇腾内核的语言/工具链——层二的 op_host 源码与 3b 的内核都是它写的

来源:算子名单与调用点以本地 vllm-ascend 源码实测(torch_npu 调用、csrc/torch_binding.cpp 注册表、device_op.py 选路);占比为 GLM-5.1(A3)prefill 窗口 op_statistic.csv 实测(姊妹篇《Profiling 实战解读》的数据同源)· 行号以当期源码为准

华杉的技术博客 · 苏ICP备2026063680号-1
首页