ONNX: C++ は .onnx モデルを使用して、onnxruntime を通じて前方計算を実行します [ダウンロードされた onnxruntime は、直接使用できるコンパイルされたライブラリ ファイルです]

1. 基本的な紹介

2017 年、Microsoft は Facebook などと協力して、すべてのモデル形式を統一し、モデルの展開をより便利にすることを目的とした、ディープラーニングおよび機械学習モデルの形式標準である ONNX を開発しました。現在、ほとんどの深層学習フレームワークは ONNX モデル転送をサポートし、対応するエクスポート インターフェイスを提供しています。

ONNXRuntime (Open Neural Network Exchange) は、Microsoft によって開始された ONNX モデル形式の推論フレームワークであり、ユーザーはこれを使用して、onnx モデルを非常に便利に実行できます。ONNXRuntime は、CPU、GPU、TensorRT、DML などを含むさまざまな実行中のバックエンドをサポートします。ONNXRuntime は ONNX モデルの最もネイティブなサポートであると言え、モデルのエクスポートの対応する操作をマスターする限り、さまざまなフレームワークでモデルをデプロイし、開発効率を向上させることができます。

onnx と onnxruntime を使用してpytorch ディープフレームワークを実装し、サーバー デプロイメントに C++ 推論を使用します。モデル推論のパフォーマンスは Python よりもはるかに高速です。

1. ダウンロード

GitHub のダウンロード アドレス:

https://github.com/microsoft/onnxruntime/releases

 ONNX ランタイム v1.9.0 をリリース · Microsoft/onnxruntime · GitHub

onnxruntime-linux-x64-1.9.0.tgz 

2.解凍する

ダウンロードされた onnxruntime は直接コンパイルされたライブラリ ファイルであり、カスタム フォルダーに直接配置できます。CMakeLists.txtにonnxruntimeのヘッダーファイルとライブラリファイルを導入します。

# 引入头文件
include_directories(......../onnxruntime/include)
# 引入库文件
link_directories(......../onnxruntime/lib)

2. Pytorch エクスポート .onnx モデル

まず、 pytorch に付属のtorch.onnxモジュールを 使用して.onnx モデル ファイルをエクスポートします。この部分についてはpytorch の公式ドキュメントを確認してください。主なプロセスは次のとおりです。

import torch
checkpoint = torch.load(model_path)
model = ModelNet(params)
model.load_state_dict(checkpoint['model'])
model.eval()

input_x_1 = torch.randn(10,20)
input_x_2 = torch.randn(1,20,5)
output, mask = model(input_x_1, input_x_2)

torch.onnx.export(model,
                 (input_x_1, input_x_2),
                 'model.onnx',
                 input_names = ['input','input_mask'],
                 output_names = ['output','output_mask'],
                 opset_version=11,
                 verbose = True,
                 dynamic_axes={'input':{1,'seqlen'}, 'input_mask':{1:'seqlen',2:'time'},'output_mask':{0:'time'}})

torch.onnx.export パラメータはドキュメントに含まれています。opset_version に対応するバージョンは非常に重要です。dynamic_axes は入力と出力の対応する次元を動的に設定できます。設定されていない場合、入力と出力の Tensor の形状を変更できません。入力が固定されている場合は追加する必要はありません。

エクスポートしたモデルがスムーズに使用できるかどうかは、まずPythonで確認できます

import onnxruntime as ort
import numpy as np
ort_session = ort.InferenceSession('model.onnx')
outputs = ort_session.run(None,{'input':np.random.randn(10,20),'input_mask':np.random.randn(1,20,5)})
# 由于设置了dynamic_axes,支持对应维度的变化
outputs = ort_session.run(None,{'input':np.random.randn(10,5),'input_mask':np.random.randn(1,26,2)})
# outputs 为 包含'output'和'output_mask'的list

import onnx
model = onnx.load('model.onnx')
onnx.checker.check_model(model)

例外がない場合は、エクスポートされたモデルに問題がないことを意味します。現時点では、torch.onnx.export はサポートされている一部の Tensor 演算のみを認識します。詳細については、サポートされる演算子を参照してください。トランスフォーマーを含む基本モデルについては問題ありません。ATen などの問題がある場合は、C++ でのモデルの使用に影響を与えないように、モデルがサポートしていない Tensor 演算を改善する必要があります。

3. モデル推論プロセス

全体として、ONNXRuntime 全体の動作は 3 つの段階に分けることができます。

  • セッション構造。
  • モデルのロードと初期化。
  • 走る;

1. フェーズ 1: セッションの構築

