本文大致介紹將深度學習算法模型移植到海思AI芯片的總體流程和一些需要注意的細節(jié)。
海思芯片移植深度學習算法模型,大致分為模型轉換,仿真運行,上板運行三步,接下來一一說明。
模型轉換 默認已在windows配置好Ruyi開發(fā)環(huán)境
模型轉換使用海思提供的Ruyi工具,模型轉換的過程其實也是模型量化的過程。海思模型轉換僅支持caffemodel,所以需要準備好.prototxt文件和.caffemodel文件。如果你的模型不是caffemodel,需要先轉換為caffemodel并驗證其正確性后再做以下操作。
prototxt文件預處理prototxt文件中的layers需要按照海思文檔中指定的要求書寫。這里要說明的是,有一些自定義層海思不支持,不支持的層要放到cpu運算,這里我假設以下是我要移植的模型,其中有一層unpooling中間層海思nnie不支持,省事更全面說明問題。
input: "data"
input_shape
{
dim:1
dim:3
dim:360
dim:640
}
layer {
bottom: "data"
top: "conv1"
name: "conv1"
type: "Convolution"
convolution_param {
num_output: 64
kernel_size: 3
pad: 0
stride: 2
bias_term: true
}
}
layer {
bottom: "conv1"
top: "unpooling"
name: "unpooling"
type: "Unpooling"
unpooling_param {
w_scale: 2
h_scale: 2
pad: 0
}
}
layer {
bottom: "unpooling"
top: "output"
name: "output"
type: "Convolution"
convolution_param {
num_output: 64
kernel_size: 3
pad: 0
stride: 2
bias_term: true
}
}如上所示,此模型有四層,對于海思不支持的層,需要將layer的type修改為Custom類型,并且該層參數(shù)只包含當前層輸出的shape,修改后如下圖所示:
input: "data"
input_shape
{
dim:1
dim:3
dim:120
dim:360
}
layer {
bottom: "data"
top: "conv1"
name: "conv1"
type: "Convolution"
convolution_param {
num_output: 64
kernel_size: 3
pad: 0
stride: 2
bias_term: true
}
}
layer {
bottom: "conv1"
top: "unpooling"
name: "unpooling"
type: "Custom"
custom_param {
shape {
dim: 1
dim: 64
dim: 120
dim: 360
}
}
}
layer {
bottom: "unpooling"
top: "output"
name: "output"
type: "Convolution"
convolution_param {
num_output: 64
kernel_size: 3
pad: 0
stride: 2
bias_term: true
}
}prototxt文件修改后,開始準備轉換模型時所用的量化圖片,以及custom(即unpooling)輸出后的featuremap文件,在轉換模型后需要對比轉換前后網(wǎng)絡每層的輸出是否相近,以確保模型轉換正確,所以第一次轉換時只使用一張圖片,假設此圖片叫1.jpg,使用這張圖片在原始網(wǎng)絡中運行,保存unpooling層的輸出到txt文件,保存格式是每張圖對應的featuremap按行保存,維度按nchw逐元素以空格分割保存。
模型轉換準備好這些文件后,使用Ruyi軟件按照海思SVP說明文檔第五章說明配置其他參數(shù)選項,然后運行進行轉換即可,這里列舉幾個配置選項,其他不再贅述。
# 在創(chuàng)建的模型轉換工程文件夾上右擊
Switch SOC Version: 設置芯片對應的nnie版本型號
Switch Emulation Library: 設置為Instruction Lib
is_simulation: 設置為Inst/Chip
log_level: 設置為Function level #此設置可以在模型轉換時保存每層featuremap的輸出轉換完成后,即可得到海思的.wk模型描述文件。如果轉換過程中出錯根據(jù)提示錯誤修改即可(應該都是些很明顯的小問題)。
仿真運行模型仿真運行是基于Ruyi軟件進行的,在海思的SDK中會提供sample,在仿真運行時可以仿照sample中的一臺例子添加自個模型的前向推理,在實現(xiàn)過程包括網(wǎng)絡模型初始化分配內(nèi)存,讀取圖片,運行推理,得到結果。
當模型有中間層海思芯片不支持的時候,實際運行時會從custom層將模型切分成兩段網(wǎng)絡,第一段網(wǎng)絡先在nnie運行,然后將結果傳到cpu,在cpu實現(xiàn)不支持的那層的運算(即custom層),然后把運算得到的featuremap再從cpu傳到nnie運行第二段網(wǎng)絡,得到結果后再傳到cpu做后處理。
也就是說custom層是切分網(wǎng)絡模型的標志,如果有n個custom層,那網(wǎng)絡模型就會被切分成n+1段。
網(wǎng)絡模型初始化海思默認沒有提供custom的例子,所以首先需要在SVP_NNIE_MULTI_SEG_S結構體中添加類型為SVP_BOLB_S的custom變量,如下所示:
typedef struct hiSVP_NNIE_MULTI_SEG_S
{
// 原始變量...
SVP_BLOB_S astCustom[SVP_NNIE_MAX_OUTPUT_NUM];
}
typedef struct hiSVP_NNIE_CFG_S
{
// 原始變量...
HI_U32 u32CustomNum;
}然后在網(wǎng)絡初始化時按照custom層的輸出大小分配內(nèi)存,此處分配內(nèi)存的原因是,custom層的輸出作為第二段網(wǎng)絡的輸入,其實是為第二段網(wǎng)絡的輸入分配硬件上的內(nèi)存,到時候直接將cunstom層在cpu運算得到的輸出保存在此內(nèi)存即可。分配內(nèi)存的代碼添加初始化函數(shù)SvpSampleMultiSegCnnInit中,大概偽代碼如下:
if(pstComCfg->u32CustomNum > 0)
{
enType = SVP_BOLB_TYPE_S32;
HI_U32 u32DstC = customNumOutChannel;
HI_U32 u32DstW = customNumOutWidth;
HI_U32 u32DstH = customNumOutHeight;
HI_S32 s32Ret = SvpSampleMallocBlob(&pstComParam->astCustom[u32DstCnt], enType, 1, u32DstC, u32DstW, u32Dsth, pu32DstAlign ? pu32DstAlign
: STRIDE_ALIGN);
}這樣基本可以加載模型進行網(wǎng)絡初始化分配內(nèi)存了。
讀取圖片
首先,先將之前轉換模型的那張圖片按BGR格式以chw的維度按字節(jié)寫入文件
import numpy as np
import cv2
def gen_img_bin(img_path):
f = open(img_path[:-4] + '.bgr', 'wb')
img = cv2.imread(img_path)
img = np.transpose(img, (2,0,1))
f.write(img.tobytes())
f.close()
if __name__ == '__main__':
img_path = './1.jpg'
gen_img_bin(img_path)然后將圖片路徑添加在代碼指定位置即可。
運行推理
由于網(wǎng)絡中含有一臺custom層,網(wǎng)絡被分成兩段,而且第二段網(wǎng)絡和RPN無關,所以此時網(wǎng)絡運行整體由以下三部分組成:
// nnie
HI_MPI_NNIE_Forward(&SvpNnieHandle,
&stDetParam.astSrc[0],
&stDetParam.stModel,
&stDetParam.astDst[0],
&stDetParam.astCtrl[0],
bInstant);
// cpu
custom_unpooling_layer(input, (HI_S32*)stDetparam.astCustom[1].u64VirAddr, ...);
// nnie
HI_MPI_NNIE_Forward(&SvpNnieHandle,
&stDetParam.astCustom[1],
&stDetParam.stModel,
&stDetParam.astDst[1],
&stDetParam.astCtrl[1],
bInstant);
// cpu
get_results((HI_S32*)stDetParam.astDst[1].u64VirAddr, ...);其中custom_unpooling_layer函數(shù)和get_results函數(shù)需要我們自個實現(xiàn),具體實現(xiàn)和原始模型中此層的實現(xiàn)一致,只是需要將輸出存儲在之前分配的內(nèi)存中。
需要注意的是,硬件為了快速訪問內(nèi)存首地址或者跨行訪問數(shù)據(jù),要求內(nèi)存地址或內(nèi)存跨度必須為對齊系數(shù)的整數(shù)倍,分別可16字節(jié)對齊,32字節(jié)對齊,256字節(jié)對齊。所以在nnie輸出的blob中,不可以直接遍歷訪問featuremap,需要先按對齊后的字節(jié)數(shù)映射回原始真是的feature大小。在這里舉的這個例子,custom_unpooling_layer函數(shù)的輸入是HI_MPI_NNIE_Forward的輸出,所以需要先對HI_MPI_NNIE_Forward的輸出做預處理,得到真是的featuremap,大致實現(xiàn)如下:
SVP_BLOB_S* pstDstBlob = pstDstParam->astDst;
// pstDstBlob->u32Stride是nnie輸出的feature的每行真正的字節(jié)數(shù),u32OneCSize是nnie輸出的feature的每個channel真正的字節(jié)數(shù)
HI_U32 u32OneCSize = pstDstBlob->u32Stride * pstDstBlob->unShape.stWhc.u32Height;
HI_U32 u32FrameStride = u32OneCSize * pstDstBlob->unShape.stWhc.u32Chn;
// 訪問第c個通道第h行第w列的元素
HI_S32* ps32Temp = (HI_S32*)((HI_U8*)pstDstBlob->u64VirAddr + c * u32OneCSize + pstDstBlob->u32Stride) + w;
HI_S32 element = (HI_S32)(*ps32Temp);根據(jù)以上代碼,先按照chw開辟一塊內(nèi)存,將所有訪問得到的元素存起來,就可以傳給custom_unpooling_layer函數(shù)了。但這個blob類型是int32類型的,這是因為海思nnie硬件計算時用的是int8或者int16類型計算的,在nnie輸出給cpu時,會將輸出層做定點化再輸出,定點化的系數(shù)是4096,所以要得到真正的featuremap,還需將上述得到的element值除以4096,才可得到輸出的float32值。
float out = (float)element / 4096;在得到custom的輸出后,再傳給nnie做HI_MPI_NNIE_Forward,注意,傳給HI_MPI_NNIE_Forward的是int32類型的值,所以custom的輸出要使用4096做定點化轉換為int32類型。輸出的結果同樣做上述操作,然后傳輸給get_results函數(shù)。
在一次前向推理結束后,會保存模型每層運行的輸出結果,將此結果和模型轉換時保存的每層的輸出結果用Ruyi提供的對比接口逐層對比結果,差異較大的層查找原因。如果每層結果都很接近,然后使用批量圖轉換模型再仿真運行。
上板運行
默認已在ubuntu系統(tǒng)配置好開發(fā)環(huán)境,配置好開發(fā)板
仿真運行成功后,就可進行這一步,海思同樣提供了上板運行的sample,模型初始化和讀取圖片的流程和仿真時差異不大,在硬件輸出的featuremap映射時和仿真時略有差異,這里說明一下,其他不再贅述:
HI_S32* nnie_out_blob = NULL;
nnie_out_blob = SAMPLE_SVP_NNIE_CONVERT_64BIT_ADDR(HI_S32, pstNnieParam->astSegData[0].astDst[0].u64VirAddr);
// 每個通道的大小
HI_U32 u32MapSize = pstSoftwareParam->au32ConvHeight[0] * pstSoftwareParam->u32ConvStride / sizeof(HI_U32);
// 每行大小
HI_U32 u32LineSize = pstSoftwareParam->u32ConvStride / sizeof(HI_U32);
float out = (float)(((HI_S32*)nnie_out_blob)[idx]) / 4096;實現(xiàn)完成后,交叉編譯,上板運行,將網(wǎng)絡的輸出保存下來,看是否和仿真運行以及模型轉換的輸出一致,不一致再定位到相關層查找原因。
在cpu執(zhí)行的代碼盡量定點化,因為海思對浮點數(shù)的計算效率不高,編譯時自個加一些編譯選項,可以優(yōu)化一點速度。
總結
至此,模型移植到海思AI芯片的流程實現(xiàn)完畢。回顧一下,根據(jù)文檔,修改模型并且轉換;仿真運行,對比結果;上板運行,對比結果。