多模型批量推理

1 前言

推理任务每执行一次,系统底层都需要响应一次中断,如果每个小模型都放在一个单独的推理任务中,那么中断出现的频率会升高,导致耗时增加。如果将多个小模型绑定到一个推理任务中,就会减少系统底层中断次数,从而降低系统开销,减少耗时。-
XJ3和J5均支持在一个推理任务中依次调用多个小模型预测多份数据。但需要注意的是,只有在推理任务中的所有模型全部预测结束后,所有的预测结果才会一并写入内存。换句话说,用户无法在推理任务结束前,提前获知部分模型的预测结果。此外,这种推理方式不支持重复加载同一个模型,若您希望让一个模型同时推理多张图片,可以考虑使用Batch的方式,参考社区文章《Batch模型推理》。

2 示例介绍

OE包的ddk/samples/ai_toolchain/horizon_runtime_sample目录包含了板端部署的大量基础示例,该目录的文件结构如下:

+---horizon_runtime_sample
├── code                        
│   ├── 00_quick_start          
│   ├── 01_api_tutorial         
│   ├── 02_advanced_samples
│   │   ├── custom_identity
│   │   ├── multi_input
│   │   ├── multi_model_batch
│   │   └── nv12_batch    
│   ├── 03_misc                 
│   ├── build_j5.sh             
│   ├── build_x86.sh            
│   ├── CMakeLists.txt
│   ├── CMakeLists_x86.txt
│   └── deps_gcc9.3             
├── j5
│   ├── data                    
│   ├── model
│   ├── script                  
│   └── script_x86              
└── README.md

其中code文件夹包含了示例的C++代码以及编译相关文件,j5文件夹包含示例运行脚本及编译生成的可执行文件,预置了数据和相关模型,在开发板上运行script目录的脚本就可以执行对应的模型推理示例。-
本文重点介绍的快速上手示例是multi_model_batch,这个示例会调用googlenet_224x224_nv12.bin和mobilenetv2_224x224_nv12.bin这两个分类模型,读取两张jpg图片,在单个推理任务中执行前向推理,并经过后处理计算得到两张图的Top5的分类结果。

在正式学习代码前,希望开发者已经熟悉地平线提供的板端部署API,这部分可以查看工具链手册的BPU SDK API章节,这个章节除了详细介绍API接口,还全面介绍了板端部署有关的数据类型、数据接口等信息,以及数据排布与对齐规则、错误码等等。您也可以一边阅读示例代码,一边翻看API手册进行学习。-
此外,建议刚开始接触工具链板端部署的开发者优先阅读《模型推理快速上手》一文,该文对horizon_runtime_sample的示例代码00_quick_start做了细致的解读,multi_model_batch的代码结构与00_quick_start相近。

3 核心代码解读

// get model handle
  hbDNNHandle_t dnn_handle_googlenet;
  hbDNNHandle_t dnn_handle_mobilenetv2;
  ......
  
  // read input file and convert img to nv12 format
  cv::Mat nv12_mat_googlenet;
  cv::Mat nv12_mat_mobilenetv2;
  ......
  
  // prepare input tensor
  hbDNNTensor input_tensor_googlenet;
  hbDNNTensor input_tensor_mobilenetv2;
  ......
  
  // prepare output tensor
  hbDNNTensor *output_tensor_googlenet = new hbDNNTensor();
  hbDNNTensor *output_tensor_mobilenetv2 = new hbDNNTensor();
  ......

因为该示例调用了两个模型,所以需要定义两个模型句柄,程序需要读取两张图片分别写入两块输入张量的内存空间中,并且每个模型都需要准备一份输入、输出张量的内存空间。

// Run inference
  hbDNNTaskHandle_t task_handle = nullptr;
  hbDNNInferCtrlParam infer_ctrl_param;
  HB_DNN_INITIALIZE_INFER_CTRL_PARAM(&infer_ctrl_param);
  // submit first model task
  infer_ctrl_param.more = 1;
  HB_CHECK_SUCCESS(hbDNNInfer(&task_handle,
                              &output_tensor_googlenet,
                              &input_tensor_googlenet,
                              dnn_handle_googlenet,
                              &infer_ctrl_param),
                   "hbDNNInfer failed");
  // submit second model task
  infer_ctrl_param.more = 0;
  HB_CHECK_SUCCESS(hbDNNInfer(&task_handle,
                              &output_tensor_mobilenetv2,
                              &input_tensor_mobilenetv2,
                              dnn_handle_mobilenetv2,
                              &infer_ctrl_param),
                   "hbDNNInfer failed");
  VLOG(EXAMPLE_DEBUG) << "infer success";
  // wait task done
  HB_CHECK_SUCCESS(hbDNNWaitTaskDone(task_handle, 0),
                   "hbDNNWaitTaskDone failed");
  VLOG(EXAMPLE_DEBUG) << "task done";

