1 实验5-1-智能编程语言算子开发与集成实验
1.1 实验目的
本节实验通过使用智能编程语言进行算子开发,对高性能库算子进行扩展,并最终集成到编程框架中,掌握对高性能库及编程框架进行扩展的能力,使读者可以在 DLP 硬件上自由设计并优化满足特定应用场景的新算子,满足日新月异智能算法的实际需求。
具体而言,在前述章节中我们用 DLP 硬件的高性能库替换了风格迁移网络中大部分 CPU 算子,但仍有需要在 CPU 上执行的 PowerDifference 算子。由于 PowerDifference 算子在 CPU 上执行,导致该算子计算的前后存在 DLP 和 CPU 之间的数据交互,即 DLP 到 CPU 的数据拷出以及CPU到DLP的数据拷入,增加了额外的访存带宽和功耗,影响了整体的执行效率。因此,本实验通过智能编程语言实现 PowerDifference 算子,掌握使用智能编程语言进行算子开发,扩展高性能库算子,并最终集成到 TensorFlow 框架中的方法和流程,使 得完整的风格迁移网络可以在 DLP 硬件上高效执行。
1.2 实验要求
• 60分标准:完成 PowerDifferenece BCL 算子的实验以及基于 CNRT 的测试,在测试数 据中精度误差在 1% 以内,延时在 100ms 以内; • 80分标准:在 60 分基础上,完成 BCL 算子与 TensorFLow 框架的集成,使用 python 在 Device 端测试大规模数据时,精度误差在 10% 以内,平均延时在 150ms 以内。 • 90分标准:在 80 分基础上,使用 DLP 推理完整 pb 模型时,输出精度正常的风格迁 移图片,输出正确的离线模型。 • 100分标准:在 90 分基础上,完成离线推理程序的编写,执行离线推理时风格迁移图 片精度正常。
1.3 实验步骤及心得收获
本次实验内容包括:
算子实现: 采用智能编程语言 BCL 实现 PowerDifference 算子并完成相应测试。首先,使用 BCL 的内置向量函数实现计算 Kernel,并利用 CNRT 接口直接调用 Kernel 运行并测试功能正确性;
框架集成: 通过高性能库 PluginOp 的接口对 PowerDifference 算子进行封装,使其调用方式和高性能库原有算子一致,将封装后的算子集成到 TensorFlow 框架中并进行测试,保证其精度和性能正确;
在线推理: 通过 TensorFlow 框架的接口,在内部高性能库 CNML 和运行时库 CNRT的配合下,完成对风格迁移模型的在线推理,并生成离线模型;
离线推理: 采用运行时库 CNRT 的接口编写应用程序,完成离线推理,并将其结果和第三步中的在线推理,以及第4.4节中的推理性能进行对比
实验步骤: 1、环境变量初始化:先进入env/,执行source env.sh; 再进入tensorflow-v1.10/,执行source env.sh 2、bangc算子填写:补齐src/bangc/PluginPowerDifferenceOp/plugin_power_difference_kernel.h,plgin_power_difference_kernel.mlu 和powerDiff.cpp,执行make.sh进行编译,运行power_diff_test测试 3、集成到cnplugin: 补齐src/bangc/PluginPowerDifferenceOp/cnplugin.h和plugin_power_difference_op.cc,将整个PluginPowerDifferenceOp文件夹 复制到env/Cambricon-CNPlugin-MLU270/pluginops,在Cambricon-CNPlugin-MLU270目录下执行build_cnplugin.sh重新编译cnplugin; 编译完成后将build/libcnplugin.so和cnplugin.h分别拷入到env/neuware/lib64和env/neuware/include中。 注:cnplugin.h中PowerDifference算子的声明可以参考其他算子声明来进行添加,plugin_power_difference_op.cc中的算子函数定义可以参考pluginops目录下其他算子的定义实现 4、集成到tensorflow: 补齐src/tf-implementation/tf-add-power-diff/power_difference.cc和cwise_op_power_difference_mlu.h, 按照readme.txt提示拷入到对应文件夹,重新编译tensorflow 5、框架算子测试:补齐src/online_mlu/power_difference_test_bcl.py 6、在线推理和生成离线模型:补齐src/online_mlu/power_difference_test_bcl.pysrc/online_mlu/transform_mlu.py 7、离线推理:补齐src/offline/src/inference.cpp
自动测试需要提交的文件: ├── inference.cpp ├── libcnplugin.so // 重新编译cnplugin生成的库文件 ├── plugin_power_difference_kernel.h ├── plugin_power_difference_kernel.mlu ├── powerDiff.cpp ├── power_difference_test_bcl.py ├── tensorflow_mlu-1.14.0-cp27-cp27mu-linux_x86_64.whl // 重新编译tensorflow生成的whl └── transform_mlu.py 将以上文件压缩成zip格式进行提交
1.3.1 Kernel 实现
1)Kernel程序编写
实验的主要内容需要完成 mlu_entry 函数供 CNRT 或 CNML 调用。这样可供调用的 mlu_entry 函数称为一个 Kernel。 采用智能编程语言BCL实现PowerDifference算子的计算逻辑并进行正确性测试。补全plugin_power_difference_kernel.h
,plugin_power_difference_kernel.mlu
。

