news 2026/5/25 8:00:02

Ascend C 算子开发实战:从零写一个矩阵乘法

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Ascend C 算子开发实战:从零写一个矩阵乘法

前言

CANN 自带的算子库已经覆盖了大部分常用算子,但总会遇到不支持的。这时候要用 Ascend C 自己写一个算子。

Ascend C 是昇腾的算子编程语言,语法类似 C++,但专门针对达芬奇架构做了优化。这篇文章讲怎么从零写一个矩阵乘法算子。


开发环境准备

安装算子开发包

# CANN 工具包自带算子开发工具whichcanndev# 输出:/usr/local/Ascend/ascend-toolkit/latest/bin/canndev# 如果没有,安装 toolkitaptinstallascend-toolkit

创建算子工程

# 用 canndev 创建算子工程canndev create--project=matmul_custom--output=./workspace# 工程结构tree ./workspace/matmul_custom/# output:# matmul_custom/# ├── CMakeLists.txt# ├── framework/# │ └── tensor_add_impl.cpp # 算子实现# ├── op_proto/# │ └── tensor_add.h # 算子原型# ├── unittest/# │ └── test_tensor_add.py # 单元测试# └── CMakeLists.txt

算子原型定义

第一步是定义算子的原型:输入、输出、属性。

头文件定义

// matmul_custom.h#ifndefMATMUL_CUSTOM_H#defineMATMUL_CUSTOM_H#include"tiking_pub_api.h"namespaceacl{namespaceops{classMatMulCustom{public:MatMulCustom()=default;~MatMulCustom()=default;// 初始化算子graphStatusInit(constTensor&a,constTensor&b,Tensor&c,booltrans_a=false,booltrans_b=false);// 推理接口graphStatusInferShape();// 数据类型推断graphStatusInferDataType();};}// namespace ops}// namespace acl#endif// MATMUL_CUSTOM_H

实现文件

// matmul_custom.cpp#include"matmul_custom.h"namespaceacl{namespaceops{graphStatusMatMulCustom::Init(constTensor&a,constTensor&b,Tensor&c,booltrans_a,booltrans_b){// 设置输入(void)ge::Node::GetNodeFromTensor(a);(void)ge::Node::GetNodeFromTensor(b);// 设置属性SetAttr("trans_a",trans_a);SetAttr("trans_b",trans_b);returnGRAPH_SUCCESS;}graphStatusMatMulCustom::InferShape(){// 获取输入 shapestd::vector<int64_t>shape_a;std::vector<int64_t>shape_b;GetInputDesc(0).GetShape(shape_a);GetInputDesc(1).GetShape(shape_b);// 计算输出 shapebooltrans_a=GetAttr("trans_a")->GetBool();booltrans_b=GetAttr("trans_b")->GetBool();int64_tm=trans_a?shape_a[1]:shape_a[0];int64_tn=trans_b?shape_b[0]:shape_b[1];std::vector<int64_t>shape_c={m,n};GetOutputDesc(0).SetShape(shape_c);returnGRAPH_SUCCESS;}graphStatusMatMulCustom::InferDataType(){// 输出数据类型和输入 A 一致DataType dtype=GetInputDesc(0).GetDataType();GetOutputDesc(0).SetDataType(dtype);returnGRAPH_SUCCESS;}}// namespace ops}// namespace acl

算子实现(核心)

这是最关键的部分:用 Ascend C 实现矩阵乘法。

基本框架

