基於昇騰計算語言AscendCL開發AI推理應用

2023-02-13 18:02:10
摘要:本文介紹了昇騰計算語言AscendCL的基本概念,並以範例程式碼的形式介紹瞭如何基於AscendCL開發AI推理應用,最後配以實際的操作演示說明如何編譯執行應用。

本文分享自華為雲社群《基於昇騰計算語言AscendCL開發AI推理應用》,作者:昇騰CANN。

初始AscendCL

AscendCL(Ascend Computing Language,昇騰計算語言)是昇騰計算開放程式設計框架,是對底層昇騰計算服務介面的封裝,它提供執行時資源(例如裝置、記憶體等)管理、模型載入與執行、運算元載入與執行、圖片資料編解碼/裁剪/縮放處理等API庫,實現在昇騰CANN平臺上進行深度學習推理計算、圖形影象預處理、單運算元加速計算等能力。簡單來說,就是統一的API框架,實現對所有資源的呼叫。

如何基於AscendCL開發推理應用

首先,我們得先了解下,使用AscendCL時,經常會提到的「資料型別的操作介面」 ,這是什麼呢?為啥會存在?

在C/C++中,對使用者開放的資料型別通常以Struct結構體方式定義、以宣告變數的方式使用,但這種方式一旦結構體要增加成員引數,使用者的程式碼就涉及相容性問題,不便於維護,因此AscendCL對使用者開放的資料型別,均以介面的方式操作該資料型別,例如,呼叫某個資料型別的Create介面建立該資料型別、呼叫Get介面獲取資料型別內引數值、呼叫Set介面設定資料型別內的引數值、呼叫Destroy介面銷燬該資料型別,使用者無需關注定義資料型別的結構體長什麼樣,這樣即使後續資料型別需擴充套件,只需增加該資料型別的操作介面即可,也不會引起相容性問題。

所以,總結下,「資料型別的操作介面」就是建立資料型別、Get/Set資料型別中的引數值、銷燬資料型別的一系列介面,存在的最大好處就是減少相容性問題。

接下來,進入我們今天的主題,怎麼用AscendCL的介面開發網路模型推理場景下的應用。看完本文介紹的關鍵知識點,也可以到 「昇騰檔案中心[1]」查閱詳細的檔案介紹。

AscendCL初始化與去初始化

使用AscendCL介面開發應用時,必須先初始化AscendCL ,否則可能會導致後續系統內部資源初始化出錯,進而導致其它業務異常。在初始化時,還支援以下跟推理相關的設定項(例如,效能相關的採集資訊設定),以json格式的組態檔傳入AscendCL初始化介面。如果當前的預設設定已滿足需求(例如,預設不開啟效能相關的採集資訊設定),無需修改,可向AscendCL初始化介面中傳入NULL,或者可將組態檔設定為空json串(即組態檔中只有{})。

有初始化就有去初始化,在確定完成了AscendCL的所有呼叫之後,或者程序退出之前,需呼叫AscendCL介面實現AscendCL去初始化。

// 此處以虛擬碼的形式展示介面的呼叫流程
// 初始化
// 此處的..表示相對路徑,相對可執行檔案所在的目錄,例如,編譯出來的可執行檔案存放在out目錄下,此處的..就表示out目錄的上一級目錄
const char *aclConfigPath = "../src/acl.json";
aclError ret = aclInit(aclConfigPath);
// ......
// 去初始化
ret = aclFinalize();

執行管理資源申請與釋放

執行管理資源包括Device、Context、Stream、Event等,此處重點介紹Device、Context、Stream,其基本概念如下圖所示 。

您需要按順序依次申請如下執行管理資源:Device、Context、Stream,確保可以使用這些資源執行運算、管理任務。所有資料處理都結束後,需要按順序依次釋放執行管理資源:Stream、Context、Device

在申請執行管理資源時,Context、Stream支援隱式建立和顯式建立兩種申請方式。

// 此處以虛擬碼的形式展示介面的呼叫流程,以顯式建立Context和Stream為例
// 執行管理資源申請
// 1、指定運算的Device
aclError ret = aclrtSetDevice(deviceId);
// 2、顯式建立一個Context,用於管理Stream物件
ret = aclrtCreateContext(context, deviceId);
// 3、顯式建立一個Stream,用於維護一些非同步操作的執行順序,確保按照應用程式中的程式碼呼叫順序執行任務
ret = aclrtCreateStream(stream);
//......
// 執行管理資源釋放
// 1、銷燬Stream
ret = aclrtDestroyStream(stream);
// 2、銷燬Context
ret = aclrtDestroyContext(context);
// 3、釋放Device資源
ret = aclrtResetDevice(deviceId);
//......

媒體資料處理

