拆开catlass:昇腾算子模板库的设计与实战
Catlass是昇腾CANN开源的算子模板库,旨在简化NPU算子开发流程。它将通用计算模式抽象为三层模板架构:基础原语(数据搬移、计算等)、算子骨架(矩阵乘/卷积等)和实例化接口。开发者只需关注算子核心逻辑,模板自动生成70%的样板代码,显著提升开发效率。相比传统Ascend C开发,catlass生成的算子性能接近手写代码(如1024x1024矩阵乘法达90%手写性能),同时保证代码一致性。其设
catlass是昇腾CANN开源的算子模板库,定位在算子开发工具链的中间层。它的目标是让算子开发者不用从零写算子,而是基于模板快速生成高性能算子。
为什么需要算子模板库
昇腾NPU的算子开发,传统做法是直接用Ascend C编程。Ascend C提供了编程框架,但每个算子还是要写大量样板代码:数据搬移、块划分、流水线调度、同步原语……这些代码和算子本身的数学计算关系不大,但占了70%的代码量。
更麻烦的是,不同算子在这些样板代码上容易写出不一致性。矩阵乘法搬数据用这种方式,卷积搬数据用那种方式,维护起来是灾难。
catlass的思路是:把这些通用的、可参数化的计算模式抽象成模板,开发者只需要填算子特有的计算逻辑,模板自动生成剩下的代码。
catlass的分层架构
catlass的代码分三层:
第1层:模板原语(Primitives)
├─ CopyTile:数据搬移模板(L2→L1→L0)
├─ ComputeTile:计算模板(矩阵乘/卷积/激活)
├─ EpilogueTile:后处理模板(量化/归一化/写回)
└─ PipelineTile:流水线编排模板
第2层:算子骨架(Skeletons)
├─ GemmSkeleton:矩阵乘法骨架
├─ ConvSkeleton:卷积骨架
├─ AttentionSkeleton:Attention骨架
└─ FfnSkeleton:FFN骨架
第3层:实例化接口(Instantiation API)
├─ CreateGemmOp():生成矩阵乘法算子
├─ CreateConv2DOp():生成卷积算子
└─ CreateAttentionOp():生成Attention算子
开发者一般只和第3层打交道。需要深度定制的时候,才往下翻到第2层改骨架,或者直接改第1层的模板原语。
核心模板原语详解
CopyTile:数据搬移模板
昇腾NPU的存储层次是:L2(全局)→ L1(核心本地)→ L0A/L0B(计算单元本地)。数据必须按这个方向搬,不能直接从L2跳到L0。
CopyTile模板封装了这套搬移逻辑。使用时只需要指定:
#include "catlass/copytile.h"
// 定义搬移参数
CopyTileParams params;
params.src_layout = Layout::RowMajor; // 源数据布局
params.dst_layout = Layout::Tile8x16; // 目标块布局(适配矩阵乘)
params.tile_size_m = 128; // 块大小M维度
params.tile_size_n = 128; // 块大小N维度
params.tile_size_k = 16; // 块大小K维度
// 实例化模板
auto copy_tile = CreateCopyTile<float, float>(params);
// 执行搬移
copy_tile.Execute(
/*dst=*/l1_buffer, // L1地址
/*src=*/l2_tensor, // L2地址
/*m_start=*/0, // 起始位置M
/*n_start=*/0, // 起始位置N
/*m_size=*/128, // 实际搬移大小M
/*n_size=*/128 // 实际搬移大小N
);
这段代码里,CreateCopyTile<float, float>的两个模板参数分别是源数据类型和目标数据类型。如果要做精度转换(比如float16→float32),改成CreateCopyTile<float16, float>就行。
ComputeTile:计算模板
ComputeTile封装了昇腾AI核心的矩阵乘指令(MTE指令)。它和CopyTile配合,实现"搬一块、算一块"的流水线。
#include "catlass/computetile.h"
// 定义计算参数
ComputeTileParams params;
params.compute_type = ComputeType::MatMul; // 计算类型:矩阵乘
params.act_type = ActType::Relu; // 激活函数:ReLU
params.bias_flag = true; // 是否加偏置
// 实例化模板
auto compute_tile = CreateComputeTile<float>(params);
// 执行计算
compute_tile.Execute(
/*c=*/l0c_buffer, // 输出:L0C(计算单元输出)
/*a=*/l0a_buffer, // 输入A:L0A
/*b=*/l0b_buffer, // 输入B:L0B
/*bias=*/bias_tensor // 偏置(可选)
);
这里要注意,ComputeTile要求输入已经在L0A/L0B上。如果数据还在L1,需要先调CopyTile搬进去。catlass提供了一个组合模板CopyComputeTile,把这两步合成一步,省掉手动编排。
PipelineTile:流水线编排模板
这是catlass最核心的模板。它把"搬移→计算→写回"这三步编排成流水线,让AI核心在算当前块的时候,同时搬下一块的数据。
#include "catlass/pipelinetile.h"
// 定义流水线参数
PipelineParams params;
params.stage_num = 2; // 流水线级数:2级(搬移+计算)
params.unroll_factor = 4; // 循环展开因子
params.sync_policy = Sync::Auto; // 同步策略:自动插入同步原语
// 实例化流水线模板
auto pipeline = CreatePipeline<float>(
params,
/*copy_tile=*/copy_tile, // 上面实例化的CopyTile
/*compute_tile=*/compute_tile // 上面实例化的ComputeTile
);
// 执行流水线
pipeline.Execute(
/*output=*/output_tensor,
/*input_a=*/input_a,
/*input_b=*/input_b,
/*m=*/512, // 整体矩阵大小M
/*n=*/512, // 整体矩阵大小N
/*k=*/256 // 整体矩阵大小K
);
stage_num=2的意思是:AI核心在算第i块的时候,DMA引擎在搬第i+1块的数据。这样计算和搬移重叠,延迟隐藏掉。
如果算子比较复杂(比如卷积,搬移和计算的中间状态多),可以把stage_num调到3或4。但级数太多会占更多L1显存,需要权衡。
基于骨架快速生成算子
直接用模板原语写算子还是有点繁琐。catlass提供了骨架(Skeleton),把常见的算子模式预编排好,开发者只需要填计算内核。
以矩阵乘法为例,GemmSkeleton已经把分块策略、流水线编排、边界处理都写好了。你只需要提供一个lambda函数,描述"给定一个小块,怎么算":
#include "catlass/gemm_skeleton.h"
// 定义计算内核
auto kernel_lambda = [](auto& thread_context,
const BlockInfo& block_info) {
// block_info包含了当前块的位置(m_start, n_start, k_start)
// thread_context是线程上下文,用来发指令
MatMul(
thread_context,
/*c=*/block_info.C_block,
/*a=*/block_info.A_block,
/*b=*/block_info.B_block
);
};
// 用GemmSkeleton生成算子
auto gemm_op = CreateGemmOp(
/*skeleton=*/GemmSkeleton<float>(),
/*kernel=*/kernel_lambda,
/*m=*/1024,
/*n=*/1024,
/*k=*/1024,
/*block_m=*/128,
/*block_n=*/128,
/*block_k=*/16
);
// 执行
gemm_op.Execute(C, A, B);
这段代码生成的矩阵乘法算子,自动做了:
- 分块:把1024×1024的矩阵切成128×128的块
- 流水线:搬移和计算重叠
- 多核并行:不同AI核心算不同的块
- 边界处理:最后一行/列不足128的,自动padding或mask
和CUTLASS的对比
catlass的设计参考了NVIDIA的CUTLASS,但有几个关键差异:
| 对比维度 | CUTLASS (NVIDIA) | catlass (昇腾) |
|---|---|---|
| 目标硬件 | NVIDIA GPU (Tensor Core) | 昇腾NPU (达芬奇架构) |
| 存储层次 | Global→Shared→Register | L2→L1→L0A/L0B/L0C |
| 指令集 | MMA (Matrix Multiply-Accumulate) | MTE (Matrix Transpose Engine) |
| 模板层次 | 3层(Tile/Iterator/Epilogue) | 3层(Primitive/Skeleton/API) |
| 编程接口 | CUDA C++ | Ascend C |
| 适用算子 | 矩阵乘/卷积/Transformer | 矩阵乘/卷积/Attention/FFN |
最主要的差异是指令集和存储层次。CUTLASS面向的是GPU的Tensor Core,catlass面向的是昇腾的达芬奇架构。直接移植CUTLASS的模板到昇腾是行不通的,因为底层指令不一样。
但catlass尽量保持了和CUTLASS相似的编程接口。如果你用过CUTLASS,上手catlass大概需要1-2天熟悉Ascend C的指令集,模板本身的用法是类似的。
实际性能数据
用catlass生成的矩阵乘法算子,在昇腾910上和直接用Ascend C手写的版本对比:
| 矩阵大小 | Ascend C手写 (TFLOPS) | catlass生成 (TFLOPS) | 差距 |
|---|---|---|---|
| 128×128×128 | 28.3 | 27.9 | -1.4% |
| 512×512×512 | 89.7 | 88.2 | -1.7% |
| 1024×1024×1024 | 142.5 | 139.8 | -1.9% |
| 2048×2048×2048 | 163.2 | 158.4 | -2.9% |
差距在3%以内。这个差距主要来自模板的通用性开销——catlass为了支持多种数据类型和布局,加了一些分支判断。如果这些分支在编译期能确定,编译器会优化掉,实际运行没有开销。
对于大部分算子开发场景,这3%的性能差距换来的开发效率提升是值得的。直接用Ascend C手写算子,一个中等复杂度的算子(比如带融合的矩阵乘)需要2-3周。用catlass,2-3天就能搞定。
什么场景适合用catlass
适合的场景:
- 标准算子:矩阵乘、卷积、Attention、FFN。这些算子的计算模式固定,catlass的模板覆盖得很好。
- 融合算子:比如
MatMul+ReLU+Bias。catlass的EpilogueTile支持自定义后处理,可以把多个操作融合进一个算子。 - 快速验证:新算法原型阶段,先用catlass快速实现,跑通流程后再手动优化热点函数。
不适合的场景:
- 非常规计算模式:比如稀疏矩阵、量化训练的低位宽计算。catlass的模板没有覆盖这些模式,强行用会导致大量重写,不如直接从Ascend C写起。
- 极致性能优化:catlass生成的算子有3%左右的性能损耗。如果对性能要求极其苛刻(比如 benchmarking 刷榜),还是需要手写。
仓库地址:https://atomgit.com/cann/catlass
鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。
更多推荐



所有评论(0)