基于征程6 C++模型推理快速上手 & 代码解读
这个章节主要介绍了模型推理相关的API、数据、结构体、排布及对齐规则等
1. 前言
在完成模型的转换编译后,会得到可以在开发板上部署的 hbm 模型,hbm(Horizon BPU Model)可以使用地平线推理库 UCP( BPU SDK API)进行推理。
horizon_j6_open_explorer 发布物的 samples/ucp_tutorial/dnn/basic_samples/ 路径下有很多的示例,本文会使用 samples/ucp_tutorial/dnn/basic_samples/code/00_quick_start/resnet_rgb 示例,这个示例会运行 resnet50_224x224_rgb.hbm 分类模型,读取一张 jpg 图片,进行一次模型推理,在后处理中计算得到 Top5 的分类结果。
开发者在编写板端部署代码前,需要先熟悉地平线提供的板端部署 API,这部分可以查看工具链手册的模型推理 API 概览,这个章节主要介绍了模型推理相关的 API、数据、结构体、排布及对齐规则等,可以在 Horizon 开发板上利用 API 完成模型的加载与释放,模型信息的获取,以及模型的推理等操作。建议一边阅读示例代码,一边翻看用户手册进行学习。
2. 程序结构
通过一张图展示代码 main 函数的十个主要步骤,虚线箭头表示该步骤相关的接口/函数,底色为绿色的函数,具体实现在 main 函数之外。
3. 代码解读
快速上手代码在 OE 开发包的路径为:
samples/ucp_tutorial/dnn/basic_samples/code/00_quick_start/resnet_rgb/src/main.cc
3.1 预定义结构
定义字符串,代表 板端/X86 仿真 运行脚本 run_resnet_rgb.sh 的命令行参数定义,即需要解析的输入参数,包括模型文件路径、图片文件路径,以及分类结果 TopK 的参数设置,这里的 TopK 默认是 Top5。
DEFINE_string(model_file, EMPTY, "model file path");
DEFINE_string(image_file, EMPTY, "Test image path");
DEFINE_int32(top_k, 5, "Top k classes, 5 by default");
定义一些用于日志打印的宏,方便在代码中输出调试信息(Debug)、信息日志(Info)、错误信息(Error)、警告信息(Warning)等,每个日志信息都与一个模块名称关联。通过定义不同级别的日志宏,开发者可以轻松在代码中插入日志,方便调试和排错,同时还可以附带模块名称来区分不同的日志来源。
#define MOUDULE_NAME "DNN_BASIC_SAMPLE"
#define LOGD(err_msg, ...) HFLOGM_D(MOUDULE_NAME, err_msg, ##
__VA_ARGS__
)
#define LOGI(err_msg, ...) HFLOGM_I(MOUDULE_NAME, err_msg, ##
__VA_ARGS__
)
#define LOGE(err_msg, ...) HFLOGM_E(MOUDULE_NAME, err_msg, ##
__VA_ARGS__
)
#define LOGW(err_msg, ...) HFLOGM_W(MOUDULE_NAME, err_msg, ##__VA_ARGS__)
定义 HB_CHECK_SUCCESS(value, errmsg)这个宏,用于判断函数是否成功执行,参数 value 处填写执行的具体函数,函数返回值为 0 代表成功执行,执行失败则会返回错误码并在终端打印显示,用户可根据错误码对照工具链手册的《错误码》章节查看报错原因,也可以使用 hbDNNGetErrorDesc 接口打印错误原因。
#define HB_CHECK_SUCCESS(value, errmsg) \
do { \
/
*value can be call of function*
/ \
auto ret_code = value; \
if (ret_code != 0) { \
LOGE("{}, error code: {}", errmsg, ret_code); \
return ret_code; \
} \
} while (0);
#define HB_CHECK_SUCCESS(value, errmsg): 宏定义,value 和 errmsg 是宏的参数, do-while :保证至少执行一次循环体,与 while 循环(先判断条件)的主要区别在于,do-while 先执行,后判断条件。用来保证宏展开时的语法安全性。 auto ret_code = value;: 宏的第一步是执行 value,并将结果赋值给变量 ret_code。 if (ret_code != 0): 在 C/C++ 编程中,通常 0 表示成功,而非零值表示失败或错误。
Classificaton 结构体主要定义了三个变量,分别是分类序号 id,分类得分 score,以及类别名 class_name,会在后处理计算 TopK 的时候使用,友元函数重载的>运算符是为了配合 TopK 计算中优先级队列的优先级设置,后文分析 TopK 代码的时候会进行详细介绍。
typedef struct Classification {
int id; // 分类的ID号
float score; // 分类的得分
const char *class_name; // 分类的名称, 字符串指针
// 默认构造函数,初始化成员变量
// 将 class_name 指针初始化为 0(即 nullptr),表示默认没有类别名称。
// 默认构造函数会在创建 Classification 对象但不传递任何参数时自动调用。
Classification() : id(0), score(0.0), class_name(0) {}
// 带参数的构造函数,初始化成员变量
Classification(int id, float score, const char *class_name)
: id(id), score(score), class_name(class_name) {}
// 重载 > 运算符,用于比较两个Classification对象的分数
// 如果 lhs(左操作数)的 score 大于 rhs(右操作数)的 score,则返回 true,否则返回 false。
// 这样可以直接使用 > 来比较两个 Classification 对象
friend bool operator>(const Classification &lhs, const Classification &rhs) {
return (lhs.score > rhs.score);
}
// 析构函数(无特殊功能,但可以避免潜在的警告)
~Classification() {}
} Classification;
3.2 解析命令行参数、初始化日志
// Parsing command line arguments
gflags::SetUsageMessage(argv[0]);
gflags::ParseCommandLineFlags(&argc, &argv, true);
std::cout << gflags::GetArgv() << std::endl;
// Init logging
hobot::hlog::HobotLog::Instance()->SetLogLevel(
"DNN_BASIC_SAMPLE", hobot::hlog::LogLevel::log_info);
3.3 获取模型句柄
加载模型相关变量定义
hbDNNPackedHandle_t packed_dnn_handle;
hbDNNHandle_t dnn_handle;
const char **model_name_list;
auto modelFileName = FLAGS_model_file.c_str();
int model_count = 0;
hbDNNPackedHandle_t packed_dnn_handle:hbDNNPackedHandle_t 是一个数据结构,表示打包的模型句柄,packed_dnn_handle 是这个句柄的变量名。用于加载和管理模型的句柄,通过该句柄可以访问和操作多个模型。
hbDNNHandle_t dnn_handle:hbDNNHandle_t 是单个模型的句柄,表示一个具体的模型。
const char **model_name_list:指向字符指针的指针,表示一个字符串数组,用于存储模型的名称列表。通过这个列表可以获取打包模型中的所有模型名称。
auto modelFileName = FLAGS_model_file.c_str();:
- 使用 auto 自动推导变量类型,modelFileName 是一个 const char*,表示模型文件名。
- FLAGS_model_file 是通过命令行参数传入的模型文件的路径,c_str() 将 FLAGS_model_file 转换为 C 风格的字符串(const char*),方便后续调用 C 库函数。
int model_count = 0;表示模型的数量,初始值为 0。在加载打包模型时,可以通过模型计数来进行遍历和操作。
这里涉及到了“pack”打包的概念,做个解释:工具链支持将多个转换后 hbm 模型整合成一个文件,如果 hbDNNInitializeFromFiles 接口解析的是没有打包的单个模型,那么 packed_dnn_handle 指向的就是那一个模型,如果该接口解析的是打包了之后的整合模型,那么 packed_dnn_handle 会指向打包的多个模型,model_name_list 列表会包含所有的模型,model_count 为所有模型的总数。
// Step1: get model handle
{
HB_CHECK_SUCCESS(hbDNNInitializeFromFiles(&packed_dnn_handle, &modelFileName, 1),
"hbDNNInitializeFromFiles failed");
HB_CHECK_SUCCESS(hbDNNGetModelNameList(&model_name_list, &model_count,
packed_dnn_handle),
"hbDNNGetModelNameList failed");
HB_CHECK_SUCCESS(
hbDNNGetModelHandle(&dnn_handle, packed_dnn_handle, model_name_list[0]),
"hbDNNGetModelHandle failed");
}
hbDNNInitializeFromFiles:表示从指定的模型文件中加载模型
- &packed_dnn_handle: 这是一个指向打包模型句柄的指针,hbDNNInitializeFromFiles 会通过它返回打包模型的句柄。
- &modelFileName: 这是一个指向模型文件名的指针,告诉函数要加载哪个模型文件。
- 1: 模型文件的数量。这里传入的是一个模型文件,因此数量是 1。
hbDNNGetModelNameList: 用于获取已经加载的模型的名称列表和模型的数量。
- &model_name_list: 指向模型名称列表的指针,函数通过它返回模型的名称。
- &model_count: 指向模型数量的指针,函数会通过它返回模型的数量。
- packed_dnn_handle: 之前 hbDNNInitializeFromFiles 函数返回的打包模型句柄,用于标识加载的模型。
hbDNNGetModelHandle: 用于获取特定模型的句柄。
- &dnn_handle: 指向模型句柄的指针,函数通过它返回该模型的句柄。
- packed_dnn_handle: 这是已经初始化的打包模型的句柄。
- model_name_list[0]: 这是从之前获取的模型名称列表中选择的第一个模型名称,用来从打包模型中指定要获取句柄的模型。
3.4 准备输入输出 tensor
声明用于存储输入和输出张量的变量,以及用于记录输入和输出数量的计数器
std::vector<hbDNNTensor> input_tensors; std::vector<hbDNNTensor> output_tensors;
int input_count = 0; // 用于记录输入张量的数量
int output_count = 0; // 用于记录输出张量的数量
std::vector input_tensors;
- 这是一个用于存储输入张量的动态数组。
- std::vector 是 C++ 的标准容器,可以动态调整大小。它能够存储多个 hbDNNTensor 对象,这里的 hbDNNTensor 是 地平线 dnn 库中表示张量(Tensor)的数据结构。
- 在 DNN 推理过程中,输入张量用于存储输入数据,比如图片或者其他数据形式,供模型处理。
std::vector output_tensors;
- 同样是一个 std::vector 容器,不过它是用于存储输出张量的。
- 输出张量用于存储 DNN 推理之后的结果,比如分类的概率、目标检测的框坐标等。
为模型推理准备输入和输出张量。通过 hbDNNGetInputCount 获取输入输出的数量,通过 resize 调整张量数组的大小,最终调用函数 prepare_tensor 来进一步准备这些张量。
// Step2: prepare input and output tensor
{
HB_CHECK_SUCCESS(hbDNNGetInputCount(&input_count, dnn_handle),
"hbDNNGetInputCount failed");
HB_CHECK_SUCCESS(hbDNNGetOutputCount(&output_count, dnn_handle),
"hbDNNGetOutputCount failed");
input_tensors.resize(input_count);
output_tensors.resize(output_count);
prepare_tensor(input_tensors.data(), output_tensors.data(), dnn_handle);
}
hbDNNGetInputCount: 用于获取模型的输入张量数量。
- &input_count: 一个指向 int 类型的指针,用于存储输入张量的数量。
- dnn_handle: 模型的句柄,在前面的步骤中已经获取到了。
hbDNNGetOutputCount: 用于获取模型的输出张量数量。 input_tensors.resize 和 output_tensors.resize:这两行代码通过 resize 函数调整 input_tensors 和 output_tensors 动态数组的大小,使它们分别能够容纳 input_count 个输入张量和 output_count 个输出张量。
prepare_tensor: 用于进一步准备输入和输出张量。
- input_tensors.data(): 获取 input_tensors 中底层数组的指针,传递给函数以便操作。
- output_tensors.data(): 获取 output_tensors 中底层数组的指针。
- dnn_handle: 模型的句柄,用于根据模型的需求准备张量。
其中,prepare_tensor 函数的主要作用有 3 点:
- 为输入和输出张量分配所需的内存。
- 获取每个张量的属性,如内存大小和名称。
- 初始化张量内存,为后续的推理过程做准备。
如果模型有多个输入输出,则每个输入输出都会分配一次内存空间。
hbDNNGetInputTensorProperties 和 hbDNNGetOutputTensorProperties 用于从模型中解析输入输出张量的属性。
input_memSize 和 output_memSize 表示某个输入/输出张量的 shape 对齐后的字节大小。
prepare_tensor 具体代码如下:
int prepare_tensor(hbDNNTensor *input_tensor, hbDNNTensor *output_tensor, hbDNNHandle_t dnn_handle) { int input_count = 0; int output_count = 0; hbDNNGetInputCount(&input_count, dnn_handle); hbDNNGetOutputCount(&output_count, dnn_handle); /** Tips: * For input memory size: * * input_memSize = input[i].properties.alignedByteSize * For output memory size: * * output_memSize = output[i].properties.alignedByteSize */ hbDNNTensor *input = input_tensor; // 模型有多个输入输出,则每个输入输出都会分配一次内存空间 for (int i = 0; i < input_count; i++) { HB_CHECK_SUCCESS( hbDNNGetInputTensorProperties(&input[i].properties, dnn_handle, i), "hbDNNGetInputTensorProperties failed"); int input_memSize = input[i].properties.alignedByteSize; HB_CHECK_SUCCESS(hbUCPMallocCached(&input[i].sysMem, input_memSize, 0), "hbUCPMallocCached failed"); // Show how to get input name const char *input_name; HB_CHECK_SUCCESS(hbDNNGetInputName(&input_name, dnn_handle, i), "hbDNNGetInputName failed"); LOGI("input[{}] name is {}", i, input_name); } hbDNNTensor *output = output_tensor; for (int i = 0; i < output_count; i++) { HB_CHECK_SUCCESS( hbDNNGetOutputTensorProperties(&output[i].properties, dnn_handle, i), "hbDNNGetOutputTensorProperties failed"); int output_memSize = output[i].properties.alignedByteSize; HB_CHECK_SUCCESS(hbUCPMallocCached(&output[i].sysMem, output_memSize, 0), "hbUCPMallocCached failed"); // Show how to get output name const char *output_name; HB_CHECK_SUCCESS(hbDNNGetOutputName(&output_name, dnn_handle, i), "hbDNNGetOutputName failed"); LOGI("output[{}] name is {}", i, output_name); } return 0;}
int prepare_tensor(hbDNNTensor *input_tensor, hbDNNTensor *output_tensor,
hbDNNHandle_t dnn_handle) {
int input_count = 0;
int output_count = 0;
hbDNNGetInputCount(&input_count, dnn_handle);
hbDNNGetOutputCount(&output_count, dnn_handle);
/** Tips:
* For input memory size:
* * input_memSize = input[i].properties.alignedByteSize
* For output memory size:
* * output_memSize = output[i].properties.alignedByteSize
*/
hbDNNTensor *input = input_tensor;
// 模型有多个输入输出,则每个输入输出都会分配一次内存空间
for (int i = 0; i < input_count; i++) {
HB_CHECK_SUCCESS(
hbDNNGetInputTensorProperties(&input[i].properties, dnn_handle, i),
"hbDNNGetInputTensorProperties failed");
int input_memSize = input[i].properties.alignedByteSize;
HB_CHECK_SUCCESS(hbUCPMallocCached(&input[i].sysMem, input_memSize, 0),
"hbUCPMallocCached failed");
// Show how to get input name
const char *input_name;
HB_CHECK_SUCCESS(hbDNNGetInputName(&input_name, dnn_handle, i),
"hbDNNGetInputName failed");
LOGI("input[{}] name is {}", i, input_name);
}
hbDNNTensor *output = output_tensor;
for (int i = 0; i < output_count; i++) {
HB_CHECK_SUCCESS(
hbDNNGetOutputTensorProperties(&output[i].properties, dnn_handle, i),
"hbDNNGetOutputTensorProperties failed");
int output_memSize = output[i].properties.alignedByteSize;
HB_CHECK_SUCCESS(hbUCPMallocCached(&output[i].sysMem, output_memSize, 0),
"hbUCPMallocCached failed");
// Show how to get output name
const char *output_name;
HB_CHECK_SUCCESS(hbDNNGetOutputName(&output_name, dnn_handle, i),
"hbDNNGetOutputName failed");
LOGI("output[{}] name is {}", i, output_name);
}
return 0;
}
首先使用 hbDNNGetInputCount() 和 hbDNNGetOutputCount() 来获取模型的输入和输出张量的数量,这两步在前面已经介绍过,是重复的,一般已经提前知道输入输出张量的数量。
接下来,代码遍历每个输入张量,通过 hbDNNGetInputTensorProperties() 获取每个输入张量的属性,包括 alignedByteSize,即为该张量所需的内存大小。然后通过 hbUCPMallocCached() 为每个输入张量分配内存。
输出张量的处理过程与输入类似,首先使用 hbDNNGetOutputTensorProperties() 获取每个输出张量的属性,并分配内存。
3.5 将输入数据置入输入 tensor
将 rgb 图像数据存放到输入张量对应的内存空间中
// Step3: set input data to input tensor
{
// read a single picture for input_tensor[0], for multi_input model, you
// should set other input data according to model input properties.
HB_CHECK_SUCCESS(
read_image_2_tensor_as_rgb(FLAGS_image_file, input_tensors.data()),
"read_image_2_tensor_as_rgb failed");
LOGI("read image to tensor as ``rgb`` success");
}
调用函数 read_image_2_tensor_as_rgb,从指定的图片文件(FLAGS_image_file)中读取图像数据,并将其以 RGB 格式存储到输入张量(input_tensors[0])中。输入张量 input_tensors.data()是指向 input_tensors 容器的指针,指向模型的第一个输入张量 input_tensors[0],对于多输入模型,除了 input_tensors[0],还需要根据模型的其他输入属性,分别读取和设置其他输入数据
read_image_2_tensor_as_rgb 是将一张图片读取并转换为适合推理的 RGB 图像张量,同时为模型输入数据做好预处理,包括调整图像大小、格式转换、填充等。
/** You can define to prepare your data **/
int32_t read_image_2_tensor_as_rgb(std::string &image_file,
hbDNNTensor *input_tensor) {
hbDNNTensor *input = input_tensor;
auto &properties = input->properties;
int tensor_id = 0;
// NHWC , the ``struct`` of resnet50 input shape is NHWC
int input_h = properties.validShape.dimensionSize[1];
int input_w = properties.validShape.dimensionSize[2];
// 使用 OpenCV 的 cv::imread 函数读取输入图像文件。图像被读取为 BGR 格式(OpenCV 默认),随后会转换为 RGB 格式。
cv::Mat bgr_mat = cv::imread(image_file, ``cv::IMREAD_COLOR``);
if (bgr_mat.empty()) {
LOGE("image ``file`` not exist!");
return -1;
}
// convert to rgb format
cv::Mat rgb_mat;
cv::cvtColor(bgr_mat, rgb_mat, CV_BGR2RGB);
// resize 调整图像大小
cv::Mat resize_rgb_mat;
resize_rgb_mat.create(input_h, input_w, rgb_mat.type());
cv::resize(rgb_mat, resize_rgb_mat, resize_rgb_mat.size(), 0, 0);
/** Tips:
* For featuremap input, the user may need to preprocess the input data externally and add padding to the input data.
**/
// copy rgb data
auto data = input->sysMem.virAddr;
// resize_rgb_mat.ptr<uint8_t>() 返回指向调整大小后的图像数据的指针
uint8_t *rgb_data = resize_rgb_mat.ptr<uint8_t>(); auto &valid_shape = properties.validShape;
auto &stride = properties.stride;
// U8
uint32_t elementSize = 1;
std::vector<uint32_t> dims;
for (size_t idx{0U}; idx < valid_shape.numDimensions; idx++) {
dims.emplace_back(static_cast<uint32_t>(valid_shape.dimensionSize[idx]));
}
// 调用 add_padding 将图像数据按照模型的输入形状和步幅信息填充到张量中。
HB_CHECK_SUCCESS(add_padding(data, rgb_data, dims.size(), dims.data(), stride,
elementSize),
"hbDNNAddPaddingWithStride failed!")
// TODO(@horizon.ai): The currently provided resnet50 model input is S8, which will be removed after the integrated compilation outputs the U8 model.
// 模型当前输入是 S8 类型(有符号 8 位整数),但输入的图像数据是 U8 类型(无符号 8 位整数)。
// 需要将无符号的数据转为有符号数据,具体做法是将每个 uint8_t 值减去 128。
uint8_t *data_u8{reinterpret_cast<uint8_t *>(data)};
int8_t *data_s8{reinterpret_cast<int8_t *>(data)};
for (size_t idx{0U}; idx < properties.alignedByteSize; idx++) {
data_s8[idx] = data_u8[idx] - 128;
}
// 在完成数据写入后,调用 hbUCPMemFlush 函数来清理系统缓存,确保数据在后续操作中是一致的。
hbUCPMemFlush(&input->sysMem, HB_SYS_MEM_CACHE_CLEAN);
return 0;
}
add_padding,主要功能是给输入张量添加填充(padding)。它通过递归的方式,在每个维度上计算填充后的张量,并将数据从输入内存复制到输出内存。
// get_prod_size模板函数用来计算给定维度数组的总元素数。
// 它接收一个维度数组 dim 和维度数量 dim_num,并返回数组中所有元素的乘积。
// 例如,若 dim 是 [3, 4, 5],则乘积为 3 * 4 * 5 = 60
template <typename T>
int32_t get_prod_size(T const *dim, uint32_t dim_num) {
int32_t size{1};
for (uint32_t idx{0}; idx < dim_num; idx++) {
size *= dim[idx];
}
return size;
}
// add_padding_core 递归函数用来将输入张量中的数据复制到输出张量中,确保数据按照给定的 stride(步长)进行对齐。
// 它根据 dim_num 的值递归处理张量的各个维度,逐层添加填充。详情见下方解读
void add_padding_core(void *output_ptr, const void *input_ptr, uint32_t dim_num,
const uint32_t *dim, const int64_t *stride,
uint32_t element_size) {
if (dim_num == 1) {
memcpy(output_ptr, input_ptr, element_size * dim[0]);
return;
}
char const *in_ptr{reinterpret_cast<char const *>(input_ptr)};
char *out_ptr{reinterpret_cast<char *>(output_ptr)};
for (int32_t idx{0U}; idx < dim[0]; idx++) {
auto size{get_prod_size(dim + 1, dim_num - 1) * element_size};
char const *input{in_ptr + idx * size};
void *output{out_ptr + stride[0] * idx};
add_padding_core(output, input, dim_num - 1, dim + 1, stride + 1,
element_size);
}
}
// 外部接口函数,调用 add_padding_core 函数来执行实际的填充操作。详情见下方解读
int32_t add_padding(void *output, const void *input, uint32_t dim_num,
const uint32_t *dim, const int64_t *stride,
uint32_t element_size) {
add_padding_core(output, input, dim_num, dim, stride, element_size);
return 0;
}
add_padding_core 参数:
- output_ptr:填充后的输出数据指针。
- input_ptr:原始输入数据指针。
- dim_num:剩余的维度数量。
- dim:当前维度数组。
- stride:步长,用来计算输出数据的对齐方式。
- element_size:每个元素的字节大小。
add_padding_core 递归处理:
- 当 dim_num == 1 时,意味着已经到达最低维度(最小元素),这时直接通过 memcpy 复制数据。
- 否则,循环遍历该维度的每个元素,递归调用 add_padding_core 处理下一维度的数据。
add_padding_core 函数会根据给定的 stride 值,将数据进行正确的对齐和填充。
add_padding 参数:
- output:输出数据指针,指向填充后的张量。
- input:输入数据指针,指向原始张量。
- dim_num:维度数量。
- dim:维度数组,表示输入张量的大小。
- stride:步长数组,用来确定每一维度的内存对齐方式。
- element_size:单个元素的字节大小。
3.6 执行推理
hbUCPTaskHandle_t task_handle{nullptr}:创建任务句柄 task_handle,用于管理一次推理任务的生命周期,此为异步执行的创建句柄方式。
hbUCPSchedParam 是调度参数结构体,通过 HB_UCP_INITIALIZE_SCHED_PARAM(&ctrl_param)宏定义初始化参数,backend 重新给了 HB_UCP_BPU_CORE_ANY,ctrl_param.backend = HB_UCP_BPU_CORE_ANY 指定任务可以在任意可用的 BPU(Brain Processing Unit)核上执行。hbUCPSubmitTask 函数提交 UCP 任务至调度器。最后调用 hbUCPWaitTaskDone 函数,等待任务完成。其中,0 表示不设定超时时间,一直等待任务完成。
hbUCPTaskHandle_t task_handle{nullptr}; // 异步执行
hbDNNTensor *output = output_tensors.data();
// Step4: run inference
{
// generate task handle
HB_CHECK_SUCCESS(
hbDNNInferV2(&task_handle, output, input_tensors.data(), dnn_handle),
"hbDNNInferV2 failed");
// submit task
hbUCPSchedParam ctrl_param;
HB_UCP_INITIALIZE_SCHED_PARAM(&ctrl_param);
ctrl_param.backend = HB_UCP_BPU_CORE_ANY;
HB_CHECK_SUCCESS(hbUCPSubmitTask(task_handle, &ctrl_param),
"hbUCPSubmitTask failed");
``// wait`` task done
HB_CHECK_SUCCESS(hbUCPWaitTaskDone(task_handle, 0),
"hbUCPWaitTaskDone failed");
}
关于 hbDNNInferV2 的解读如下:
int32_t hbDNNInferV2(hbUCPTaskHandle_t *taskHandle,
hbDNNTensor *output,
hbDNNTensor const *input,
hbDNNHandle_t dnnHandle);
参数解读:
- [out] taskHandle 任务句柄指针。
- [in/out] output 推理任务的输出。
- [in] input 推理任务的输入。
- [in] dnnHandle DNN 句柄指针。
关于同步与异步执行,task_handle 如何配置,解释如下:
- 如果 taskHandle 置为 nullptr,则会自动创建同步任务,接口返回即推理完成。
- 如果 *taskHandle 置为 nullptr,则会自动创建异步任务,接口返回的 taskHandle 可用于后续阻塞或回调。
- 如果 *taskHandle 非空,并且指向之前已经创建但未提交的任务,则会自动创建新任务并添加进来。最多支持同时存在 32 个模型任务。
#define HB_UCP_INITIALIZE_SCHED_PARAM(param) { (param)->priority = HB_UCP_PRIORITY_LOWEST; (param)->deviceId = 0; (param)->customId = 0; (param)->backend = HB_UCP_CORE_ANY; }
int32_t hbUCPSubmitTask(hbUCPTaskHandle_t taskHandle, hbUCPSchedParam *schedParam); 提交 UCP 任务至调度器。 参数解读:
- [in] taskHandle 任务句柄指针。
- [in] schedParam 任务调度参数。
int32_t hbUCPWaitTaskDone(hbUCPTaskHandle_t taskHandle, int32_t timeout); 参数解读:
- [in] taskHandle 任务句柄指针。
- [in] timeout 超时配置(单位:毫秒)。
3.7 后处理
对推理的输出进行后处理,具体步骤包括:
- 刷新输出张量数据缓存:使用 hbUCPMemFlush 将输出张量从内存中刷到 CPU 缓存,以确保数据正确地被 CPU 读取。这个操作特别重要。
- 获取 Top-k 结果:函数 get_topk_result 根据模型的输出数据,从推理结果中提取前 k 个分类结果并存储到 top_k_cls 变量中。 FLAGS_top_k 用来指定提取多少个最高概率的分类结果。
- 打印 Top-k 结果:通过 LOGI 打印前 k 个分类结果的 id,方便观察推理结果。
// Step5: do postprocess with output data
std::vector<Classification> top_k_cls;
{
// make sure CPU read data from DDR before using output tensor data
for (int i = 0; i < output_count; i++) {
hbUCPMemFlush(&output_tensors[i].sysMem, HB_SYS_MEM_CACHE_INVALIDATE);
}
get_topk_result(output, top_k_cls, FLAGS_top_k);
for (int i = 0; i < FLAGS_top_k; i++) {
LOGI("TOP {} result id: {}", i, top_k_cls[i].id);
}
}
在 get_topk_result 函数中,模型输出的 Top-k 结果通过一个优先队列(小顶堆)来实现,从而快速获取最高的 k 个分类分数。具体操作步骤如下:
- 处理输出张量数据:
-
tensor->sysMem.virAddr 是一个指向输出数据的指针,通过 reinterpret_cast<float *> 将其转换为浮点类型指针,假设输出类型为 float。这一步通常需要根据模型的输出数据类型进行相应的类型转换。
-
quantiType 用于判断输出是否为量化数据。如果 quanti_type 不是 NONE,则需要对输出数据进行反量化处理,但在此例中,量化类型为 NONE,无需反量化。
2.构建优先队列:
-
使用 C++ 标准库的 std::priority_queue 来存储前 top_k 个分类结果。由于优先队列默认是大顶堆,这里通过 std::greater 使其变为小顶堆,以便始终保留最高的 k 个分数。
-
每次迭代时将当前分类结果(Classification 对象,包含类别索引、分数等信息)压入队列。如果队列中元素超过 top_k,则移除分数最低的元素。
3.生成结果:
-
当所有分类分数遍历完成后,优先队列中保留了最高的 k 个分数。随后将其逆序插入 top_k_cls,因为优先队列在移出时顺序是从小到大的。
void get_topk_result(hbDNNTensor *tensor,
std::vector<Classification> &top_k_cls, int top_k) {
std::priority_queue<Classification, std::vector<Classification>,
std::greater<Classification>>
queue;
// The type reinterpret_cast should be determined according to the output type
// For example: HB_DNN_TENSOR_TYPE_F32 is float
// // 将 tensor 数据强制转换为 float 指针
auto data = reinterpret_cast<float *>(tensor->sysMem.virAddr);
auto quanti_type{tensor->properties.quantiType};
// For example model, quantiType is NONE and no dequantize processing is required.
// 检查是否需要反量化
// 如果模型量化类型不是 NONE,可以根据需要实现对应的反量化处理逻辑
if (quanti_type != hbDNNQuantiType::NONE) {
LOGE("quanti_type is not NONE, and the output needs to be dequantized!");
}
// 1000 classification score values
// 模型输出的分类得分数量,这里假设为1000
int tensor_len = 1000;
for (auto i = 0; i < tensor_len; i++) {
float score = data[i];
queue.push(Classification(i, score, ""));
// 保证队列中最多只保存 top_k 个元素
if (queue.size() > top_k) {
queue.pop();
}
}
// 从优先队列中取出结果
while (!queue.empty()) {
top_k_cls.emplace_back(queue.top());
queue.pop();
}
// 倒序排列
std::reverse(top_k_cls.begin(), top_k_cls.end());
}
3.8 释放资源
最后,进行资源的释放操作,确保在完成推理任务后清理分配的内存和任务句柄。
依次释放任务句柄,释放输入/输出申请的内存空间,释放模型句柄。
// Step6: release resources
{
// release task handle
HB_CHECK_SUCCESS(hbUCPReleaseTask(task_handle), "hbUCPReleaseTask failed");
// free input mem
for (int i = 0; i < input_count; i++) {
HB_CHECK_SUCCESS(hbUCPFree(&(input_tensors[i].sysMem)),
"hbUCPFree failed");
}
// free output mem
for (int i = 0; i < output_count; i++) {
HB_CHECK_SUCCESS(hbUCPFree(&(output_tensors[i].sysMem)),
"hbUCPFree failed");
}
// release model
HB_CHECK_SUCCESS(hbDNNRelease(packed_dnn_handle), "hbDNNRelease failed");
}
/ Step6: release resources
{
// release task handle
HB_CHECK_SUCCESS(hbUCPReleaseTask(task_handle), "hbUCPReleaseTask failed");
// free input mem
for (int i = 0; i < input_count; i++) {
HB_CHECK_SUCCESS(hbUCPFree(&(input_tensors[i].sysMem)),
"hbUCPFree failed");
}
// free output mem
for (int i = 0; i < output_count; i++) {
HB_CHECK_SUCCESS(hbUCPFree(&(output_tensors[i].sysMem)),
"hbUCPFree failed");
}
// release model
HB_CHECK_SUCCESS(hbDNNRelease(packed_dnn_handle), "hbDNNRelease failed");
}
到这里,整个快速上手的代码就解读完成了。
更多推荐
所有评论(0)