構築フェーズでは、InferenceSession オブジェクトが作成されます。Python フロントエンドで Session オブジェクトを構築する場合、Python エンドはhttp://onnxruntime_pybind_state.ccを通じて C++ の InferenceSession クラス コンストラクターを呼び出して、InferenceSession オブジェクトを取得します。

InferenceSession 構築ステージでは、各メンバーが初期化されます。メンバーには、OpKernel 管理を担当する KernelRegistryManager オブジェクト、セッション構成情報を保持する SessionOptions オブジェクト、グラフ セグメンテーションを担当する GraphTransformerManager、およびログ管理を担当する LoggingManager が含まれます。もちろん、この時点では InferenceSession は単なる空のシェルであり、メンバー オブジェクトの初期構築のみが完了しています。

2. フェーズ 2: モデルの読み込みと初期化

InferenceSession オブジェクトの構築が完了すると、onnx モデルが InferenceSession にロードされ、さらに初期化されます。

2.1. モデルのロード

モデルがロードされると、対応する Load() 関数が C++ バックエンドで呼び出されます。InferenceSession には、合計 8 つの Load 関数が用意されています。パッケージは URL、ModelProto、void* モデル データ、モデル istream などから ModelProto を読み取ります。InferenceSession は ModelProto を解析し、対応する Model メンバーを保持します。

2.2. プロバイダーの登録

Load 関数が終了すると、InferenceSession は RegisterExecutionProviders() と sess->Initialize() という 2 つの関数を呼び出します。

RegisterExecutionProviders 関数により、ExecutionProvider の登録が完了します。ここでは ExecutionProvider について説明します。ONNXRuntime は、CUDAProvider などのさまざまなオペレーティング デバイスを表すために Provider を使用します。現在、ONNXRuntimev1.0 は、CPU、CUDA、TensorRT、MKL を含む 7 つのプロバイダーをサポートしています。sess->RegisterExecutionProvider() 関数を呼び出すことにより、InferenceSession は現在の実行環境でサポートされている ExecutionProvider をリストを通じて保持します。

2.3. InferenceSession の初期化

このとき、InferenceSession は自身が保持するモデルと実行プロバイダーに従ってさらに初期化を実行します (Session 構築の最初の段階では、空のシェル メンバー変数のみが保持されます)。このステップは InferenceSession の初期化の中核であり、メモリの割り当て、モデルの分割、カーネルの登録などの一連のコア操作がこの段階で完了します。

  1. まず、セッションはレベルに応じてグラフ最適化トランスフォーマーを登録し、GraphTransformerManager メンバーを通じてそれらを保持します。
  2. 次に、セッションは、異なるオペレーティングデバイス上で定義された各ノードの計算ロジックである OpKernel の登録を実行します。このプロセスでは、セッションに保持されている各 ExecutionProvider で定義されているノードに対応するすべてのカーネルが登録され、セッションは KernelRegistryManager メンバーによって保持および管理されます。
  3. 次に、セッションはコピー ノード、キャスト ノードなどの挿入を含めてグラフを変換します。
  4. 次はモデル パーティションです。つまり、ルート オペレーティング デバイスがグラフを分割して、各ノードがどのプロバイダーで実行されるかを決定します。
  5. 最後に、各ノードの ExecutePlan を作成します (主に各オペレーションの実行シーケンス、メモリ アプリケーション管理、メモリ多重化管理などの動作を含む)。

3. フェーズ 3: モデルの実行

モデルの実行とは、InferenceSession が毎回データのバッチを読み取り、計算を実行してモデルの最終出力を取得することを意味します。ただし、ほとんどの作業は InferenceSession の初期化フェーズですでに完了しています。ソース コードを詳しく見てみると、実行ステージでは主に各ノードの対応する OpKernel を順番に呼び出して計算していることがわかります。

4. コード

他のすべての主流フレームワークと同様、ONNXRuntime で最も一般的に使用される言語は Python ですが、実際にフレームワークを実行するのは C++ です。

以下は、onnxruntime を介した C++ による .onnx モデルの使用です。複数の入力と複数の出力を使用してモデルを作成するには、公式サンプルと FAQ を参照してください。一部のパラメーターについては、サンプルを参照するか、公式 API ドキュメントを確認してください。

1. 事例01

BasicOrtHandler.h

