1 前言
DSP是J5上专用于视觉/图像处理的数字信号处理器。在OE包的ddk/samples/vdsp_rpc_sample路径下,提供了DSP使用示例,包括nn和cv两部分。nn示例涵盖了深度学习模型的相关算子,包括量化、反量化、Softmax和雷达点云预处理。cv示例展示了如何调用地平线基于DSP封装的图像处理算子,目前已支持20多个,并且仍在持续扩充当中。-
在正式阅读前,希望您已经对DSP的软硬件特点、编程思路和板端运行方法有基本的了解,关于这方面的内容可以查看社区文章《DSP开发快速上手》。
2 功能说明
量化、反量化因为涉及浮点计算,无法使用只支持整型计算的BPU,因此通常交由ARM计算。一般来说有两种使用方法,第一种是量化、反量化算子保留在模型内部,模型推理的时候调度ARM计算,另一种是删除模型内部的量化、反量化算子,将相关计算融合进前后处理中。通常更推荐第二种方式,因为能减少数据遍历的次数,提高推理性能。-
J5的DSP能替代ARM计算量化、反量化,且同样支持以两种方式进行。
方式1
删去模型内部的量化、反量化算子,将量化、反量化移到前后处理中由DSP计算。-
关于如何删除模型内部的量化、反量化算子,以及将相关操作融合进前后处理的大致思路,可以查看社区文章《反量化节点的融合实现》。-
注意:由于删去量化、反量化算子后,会让模型的BPU算子直接暴露在模型最外侧,此时板端推理库便不会在模型推理时自动做对齐和删除对齐的操作(模型最外侧是CPU算子时,有方法可以让板端自动做对齐和去除对齐),因此当用户使用方式1时,需要编写代码为输入数据做对齐,并且在使用输出数据前需要编写代码跳用于过对齐的无效数据。关于数据对齐的更多介绍,可以查看社区文章《数据排布与跨距对齐》《模型输入输出对齐规则解析》《在部署时为输入数据做padding》。
方式2
保留模型内部的量化、反量化算子,将原本派发到ARM上的计算调度到DSP执行。-
可使用如下环境变量控制ARM是否将计算调度到DSP上执行:
export HB_DNN_PLUGIN_PATH=${ARM_PLUGIN} // libhb_dsp_nn_plugin.so所在路径
export HB_DNN_ENABLE_DSP=1
需要注意的是,如果模型输入、输出数据的尺寸过小,那么ARM的量化、反量化性能是会高于DSP的,具体规则如下:
- quantize输入数据尺寸需要大于等于1x2^18,否则DSP性能会低于ARM
- dequantize输入数据尺寸需要大于等于1x2^20,否则DSP性能会低于ARM
当板端推理库判断量化、反量化算子的DSP计算性能低于ARM时,会将相关计算还原到ARM侧进行。但如果您十分确定要将所有尺寸的量化、反量化全部交给DSP计算,以节约CPU资源,那么就可以使用下方环境变量:
// 此环境变量控制是否所有尺寸量化、反量化均调度到DSP执行
export HB_DNN_DISABLE_ACC_AUTO_DEPLOY=1
3 示例文件介绍
OE包的ddk/samples/vdsp_rpc_sample目录提供了量化、反量化示例,文件结构如下:
+---vdsp_rpc_sample
├── arm # arm侧
│ ├── cv
│ └── nn
│ ├── build_arm.sh # arm侧构建脚本
│ ├── CMakeLists.txt
│ ├── main.cc # main函数
│ └── src # nn算子示例源文件目录
├── dsp
│ ├── build_dsp.sh # dsp侧构建脚本
│ ├── CMakeLists.txt
│ ├── src # dsp算子实现目录
│ └── main.cc # dsp侧镜像源文件
├── script
│ ├── cv
│ ├── nn
│ │ ├── run_nn_test.sh
│ │ ├── bin # 可执行程序目录
│ │ ├── model # 模型目录
│ │ └── data # 图像目录
│ ├── image
│ └── lib
├── deps
└── README.md
-
arm:arm侧示例,封装了常用api,主要负责发起RPC调用,接收dsp处理结果。
- cv:cv示例,包含了图片处理的cv算子示例。
- nn:nn示例,包含quantize和dequantize api,自定义算子softmax以及pointpillar前处理。
-
dsp:dsp侧示例,实现了dsp算子功能,主要负责接收arm侧发来的任务,完成softmax等算子的计算,将结果发送给arm。
- src:包含quantize和dequantize api,以及自定义算子softmax以及pointpillar前处理的dsp侧实现。
-
script:示例的生成文件及脚本目录。
- cv:包含cv示例的可执行文件、输入数据及执行脚本。
- nn:包含nn示例的可执行文件、输入数据、模型及执行脚本。
- image: DSP镜像目录。
- lib: 可执行程序的依赖库目录。
-
deps:所有示例的依赖文件目录。
- aarch64:arm侧的依赖目录。
- vdsp:dsp侧的依赖目录。
├── arm/nn
├── CMakeLists.txt
├── build_arm.sh
├── main.cc
└── src
├── test_quantize.cc
├── test_dequantize.cc
├── test_nn_plugin.cc
├── common.cc
├── common.h
├── test_pointpillar_preprocess.cc
├── custom_dsp_softmax.cc
├── custom_dsp_softmax.h
├── test_softmax.cc
└── test_softmax_op.cc
在arm/nn/src文件夹内,提供了在ARM侧调用DSP计算量化、反量化的示例。-
test_quantize.cc和test_dequantize.cc是以方式1计算量化、反量化的示例代码,代码中设定了一组输入数据,以RPC的方式调用DSP做计算,同时将DSP计算结果和ARM的计算结果做了对比,以表明DSP和ARM在计算量化、反量化的精度一致性。-
test_nn_plugin.cc是以方式2计算量化、反量化的示例代码,该示例推理了一个实际的模型,并将模型内部的量化和反量化算子交由DSP运行。-
common.cc和common.h包含了示例运行的必备组件,其余代码属于Softmax示例,在此不做赘述。-
nn文件夹的main.cc集成了调用量化反量化算子的完整功能,CMakeLists.txt是编译必备的配置文件,执行build_arm.sh后,即可编译出可上板运行的可执行文件即相关依赖,这些生成的文件会自动存放进script目录中。我们已提供了编译好的上述文件,无需用户重复编译。
├── dsp
├── CMakeLists.txt
├── build_dsp.sh
├── main.cc
└── src
├── quantize_ivp.cc
├── quantize_ivp.h
├── dequantize_ivp.cc
├── dequantize_ivp.h
├── common.cc
├── common.h
├── pointpillar_preprocess_ivp.cc
├── pointpillar_preprocess_ivp.h
├── softmax_ivp.cc
└── softmax_ivp.h
量化与反量化计算的DSP实现源码是开源的,位于dsp/src路径,用户可参考学习,或者基于此代码做二次开发。main.cc主要用于注册编写的DSP算子,量化与反量化算子已经注册,CMakeLists.txt是编译必备的配置文件,执行build_dsp.sh后,即可编译出可以在板端配置的vdsp0和vdsp1镜像,这两个镜像文件还会自动存放进script/image目录中。我们提供了已经编译好的镜像文件,无需用户重复编译。
4 ARM调用代码解读
方式1
删去模型内部的量化、反量化算子,将量化、反量化移到前后处理中由DSP计算。
示例代码为vdsp_rpc_sample/arm/nn/src路径下的test_quantize.cc和test_dequantize.cc,此处以量化调用代码为例进行介绍,反量化调用的代码编写思路相近。
static float32_t _round(float32_t input) {
std::fesetround(FE_TONEAREST);
float32_t result = nearbyintf(input);
return result;
}
static void quantize_ref(int8_t *dst,
const float32_t *src,
float32_t scale,
float32_t zero_point,
float32_t min,
float32_t max,
int32_t size) {
for (int32_t i = 0; i < size; ++i) {
float32_t value = _round(src[i] / scale + zero_point);
value = std::min(std::max(value, min), max);
dst[i] = static_cast<int8_t>(value);
}
}
该部分代码是为了让ARM做量化计算编写的,为了验证DSP的计算结果是否和ARM一致。
const int32_t n = 1;
const int32_t c = 8;
const int32_t h = 256;
const int32_t w = 256;
float32_t scale = 10.f;
float32_t zero_point = 1.0f;
int32_t shape = n * c * h * w;
float32_t min = -128;
float32_t max = 127;
在test_quantize函数中,这里手动设置了一组输入数据信息。
hbSysMem src_mem, scale_mem, zero_point_mem, dst_arm_mem, dst_dsp_mem;
hbSysAllocCachedMem(&src_mem, shape * sizeof(float32_t));
hbSysAllocCachedMem(&dst_arm_mem, shape);
hbSysAllocCachedMem(&dst_dsp_mem, shape);
hbSysAllocCachedMem(&scale_mem, sizeof(float32_t) * c);
for (int32_t i = 0; i < c; ++i) {
((float32_t *)(scale_mem.virAddr))[i] = scale;
}
hbSysAllocCachedMem(&zero_point_mem, 4);
((float32_t *)(zero_point_mem.virAddr))[0] = zero_point;
data_generate(src_mem.virAddr, shape, HB_DSP_TENSOR_TYPE_F32);
// reference arm
quantize_ref((int8_t *)(dst_arm_mem.virAddr),
(float32_t *)(src_mem.virAddr),
scale,
zero_point,
min,
max,
shape);
-
src_mem:存放所有待量化计算的输入数据
-
scale_mem:存放所有scale值
-
zero_point_mem:存放所有zero_point值
-
dst_arm_mem:存放ARM的量化计算结果
-
dst_dsp_mem:存放DSP的量化计算结果
-
data_generate函数的具体实现在common.cc,基于shape生成待量化计算的输入数据
-
quantize_ref函数用于让ARM做量化计算
uint8_t data_layout = HB_DSP_LAYOUT_NCHW;
hbDSPTensorShape data_shape;
data_shape.numDimensions = 4;
data_shape.dimensionSize[0] = n;
data_shape.dimensionSize[1] = c;
data_shape.dimensionSize[2] = h;
data_shape.dimensionSize[3] = w;hbDSPTensor src{
data_shape, data_layout, HB_DSP_TENSOR_TYPE_F32, src_mem.phyAddr};
hbDSPTensor dst{
data_shape, data_layout, HB_DSP_TENSOR_TYPE_S8, dst_dsp_mem.phyAddr};
定义结构体src和dst,用于RPC调用时传递给DSP。
hbDSPQuantizeParam quantize_param;
quantize_param.scaleChannel = c;
quantize_param.scalePhyAddr = scale_mem.phyAddr;
quantize_param.zeroPointChannel = 1;
quantize_param.zeroPointPhyAddr = zero_point_mem.phyAddr;
quantize_param.min = (int8_t)min;
quantize_param.max = (int8_t)max;
VLOG(EXAMPLE_DEBUG) << "rpc begin";
HB_CHECK_SUCCESS(
hbDSPQuantize(&quantize_task, &dst, &src, &quantize_param, &ctrl_param),
"hbDSPQuantize failed");
HB_CHECK_SUCCESS(hbDSPWaitTaskDone(quantize_task, 100),
"hbDSPWaitTaskDone failed");
HB_CHECK_SUCCESS(hbDSPReleaseTask(quantize_task), "hbDSPReleaseTask failed");
VLOG(EXAMPLE_DEBUG) << "rpc finish";
hbDSPQuantize函数封装了hbDSPRpc接口,通过RPC的方式向DSP发送量化计算指令。
if (check_result(dst_arm_mem.virAddr,
dst_dsp_mem.virAddr,
shape,
HB_DSP_TENSOR_TYPE_S8)) {
VLOG(EXAMPLE_DEBUG) << "check result right";
} else {
VLOG(EXAMPLE_SYSTEM) << "check result false";
}
验证ARM计算结果和DSP计算结果的一致性,验证通过则打印check result right。
方式2
保留模型内部的量化、反量化算子,将原本派发到ARM上的计算调度到DSP执行。
示例代码为vdsp_rpc_sample/arm/nn/src路径下的test_nn_plugin.cc,由于方式2的计算调度是由板端推理库自动进行的,无需用户在代码中额外编写程序,程序执行前在板端配置好相关环境变量即可,因此对该部分代码的理解可参考社区文章《模型推理快速上手》。
5 示例运行说明
由于ARM侧和DSP侧所有需要编译的文件都已经包括在了OE包当中,因此用户可以跳过编译这一步,直接将script文件夹复制到J5开发板上的可写路径下,如/userdata目录。-
此时我们可以编写一个deploy.sh脚本并执行,用于在J5开发板上部署DSP镜像:
echo stop > /sys/class/remoteproc/remoteproc1/state
echo stop > /sys/class/remoteproc/remoteproc2/state
echo -n "/userdata/script/image" > /sys/module/firmware_class/parameters/path
echo vdsp0 > /sys/class/remoteproc/remoteproc1/firmware
echo vdsp1 > /sys/class/remoteproc/remoteproc2/firmware
echo start > /sys/class/remoteproc/remoteproc1/state
echo start > /sys/class/remoteproc/remoteproc2/state
之后执行以下命令,给予dsp_relay_server和test_cv文件可执行权限:
chmod 777 /userdata/script/lib/dsp_relay_server
chmod 777 /userdata/script/nn/bin/test_nn
最后进入script/nn文件夹,执行以下命令即可运行NN侧包括量化、反量化的全部示例:
sh run_nn_test.sh
用户也可以通过追加参数的形式指定需要执行的算子,所有可执行算子可以在运行脚本后添加help查看:
root@j5dvb:/userdata/script/nn# sh run_nn_test.sh help
I0000 00:00:00.000000 1655 vlog_is_on.cc:197] RAW: Set VLOG level for "*" to 3
Usage: test_nn [command]
Run specified example.
Command:
quantize
dequantize
nn_plugin
softmax
pointpillar_preprocess
all
help
Extra:
You can use tools such as valgrind to check memory leaks
valgrind --leak-check=full --show-leak-kinds=all example [command]
以方式1运行量化计算的指令和打印信息如下:
root@j5dvb:/userdata/script/nn# sh run_nn_test.sh quantize
I0000 00:00:00.000000 1657 vlog_is_on.cc:197] RAW: Set VLOG level for "*" to 3
I0101 08:05:40.618717 1657 test_quantize.cc:51] quantize begin
I0101 08:05:40.657302 1657 test_quantize.cc:112] rpc begin
[DSP] DSP version = 0.3.14
[A][DSP][initializer.cc:41](340859) Init logger, level:3
[A][DSP][initializer.cc:56](340859) Relay server mode
[I][DSP][engine.cc:555](340859) Start thread for receive msg from relay server
I0101 08:05:40.660331 1657 test_quantize.cc:119] rpc finish
I0101 08:05:40.663142 1657 test_quantize.cc:127] check result right
I0101 08:05:40.663685 1657 test_quantize.cc:137] quantize finish
以方式1运行反量化计算的指令和打印信息如下:
root@j5dvb:/userdata/script/nn# sh run_nn_test.sh dequantize
I0000 00:00:00.000000 1662 vlog_is_on.cc:197] RAW: Set VLOG level for "*" to 3
I0101 08:06:02.638142 1662 test_dequantize.cc:56] dequantize begin
I0101 08:06:02.683870 1662 test_dequantize.cc:118] rpc begin
[DSP] DSP version = 0.3.14
[A][DSP][initializer.cc:41](362885) Init logger, level:3
[A][DSP][initializer.cc:56](362885) Relay server mode
[I][DSP][engine.cc:555](362886) Start thread for receive msg from relay server
I0101 08:06:02.687968 1662 test_dequantize.cc:127] rpc finish
I0101 08:06:02.700351 1662 test_dequantize.cc:135] check result right
I0101 08:06:02.702279 1662 test_dequantize.cc:145] dequantize finish
以方式2运行量化、反量化计算的指令和打印信息如下:
root@j5dvb:/userdata/script/nn# sh run_nn_test.sh nn_plugin
I0000 00:00:00.000000 1667 vlog_is_on.cc:197] RAW: Set VLOG level for "*" to 3
I0101 08:06:40.124614 1667 test_nn_plugin.cc:22] nn_plugin begin
[BPU_PLAT]BPU Platform Version(1.3.5)!
[HBRT] set log level as 0. version = 3.15.29.0
[A][DNN][configuration.cpp:270][Util](2000-01-01,08:06:40.300.247) Run Quantize and Dequantize on DSP
[DNN] Runtime version = 1.19.3_(3.15.29 HBRT)
[A][DNN][packed_model.cpp:237][Model](2000-01-01,08:06:40.313.641) [HorizonRT] The model builder version = 1.7.9
I0101 08:06:40.317533 1667 test_nn_plugin.cc:36] hbDNNGetModelNameList success
I0101 08:06:40.317644 1667 test_nn_plugin.cc:43] hbDNNGetModelHandle success
I0101 08:06:40.320661 1667 test_nn_plugin.cc:54] prepare output tensor success
I0101 08:06:40.320847 1667 test_nn_plugin.cc:64] hbDNNInfer success
[DSP] DSP version = 0.3.14
[A][DSP][initializer.cc:41](401008) Init logger, level:3
[A][DSP][initializer.cc:56](401008) Relay server mode
[I][DSP][engine.cc:555](401009) Start thread for receive msg from relay server
I0101 08:06:40.846292 1667 test_nn_plugin.cc:69] task done
I0101 08:06:40.857345 1667 test_nn_plugin.cc:86] nn_plugin finish
环境变量的相关配置已写在运行脚本中。-
打印信息中,如果出现Run Quantize and Dequantize on DSP,就表明正在以方式2让DSP计算量化反量化算子。