编译器自定义TPU层插件

本功能是利用bmkernel,直接在设备底层实现可接入bmcompiler的自定义算子。通过调用bmkernel的原子操作,用户可以直接在设备上编程,以充分利用TPU设备。 由于主要开发工作是在bmkernel基础上,建议用户先熟悉bmkernel编程相关内容

1. 目录说明

由于自定义layer牵扯比较多的模块,这里用user_layers工程目录,没有在plugin目录上直接开发

相关工程放在bmnet/bmcompiler/user_layers下,提供了自定义tpu层的基本框架和工具

  • envsetup.sh: 基本的工具集,进入目录后请source envsetup.sh

  • device目录: 存放用bmkernel编写的代码,涉及到global layer,local layer, reshape相关操作。

  • host目录: 存放LayerDescriptor代码,用于设置bmcompiler用到的一些属性,可用cpp编写

  • include目录: 用户的头文件目录

  • config目录: Makefile的相关配置

  • Makefile: 整个工程的make file,不建议直接使用,建议通过工具build_layers来间接使用

  • generated目录: 由gen_entry生成,里面根据op_type进行分发调用相关接口函数及相关数据结构定义

  • build目录: 存放编译时的中间文件

  • tool目录: 存放相关的工具程序,如load_firmware

  • install目录: 存放最终编译出来的结果

另外,SDK包中的example目录下还有一个bmkernel_demo目录,这里也存放着bmkernel代码。 user_layers和bmkernel_demo两个目录的bmkernel工程关系是:

1. bmkernel_demo工程目录用于开发CV算子等运行时可直接调用的API接口。bmkernel_demo工程开发编译时,是不会包含user_layers里的bmkernel内容。 所以我们在bmkernel_demo工程里load_firmware时,是排除user_layers的bmkernel程序在外的。

2. user_layers工程目录主要用于为bmcompiler开发自定义算子,但在user_layers工程目录下的bmkernel程序是可以调用bmkernel_demo里的程序的。 因此user_layers工程开发编译时,包含bmkernel_demo里的内容。

综上,如果用户即要在bmkernel_demo工程里开发API接口,又要在user_layers里开发深度学习模型的自定义算子,那么我们建议用户在编译bmkernel程序并load_firmware时, 只需用user_layers下的编译和load_firmware即可。

如果用户只用在bmkernel_demo工程里开发API接口,那我们建议用户只在bmkenrel_demo工程开发即可。

2. envsetup.sh中的工具

在user_layers目录下执行``source envsetup.sh``后,终端可以使用以下命令:

  1. add_layer name [has_local]
    • 增加layer

    • name为自定义layer的名称;

    • has_local取值为0或1,表示是否增加local layer,默认为0;

    • 在device目录中生成bm_device_xxx.c文件,需要由用户编写bmkernel代码

    • 在host目录下生成bm_host_xxx.h和bm_host_xxx.cpp文件,需要由用户编写layer的描述符接口,供bmcompiler内部优化使用

    • 如果当前layer相关文件已经存在,会报添加失败

  2. del_layer name :
    • 删除layer,会依次询问是否删除相关文件

    • name为自定义layer的名称;

  3. list_layers
    • 列出当前已定义的layer

  4. set_bmkernel_path path/to/bmkernel
    • 设置bmkernel_demo的工程目录,在gen_config中会引入相关的必要文件路径

  5. gen_entry
    • 在generated中生成接口分发函数代码(bmtpu_device_entry.c和bmtpu_host_entry.cpp)及相关参数定义文件(bmtpu.h)

    • bmtpu.h可以供外部add_tpu_layer使用,定义了tpu_op_type_t和相关的layer_param;

    • bmtpu_device_entry.c提供了设备上的分发函数代码,最终会被编译进firmware

    • bmtpu_host_entry.cpp提供了bmcompiler_plugin的layer注册相关代码,会编译进libbmplugin_layer.so

  6. build_bmkernel
    • 根据set_bmkernel_path设置的路径,生成bmkernel相关的库

  7. build_layers
    • 完整地编译整个工程,相当于`build_bmkernel && gen_config && gen_entry && make all`

  8. rebuild_layers
    • 清空所有生成文件,重新生成config,并build_layers

3. 接口代码修改

利用add_layer增加新的xxx layer后, 需要实现以下接口:

  1. 在device/bm_device_xxx.c中实现

__BM_PARAM__(TPU_XXX) typedef struct {
} tpu_xxx_param_t;

