【Atlas 移植训练营 极市×昇腾】 Atlas 移植初体验

华为的Atlas系列硬件是基于昇腾系列(HUAWEI Ascend)AI 处理器,通过模块、标卡、小站、服务器、集群等丰富的产品形态,打造面向 “ 端、边、云 ” 的全场景AI基础设施方案。在这次的极市与昇腾举行的【Atlas移植营】中,体验了将YOLOv5模型移植到Atlas设备这一过程,期间收获很多。

开发流程

开发流程包括:视频和图片资源的硬件解码、对输入图片的预处理、推理过程以及对推理输出的后处理。整体流程如下所示:

开发流程

视觉与处理模块(VPC),可以实现硬件加速的图片抠图(crop)、缩放(resize)、粘贴(paste)等功能。比通用视觉算法库OpenCV要快,能够充分利用硬件性能。处理流程如下所示:

VPC处理流程

ATC模型转换

昇腾张量编译器(Ascend Tensor Compiler,简称ATC)是昇腾CANN架构体系下的模型转换工具, 它可以将开源框架的网络模型或Ascend IR定义的单算子描述文件(json格式)转换为昇腾AI处理器支持的.om格式离线模型。其功能架构如下所示:

ATC功能架构

将PyTorch模型转换为Json文件,可使用如下脚本:

1
2
3
4
5
6
7
8
9
10
export LD_LIBRARY_PATH=/usr/local/Ascend/driver/lib64:/usr/local/Ascend/driver/lib64/common:/usr/local/Ascend/driver/lib64/driver:$LD_LIBRARY_PATH
export LD_LIBRARY_PATH=/usr/local/Ascend/ascend-toolkit/latest/lib64:/usr/local/Ascend/ascend-toolkit/latest/compiler/lib64/plugin/opskernel:/usr/local/Ascend/ascend-toolkit/latest/compiler/lib64/plugin/nnengine:$LD_LIBRARY_PATH
export PYTHONPATH=/usr/local/Ascend/ascend-toolkit/latest/python/site-packages:/usr/local/Ascend/ascend-toolkit/latest/opp/op_impl/built-in/ai_core/tbe:$PYTHONPATH
export PATH=/usr/local/Ascend/ascend-toolkit/latest/bin:/usr/local/Ascend/ascend-toolkit/latest/compiler/ccec_compiler/bin:$PATH
export ASCEND_AICPU_PATH=/usr/local/Ascend/ascend-toolkit/latest
export ASCEND_OPP_PATH=/usr/local/Ascend/ascend-toolkit/latest/opp
export TOOLCHAIN_HOME=/usr/local/Ascend/ascend-toolkit/latest/toolkit
export ASCEND_HOME_PATH=/usr/local/Ascend/ascend-toolkit/latest:$ASCEND_HOME_PATH

atc --model best.pt --framework 5 --output best --soc_version <soc_version>

C++ API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
#include "acl/acl.h"
#include "acl/ops/acl_dvpp.h"

// 系统配置
aclError ret = aclInit(); // AscendCL 初始化函数
ret = aclFinalize(); // AscendCL去初始化函数,用于释放进程内的AscendCL相关资源

// 模型加载流程
uint32_t device_count;
ret = aclrtGetDeviceCount(device_count); // 获取可用Device的数量
uint32_t deviceid = 0;
ret = aclrtSetDevice(deviceid); // 指定当前进程或线程中用于运算的Device
// 在当前进程或线程中显式创建一个Context
aclrtContext context;
ret = aclrtCreatecontext(&context,deviceid);
// 在当前进程或线程中创建一个Stream
aclrtstream stream;
ret = aclrtCreateStream(&stream);
// 获取当前昇腾AI软件栈的运行模式,分为HOST端模式和DEVICE端模式两种
aclrtRunMode runmode;
ret = aclrtGetRunMode(&runmode);
// 从文件加载离线模型数据(适配昇腾AI处理器的离线模型)
int modelIdl;
model_file = "***.om";
ret = aclmdlLoadFromFileWithMem(strModelName.c_str(), &mModelID, mModelMptr, mModelMSize, mModelWptr, mModelWSize);
// modelDesc 模型描述结构体,其中包括模型输入端个数,输出端个数等信息
aclmdlDesc *modelDesc
model = aclmdlCreateDesc();
ret = aclmdlGetDesc(modelDesc,modelId);