#include "onnxruntime_cxx_api.h"
#include "opencv2/opencv.hpp"
#include <vector>
#define CHW 0
class BasicOrtHandler {
public:
    Ort::Value BasicOrtHandler::create_tensor(const cv::Mat &mat, const std::vector<int64_t> &tensor_dims, const Ort::MemoryInfo &memory_info_handler, std::vector<float> &tensor_value_handler, unsigned int data_format);
protected:
    Ort::Env ort_env;
    Ort::Session *ort_session = nullptr;
    const char *input_name = nullptr;
    std::vector<const char *> input_node_names;
    std::vector<int64_t> input_node_dims; // 1 input only.
    std::size_t input_tensor_size = 1;
    std::vector<float> input_values_handler;
    // create input tensor
    Ort::MemoryInfo memory_info_handler = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
    std::vector<const char *> output_node_names;
    std::vector<std::vector<int64_t>> output_node_dims; // >=1 outputs
    const char*onnx_path = nullptr;
    const char *log_id = nullptr;
    int num_outputs = 1;
protected:
    const unsigned int num_threads; // initialize at runtime.
protected:
    explicit BasicOrtHandler(const std::string &_onnx_path, unsigned int _num_threads = 1);
    virtual ~BasicOrtHandler();
protected:
    BasicOrtHandler(const BasicOrtHandler &) = delete;
    BasicOrtHandler(BasicOrtHandler &&) = delete;
    BasicOrtHandler &operator=(const BasicOrtHandler &) = delete;
    BasicOrtHandler &operator=(BasicOrtHandler &&) = delete;
protected:
    virtual Ort::Value transform(const cv::Mat &mat) = 0;
private:
    void initialize_handler();
};

BasicOrtHandler.cpp

BasicOrtHandler::BasicOrtHandler(const std::string &_onnx_path, unsigned int _num_threads) : log_id(_onnx_path.data()), num_threads(_num_threads) {
// string to wstring
#ifdef LITE_WIN32
    std::wstring _w_onnx_path(lite::utils::to_wstring(_onnx_path));
  onnx_path = _w_onnx_path.data();
#else
    onnx_path = _onnx_path.data();
#endif
    initialize_handler();
}

void BasicOrtHandler::initialize_handler() {
    // set ort env
    ort_env = Ort::Env(ORT_LOGGING_LEVEL_ERROR, log_id);
    // 0. session options
    Ort::SessionOptions session_options;
    // set op threads
    session_options.SetIntraOpNumThreads(num_threads);
    // set Optimization options:
    session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_ALL);
    // set log level
    session_options.SetLogSeverityLevel(4);

    // GPU compatiable.
    // OrtCUDAProviderOptions provider_options;
    // session_options.AppendExecutionProvider_CUDA(provider_options);
    // #ifdef USE_CUDA
    //  OrtSessionOptionsAppendExecutionProvider_CUDA(session_options, 0); // C API stable.
    // #endif

    // 1. session
    ort_session = new Ort::Session(ort_env, onnx_path, session_options);
    // memory allocation and options
    Ort::AllocatorWithDefaultOptions allocator;
    // 2. input name & input dims
    input_name = ort_session->GetInputName(0, allocator);
    input_node_names.resize(1);
    input_node_names[0] = input_name;
    // 3. input names & output dimms
    Ort::TypeInfo type_info = ort_session->GetInputTypeInfo(0);
    auto tensor_info = type_info.GetTensorTypeAndShapeInfo();
    input_tensor_size = 1;
    input_node_dims = tensor_info.GetShape();

    for (unsigned int i = 0; i < input_node_dims.size(); ++i) {
        input_tensor_size *= input_node_dims.at(i);
    }
    input_values_handler.resize(input_tensor_size);
    // 4. output names & output dimms
    num_outputs = ort_session->GetOutputCount();
    output_node_names.resize(num_outputs);
    for (unsigned int i = 0; i < num_outputs; ++i) {
        output_node_names[i] = ort_session->GetOutputName(i, allocator);
        Ort::TypeInfo output_type_info = ort_session->GetOutputTypeInfo(i);
        auto output_tensor_info = output_type_info.GetTensorTypeAndShapeInfo();
        auto output_dims = output_tensor_info.GetShape();
        output_node_dims.push_back(output_dims);
    }
}

