从|从 0 到 1,使用 OpenPPL 实现一个 AI 推理应用

深度学习推理框架 OpenPPL 已经开源了,本文以一个图像分类实例,从 0 到 1 讲解如何部署一个深度学习模型,完成一个 AI 推理应用。
最终效果:通过上传一张猫咪照片(狗狗也可以),识别出图片中的动物
背景知识
OpenPPL 是基于自研高性能算子库的推理引擎,提供云原生环境下 的 AI 模型多后端部署能力,并支持 OpenMMLab 等深度学习模型的高效部署。

OpenPPL 的源码链接: https://github.com/openppl-pu...
安装 1. 下载 PPLNN 源码
git clone https://github.com/openppl-public/ppl.nn.git

2. 安装依赖
PPLNN 编译依赖如下:
  • GCC >= 4.9 或 LLVM/Clang >= 6.0
  • CMake >= 3.14
  • Git >= 2.7.0
本文讲解的图像分类例程 classification 还需要额外安装 OpenCV:
  • 对于 apt 包管理系统(如:Ubuntu/Debian):
sudo apt install libopencv-dev

  • 对于 yum 包管理系统(如:CentOS):
sudo yum install opencv opencv-devel

  • 或者从源码安装 OpenCV
注意:编译时会自动检测是否安装了OpenCV,如果没安装的话,不会生成本文的例程
3. 编译
  • X86
cd ppl.nn ./build.sh -DHPCC_USE_OPENMP=ON# 不开启多线程的话,可以不加后面的-DHPCC_USE_OPENMP选项

  • CUDA
cd ppl.nn ./build.sh -DHPCC_USE_CUDA=ON

