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

Logo

鲲鹏昇腾开发者社区是面向全社会开放的“联接全球计算开发者,聚合华为+生态”的社区,内容涵盖鲲鹏、昇腾资源,帮助开发者快速获取所需的知识、经验、软件、工具、算力,支撑开发者易学、好用、成功,成为核心开发者。

更多推荐