Ort::Value BasicOrtHandler::create_tensor(const cv::Mat &mat, const std::vector<int64_t> &tensor_dims, const Ort::MemoryInfo &memory_info_handler, std::vector<float> &tensor_value_handler, unsigned int data_format) throw(std::runtime_error) {
    const unsigned int rows = mat.rows;
    const unsigned int cols = mat.cols;
    const unsigned int channels = mat.channels();

    cv::Mat mat_ref;
    if (mat.type() != CV_32FC(channels)){
        mat.convertTo(mat_ref, CV_32FC(channels));
    } else{
        mat_ref = mat;  // reference only. zero-time cost. support 1/2/3/... channels
    }
    if (tensor_dims.size() != 4) {
        throw std::runtime_error("dims mismatch.");
    }
    if (tensor_dims.at(0) != 1) {
        throw std::runtime_error("batch != 1");
    }
    // CXHXW
    if (data_format == CHW) {
        const unsigned int target_channel = tensor_dims.at(1);
        const unsigned int target_height = tensor_dims.at(2);
        const unsigned int target_width = tensor_dims.at(3);
        const unsigned int target_tensor_size = target_channel * target_height * target_width;
        if (target_channel != channels) {
            throw std::runtime_error("channel mismatch.");
        }
        tensor_value_handler.resize(target_tensor_size);
        cv::Mat resize_mat_ref;
        if (target_height != rows || target_width != cols) {
            cv::resize(mat_ref, resize_mat_ref, cv::Size(target_width, target_height));
        } else{
            resize_mat_ref = mat_ref; // reference only. zero-time cost.
        }
        std::vector<cv::Mat> mat_channels;
        cv::split(resize_mat_ref, mat_channels);
        // CXHXW
        for (unsigned int i = 0; i < channels; ++i){
            std::memcpy(tensor_value_handler.data() + i * (target_height * target_width), mat_channels.at(i).data,target_height * target_width * sizeof(float));
        }
        return Ort::Value::CreateTensor<float>(memory_info_handler, tensor_value_handler.data(), target_tensor_size, tensor_dims.data(), tensor_dims.size());
    }
    // HXWXC
    const unsigned int target_channel = tensor_dims.at(3);
    const unsigned int target_height = tensor_dims.at(1);
    const unsigned int target_width = tensor_dims.at(2);
    const unsigned int target_tensor_size = target_channel * target_height * target_width;
    if (target_channel != channels) {
        throw std::runtime_error("channel mismatch!");
    }
    tensor_value_handler.resize(target_tensor_size);
    cv::Mat resize_mat_ref;
    if (target_height != rows || target_width != cols) {
        cv::resize(mat_ref, resize_mat_ref, cv::Size(target_width, target_height));
    } else {
        resize_mat_ref = mat_ref; // reference only. zero-time cost.
    }
    std::memcpy(tensor_value_handler.data(), resize_mat_ref.data, target_tensor_size * sizeof(float));
    return Ort::Value::CreateTensor<float>(memory_info_handler, tensor_value_handler.data(), target_tensor_size, tensor_dims.data(), tensor_dims.size());
}

main.cpp

const std::string _onnx_path="";
unsigned int _num_threads = 1;

//init inference
BasicOrtHandler basicOrtHandler(_onnx_path,_num_threads);

// after transform image
const cv::Mat mat = "";
const std::vector<int64_t> &tensor_dims = basicOrtHandler.input_node_dims;
const Ort::MemoryInfo &memory_info_handler = basicOrtHandler.memory_info_handler;
std::vector<float> &tensor_value_handler = basicOrtHandler.input_values_handler;
unsigned int data_format = CHW; // 预处理后的模式

// 1. make input tensor
Ort::Value input_tensor = basicOrtHandler.create_tensor(mat_rs);

// 2. inference scores & boxes.
auto output_tensors = ort_session->Run(Ort::RunOptions{nullptr}, input_node_names.data(), &input_tensor, 1, output_node_names.data(), num_outputs);

// 3. get output tensor
Ort::Value &pred = output_tensors.at(0); // (1,n,c)

//postprocess
...

2. 事例02
 

#include <assert.h>
#include <vector>
#include <onnxruntime_cxx_api.h>