// **析构流程**
// 析构顺序为 用modelid卸载模型、析构modelDesc、重置推理卡及关闭acl服务
ret = aclmdlUnload(modelId);
ret = aclmdlDestroyDesc(modelDesc);
ret = aclrtResetDevice(deviceId_);
ret = aclFinalize(); // 理论上后两步不应该在推理引擎内,而应该在调度器上完成

// 模型推理流程
// **创建流程**
// 模型的推理输入需要每次进行创建,推理输出的buffer可以一次性创建并一直复用
//// 创建输入
size_t modelInputSize;
void *modelInputBuffer = nullptr;
modelInputSize = aclmdlGetInputSizeByIndex(modelDesc, 0);
// 申请一块HOST侧的内存
ret = aclrtMalloc(&modelInputBuffer, modelInputSize, ACL_MEM_MALLOC_NORMAL_ONLY);

// 创建aclmdlDataset类型的数据,描述模型推理的输入,input_为aclmdlDataset类型
// 利用之前创建的HOST端内存,在DEVICE侧挂载,然后把自己的数据赋值到该块内存就完成的模型输入准备
aclmdlDataset *input_;
input_ = aclmdlCreateDataset();
aclDataBuffer *inputData = aclCreateDataBuffer(modelInputBuffer, modelInputSize);
ret = aclmdlAddDatasetBuffer(input_, inputData);
ret = aclrtMemcpy(modelInputBuffer, modelInputSize, input_host_memory_+i*3*yolo_params_.INPUT_H*yolo_params_.INPUT_W,yolo_params_.INPUT_H*yolo_params_.INPUT_W*3*sizeof(float), ACL_MEMCPY_DEVICE_TO_DEVICE);
//// 创建输出
aclmdlDataset *output_;
size_t outputSize = aclmdlGetNumOutputs(modelDesc);
output_ = aclmdlCreateDataset();
// 循环为每个输出申请内存,并将每个输出添加到aclmdlDataset类型的数据中
// 动态batch下,output为按最大batch值创建的内存块
for (size_t i = 0; i < outputSize; ++i) {
size_t buffer_size = aclmdlGetOutputSizeByIndex(modelDesc, i);
void *outputBuffer = nullptr;
ret = aclrtMalloc(&outputBuffer, buffer_size, ACL_MEM_MALLOC_NORMAL_ONLY);
if(ret != 0)
{
return -2;
}
aclDataBuffer* outputData = aclCreateDataBuffer(outputBuffer, buffer_size);
ret = aclmdlAddDatasetBuffer(output_, outputData);
if(ret != 0)
{
return -2;
}
}
//// 执行推理
ret = aclmdlExecute(modelId, input_, output_);
//// 获取输出到host侧
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(output_, idx);

// 获取buffer地址
void* dataBufferDev = aclGetDataBufferAddr(dataBuffer);

// 获取buffer的长度
size_t bufferSize = aclGetDataBufferSizeV2(dataBuffer);

// 将指定内存从device拷贝到host的内存上,此时buffer内存即为模型多个推理输出中的指定索引输出
void* buffer = new uint8_t[bufferSize];
aclError aclRet = aclrtMemcpy(buffer, bufferSize, dataBufferDev, bufferSize, ACL_MEMCPY_DEVICE_TO_HOST);
// **析构流程**
这里的析构是说,在每次执行完之后,都要释放掉input重新创建,因为input部分不可复用
if (input_ != nullptr)
{
for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(input_); ++i)
{
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(input_, i);
aclDestroyDataBuffer(dataBuffer);
}
aclmdlDestroyDataset(input_);
input_ = nullptr;
}

// 输出部分的析构可以放在类的析构时再做
if (output_ != nullptr) {
// 此处应该写入日志
for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(output_); ++i)
{
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(output_, i);
void* data = aclGetDataBufferAddr(dataBuffer);
(void)aclrtFree(data);
(void)aclDestroyDataBuffer(dataBuffer);
}
(void)aclmdlDestroyDataset(output_);
output_ = nullptr;
}