ONNX: C ++ usa el modelo .onnx para realizar cálculos directos a través de onnxruntime [El onnxruntime descargado es un archivo de biblioteca compilado y se puede usar directamente]

1. Introducción básica

En 2017, Microsoft, junto con Facebook y otros, desarrolló un estándar de formato para modelos de aprendizaje profundo y aprendizaje automático: ONNX, cuyo objetivo es unificar todos los formatos de modelos y hacer que la implementación de modelos sea más conveniente. La mayoría de los marcos de aprendizaje profundo ahora admiten la transferencia de modelos ONNX y proporcionan las interfaces de exportación correspondientes.

ONNXRuntime (Open Neural Network Exchange) es un marco de inferencia para el formato de modelo ONNX lanzado por Microsoft. Los usuarios pueden usarlo para ejecutar un modelo onnx de manera muy conveniente. ONNXRuntime admite una variedad de backends en ejecución, incluidos CPU, GPU, TensorRT, DML, etc. Se puede decir que ONNXRuntime es el soporte más nativo para los modelos ONNX. Siempre que domine las operaciones correspondientes de exportación de modelos, puede implementar modelos con diferentes marcos y mejorar la eficiencia del desarrollo.

Use onnx y onnxruntime para implementar el marco profundo de pytorch y use el razonamiento de C++ para la implementación del servidor. El rendimiento del razonamiento del modelo es mucho más rápido que el de python.

1. Descargar

Dirección de descarga de GitHub:

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

 Lanzamiento de ONNX Runtime v1.9.0 · microsoft/onnxruntime · GitHub

onnxruntime-linux-x64-1.9.0.tgz 

2. Descomprimir

El onnxruntime descargado es un archivo de biblioteca compilado directamente, que se puede colocar directamente en una carpeta personalizada. Introduzca los archivos de encabezado y los archivos de biblioteca de onnxruntime en CMakeLists.txt.

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

2. Modelo .onnx de exportación de Pytorch

torch.onnxPrimero, use el módulo  que viene con pytorch para exportar .onnx el archivo del modelo. Consulte la documentación oficial de pytorch para esta parte . El proceso principal es el siguiente:

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'}})

El parámetro torch.onnx.export está incluido en el documento. La versión correspondiente a opset_version es muy importante. Los ejes dinámicos pueden establecer dinámicamente las dimensiones correspondientes de la entrada y la salida. Si no se establece, la forma del tensor de entrada y salida no se puede cambiar. Si la entrada es fija, no es necesario agregarla.

Si el modelo exportado se puede usar sin problemas, se puede verificar primero con 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)

Si no hay ninguna excepción, significa que no hay ningún problema con el modelo exportado. En la actualidad, torch.onnx.export solo puede reconocer algunas operaciones de tensor admitidas. Para obtener más detalles, consulte Operadores admitidos. No hay problema para los modelos básicos que incluyen transformadores. Si hay problemas como ATen, debe mejorar las operaciones de tensor que el modelo no admite, para no afectar el uso del modelo por parte de C++.

3. Modelar el proceso de razonamiento

En general, el funcionamiento de todo el ONNXRuntime se puede dividir en tres etapas:

  • Estructura de la sesión;
  • Carga e inicialización del modelo;
  • correr;

1. Fase 1: Construcción de la sesión

En la fase de construcción, se crea un objeto InferenceSession. Al crear un objeto de sesión en el front-end de python, el extremo de python llamará al constructor de la clase InferenceSession en C++ a través de http://onnxruntime_pybind_state.cc para obtener un objeto de InferenceSession.

La etapa de construcción de InferenceSession inicializará cada miembro. Los miembros incluyen el objeto KernelRegistryManager responsable de la administración de OpKernel, el objeto SessionOptions que contiene la información de configuración de la sesión, el GraphTransformerManager responsable de la segmentación de gráficos y el LoggingManager responsable de la administración de registros. Por supuesto, InferenceSession es solo un caparazón vacío en este momento, y solo se ha completado la construcción inicial de los objetos miembro.

2. Fase 2: Carga e inicialización del modelo

Después de completar la construcción del objeto InferenceSession, el modelo onnx se cargará en InferenceSession y se inicializará.

2.1 Carga del modelo

Cuando se carga el modelo, se llamará a la función Load() correspondiente en el backend de C++. InferenceSession proporciona un total de 8 funciones de carga. El paquete lee ModelProto de url, ModelProto, void* model data, model istream, etc. InferenceSession analizará ModelProto y mantendrá sus miembros Model correspondientes.

