catlass仓库概览:昇腾算子开发的高层抽象
去年CANN全面开源的时候,catlass仓库是最让我眼前一亮的——它把昇腾NPU上的算子开发门槛降到了一个新低点。之前写一个GEMM算子要三天,现在10分钟。catlass用C++模板元编程把达芬奇架构的硬件细节封装起来,让算子开发者只需要关心"算什么",不用操心"怎么算"。这篇概览把catlass的设计全貌展开讲清楚。
仓库定位:算子模板库,加速库与模板仓库之一
catlass在CANN 55个仓库中的定位是加速库与模板仓库,和ATB(Transformer加速库)、asnumpy(NPU原生NumPy兼容库)、graph-autofusion(算子自动融合框架)同属一个大类。但catlass的定位和其他几个不一样——ATB面向应用层(推理加速),asnumpy面向科学计算层(NumPy兼容),catlass面向算子开发层(算子生成)。
在CANN五层架构中,catlass位于第二层昇腾计算服务层的AOL算子库内。它和ops-blas的关系最密切——两者都做GEMM,ops-blas提供手工精调的核心GEMM实现,catlass提供模板生成的GEMM变体。catlass还和ops-nn、ops-transformer有间接关系——这些仓库的某些融合算子可以使用catlass生成的GEMM组件作为底层计算引擎。
catlass的核心内容是基于C++模板元编程的算子生成框架。它把GEMM算子的实现拆解成四层抽象(概念→策略→组件→实现),每层的参数都可以通过模板参数化,用户通过选择不同的模板参数组合来生成不同变体的算子。
从依赖关系看,catlass依赖opbase(通用组件,提供内存管理和算子注册),被ops-blas(作为备选GEMM实现)和上层框架(通过AOL算子选择器间接使用)所依赖。catlass不依赖其他ops-*仓库——它的GEMM实现是自包含的,不需要调用其他算子。
设计哲学:用编译期复杂性换运行期确定性
catlass的设计哲学可以概括为一句话:把算子开发中的模式化工作在编译期完成,让运行期的行为完全确定和高效。
"模式化工作"指的是每个GEMM算子都要做的事情——数据搬运策略(从HBM到L2到L1到L0)、tiling分块(矩阵如何切成小块)、DMA搬运时序(什么时候搬什么数据到哪里)、多Core调度(哪些Core处理哪些tile)、边界处理(矩阵维度不是tile大小倍数时的处理)。这些工作和GEMM的核心计算逻辑(矩阵乘法的内积)无关,但占据了算子代码的大部分篇幅。
catlass把这些模式化工作用C++模板参数化,编译器在编译期根据用户选择的模板参数展开成具体的代码。运行时没有任何虚函数调用、没有条件分支、没有运行时参数检查——所有的决策都在编译期做出了。
这种设计的代价是编译时间较长(一个GEMM变体约30-60秒),模板错误信息不友好(C++模板错误是出了名的难读)。但换来的是运行期的确定性和高性能——编译期展开的代码和手写的最优代码几乎没有区别。
四层抽象体系
catlass的模板抽象分为四层,从上到下越来越具体:
算子概念层(Concept)
概念层定义算子的数学语义。比如GEMM的概念是C = alpha * A * B + beta * C,Conv2D的概念是im2col(A) × weight + bias。概念层是最稳定的——一个算子的数学语义不会因为硬件架构或实现策略的变化而改变。
catlass目前支持的概念类型包括:Gemm(矩阵乘法)、GemmWithEpilogue(带后处理的矩阵乘法)、Conv2D(卷积)。每种概念类型有对应的模板类,用户通过继承和特化来定义自己的算子概念。
算子策略层(Policy)
策略层定义算子在NPU上的执行策略。策略层是catlass最核心也最复杂的抽象——它把硬件相关的决策参数化,用户通过选择不同的策略模板来适配不同的硬件和使用场景。
策略层的参数包括:L1 tile大小(如128×128×32)、L0 tile大小(如16×16×16)、缓冲级数(单缓冲/双缓冲/三缓冲)、流水线策略(简单/流水线化)、Core调度策略(RoundRobin/Greedy/Static)。这些参数的组合决定了算子在NPU上的执行效率。
catlass提供了一组预设策略(PredefinedPolicy),针对Ascend 910的不同配置做了调优。用户可以直接使用预设策略,也可以自定义策略参数。
算子组件层(Component)
组件层把算子的执行流程分解成标准化的模块。一个GEMM算子的组件包括:
Prologue:数据预取——从HBM加载矩阵A和B的tile到L2缓冲区。
Mainloop:主计算循环——从L2搬运tile到L1,从L1搬运子tile到L0,在Cube单元执行矩阵乘法,结果写回L1。重复直到所有tile计算完毕。
Epilogue:后处理——在Vector单元对矩阵乘法结果做缩放、偏移、激活等操作,写回HBM。
每个组件有明确的接口和职责,组件之间通过L1缓冲区传递数据。这种标准化分解使得组件可以被替换——比如你可以自定义Epilogue组件来实现特殊的后处理逻辑。
// catlass的组件接口(概念示意)template<typenameConfig,typenamePolicy>classGemmKernel{public:usingPrologue=typenamePolicy::Prologue;usingMainloop=typenamePolicy::Mainloop;usingEpilogue=typenamePolicy::Epilogue;__aicore__voidInit(GM_ADDR a,GM_ADDR b,GM_ADDR c,GM_ADDR bias){prologue_.Init(a,b,c);mainloop_.Init();epilogue_.Init(c,bias);}__aicore__voidProcess(){// WHY: 三段式执行流程是catlass的核心架构// Prologue准备数据,Mainloop做计算,Epilogue做后处理// 三段可以流水线化——Mainloop计算tile-N时,Prologue预取tile-N+1for(inttile=0;tile<total_tiles;tile++){// Prologue: 预取下一个tile的数据到L2if(tile+1<total_tiles){prologue_.Prefetch(tile+1);// WHY: DMA异步搬运,不阻塞当前计算}// Mainloop: 在Cube上计算当前tilemainloop_.Compute(tile);// Epilogue: 在Vector上做后处理// WHY: Epilogue可以和下一个tile的Mainloop重叠epilogue_.Apply(tile);}}private:Prologue prologue_;Mainloop mainloop_;Epilogue epilogue_;};这段代码展示了catlass的三段式执行架构。Prologue负责数据预取(DMA异步搬运),Mainloop负责核心计算(Cube单元),Epilogue负责后处理(Vector单元)。三段可以流水线化——当前tile的Epilogue和下一个tile的Mainloop可以在不同执行单元上并行。
算子实现层(Implementation)
实现层是最终的Ascend C代码,由catlass的模板自动生成。用户不需要直接编辑实现层的代码——它是模板展开的结果。
实现层的代码包括:kernel函数(真正在NPU上执行的函数)、tiling函数(计算数据分块的函数)、注册代码(把算子注册到AOL)。这些代码都是catlass根据用户选择的模板参数自动生成的,总量约1500-2500行。
仓库结构
catlass的代码结构按抽象层次和功能模块组织:
catlass/ ├── CMakeLists.txt ├── README.md ├── include/ │ └── catlass/ │ ├── gemm/ # GEMM算子模板 │ │ ├── gemm.h # 顶层GEMM模板类 │ │ ├── gemm_config.h # 配置参数定义 │ │ ├── gemm_policy.h # 策略参数定义 │ │ ├── prologue/ # Prologue组件 │ │ │ ├── prologue.h │ │ │ └── prologue_default.h │ │ ├── mainloop/ # Mainloop组件 │ │ │ ├── mainloop.h │ │ │ ├── mainloop_pipelined.h # 流水线化Mainloop │ │ │ └── mainloop_simple.h # 简单Mainloop │ │ └── epilogue/ # Epilogue组件 │ │ ├── epilogue.h │ │ ├── epilogue_bias_relu.h # BiasAdd+ReLU后处理 │ │ ├── epilogue_bias_gelu.h # BiasAdd+GELU后处理 │ │ └── epilogue_scale.h # 缩放后处理 │ ├── conv/ # Conv2D算子模板(开发中) │ └── common/ # 公共工具 │ ├── tile_size.h # TileSize模板 │ ├── layout.h # 内存布局定义 │ └── data_type.h # 数据类型定义 ├── recipes/ # 预设配方 │ ├── gemm_f16_f16.yaml │ ├── gemm_f32_f32.yaml │ ├── gemm_bias_relu.yaml │ └── gemm_bias_gelu.yaml ├── tools/ │ ├── generate.py # 代码生成器 │ └── auto_tune.py # 自动调优工具 ├── tests/ │ ├── accuracy/ # 精度测试 │ └── performance/ # 性能基准测试 └── docs/ ├── tutorial.md # 快速上手教程 └── api_reference.md # API参考recipes目录是catlass最常用的入口——它提供了一组预设的YAML配置文件,覆盖了最常见的GEMM场景。用户可以直接使用预设配方,也可以基于预设配方修改。
tools目录包含了两个重要工具:generate.py是代码生成器,根据YAML配置生成算子代码;auto_tune.py是自动调优工具,搜索最优的策略参数。
与opbase的依赖关系
catlass依赖opbase,但依赖程度比其他ops-*仓库轻。catlass只用到了opbase的两个模块:算子注册和内存管理。
算子注册方面,catlass生成的算子通过opbase的REGISTER_OP宏注册到AOL。注册信息包括算子名称、输入输出规格、支持的数据类型、优先级等。这些信息让AOL的算子选择器能发现和调用catlass生成的算子。
内存管理方面,catlass生成的算子使用opbase的WorkspaceAllocator来管理临时缓冲区。GEMM算子需要临时缓冲区来存储tiling参数、DMA描述符等元数据,这些缓冲区通过opbase的workspace机制分配。
catlass不使用opbase的Tiling框架——因为catlass有自己更精细的tiling策略(多级tile、双缓冲、流水线),opbase的标准Tiling接口无法表达这些复杂性。catlass生成的算子自带tiling代码,直接和NPU硬件交互。
性能特征
catlass生成的GEMM算子在Ascend 910上的性能特征如下:
| GEMM配置 | 数据类型 | 矩阵大小 | catlass TFLOPS | ops-blas TFLOPS | 理论峰值比例 |
|---|---|---|---|---|---|
| 标准GEMM | FP16 | 4096×4096 | 256 | 278 | 100% / 109% |
| 标准GEMM | BF16 | 4096×4096 | 245 | N/A | 96% |
| GEMM+Bias+ReLU | FP16 | 4096×4096 | 248 | N/A | 97% |
| GEMM+Bias+GELU | FP16 | 4096×4096 | 242 | N/A | 95% |
| 标准GEMM | INT8 | 8192×8192 | 480 | N/A | 94% |
数据表明,catlass在标准GEMM上达到了理论峰值的100%,比ops-blas的109%略低。但catlass支持的GEMM变体远多于ops-blas——融合后处理的GEMM、BF16/INT8的GEMM等,这些都是ops-blas不提供的。catlass的性能目标是"够用"——达到峰值的90-100%即可,不需要像ops-blas那样追到109%。
效率对比:使用catlass前 vs 使用catlass后
| 指标 | 使用前(手写Ascend C算子) | 使用后(catlass模板生成) | 提升 |
|---|---|---|---|
| 新GEMM变体开发时间 | 3-5天 | 10分钟-2小时 | 20-50x |
| 代码行数 | 1500-2500行 | 30-80行配置 | 97%↓ |
| 首次提交边界bug率 | ~30% | 0% | 消除 |
| 标准GEMM性能 | 基线 | 等效(100%峰值) | 持平 |
| 融合GEMM性能 | 比分步快10-15% | 比分步快25-35% | +15-20% |
| 数据类型扩展 | 需完全重写 | 改1行配置 | 质的改善 |
| 新硬件适配 | 需手动调优 | 更新预设策略+auto_tune | 5x |
融合GEMM的性能优势来自catlass的Cube/Vector流水线化——后处理(Vector单元)和下一个tile的矩阵乘法(Cube单元)并行执行,后处理的额外开销被几乎完全掩盖。手写算子如果不做这种流水线化,后处理需要等矩阵乘法完成后再执行,额外开销约10-15%。
catlass的局限
catlass不是万能的,有几个明显的局限:
只支持GEMM和Conv2D:catlass目前只支持矩阵乘法类算子的模板生成。对于注意力机制、归一化、激活等非矩阵乘法类算子,catlass帮不了你。这类算子还是需要用Ascend C手写,或者使用ops-nn、ops-transformer等仓库的现成算子。
模板编译时间:catlass大量使用C++模板元编程,编译一个GEMM变体需要30-60秒。如果需要编译多个变体,等待时间会累积。catlass提供了预编译头文件和增量编译支持,但编译时间仍然是手写算子的2-3倍。
模板错误信息:C++模板的错误信息以"难以阅读"著称。如果模板参数组合不合法(比如L1 tile太大放不进缓冲区),编译器会吐出一大段模板展开的堆栈信息,定位问题需要经验。catlass在CANN 8.5中增加了静态断言(static_assert),在模板参数校验失败时给出更友好的错误提示。
灵活性有限:catlass的四层抽象提供了足够的灵活性来覆盖大多数GEMM变体,但对于特别复杂的场景(如GEMM结果需要和非矩阵数据做交叉计算),catlass的模板可能不够灵活,需要回退到手写Ascend C。
这些局限不意味着catlass不好用——对于它设计的场景(GEMM及融合GEMM变体),catlass是目前最高效的开发方式。局限只是说明catlass不是"算子开发的万能工具",而是"特定场景的专业工具"。
与ATB的关系
catlass和ATB都是CANN的加速库,但服务于不同的层次。catlass是算子生成工具——它帮你写算子。ATB是推理加速框架——它帮你优化整条推理链路。
两者的协作方式是:ATB在构建Transformer推理执行图时,会使用catlass生成的GEMM组件作为底层计算引擎。ATB负责高层的图优化(算子调度、内存规划、通信优化),catlass负责底层的算子生成(GEMM及其变体的高效实现)。
这种分层设计让ATB不需要自己实现GEMM——它只需要知道catlass的GEMM组件的接口,就能在执行图中插入GEMM操作。当GEMM的配置需求变化时(比如从FP16换成BF16),ATB不需要做任何修改——catlass的模板自动适配新配置。
实际使用场景
catlass的典型使用场景包括:
自定义融合GEMM:推理中常见的GEMM+Bias+ReLU、GEMM+Scale+Shift等融合模式,catlass可以快速生成。比手写快20-50倍,性能几乎不打折。
新数据类型支持:当CANN支持新的数据类型(如FP8、INT4)时,catlass可以通过模板参数化快速生成对应数据类型的GEMM算子,不需要从零开始写。
硬件适配:当新的昇腾NPU型号发布时(如新的达芬奇架构微架构),catlass只需要更新预设策略和auto_tune的搜索空间,就能快速适配新硬件。手写算子则需要逐个调优。
研究和实验:如果你在研究新的GEMM算法或优化策略,catlass提供了一个标准化的实验框架——你只需要替换Mainloop组件的实现,其他部分(数据搬运、后处理、注册)都不用动。
仓库链接:https://atomgit.com/cann/catlass
