本文大致介绍将深度学习算法模型移植到海思AI芯片的总体流程和一些需要注意的细节。海思芯片移植深度学习算法模型,大致分为模型转换,仿真运行,上板运行三步,接下来一一说明。
作者:猿音Land
授权转载自:https://zhuanlan.zhihu.com/p/103776174
模型转换
默认已在windows配置好Ruyi开发环境
模型转换使用海思提供的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
类型,并且该层参数只包含当前层输出的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文件,在转换模型后需要对比转换前后网络每层的输出是否相近,以确保模型转换正确,所以第一次转换时只使用一张图片,假设此图片叫1.jpg
,使用这张图片在原始网络中运行,保存unpooling层的输出到txt文件,保存格式是每张图对应的featuremap按行保存,维度按nchw
逐元素以空格分割保存。
模型转换
准备好这些文件后,使用Ruyi软件按照海思SVP说明文档第五章
说明配置其他参数选项,然后运行进行转换即可,这里列举几个配置选项,其他不再赘述。
# 在创建的模型转换工程文件夹上右击
Switch SOC Version: 设置芯片对应的nnie版本型号
Switch Emulation Library: 设置为Instruction Lib
is_simulation: 设置为Inst/Chip
log_level: 设置为Function level #此设置可以在模型转换时保存每层featuremap的输出
转换完成后,即可得到海思的.wk
模型描述文件。如果转换过程中出错根据提示错误修改即可(应该都是些很明显的小问题)。
仿真运行
模型仿真运行是基于Ruyi软件进行的,在海思的SDK中会提供sample,在仿真运行时可以仿照sample中的一个例子添加自己模型的前向推理,在实现过程包括网络模型初始化分配内存,读取图片,运行推理,得到结果。
当模型有中间层海思芯片不支持的时候,实际运行时会从custom层将模型切分成两段网络,第一段网络先在nnie运行,然后将结果传到cpu,在cpu实现不支持的那层的运算(即custom层),然后把运算得到的featuremap再从cpu传到nnie运行第二段网络,得到结果后再传到cpu做后处理。
也就是说custom层是切分网络模型的标志,如果有n个custom层,那网络模型就会被切分成n+1段。
网络模型初始化
海思默认没有提供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;
}
然后在网络初始化时按照custom层的输出大小分配内存,此处分配内存的原因是,custom层的输出作为第二段网络的输入,其实是为第二段网络的输入分配硬件上的内存,到时候直接将cunstom层在cpu运算得到的输出保存在此内存即可。分配内存的代码添加初始化函数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[i] : STRIDE_ALIGN);
}
这样基本可以加载模型进行网络初始化分配内存了。
读取图片
首先,先将之前转换模型的那张图片按BGR格式以chw的维度按字节写入文件
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)
然后将图片路径添加在代码指定位置即可。
运行推理
由于网络中含有一个custom层,网络被分成两段,而且第二段网络和RPN无关,所以此时网络运行整体由以下三部分组成:
// 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函数和get\_results函数需要我们自己实现,具体实现和原始模型中此层的实现一致,只是需要将输出存储在之前分配的内存中。
需要注意的是,硬件为了快速访问内存首地址或者跨行访问数据,要求内存地址或内存跨度必须为对齐系数的整数倍,分别可16字节对齐,32字节对齐,256字节对齐。所以在nnie输出的blob中,不可以直接遍历访问featuremap,需要先按对齐后的字节数映射回原始真是的feature大小。在这里举的这个例子,custom\_unpooling\_layer函数的输入是HI\_MPI\_NNIE\_Forward的输出,所以需要先对HI\_MPI\_NNIE\_Forward的输出做预处理,得到真是的featuremap,大致实现如下:
SVP_BLOB_S* pstDstBlob = pstDstParam->astDst;
// pstDstBlob->u32Stride是nnie输出的feature的每行真正的字节数,u32OneCSize是nnie输出的feature的每个channel真正的字节数
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);
根据以上代码,先按照chw开辟一块内存,将所有访问得到的元素存起来,就可以传给custom\_unpooling\_layer函数了。但这个blob类型是int32类型的,这是因为海思nnie硬件计算时用的是int8或者int16类型计算的,在nnie输出给cpu时,会将输出层做定点化再输出,定点化的系数是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函数。
在一次前向推理结束后,会保存模型每层运行的输出结果,将此结果和模型转换时保存的每层的输出结果用Ruyi提供的对比接口逐层对比结果,差异较大的层查找原因。如果每层结果都很接近,然后使用批量图转换模型再仿真运行。
上板运行
默认已在ubuntu系统配置好开发环境,配置好开发板
仿真运行成功后,就可进行这一步,海思同样提供了上板运行的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;
实现完成后,交叉编译,上板运行,将网络的输出保存下来,看是否和仿真运行以及模型转换的输出一致,不一致再定位到相关层查找原因。
在cpu执行的代码尽量定点化,因为海思对浮点数的计算效率不高,编译时自己加一些编译选项,可以优化一点速度。
总结
至此,模型移植到海思AI芯片的流程实现完毕。回顾一下,根据文档,修改模型并且转换;仿真运行,对比结果;上板运行,对比结果。
推荐专栏文章
- Tengine armv8.2 with ncnn serializer
- 百度轻量深度学习推理引擎Paddle Lite,极致的 ARM CPU 性能优化
- PaddleLite底层在backend上的kernel选择策略
更多嵌入式AI算法部署等请关注极术嵌入式AI专栏。