2) 运行时程序编写
运行时程序通过利用运行时库 CNRT 的接口调用 BCL 算子来实现。首先声明被调用的算子实现函数,然后在 MLUPowerDifferenceOp 中通过一系列 CNRT 接口的调用完成,包括:使用 cnrtKernelParamsBuffer 来设置 PowerDifference 算子的输入参数,补全powerDiff.cpp
。

3)编译运行
./make.sh
./power_diff_test
1.3.2 框架算子集成
通过高性能库PluginOp的接口对PowerDifference算子进行封装,使其调用方式和高性能库原有算子一致,将封装后的算子集成到TensorFlow编程框架中并进行测试,保证其精度和功能正确行。
为了使算子往TensorFlow中集成更加模块化,这里对算子进行了多个层次的封装,如下图。
自底向上分为以下几个层次:
- MLULib层:对CNML和CNPlugin算子的直接封装,封装的结果供MLUOp层调用,这一层封装的目的是将高层调用和底层的计算库的接口实现有效的隔离,避免相互干扰。
- 封装MLUOp层:负责TensorFlow算子的DLP实现,调用MLULib实现算子后供MLUStream层调用。可以只调用单独的MLULib算子,也可以调用多个MLULib算子拼接为更复杂的TensorFlow算子。
- MLUStream层:与MLUOpKernel类接口相关联,负责MLU算子的实例化并与运行时队列结合。
- 封装MLUOpKernel:定义并组册最终运行的算子类MLUOpKernel,集成TensorFLow中的OpKernel,作为与TensorFlow算子层的接口。
- 算子注册:注册最终的算子供上层调用。
- 封装MLULib层
MLULib层的调用:
- 算子融合
根据实验手册补全plugin_power_difference_op.cc
和cnplugin.h
。
1 // file: plugin_power_difference_op.cc
////////////////////////////////////
// 参数初始化(常量数据)
cnmlStatus_t cnmlCreatePluginPowerDifferenceOpParam(
cnmlPluginPowerDifferenceOpParam_t *param,
// TODO:添加变量
) {
*param = new cnmlPluginPowerDifferenceOpParam();
// TODO:配置变量
return CNML_STATUS_SUCCESS;
}
//////////////////////////////////////
3 // 算子创建接口:cnmlCreatePluginPowerDifferenceOp
4 cnmlStatus_t cnmlCreatePluginPowerDifferenceOp (
5 cnmlBaseOp_t *op,
6 cnmlTensor_t* input_tensors,
7 int power,
8 cnmlTensor_t* output_tensors,
9 int len
10 ) {
11 void** InterfacePtr;
12 InterfacePtr = reinterpret_cast <void**>(&PowerDifferenceKernel);
13 // 传递参数
14 cnrtKernelParamsBuffer_t params;
15 cnrtGetKernelParamsBuffer(¶ms);
16 cnrtKernelParamsBufferMarkInput(params); // input 0
17 cnrtKernelParamsBufferMarkInput(params); // input 1
18 cnrtKernelParamsBufferAddParam(params, &power, sizeof(int));
19 cnrtKernelParamsBufferMarkOutput(params); // output 0
20 cnrtKernelParamsBufferAddParam(params, &len, sizeof(int));
21 cnmlCreatePluginOp(op,
22 "PowerDifference",
23 InterfacePtr,
24 params,
25 input_tensors,
26 2,
27 output_tensors,
28 1,
29 nullptr,
30 0);
31 cnrtDestroyKernelParamsBuffer(params);
32 return CNML_STATUS_SUCCESS;
33 }
34 // 算子计算接口:cnmlComputePluginPowerDifferenceOpForward
35 cnmlStatus_t cnmlComputePluginPowerDifferenceOpForward(
36 cnmlBaseOp_t op,
37 void **inputs,
38 void **outputs,
39 cnrtQueue_t queue
40 ) {
41 cnmlComputePluginOpForward_V4(op,
42 nullptr,
43 inputs,
44 2,
45 nullptr,
46 outputs,
47 1,
48 queue,
49 nullptr);
50 return CNML_STATUS_SUCCESS;
51 }
cnmlPluginPowerDifferenceOpParam_t
的实现可以参考cnplugin.h
中其他算子的实现。
编译生成.so
文件:
1. 将整个PluginPowerDifferenceOp文件夹复制到env/Cambricon-CNPlugin-MLU270/pluginops
2. 在Cambricon-CNPlugin-MLU270目录下执行build_cnplugin.sh重新编译cnplugin;
3. 编译完成后将build/libcnplugin.so和cnplugin.h分别拷入到env/neuware/lib64和env/neuware/include中。
- 封装MLUOp层
MLUOp层负责TensorFlow算子的DLP实现,调用MLULib实现算子后供MLUStream层调用。可以只调用单独的MLULib算子,也可以调用多个MLULib算子拼接为更复杂的TensorFlow算子。
powerdifference.cc
的代码补全可以参考env/tensorflow-v1.10/tensorflow/stream_executor/mlu/mlu_api/ops/squared_difference.cc
,唯一不同的是PowerDifference需要计算tensor的length。
- 封装MLUStream层
MLUStream主要是在MLUStream层添加算子类说明,与MLUOpKernel类接口相关联,负责MLU算子的实例化并与运行时队列结合。
1 Status PowerDifference(OpKernelContext* ctx,
2 Tensor* input1, Tensor* input2, Tensor* output, int input3) {
3 return CommonOpImpl<ops::MLUPowerDifference>(ctx,
4 {input1, input2}, {output}, static_cast <void*>(&input3));
5 }
- 封装MLUOpKernel层
定义MLUOpKernel层接口主要是在MLUOpKernel层定义MLUPowerDifferenceOp,在其中通过stream机制调用MLUStream层具体的PowerDifference函数。
- 算子注册
注册最终的算子供上层调用。
PowerDifference DLP算子会与CPU算子共享tensorflow/core/ops/math_ops.cc中的算子注册方法,这样用户可以使用相同的Python API(powerdifference)调用自定义算子,在编程上无需感知底层硬件的差异,通过环境变量来区分。
os.environ['MLU_VISIBLE_DEVICES']="0"
1 REGISTER_OP("PowerDifference")
2 .Input ("x: T")
3 .Input ("y: T")
4 .Input ("pow: T")
5 .Output ("z: T")
6 .Attr (
7 "T: {bfloat16, float, half, double, int32, int64, complex64,"
8 "complex128}")
9 .SetShapeFn ([] (:: tensorflow:: shape_inference:: InferenceContext* c) {
10 c−>set_output (0, c−>input (0));
11 c−>set_output (0, c−>input (1));
12 c−>set_output (0, c−>input (2));
13 return Status::OK();
14 });
算子集成到Tensorflow之后需要重新进行编译,按照readme.txt提示拷入到对应文件夹,重新编译tensorflow。
# file src/tf-implementation/tf-add-power-diff/readme.txt
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/core/kernels/cwise_op_power_difference*
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/core/kernels/BUILD
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/stream_executor/mlu/mlu_stream.h
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/stream_executor/mlu/mlu_api/lib_ops/mlu_lib_ops.*
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/stream_executor/mlu/mlu_api/ops/mlu_ops.h
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/stream_executor/mlu/mlu_api/ops/power_difference.cc
/opt/code_chap_5_student/env/tensorflow-v1.10/tensorflow/core/ops/math_ops.cc
编译结果如下:
INFO: Elapsed time: 1909.924s, Critical Path: 192.78s
INFO: 9895 processes: 9895 local.
INFO: Build completed successfully, 10553 total actions
...
Installing collected packages: tensorflow-mlu
Attempting uninstall: tensorflow-mlu
Found existing installation: tensorflow-mlu 1.14.0
Uninstalling tensorflow-mlu-1.14.0:
Successfully uninstalled tensorflow-mlu-1.14.0
Successfully installed tensorflow-mlu-1.14.0
补齐online_mlu/power_difference_test_bcl.py
comput BCL op cost 147.653102875ms
comput op cost 255.165815353ms
err rate= 0.06631744635811314
1.3.3 模型在线推断
通过TensorFlow框架的接口,在内部高性能库CNML和运行时库CNRT的配合下,完成对风格迁移模型的在线推理,并生成离线模型。
参考实验4-4的transform_cpu.py
补全transform_mlu.py
。
config.mlu_options.data_parallelism = 1
config.mlu_options.model_parallelism = 1
config.mlu_options.core_num = 16
config.mlu_options.core_version = "MLU270"
config.mlu_options.precision = "int8"
config.mlu_options.save_offline_model = True
# run_ori_power_diff_pb
input_pow = np.array(2, dtype=float)
ret = sess.run(output_tensor, feed_dict={input_tensor:[X], input_tensor_pow: input_pow})
# run_numpy_pb
input_pow = 2
output = power_diff_numpy(input_x, input_y, input_pow).reshape(1, 256, 256, 3)
底层算子在实现的时候是通过dim_size来确定的power_value,所以传入的是一个张量。
1.3.4 模型离线推断
通过上一节的在线推断,可以得到所有算子在DLP硬件上运行的实时风格迁移离线模型,在实际场景中,为了尽可能提高部署的效率,通常会选择离线部署的方式。
离线部署与在线的区别主要是脱离了TensorFlow编程框架和高性能库CNML,仅与运行时库CNRT有关,减少了不必要的开销,提升了执行效率。
在编写离线模型时,DLP目前只支持C++语言,主要包括输入数据预处理、离线预测以及后处理。
- main函数
1 //file:src/style_transfer.cpp
2 #include "style_transfer.h"
3 #include <math.h>
4 #include <time.h>
5 #include "stdio.h"
6 #include <stdlib.h>
7 #include <sys / time.h>
8
9 int main(int argc, char** argv){
10 // 解析参数
11 std:: string file_list = "/path/to/images/" + std:: string (argv [1]) + ".jpg";
12 std:: string offline_model = "/path/to/models/offline_models/" + std:: string (argv[2]) + ".cambricon";
13
14 // 创建数据
15 DataTransfer* DataT =(DataTransfer *) new DataTransfer();
16 DataT−>image_name = argv [1];
17 DataT−>model_name = argv [2];
18 // 处理图像 474x712 to 256x256
19 DataProvider *image = new DataProvider (file_list);
20 image−>run (DataT);
21
22 // 运行推断
23 Inference *infer = new Inference (offline_model);
24 infer −>run (DataT);
25
26 // 图像后处理
27 PostProcessor *post_process = new PostProcessor();
28 post_process −>run (DataT);
29
30 delete DataT;
31 DataT = NULL;
32 delete image;
33 image = NULL;
34 delete infer;
35 infer = NULL;
36 delete post_process;
37 post_process = NULL;
38 }
- CNRT离线推断
采用运行时库CNRT的接口编写应用程序,完成离线推理,并将其结果和在线推理以及4.4节中的模型推断进行性能对比。

