开启辅助访问
 找回密码
 立即注册

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

河的右岸 回答数13 浏览数1447
本文大致介绍将深度学习算法模型移植到海思AI芯片的总体流程和一些需要注意的细节。
海思芯片移植深度学习算法模型,大致分为模型转换,仿真运行,上板运行三步,接下来一一说明。
模型转换

默认已在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 : 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芯片的流程实现完毕。回顾一下,根据文档,修改模型并且转换;仿真运行,对比结果;上板运行,对比结果。
使用道具 举报
| 来自北京
xufang1985 | 来自北京
网络输入是浮点数的话,应该是中间层吧,用2**12定点化feature,然后送给tpu
回复
使用道具 举报
zf520678 | 来自北京
是主干网络提取的一个512*4*4的特征 打印发现都是-1—1之间的浮点数   然后有两个6层的网络的分别以这个特征为输入做2和4分类 您的意思是对得到的输出乘4096然后取整吗
回复
使用道具 举报
星云论 | 来自广东
如果两个网络中间没有预处理的话,可以第一个网络输出的int32直接传给后面的小网络。或者预处理阶段三个模型合成一个模型。
回复
使用道具 举报
mohun | 来自北京
中间没有预处理   您说的预处理阶段合成一个模型应该如何操作呢
回复
使用道具 举报
yangyunaaa | 来自北京
prototxt写到一个文件,caffemodel的权重写到一个文件
回复
使用道具 举报
键油授氲苫 | 来自北京
[赞同]OK 灰常感谢   我试试这两种方法
回复
使用道具 举报
mengOY | 来自北京
试了你的方法可以了  在此感谢  另外想问您有没有试过膨胀卷积   我有个膨胀卷积的网络  量化后和caffe差异很大   然后我只量化输入层到膨胀卷积的这部分  发现大部分是这里的误差   不知道是不是海思不支持膨胀卷积   但是又没报错   挺奇怪
回复
使用道具 举报
buluoboy1 | 来自北京
你看看海思的文档,有没有支持膨胀卷积就可以
回复
使用道具 举报
alk03073135 | 来自北京
关于海思的Custom层具体是如何应用在.prototxt文件和工程中,可以分享一个简单的Demo工程吗?海思自带的sample中没有Custom的用法
回复
使用道具 举报
12下一页
快速回复
您需要登录后才可以回帖 登录 | 立即注册

当贝投影