固定Shape算子
接口调用流程
开发应用时,如果涉及执行单个算子,则应用程序中必须包含执行单个算子的代码逻辑。关于执行单个算子的接口调用流程,请先参见pyACL接口调用流程了解整体流程,再查看本节中的流程说明。
系统支持的算子请参见《算子清单》。
图1 算子调用流程

关键接口的说明如下:
-
加载算子模型文件。
支持以下2种方式中的一种加载单算子模型文件:
- 调用acl.op.set_model_dir接口,设置加载模型文件的目录,目录下存放单算子模型文件(*.om文件)。
- 调用acl.op.load接口,从内存中加载单算子模型数据,由用户管理内存。单算子模型数据是指“单算子编译成*.om文件后,再将om文件读取到内存中”的数据。
-
调用acl.rt.malloc接口申请Device上的内存,存放执行算子的输入、输出数据。
-
动态Shape场景,如果无法明确算子的输出Shape时,在执行算子前,还需推导或预估算子的输出Shape。
需用户调用acl.op.infer_shape接口、acl.get_tensor_desc_num_dims接口、acl.get_tensor_desc_dim_v2接口、acl.get_tensor_desc_dim_range等接口,推导或预估算子的输出Shape,作为算子执行接口acl.op.execute_v2的输入。
-
执行算子。
- 对于被封装成pyACL接口的算子(参见调用CBLAS接口),包括GEMM算子、Cast算子,目前支持以下两种执行方式:
- 不以handle方式执行算子,接口名称中不包含“Handle”关键字,例如,调用acl.blas.gemm_ex接口(封装GEMM算子)、acl.op.cast接口(封装Cast算子)等执行算子。
- 以handle方式执行算子,接口名称中包含“Handle”关键字,例如,调用acl.blas.create_handle_for_gemm_ex接口、acl.op.create_handle_for_cast接口等创建handle后,还需要调用acl.op.execute_with_handle接口执行算子。
- 对于未被封装成pyACL接口的算子,目前执行以下两种执行方式:
- 不以handle方式执行算子,调用acl.op.execute_v2接口执行算子。
- 以handle方式执行算子,调用acl.op.create_handle接口创建handle,再调用acl.op.execute_with_handle接口执行算子。
:::note 说明 不以handle方式执行算子时,每次执行算子时,系统内部都会根据算子描述信息匹配内存中的模型。
以handle方式执行算子时,系统内部将算子描述信息匹配到内存中的模型,并缓存在Handle中,每次执行算子时,无需重复匹配算子与模型,因此在涉及多次执行同一个算子时,效率更高。但Handle使用结束后,需调用acl.op.destroy_handle接口释放。 :::
- 对于被封装成pyACL接口的算子(参见调用CBLAS接口),包括GEMM算子、Cast算子,目前支持以下两种执行方式:
-
调用acl.rt.synchronize_stream接口阻塞应用运行,直到指定Stream中的所有任务都完成。
-
调用acl.rt.free接口释放内存。
父主题: 单算子调用
调用CBLAS接口
基本原理
目前,pyACL已将GEMM算子(用于矩阵-向量乘、矩阵-矩阵乘)、Cast算子(用于转换数据类型)封装成pyACL接口,可参见CBLAS接口,目前支持以下两种执行方式:
- 不以handle方式执行算子,接口名称中不包含“handle”关键字,例如,调用acl.blas.gemm_ex接口(封装GEMM算子)、acl.op.cast接口(封装Cast算子)等执行算子。
- 以handle方式执行算子,接口名称中包含“handle”关键字,例如,调用acl.blas.create_handle_for_gemm_ex接口、acl.op.create_handle_for_cast接口等创建handle后,还需要调用acl.op.execute_with_handle接口执行算子。
:::note 说明 不以handle方式执行算子时,每次执行算子时,系统内部都会根据算子描述信息匹配内存中的模型。
以handle方式执行算子时,系统内部将算子描述信息匹配到内存中的模型,并缓存在Handle中,每次执行算子时,无需重复匹配算子与模型,因此在涉及多次执行同一个算子时,效率更高。但Handle使用结束后,需调用acl.op.destroy_handle接口释放。 :::
示例代码
本章以acl.blas.gemm_ex接口为例,该示例中矩阵乘的计算公式为:C = αAB + βC。
调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。以下是关键步骤的代码示例,不可以直接拷贝运行,仅供参考。
import acl
## ......
ACL_MEM_MALLOC_NORMAL_ONLY = 2
ACL_TRANS_N = 0
ACL_COMPUTE_HIGH_PRECISION = 0
ACL_MEMCPY_HOST_TO_DEVICE = 1
ACL_MEMCPY_DEVICE_TO_HOST = 2
## 1.pyACL初始化。
ret = acl.init("test_data/config/acl.json")
## 2.设置单算子模型文件所在的目录。
ret = acl.op.set_model_dir("op_models")
## 3.指定用于运算的设备。
device_id = 0
ret = acl.rt.set_device(device_id)
## 4.申请内存。
## 4.1 申请Device上的内存存放执行算子的输入数据。
## 对于该矩阵乘示例,size_a表示矩阵A数据的大小,size_b表示矩阵B数据的大小,size_c表示矩阵C数据的大小。
in_dtype, out_dtype = 1, 1
size_a = m * k * acl.data_type_size(acl_dtype)
size_b = m * k * acl.data_type_size(acl_dtype)
size_c = m * k * acl.data_type_size(acl_dtype)
dev_matrix_a, ret = acl.rt.malloc(size_a, ACL_MEM_MALLOC_NORMAL_ONLY)
dev_matrix_b, ret = acl.rt.malloc(size_b, ACL_MEM_MALLOC_NORMAL_ONLY)
dev_matrix_c, ret = acl.rt.malloc(size_c, ACL_MEM_MALLOC_NORMAL_ONLY)
## 4.2 申请Host上的内存。
## 对于该矩阵乘示例,m表示矩阵A的行数与矩阵C的行数,n表示矩阵B的列数与矩阵C的列数。
## k表示矩阵A的列数与矩阵B的行数。
host_matrix_a, ret = acl.rt.malloc_host(size_a)
host_matrix_b, ret = acl.rt.malloc_host(size_b)
host_matrix_c, ret = acl.rt.malloc_host(size_c)
## 5.准备输入数据。
## 从文件读入到host_matrix_a和host_matrix_b中。
## 对于该矩阵乘示例,将矩阵A和矩阵B的数据从Host复制到Device。
ret = acl.rt.memcpy(dev_matrix_a, size_a, host_matrix_a, size_a, ACL_MEMCPY_HOST_TO_DEVICE)
ret = acl.rt.memcpy(dev_matrix_b, size_b, host_matrix_b, size_b, ACL_MEMCPY_HOST_TO_DEVICE)
## 6.执行单算子。
stream, ret = acl.rt.create_stream()
## 对于该示例,调用acl.blas.gemm_ex接口(异步接口)实现矩阵-矩阵的乘法。
ret = acl.blas.gemm_ex(ACL_TRANS_N, ACL_TRANS_N, ACL_TRANS_N, m, n, k, dev_alpha, dev_matrix_a, k, input_type, dev_matrix_b, n, input_type, dev_beta, dev_matrix_c, n, output_type, ACL_COMPUTE_HIGH_PRECISION, stream)
## 调用acl.rt.synchronize_stream接口阻塞Host运行,直到指定Stream中的所有任务都完成。
ret = acl.rt.synchronize_stream(stream)
## 7.将算子的输出数据从Device复制到Host。
ret = acl.rt.memcpy(host_matrix_c, size_c, dev_matrix_c, size_c, ACL_MEMCPY_DEVICE_TO_HOST)
## 8.释放运行管理资源。
ret = acl.rt.destroy_stream(stream)
ret = acl.rt.reset_device(device_id)
ret = acl.finalize()
## ......
父主题: 单算子调用
固定Shape算子
对于昇腾AI处理器不支持的算子,用户需参考《TBE&AI CPU算子开发指南》先完成自定义算子的开发,再参考如下内容执行单算子。
父主题: 单算子调用
动态Shape算子(不注册算子选择器)
基本原理
对于支持动态Shape的算子:
- 如果算子输出Shape明确,该类算子执行的基本流程与固定Shape算子执行类似,接口调用流程请参见接口调用流程,执行固定Shape算子的示例代码请参见固定Shape算子。
- 如果无法明确算子的输出Shape,在调用acl.op.execute_v2接口前,需用户调用acl.op.infer_shape接口、acl.get_tensor_desc_num_dims接口、acl.get_tensor_desc_dim_v2接口、acl.get_tensor_desc_dim_range等接口,推导或预估算子的输出Shape,作为算子执行接口acl.op.execute_v2的输入。
示例代码
调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。以下是关键步骤的代码示例,不可以直接拷贝运行,仅供参考。
import acl
## ......
## 此处算子输入tensor数据的内存必须根据应用运行模式来确定,应用运行在Host时,此处需申请Host上的内存。
## 应用运行在Device时,此处需申请Device上的内存。
ret = acl.op.infer_shape(op_type, self.in_desc_list, self.in_list, num_outputs, self.out_desc_list, self.attr)
tensor_dims = []
## 循环算子的每一个输出,推导或预估Shape值:
for i in range(len(infer_desc_list)):
dim_nums = acl.get_tensor_desc_num_dims(infer_desc_list[i])
dim_size = []
for j in range(dim_nums):
dim, ret = acl.get_tensor_desc_dim_v2(infer_desc_list[i], j)
# 表示动态Shape场景下维度值是动态的。
if dim == -1:
# 获取Shape范围,使用该范围的Shape最大值来构造输出tensor_desc,作为acl.op.execute_v2的输入。
dim_range, ret = acl.get_tensor_desc_dim_range(infer_desc_list[i], j, 2)
dim = dim_range[1]
dim_size.append(dim)
tensor_dims.append(dim_size)
## 以上给出了执行算子时输出的shape,根据tensor_dims中的dims构造输出tensor_desc(即self.out_desc_list参数值),用于调用acl.op.execute_v2。
ret = acl.op.execute_v2(op_type, self.in_desc_list, self.in_list, self.out_desc_list, self.out_list, self.attr, self.stream)
out_tensor_dims = []
## 针对上面用户预估Shape值以及使用Shape范围中的最大Shape的场景,设在算子执行结束后,需增加下面的调用,获取准确的shape。
for i in range(len(self.out_desc_list)):
dim_nums = acl.get_tensor_desc_num_dims(self.out_desc_list[i])
dim_size = []
for j in range(dim_nums):
dim, ret = acl.get_tensor_desc_dim_v2(self.out_desc_list[i], j)
dim_size.append(dim)
out_tensor_dims.append(dim_size)
## ......
父主题: 单算子调用
内存二次分配管理
用户内存管理有两种管理方式:
- 独立内存管理,根据需要单独申请所需的内存,内存不做拆分或者二次分配。
- 内存池管理内存,用户一次性申请一块较大内存,并在使用时从这块较大内存中二次分配所需内存。
在内存二次分配时,请使用如下接口从内存池申请对应内存。由于各接口对申请的内存地址、大小有约束,在内存池管理时,需要对该情况关注处理,否则容易出现内存越界。
内存管理的总体说明请参见总体说明。
| 接口 | 用途 | 输入内存/输出内存 |
|---|---|---|
| acl.rt.memcpy_async | 实现异步内存复制。 | 调用本接口进行内存复制时,源地址和目的地址都必须64字节对齐。 |
| acl.rt.malloc | 在Device上申请size大小的线性内存,该接口对用户申请的size向上对齐成32字节整数倍后再多加32字节,并通过“dev_ptr”返回已分配内存的指针地址。 | 若用户使用本接口申请大块内存并自行划分、管理内存时,每段内存需同时满足以下需求: - 内存大小向上对齐成32整数倍加32字节(m = ALIGN_UP[len, 32] + 32字节)。 - 内存起始地址需满足64字节对齐(ALIGN_UP[m, 64])。 (说明:len 表示某段内存的大小,ALIGN_UP[len, k]表示向上按k字节对齐:((len - 1) / k + 1) * k。) |
| acl.rt.malloc_host | 应用在Device上运行时,调用该接口申请的是Device内存,且Device上的内存按普通页申请,如需首地址64字节对齐,需要用户自行处理对齐。同步接口。 | 若用户使用本接口申请大块内存并自行划分、管理内存时,每段内存需同时满足以下需求: - 内存大小向上对齐成32整数倍加32字节(m = ALIGN_UP[len, 32] + 32字节)。 - 内存起始地址需满足64字节对齐(ALIGN_UP[m, 64])。 (说明:len 表示某段内存的大小,ALIGN_UP[len, k]表示向上按k字节对齐:((len - 1) / k + 1) * k。) |
| acl.rt.malloc_cached | 申请Device上的内存,同步接口,该接口在任何场景下申请的内存都是支持cache缓存。在Device上申请size大小的线性内存,通过dev_ptr返回已分配内存的指针地址。 | 其它约束与acl.rt.malloc接口相同。 |
计算机视觉领域一般涉及使用媒体数据处理功能,因此会涉及以上多种内存申请接口。内存首地址涉及64字节或128字节对齐,为方便统一管理,内存首地址对齐值建议选取较大项,比如内存首地址128字节对齐。
关于媒体数据处理时自行管理内存时的典型场景如下,媒体数据处理的功能点介绍请参见媒体数据处理V1。
图1 VDEC场景

图2 JPEGD场景

父主题: 扩展更多特性
多Device场景
开发应用时,如果涉及多Device之间的任务等待,则应用程序中必须包含相关的代码逻辑。关于该场景的接口调用流程,请依次参见pyACL接口调用流程以及本节中的说明。
图1 同步等待流程_多Device场景

- 在多Device时,利用Context切换(调用acl.rt.set_context接口)来切换Device,比使用acl.rt.set_device接口效率高。
- 调用acl.rt.synchronize_device接口等待Device上的计算任务结束。
- 模型加载与执行的流程请参见开发基础推理应用。
- 算子加载与执行的流程请参见单算子调用。
父主题: 扩展更多特性
在线提单