昇腾CANN atvc 仓:Vector 算子模板库,5 行代码写一个算子
摘要: 本文对比了使用原生Ascend C和atvc模板库开发NPU算子的效率差异。以Abs算子为例,原生实现需要约120行代码处理内存分配、数据搬运等底层细节,而atvc模板仅需30行Python代码即可自动生成等效实现。对于更复杂的Softmax算子,原生实现需要180行代码处理指数运算、求和等操作,而atvc模板能自动生成完整实现。atvc通过封装常用Vector操作模板,显著降低了算子开发
前言
写一个算子要多少行代码?用 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
更多推荐




所有评论(0)