Maix-EMC, Embedded Model Convertor Discuss topic

Maix-EMC, Embedded Model Convertor Discuss topic
The Post is written in chinese at the moment, please use google translate to read it.

EMC讨论帖(github地址:https://github.com/sipeed/Maix-EMC)

EMC的基础结构


相关文件的描述可以参见github的readme, 本帖子主要对如何参与EMC开发进行介绍。

EMC的设计目的和工作

EMC的设计目的是让NN模型在更多的嵌入式平台上运行。
嘉博的NNoM已经有了一个很好的开头,但是我觉得类似于kmodel的二进制模型文件+kmodel解释器的形式,更方便大家使用,并且通用性更好。

EMC即这个工作的左侧,将PC端的模型文件转换成二进制扁平化的模型文件。
PC端的模型文件我们选用了TensorLayer,因为tensorlayer的层定义高度比较合适,基本等同于kmodel定义的层的高度,两者转换基本只是作了 量化,某些层的合并优化, 极大地加快了开发进度。
反观tensorflow的pb文件里算子,低到了Add,mul的程度, 需要手工整合这些低层次算子到层定义,非常不便;

这个工作的中间是kmodel文件,类似于字节码或者说IR。
这里模型文件没有采用通用的protobuf(pb)或者flatbuffer(tflite), 因为对于嵌入式平台来说,它们的解析以及内存消耗都太大。
为了快速实现demo,并考虑嵌入式的效率,这里我们借用了k210 sdk中的kmodel v3格式。
这个格式本是为K210设计,但是同样适用于普通嵌入式平台,只需新增通用层类型的定义。

这个工作的右侧是嵌入式硬件平台上的kmodel解释器(interpreter)
对于k210, 我们只需借用SDK本身的kpu.c, 稍作修改即可。
对于普通单片机,我们只需将kpu.c中的卷积层计算函数替换成普通的cpu计算函数。
这里的计算后端可以借用CMSIS-NN或者NNoM的计算后端,往上套上kmodel层参数的调用wrapper即可。

EMC的现状

2019.6.28

由于在设计之初就借用了K210的相关驱动,所以我用了一个多星期完成了可初步运行的EMC very alpha版。
目前版本仅调试通过了TL的example里的cifar10 demo(稍微改动了下全连接的通道数以减小模型大小)
实现了普通卷积层,FC,upload, maxpool2d, flatten, GAP2D , quant, dequant 等层类型。
但是在处理量化,去量化的时候,还没仔细处理kpu上传下载的顺序,所以导致了mobilenet转换出错,这里需要修正(约1天工作量)。

另外对于FC和其它一些可以使用conv加速器优化的层,目前也没有进行优化,是直接原本地使用CPU计算,效率会比较低。

目前也只支持简单堆叠的网络结构,尚不支持分叉的结构,需要完善。

工作的优先级:

  1. 修复conv相关层的upload/download顺序,完成mobilenet的演示。
  2. 进行一些层的加速优化, 对TL不支持的softmax进行支持。
  3. 量化算法的优化
  4. 普通MCU普通的支持,整合NNoM后端
  5. 支持分叉网络结构,能支持RNN更好。

协作开发的快速入门教学

首先我们需要先完善EMC对K210平台的支持。
参与EMC完善计划的朋友可以免费获赠K210豪华开发板一套,详情请联系 泽畔 或者发邮件到support@sipeed.com

K210简介

K210是RV64GC内核的AIoT芯片,内置KPU可进行卷积/池化/BN/激活 的打包加速操作。
详细说明见:MAIX KPU demystify: write your first layer by hand config registers

Sipeed为k210移植了maixpy(micropython)环境,所以大家可以直接使用python在K210板子上运行自己的模型,但是目前社区仍然是苦于嵌入式开发者多,AI开发者少,缺乏更多模型支持的局面,所以我抽空做了Maix-EMC项目,希望导入更多的AI开发者和模型。

Kmodel格式简介

kmodel是一个自定义的,扁平化的模型存储格式,模型格式的封装我已经在EMC代码里完成,这里简要介绍一下:
在EMC中,我们调用了dissect.cstruct, 这是pyhton解析c结构体的库, 很方便我们使用k210的kpu.c中关于kmodel的结构体定义。在EMC中,这部分定义放在k210_constant.py中:

kmodel_def ="""
typedef struct
{
    uint32 version;			
    uint32 flags;
    uint32 arch;				
    uint32 layers_length;		
    uint32 max_start_address;
    uint32 main_mem_usage;	
    uint32 output_count;		
} kpu_model_header_t;
typedef struct
{
    uint32 address;
    uint32 size;
} kpu_model_output_t;
typedef struct
{
    uint32 type;
    uint32 body_size;
} kpu_model_layer_header_t;
...

kmodel 头部是kpu_model_header_t, 描述了 版本,量化位数,层数,最大内存占用大小(驱动中一次性申请该模型需要的动态内存),输出节点数量。
在头部之后,排列着若干个kpu_model_output_t,描述输出节点的信息。
在输出节点信息之后,排列着所有层的头部信息:kpu_model_layer_header_t,依次描述层的类型,层body的大小。
在层头部信息之后,就按层信息依次排列层body数据,其中某些部分会要求一定的字节对齐。

层类型定义在edge_constant.py中,在原始的kpu.h的定义上稍作修改,区分了k210专用层和普通层(这里是为了快速移植K210驱动才使用了K210专用层,理论上仅定义一套通用层标准比较好)

K210的kmodel解释器的实现

可以参见kpu.c, 驱动会按顺序读取kmodel每一层的层信息,根据层类型执行对应函数。
需要注意的是上传/下载操作。
K210内存分为6M CPU内存 和 2M KPU内存。
使用KPU计算的层,需要将待计算的数据上传到KPU内存。
在KPU中,可以连续计算很多层CONV相关计算,而无需将结果下载到CPU内存。
但是一旦下一层是需要CPU运算的层,则需要进行一次下载才能继续运行。
所以,我们需要留意TL层的顺序,在需要切换KPU/CPU运行的层前后,插入上传,下载的dummy 层。
在EMC中,我们使用meta_info[‘is_inai’]字段确认当前的待计算内容是否在AI内存。

另外,KPU计算,使用的2M内存,是以乒乓形式使用,
即输入数据在开端,则输出结果在末端
进入下一层后,上一层的输出结果作为了输入结果,在末端,计算结果放到了开端。
如此往复计算,EMC中meta_info[‘conv_idx’]记录了当前的卷积层序号,进而可以确认当前的输出结果所在KPU内存的偏移。

其它注意点,需要下载kpu.c查看:https://github.com/kendryte/kendryte-standalone-sdk/blob/develop/lib/drivers/kpu.c

EMC 将TL层转换为kmodel的流程

入口文件是edge_model.py
gen_edge_layers_from_network 将TL层转换为EMC的层中间表示形式。
这里首先通过platform_table 查表选择当前硬件平台使用的TL层转EMC层的函数表,以及打包模型的函数。

platform_table = {
#   platform       tl layer convertor   model generator
    'k210'      :   [tl_to_k210_table, gen_kmodel]
    #'stm32'    :   gen_stm32_layer_func_table,
} 

在tl_to_k210_table中,gen_edge_layer_from_network查找到对应的TL层类型的表项,并往后匹配到最长的列表,将该列表交给layer_generator 来生成 EMC中间层的list (可能会在前后加了上传/下载/量化/去量化的dummy层)

tl_to_k210_table= {
#   TL layer class          layer_generator     merge
    'Dense'                 :[gen_fc_layer,         []] ,
    'Flatten'               :[gen_flatten_layer,    []] ,
    'Reshape'               :[None,                 []] ,
    'GlobalMaxPool2d'       :[gen_gmaxpool2d_layer, []] ,
    'GlobalMeanPool2d'      :[gen_gavgpool2d_layer, []] ,
    'MaxPool2d'             :[gen_maxpool2d_layer,  []] ,
    'MeanPool2d'            :[gen_avgpool2d_layer,  []] ,
    'Concat'                :[gen_concat_layer,     []] ,
    'Conv2d'                :[gen_k210_conv_layer,  [['BatchNorm'],]] ,
    'DepthwiseConv2d'       :[gen_k210_conv_layer,  [['BatchNorm'],]] ,
    'DummyDequant'          :[gen_dequant_layer,    []] ,  
}

至此我们初步将TL层转换成了一系列层列表,我们再使用optimize_layers来优化层列表,去除一些抵消的层(如相邻的量化/去量化层)
然后我们使用gen_kmodel将层列表转换成kmodel
层列表中的每个层有to_kmodel方法,调用该方法即可获得当前层符合kmodel格式的layer body的bytearray结果。
gen_kmodel再把所有层的body堆叠在一起,统计好最大的动态内存需求,加好头部,即得到了kmodel。

EMC 层支持的添加

首先在edge_model.py的tl_to_k210_table里加上你需要添加的TL层与EMC层的转换表项。
然后在对应的xxx_layer.py中加上对应的实现。

K210相关的加速层在k210_layer.py中实现(目前已经基本实现,但是需要修复一些bug)
CPU计算的非加速层,在edge_layer中实现。
只需模仿其中的层的实现,对每个层类型,完成以下一个函数和一个类:
gen_xxx_layer: 输入TL layer list, 转换成EMC layer list
class xxx_Layer: 需要实现init方法(填充层信息),以及to_kmodel方法(按kmodel格式填充信息,返回打包的bytearray)
事实上,你可以实现自定义的to_xxxmodel方法,在此框架上实现你自己的模型格式。

K210实机调试

建议先从EMC的example目录下的k210_project工程入手测试,对于刚上手嵌入式的AI开发者,可以在微信群中咨询我详细步骤。

1 Like