需要对输入数据的格式进行转换float32 to flaot16,另外还需要对输入的数据的维度进行变换。
extern cnrtRet_t cnrtTransDataOrder(void *src_addr,
cnrtDataType_t data_type,
void *dst_addr,
int dimNum,
int dimValues[],
int dimOrder[]);
extern CNRT_DLL_API cnrtRet_t cnrtCastDataType(void *src_addr,
cnrtDataType_t src_data_type,
void *dst_addr,
cnrtDataType_t dst_data_type,
int data_num,
cnrtQuantizedParam_t param);
name没有给出来,可以参考文件models/offline_models/udnie_int8_power_diff.cambricon_twins
cnrtExtractFunction(&function, model, "subnet0");
代码补全之后,进入build文件夹,重新编译代码,然后运行run.sh
进行测试。
cd build
cmake ..
make
1.4 实验结果
PowerDifference BCL算子的实现 | BCL算子与TensorFlow框架的集成 | 使用DLP推理完整pb模型 | 编写离线推理应用 |
---|---|---|---|
通过 | 通过 | 通过 | 通过 |
2 实验5-2-智能编程语言性能优化实验
2.1 实验目的
对于一般情况而言,智能编程语言可以简单的快速开发算子从而达到计算功能扩展实 现深度学习需求的目的。但是对于一些对算力要求较高的瓶颈算子则需要兼顾算法本身结 构特点和 DLP 架构优势进一步的细致优化。因此,本实验的主要目的是通过矩阵乘这样一 个在现实中非常常见的计算需求经过一系列的优化手段,最终帮助读者加深对智能计算系 统和智能编程语言的理解。
2.2 实验要求
• 60 分标准:在规模 m=256,k=256,n=327680 下性能与实验效果误差在 10% 以内。
• 80分标准:在规模 m=256,k=256,n=327680 下性能与实验效果误差在 1% 以内。
• 90分标准:在规模 m=256,k=256,n=327680 下性能与实验效果误差在 0.1% 以内,耗时 小于 20ms。
• 100 分标准:在规模 m=256,k=256,n=327680 下性能与实验效果误差在 0.06% 以内,耗 时小于 15ms。
2.3 实验步骤及心得收获
下图为MLU加速卡的架构,有4个Cluster,每个Cluster有4个Core,内存方面:全局内存GDRAM,每个Cluster的SRAM,每个Core有NRAM和WRAM。