// buffer 当layer描述符中get_global_buffer_size()返回大于0时有效
__BM_GLOBAL__(TPU_XXX) int xxx_global(const void* param, const int param_size,  // layer param
                const tpu_tensor_t* in_tensors, const int in_num,               // input tensors
                const bm_addr_t buffer_addr, const bm_size_t buffer_size,       // buffer
                tpu_tensor_t* out_tensors, const int out_num);                  // output tensors

// local layer可选择实现,如不实现,请注释掉
// buffer 当layer描述符中get_local_buffer_size()返回大于0时有效
__BM_LOCAL__(TPU_XXX) int xxx_local(const void* param, const int param_size,   // layer param
               const tpu_tensor_t* in_tensors, const int in_num,               // input tensors
               const u64 buffer_offset, const u64 buffer_size,                 // buffer
               tpu_tensor_t* out_tensors, const int out_num);                  // output tensors

// 需要根据param,in_tensors来确定out_tensors的ttype,dtype,shape信息
// 如果实现local layer,还需要确定out_tensors的slice_pad,slice_shape,processed内容
__BM_SHAPE__(TPU_XXX) int xxx_reshape(const void* param, const int param_size, // layer param
                const tpu_tensor_t* in_tensors, const int in_num,              // input tensors
                tpu_tensor_t* out_tensors, const int out_num);                 // output tensors
  1. 在host/bm_host_xxx.h, 及host/bm_host_xxx.cpp中通过继承LayerDescriptor实现XxxDescriptor类

__BM_DESC__(TPU_XXX)
class XxxDescriptor: public bmcompiler::LayerDescriptor {
public:
    virtual std::string get_name() const override { return "XXX"; }
}

相关数据结构说明

  1. tensor结构定义(定义在bmcompiler_tpulayer_device.h中)

typedef struct {
  bm_addr_t addr;                    // can be local_offset or global_addr
  unsigned int dims;                 // dimension of tensor
  int elem_num;                      // real element number
  int shape[MAX_SHAPE_DIMS];         // for global or local calculate
  int slice_pad[MAX_SHAPE_DIMS];     // for local calculate
  int slice_shape[MAX_SHAPE_DIMS];   // for local calculate
  int processed[MAX_SHAPE_DIMS];     // for local calculate
  int * host_data;                   // used for read or write host data
  unsigned char ttype;               // TENSOR_TYPE_T
  unsigned char dtype;               // DATA_TYPE_T
  unsigned char store_mode;          // STORE_MODE_1N, STORE_MODE_4N, STORE_MODE_2N
} tpu_tensor_t;
  1. 标记宏(定义在bmcompiler_tpulayer_device.h中)

add_layer 会在生成的框架代码中自动加入相关标记宏。 gen_entry 会根据标记宏提取相关代码,生成参数定义及entry文件。

__BM_LOCAL__(op_type)  //标记该函数为local layer函数
__BM_GLOBAL__(op_type) //标记该函数为global layer函数
__BM_SHAPE__(op_type)  //标记该函数为reshape函数
__BM_DESC__(op_type)   //标记该class为类描述符
__BM_PARAM__(op_type)  //标记layer的参数,该定义会放入bmtpu.h
  1. 接口函数返回值(定义在bmcompiler_tpulayer_device.h中)

    返回值大于等于0时,表示成功,小于0时表示出错, 当前定义了如下错误码

#define BM_LAYER_OK (0)
#define BM_LAYER_NOT_SUPPORTED (-1)
#define BM_LAYER_PARAM_INVALID (-2)
#define BM_LAYER_RUNTIME_ERROR (-3)
  1. LayerDescriptor(定义在bmcompiler_tpulayer_host.hpp)

    这里仅列出了目前有效的接口

class LayerDescriptor {
public:
  // 通过该接口可以得到layer实例,进而得到layer的参数,输入及输出
  GraphNode* layer() const;

  // 返回layer的name,供日志显示
  virtual std::string get_name()  const { return "UNKNOWN"; }

  // 如果在bmkernel中使用了cdma或gde,必须返回true,以保证动态运行
  virtual bool must_dynamic_run() const { return false; }

  // 如果输出的shape无法根据输入的shape和layer参数确定(通常取决输入的数据,比如where操作),返回true
  virtual bool output_shape_is_dynamic() const { return false; }

  // 强制用户使用global layer编译
  virtual bool force_use_global_layer() const { return false; }