int main(int argc, char* argv[]) {
  Ort::Env env(ORT_LOGGING_LEVEL_WARNING, "test");
  Ort::SessionOptions session_options;
  session_options.SetIntraOpNumThreads(1);
  
  session_options.SetGraphOptimizationLevel(GraphOptimizationLevel::ORT_ENABLE_EXTENDED);

#ifdef _WIN32
  const wchar_t* model_path = L"model.onnx";
#else
  const char* model_path = "model.onnx";
#endif

  Ort::Session session(env, model_path, session_options);
  // print model input layer (node names, types, shape etc.)
  Ort::AllocatorWithDefaultOptions allocator;

  // print number of model input nodes
  size_t num_input_nodes = session.GetInputCount();
  std::vector<const char*> input_node_names = {"input","input_mask"};
  std::vector<const char*> output_node_names = {"output","output_mask"};
    
  std::vector<int64_t> input_node_dims = {10, 20};
  size_t input_tensor_size = 10 * 20; 
  std::vector<float> input_tensor_values(input_tensor_size);
  for (unsigned int i = 0; i < input_tensor_size; i++)
    input_tensor_values[i] = (float)i / (input_tensor_size + 1);
  // create input tensor object from data values
  auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
  Ort::Value input_tensor = Ort::Value::CreateTensor<float>(memory_info, input_tensor_values.data(), input_tensor_size, input_node_dims.data(), 2);
  assert(input_tensor.IsTensor());

  std::vector<int64_t> input_mask_node_dims = {1, 20, 4};
  size_t input_mask_tensor_size = 1 * 20 * 4; 
  std::vector<float> input_mask_tensor_values(input_mask_tensor_size);
  for (unsigned int i = 0; i < input_mask_tensor_size; i++)
    input_mask_tensor_values[i] = (float)i / (input_mask_tensor_size + 1);
  // create input tensor object from data values
  auto mask_memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
  Ort::Value input_mask_tensor = Ort::Value::CreateTensor<float>(mask_memory_info, input_mask_tensor_values.data(), input_mask_tensor_size, input_mask_node_dims.data(), 3);
  assert(input_mask_tensor.IsTensor());
    
  std::vector<Ort::Value> ort_inputs;
  ort_inputs.push_back(std::move(input_tensor));
  ort_inputs.push_back(std::move(input_mask_tensor));
  // score model & input tensor, get back output tensor
  auto output_tensors = session.Run(Ort::RunOptions{nullptr}, input_node_names.data(), ort_inputs.data(), ort_inputs.size(), output_node_names.data(), 2);
  
  // Get pointer to output tensor float values
  float* floatarr = output_tensors[0].GetTensorMutableData<float>();
  float* floatarr_mask = output_tensors[1].GetTensorMutableData<float>();
  
  printf("Done!\n");
  return 0;
}

コンパイルコマンド:

g++ infer.cpp -o infer onnxruntime-linux-x64-1.4.0/lib/libonnxruntime.so.1.4.0 -Ionnxruntime-linux-x64-1.4.0/include/ -std=c++11

onnxruntime の Tensor でサポートされるデータ型には次のものがあります。

typedef enum ONNXTensorElementDataType {
  ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED,
  ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT,   // maps to c type float
  ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT8,   // maps to c type uint8_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_INT8,    // maps to c type int8_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT16,  // maps to c type uint16_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_INT16,   // maps to c type int16_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_INT32,   // maps to c type int32_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_INT64,   // maps to c type int64_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_STRING,  // maps to c++ type std::string
  ONNX_TENSOR_ELEMENT_DATA_TYPE_BOOL,
  ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT16,
  ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE,      // maps to c type double
  ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT32,      // maps to c type uint32_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_UINT64,      // maps to c type uint64_t
  ONNX_TENSOR_ELEMENT_DATA_TYPE_COMPLEX64,   // complex with float32 real and imaginary components
  ONNX_TENSOR_ELEMENT_DATA_TYPE_COMPLEX128,  // complex with float64 real and imaginary components
  ONNX_TENSOR_ELEMENT_DATA_TYPE_BFLOAT16     // Non-IEEE floating-point format based on IEEE754 single-precision
} ONNXTensorElementDataType;

bool 型を使用するには、uint_8 ベクトルから bool 型に変換する必要があることに注意してください。

std::vector<uint8_t> mask_tensor_values;
for(int i = 0; i < mask_tensor_size; i++){
	mask_tensor_values.push_back((uint8_t)(true));
}
auto mask_memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
Ort::Value mask_tensor = Ort::Value::CreateTensor<bool>(mask_memory_info, reinterpret_cast<bool *>(mask_tensor_values.data()),mask_tensor_size, mask_node_dims.data(), 3);

性能試験

実際の状況の大まかな統計によると、transformer を例にとると、onnxruntime-c++ の実行効率は pytorch-python の実行効率より 2 ~ 5 倍高速です。

C++-onnx: onnxruntime_u013250861 のブログ - CSDN ブログを使用して独自のモデルをデプロイする

ONNX ランタイムの使用方法の簡単な紹介

onnxruntime の C++ uses_chencision のブログ - CSDN blog_c++ onnxruntime

Onnxruntime C++ の使用 (1)_SongpingWang のテクノロジー ブログ_51CTO ブログ

OnnxRunTime_hjxu2016 のブログ - CSDN Blog_onnxruntime の推論プロセス

Onnxruntime のインストールと使用 (実際に見つかったいくつかの問題を添付)

おすすめ

転載: blog.csdn.net/u013250861/article/details/127829944