编译器自定义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``后,终端可以使用以下命令:
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相关文件已经存在,会报添加失败
del_layer name
:删除layer,会依次询问是否删除相关文件
name为自定义layer的名称;
list_layers
列出当前已定义的layer
set_bmkernel_path path/to/bmkernel
设置bmkernel_demo的工程目录,在gen_config中会引入相关的必要文件路径
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
build_bmkernel
根据set_bmkernel_path设置的路径,生成bmkernel相关的库
build_layers
完整地编译整个工程,相当于`build_bmkernel && gen_config && gen_entry && make all`
rebuild_layers
清空所有生成文件,重新生成config,并build_layers
3. 接口代码修改¶
利用add_layer增加新的xxx layer后, 需要实现以下接口:
在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
在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"; }
}
相关数据结构说明¶
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;
标记宏(定义在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
- 接口函数返回值(定义在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)
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指令均可使用
- 通常的使用模式是:
通过gdma将输入数据从global mem搬运到local mem中
在local mem中完成相关运算
通过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运算指令
- 通常的使用模式是:
在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. 使用说明¶
目录说明
编译完成后,全部文件会在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用到的插件,用于注册算子信息
带有自定义layer编译模型
设置环境变量
export BMCOMPILER_PLUGIN_PATH=./install/plugin:$BMCOMPILER_PLUGIN_PATH
。注意这里考虑了会存在多个plugin的情况编译带有自定义layer的网络模型
在设备上运行
设置device版的libbmlib.so所在目录到LD_LIBRARY_PATH环境变量中
利用install/bin/load_firmware程序: 使用方法
load_firmware firmware_tcm firmware_ddr [dev_id]
。 注意:只有动态或有动态子网模型才需要这步,但保险起见,最好执行一次;开机后加载一次即可,之后运行不再需要运行编译出的模型
在cmodel上运行
设置cmodel版的libbmlib.so所在目录到LD_LIBRARY_PATH环境变量中
通过bmruntime的环境变量
export BMRUNTIME_FIRMWARE_PATH=./install/firmware
运行编译出的模型
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:
install/test目录下有test_tpu_layer,里面利用bmcompiler的原生接口生成了一个简单测试网络。这个example包含4个算子,分别是自定义的tpu(crop)和tpu(double),内建的expand_dim和const_binary:
tpu(crop)->expand_dim->const_binary->tpu(double)
在scripts里source envsetup_pcie.sh或者envsetup_cmodel.sh
根据目标平台设置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。
在user_layres目录下,source envsetup.sh,然后rebuild_layers或者build_layers
export BMCOMPILER_PLUGIN_PATH=/path/to/install/plugin/libbmplugin_tpu_layer.so
运行
install/test/test_tpu_layer 0
, 会进行静态编译,生成tpulayer_static的模型目录运行
install/test/test_tpu_layer 1
, 会进行动态编译,生成tpulayer_dynamic的模型目录
结合imp_bmnetu用自定义Layer实现yolov3的后处理层detection_output,从而实现完整的yolov3网络
编译user_layers,生成相应文件,步骤同上面的a, b, c三步;
设置plugin路径: export BMCOMPILER_PLUGIN_PATH=path/to/libbmplugin_tpu_layer.so;
到imp_bmnetu目录编译imp_bmnetu,make && make isntall,生成libimpbmnetu.so。因为imp_bmnetu里已经增加了名为TPUYolov3DetectOutLayer的新layer, 里面通过add_tpu_layer接口接入到了user_layers中自定义的yolov3_detect_out层;
编译imp_bmnetu是如果遇到报错#include “interface/bmcompiler_op_code.h”,将该头文件改成#include “bmcompiler_op_code.h”
使用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。
查看log,我们可以找到layer_id 376 “TPU(YOLOV3_DETECT_OUT)”,这里表示该自定义层成功融入到bmnet中。
bmnetu结束后生成bmodel
运行bmodel:
在PCIE卡上运行
到user_layers里执行
install/bin/load_firmware install/firmware/firmware_tcm.bin $PWD/install/firmware/firmware_ddr.bin
执行
bmrt_test --bmodel /path/to/bmodel
在cmodel上运行
到user_layers里执行
export BMRUNTIME_FIRMWARE_PATH=$PWD/install/firmware
执行
bmrt_test --bmodel /path/to/bmodel
在SOC上运行
将install中的load_firmware、firmware_tcm.bin、firmware_ddr.bin拷贝到SOC上
执行
load_firmware firmware_tcm.bin firmware_ddr.bin
将bmodel拷贝到SOC上,执行
bmrt_test --bmodel /path/to/bmodel
7. 注意事项¶
layer注册是通过bmcompiler插件机制完成的;对于注册layer的插件,不能超过一个,否则可能由于op_type重复造成内部混乱