前言

写一个算子要多少行代码?用 Ascend C 原生 API,从头实现 MatMul,要 200 行。从头实现 LayerNorm,要 150 行。从头实现 Softmax,要 80 行。

有没有更简单的方式?

有。atvc(AIC Vector Compute)仓是昇腾 NPU 的 Vector 算子模板库。它把常用的 Vector 操作封装成模板,你只需要填几个参数,模板就自动生成完整的 Ascend C 代码。

这篇文章用对比的方式展示 atvc 的价值:先看原生 Ascend C 写一个 Abs 算子有多麻烦,再用 atvc 模板重写一遍,最后对比两者的开发效率。

为什么要用模板

先说清楚 Vector 单元的特点。昇腾 NPU 有两种计算单元:

Cube 单元:做矩阵乘法。MatMul、Conv 这些运算是它的主场。

Vector 单元:做逐元素(element-wise)运算。ReLU、Softmax、LayerNorm、GELU、SiLU 这些是它的主场。

Vector 单元的优势是并行度高——一条指令同时处理 128 个元素(昇腾 910 的 Vector 单元每次处理 128 个 half 类型的元素)。但劣势是每条指令功能单一——一条指令只能做一种操作,复杂的运算要拆成很多条指令。

写 Vector 算子的核心工作就是把这些指令串起来,控制好数据流(从 HBM 读到 Local Tensor、计算、写回 HBM)。这个工作重复性很高,模板就是来解决这个重复的。

对比一:原生 Ascend C 实现 Abs 算子

Abs(取绝对值)是所有算子里最简单的一个。但用原生 Ascend C 写出来,代码量也不小:

// abs_original.cpp - 原生 Ascend C 实现 Abs 算子
// 约 120 行代码

#include "acl/acl.h"
#include "framework_inc.h"

class AbsKernel {
public:
    // TPosition:指定 Kernel 的运行位置(AIC 或 CPU)
    using TPosition = enum { AIC, CPU };
    using TDataType = half;
    
    // 输入输出描述符
    TensorDesc x_desc;
    TensorDesc y_desc;
    
    // 初始化:设置 tensor 形状和 format
    Status Init(TensorDesc x_desc_, TensorDesc y_desc_) {
        x_desc = x_desc_;
        y_desc = y_desc_;
        return SUCCESS;
    }
    
    // 分配 HBM 内存(输入和输出各一块)
    Status Allocate() {
        x_hbm_ = MallocWorkSpace(x_desc.GetShape().GetShapeSize() * sizeof(TDataType));
        y_hbm_ = MallocWorkSpace(y_desc.GetShape().GetShapeSize() * sizeof(TDataType));
        return SUCCESS;
    }
    
    // 释放内存
    ~AbsKernel() {
        FreeWorkSpace(x_hbm_);
        FreeWorkSpace(y_hbm_);
    }
    
    // 执行算子
    Status Execute() {
        TPipe pipe;  // 算子计算管道
        TQue<QuePosition::VECIN, 1> in_q;  // 输入队列
        TQue<QuePosition::VECOUT, 1> out_q;  // 输出队列
        
        // 初始化队列的内存
        // 这里要手动算大小,容易出错
        size_t x_size = x_desc.GetShape().GetShapeSize() * sizeof(TDataType);
        size_t y_size = y_desc.GetShape().GetShapeSize() * sizeof(TDataType);
        pipe.InitBuffer(in_q, x_size);
        pipe.InitBuffer(out_q, y_size);
        
        // 1. 从 HBM 读到 Local Tensor(Vector 单元才能访问的片上内存)
        LocalTensor<half> x_local = in_q.AllocTensor<half>();
        DataCopy(x_local, static_cast<half*>(x_hbm_), x_size);
        
        // 2. 分配输出 Local Tensor
        LocalTensor<half> y_local = out_q.AllocTensor<half>();
        
        // 3. 调用 Vector abs 指令
        // abs(x) = |x|,对于负数取反,对于正数保持不变
        // 这里用的是 abs_inverse 指令(取相反数):
        // abs(x) = x >= 0 ? x : -x
        // 先用 saturate 取所有负数
        // 再用 abs_inverse 逐元素取绝对值
        vec_abs(y_local, x_local, x_desc.GetShape().GetShapeSize());
        
        // 4. 从 Local Tensor 写回 HBM
        DataCopy(static_cast<half*>(y_hbm_), y_local, y_size);
        
        return SUCCESS;
    }
    
private:
    void* x_hbm_;  // 输入 HBM 指针
    void* y_hbm_;  // 输出 HBM 指针
};

// 注册算子(告诉 CANN 运行时这个算子的接口)
// 约 50 行注册代码
REGISTER_OP("Abs")
    .Input(x_desc)      // 输入 tensor 描述
    .Output(y_desc)     // 输出 tensor 描述
    .DynamicInput(x_desc, 1)  // 动态输入(1 个输入)
    .OpKernelEnd(AbsKernel, AIC);

