PS:要轉載請注明出處,本人版權所有。
PS: 這個只是基於《我自己》的理解,
如果和你的原則及想法相沖突,請諒解,勿噴。
前置說明
本文作為本人csdn blog的主站的備份。(BlogID=114)
環境說明
- MLU220 開發板
- Ubuntu18.04 + MLU270開發主機一台
- aarch64-linux-gnu-gcc 6.x 交叉編譯環境
前言
閱讀本文前,請務必須知以下前置文章概念:
- 《寒武紀加速平台(MLU200系列) 摸魚指南(一)--- 基本概念及相關介紹》 ( https://blog.csdn.net/u011728480/article/details/121194076 )
- 《寒武紀加速平台(MLU200系列) 摸魚指南(二)--- 模型移植-環境搭建》 ( https://blog.csdn.net/u011728480/article/details/121320982 )
- 《寒武紀加速平台(MLU200系列) 摸魚指南(三)--- 模型移植-分割網絡實例》 ( https://blog.csdn.net/u011728480/article/details/121456789 )
這里再次回顧一下《寒武紀加速平台(MLU200系列) 摸魚指南(一)--- 基本概念及相關介紹》的內容,寒武紀加速卡是一個硬件平台,是最底層的存在,在硬件平台之上,有驅動,在驅動之上,有運行時。本文的離線模型就是基於運行時的api進行調用和處理,得到模型結果。
本文作為本系列的終篇,將會從上文的離線模型開始,從頭開始搭建我們的離線模型推理代碼結構,經過合理的部署,能夠在MLU220開發板正常的運行。
若文中引用部分存在侵權,請及時聯系我刪除。
離線模型推理前置須知
經過前文相關的介紹,我們可以知道我們主要調用的是運行時相關的api。這里存在兩套api可以使用,一個是cnrt,一個是easydk。easydk是基於cnrt封裝的api,大大簡化了離線模型推理的開發流程。但是我們的開發主線是一致的,就是初始化mlu設備,加載模型,預處理,模型推理,后處理,處理結果。
此外,寒武紀還提供了CNStream程序框架,基於EasyDk開發,以pipeline+observer的方式,提供了一個簡單易用的框架,如果有興趣,請查看其官網 https://github.com/Cambricon/CNStream 。
我們其實要用的是EasyDK+CNRT的這種開發方式,構造一個類似CNStream這樣的程序。
EasyDK簡介與編譯
首先其官網是:https://github.com/Cambricon/easydk。
其除了CNToolKit的依賴(neuware)之外,還依賴於glog,gflags,opencv,這些需要提前安裝好。至於CNToolKit x86的版本的相關介紹已經在模型移植環境部分介紹過了。
由於我們要開發邊緣端離線模型推理程序,一般來說,我們主要還是使用到EasyDK里面的EasyInfer部分的內容。其編譯流程的話就如其官方介紹:
- cmake ..
- make
如果熟悉cmake的編譯流程的話,其實就知道上面的流程是非常普遍的。對於EasyDK的編譯來說,在x86下面進行編譯是很簡單的,但是如果要進行交叉編譯的話,最好只生成核心庫,其他的sample和test都關閉。
完成離線模型在MLU270上的運行
還記得在《寒武紀加速平台(MLU200系列) 摸魚指南(三)--- 模型移植-分割網絡實例》一文中,我們可以看到,通過torch_mlu.core.mlu_model.set_core_version('MLU220'),我們可以調整生成的離線模型運行的平台,可以是邊緣端MLU220,可以是服務器部署端的MLU270。對的,沒有看錯,MLU270既可以做模型移植,也可以做離線模型的部署,就如同本系列的開篇所講,這幾個平台僅僅是部署場景的差異。
為什么我們需要在MLU270上調試離線模型呢?因為方便。因為MLU220一般來說都需要交叉編譯,特別是相關的依賴庫的生成比較麻煩,如果MLU220的板卡也方便調試的話,那也可以直接基於MLU220進行開發和調試。
由於我這邊是EasyDK+CNRT混合調用的形式,因此我主要根據其官方的CNRT推理代碼進行注釋和介紹,並在相關位置標注哪些內容是EasyDK可以一步到位的。這里簡單的回答幾個疑問。為什么需要EasyDK這個庫呢?為什么不直接基於CNRT進行開發?我的回答是根據項目推進的需要做出的選擇。關於這種程序的最終形態,其實我還是期待直接基於CNRT開發,因為其有效,但是代價是你必須很熟悉的了解相關的內容。
下面是官方推理代碼介紹(可能會在順序上做一些變更,其官方代碼未處理一些返回值,自己編寫的時候需要注意,此外,官方的代碼有些遺漏的地方,我進行了修改):
/* Copyright (C) [2019] by Cambricon, Inc. */
/* offline_test */
/*
* A test which shows how to load and run an offline model.
* This test consists of one operation --mlp.
*
* This example is used for MLU270 and MLU220.
*
*/
#include "cnrt.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int offline_test(const char *name) {
// This example is used for MLU270 and MLU220. You need to choose the corresponding offline model.
// when generating an offline model, u need cnml and cnrt both
// when running an offline model, u need cnrt only
// 第一步,你必須調用cnrtInit()初始化運行時環境
cnrtInit(0);
// 第二步, 就是檢查MLU設備,並設定當前設備,這一步其實是為多設備推理准備的
cnrtDev_t dev;
cnrtGetDeviceHandle(&dev, 0);
cnrtSetCurrentDevice(dev);
// 這里的第一二步其實可能創建一個EasyInfer對象就完成了,而且具備了完整的錯誤檢查。
//
// prepare model name
// load model
// 第三步,就是加載模型
cnrtModel_t model;
cnrtLoadModel(&model, "test.cambricon");
// get model total memory
// 第四步,就是獲取模型的一些屬性,比如內存占用,並行性這些,這一步是可選的。
int64_t totalMem;
cnrtGetModelMemUsed(model, &totalMem);
printf("total memory used: %ld Bytes\n", totalMem);
// get model parallelism
int model_parallelism;
cnrtQueryModelParallelism(model, &model_parallelism);
printf("model parallelism: %d.\n", model_parallelism);
// 第五步,是建立推理邏輯流,這一步就是根據離線模型,在內存中動態生成模型推理結構。這里的"subnet0"是一個 kernel-func 名字,非固定,但是常見的名字都是subnet0,
// 在.cambricon_twins配套文件中有相關定義。
// 他這個和cuda編程其實比較類似,我們剛剛生成的模型推理結構其實被當做一個內核函數,函數名就是這個。
// load extract function
cnrtFunction_t function;
cnrtCreateFunction(&function);
cnrtExtractFunction(&function, model, "subnet0");
// 第六步,獲取輸入輸出節點個數和輸入輸出數據size。注意許多網絡中有多個輸入,也可能有多個輸出。在次注意這個概念,后續的操作都在多個輸入和多個輸出的基礎上設計的。
int inputNum, outputNum;
int64_t *inputSizeS, *outputSizeS;
cnrtGetInputDataSize(&inputSizeS, &inputNum, function);
cnrtGetOutputDataSize(&outputSizeS, &outputNum, function);
// prepare data on cpu
// 第7步,申請cpu上的輸入輸出內存,這里是二維指針哈,代表多個輸入輸出
void **inputCpuPtrS = (void **)malloc(inputNum * sizeof(void *));
void **outputCpuPtrS = (void **)malloc(outputNum * sizeof(void *));
// allocate I/O data memory on MLU
// 第8步,申請mlu上的輸入輸出內存,這里是二維指針哈,代表多個輸入輸出。
// 此時還未真正申請數據內存,只是申請了數據節點的句柄(指針)
void **inputMluPtrS = (void **)malloc(inputNum * sizeof(void *));
void **outputMluPtrS = (void **)malloc(outputNum * sizeof(void *));
// prepare input buffer
// 第9步,分別對第8步的數據節點句柄申請真正的內存空間。這里的inputCpuPtrS和inputMluPtrS是一一對應的,這是內存地址不一樣,內存管理者不一樣。對輸出也是同理。
// malloc是標准c申請堆內存接口
// cnrtMalloc是申請mlu所管理的內存接口,類比cuda的話,可以直接理解為申請顯存
for (int i = 0; i < inputNum; i++) {
// converts data format when using new interface model
inputCpuPtrS[i] = malloc(inputSizeS[i]);
// malloc mlu memory
cnrtMalloc(&(inputMluPtrS[i]), inputSizeS[i]);
cnrtMemcpy(inputMluPtrS[i], inputCpuPtrS[i], inputSizeS[i], CNRT_MEM_TRANS_DIR_HOST2DEV);
}
// prepare output buffer
for (int i = 0; i < outputNum; i++) {
outputCpuPtrS[i] = malloc(outputSizeS[i]);
// malloc mlu memory
cnrtMalloc(&(outputMluPtrS[i]), outputSizeS[i]);
}
// 第10步,首先將預處理好的圖像數據填充inputCpuPtrS, 拷貝cpu輸入數據內存到mlu輸入數據內存
cv::Mat _in_img;
_in_img = cv::imread("test.jpg");
_in_img.convertTo(_in_img, CV_32FC3);
for (int i = 0; i < inputNum; i++) {
// 注意這里的memcpy是我添加的,一般來說,模型的數據輸入都是fp16或者fp32,但是一般我們的opencv生成的uint8。需要轉換成fp32,或者fp16。
// 重要是事情發3次,注意圖片的預處理后的數據格式以及模型輸入的數據格式,不同的話,需要經過轉換,官方提供了cnrtCastDataType來輔助轉換過程。
// 重要是事情發3次,注意圖片的預處理后的數據格式以及模型輸入的數據格式,不同的話,需要經過轉換,官方提供了cnrtCastDataType來輔助轉換過程。
// 重要是事情發3次,注意圖片的預處理后的數據格式以及模型輸入的數據格式,不同的話,需要經過轉換,官方提供了cnrtCastDataType來輔助轉換過程。
::memcpy( inputCpuPtrS[i], _in_img.data, inputSizeS[i]);
cnrtMemcpy(inputMluPtrS[i], inputCpuPtrS[i], inputSizeS[i], CNRT_MEM_TRANS_DIR_HOST2DEV);
}
// 第10步,主要就是圖像預處理,將圖像數據傳輸到mlu上面去。
// 第11步,主要是開始設置推理參數
// prepare parameters for cnrtInvokeRuntimeContext
void **param = (void **)malloc(sizeof(void *) * (inputNum + outputNum));
for (int i = 0; i < inputNum; ++i) {
param[i] = inputMluPtrS[i];
}
for (int i = 0; i < outputNum; ++i) {
param[inputNum + i] = outputMluPtrS[i];
}
// 第12步,綁定設備和設置推理context
// setup runtime ctx
cnrtRuntimeContext_t ctx;
cnrtCreateRuntimeContext(&ctx, function, NULL);
// compute offline
cnrtQueue_t queue;
cnrtRuntimeContextCreateQueue(ctx, &queue);
// bind device
cnrtSetRuntimeContextDeviceId(ctx, 0);
cnrtInitRuntimeContext(ctx, NULL);
// 第13步,推理並等待推理結束。
// invoke
cnrtInvokeRuntimeContext(ctx, param, queue, NULL);
// sync
cnrtSyncQueue(queue);
// 第14步,將數據從mlu拷貝回cpu,然后進行后續的后處理
// copy mlu result to cpu
for (int i = 0; i < outputNum; i++) {
cnrtMemcpy(outputCpuPtrS[i], outputMluPtrS[i], outputSizeS[i], CNRT_MEM_TRANS_DIR_DEV2HOST);
}
// 第15步,清理環境。
// free memory space
for (int i = 0; i < inputNum; i++) {
free(inputCpuPtrS[i]);
cnrtFree(inputMluPtrS[i]);
}
for (int i = 0; i < outputNum; i++) {
free(outputCpuPtrS[i]);
cnrtFree(outputMluPtrS[i]);
}
free(inputCpuPtrS);
free(outputCpuPtrS);
free(param);
cnrtDestroyQueue(queue);
cnrtDestroyRuntimeContext(ctx);
cnrtDestroyFunction(function);
cnrtUnloadModel(model);
cnrtDestroy();
return 0;
}
int main() {
printf("mlp offline test\n");
offline_test("mlp");
return 0;
}
下面我簡單列出一些EasyDK的操作順序:
- 上文的第三四五步其實對應的是EasyInfer下面的ModelLoader模塊,當初始化ModelLoader模塊,並傳參給EasyInfer實例,就完成了三四五步的內容。其實前面這些內容都是固定形式的,並不是重點。重點是后面的數據輸入、推理、數據輸出部分。
- 上文的第6,7,8,9步其實都是在為模型在cpu和mlu上申請相關的內存空間。在EasyDk中有對應的接口直接完成內存申請。
- 注意上文的第10步,是比較重要的一步,包含了圖像數據預處理,到圖像數據類型轉換,再到圖像數據輸入到mlu內存。
- 上文的第11,12步是為推理准備參數
- 上文的第13步開始推理
- 上文的第14步從mlu內存中拷貝出推理結果到cpu內存,然后進行后處理。
- 上文的第15步,清理環境。
離線模型在MLU220上的部署
前一小節,主要還是完成離線模型推理的程序開發,並在MLU270上運行測試。本小節的主要內容是怎么將我們調好的程序部署到MLU220。
部署到MLU220,我們面臨的第一個問題就是交叉編譯生成AARCH64的程序。這里包含3個部分依賴,CNToolkit-aarch64,easydk-aarch64,其他第三方庫如opencv-aarch64等。這時,我們得到了aarch64的離線推理程序,並配合之前我們轉換得到的mlu220版本的離線模型。
當我們把生成好的程序放到mlu220板卡,這個時候,可能程序還是無法跑起來,因為可能驅動未加載,這個時候,建議找到驅動和固件,讓mlu220運行起來。然后運行程序即可。
下面是無mlu設備的報錯示例:

下面是加載驅動的最后日志:

下面是運行程序的輸出:

后記
對於RK3399pro和寒武紀MLU220平台來說,一些出來時間較為長久的模型,由於優化的比較到位,速度相較於RK3399pro可能有個300%+的性能提升。但是對於一些新的模型和一些非經典(非大眾)的模型,由於自帶的優化或者網絡結構的原因,可能只有30%+的性能提升,但是這也是令人高興的事情,畢竟硬件升級之后,好多事情可以達到准實時。
本系列的基本介紹就這些了,完結撒花~~~ ~~~。
參考文獻
- 《寒武紀加速平台(MLU200系列) 摸魚指南(一)--- 基本概念及相關介紹》 ( https://blog.csdn.net/u011728480/article/details/121194076 )
- 《寒武紀加速平台(MLU200系列) 摸魚指南(二)--- 模型移植-環境搭建》 ( https://blog.csdn.net/u011728480/article/details/121320982 )
- 《寒武紀加速平台(MLU200系列) 摸魚指南(三)--- 模型移植-分割網絡實例》 ( https://blog.csdn.net/u011728480/article/details/121456789 )
- https://www.cambricon.com/
- https://www.cambricon.com/docs/cnrt/user_guide_html/example/offline_mode.html
- 其他相關保密資料。

PS: 請尊重原創,不喜勿噴。
PS: 要轉載請注明出處,本人版權所有。
PS: 有問題請留言,看到后我會第一時間回復。