如果模型對輸入圖片的寬高要求與使用者提供的源圖不一致,AscendCL提供了媒體資料處理的介面,可實現摳圖、縮放、格式轉換、視訊或圖片的編解碼等,將源圖裁剪成符合模型的要求。後續期刊中會展開說明這個功能,本期著重介紹模型推理的部分,以輸入圖片滿足模型的要求為例。

模型載入

模型推理場景下,必須要有適配昇騰AI處理器的離線模型(*.om檔案),我們可以使用ATC(Ascend Tensor Compiler)來構建模型。如果模型推理涉及動態Batch、動態解析度等特性,需在構建模型增加相關設定。關於如何使用ATC來構建模型,請參見「昇騰檔案中心[1]」。

有了模型,就可以開始載入了,當前AscendCL支援以下幾種方式載入模型:

  • 從*.om檔案中載入模型資料,由AscendCL管理記憶體
  • 從*.om檔案中載入模型資料,由使用者自行管理記憶體
  • 從記憶體中載入模型資料,由AscendCL管理記憶體
  • 從記憶體中載入模型資料,由使用者自行管理記憶體

由使用者自行管理記憶體時,需關注工作記憶體、權值記憶體。工作記憶體用於存放模型執行過程中的臨時資料,權值記憶體用於存放權值資料。這個時候,是不是有疑問了,我怎麼知道工作記憶體、權值記憶體需要多大?不用擔心,AscendCL不僅提供了載入模型的介面,同時也提供了「根據模型檔案獲取模型執行時所需的工作記憶體和權值記憶體大小」的介面,方便使用者使用 。

// 此處以虛擬碼的形式展示介面的呼叫流程,以「由使用者管理記憶體」為例
// 1.根據om模型檔案獲取模型執行時所需的權值記憶體大小、工作記憶體大小。
aclError ret = aclmdlQuerySize(omModelPath, &modelWorkSize,
 &modelWeightSize);
// 2.根據工作記憶體大小,申請Device上模型執行的工作記憶體。
ret = aclrtMalloc(&modelWorkPtr, modelWorkSize, 
          ACL_MEM_MALLOC_HUGE_FIRST);
// 3.根據權值記憶體的大小,申請Device上模型執行的權值記憶體。
ret = aclrtMalloc(&modelWeightPtr, modelWeightSize, 
          ACL_MEM_MALLOC_HUGE_FIRST);
// 4.以從om模型檔案載入模型、由使用者管理工作記憶體和權值記憶體為例
// 模型載入成功,返回標識模型的ID。
ret = aclmdlLoadFromFileWithMem(modelPath, &modelId, modelWorkPtr, 
 modelWorkSize, modelWeightPtr, 
modelWeightSize);

模型執行

在呼叫AscendCL介面進行模型推理時,模型推理有輸入、輸出資料,輸入、輸出資料需要按照AscendCL規定的資料型別存放。相關資料型別如下:

  • 使用aclmdlDesc型別的資料描述模型基本資訊(例如輸入/輸出的個數、名稱、資料型別、Format、維度資訊等)。

模型載入成功後,使用者可根據模型的ID,呼叫該資料型別下的操作介面獲取該模型的描述資訊,進而從模型的描述資訊中獲取模型輸入/輸出的個數、記憶體大小、維度資訊、Format、資料型別等資訊。

  • 使用aclDataBuffer型別的資料來描述每個輸入/輸出的記憶體地址、記憶體大小。

呼叫aclDataBuffer型別下的操作介面獲取記憶體地址、記憶體大小等,便於向記憶體中存放輸入資料、獲取輸出資料。

  • 使用aclmdlDataset型別的資料描述模型的輸入/輸出資料。

模型可能存在多個輸入、多個輸出,呼叫aclmdlDataset型別的操作介面新增多個aclDataBuffer型別的資料。

// 此處以虛擬碼的形式展示如何準備模型的輸入、輸出資料結構
// 1.根據載入成功的模型的ID,獲取該模型的描述資訊
aclmdlDesc *modelDesc = aclmdlCreateDesc();
aclError ret = aclmdlGetDesc(modelDesc, modelId);
// 2.準備模型推理的輸入資料結構
// (1)申請輸入記憶體
// 當前範例程式碼中的模型只有一個輸入,所以index為0,如果模型有多個輸入,則需要先呼叫aclmdlGetNumInputs介面獲取模型輸入的數量
void *modelInputBuffer = nullptr;
size_t modelInputSize = aclmdlGetInputSizeByIndex(modelDesc, 0);
ret = aclrtMalloc(&modelInputBuffer, modelInputSize,                                              ACL_MEM_MALLOC_NORMAL_ONLY);
// (2)準備模型的輸入資料結構
// 建立aclmdlDataset型別的資料,描述模型推理的輸入
aclmdlDataset *input = aclmdlCreateDataset();
aclDataBuffer *inputData = aclCreateDataBuffer(modelInputBuffer, modelInputSize);
ret = aclmdlAddDatasetBuffer(input, inputData);
// 3.準備模型推理的輸出資料結構
// (1)建立aclmdlDataset型別的資料output,描述模型推理的輸出
aclmdlDataset *output = aclmdlCreateDataset();
// (2)獲取模型的輸出個數.
size_t outputSize = aclmdlGetNumOutputs(modelDesc);
// (3)迴圈為每個輸出申請記憶體,並將每個輸出新增到aclmdlDataset型別的資料中
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);
aclDataBuffer *outputData = aclCreateDataBuffer(outputBuffer, buffer_size); 
ret = aclmdlAddDatasetBuffer(output, outputData);
}

