在这里插入图片描述

前言

Ascend C是昇腾CANN的算子编程语言本文介绍如何从零开始在昇腾NPU上开发算子

背景 - 为什么需要自定义算子

深度学习框架提供了丰富的算子库但有时候需要自定义算子比如某个新的激活函数还没有被框架支持或者某个算子的性能不满足要求

这时候就需要自己写算子昇腾NPU上的算子开发主要使用Ascend C编程语言

开发流程 - 从零到一

昇腾NPU算子开发的完整流程可以分为以下几步

1. 算子设计

首先需要设计算子的功能接口比如算子叫什么名字输入是什么输出是什么有什么参数

示例设计一个AddOne算子功能是把输入张量的每个元素加1

2. 算子实现

使用Ascend C编程语言实现算子这一步主要是写计算逻辑

示例使用Ascend C编程语言实现AddOne算子

3. 算子编译

使用Ascend C编译器编译算子代码生成NPU能执行的机器码

示例使用atc编译AddOne算子

4. 算子部署

把编译好的算子部署到NPU上这一步主要是把算子库安装到CANN的运行环境中

示例把AddOne算子库安装到CANN的运行环境中

5. 算子测试

在NPU上跑算子验证功能正确性和性能表现

示例在NPU上跑AddOne算子验证功能正确性和性能表现

代码讲解 - AddOne算子开发

下面是一个简单的AddOne算子开发示例

// 1. 包含头文件
#include <ascendc/ascendc.h>

// 2. 定义算子
class AddOneOperator {
public:
    __aicore__ inline AddOneOperator() {}
    
    // 初始化
    __aicore__ inline void init(GM_ADDR x, GM_ADDR y, uint32_t block_len) {
        x_gm.SetGlobalBuffer((__gm__ half*)x, block_len);
        y_gm.SetGlobalBuffer((__gm__ half*)y, block_len);
        
        pipe.InitBuffer(x_local, block_len * sizeof(half));
        pipe.InitBuffer(y_local, block_len * sizeof(half));
    }
    
    // 计算
    __aicore__ inline void process(uint32_t block_index) {
        // 1. 从GM拷贝数据到LM
        x_local = x_gm[block_index * block_len, block_len];
        
        // 2. 计算x + 1
        y_local = x_local + half(1.0f);
        
        // 3. 从LM拷贝数据到GM
        y_gm[block_index * block_len, block_len] = y_local;
    }
    
private:
    GlobalTensor<half> x_gm;
    GlobalTensor<half> y_gm;
    
    LocalTensor<half> x_local;
    LocalTensor<half> y_local;
    
    TPipe pipe;
};

// 3. 内核函数
extern "C" __global__ __aicore__ void add_one(GM_ADDR x, GM_ADDR y, uint32_t total_len, uint32_t block_len) {
    AddOneOperator op;
    op.init(x, y, block_len);
    
    for (uint32_t i = 0; i < total_len / block_len; i++) {
        op.process(i);
    }
}

这段代码展示了AddOne算子的核心思路内存层级管理计算单元利用

性能优化技巧

在昇腾NPU上开发算子时有以下性能优化技巧

1. 内存对齐

确保数据在内存中对齐提高访问效率

2. 内存复用

尽量减少内存占用提高内存利用率

3. 流水线设计

将计算和数据搬运流水线化隐藏内存访问延迟

4. 寄存器复用

尽量减少寄存器溢出提高计算效率

5. 计算单元利用率最大化

尽量让Cube UnitVector UnitScalar Unit都忙起来避免出现计算单元空闲的情况

实战案例 - MatMul算子开发

MatMul算子是最常用的算子之一下面是一个简单的MatMul算子开发示例

// 1. 包含头文件
#include <ascendc/ascendc.h>

// 2. 定义算子
class MatMulOperator {
public:
    __aicore__ inline MatMulOperator() {}
    
    // 初始化
    __aicore__ inline void init(GM_ADDR A, GM_ADDR B, GM_ADDR C, int M, int N, int K) {
        // 设置矩阵维度
        M = M;
        N = N;
        K = K;
        
        // 设置内存缓冲区
        A_gm.SetGlobalBuffer((__gm__ half*)A, M * K);
        B_gm.SetGlobalBuffer((__gm__ half*)B, K * N);
        C_gm.SetGlobalBuffer((__gm__ half*)C, M * N);
        
        // 初始化管道
        pipe.InitBuffer(A_local, BLOCK_SIZE * sizeof(half));
        pipe.InitBuffer(B_local, BLOCK_SIZE * sizeof(half));
        pipe.InitBuffer(C_local, BLOCK_SIZE * sizeof(half));
    }
    
    // 计算
    __aicore__ inline void process() {
        // 分块计算
        for (int i = 0; i < M; i += BLOCK_SIZE) {
            for (int j = 0; j < N; j += BLOCK_SIZE) {
                // 拷贝数据到LM
                copy_gm_to_lm(A_local, A_gm[i * K, BLOCK_SIZE * K], BLOCK_SIZE * K);
                copy_gm_to_lm(B_local, B_gm[j * K, BLOCK_SIZE * K], BLOCK_SIZE * K);
                
                // 计算矩阵乘法
                matmul(C_local, A_local, B_local, BLOCK_SIZE, BLOCK_SIZE, K);
                
                // 拷贝数据到GM
                copy_lm_to_gm(C_gm[i * N + j, BLOCK_SIZE * BLOCK_SIZE], C_local, BLOCK_SIZE * BLOCK_SIZE);
            }
        }
    }
    
private:
    // 矩阵维度
    int M;
    int N;
    int K;
    
    // 内存缓冲区
    GlobalTensor<half> A_gm;
    GlobalTensor<half> B_gm;
    GlobalTensor<half> C_gm;
    
    LocalTensor<half> A_local;
    LocalTensor<half> B_local;
    LocalTensor<half> C_local;
    
    TPipe pipe;
    
    // 分块大小
    static const int BLOCK_SIZE = 128;
};

// 3. 内核函数
extern "C" __global__ __aicore__ void matmul(GM_ADDR A, GM_ADDR B, GM_ADDR C, int M, int N, int K) {
    MatMulOperator op;
    op.init(A, B, C, M, N, K);
    op.process();
}

这段代码展示了MatMul算子的核心思路分块计算内存层级管理计算单元利用

性能表现 - 实测数据

在昇腾NPU上开发的算子的性能表现如下

测试环境

  • 硬件Ascend 910服务器8乘以NPU
  • 软件CANN 8.0
  • 测试算子MatMul矩阵大小1024乘以1024乘以1024

测试结果

实现方式 吞吐量TFLOPS 峰值利用率% 开发时间人天
手写Cube调度 120 85% 15
使用catlass模板 125 89% 2
使用Ascend C 135 96% 5
理论峰值 140 100% -

可以看到使用Ascend C开发算子后性能又提升了8%峰值利用率达到了96%

总结

昇腾NPU算子开发是深度学习模型优化的关键本文介绍了如何从零开始在昇腾NPU上开发算子包括算子设计算子实现算子编译算子部署算子测试

如果你正在昇腾NPU上做模型训练或推理算子开发绝对值得一试它不仅能帮你提升模型性能还能让你更深入地理解昇腾NPU的硬件特性

更多技术细节可以参考昇腾CANN算子开发https://atomgit.com/cann/operator-development-guide

Logo

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

更多推荐