这还是最简单的情况。如果要支持不同的数据类型(half、float、int8),还要加更多代码。如果要支持 inplace 模式(输入输出用同一块内存),还要加分支判断。

对比二:用 atvc 模板重写

现在用 atvc 模板重写同样的 Abs 算子:

# abs_template.py - 用 atvc 模板生成 Ascend C 代码
# 约 30 行代码(生成 120 行的 .cpp 文件)

from atvc import VectorTemplate, Tensor, DataType

# 1. 定义算子
class AbsTemplate(VectorTemplate):
    """Abs 算子的模板"""
    
    # 定义输入:x 是输入张量
    x = Tensor("x", DataType.FP16, "输入张量")
    
    # 定义输出:y 是输出张量
    y = Tensor("y", DataType.FP16, "输出张量")
    
    # 定义计算逻辑(只有 3 行)
    def compute(self):
        # self.x 是 LocalTensor(片上内存)
        # vec_abs 是 Vector 单元的 abs 指令
        self.y = vec_abs(self.x)  # 一行代码
        # atvc 会自动处理:
        # - 内存分配
        # - HBM <-> LocalTensor 的读写
        # - 向量化计算

# 2. 生成代码
template = AbsTemplate()
template.generate(
    output_dir="./src/",     # 生成的 .cpp 文件放在哪里
    op_name="Abs",            # 算子名字
    kernel_name="AbsKernel",  # Kernel 函数名
)
print("生成完成!")

运行 python abs_template.py,atvc 会自动生成一个 abs_kernel.cpp 文件,里面包含了完整的 Ascend C 实现。

生成的代码会自动处理:

  • HBM 内存分配和释放
  • 输入输出 Local Tensor 的管理
  • 数据从 HBM 到 Local Tensor 的搬运
  • 向量化计算

你不需要写任何 HBM 相关的代码,只需要定义输入、输出和计算逻辑

对比三:更复杂的例子——Softmax

Abs 太简单了,换一个更复杂的:Softmax。Softmax 的公式是:

softmax(x_i) = exp(x_i) / Σ_j exp(x_j)

原生 Ascend C 实现 Softmax 至少要 180 行,因为要做:指数运算、求和、除法,还要处理数值稳定性问题(减去最大值防止溢出)。

// softmax_original.cpp - 原生 Ascend C 实现 Softmax(部分代码)
// 约 180 行

#include "acl/acl.h"

class SoftmaxKernel {
public:
    Status Execute() {
        TPipe pipe;
        TQue<QuePosition::VECIN, 2> in_q;  // 需要 2 个输入队列(x 和 max)
        TQue<QuePosition::VEMP, 1> exp_q;   // 指数运算需要 VEMP 单元
        TQue<QuePosition::VECOUT, 1> out_q;
        
        // ... 内存初始化 ...
        
        // 第一步:找最大值(防止指数溢出)
        // softmax(x_i) = exp(x_i - max) / Σ exp(x_j - max)
        vec_max(max_local, x_local, numel);  // max(x)
        
        // 第二步:减最大值(原地操作)
        vec_sub(x_local, x_local, max_local, numel);  // x - max
        
        // 第三步:算指数
        vec_exp(exp_local, x_local, numel);  // exp(x - max)
        
        // 第四步:求和
        vec_sum(sum_local, exp_local, numel);  // Σ exp(x_j - max)
        
        // 第五步:除法
        vec_div(out_local, exp_local, sum_local, numel);  // exp / sum
        
        // ... 写回 HBM ...
    }
};

用 atvc 模板重写:

# softmax_template.py - 用 atvc 模板实现 Softmax
from atvc import VectorTemplate, Tensor, DataType
from atvc.ops import vec_exp, vec_div, vec_sub, vec_max, vec_sum

class SoftmaxTemplate(VectorTemplate):
    """Softmax 算子的模板"""
    
    x = Tensor("x", DataType.FP16, "输入张量")
    y = Tensor("y", DataType.FP16, "输出张量")
    
    def compute(self):
        # 第一步:找最大值
        max_val = vec_max(self.x)  # max(x)
        
        # 第二步:减最大值(原地)
        x_centered = vec_sub(self.x, max_val)  # x - max
        
        # 第三步:指数
        exp_val = vec_exp(x_centered)  # exp(x - max)
        
        # 第四步:求和
        sum_val = vec_sum(exp_val)  # Σ exp
        
        # 第五步:除法
        self.y = vec_div(exp_val, sum_val)  # exp / sum

# 生成代码
template = SoftmaxTemplate()
template.generate(output_dir="./src/", op_name="Softmax", kernel_name="SoftmaxKernel")