#include"kernel_operator.h"classMatMulCustomKernel{public:__aicore__inlineMatMulCustomKernel(){}__aicore__voidInit(GM_ADDR a,GM_ADDR b,GM_ADDR c,int32_tM,int32_tN,int32_tK);__aicore__voidProcess();private:GlobalTensor<float>aGm;// 输入 A(GM 内存)GlobalTensor<float>bGm;// 输入 B(GM 内存)GlobalTensor<float>cGm;// 输出 C(GM 内存)LocalTensor<float>aLocal;// 输入 A(UB 内存)LocalTensor<float>bLocal;// 输入 B(UB 内存)LocalTensor<float>cLocal;// 输出 C(UB 内存)int32_tM,N,K;};extern"C"__global__voidmatmul_custom(GM_ADDR a,GM_ADDR b,GM_ADDR c,int32_tM,int32_tN,int32_tK){MatMulCustomKernel op;op.Init(a,b,c,M,N,K);op.Process();}

Init 函数

__aicore__voidMatMulCustomKernel::Init(GM_ADDR a,GM_ADDR b,GM_ADDR c,int32_tM,int32_tN,int32_tK){// 设置 GM 地址aGm.SetGlobalBuffer((__gm__float*)a);bGm.SetGlobalBuffer((__gm__float*)b);cGm.SetGlobalBuffer((__gm__float*)c);// 保存参数this->M=M;this->N=N;this->K=K;// 分配 UB 空间constexprint32_tBLOCK_SIZE=32;// 一个 block 的大小aLocal=GetUbBlock<float>(M*K/BLOCK_SIZE);bLocal=GetUbBlock<float>(K*N/BLOCK_SIZE);cLocal=GetUbBlock<float>(M*N/BLOCK_SIZE);}

Process 函数(核心计算)

__aicore__voidMatMulCustomKernel::Process(){// 1. 把数据从 GM 搬到 UBCopyIn(aGm,aLocal,M*K);CopyIn(bGm,bLocal,K*N);// 2. 矩阵乘法计算MatMul(cLocal,aLocal,bLocal,M,N,K);// 3. 把结果从 UB 写回 GMCopyOut(cLocal,cGm,M*N);}// 矩阵乘法实现voidMatMul(LocalTensor<float>&c,LocalTensor<float>&a,LocalTensor<float>&b,int32_tM,int32_tN,int32_tK){// 简化版本:逐行逐列计算for(int32_ti=0;i<M;i++){for(int32_tj=0;j<N;j++){floatsum=0.0f;for(int32_tk=0;k<K;k++){sum+=a[i*K+k]*b[k*N+j];}c[i*N+j]=sum;}}}

优化版本(使用 Cube Unit)

#include"lib_api.h"// 使用 Cube Unit 做矩阵乘法voidMatMulCube(LocalTensor<float>&c,LocalTensor<float>&a,LocalTensor<float>&b,int32_tM,int32_tN,int32_tK){// 调用 Cube Unit 的 MatMul 接口matmul_t matmul_para;matmul_para.M=M;matmul_para.N=N;matmul_para.K=K;matmul_para.is_trans_a=false;matmul_para.is_trans_b=false;// 用 Cube Unit 计算MatMul(c,a,b,matmul_para);}

编译算子

CMake 配置

# CMakeLists.txt cmake_minimum_required(VERSION 3.10) project(matmul_custom) # 设置 Ascend C 编译选项 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -march=davinci") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -O2") # 添加算子实现文件 add_library(matmul_custom SHARED matmul_custom.cpp matmul_custom_kernel.cpp ) # 链接 Ascend C 库 target_link_libraries(matmul_custom ascendcl tbe_operator )

编译命令

# 创建编译目录mkdirbuild&&cdbuild# 配置cmake..# 编译make-j8# 输出:libmatmul_custom.so

在 PyTorch 里调用

注册自定义算子

importtorchimporttorch.npuimportctypes# 加载自定义算子库lib=ctypes.CDLL("./libmatmul_custom.so")# 定义 Python 接口defmatmul_custom(a:torch.Tensor,b:torch.Tensor)->torch.Tensor:"""自定义矩阵乘法"""# 检查输入asserta.dim()==2andb.dim()==2asserta.shape[1]==b.shape[0]M,K=a.shape K2,N=b.shape# 创建输出 tensorc=torch.empty(M,N,device='npu:0',dtype=torch.float32)# 调用自定义算子lib.matmul_custom(a.data_ptr(),b.data_ptr(),c.data_ptr(),M,N,K)returnc

测试自定义算子

# 创建测试数据a=torch.randn(128,256,device='npu:0')b=torch.randn(256,64,device='npu:0')# 用自定义算子计算c_custom=matmul_custom(a,b)# 用 PyTorch 内置算子计算(参考)c_ref=torch.matmul(a,b)# 对比精度cosine_sim=torch.nn.functional.cosine_similarity(c_custom.flatten(),c_ref.flatten(),dim=0)print(f"余弦相似度:{cosine_sim.item():.6f}")# 对比性能importtime# 预热for_inrange(10):matmul_custom(a,b)torch.npu.synchronize()start=time.time()for_inrange(100):matmul_custom(a,b)torch.npu.synchronize()end=time.time()print(f"自定义算子延迟:{(end-start)/100*1000:.2f}ms")

性能优化

使用 Cube Unit

达芬奇架构的 Cube Unit 是专门为矩阵乘法设计的硬件单元,比用 Vector Unit 算快得多。

// 使用 Cube Unit#include"lib_api.h"voidMatMulOptimized(LocalTensor<float>&c,LocalTensor<float>&a,LocalTensor<float>&b,int32_tM,int32_tN,int32_tK){// 检查是否可以用 Cubeif(M>=16&&N>=16&&K>=16){// 用 Cube Unitmatmul_t params;params.M=M;params.N=N;params.K=K;MatMul(c,a,b,params);}else{// 用小矩阵算法MatMulSmall(c,a,b,M,N,K);}}

分块计算

当矩阵太大,UB 放不下时,要分块计算。

constexprint32_tTILE_SIZE=32;// 块大小voidMatMulTiled(LocalTensor<float>&c,GlobalTensor<float>&aGm,GlobalTensor<float>&bGm,int32_tM,int32_tN,int32_tK){// 分块计算for(int32_ti=0;i<M;i+=TILE_SIZE){for(int32_tj=0;j<N;j+=TILE_SIZE){// 搬运当前块CopyInA(aGm,aLocal,i,min(i+TILE_SIZE,M),K);CopyInB(bGm,bLocal,j,min(j+TILE_SIZE,N),K);// 计算当前块MatMulTile(cLocal,aLocal,bLocal,min(TILE_SIZE,M-i),min(TILE_SIZE,N-j),K);// 写回当前块CopyOutC(cLocal,cGm,i,j,min(TILE_SIZE,M-i),min(TILE_SIZE,N-j));}}}

调试技巧

打印调试

// 在算子实现里加打印#include<cstdio>__aicore__voidMatMulCustomKernel::Process(){// 打印输入参数printf("M=%d, N=%d, K=%d\n",M,N,K);// 打印输入数据(前 10 个)for(inti=0;i<min(10,M*K);i++){printf("a[%d]=%f\n",i,aLocal.GetValue(i));}// 计算结果MatMul(cLocal,aLocal,bLocal,M,N,K);// 打印输出数据(前 10 个)for(inti=0;i<min(10,M*N);i++){printf("c[%d]=%f\n",i,cLocal.GetValue(i));}}

用 Ascend CL 调试

importacl# 初始化acl.init()acl.rt.set_device(0)# 运行算子(调试模式)acl.rt.set_op_execute_mode("debug")# 运行# ...(运行算子)# 查看日志# 日志在 /var/log/Ascend/ascend_toolkit/matmul_custom.log

常见问题

问题一:编译报错

error: 'matmul_t' was not declared in this scope

解决:包含正确的头文件

#include"lib_api.h"// 包含 matmul_t 的定义

问题二:运行时报错

[ERROR] Kernel execute failed: out of memory

解决:减小块大小,或者检查 UB 大小是否足够

// 检查 UB 大小uint32_tub_size=GetUbSize();printf("UB size: %u bytes\n",ub_size);// 减小块大小constexprint32_tTILE_SIZE=16;// 从 32 改成 16

问题三:精度不达标

余弦相似度: 0.97 (应该 > 0.99)

解决:检查计算精度,可能需要用 FP32

// 用 FP32 计算GlobalTensor<float>aGm;// float = FP32GlobalTensor<float>bGm;GlobalTensor<float>cGm;// 不要用 FP16// GlobalTensor<half> aGm; // half = FP16(精度可能不够)

参考资源

  • Ascend C 编程指南: https://www.hiascend.com/document/detail/zh/CANN/
  • 算子开发最佳实践: https://www.hiascend.com/document/detail/zh/CANN/
  • 算子样例代码: https://atomgit.com/cann/samples
  • 达芬奇架构白皮书: https://www.hiascend.com/document/detail/zh/CANN/

总结

用 Ascend C 开发自定义算子,流程是:定义算子原型 → 实现算子逻辑 → 编译成.so→ 在 PyTorch 里调用。核心是要理解达芬奇架构的内存层次(GM → UB → 计算单元),以及 Cube Unit 和 Vector Unit 的适用场景。性能优化的关键是尽量用 Cube Unit 做矩阵乘法,太大的矩阵要分块计算。调试可以用printf打印中间结果,或者用 Ascend CL 的调试模式。精度问题通常是因为用了 FP16,改成 FP32 一般能解决。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/25 7:55:05

深入 QEMU 热迁移

深入 QEMU 热迁移&#xff1a;从状态机到数据平面的全链路剖析 “把一台正在运行的虚拟机从一台主机搬到另一台&#xff0c;还让里面的操作系统浑然不觉——这听起来像魔法&#xff0c;实则是精密的工程。” 引言 实时迁移是 QEMU 最核心的子系统之一。它允许将一个正在运行的…

作者头像 李华
网站建设 2026/5/25 7:54:59

英语 16 大时态 | 一页速记版(核心区别 + 易错点)

一、时态底层逻辑速分法所有时态都由「时间轴&#xff08;过去 / 现在 / 将来 / 过去将来&#xff09;」「动作状态&#xff08;一般 / 进行 / 完成 / 完成进行&#xff09;」组合而成&#xff0c;掌握这两个维度&#xff0c;就能一眼区分时态本质。二、16 大时态核心区别 易错…

作者头像 李华
网站建设 2026/5/25 7:52:04

神经网络与深度学习(二)

五、深度学习视觉应用1、数据集常用数据集包括MNIST、Fashion-MNIST、CIFAR-10、PASCAL VOC、MS COCO、ImageNet、JFT-300M等。2、任务评价指标&#xff08;1&#xff09;精确率P与召回率RPTP/(TPFP) &#xff0c;表示“挑剔”的程度RTP/(TPFN&#xff09; &#x…

作者头像 李华
网站建设 2026/5/25 7:52:03

工业异常检测实战:从多模态数据集构建到AI模型评估全解析

1. 项目概述与核心价值在化工、制药、能源等流程工业领域&#xff0c;生产装置的平稳运行是安全与效益的基石。异常检测技术&#xff0c;作为保障这方基石的关键工具&#xff0c;其核心任务是从海量的传感器数据中&#xff0c;敏锐地捕捉到那些预示着潜在故障或性能衰退的“异常…

作者头像 李华
网站建设 2026/5/25 7:50:01

引力波透镜探测:参数偏移与似然比检验的统计框架与应用

1. 引力波透镜探测&#xff1a;从参数估计到一致性检验在引力波天文学领域&#xff0c;确认两个看似独立的引力波事件是否源自同一个天体物理源&#xff0c;只是被前景大质量天体&#xff08;如星系或星系团&#xff09;的引力透镜效应放大了&#xff0c;是当前一个极具挑战性且…

作者头像 李华