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.onnx
Primero, 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.
- 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.
- 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.
- Luego, la sesión transformará el gráfico, incluida la inserción de nodos de copia, nodos de conversión, etc.
- 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.
- 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)