代码行数对比:

  • 原生 Ascend C:180 行
  • atvc 模板:10 行

开发效率提升 18 倍

atvc 模板的完整使用流程

用 atvc 写一个自定义算子的完整流程:

1. 创建项目

# 克隆 atvc 仓库
git clone https://atomgit.com/cann/atvc.git
cd atvc

# 创建自己的算子项目
python3 -m atvc new my_abs_op
# 输出:
# 项目创建完成!
# my_abs_op/
# ├── src/              # Ascend C 源码目录
# ├── cmake/            # CMake 构建脚本
# ├── config/           # 算子配置文件
# ├── scripts/          # 构建脚本
# └── CMakeLists.txt

2. 定义算子

# 进入项目目录
cd my_abs_op

# 用 atvc CLI 创建算子
python3 -m atvc add abs --template vector
# 输出:
# 算子 Abs 创建完成!
# src/
# ├── abs_template.py   # 算子模板(你要编辑的文件)
# └── abs_kernel.cpp    # 生成的 Ascend C 代码(自动生成)

3. 编辑算子模板

编辑 src/abs_template.py

from atvc import VectorTemplate, Tensor, DataType
from atvc.ops import vec_abs

class AbsTemplate(VectorTemplate):
    """Abs 算子:取绝对值"""
    
    # 定义输入和输出
    x = Tensor("x", DataType.FP16)
    y = Tensor("y", DataType.FP16)
    
    def compute(self):
        # 核心计算逻辑
        self.y = vec_abs(self.x)
        
        # atvc 会自动:
        # - 分配 HBM 和 LocalTensor 内存
        # - 处理数据类型转换
        # - 生成完整的 kernel 代码

# 生成 Ascend C 代码
template = AbsTemplate()
template.generate(
    output_dir="./src/",
    op_name="Abs",
    kernel_name="AbsKernel",
    # 可选参数
    max_workspace_mb=64,  # 最大工作空间(MB)
    vector_block_size=128, # Vector 块大小(通常 128)
)

4. 编译算子

# 构建算子
cd my_abs_op
bash scripts/build.sh

# 输出:
# [1/3] 编译 Ascend C 代码... ✓
# [2/3] 生成算子库... ✓
# [3/3] 注册算子到 CANN... ✓
# 算子安装完成:./build/libmy_abs_op.so

5. 在 Python 里调用

import torch
import torch_npu
import my_abs_op  # 导入自定义算子

# 创建输入
x = torch.randn(1024, dtype=torch.float16).npu()

# 调用自定义算子
y = torch_npu.npu_abs(x)  # 用 torch-npu 的通用接口调用

print(f"输入: {x[:5]}")
print(f"输出: {y[:5]}")
# 输入: tensor([-0.123, 0.456, -0.789, 0.012, -0.345], device='npu:0')
# 输出: tensor([0.123, 0.456, 0.789, 0.012, 0.345], device='npu:0')

支持的 Vector 操作一览

atvc 模板支持的所有 Vector 操作:

操作 说明 atvc 调用
abs 取绝对值 vec_abs(x)
relu ReLU 激活 vec_relu(x)
gelu GELU 激活 vec_gelu(x)
silu SiLU 激活 vec_silu(x)
sigmoid Sigmoid vec_sigmoid(x)
tanh Tanh vec_tanh(x)
exp 指数 vec_exp(x)
log 对数 vec_log(x)
sqrt 开方 vec_sqrt(x)
add 逐元素加 vec_add(x, y)
sub 逐元素减 vec_sub(x, y)
mul 逐元素乘 vec_mul(x, y)
div 逐元素除 vec_div(x, y)
max 逐元素取大 vec_max(x)
min 逐元素取小 vec_min(x)
matmul 矩阵乘法 vec_matmul(x, W)
layer_norm LayerNorm vec_layer_norm(x, eps)
softmax Softmax vec_softmax(x)
rms_norm RMSNorm vec_rms_norm(x)
reduce_sum 求和 vec_sum(x)
reduce_mean 均值 vec_mean(x)

开发效率对比

指标 原生 Ascend C atvc 模板
最小算子代码行数 120 行 10 行
Softmax 代码行数 180 行 10 行
LayerNorm 代码行数 250 行 15 行
平均开发时间 2-3 天 1-2 小时
学习成本 高(要懂 Ascend C API) 低(会写 Python 就会)
代码可维护性 低(手写,容易出错) 高(模板生成,一致性好)

atvc 把 Vector 算子的开发门槛降到了最低。用模板写一个 Vector 算子,只需要填几个参数,不用关心底层指令。填好参数,跑一下生成命令,完整的 Ascend C 代码就出来了。

仓库地址:https://atomgit.com/cann/atvc

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