2.3.1 标量操作
每个DLP的计算核都有自己的NRAM,虽然相对于GDRAM,内存空间较小,但是其具有更高的读写带宽和更低的延迟。下面的标量实现将矩阵全部读到NRAM中,然后循环进行计算,如果输入矩阵过大,则需要多次读写NRAM。

2.3.2 单核向量化
将输入矩阵A和B分别存放在DLP计算核的两个存储单元NRAM和WRAM,然后使用DLP的卷积指令做矩阵乘法计算。当矩阵B的规模很大时,需要分批次(N/256)拷贝,另外在使用卷积指令计算时,需要将矩阵转化为__bang_conv支持的格式(内存对齐)。

void __memcpy (void* dst, void* src, int size, mluMemcpyDirection_t dir, int dst_stride, int src_stride, int count);
void __bang_conv(float* dst, int16* src, int16* kernel, const int channel_input, const int height, const int width, const int kernel_height, const int kernel_width, const int stride_w, const int stride_h, const int channel_output, int fix_position);
2.3.3 多核并行化
以上的只调用用了DLP的一个计算核进行计算,DLP有16个核,可以通过并行计算的方式进一步优化计算速度。基本思想是将输入矩阵的规模拆分成多份,并将每份分配给不同的计算和进行计算,最后再对计算结果进行合并。每个计算核在读取数据的时候,根据自己的coreId来确定目标数据的GDRAM地址,并将自己负责的数据快拷入到NRAM中。
2.3.4 SRAM数据访问优化
多核并行的实现中使用了4个Cluster的16个核进行并行计算,而相同的Cluster中的核在从GDRAM中拷贝数据到各自的NRAM时,会争抢GDRAM到Cluster的带宽,导致数据读取速度降低。
每个Cluster又一个共享的SRAM,我们可以将矩阵B先从GDRAM拷贝到SRAM中,然后在分发给不同的核。这种方式可以避免GDRAM到Cluster的带宽竞争,同时SRAM的存取速度高于GDRAM,因此也可以降低访问延迟,提高数据读取速度。本实验打破了数据访问核计算在时许上的独立性,因此需要进行同步操作。

2.3.5 访存和计算流水线优化
从下图可以看出,从GDRAM数据拷贝的时间较长,且数据的拷贝与计算串行,DLLP的利用率不高。针对这个问题可以将数据的拷贝和四个核的计算做流水处理,从而隐藏数据拷贝的时间。

基本思想是将SRAM分成两个部分(S1和S2),轮流存放GDRAM的数据,即“乒乓操作”。当GDRAM往S1拷贝完数据之后,计算核开始计算,同时GDRAM往S2拷贝数据;当计算核计算完成,同时GDRAM往S2拷贝完数据之后,计算核开始处理S2的数据,同时GDRAM开始往S1拷贝数据。通过以上操作可以隐藏GDRAM到SRAM的访问延迟。

2.4 实验结果
60分测试点 | 80分测试点 | 90分测试点 | 100分测试点 |
---|---|---|---|
通过 | 通过 | 通过 | 通过 |