3.2.2. C++ 代码解析¶
3.2.2.1. Case 0: 基础示例程序¶
在该示例中,我们封装了一个函数,完成图像分类的过程,如下:
bool inference( const std::string& bmodel_path, const std::string& input_path, int tpu_id, int loops, const std::string& compare_path);在该函数中,我们通过循环处理同一张图像来模拟真实的图像分类业务。
// pipeline of inference for (int i = 0; i < loops; ++i) { // read image cv::Mat frame = cv::imread(input_path); // preprocess preprocessor.process(input, frame); // scale input data if input data type is int8 or uint8 if (in_dtype != BM_FLOAT32) { engine.scale_input_tensor(graph_name, input_name, input); } // inference engine.process(graph_name); // scale output data if input data type is int8 or uint8 if (out_dtype != BM_FLOAT32) { engine.scale_output_tensor(graph_name, output_name, output); } // postprocess auto result = postprocessor.process(output); // print result // ... }如上代码所示,在每次循环中,我们首先使用 opencv 的 ”imread“ 函数解码图片。 然后对图像进行预处理,获取 bmodel 的输入张量,并根据 bmodel 的数据类型缩放输入张量, 缩放比例是 bmodel 中的参数,已经事先被加载到 engine 中。 接着驱动 TPU 进行推理。 最后对 TPU 输出的张量进行后处理,获得最终的结果(top-5)。
在该示例程序中,图片解码和预处理的实现中直接调用了 opencv 的相关函数, 同时后处理比较简单,只计算了 top-5 分类结果。 因此你可以直接参考相关代码,下面我们主要介绍模型推理部分。
bmodel_path 是本例中用到的 resnet50 bmodel 的路径,fp32 或 int8 皆可。 我们使用该 bmodel 初始化了一个 sail::Engine 的实例, 该实例中保存了模型的基础信息,将作为我们后续模型推理的载体。 如果你的机器上有多个 TPU,那么可以通过 tpu_id 指定使用哪个 TPU 进行推理,编号从0开始。
sail::Engine engine(bmodel_path, tpu_id, sail::SYSIO);我们可以通过该实例提供的方法获取模型的属性,代码如下:
sail::Engine engine(bmodel_path, tpu_id, sail::SYSIO); auto graph_name = engine.get_graph_names().front(); auto input_name = engine.get_input_names(graph_name).front(); auto output_name = engine.get_output_names(graph_name).front(); auto input_shape = engine.get_input_shape(graph_name, input_name); auto output_shape = engine.get_output_shape(graph_name, output_name); auto in_dtype = engine.get_input_dtype(graph_name, input_name); auto out_dtype = engine.get_output_dtype(graph_name, output_name);graph_name 代表 bmodel 中我们要使用的具体模型的名称,在该示例程序中,bmodel 中只包含一个模型。 input_name 代表我们使用的具体模型中的输入张量名称,在该模型中,只有一个输入张量。 output_name 代表我们使用的具体模型中的输出张量名称,在该模型中,只有一个输出张量。 input_shape 和 output_shape 代表指定张量的尺寸。 in_dtype 和 out_dtype 代表指定张量的数据类型。
实际上,你也可以使用 BMNNSDK 中的 bm_model.bin 工具获取上述信息,如下:
# fp32_bmodel # bitmain@bitmain:~$ bm_model.bin --info resnet50_fp32_191115.bmodel # bmodel version: B.2.2 # chip: BM1684 # create time: Sat Nov 23 14:37:37 2019 # # ========================================== # net: [ResNet-50_fp32] index: [0] # ------------ # stage: [0] static # input: data, [1, 3, 224, 224], float32 # output: fc1000, [1, 1000], float32 # int8_bmodel # bitmain@bitmain:~$ bm_model.bin --info resnet50_int8_191115.bmodel # bmodel version: B.2.2 # chip: BM1684 # create time: Sat Nov 23 14:38:50 2019 # # ========================================== # net: [ResNet-50_int8] index: [0] # ------------ # stage: [0] static # input: data, [1, 3, 224, 224], int8 # output: fc1000, [1, 1000], int8
3.2.2.2. Case 1: 单模型多线程¶
在 case 0 中,我们使用了 bmodel 初始化了一个 sail::Engine 的实例,模型推理的过程是在主线程中进行的。 在 case 1 中,我们将展示如果在多个线程中使用同一个 sail::Engine 实例做模型推理。
我们定义了一个 ”thread_infer“ 函数来实现子线程中的模型推理逻辑,如下:
void thread_infer( int thread_id, sail::Engine* engine, const std::string& input_path, int loops, const std::string& compare_path, std::promise<bool>& status);在该函数中,我们也通过循环处理一张图片来模拟真实的图像分类业务, 整体的代码逻辑与 case 0 类似, 在这里不再重复介绍。
case 1 与 case 0 主要的区别在于对 sail::Engine 实例的处理上,下面简写为 engine。
首先,我们在主线程中创建 engine,使用了与 case 0 不同构造函数并使用 engine 的 ”load“ 函数加载 bmodel:
sail::Engine engine(tpu_id); int ret = engine.load(bmodel_path);不同于 case 0,使用该构造函数创建 engine 时,engine 中不会为 bmodel 的输入与输入张量创建内存, 而是需要用户额外提供输入与输出张量,即 sail::Tensor 的实例。
因此,在子线程中,我们根据从 engine 实例中获取的模型信息创建对应张量:
// get handle to create input and output tensors sail::Handle handle = engine->get_handle(); // allocate input and output tensors with both system and device memory sail::Tensor in(handle, input_shape, in_dtype, true, true); sail::Tensor out(handle, output_shape, out_dtype, true, true); std::map<std::string, sail::Tensor*> input_tensors = {{input_name, &in}}; std::map<std::string, sail::Tensor*> output_tensors = {{output_name, &out}};而在模型推理时,也需要指定对应的张量,选择下面的重载函数即可:
engine->process(graph_name, input_tensors, output_tensors);
3.2.2.3. Case 2: 多线程多模型¶
case 2 是 case 1 在模型数量上的扩展。 在 case 1 中,我们在每个线程中都是用同一个在主线程中加载的 bmodel 做推理。 而在 case 2 中,我们将会在 engine 中加载多个 bmodel,如下是加载模型的子线程函数:
/** * @brief Load a bmodel. * * @param thread_id Thread id * @param engine Pointer to an Engine instance * @param bmodel_path Path to bmodel */ void thread_load( int thread_id, sail::Engine* engine, const std::string& bmodel_path) { int ret = engine->load(bmodel_path); if (ret == 0) { auto graph_name = engine->get_graph_names().back(); spdlog::info("Thread {} load {} successfully.", thread_id, graph_name); } }其它代码与 case 1 基本一致.
3.2.2.4. Case 3: 多线程多 TPU 模式¶
case 3 是 case 1 在 TPU 数量上的扩展。 由于 engine 是与 TPU 一一对应的。 因此我们将对每个线程创建一个指定 tpu_id 的 engine,如下:
// init Engine to load bmodel and allocate input and output tensors // one engine for one TPU std::vector<sail::Engine*> engines(thread_num, nullptr); for (int i = 0; i < thread_num; ++i) { engines[i] = new sail::Engine(bmodel_path, tpu_ids[i], sail::SYSIO); }其它代码与 case 1 基本一致.