準備好模型執行所需的輸入和輸出資料型別、且存放好模型執行的輸入資料後,可以執行模型推理了,如果模型的輸入涉及動態Batch、動態解析度等特性,則在模型執行前,還需要呼叫AscendCL介面告訴模型本次執行時需要用的Batch數、解析度等。

當前AscendCL支援同步模型執行、非同步模型執行兩種方式,這裡說的同步、非同步是站在呼叫者和執行者的角度。

  • 若呼叫模型執行的介面後需等待推理完成再返回,則表示模型執行是同步的。當用戶呼叫同步模型執行介面後,可直接從該介面的輸出引數中獲取模型執行的結果資料,如果需要推理的輸入資料量很大,同步模型執行時,需要等所有資料都處理完成後,才能獲取推理的結果資料。
  • 若呼叫模型執行的介面後不等待推理完成完成再返回,則表示模型執行是非同步的。當用戶呼叫非同步模型執行介面時,需指定Stream(Stream用於維護一些非同步操作的執行順序,確保按照應用程式中的程式碼呼叫順序在Device上執行),另外,還需呼叫aclrtSynchronizeStream介面阻塞程式執行,直到指定Stream中的所有任務都完成,才可以獲取推理的結果資料。如果需要推理的輸入資料量很大,非同步模型執行時,AscendCL提供了Callback機制,觸發回撥函數,在指定時間內一旦有推理的結果資料,就獲取出來,達到分批獲取推理結果資料的目的,提高效率。
// 此處以虛擬碼的形式展示同步模型執行的過程
// 1. 由使用者自行編碼,將模型所需的輸入資料讀入記憶體
// 如果模型推理之前先進行媒體資料處理,則此處可以將媒體資料處理後的輸出內容作為模型推理的輸入記憶體,
// ......
// 2. 執行模型推理
// modelId表示模型ID,在模型載入成功後,會返回標識模型的ID
// input、output分別表示模型推理的輸入、輸出資料,在準備模型推理的輸入、輸出資料結構時已定義
aclError ret = aclmdlExecute(modelId, input, output)
// 3. 處理模型推理的輸出資料
for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(output); ++i) {
//獲取每個輸出的記憶體地址和記憶體大小
aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(output, i);
void* data = aclGetDataBufferAddr(dataBuffer);
size_t len = aclGetDataBufferSizeV2(dataBuffer);
//獲取到輸出資料後,由使用者自行編碼,處理輸出資料
//......
}
// 4.銷燬模型輸入、輸出資料結構
// 釋放輸入資源,包括資料結構和記憶體
(void)aclDestroyDataBuffer(dataBuffer);
(void)aclmdlDestroyDataset(mdlDataset);
// 5.釋放記憶體資源,防止記憶體洩露
// ......

推理結束後,如果需要獲取並進一步處理推理結果資料,則由使用者自行編碼實現。最後,別忘了,我們還要銷燬aclmdlDataset、aclDataBuffer等資料型別,釋放相關記憶體,防止記憶體洩露。

模型解除安裝

在模型推理結束後,還需要通過aclmdlUnload介面解除安裝模型,並銷燬aclmdlDesc型別的模型描述資訊、釋放模型執行的工作記憶體和權值記憶體。

// 此處以虛擬碼的形式展示模型解除安裝的過程
// 1. 解除安裝模型
aclError ret = aclmdlUnload(modelId);
// 2. 釋放模型描述資訊
(void)aclmdlDestroyDesc(modelDesc);
// 3. 釋放模型執行的工作記憶體和權值記憶體
(void)aclrtFree(modelWorkPtr);
(void)aclrtFree(modelWeightPtr);

以上就是基於AscendCL開發基礎推理應用的相關知識點,您也可以在「昇騰社群線上課程[2]」板塊學習視訊課程,學習過程中的任何疑問,都可以在「昇騰論壇[3]」互動交流!

是不是意猶未盡,想自己操作一把呢,來吧!您可以從昇騰CANN樣例倉獲取該樣例以及詳細的使用說明。

更多介紹

[1]昇騰檔案中心:https://www.hiascend.com/zh/document

[2]昇騰社群線上課程:https://www.hiascend.com/zh/edu/courses

[3]昇騰論壇:https://www.hiascend.com/forum

 

點選關注,第一時間瞭解華為雲新鮮技術~