2.2 Registro de proveedores

Una vez que finaliza la función Cargar, InferenceSession llama a dos funciones: RegisterExecutionProviders() y sess->Initialize();

La función RegisterExecutionProviders completará el registro de ExecutionProvider. Aquí hay una explicación de ExecutionProvider: ONNXRuntime usa Provider para representar diferentes dispositivos operativos como CUDAProvider. Actualmente, ONNXRuntimev1.0 admite siete proveedores, incluidos CPU, CUDA, TensorRT y MKL. Al llamar a la función sess->RegisterExecutionProvider(), InferenceSession mantiene ExecutionProviders admitidos en el entorno de ejecución actual a través de una lista.

2.3 Inicialización de sesión de inferencia

Es decir, sess->Initialize(). En este momento, InferenceSession realizará una inicialización adicional de acuerdo con el modelo y los proveedores de ejecución que posee (en la primera etapa de la construcción de la sesión, solo se mantienen las variables de submiembro de shell vacías). Este paso es el núcleo de la inicialización de InferenceSession. En esta etapa, se completará una serie de operaciones fundamentales, como la asignación de memoria, la partición del modelo, el registro del kernel, etc.

  1. Primero, la sesión registrará los transformadores de optimización de gráficos según el nivel y los mantendrá a través del miembro GraphTransformerManager.
  2. A continuación, la sesión realizará el registro de OpKernel, OpKernel es la lógica de cálculo de cada nodo definido en diferentes dispositivos operativos. Este proceso registrará todos los Kernels correspondientes a los nodos definidos en cada ExecutionProvider mantenido en la sesión, y el miembro KernelRegistryManager mantiene y administra la sesión.
  3. Luego, la sesión transformará el gráfico, incluida la inserción de nodos de copia, nodos de conversión, etc.
  4. Lo siguiente es la partición del modelo, es decir, el dispositivo operativo raíz divide el gráfico para determinar en qué proveedor se ejecuta cada nodo.
  5. Finalmente, cree un ExecutePlan para cada nodo. El plan de operación incluye principalmente la secuencia de ejecución de cada operación, administración de aplicaciones de memoria, administración de multiplexación de memoria y otras operaciones.

3. Fase 3: Modelo en marcha

La ejecución del modelo significa que InferenceSession lee un lote de datos cada vez y realiza cálculos para obtener el resultado final del modelo. Sin embargo, la mayor parte del trabajo ya se completó en la fase de inicialización de InferenceSession. Si observa más de cerca el código fuente, encontrará que la etapa de ejecución es principalmente para llamar al OpKernel correspondiente de cada nodo en secuencia para el cálculo.

4. Código

Al igual que todos los demás marcos principales, el lenguaje más utilizado para ONNXRuntime es python, mientras que C++ es el responsable de ejecutar el marco.

El siguiente es el uso del modelo .onnx por parte de C++ a través de onnxruntime. Consulte las muestras oficiales y las preguntas frecuentes para escribir el modelo con múltiples entradas y múltiples salidas. Para algunos parámetros, puede consultar las muestras o consultar la documentación oficial de la API.

1. Caso 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());
}

principal.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. Caso 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;
}

Comando de compilación:

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

Los tipos de datos admitidos por Tensor en onnxruntime incluyen:

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;

Cabe señalar que el uso del tipo bool debe convertirse del vector uint_8 al tipo 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);

Pruebas de rendimiento

De acuerdo con las estadísticas aproximadas de la situación real, tomando el transformador como ejemplo, la eficiencia de ejecución de onnxruntime-c++ es de 2 a 5 veces más rápida que la de pytorch-python

C++-onnx: implemente su propio modelo con el blog de onnxruntime_u013250861-CSDN Blog

Una breve introducción al uso de ONNX Runtime

Uso de C++ del Blog-CSDN de onnxruntime_chencision Blog_c++ onnxruntime

Onnxruntime C++ use (1)_SongpingWang's Technology Blog_51CTO Blog

Proceso de inferencia del Blog-CSDN de OnnxRunTime_hjxu2016 Blog_onnxruntime

Instalación y uso de Onnxruntime (con algunos problemas encontrados en la práctica)

Supongo que te gusta

Origin blog.csdn.net/u013250861/article/details/127829944
Recomendado
Clasificación