AI学习者 · 2020年06月23日

移植深度学习算法模型到海思AI芯片

本文大致介绍将深度学习算法模型移植到海思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芯片的流程实现完毕。回顾一下,根据文档,修改模型并且转换;仿真运行,对比结果;上板运行,对比结果。


推荐专栏文章


更多嵌入式AI算法部署等请关注极术嵌入式AI专栏
推荐阅读
关注数
18838
内容数
1371
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息