  // 该算子是否支持对h/w进行切分,如不支持,bmcompiler会在安排local layer时尝试将整个tensor放入到local mem中
  virtual bool can_split_local_h_w() const { return true; }

  // 该层是否可做原地操作
  virtual bool can_inplace(DATA_TYPE_T data_prec) const { return false; }

  // if the layer can support size of h/w dimension is dynamic
  virtual bool can_h_w_dynamic() const { return true; }

  // 是否仅更改的input的shape, output数据和input数据内容是一样的
  virtual bool just_reshape() const { return false; }

  // 输入输出能否共享同一块mem,如是,则输入输出的mem地址相同
  virtual bool is_share_mem_layer() const { return false; }

  // 针对global layer,是否需要buffer, 如果返回值大于0,则会申请相应大小的buffer
  virtual u64 get_global_buffer_size() const { return 0; }

  // 设置第in_idx个tensor的类型
  virtual TENSOR_TYPE_T input_type(int in_idx) const { return BMNET_NEURON; }

  //---------------------------------------------------------------------------
  // 以下参数仅用于local layer
  // 如果未实现local layer,可以忽略

  // 针对4维的split_shape大小,返回需要的buffer大小
  virtual u32 get_local_buffer_size(const int* split_shape) const { return 0; }

  // 该layer在反向推断data slice时,是否支持相同tensor可设置不同data slice
  virtual bool is_backslice_optimize_support_layer() const { return false; }

  // 当支持反向slice大小推断时,要提供以下参数
  // backward
  virtual int backward_hslice(int out_hslice) const { return out_hslice; }
  virtual int backward_wslice(int out_wslice) const { return out_wslice; }
  virtual int backward_hidx(int out_hidx) const { return out_hidx; }
  virtual int backward_widx(int out_widx) const { return out_widx; }
  virtual int backward_kh(int out_kh) const { return out_kh; }
  virtual int backward_stride_h(int out_stride_h) const { return out_stride_h; }
  virtual int backward_up_pad_h(int out_up_pad_h) const { return out_up_pad_h; }
  virtual int backward_down_pad_h(int out_down_pad_h) const { return out_down_pad_h; }

  // 该layer在做local layer时,第in_idx个输入tensor是否要在l2 mem上,默认是在local mem上。
  virtual bool should_input_on_l2mem(int in_idx) const { return false; };

  // forward
  virtual int forward_height(int in_height) const { return in_height; }
};

global layer与local layer说明

global layer有以下特点:
  • 输入和输出的数据是放在ddr中

  • 输入和输出的addr是绝对地址

  • local mem可以任意使用,自行安排

  • 所有的bmkernel指令均可使用

  • 通常的使用模式是:
    1. 通过gdma将输入数据从global mem搬运到local mem中

    2. 在local mem中完成相关运算

    3. 通过gdma将输出数据从local mem搬回到global mem中

  • 优点是非常灵活,可以实现任意算子;缺点是会产生较多的gdma搬运

  • 所有layer必须实现global layer

local layer有以下特点:
  • 可以与其他layer组合进行layer group优化,避免该layer计算时数据要搬入搬出到global mem中

  • 输入和输出的数据是放在local mem中

  • 输入和输出的addr是相对地址,对于BM1684来说,范围从0-512K

  • local mem不可任意使用,由bmcompiler提前分配好

  • 只支持小于等于4维输入和输出

  • 通常不能使用gdma指令,仅可使用bd运算指令

  • 通常的使用模式是:
    1. 在local mem中完成相关运算

  • 优点是可以节省gdma搬运, 运算效率高,缺点是复杂,有些算子无法实现;

  • layer可不实现local layer

4. bmkernel代码结构建议

在bmkernel_demo工程开发时,我们建议如下

// 注意: kernel函数里只包含必要的原子操作及必要的同步函数
//       不包含bm_initialize()函数调用, 把相关的初始和等待结果代码放到下面包装函数里
//       这里把local部分单独提取出来,可以供local layer里直接调用,但有时无法单独把local提取出来
void bm_xxx_local_kernel(...){
  ...
}

// 注意: kernel函数里只包含必要的原子操作及必要的同步函数
//       不包含bm_initialize()函数调用, 把相关的初始和等待结果代码放到下面包装函数里
//       这个函数可供global layer调用
void bm_xxx_kernel(...){
  ...
  bm_xxx_local_kernel(...); // 调用单独提取出的local kernel
  ...
}

