专栏首页高性能计算从 0 到 1,使用 OpenPPL 实现一个 AI 推理应用
原创

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

深度学习推理框架 OpenPPL 已经开源了,本文以一个图像分类实例,从 0 到 1 讲解如何部署一个深度学习模型,完成一个 AI 推理应用。

最终效果:通过上传一张猫咪照片(狗狗也可以),识别出图片中的动物

OpenPPL 的源码链接:https://github.com/openppl-public/ppl.nn

背景知识

OpenPPL 是基于自研高性能算子库的推理引擎,提供云原生环境下 的 AI 模型多后端部署能力,并支持 OpenMMLab 等深度学习模型的高效部署。

以下以在 Linux x86 平台上部署图像分类模型为例,详述 OpenPPL 的安装与使用流程,帮助同学们从 0 到 1 来实现一个人工智能应用推理服务。

安装

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<Mat> 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<Mat> 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<unique_ptr<Engine>> engines;
    engines.emplace_back(unique_ptr<Engine>(x86_engine));

接着使用 ONNXRuntimeBuilderFactory::Create() 函数,读入ONNX model,根据注册的 engine 创建 runtime builder:

vector<Engine*> engine_ptrs;
    engine_ptrs.emplace_back(engines[0].get());
    auto builder = unique_ptr<ONNXRuntimeBuilder>(
        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;
    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<int64_t> 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<float> output_data_(output_size);
    float* output_data = 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<pair<float, int>> pairs(size);
    for (int32_t i = 0; i < size; i++) {
        pairs[i] = make_pair(scores[i], i);
    }

    auto cmp_func = [](const pair<float, int>& p0, const pair<float, int>& 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 模型:

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 的验证集图片):

任意大小的图片都可以正常运行,如果想要 resize 到 224 x 224 的话,可以修改程序里的如下变量:

const bool resize_input = false; // 想要resize的话,修改为true即可

3. 运行

pplnn-build/samples/cpp/run_model/classification <image_file> <onnx_model_file>

推理完成后,会得到如下输出:

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.416199   284        n02123597 Siamese cat, Siamese
2th: 3.049764   285        n02124075 Egyptian cat
3th: 2.989676   606        n03584829 iron, smoothing iron
4th: 2.812310   283        n02123394 Persian cat
5th: 2.796991   749        n04033901 quill, quill pen

不难看出,这个程序正确判断了我家猫主子是真猫 (>^ω^<)

至此 OpenPPL 的安装与图像分类模型推理已完成

另外,在 pplnn-build/tools 目录下有可执行文件 pplnn,可以进行任意模型推理、dump 输出数据、benchmark 等操作。

具体用法可使用 --help 选项查看。大家可以基于该示例进行改动,从而更熟悉 OpenPPL 的用法。

交流 QQ 群:627853444,入群密令 OpenPPL

原创声明,本文系作者授权云+社区发表,未经许可,不得转载。

如有侵权,请联系 yunjia_community@tencent.com 删除。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 高性能深度学习推理引擎 OpenPPL 正式开源!

    OpenPPL 是商汤基于自研高性能算子库的开源深度学习推理平台,能够让人工智能应用高效可靠地运行在现有的 CPU、GPU 等计算平台上,为云端场景提供人工智能...

    Aceyclee
  • 模型部署优化的学习路线是什么?

    模型部署优化这个方向其实比较宽泛。从模型完成训练,到最终将模型部署到实际硬件上,整个流程中会涉及到很多不同层面的工作,每一个环节对技术点的要求也不尽相同。

    Aceyclee
  • 使用SCF从0到1构建一个Web应用【含源码】

    现在前端已经进入了大前端时代。作为一个前端工程师,不能局限在html,css和js的纯前端圈子里,是时候进击nodejs了。也许你已早早接触过nodejs,并使...

    可可爱爱没有脑袋
  • SpringBoot 应用篇之从 0 到 1 实现一个自定义 Bean 注册器

    我们知道在 spring 中可以通过@Component,@Service, @Repository 装饰一个类,通过自动扫描注册为 bean;也可以通过在配置...

    一灰灰blog
  • 从 0 到 1 使用 Python 开发一个钉钉群应答机器人[附完整源码]

    很多时候,我们都希望计算机程序能够为我们自动化的处理一些学习、工作和生活上的任务,毕竟「偷懒」可是促进科技进步和社会发展的一大动力。基于这种需求,诞生了很多种自...

    州的先生
  • 从0到1使用python开发一个半自动答题小程序的实现

    最近每天都有玩微信读书上面的每日一答的答题游戏,完全答对12题后,可以瓜分无限阅读卡。但是从小就不太爱看书的我,很难连续答对12道题,由此,产生了写一个半自动答...

    砸漏
  • 实战:从0搭建完整 AI 开发环境写出第一个 AI 应用

    那么,作为多年的程序员,或者准备着成为新一代程序员的读者们,该如何为智能时代做好准备,成为 AI 时代的程序员呢?

    用户1737318
  • 从0到1用java再造tcpip协议栈:架构重建,完整实现ping应用

    在原先代码设计中,我们为了方便,喜欢在一个模块中组织数据包的协议头,然后将要发送的数据融合在一起,并调用网卡将数据发送出去,这种偷懒的做法将多种逻辑融合在一起。...

    望月从良
  • 从0到1用java再造tcpip协议栈:ICMP协议的原理和实现

    绝大多数TCPIP传输协议基于IP寻址协议,然后建造在IP之上的TCP和UDP两种协议用于控制数据包的传输。问题在于这些协议只关注数据传输,在传输过程中如果出现...

    望月从良
  • 从0到1详解推荐系统中的嵌入方法,原理、算法到应用都讲明白了

    作者曾在《矩阵分解推荐算法》这篇文章中提到,矩阵分解算法是一类嵌入方法,通过将用户行为矩阵分解为用户特征矩阵和标的物特征矩阵的乘积,最终将用户和标的物嵌入到低维...

    AI科技大本营
  • 数栈技术干货:从0到1实现谷歌插件开发探索及应用

    数栈是云原生—站式数据中台PaaS,我们在github和gitee上有一个有趣的开源项目:FlinkX,记得给我们点个star!star!star!

    数栈DTinsight
  • 从0到1用java再造tcpip协议栈:使用责任链模式实现ICMP错误数据报解析

    上一节我们讲述了ICMP协议的数据格式,说到了ICMP数据报包含两种类型的信息:错误消息和控制消息。同时我们详细解析了包含错误消息时的数据格式,本节我们使用代码...

    望月从良
  • 从0到1教你学Maven(全网最详细)(九)pom文件中依赖管理scope属性如何使用,pom中全局配置如何实现,资源插件如何使用

    scope:表示依赖使用的范围,也就是在maven构建项目的那些阶段中起作用。 maven构建项目 编译, 测试 ,打包, 安装 ,部署 过程(阶段)

    一天不写程序难受
  • 使用 Serverless 进行 AI 预测推理

    本文演示如何使用腾讯云的 SCF 无服务器云函数来实现 AI Serving 能力。

    腾讯云serverless团队
  • 腾讯AI制霸王者荣耀,世界杯5V5 「绝悟」绝杀职业玩家,1天训练强度超人类440年

    昨日,腾讯AI Lab研发的智能体「绝悟」在王者荣耀世界冠军杯半决赛特设环节,与职业选手职业选手赛区联队的5v5对决中获胜!

    代码医生工作室
  • 基于PaddleDetection的锥桶检测并在Gazebo环境中实现部署

    【飞桨开发者说】吴瀚,武汉理工大学本科在读,人工智能技术爱好者、飞桨开发者,希望能将AI技术更好地落地实践、服务生活。感兴趣的方向有:计算机视觉、迁移学习、推理...

    用户1386409
  • 不仅仅是美食,我们的目标是通用物体识别

    作者:围城之上 编辑:夏歌 偶然间看到了 Second State 和腾讯云联合举办的 AI 推理函数活动,试了一下食物识别的模板函数,识别结果还是挺准确的。...

    腾讯云serverless团队
  • 快来尝鲜!移动开发者实现AI业务的必选神器——飞桨移动端部署工具LiteKit

    目前,越来越多的AI场景将AI能力直接部署在移动端,其优势主要在于实时、省流、以及安全性等方面。这些AI能力给移动端产品带来巨大的想象空间,促进了移动互联网下半...

    用户1386409
  • 一边吃瓜看球,一边完成AI应用实践——手写体识别入门

    用户1737318

扫码关注云+社区

领取腾讯云代金券