DSP的量化/反量化算子调用

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计算量化反量化算子。