因为是在单个任务中推理两个模型,所以hbDNNInfer接口会调用两次。这两次调用的不同之处在于推理控制参数的more参数:最后一个推理控制参数的more设置为0,之前的more都设置为1。more参数的意义,就是设定该模型之后是否还有跟随别的模型,如果有跟随,则设置1,无跟随,则设置0。-
这两次推理共用一个task_handle,因此属于同一个任务。在两次推理先后结束之后,调用hbDNNWaitTaskDone接口,会将推理结果一并写入输出张量的内存当中。

// post process
  get_topk_result(output_tensor_googlenet, top_k_cls, 1);
  ......
  get_topk_result(output_tensor_mobilenetv2, top_k_cls, 1);
  ......
  
  // release task handle
  HB_CHECK_SUCCESS(hbDNNReleaseTask(task_handle), "hbDNNReleaseTask failed");

在推理结果写入输出内存后,执行两次TopK后处理得到两张图片的分类预测结果。之后释放任务句柄,结束当前任务。

4 板端运行

该示例的板端运行非常简单,先执行code文件夹下的build_j5.sh脚本,执行完毕后会在j5文件夹下生成文件及相关依赖。 在J5文件夹中,data存放了模型推理使用的输入数据,model文件夹存放了各个示例的模型,script文件夹下除了运行脚本,还会有编译后生成的动态链接库.so文件,以及可执行文件。我们将整个j5文件夹复制到板端,再进入j5/script/02_advanced_samples目录,运行run_multi_model_batch.sh脚本,即可在开发板上运行多模型批量推理示例了。-
这个示例在J5开发板上运行的终端打印信息如下:

root@j5dvb-hynix8G:/userdata/chaoliang/j5/script/02_advanced_samples# sh run_multi_model_batch.sh
../aarch64/bin/run_multi_model_batch --model_file=../../model/runtime/googlenet/googlenet_224x224_nv12.bin,../../model/runtime/mobilenetv2/mobilenetv2_224x224_nv12.bin --input_file=../../data/cls_images/zebra_cls.jpg,../../data/cls_images/zebra_cls.jpg
I0000 00:00:00.000000 10916 vlog_is_on.cc:197] RAW: Set VLOG level for "*" to 3[BPU_PLAT]BPU Platform Version(1.3.3)!
[HBRT] set log level as 0. version = 3.15.18.0
[DNN] Runtime version = 1.17.2_(3.15.18 HBRT)[A][DNN][packed_model.cpp:225][Model](2023-04-11,17:57:43.547.52) [HorizonRT] The model builder version = 1.15.0
[A][DNN][packed_model.cpp:225][Model](2023-04-11,17:57:51.811.477) [HorizonRT] The model builder version = 1.15.0
I0411 17:57:51.844280 10916 main.cpp:117] hbDNNInitializeFromFiles success
I0411 17:57:51.844388 10916 main.cpp:125] hbDNNGetModelNameList success
I0411 17:57:51.844424 10916 main.cpp:139] hbDNNGetModelHandle success
I0411 17:57:51.875140 10916 main.cpp:153] read image to nv12 success
I0411 17:57:51.875686 10916 main.cpp:170] prepare input tensor success
I0411 17:57:51.875875 10916 main.cpp:182] prepare output tensor success
I0411 17:57:51.876082 10916 main.cpp:216] infer success
I0411 17:57:51.878844 10916 main.cpp:221] task done
I0411 17:57:51.878948 10916 main.cpp:226] googlenet class result id: 340
I0411 17:57:51.879084 10916 main.cpp:230] mobilenetv2 class result id: 340
I0411 17:57:51.879177 10916 main.cpp:234] release task success

可以看到,终端打印出了这两个模型的推理结果,由于推理的是同一张图片(zebra_cls.jpg),因此推理结果相同。