快速入门
在本节中,您可以通过一个简单的图片分类应用了解使用AscendCL接口(C语言接口)开发应用的基本过程以及开发过程中涉及的关键概念。
了解图片分类应用
“图片分类应用”,从名称上,我们也能直观地看出它的作用:标识图片所属的分类。
“图片分类应用”是怎么做到这一点的呢?当然得先有一个能做到图片分类的模型,我们可以直接使用一些训练好的开源模型,也可以基于开源模型的源码进行修改、重新训练,还可以自己基于算法、框架构建适合自己的模型。
鉴于当前我们是入门内容,此处我们直接获取已训练好的开源模型,这种方式相对简单。此处我们选择的是ONNX框架的ResNet-50模型。
ResNet-50模型的基本介绍如下:
- 输入数据:BGR格式、224*224分辨率的输入图片
- 输出数据:图片的类别标签及其对应置信度
- 置信度是指图片所属某个类别可能性。
- 类别标签和类别的对应关系与训练模型时使用的数据集有关,需要查阅对应数据集的标签及类别的对应关系。
了解基本概念
- Host
Host指与Device相连接的X86服务器、ARM服务器,会利用Device提供的NN(Neural-Network)计算能力,完成业务。
- Device
- 开发环境、运行环境
开发环境指编译开发代码的环境,运行环境指运行算子、推理或训练等程序的环境,运行环境上必须带昇腾AI处理器。
开发环境和运行环境可以合设在同一台服务器上,也可以分设:
- 合设场景下,登录到合设的服务器上,不用切换开发环境、运行环境。
- 分设场景下,若开发环境和运行环境上的操作系统架构不同,在开发环境中需使用交叉编译,这样编译出来的可执行文件,才可以在运行环境中执行。
您可以登录对应的环境,执行“uname -a”命令查询其操作系统的架构。
- 运行用户:
了解开发过程
AscendCL(Ascend Computing Language)是一套用于在CANN(Compute Architecture for Neural Networks)上开发深度神经网络推理应用的C语言API库,提供模型加载与执行、媒体数据处理、算子加载与执行等API,能够实现在昇腾CANN平台上进行深度学习推理计算、图形图像预处理、单算子加速计算等能力。
了解了这些大步骤后,下面我们再展开来说明开发应用具体涉及哪些关键功能?各功能又使用哪些AscendCL接口,这些AscendCL接口怎么串联?
虽然此时您可能不理解所有细节,但这也不影响,此处先了解整体的代码逻辑,后续再通过编译及运行应用、解析样例代码了解细节。
解析样例代码
体验快速入门样例的编译运行后,若还想了解代码的实现逻辑,可查看下文的代码解析。此处按各函数的功能来介绍代码逻辑,为了便于阅读、理解代码,将各函数相关的变量与函数放在一起介绍。
打开“resnet50_firstapp/src/main.cpp”文件,先从main函数开始,了解整个样例的代码串接逻辑,再分别了解各自定义函数的实现。
- main函数用来串联整个应用的代码逻辑。
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
int main() { // 1.定义一个资源初始化的函数,用于AscendCL初始化、运行管理资源申请(指定计算设备) InitResource(); // 2.定义一个模型加载的函数,加载图片分类的模型,后续推理使用,若om文件不在model目录下,请根据实际存放目录修改此处的代码 const char *modelPath = "../model/resnet50.om"; LoadModel(modelPath); // 3.定义一个读图片数据的函数,将测试图片数据读入内存,并传输到Device侧,后续推理使用 const char *picturePath = "../data/dog1_1024_683.bin"; LoadPicture(picturePath); // 4.定义一个推理的函数,用于执行推理 Inference(); // 5.定义一个推理结果数据处理的函数,用于在终端上屏显测试图片的top5置信度的类别编号 PrintResult(); // 6.定义一个模型卸载的函数,卸载图片分类的模型 UnloadModel(); // 7.定义一个函数,用于释放内存、销毁推理相关的数据类型,防止内存泄露 UnloadPicture(); // 8.定义一个资源去初始化的函数,用于AscendCL去初始化、运行管理资源释放(释放计算设备) DestroyResource(); }
了解总体的代码逻辑后,接下来开始解析各自定义函数的实现。
- include依赖的头文件,包括AscendCL、C或C++标准库的头文件。
1 2 3 4 5 6 7 8
#include "acl/acl.h" #include <iostream> #include <fstream> #include <cstring> #include <map> #include <math.h> using namespace std;
- 资源初始化。
资源初始化包括2部分:
- 调用aclInit接口初始化AscendCL:
使用AscendCL接口开发应用时,必须先初始化AscendCL ,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。
在初始化时,还支持跟推理相关的可配置项(例如,性能相关的采集信息配置),以json格式的配置文件传入AscendCL初始化接口。如果当前的默认配置已满足需求(例如,默认不开启性能相关的采集信息配置),无需修改,可向AscendCL初始化接口中传入nullptr。
- 调用aclrtSetDevice接口指定计算设备。
1 2 3 4 5 6
int32_t deviceId = 0; void InitResource() { aclError ret = aclInit(nullptr); ret = aclrtSetDevice(deviceId); }
有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需执行资源去初始化,请参见10。
- 调用aclInit接口初始化AscendCL:
- 模型加载。此处加载的是*.om模型文件,如果将ONNX框架的ResNet-50模型文件转换为*.om模型文件,请单击Link,查看README.md中的“准备模型”。
1 2 3 4 5
uint32_t modelId; void LoadModel(const char* modelPath) { aclError ret = aclmdlLoadFromFile(modelPath, &modelId); }
有加载就有卸载,模型推理结束后,需要卸载模型,请参见8。
- 将测试图片读入内存,再传输到Device侧,供推理使用。
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
size_t pictureDataSize = 0; void *pictureHostData; void *pictureDeviceData; //申请内存,使用C/C++标准库的函数将测试图片读入内存 void ReadPictureTotHost(const char *picturePath) { string fileName = picturePath; ifstream binFile(fileName, ifstream::binary); binFile.seekg(0, binFile.end); pictureDataSize = binFile.tellg(); binFile.seekg(0, binFile.beg); aclError ret = aclrtMallocHost(&pictureHostData, pictureDataSize); binFile.read((char*)pictureHostData, pictureDataSize); binFile.close(); } //申请Device侧的内存,再以内存复制的方式将内存中的图片数据传输到Device void CopyDataFromHostToDevice() { aclError ret = aclrtMalloc(&pictureDeviceData, pictureDataSize, ACL_MEM_MALLOC_HUGE_FIRST); ret = aclrtMemcpy(pictureDeviceData, pictureDataSize, pictureHostData, pictureDataSize, ACL_MEMCPY_HOST_TO_DEVICE); } void LoadPicture(const char* picturePath) { ReadPictureTotHost(picturePath); CopyDataFromHostToDevice(); }
- 执行推理。
在调用AscendCL接口进行模型推理时,模型推理有输入、输出数据,输入、输出数据需要按照AscendCL规定的数据类型存放。相关数据类型如下:
- 使用aclmdlDesc类型的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。
模型加载成功后,用户可根据模型的ID,调用该数据类型下的操作接口获取该模型的描述信息,进而从模型的描述信息中获取模型输入/输出的个数、内存大小、维度信息、Format、数据类型等信息。
- 使用aclDataBuffer类型的数据来描述每个输入/输出的内存地址、内存大小。
调用aclDataBuffer类型下的操作接口获取内存地址、内存大小等,便于向内存中存放输入数据、获取输出数据。
- 使用aclmdlDataset类型的数据描述模型的输入/输出数据。
模型可能存在多个输入、多个输出,调用aclmdlDataset类型的操作接口添加多个aclDataBuffer类型的数据。
图3 aclmdlDataset类型与aclDataBuffer类型的关系
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
aclmdlDataset *inputDataSet; aclDataBuffer *inputDataBuffer; aclmdlDataset *outputDataSet; aclDataBuffer *outputDataBuffer; aclmdlDesc *modelDesc; size_t outputDataSize = 0; void *outputDeviceData; // 准备模型推理的输入数据结构 void CreateModelInput() { // 创建aclmdlDataset类型的数据,描述模型推理的输入 inputDataSet = aclmdlCreateDataset(); inputDataBuffer = aclCreateDataBuffer(pictureDeviceData, pictureDataSize); aclError ret = aclmdlAddDatasetBuffer(inputDataSet, inputDataBuffer); } // 准备模型推理的输出数据结构 void CreateModelOutput() { // 创建模型描述信息 modelDesc = aclmdlCreateDesc(); aclError ret = aclmdlGetDesc(modelDesc, modelId); // 创建aclmdlDataset类型的数据,描述模型推理的输出 outputDataSet = aclmdlCreateDataset(); // 获取模型输出数据需占用的内存大小,单位为Byte outputDataSize = aclmdlGetOutputSizeByIndex(modelDesc, 0); // 申请输出内存 ret = aclrtMalloc(&outputDeviceData, outputDataSize, ACL_MEM_MALLOC_HUGE_FIRST); outputDataBuffer = aclCreateDataBuffer(outputDeviceData, outputDataSize); ret = aclmdlAddDatasetBuffer(outputDataSet, outputDataBuffer); } // 执行模型 void Inference() { CreateModelInput(); CreateModelOutput(); aclError ret = aclmdlExecute(modelId, inputDataSet, outputDataSet); }
- 使用aclmdlDesc类型的数据描述模型基本信息(例如输入/输出的个数、名称、数据类型、Format、维度信息等)。
- 处理模型推理的结果数据,在屏幕上显示图片的top5置信度的类别编号。
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
void *outputHostData; void PrintResult() { // 获取推理结果数据 aclError ret = aclrtMallocHost(&outputHostData, outputDataSize); ret = aclrtMemcpy(outputHostData, outputDataSize, outputDeviceData, outputDataSize, ACL_MEMCPY_DEVICE_TO_HOST); // 将内存中的数据转换为float类型 float* outFloatData = reinterpret_cast<float *>(outputHostData); // 屏显测试图片的top5置信度的类别编号 map<float, unsigned int, greater<float>> resultMap; for (unsigned int j = 0; j < outputDataSize / sizeof(float);++j) { resultMap[*outFloatData] = j; outFloatData++; } // do data processing with softmax and print top 5 classes double totalValue=0.0; for (auto it = resultMap.begin(); it != resultMap.end(); ++it) { totalValue += exp(it->first); } int cnt = 0; for (auto it = resultMap.begin();it != resultMap.end();++it) { if(++cnt > 5) { break; } printf("top %d: index[%d] value[%lf] \n", cnt, it->second, exp(it->first) /totalValue); } }
- 卸载模型,并释放模型描述信息。推理结束,需及时释放模型描述信息、卸载模型。
1 2 3 4 5 6 7
void UnloadModel() { // 释放模型描述信息 aclmdlDestroyDesc(modelDesc); // 卸载模型 aclmdlUnload(modelId); }
- 释放内存、销毁推理相关的数据类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
void UnloadPicture() { aclError ret = aclrtFreeHost(pictureHostData); pictureHostData = nullptr; ret = aclrtFree(pictureDeviceData); pictureDeviceData = nullptr; aclDestroyDataBuffer(inputDataBuffer); inputDataBuffer = nullptr; aclmdlDestroyDataset(inputDataSet); inputDataSet = nullptr; ret = aclrtFreeHost(outputHostData); outputHostData = nullptr; ret = aclrtFree(outputDeviceData); outputDeviceData = nullptr; aclDestroyDataBuffer(outputDataBuffer); outputDataBuffer = nullptr; aclmdlDestroyDataset(outputDataSet); outputDataSet = nullptr; }
- 资源释放。在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用如下接口实现计算设备释放,AscendCL去初始化。
1 2 3 4 5
void DestroyResource() { aclError ret = aclrtResetDevice(deviceId); aclFinalize(); }