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 点:

  1. 为输入和输出张量分配所需的内存。
  2. 获取每个张量的属性,如内存大小和名称。
  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 如何配置,解释如下:

  1. 如果 taskHandle 置为 nullptr,则会自动创建同步任务,接口返回即推理完成。
  2. 如果 *taskHandle 置为 nullptr,则会自动创建异步任务,接口返回的 taskHandle 可用于后续阻塞或回调。
  3. 如果 *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 后处理

对推理的输出进行后处理,具体步骤包括:

  1. 刷新输出张量数据缓存:使用 hbUCPMemFlush 将输出张量从内存中刷到 CPU 缓存,以确保数据正确地被 CPU 读取。这个操作特别重要。
  2. 获取 Top-k 结果:函数 get_topk_result 根据模型的输出数据,从推理结果中提取前 k 个分类结果并存储到 top_k_cls 变量中。 FLAGS_top_k 用来指定提取多少个最高概率的分类结果。
  3. 打印 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 个分类分数。具体操作步骤如下:

  1. 处理输出张量数据:
  • 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");
  }

到这里,整个快速上手的代码就解读完成了。

Logo

加入社区

更多推荐