CANN学习资源开源仓的中级算子开发一
与将host和device代码混合到单个asc源文件的开发模式不同,Ascend C单算子工程实现了从Host侧的算子注册、形状推导、Tiling分块、任务下发与内存管理,到kernel侧使用 Ascend C编写核函数计算逻辑的完整流程,最终生成一个可编译和部署的完整算子包。
msOpGen可基于算子原型定义输出算子工程:包括算子host侧代码实现文件(大致框架,代码自己写)、算子kernel侧实现文件(同样)以及工程编译配置文件等。CANN安装好后就自带有msopgen等工具的。这里仓里的做法却是从头编译安装,从https://gitcode.com/Ascend/msopgen.git克隆,编译过程中又下载子模块https://gitcode.com/cann/asc-tools.git。编译安装好,并写好算子原型文件add_custom.json,运行:
msopgen gen -i Sources/add_custom.json -c ai_core-ascend910b1 -lan cpp -out Sources/custom_op
在我看来尽是瞎折腾,折腾出来就是个毛坯房,有啥意思。我去拷贝一个人家开发好的精装修完的算子工程,自己改吧改吧不省事吗。
在Ascend C算子工程中,Host侧是算子执行的控制与管理层,弥补kernel侧仅擅长高密度并行计算的短板,承担三类关键工作:
- 算子原型注册: 注册算子的原型定义,从而确保算子能够被框架正确识别、编译和执行。算子原型主要描述了算子的输入输出、属性等信息以及算子在AI处理器上相关实现信息,并关联tiling实现等函数。
- Shape、Dtype推导函数实现: 根据算子的输入张量描述、算子逻辑及算子属性,推理出算子的输出张量描述,包括张量的形状、数据类型及数据排布格式等信息。这样算子构图准备阶段就可以为所有的张量静态分配内存,避免动态内存分配带来的开销(应该只有在输入形状固定或在编译时确定,才能做到吧?)。
- Tiling实现: 大多数情况下,Local Memory的存储,无法完整的容纳算子的输入与输出,需要每次搬运一部分输入进行计算然后搬出,再搬运下一部分输入进行计算,直到得到完整的最终结果,这个数据切分、分块计算的过程称之为Tiling。根据算子的shape等信息来确定数据切分算法相关参数(比如每次搬运的块大小,以及总共循环多少次)的计算程序,称之为Tiling实现。可看下面示意图(设定场景:假设某 AI Core 的片上存储容量上限为 10 个数据元素):

与前面单个ASC文件的算子类实现不同,算子工程支撑动态shape输入,所以在Init函数中需要根据Tiling结构体数据来动态初始化内存,而不是使用固定的大小进行初始化。
算子工程编译和安装完成后,运行预先编写的单算子API调用程序,对该算子工程进行测试
# 编译测试代码
g++ -I$ASCEND_TOOLKIT_HOME/include -I${HOME}/vendors/customize/op_api/include -L$ASCEND_TOOLKIT_HOME/lib64 -L${HOME}/vendors/customize/op_api/lib Sources/test/main.cpp -lcust_opapi -lnnopbase -lacl_rt -o execute_add_op;
# 设置自定义算子so路径并执行调用代码
source ${HOME}/vendors/customize/bin/set_env.bash;./execute_add_op
单算子API调用,是直接调用单算子API接口,无需提供单算子描述文件进行离线模型的转换,是最重要的一种调用方式。算子编译安装后可以在安装目录下找到生成的单算子API:单算子调用的头文件.h和动态库libcust_opapi.so
.
|____customize
| |____scripts
| | |____uninstall.sh
| |____framework
| | |____plugin
| | | |____npu_supported_ops.json
| | |____tensorflow
| | | |____libcust_tf_parsers.so
| |____op_api
| | |____lib
| | | |____libcust_opapi.so #这里
| | |____include
| | | |____aclnn_add_custom_template.h #还有这里
....
aclnn_add_custom_template.h中的算子API形式定义为“两段式接口”:
15 /* funtion: aclnnAddCustomTemplateGetWorkspaceSize
16 * parameters :
17 * x : required
18 * y : required
19 * out : required
20 * workspaceSize : size of workspace(output).
21 * executor : executor context(output).
22 */
23 __attribute__((visibility("default")))
24 aclnnStatus aclnnAddCustomTemplateGetWorkspaceSize(
25 const aclTensor *x,
26 const aclTensor *y,
27 const aclTensor *out,
28 uint64_t *workspaceSize,
29 aclOpExecutor **executor);
30
31 /* funtion: aclnnAddCustomTemplate
32 * parameters :
33 * workspace : workspace memory addr(input).
34 * workspaceSize : size of workspace(input).
35 * executor : executor context(input).
36 * stream : acl stream.
37 */
38 __attribute__((visibility("default")))
39 aclnnStatus aclnnAddCustomTemplate(
40 void *workspace,
41 uint64_t workspaceSize,
42 aclOpExecutor *executor,
43 aclrtStream stream);
然后单算子API调用代码与之前大同小异,除了调用本身,调用之前要分配内存准备好Tensor,计算标杆数据,调用后比较结果等。这里说一下代码里定义的CHECK_ACL宏,目的是简化代码的编写,用于检查ACL API调用是否成功:
#define CHECK_ACL(expr) \
do { \
auto __ret = (expr); \
int32_t __code = static_cast<int32_t>(__ret); \
if (__code != 0) { \
fprintf(stderr, "[ERROR] %s failed at %s:%d, ret=%d\n", #expr, __FILE__, __LINE__, __code); \
} \
} while (0)
这里面有一个很大的空隙,就是算子工程的CMake编译配置已经大大的复杂化了,构建产出也大大的增加,但是对这块并没有一个讲解。
- 点赞
- 收藏
- 关注作者
评论(0)