// bmkernel的外层包装函数,包含了环境初始化及最后的结果同步代码
int bm_api_xxx(void* args) {
  // 注意: bmkernel_api_xxx_t结构中包含了输入输出的地址以及其他所有调用参数
  //       而layer_param中只包含layer所需的参数,输入输出的地址及shape信息可从in_tensors和out_tensors中取得
  bmkernel_api_xxx_t *api = (bmkernel_api_xxx_t *)args;
  bm_initialize(); //环境初始化
  bm_xxx_kernel(   //调用bm_xxx_kernel
     api->in_addr,
     api->out_addr,
     api->param1,
     api->param2,
     ...
  );
  // polling done
  bm_poll();
  return 0;
}

在user_layers工程里开发时,可在实现global layer时直接调用前面bmkernel中写好的 bm_xxx_kernel(…)函数

void bm_xxx_kernel(...); //声明bmkernel中写好的kernel函数

__BM_GLOBAL__(TPU_XXX) int xxx_global(const void* param, const int param_size,  // layer param
                const tpu_tensor_t* in_tensors, const int in_num,               // input tensors
                const bm_addr_t buffer_addr, const bm_size_t buffer_size,       // buffer
                tpu_tensor_t* out_tensors, const int out_num){                  // output tensors
    ...
    // 这里直接调用
    bm_xxx_kernel(
        in_tensors[0].addr,
        out_tensors[0].addr,
        layer_param->param1,
        layer_param->param2,
    );
    return 0;
}

在编译前,由于调用了bmkernel_demo的文件, 设置bmkernel的路径

set_bmkernel_path path/to/bmkernel/project

4. 使用说明

  1. 目录说明

编译完成后,全部文件会在install目录下

install/
|-- firmware
|   |-- firmware.so             # cmodel的firmware
|   |-- firmware_layer.a        # 仅包含本地device目录代码的库文件,可以用于和其他firmware合并
|   |-- firmware_ddr.bin        # 加载到ddr的firmware, load_firmware时使用
|   `-- firmware_tcm.bin        # 加载到tcm的firmware, load_firmware时使用
|-- include
|   `-- bmtpu.h                 # 相关的定义文件,供外部引用
|-- bin
|   `-- load_firmware           # 辅助程序,用于加载firmware,使用方法`load_firmware firmware_tcm firmware_ddr [dev_id]`
|-- test
|   `-- test_tpu_layer          # 测试程序,使用方法`test_tpu_layer [0|1]`,0表示静态,1表示动态
`-- plugin
    `-- libbmplugin_tpulayer.so # bmcompiler用到的插件,用于注册算子信息
  1. 带有自定义layer编译模型

  1. 设置环境变量 export BMCOMPILER_PLUGIN_PATH=./install/plugin:$BMCOMPILER_PLUGIN_PATH 。注意这里考虑了会存在多个plugin的情况

  2. 编译带有自定义layer的网络模型

  1. 在设备上运行

  1. 设置device版的libbmlib.so所在目录到LD_LIBRARY_PATH环境变量中

  2. 利用install/bin/load_firmware程序: 使用方法 load_firmware firmware_tcm firmware_ddr [dev_id] 。 注意:只有动态或有动态子网模型才需要这步,但保险起见,最好执行一次;开机后加载一次即可,之后运行不再需要

  3. 运行编译出的模型

  1. 在cmodel上运行

  1. 设置cmodel版的libbmlib.so所在目录到LD_LIBRARY_PATH环境变量中

  2. 通过bmruntime的环境变量 export BMRUNTIME_FIRMWARE_PATH=./install/firmware

  3. 运行编译出的模型

5. 自定义bmkernel层接入

以上说明,可以对bmcompiler增加bmkernel编程的自定义层,这里介绍如何在Graph构建中接入该自定义层。

接入自定义bmkernel层的接口如下:

/**
 * @brief add_tpu_layer
 * @param p_bmcpl
 * @param input_num
 * @param input_names
 * @param input_shapes
 * @param input_dims
 * @param input_dtypes
 * @param output_num
 * @param output_names
 * @param output_shapes: can be NULL, shape_infer will work
 * @param output_dims: can be NULL, shape_infer will work
 * @param output_dtypes: can be NULL, shape_infer will work
 * @param op_type
 * @param layer_param
 * @param param_size
 */
