3.1 Split+Sub+Concat合并算子的BCL实现
3.1.1 需求分析
要实现这个算子,首先要知道这个算子具体要做什么。首先尝试读论文《EAST: An Efficient and Accurate Scene Text Detector》和《PVANET: Deep but Lightweight Neural Networks for Real-time Detection》,从论文中大致读懂了算法的框架,如下图,但是并没有什么用:

图3-1 算法框架
之后从eval
Python脚本中查找相关信息,其中某处引用了model.py
中的model(images, weight_decay, is_training)
函数,然后逐渐发现mean_image_subtraction(images, means)
就是我要找的函数:
代码3-1 Split-Sub-Concat算子的Python实现
def mean_image_subtraction(images, means=[123.68, 116.78, 103.94]):
'''
image normalization
:param images:
:param means:
:return:
'''
num_channels = images.get_shape().as_list()[-1] # 获取最后一个维度
if len(means) != num_channels:
raise ValueError('len(means) must match the number of channels')
# 在channel维度上拆分
channels = tf.split(axis=3, num_or_size_splits=num_channels, value=images)
for i in range(num_channels):
channels[i] -= means[i] # 每个通道减不同的数字
return tf.concat(axis=3, values=channels)123456789101112131415
从上述代码中可以看出,算子首先需要在通道上拆分,然后3个通道分别减去means的3个值,最后再拼接。
然后要计算means值:对每一张图片求平均,还是一个常数?从Python代码中发现是常数,所以这个means值应该是一个图片数据集所有图片的平均,不能自己计算。
另外还需要知道图像的batch、宽、高和通道分别在哪个维度上。从channels = tf.split(axis=3,...)
中可以看出通道在第3个维度,另外按照习惯,第0个维度是batch_size
,第1个维度是宽度,第2个维度是高度。从main.cpp
中得知,图像的宽度是1280,高度是672。
3.1.2 算子设计
由于图像数据的排列方式,means的3个数字是交错的。而MLU处理连续的数据比较迅速,所以简单的对每个位置求余数,根据余数减去相应的值,耗时一定会比较长。

图3-2 图像数据和平均值的位置关系
所以需要在nram上构造一些数据块,以便进行整体的减法。这样的话需要(1)决定每个块的大小,(2)如何对块赋值。
对于问题(1),根据MLU的要求,块的大小需要是16的倍数;其次,为了让每次迭代,图像都减去相同的数据块,块大小需要是3的倍数;最后,在taskdim
为16的情况下,nram
块的大小不能超过512。因此我决定设置块大小为384=128×3
。
对于问题(2),最简单的思路是用for循环,如下所示:
代码3-2 用for循环对减数赋值
__nram__ minuse[384];
for(int i = 0; i < 384; ++i) {
switch(i % 3) {
case 0: minuse[i] = VALUE1; break;
case 1: minuse[i] = VALUE2; break;
case 2: minuse[i] = VALUE3; break;
}
}12345678
但是经过测试,这样的延迟比较高。所以我考虑不用计算的方式,而是直接赋值。所以我用下面的代码来生成代码,粘贴到mlu文件中。
代码3-3 用于生成代码的Python脚本
values = ["(half)123.68", "(half)116.78", "(half)103.94"]for i in range(384):
print("minused_nram[%d] = %s;" % (i, values[i%3]))123
代码3-4 生成的部分代码
minused_nram[0] = (half)123.68;
minused_nram[1] = (half)116.78;
minused_nram[2] = (half)103.94;
minused_nram[3] = (half)123.68;
minused_nram[4] = (half)116.78;
minused_nram[5] = (half)103.94;
......1234567
接下来要对这种方法的速度进行测试,首先在使用这种方法的情况下,查看运行时间,然后把这些赋值的代码全部注释掉,再查看运行时间。经过比较得到结论:这种方法的速度是比较快的。
数据块构造完成之后如何做减法。BANGC提供了__bang_cycle_sub
,其功能是,假设有长度m × n m×nm×n的被减数数组和长度为n nn的减数数组,则将被减数数组拆分成m mm份,分别减去减数数组。同时为了减少拷贝次数,我将块大小乘了64倍,但是减数minused_nram
保持不变。运行结果见图3-2。
代码3-5 用__bang_cycle_sub做减法
#define ONELINE 384#define MUCH_DOUBLE 64
int quotient = batch_num_ * 1280 * 672 * 3 / ONELINE;
__nram__ half input_nram[ONELINE * MUCH_DOUBLE];
quotient = quotient / MUCH_DOUBLE;
for (int32_t i = taskId; i < quotient; i += taskDim) {
offset = i * ONELINE * MUCH_DOUBLE;
// 查看是否遍历了所有数据
// __bang_printf("loop1 %d\t%d\t%d\n", taskId, i, offset);
__memcpy(input_nram, input_data_ + offset,
MUCH_DOUBLE * ONELINE * sizeof(half), GDRAM2NRAM);
__bang_cycle_sub(input_nram, input_nram, minused_nram,
ONELINE * MUCH_DOUBLE, ONELINE);
__memcpy(output_data_ + offset, input_nram,
MUCH_DOUBLE * ONELINE * sizeof(half), NRAM2GDRAM);}12345678910111213141516