编译完成后,图像分类例程 classification 会生成在 pplnn-build/samples/cpp/run_model/ 目录下,可以读取图片和模型文件,输出分类结果。
更多编译相关描述请参见:building-from-source.md
图像分类例程讲解 图像分类例程源码在 samples/cpp/run_model/classification.cpp 内,本节将对其主要部分进行讲解。
1. 图像预处理
OpenCV 读入的数据格式为 BGR HWC uint8 格式,而 ONNX 模型需要的输入格式为 RGB NCHW fp32,需要对图像数据进行转换:
int32_t ImagePreprocess(const Mat& src_img, float* in_data) { const int32_t height = src_img.rows; const int32_t width = src_img.cols; const int32_t channels = src_img.channels(); // 将颜色空间从 BGR/GRAY 转换到 RGB Mat rgb_img; if (channels == 3) { cvtColor(src_img, rgb_img, COLOR_BGR2RGB); } else if (channels == 1) { cvtColor(src_img, rgb_img, COLOR_GRAY2RGB); } else { fprintf(stderr, "unsupported channel num: %d\n", channels); return -1; }// 将 HWC 格式的三通道分开 vector rgb_channels(3); split(rgb_img, rgb_channels); // 这里构造 cv::Mat 时,直接用 in_data 为 cv::Mat 提供数据空间。这样当 cv::Mat 变化时,数据会直接写到 in_data 内 Mat r_channel_fp32(height, width, CV_32FC1, in_data + 0 * height * width); Mat g_channel_fp32(height, width, CV_32FC1, in_data + 1 * height * width); Mat b_channel_fp32(height, width, CV_32FC1, in_data + 2 * height * width); vector rgb_channels_fp32{r_channel_fp32, g_channel_fp32, b_channel_fp32}; // 将 uint8 数据转换为 fp32,并减均值除标准差,y = (x - mean) / std const float mean[3] = {0, 0, 0}; // 根据数据集和训练参数调整均值和方差 const float std[3] = {255.0f, 255.0f, 255.0f}; for (uint32_t i = 0; i < rgb_channels.size(); ++i) { rgb_channels[i].convertTo(rgb_channels_fp32[i], CV_32FC1, 1.0f / std[i], -mean[i] / std[i]); }return 0; }

2. 从 ONNX 模型生成 runtime builder
首先需要创建并注册想使用的 engine,每个 engine 对应一个推理后端,目前支持 x86 和 CUDA。
创建 x86 engine:
auto x86_engine = X86EngineFactory::Create();

或者 cuda engine:
auto cuda_engine = CudaEngineFactory::Create(CudaEngineOptions());

以下例子仅使用 x86 engine:
// 注册所有想使用的 engine vector> engines; engines.emplace_back(unique_ptr(x86_engine));

接着使用 ONNXRuntimeBuilderFactory::Create() 函数,读入ONNX model,根据注册的 engine 创建 runtime builder:
vector engine_ptrs; engine_ptrs.emplace_back(engines[0].get()); auto builder = unique_ptr( ONNXRuntimeBuilderFactory::Create(ONNX_model_path, engine_ptrs.data(), engine_ptrs.size()));

补充说明:PPLNN 框架层面支持多种异构设备混合推理。可以注册多种不同的 engine,框架会自动将计算图拆分成多个子图,并调度不同的 engine 进行计算。
3. 创建 runtime
使用 runtime_options 配置 runtime 选项,例如配置 mm_policy 字段到 MM_LESS_MEMORY(省内存模式):
RuntimeOptions runtime_options; runtime_options.mm_policy = MM_LESS_MEMORY; // 使用省内存模式

使用上一步生成的 runtime builder 创建一个 runtime 实例:
unique_ptr runtime; runtime.reset(builder->CreateRuntime(runtime_options));

一个 runtime builder 可以创建多个 runtime 实例。这些 runtime 实例会共享常量数据(权重等)和网络拓扑,从而节省内存开销。
4. 设置网络输入数据
首先通过 GetInputTensor() 接口获取 runtime 的输入 tensor:
auto input_tensor = runtime->GetInputTensor(0); // 分类网络仅有一个输入

Reshape 输入 tensor,并重新分配 tensor 的内存:
const std::vector input_shape{1, channels, height, width}; input_tensor->GetShape().Reshape(input_shape); // 即使 ONNX 模型里已经将输入尺寸固定,PPLNN 仍会动态调整输入尺寸 auto status = input_tensor->ReallocBuffer(); // 当调用了 Reshape 后,必须调用此接口重新分配内存

跟 ONNX Runtime 不同的是,即使 ONNX 模型里固定了输入尺寸,PPLNN 仍可以动态调整网络的输入尺寸(但需保证输入尺寸是合理的)。
上文预处理得到的数据 in_data 数据类型为 fp32,格式为 NDARRAY(4 维数据 NDARRAY 等同于 NCHW),由此定义用户输入数据的格式描述:
TensorShape src_desc = input_tensor->GetShape(); src_desc.SetDataType(DATATYPE_FLOAT32); src_desc.SetDataFormat(DATAFORMAT_NDARRAY); // 对于4维数据来说,NDARRAY 等同于 NCHW

最后调用 ConvertFromHost() 接口将数据 in_data 转换成 input_tensor 所需的格式,完成数据填充:
status = input_tensor->ConvertFromHost(in_data, src_desc);

5. 模型推理
status = runtime->Run(); // 执行网络推理

6. 获取网络输出数据
通过 GetOutputTensor() 接口获取 runtime 的输出 tensor:
auto output_tensor = runtime->GetOutputTensor(0); // 分类网络仅有一个输出

分配数据空间来存储网络输出:
uint64_t output_size = output_tensor->GetShape().GetElementsExcludingPadding(); std::vector output_data_(output_size); float* output_data = https://www.it610.com/article/output_data_.data();

和输入数据一样,需要先定义想要的输出格式描述:
TensorShape dst_desc = output_tensor->GetShape(); dst_desc.SetDataType(DATATYPE_FLOAT32); dst_desc.SetDataFormat(DATAFORMAT_NDARRAY); // 对于1维数据而言,NDARRAY 等同于 vector

调用 ConvertToHost() 接口将 output_tensor 的数据转换成 dst_desc 所描述的格式,得到输出数据:
status = output_tensor->ConvertToHost(output_data, dst_desc);

7. 解析输出结果
解析网络输出的score,获取分类结果:
int32_t GetClassificationResult(const float* scores, const int32_t size) { vector> pairs(size); for (int32_t i = 0; i < size; i++) { pairs[i] = make_pair(scores[i], i); }auto cmp_func = [](const pair& p0, const pair& p1) -> bool { return p0.first > p1.first; }; const int32_t top_k = 5; nth_element(pairs.begin(), pairs.begin() + top_k, pairs.end(), cmp_func); // get top K results & sort sort(pairs.begin(), pairs.begin() + top_k, cmp_func); printf("top %d results:\n", top_k); for (int32_t i = 0; i < top_k; ++i) { printf("%dth: %-10f %-10d %s\n", i + 1, pairs[i].first, pairs[i].second, imagenet_labels_tab[pairs[i].second]); }return 0; }

运行 1. 准备 ONNX 模型
我们在 tests/testdata 下准备了一个分类模型 mnasnet0_5.onnx,可用于测试。
通过如下手段可以获取更多的 ONNX 模型:
  • 可以从 OpenMMLab/PyTorch 导出 ONNX 模型:model-convert-guide.md
  • 从 ONNX Model Zoo 获取模型:https://github.com/onnx/models
ONNX Model Zoo 的模型 opset 版本都较低,可以通过 tools 下的 convert_onnx_opset_version.py 将 opset 转换为 11:
python convert_onnx_opset_version.py --input_model input_model.onnx --output_model output_model.onnx --output_opset 11

转换 opset 具体请参考:onnx-model-opset-convert-guide.md
2. 准备测试图片
测试图片使用任何格式均可。我们在 tests/testdata 下准备了 cat0.png(我们家喵主子的大头照)和 cat1.jpg(ImageNet 的验证集图片):
从|从 0 到 1,使用 OpenPPL 实现一个 AI 推理应用
文章图片

任意大小的图片都可以正常运行,如果想要 resize 到 224 x 224 的话,可以修改程序里的如下变量:
const bool resize_input = false; // 想要resize的话,修改为true即可

3. 运行
pplnn-build/samples/cpp/run_model/classification

推理完成后,会得到如下输出:
image preprocess succeed! [INFO][2021-07-23 17:29:31.341][simple_graph_partitioner.cc:107] total partition(s) of graph[torch-jit-export]: 1. successfully create runtime builder! successfully build runtime! successfully set input data to tensor [input]! successfully run network! successfully get outputs! top 5 results: 1th: 3.416199284n02123597 Siamese cat, Siamese 2th: 3.049764285n02124075 Egyptian cat 3th: 2.989676606n03584829 iron, smoothing iron 4th: 2.812310283n02123394 Persian cat 5th: 2.796991749n04033901 quill, quill pen

不难看出,这个程序正确判断了我家猫主子是真猫 (>^ω^<)
至此 OpenPPL 的安装与图像分类模型推理已完成
另外,在 pplnn-build/tools 目录下有可执行文件 pplnn,可以进行任意模型推理、dump 输出数据、benchmark 等操作。
【从|从 0 到 1,使用 OpenPPL 实现一个 AI 推理应用】具体用法可使用 --help 选项查看。大家可以基于该示例进行改动,从而更熟悉 OpenPPL 的用法。
交流 QQ 群:627853444,入群密令 OpenPPL

    推荐阅读