摘要: 本文介紹了CANN自定義運算元開發的幾種開發方式和運算元的編譯執行流程。然後以開發一個DSL Add運算元為例,講解運算元開發的基本流程。
本文分享自華為雲社群《昇騰CANN運算元開發揭秘》,作者:昇騰CANN 。
開發者在利用昇騰硬體進行神經網路模型訓練或者推理的過程中,可能會遇到以下場景:
- 訓練場景下,將第三方框架(例如TensorFlow、PyTorch等)的網路訓練指令碼遷移到昇騰AI處理器時遇到了不支援的運算元。
- 推理場景下,將第三方框架模型(例如TensorFlow、Caffe、ONNX等)使用ATC工具轉換為適配昇騰AI處理器的離線模型時遇到了不支援的運算元。
- 網路調優時,發現某運算元效能較低,影響網路效能,需要重新開發一個高效能運算元替換效能較低的運算元。
- 推理場景下,應用程式中的某些邏輯涉及到數學運算(例如查詢最大值,進行資料型別轉換等),希望透過自定義運算元的方式實現這些邏輯,從而獲得效能提升。
此時我們就需要考慮進行自定義運算元的開發,本期我們主要帶您瞭解CANN自定義運算元的幾種開發方式和基本開發流程,讓您對CANN運算元有宏觀的瞭解。
一、運算元基本概念
相信大家對運算元的概念並不陌生,這裡我們來做簡單回顧。深度學習演算法由一個個計算單元組成,我們稱這些計算單元為運算元(Operator,簡稱OP)。
在網路模型中,運算元對應層中的計算邏輯,例如:卷積層(Convolution Layer)是一個運算元;全連線層(Fully-connected Layer, FC layer)中的權值求和過程,是一個運算元。
再例如:tanh、ReLU等,為在網路模型中被用做啟用函式的運算元。
二、CANN自定義運算元開發方式
學習CANN自定義運算元開發方式之前,我們先來了解一下CANN運算元的執行位置:包括AI Core和AI CPU。
- AI Core是昇騰AI處理器的計算核心,負責執行矩陣、向量、標量計算密集的運算元任務。
- AI CPU負責執行不適合跑在AI Core上的運算元,是AI Core運算元的補充,主要承擔非矩陣類、邏輯比較複雜的分支密集型計算。
CANN支援使用者使用多種方式來開發自定義運算元,包括TBE DSL、TBE TIK、AICPU三種開發方式。其中TBE DSL、TBE TIK運算元執行在AI Core上,AI CPU運算元執行在AI CPU上。
1. 基於TBE開發框架的運算元開發
TBE(Tensor Boost Engine:張量加速引擎)是CANN提供的運算元開發框架,開發者可以基於此框架使用Python語言開發自定義運算元,透過TBE進行運算元開發有TBE DSL、TBE TIK兩種方式。
- TBE DSL(Domain-Specific Language ,基於特性域語言)開發方式
為了方便開發者進行自定義運算元開發,CANN預先提供一些常用運算的排程,封裝成一個個運算介面,稱為基於TBE DSL開發。DSL介面已高度封裝,使用者僅需要使用DSL介面完成計算過程的表達,後續的運算元排程、運算元最佳化及編譯都可透過已有的介面一鍵式完成,適合初級開發使用者。
- TBE TIK(Tensor Iterator Kernel)開發方式
TIK(Tensor Iterator Kernel)是一種基於Python語言的動態程式設計框架,呈現為一個Python模組,執行於Host CPU上。開發者可以透過呼叫TIK提供的API基於Python語言編寫自定義運算元,TIK編譯器會將其編譯為昇騰AI處理器應用程式的二進位制檔案。
TIK需要使用者手工控制資料搬運和計算流程,入門較高,但開發方式比較靈活,能夠充分挖掘硬體能力,在效能上有一定的優勢。
2. AI CPU運算元開發方式
AI CPU運算元的開發介面即為原生C++介面,具備C++程式開發能力的開發者能夠較容易的開發出AI CPU運算元。AI CPU運算元在AI CPU上執行。
下面的開發方式一覽表,對上述幾種開發方式作對比說明,您可以根據各種開發方式的適用場景選擇適合的開發方式。
三、CANN運算元編譯執行
運算元構成
一個完整的CANN運算元包含四部分:運算元原型定義、對應開源框架的運算元適配外掛、運算元資訊庫和運算元實現。這四個組成部分會在運算元編譯執行的過程中使用。
運算元編譯
推理場景下,進行模型推理前,我們需要使用ATC模型轉換工具將原始網路模型轉換為適配昇騰AI處理器的離線模型,該過程中會對網路中的運算元進行編譯。
訓練場景下,當我們跑訓練指令碼時,CANN內部實現邏輯會先將開源框架網路模型下發給Graph Engine進行圖編譯,該過程中會對網路中的運算元進行編譯。
CANN運算元的編譯邏輯架構如下:
具體的CANN運算元編譯流程如下,在編譯流程中會用到上文提到的運算元的四個組成部分。
- Graph Engine呼叫運算元外掛,將原始網路模型中的運算元對映為適配昇騰AI處理器的運算元,從而將原始開源框架圖解析為適配昇騰AI處理器的圖。
- 呼叫運算元原型庫校驗介面進行基本引數的校驗,校驗透過後,會根據原型庫中的推導函式推導每個節點的輸出shape與dtype,進行輸出tensor的靜態記憶體的分配。
- Graph Engine根據圖中資料將圖拆分為子圖並下發給FE。FE在處理過程中根據運算元資訊庫中運算元資訊找到運算元實現,將其編譯成運算元kernel,最後將最佳化後子圖返回給Graph Engine。
- Graph Engine進行圖編譯,包含記憶體分配、流資源分配等,並向FE傳送tasking請求,FE返回運算元的taskinfo資訊給Graph Engine,圖編譯完成後生成適配昇騰AI處理器的模型。
運算元執行
推理場景下,使用ATC模型轉換工具將原始網路模型轉換為適配昇騰AI處理器的離線模型後,開發AscendCL應用程式,載入轉換好的離線模型檔案進行模型推理,該過程中會進行運算元的呼叫執行。
訓練場景下,當我們跑訓練指令碼時,內部實現邏輯將開源框架網路模型下發給Graph Engine進行圖編譯後,後續的訓練流程會進行運算元的呼叫執行。
CANN運算元的執行邏輯架構如下:
具體流程如下,首先Graph Engine下發運算元執行請求給Runtime,然後Runtime會判斷運算元的Task型別,若是TBE運算元,則將運算元執行請求下發到AI Core上執行;若是AI CPU運算元,則將運算元執行請求下發到AI CPU上執行。
四、運算元開發流程
本章節以透過DSL開發方式開發一個Add運算元為例,帶您快速體驗CANN運算元開發的流程。流程圖如下:
運算元開發準備
環境準備:準備運算元開發及執行驗證所依賴的開發環境與執行環境。工程建立:建立運算元開發工程,有以下幾種實現方式:
- 基於MindStudio工具進行運算元開發,直接使用MindStudio工具建立運算元工程,會自動生成運算元工程及程式碼模板。
- 基於msopgen工具進行開發,會自動生成運算元工程及程式碼模板。
- 基於自定義運算元樣例工程進行開發,開發者需要自己建立運算元相關實現檔案,或者基於已有樣例進行修改。
下面以msopgen工具建立運算元開發工程為例進行介紹:
定義AddDSL運算元的原型定義json檔案,用於生成AddDSL的運算元開發工程。例如,定義的json檔案的名字為add_dsl.json,儲存路徑為:$HOME/sample,檔案內容如下:
[ { "op":"AddDSL", "input_desc":[ { "name":"x1", "param_type":"required", "format":[ "NCHW" ], "type":[ "fp16" ] }, { "name":"x2", "param_type":"required", "format":[ "NCHW" ], "type":[ "fp16" ] } ], "output_desc":[ { "name":"y", "param_type":"required", "format":[ "NCHW" ], "type":[ "fp16" ] } ] } ]
使用msopgen工具生成AddDSL運算元的開發工程。
$HOME/Ascend/ascend-toolkit/latest/python/site-packages/bin/msopgen gen -i $HOME/sample/add_dsl.json -f tf -c ai_core-Ascend310 -out $HOME/sample/AddDsl
“$HOME/Ascend”為CANN軟體安裝目錄;
“-f tf”引數代表選擇的原始框架為TensorFlow;
“ai_core-<soc_version>”代表運算元在AI Core上執行,<soc_version>為昇騰AI處理器的型號。
此命令執行完後,會在$HOME/sample/AddDsl目錄下生成運算元工程,工程中包含各交付件的模板檔案,編譯指令碼等,如下所示:
AddDsl ├── build.sh // 編譯入口指令碼 ├── cmake // 編譯解析指令碼存放目錄 ├── CMakeLists.txt ├── framework // 運算元適配外掛相關檔案存放目錄 │ ├── CMakeLists.txt │ └── tf_plugin │ ├── CMakeLists.txt │ └── tensorflow_add_dsl_plugin.cc // 運算元適配外掛實現檔案 ├── op_proto // 運算元原型定義相關檔案存放目錄 │ ├── add_dsl.cc │ ├── add_dsl.h │ └── CMakeLists.txt ├── op_tiling │ └── CMakeLists.txt ├── scripts // 自定義運算元工程打包指令碼存放目錄 └── tbe ├── CMakeLists.txt ├── impl // 運算元程式碼實現 │ └── add_dsl.py └── op_info_cfg // 運算元資訊庫存放目錄 └── ai_core └── <soc_version> └── add_dsl.ini
運算元開發過程
實現AddDSL運算元的原型定義。
運算元原型定義檔案包含運算元註冊程式碼的標頭檔案(*.h)以及實現基本校驗、Shape推導的實現檔案(*.cc)。
- msopgen工具根據add_dsl.json檔案在“op_proto/add_dsl.h”中生成了運算元註冊程式碼,開發者需要檢查自動生成的程式碼邏輯是否正確,一般無需修改。
- 改“op_proto/add_dsl.cc”檔案,實現運算元的輸出描述推導函式及校驗函式。
在IMPLEMT_COMMON_INFERFUNC(AddDSLInferShape)函式中,填充推導輸出描述的程式碼,針對AddDSL運算元,輸出Tensor的描述資訊與輸入Tensor的描述資訊相同,所以直接將任意一個輸入Tensor的描述賦給輸出Tensor即可。
IMPLEMT_COMMON_INFERFUNC(AddDSLInferShape) { // 獲取輸出資料描述 TensorDesc tensordesc_output = op.GetOutputDescByName("y"); tensordesc_output.SetShape(op.GetInputDescByName("x1").GetShape()); tensordesc_output.SetDataType(op.GetInputDescByName("x1").GetDataType()); tensordesc_output.SetFormat(op.GetInputDescByName("x1").GetFormat()); //直接將輸入x1的Tensor描述資訊賦給輸出 (void)op.UpdateOutputDesc("y", tensordesc_output); return GRAPH_SUCCESS; }
在IMPLEMT_VERIFIER(AddDSL, AddDSLVerify)函式中,填充運算元引數校驗程式碼。
IMPLEMT_VERIFIER(AddDSL, AddDSLVerify) { // 校驗運算元的兩個輸入的資料型別是否一致,若不一致,則返回失敗。 if (op.GetInputDescByName("x1").GetDataType() != op.GetInputDescByName("x2").GetDataType()) { return GRAPH_FAILED; } return GRAPH_SUCCESS; }
實現AddDSL運算元的計算邏輯。
“tbe/impl/add_dsl.py”檔案中已經自動生成了運算元程式碼的框架,開發者需要在此檔案中修改add_dsl_compute函式,實現此運算元的計算邏輯。
add_dsl_compute函式的實現程式碼如下:
@register_op_compute("add_dsl") def add_dsl_compute(x1, x2, y, kernel_name="add_dsl"): # 呼叫dsl的vadd計算介面 res = tbe.vadd(x1, x2) return res
配置運算元資訊庫。
運算元資訊庫的路徑為“tbe/op_info_cfg/ai_core/<soc_version>/add_dsl.ini”,包含了運算元的型別,輸入輸出的名稱、資料型別、資料排布格式等資訊,msopgen工具已經根據add_dsl.json檔案將上述內容自動填充,開發者無需修改。
AddDSL運算元的資訊庫如下:
[AddDSL] // 運算元的型別 input0.name=x1 // 第一個輸入的名稱 input0.dtype=float16 // 第一個輸入的資料型別 input0.paramType=required // 代表此輸入必選,且僅有一個 input0.format=NCHW // 第一個輸入的資料排布格式 input1.name=x2 // 第二個輸入的名稱 input1.dtype=float16 // 第二個輸入的資料型別 input1.paramType=required // 代表此輸入必選,且僅有一個 input1.format=NCHW // 第二個輸入的資料排布格式 output0.name=y // 運算元輸出的名稱 output0.dtype=float16 // 運算元輸出的資料型別 output0.paramType=required // 代表此輸出必選,有且僅有一個 output0.format=NCHW // 運算元輸出的資料排布格式 opFile.value=add_dsl // 運算元實現檔案的名稱 opInterface.value=add_dsl // 運算元實現函式的名稱
實現運算元適配外掛。
運算元適配外掛實現檔案的路徑為“framework/tf_plugin/tensorflow_add_dsl_plugin.cc”,針對原始框架為TensorFlow的運算元,CANN提供了自動解析對映介面“AutoMappingByOpFn”,如下所示:
#include "register/register.h" namespace domi { // register op info to GE REGISTER_CUSTOM_OP("AddDSL") // CANN運算元的型別 .FrameworkType(TENSORFLOW) // type: CAFFE, TENSORFLOW .OriginOpType("AddDSL") // 原始框架模型中的運算元型別 .ParseParamsByOperatorFn(AutoMappingByOpFn); //解析對映函式 } // namespace domi
以上為工程自動生成的程式碼,開發者僅需要修改.OriginOpType("AddDSL")中的運算元型別即可。此處我們僅展示運算元開發流程,不涉及原始模型,我們不做任何修改。
至此,AddDSL運算元的所有交付件都已開發完畢。
運算元工程編譯及運算元包部署
運算元開發過程完成後,需要編譯自定義運算元工程,生成自定義運算元安裝包並進行自定義運算元包的安裝,將自定義運算元部署到運算元庫。
運算元工程編譯
1. 修改build.sh指令碼,配置運算元編譯所需環境變數。
將build.sh中環境變數ASCEND_TENSOR_COMPILER_INCLUDE配置為CANN軟體標頭檔案所在路徑。
修改前,環境變數配置的原有程式碼行如下:
# export ASCEND_TENSOR_COMPILER_INCLUDE=/usr/local/Ascend/ascend-toolkit/latest/compiler/include
修改後,新的程式碼行如下:
export ASCEND_TENSOR_COMPILER_INCLUDE=${INSTALL_DIR}/include
${INSTALL_DIR}請替換為CANN軟體安裝後檔案儲存路徑。例如,若安裝的Ascend-cann-toolkit軟體包,則安裝後檔案儲存路徑為:$HOME/Ascend/ascend-toolkit/latest。
2. 在運算元工程目錄下執行如下命令,進行運算元工程編譯。
./build.sh
編譯成功後,會在當前目錄下建立build_out目錄,並在build_out目錄下生成自定義運算元安裝包custom_opp_<target os>_<target architecture>.run。
自定義運算元安裝包部署
以執行使用者執行如下命令,安裝自定義運算元包。
./custom_opp_<target os>_<target architecture>.run
命令執行成功後,自定義運算元包中的相關檔案部署到CANN運算元庫中。
運算元執行驗證
運算元包部署完成後,可以進行ST測試(System Test)和網路測試,對運算元進行執行驗證。
ST測試
ST測試的主要功能是:基於運算元測試用例定義檔案*.json生成單運算元的om檔案;使用AscendCL介面載入並執行單運算元om檔案,驗證運算元執行結果的正確性。ST測試會覆蓋運算元實現檔案,運算元原型定義與運算元資訊庫,不會對運算元適配外掛進行測試。
網路測試
你可以將運算元載入到網路模型中進行整網的推理驗證,驗證自定義運算元在網路中執行結果是否正確。網路測試會覆蓋運算元開發的所有交付件,包含實現檔案,運算元原型定義、運算元資訊庫以及運算元適配外掛。
具體的驗證過程請參考“昇騰文件中心”。
以上就是CANN自定義運算元開發的相關知識點,您也可以在“昇騰社群線上課程”板塊學習影片課程,學習過程中的任何疑問,都可以在“昇騰論壇”互動交流!
相關參考:
[1]昇騰文件中心
[2]昇騰社群線上課程
[3]昇騰論壇