图3-2 算子测试
3.2 算子集成
算子集成分为两步,用BCL将上述算子集成到CNPlugin中,和通过CNPlugin接口集成到TensorFlow框架中。
3.2.1 集成到CNPlugin框架
补全plugin_sbc_op.cc
文件:
对于cnmlCreatePluginSBCOp
函数,首先获得Kernel的ParamsBuffer
,然后根据算子的函数声明,将第一个参数标记为Input
,将第二个参数标记为Output
,然后传入第三个参数batch_num_
。然后用cnmlCreatePluginOp
创建算子。
代码3-6 cnmlCreatePluginSBCOp
函数
// 补全cnmlCreatePluginSBCOp
cnrtKernelParamsBuffer_t params;
cnrtGetKernelParamsBuffer(¶ms);
cnrtKernelParamsBufferMarkInput(params);
cnrtKernelParamsBufferMarkOutput(params);
cnrtKernelParamsBufferAddParam(params, &batch_num_, sizeof(int));
// 由于main.cpp中只添加了3个参数,所以core_num_应该是不用添加的
cnmlCreatePluginOp(
op, "SBC",
reinterpret_cast<void **>(&SBCKernel), params,
SBC_input_tensors, 1,
SBC_output_tensors, 1,
nullptr, 0
);
cnrtDestroyKernelParamsBuffer(params);12345678910111213141516
对于cnmlComputePluginSBCOpForward
函数,调用cnmlComputePluginOpForward_V4
即可。
代码3-7 cnmlComputePluginSBCOpForward函数
// 补全cnmlComputePluginSBCOpForward
cnmlComputePluginOpForward_V4(
op,
nullptr, inputs, input_num,
nullptr, outputs, output_num,
queue, nullptr
);1234567
补全cnplugin.h
文件:
定义cnmlPluginSBCOpParam_t
结构体,如下
代码3-8 cnmlPluginSBCOpParam结构体定义
struct cnmlPluginSBCOpParam {
int batch_num_;
cnmlCoreVersion_t core_version;};typedef cnmlPluginSBCOpParam *cnmlPluginSBCOpParam_t;12345
添加函数声明,如下
代码3-9 cnml函数声明
cnmlStatus_t cnmlCreatPluginSBCOpParam(
cnmlPluginSBCOpParam_t *param,
int batch_num_);cnmlStatus_t cnmlDestroyPluginSBCOpParam(
cnmlPluginSBCOpParam_t *param);cnmlStatus_t cnmlCreatePluginSBCOp(
cnml Op_t *op,
//cnmlPluginSBCOpParam_t param,
cnmlTensor_t *SBC_input_tensors,
cnmlTensor_t *SBC_output_tensors,
int batch_num_);cnmlStatus_t cnmlComputePluginSBCOpForward(
cnml Op_t op,
void **inputs,
int input_num, // == 1
void **outputs,
int output_num, // == 1
cnrtQueue_t queue);123456789101112131415161718
然后将算子的文件拷贝到相关文件夹中,进行算子编译。在编译中出现了错误:tensorflow/stream_executor/mlu/mlu_api/lib_ops/mlu_lib_ops.cc:1926:96: error: 'cnmlCreatePluginPowerDifferenceOp' was not declared in this scope
过思考后发现,这个错误应该是mlu_lib_ops.cc
调用了PowerDifference算子,而cnplugin.h
中没有PowerDifference算子的信息。所以我将实验一中PowerDifference算子的声明也添加到了cnplu