模型推理应用开发指导
概述
本章节介绍如何在地平线平台进行模型推理应用开发,以及需要您注意的相关事项。
注意
在开始开发应用前,请确保您已经根据 环境部署 章节的内容完成了开发环境准备。
最简易的开发过程包括工程创建、工程实现、工程编译与运行三个阶段。
考虑到实际业务场景开发的较复杂需求,对于常用的多模型控制概念和应用调优建议也都提供了一些说明。
工程创建
地平线推荐使用cmake进行应用工程管理,前文介绍的环境部署部分也已经完成了cmake安装。
在阅读本节内容前,我们希望您已经了解cmake的使用。
地平线开发库提供了相关的工程依赖。具体依赖信息如下:
- 地平线部署依赖库libdnn.so,libucp.so等,路径:
${OE_DIR}/samples/ucp_tutorial/deps_aarch64/ucp/。
- C编译器 aarch64-linux-gnu-gcc。
- C++编译器 aarch64-linux-gnu-g++。
注解
上方$ {OE_DIR} 指地平线提供的OE包路径。
创建一个工程,您需要编写 CMakeLists.txt 文件,CMakeLists.txt 文件中定义了一些编译选项,以及依赖库、头文件的路径。参考如下:
cmake_minimum_required(VERSION 2.8)
project(your_project_name)
# libdnn.so depends on system software dynamic link library, use -Wl,-unresolved-symbols=ignore-in-shared-libs to shield during compilation
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wl,-unresolved-symbols=ignore-in-shared-libs")
set(CMAKE_CXX_FLAGS_DEBUG " -Wall -Werror -g -O0 ")
set(CMAKE_C_FLAGS_DEBUG " -Wall -Werror -g -O0 ")
set(CMAKE_CXX_FLAGS_RELEASE " -Wall -Werror -O3 ")
set(CMAKE_C_FLAGS_RELEASE " -Wall -Werror -O3 ")
if (NOT CMAKE_BUILD_TYPE)
set(CMAKE_BUILD_TYPE Release)
endif ()
message(STATUS "Build type: ${CMAKE_BUILD_TYPE}")
# define dnn lib path
set(DNN_PATH "${OE_DIR}/samples/ucp_tutorial/deps_aarch64/ucp/")
set(DNN_LIB_PATH ${DNN_PATH}/lib)
include_directories(${DNN_PATH}/include)
link_directories(${DNN_LIB_PATH})
add_executable(user_app main.cc)
target_link_libraries(user_app
dnn
ucp
pthread
rt
dl)
注意在以上示例中,我们没有指定编译器位置,会在工程编译阶段补充编译器指定,请参考 工程编译与运行 小节部分的介绍。
工程实现
工程实现部分,我们主要为您介绍如何将hbm模型在地平线平台运行起来。
最简单的步骤应该包括模型加载、准备输入数据、准备输出内存、推理和结果解析,以下是一份简单的模型部署参考代码:
#include <iostream>
#include "hobot/dnn/hb_dnn.h"
#include "hobot/hb_ucp.h"
#include "hobot/hb_ucp_sys.h"
int main(int argc, char **argv) {
// 第一步加载模型
hbDNNPackedHandle_t packed_dnn_handle;
const char* model_file_name= "./mobilenetv1/mobilenetv1_224x224_nv12.hbm";
hbDNNInitializeFromFiles(&packed_dnn_handle, &model_file_name, 1);
// 第二步获取模型名称
const char **model_name_list;
int model_count = 0;
hbDNNGetModelNameList(&model_name_list, &model_count, packed_dnn_handle);
// 第三步获取dnn_handle
hbDNNHandle_t dnn_handle;
hbDNNGetModelHandle(&dnn_handle, packed_dnn_handle, model_name_list[0]);
// 第四步准备输入数据
int input_count = 0;
hbDNNGetInputCount(&input_count, dnn_handle);
std::vector<hbDNNTensor> input(input_count);
for (int i = 0; i < input_count; i++) {
hbDNNGetInputTensorProperties(&input[i].properties, dnn_handle, i);
auto &mem = input[i].sysMem[0];
/*
1. 对于动态输入,需要设置input[i].properties中对应的动态参数
2. 根据模型输入调用hbUCPMalloc/hbUCPMallocCached接口申请对应的内存大小
如 hbUCPMallocCached(&mem, size, 0);
3. 根据输入属性判断输入是否需要量化,是否需要padding,把对应输入数据填充到mem中
4. 如果申请内存是cacheable属性,写完后要主动做flush操作
如 hbUCPMemFlush(&mem, HB_SYS_MEM_CACHE_CLEAN);
*/
}
// 第五步准备模型输出数据的空间
int output_count = 0;
hbDNNGetOutputCount(&output_count, dnn_handle);
std::vector<hbDNNTensor> output(output_count);
for (int i = 0; i < output_count; i++) {
hbDNNTensorProperties &output_properties = output[i].properties;
hbDNNGetOutputTensorProperties(&output_properties, dnn_handle, i);
int out_aligned_size = output_properties.alignedByteSize;
hbUCPSysMem &mem = output[i].sysMem[0];
hbUCPMallocCached(&mem, out_aligned_size, 0);
}
// 第六步创建异步推理任务
hbUCPTaskHandle_t task_handle{nullptr};
hbDNNInferV2(&task_handle,
output.data(),
input.data(),
dnn_handle);
// 第七步提交任务
hbUCPSchedParam infer_sched_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&infer_sched_param);
hbUCPSubmitTask(task_handle, &infer_sched_param);
// 第八步等待任务结束
hbUCPWaitTaskDone(task_handle, 0);
// 第九步解析模型输出
for (int i = 0; i < output_count; i++) {
// 对于申请cacheable属性的输出内存,读之前需要主动flush
hbUCPMemFlush(&(output[i].sysMem[0]), HB_SYS_MEM_CACHE_INVALIDATE);
/*
1. 根据输出属性判断输出是否带padding,是否需要做反量化,反量化类型以及反量化需要的量化参数信息等
2. 解析处理
*/
}
// 释放任务
hbUCPReleaseTask(task_handle);
// 释放内存
for (int i = 0; i < input_count; i++) {
hbUCPFree(&(input[i].sysMem[0]));
}
for (int i = 0; i < output_count; i++) {
hbUCPFree(&(output[i].sysMem[0]));
}
// 释放模型
hbDNNRelease(packed_dnn_handle);
return 0;
}
示例代码中,为了缩减篇幅,模型部分处理以注释形式描述,更详细的细节后续文档有说明,比如
动态输入说明请参考 动态输入说明,
内存对齐规则请参考 对齐规则 章节。
更加全面的工程实现指导请阅读 模型推理API手册 和 模型推理基础示例包使用说明 。
工程编译和运行
结合 工程创建 一节中的cmake工程配置,参考如下编译脚本:
# 为arm定义gcc路径
LINARO_GCC_ROOT=/usr
DIR=$(cd "$(dirname "$0")";pwd)
export CC=${LINARO_GCC_ROOT}/bin/aarch64-linux-gnu-gcc
export CXX=${LINARO_GCC_ROOT}/bin/aarch64-linux-gnu-g++
rm -rf build_arm
mkdir build_arm
cd build_arm
cmake ${DIR}
make -j8
根据 环境部署 部分的指引,您的开发机中应该已经安装有相应编译器,将上述脚本中的编译器配置指定为您的安装项目即可。
arm程序拷贝到地平线开发板上可运行,注意程序依赖的文件也需要一同拷贝到开发板,并在启动脚本中配置依赖。
例如我们的示例程序依赖库有: libucp.so、libdnn.so 以及其他bsp库,这些依赖库都可在OE包的 ucp_tutorial/deps_aarch64/ 路径下找到,需要上传到板子的运行环境中。
我们建议您在板端的 /userdata 路径下新建 lib 路径并将库传送至该目录下,则在板端运行程序前,需指定的依赖库路径信息如下:
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/userdata/lib
多模型控制策略
多模型场景中,每个模型都需要使用有限的计算资源完成推理,不可避免地会出现计算资源的争夺情况。
为了便于您控制多模型的执行,地平线提供了模型优先级的控制策略供您使用。
模型优先级控制
注意
请注意,此功能仅支持在开发板端实现,x86模拟器不支持此功能。
J6计算平台BPU计算单元硬件本身没有任务抢占功能,对于每一个推理任务,一旦它进到BPU模型计算之后,在该任务执行完成之前都会一直占用BPU,其他任务只能排队等待。
此时很容易出现BPU计算资源被一个大模型推理任务所独占,进而影响其他高优先级模型的推理任务执行。
针对这种问题,Runtime SDK基于模型的优先级通过软件的方式实现了BPU资源抢占的功能。
其中有以下点需要被关注:
- 编译后的数据指令模型在BPU上进行推理计算时,它将表现为1个或者多个function-call的调用,其中function-call是BPU的执行粒度,多个function-call调用任务将在BPU的硬件队列上按序进行调度,当一个模型所有的function-call都执行完成,那么一个模型推理任务也就执行完成了。
- 基于上述描述,BPU模型任务抢占粒度设计为function-call更为简单,即BPU执行完一个function-call之后,暂时挂起当前模型,然后切入执行另外一个模型,当新模型执行完成之后,再恢复原来模型的状态继续运行。
但是这里存在两个问题,第一是经过编译器编译出来的模型function-call都是merge在一起,此时模型只有一个大的function-call,它无法被抢占;第二是每个function-call的执行时间比较长或者不固定,也会造成抢占时机不固定,影响抢占效果。
为了解决上述的两个问题,地平线在模型编译和系统软件层面都给予了支持,下面分别介绍其实现原理和操作方法:
- 首先,如果您选择使用QAT方案处理模型,则在 模型编译 阶段,您需要在编译接口中的额外参数配置中添加
max-time-per-fc 选项,用于设置每个function call的执行时间(以微秒为单位),其默认取值为 0 (即不做限制)。
用户可以自行设置这个选项控制上板运行阶段个别大function-call的执行时间。假设某function-call执行时间为10ms,当模型编译时 将 max-time-per-fc 设置为 500,则这个function-call将会被拆分成20个。
而如果您使用PTQ方案处理模型,则在 模型转换 阶段,可以在模型的YAML配置文件中的编译器相关参数( compiler_parameters )中,添加 max_time_per_fc 参数。
- 其次,需要在推理任务提交时设置
hbUCPSchedParam.priority 参数。按照优先级还可以支持高优抢占嵌套能力。 如:配置 infer 任务优先级小于 254,则为普通任务,不可抢占其他任务。
配置 infer 任务优先级等于 254,则为hight抢占任务,可支持抢占普通任务。 配置 infer 任务优先级等于 HB_DNN_PRIORITY_PREEMP(255),则为urgent抢占任务,可抢占普通任务和hight抢占任务。
应用调优建议
地平线建议的应用调优策略包括工程任务调度和算法任务整合两个方面。
工程任务调度
方面,我们推荐您使用一些workflow调度管理工具,充分发挥不同任务阶段的并行能力。一般应用可以简单拆分为输入前处理、模型推理、输出后处理三个阶段,在简易流程下,其处理流程如下图。
充分利用workflow管理实现不同任务阶段并行后,理想的任务处理流程将达到下图效果。
算法任务整合 方面,地平线推荐您使用多任务模型。
这样一方面可以在一定程度上避免多模型调度管理的困难;另一方面多任务模型也能充分共享主干网络的计算量,较于使用各个独立的模型,可以在整个应用级别明显减少计算量,从而达到更高的整体性能。
在地平线内部和许多合作客户的业务实践中,多任务也是常见的应用级优化策略。
其他应用开发工具
hrt_model_exec 是一个模型执行工具,可直接在开发板上评测模型的推理性能、获取模型信息。
一方面可以让用户拿到模型时实际了解模型真实性能,另一方面也可以帮助用户了解模型可以做到的速度极限,对于应用调优的目标极限具有指导意义。
hrt_model_exec 工具分别提供了模型推理 infer 和查看模型信息 model_info 功能,工具使用方法请参考 hrt_model_exec工具介绍 。
UCP还提供了性能分析工具协助您定位应用程序的性能瓶颈,其中 UCP Trace 用于分析应用程序pipline调度的能力,hrt_ucp_monitor 用于监控监控硬件后端的占用率。
工具使用方法请分别参考 UCP Trace 使用说明 和 hrt_ucp_monitor 工具介绍 章节。