乐趣区

关于高性能计算:从-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<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 模型:

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

任意大小的图片都能够失常运行,如果想要 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

退出移动版