前言
平时喜欢玩开发板,之前在jetson nano,RK3399pro,英特尔神经计算棒上实现了各种深度学习算法的迁移部署,比较熟悉tensorrt,rknn,openvino各种花里胡哨的推理引擎
最近又接触了华为Atlas200DK开发板,体验了一把达芬奇架构的昇腾310NPU,实力的确不可小觑,在几天内的学习和努力下终于熟悉了与NPU配套的CANN计算框架(我比较喜欢叫推理引擎),因此分享一下使用CANN的pyACL接口部署YOLOX、YOLOv5和Nanodet的方法。
YOLOX是YOLO目标检测系列里的最新方法,代码风格友好,并且根据我上一篇博客的结果其轻量模型推理效率很高,因此本文重点以YOLOX为例。
深度学习模型在AI芯片上部署的一般流程
首先介绍一下在AI芯片上的部署流程,无论是英伟达的tensorrt,瑞芯微的rknn,还是openvino、CANN等推理引擎,虽然各引擎的API不同,但基本流程都差不多,如下图所示:
从深度学习训练框架(如pytorch,tf,paddle等)训练得到的模型,通过各推理引擎附带的模型转换器(有的是引擎自身的API转换比如RKNN,有的配置了专门的转换工具tools比如CANN的ATC),转换成对应引擎的的离线模型。在推理时,通过引擎API将模型和预处理后的图片加载到内存中,然后载入AI芯片中执行推理。
不过不同的推理引擎的推理上仍然存在一些细节差异。
CANN模型部署流程
对于CANN而言,模型部署和推理过程相对复杂一些。这里引用一下官网的流程,对于常用的目标检测模型,只需要看右边的模型推理就OK了:
主要就是ACL环境(运行资源)的初始化、模型转换和加载、图像预处理、模型推理、后处理、模型卸载和ACL环境的去初始化。
乍一看,推理前进行环境初始化、结束后去初始化的操作比较繁琐,但这种规范的流程设计其实是很有必要的:
举个反例,openvino没有这些初始化之类的步骤,于是我在英特尔的NCS2 VPU上经常出现跑完程序后需要等待很长时间才能进行下一次运行的问题,原因就是没有把上一次加载的内存数据卸载掉:)
CANN ACL接口调用流程(python)
ACL(Ascend Computing Language)提供了CANN的推理接口,可用C++(AscendCL)和python(pyACL)进行开发,我比较喜欢用pyACL。
一般的接口调用流程如下:
步骤1.ACL环境初始化和资源申请
import acl
# pyACL初始化
ret = acl.init()
# 指定运算的Device
self.device_id = 0
ret = acl.rt.set_device(self.device_id)
# 显式创建一个Context,用于管理Stream对象
self.context, ret = acl.rt.create_context(self.device_id)
步骤2.模型加载
与pytorch等框架下的模型加载不同,CANN在加载模型时,除了要把模型文件加载到内存中,还要通过模型文件把模型的输入输出类型、个数、内存等设置好(openvino也有类似操作)。
'''
加载模型文件
'''
self.model_path = './model/resnet50.om'
# 加载离线模型文件,返回标识模型的ID。
self.model_id, ret = acl.mdl.load_from_file(self.model_path)
# 根据加载成功的模型的ID,获取该模型的描述信息。
self.model_desc = acl.mdl.create_desc()
ret = acl.mdl.get_desc(self.model_desc, self.model_id)
'''
设置模型的输入
'''
ACL_MEM_MALLOC_HUGE_FIRST = 0
# 创建aclmdlDataset类型的数据,描述模型推理的输入
self.load_input_dataset = acl.mdl.create_dataset()
# 获取模型输入的数量
input_size = acl.mdl.get_num_inputs(self.model_desc)
self.input_data = []
# 循环为每个输入申请内存,并将每个输入添加到aclmdlDataset类型的数据中
for i in range(input_size):
buffer_size = acl.mdl.get_input_size_by_index(self.model_desc, i)
# 申请输入内存
buffer, ret = acl.rt.malloc(buffer_size, ACL_MEM_MALLOC_HUGE_FIRST)
data = acl.create_data_buffer(buffer, buffer_size)
_, ret = acl.mdl.add_dataset_buffer(self.load_input_dataset, data)
self.input_data.append({"buffer": buffer, "size": buffer_size})
'''
设置模型的输出
'''
# 创建aclmdlDataset类型的数据,描述模型推理的输出
self.load_output_dataset = acl.mdl.create_dataset()
# 获取模型输出的数量
output_size = acl.mdl.get_num_outputs(self.model_desc)
self.output_data = []
# 循环为每个输出申请内存,并将每个输出添加到aclmdlDataset类型的数据中
for i in range(output_size):
buffer_size = acl.mdl.get_input_size_by_index(self.model_desc, i)
# 申请输出内存
buffer, ret = acl.rt.malloc(buffer_size, ACL_MEM_MALLOC_HUGE_FIRST)
data = acl.create_data_buffer(buffer, buffer_size)
_, ret = acl.mdl.add_dataset_buffer(self.load_output_dataset, data)
self.output_data.append({"buffer": buffer, "size": buffer_size})
这一段容易看晕,因为获取模型输入输出数据信息时都需要先使用API初始化这个数据类型,然后再通过另一个API获得信息,类似于C++中的先声明再使用。
步骤3.准备输入数据,预处理,推理,后处理
ACL_MEMCPY_DEVICE_TO_DEVICE = 3
NPY_BYTE = 1
images_list = ["./data/dog1_1024_683.jpg", "./data/dog2_1024_683.jpg"]
for image in images_list:
# 1.自定义函数transfer_pic,对图片进行缩放、剪裁等预处理操作
img = transfer_pic(image)
# 2.准备模型推理的输入数据
np_ptr = acl.util.numpy_to_ptr(img)
# 将图片数据拷贝到输入数据内存中
ret = acl.rt.memcpy(self.input_data[0]["buffer"], self.input_data[0]["size"], np_ptr,
self.input_data[0]["size"], ACL_MEMCPY_DEVICE_TO_DEVICE)
# 3.执行模型推理
# self.model_id表示模型ID,在模型加载成功后,会返回标识模型的ID
ret = acl.mdl.execute(self.model_id, self.load_input_dataset, self.load_output_dataset)
# 4.处理模型推理的输出数据,输出top5置信度的类别编号
inference_result = []
for i, item in enumerate(self.output_data):
buffer_d, ret = acl.rt.malloc(self.output_data[i]["size"], ACL_MEM_MALLOC_HUGE_FIRST)
ret = acl.rt.memcpy(buffer_d, self.output_data[i]["size"], self.output_data[i]["buffer"],
self.output_data[i]["size"], ACL_MEMCPY_DEVICE_TO_DEVICE)
data = acl.util.ptr_to_numpy(buffer_d, (self.output_data[i]["size"],), NPY_BYTE)
inference_result.append(data)
tuple_st = struct.unpack("1000f", bytearray(inference_result[0]))
vals = np.array(tuple_st).flatten()
top_k = vals.argsort()[-1:-6:-1]
print("======== top5 inference results: =============")
for j in top_k:
print("[%d]: %f" % (j, vals[j]))
步骤4.卸载模型
# 释放输入资源,包括数据结构和内存
while self.input_data:
item = self.input_data.pop()
ret = acl.rt.free(item["buffer"])
input_number = acl.mdl.get_dataset_num_buffers(self.load_input_dataset)
for i in range(input_number):
data_buf = acl.mdl.get_dataset_buffer(self.load_input_dataset, i)
if data_buf:
ret = acl.destroy_data_buffer(data_buf)
ret = acl.mdl.destroy_dataset(self.load_input_dataset)
# 释放输出资源,包括数据结构和内存
while self.output_data:
item = self.output_data.pop()
ret = acl.rt.free(item["buffer"])
output_number = acl.mdl.get_dataset_num_buffers(self.load_output_dataset)
for i in range(output_number):
data_buf = acl.mdl.get_dataset_buffer(self.load_output_dataset, i)
if data_buf:
ret = acl.destroy_data_buffer(data_buf)
ret = acl.mdl.destroy_dataset(self.load_output_dataset)
# 卸载模型
ret = acl.mdl.unload(self.model_id)
# 释放模型描述信息
if self.model_desc:
ret = acl.mdl.destroy_desc(self.model_desc)
self.model_desc = None
步骤5.资源释放,acl去初始化
# 释放Context
if self.context:
ret = acl.rt.destroy_context(self.context)
self.context = None
# 释放Device
ret = acl.rt.reset_device(self.device_id)
# pyACL去初始化
ret = acl.finalize()
以上就是pyACL接口的基本调用流程
用于简化部署的Atlas Utils
由于ACL调用比较繁琐,Ascend社区根据Atlas200DK硬件将ACL api进一步的封装成了Utils的形式,在Atlas Utils官方仓库中的cplusplus/common和python/common子文件夹中,python的utils主要包含了以下几个文件:
acl_dvpp.py # 用于预处理
acl_image.py # 用于读取图片数据
acl_model.py # 用于模型加载和推理
acl_resource.py # 用于acl初始化和资源申请
acl_logging.py # 消息提示
constants.py # 一些常用flags
utils.py # 一些常用工具
这些文件内部已经把繁琐的初始化、资源申请、模型加载、模型输入输出准备、推理后的资源释放进行了内部封装,因此使用atlas_utils来部署模型可以专注于算法实现。
下面我将简要演示如何用atlas_utils部署模型,使用到的源码在这yolox_for_cann_atlas200dk,其中atlas_utils文件夹来自上面的官方仓库。
1.导入相关模块、通过atlas_utils.acl_resource初始化pyACL
首先,把需要用到的python模块和atlas_utils导入,然后初始化pyACL环境:
'''
导入常用包、图像处理包
'''
import sys
import os
import functools
import time
import numpy as np
import cv2
from PIL import Image, ImageDraw, ImageFont, ImageOps
path = os.path.dirname(os.path.abspath(__file__))
sys.path.append(os.path.join("../../"))
currentPath = os.path.join(path, "..")
OUTPUT_DIR = os.path.join(currentPath, 'outputs')
MODEL_PATH = os.path.join(currentPath, "model/yolox_s.om")
'''
导入pyACL和atlas_utils
'''
import acl
from atlas_utils.acl_resource import AclResource
from atlas_utils.acl_model import Model
from atlas_utils.acl_image import AclImage
from atlas_utils.acl_dvpp import Dvpp
import atlas_utils.constants as const
import atlas_utils.utils as utils
'''
pyACL初始化(调整了一下代码的顺序,方便梳理步骤)
'''
acl_resource = AclResource()
acl_resource.init()
2.通过atlas_utils.acl_model加载CANN模型
acl_model中的Model类已经将acl模型加载与模型输入输出准备封装好了,因此只需要加载模型路径即可:
# 省略了YOLOX类的定义代码,细节可参考我的git repo源码
self.model = Model(self.model_path)
3.(可选)通过atlas_utils.acl_dvpp、atlas_utils.acl_image加载图像、进行预处理
atlas_utils也将acl的dvpp模块进行了封装,可用来resize图像,不过我还是采用opencv、PIL和numpy来做预处理,因此该部分可以参考官方samples的做法,这里不做赘述。
4.模型推理
# image是已经通过cv2.imread读取的图片
# pre_process是预处理函数,一般包括resize、crop、padding、normalize、expand_dims
img = self.pre_process(image)
# 通过acl_model.Model.execute()方法进行CANN模型推理
outs = self.model.execute([inp, ])
# post_process后处理函数,我习惯用numpy实现
dets = self.post_process(prediction)
在这里需要注意两个点,第一就是acl_model.Model.execute()函数输入必须是list,比如[img, ],不然会报错(原因是CANN模型需要通过list来对应每个输入节点)
5.推理完成
在ACL中,当推理完成后需要将模型卸载、运行内存、上下文等资源释放、acl去初始化。
不过在atlas_utils的封装下,这些步骤都将在python运行结束时自动执行,如果你有兴趣的话可以参考atlas_utils中的文件,它将model、acl等都封装成为了类,并通过__del__()函数使得类的对象自动销毁、释放资源。
总结
本次blog记录了通过两种pyACL调用CANN接口实现模型推理的方法,一是直接采用华为CANN的pyACL,二是通过华为官方进一步封装的atlas_utils接口。
下一次blog我将给出YOLOX在Atlas200DK上使用CANN部署的具体实现步骤。
版权声明:本文为CSDN博主「RuiH.AI」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41035283/article/details/119279858
暂无评论