void add_tpu_layer(
    void*               p_bmcpl,
    int                 input_num,
    const char* const*  input_names,         /* input1 name, input2 name...*/
    const int* const*   input_shapes,        /* input1 shape, input2 shape */
    const int*          input_dims,          /* input1 dim, input2 dim,... */
    const int*          input_dtypes,        /* bm_data_type_t: DTYPE_FP32 ... */
    int                 output_num,
    const char* const*  output_names,
    const int* const*   output_shapes,
    const int*          output_dims,
    const int*          output_dtypes,      /* bm_data_type_t: DTYPE_FP32 ... */
    int                 op_type,
    const void*         layer_param,        /* not parse in compiler */
    int                 param_size
 );

其中,op_type是生成的bmtpu.h中tpu_op_type_t中指定layer,layer_param是bmtpu.h中的指定layer param,param_size则是指定layer param的sizeof。

通过调用该接口,即可在构建Graph时插入该层。

6. demo使用说明

编译bmodel:

  1. install/test目录下有test_tpu_layer,里面利用bmcompiler的原生接口生成了一个简单测试网络。这个example包含4个算子,分别是自定义的tpu(crop)和tpu(double),内建的expand_dim和const_binary:

    tpu(crop)->expand_dim->const_binary->tpu(double)

    1. 在scripts里source envsetup_pcie.sh或者envsetup_cmodel.sh

    2. 根据目标平台设置example/bmkernel_demo/Makefile的HOST_ARCH,以及bmnet/bmcompiler/user_layers/Makefile.config里的FIRMWARE_ARCH 例如:如果在x86平台下用的pcie卡,则HOST_ARCH ?= x86, FIRMWARE_ARCH ?= pcie;如果用的SOC模式,则HOST_ARCH ?= aarch64, FIRMWARE_ARCH ?= soc。

    3. 在user_layres目录下,source envsetup.sh,然后rebuild_layers或者build_layers

    4. export BMCOMPILER_PLUGIN_PATH=/path/to/install/plugin/libbmplugin_tpu_layer.so

    5. 运行 install/test/test_tpu_layer 0 , 会进行静态编译,生成tpulayer_static的模型目录

    6. 运行 install/test/test_tpu_layer 1 , 会进行动态编译,生成tpulayer_dynamic的模型目录

  2. 结合imp_bmnetu用自定义Layer实现yolov3的后处理层detection_output,从而实现完整的yolov3网络

    1. 编译user_layers,生成相应文件,步骤同上面的a, b, c三步;

    2. 设置plugin路径: export BMCOMPILER_PLUGIN_PATH=path/to/libbmplugin_tpu_layer.so;

    3. 到imp_bmnetu目录编译imp_bmnetu,make && make isntall,生成libimpbmnetu.so。因为imp_bmnetu里已经增加了名为TPUYolov3DetectOutLayer的新layer, 里面通过add_tpu_layer接口接入到了user_layers中自定义的yolov3_detect_out层;

    4. 编译imp_bmnetu是如果遇到报错#include “interface/bmcompiler_op_code.h”,将该头文件改成#include “bmcompiler_op_code.h”

    5. 使用bmnetu编译模型,命令为/path/to/bmnetu –model yolov3.prototxt –weight yolov3.caffemodel –target=BM1684 –v=4 模型可在网盘中获取(链接: https://pan.baidu.com/s/1w-x3qVpUjWsftgT9aDD48g 提取码: acwa)。 该protoxt中type: “TPUYolov3DetectOut”,即是imp_bmnetu中定义的自定义layer。

    6. 查看log,我们可以找到layer_id 376 “TPU(YOLOV3_DETECT_OUT)”,这里表示该自定义层成功融入到bmnet中。

    7. bmnetu结束后生成bmodel

运行bmodel:

  1. 在PCIE卡上运行

    1. 到user_layers里执行 install/bin/load_firmware install/firmware/firmware_tcm.bin $PWD/install/firmware/firmware_ddr.bin

    2. 执行 bmrt_test --bmodel /path/to/bmodel

  2. 在cmodel上运行

    1. 到user_layers里执行 export BMRUNTIME_FIRMWARE_PATH=$PWD/install/firmware

    2. 执行 bmrt_test --bmodel /path/to/bmodel

  3. 在SOC上运行

    1. 将install中的load_firmware、firmware_tcm.bin、firmware_ddr.bin拷贝到SOC上

    2. 执行 load_firmware  firmware_tcm.bin firmware_ddr.bin

    3. 将bmodel拷贝到SOC上,执行 bmrt_test --bmodel /path/to/bmodel

7. 注意事项

layer注册是通过bmcompiler插件机制完成的;对于注册layer的插件,不能超过一个,否则可能由于op_type重复造成内部混乱