ONNXRuntime

onnxruntime 运行原理及步骤

使用 ONNX 常见的使用路线?

  • Pytorch -> ONNX -> TensorRT
  • Pytorch -> ONNX -> TVM
  • TF –> ONNX –> ncnn
  • Pytorch -> ONNX -> tensorflow
  • Pytorch -> ONNX -> ONNX Runtime

什么是 ONNX Runtime?

  • ONNX Runtime 是由微软维护的一个跨平台机器学习推理加速器,它直接对接 ONNX,可以直接读取.onnx 文件并实现推理,不需要再把 .onnx 格式的文件转换成其他格式的文件
  • 适用于 Linux、Windows 和 Mac。编写 C++,它还具有 C、Python 和 C# api

ONNX Runtime 的原理?

  • 从 ONNX 模型开始,ONNXRuntime 首先将模型图转换为其内存中的图表示形式
  • 然后,应用 graph transformations 转换,包括:a)执行一组独立于提供程序的优化,例如 float16 和 float32 之间的转换转换;b)根据可用的 execution provider 将图形划分为一组子图
  • 每个子图都分配给一个 execution provider。通过使用 GetCapabilityAPI 查询 execution provider 的功能,使得我们确保可以由对应的 execution provider 执行子图
  • 将子图分配给不同硬件设备执行,以保证执行速度为目标,期间还涉及不同分支或者 OP 内部的并行计算

ONNXRuntime 的 Execution Providers (EP)?

  • ONNX Runtime 通过提供不同的 Execution Providers (EP) 支持多种硬件加速库,以实现同一个模型部署在不同的软件和硬件平台,并充分使用平台的计算资源和加速器,如 CUDA、DirectML、Arm NN、NPU 等
  • ONNX Runtime 则把计算图的节点分配到对应的计算平台进行计算。但是,加速器可能无法支持全部的算子(Operator),而只是支持其中一个子集,因此对应的 EP 也只能支持该算子子集。如果要求 EP 执行一个完整的模型,则无法使用该加速器。因此,ONNX Runtime 的设计并不要求 EP 支持所有的算子,而是把一个完整的计算图拆分为多个子图,尽可能多地把子图分配到加速平台,而不被支持的节点则使用默认的 EP(CPU)进行计算

ONNXRuntime 执行推理三个步骤的原理?

  • Session 构造:创建一个 InferenceSession 对象,包括负责 OpKernel 管理的 KernelRegistryManager 对象,持有 Session 配置信息的 SessionOptions 对象,负责图分割的 GraphTransformerManager,负责 log 管理的 LoggingManager 等。这个时候 InferenceSession 就是一个空壳子,只完成了对成员对象的初始构建
  • 模型初始化:将 onnx 模型加载到 InferenceSession 中并进行进一步的初始化
  • 模型运行:即 InferenceSession 每次读入一个 batch 的数据并进行计算得到模型的最终输出。然而其实绝大多数的工作早已经在 InferenceSession 初始化阶段完成。细看下源码就会发现 run 阶段主要是顺序调用各个 node 的对应 OpKernel 进行计算
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    import psutil
    import onnxruntime
    import numpy
    assert 'CUDAExecutionProvider' in onnxruntime.get_available_providers()
    device_name = 'gpu'
    # 1.Session构造
    sess_options = onnxruntime.SessionOptions()
    sess_options.optimized_model_filepath = os.path.join(output_dir, "optimized_model_{}.onnx".format(device_name))
    sess_options.intra_op_num_threads=psutil.cpu_count(logical=True)
    # 2.模型加载与初始化
    session = onnxruntime.InferenceSession(export_model_path, sess_options,providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider'])
    # onnxruntime.InferenceSession(onnx_path,providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'])
    # 3.模型运行
    # 构造输入,这里3输入
    data=batch_data[i]
    ort_inputs = {
    'input_ids': data[0].cpu().reshape(1, max_seq_length).numpy(),
    'input_mask': data[1].cpu().reshape(1, max_seq_length).numpy(),
    'segment_ids': data[2].cpu().reshape(1, max_seq_length).numpy()
    }
    # 推理
    ort_outputs = session.run(None, ort_inputs)

ONNXRuntime 模型初始化的原理?

  • 模型加载:在 C++ 后端会调用对应的 Load () 函数,InferenceSession 一共提供了 8 种 Load 函数。包读从 url,ModelProto,void* model data,model istream 等读取 ModelProto
  • Providers 注册:在 Load 函数结束后,InferenceSession 会调用函数 RegisterExecutionProviders (),该函数会完成 ExecutionProvider 的注册工作。这里解释一下 ExecutionProvider,ONNXRuntime 用 Provider 表示不同的运行设备比如 CUDAProvider 等。目前 ONNXRuntimev1.0 支持了包括 CPU,CUDA,TensorRT,MKL 等七种 Providers。通过调用 sess->RegisterExecutionProvider () 函数,InferenceSession 通过一个 list 持有当前运行环境中支持的 ExecutionProviders
  • InferenceSession 初始化:在 Load 函数结束后,InferenceSession 会调用函数 sess->Initialize (),这时 InferenceSession 会根据自身持有的 model 和 execution providers 进行进一步的初始化(在第一阶段 Session 构造时仅仅持有了空壳子成员变量)。该步骤是 InferenceSession 初始化的核心,一系列核心操作如内存分配,model partition,kernel 注册等都会在这个阶段完成

ONNXRuntime 的 “InferenceSession 初始化” 原理?

  • 首先,session 会根据 level 注册 graph optimization transformers,并通过 GraphTransformerManager 成员进行持有
  • 接下来 session 会进行 OpKernel 注册,OpKernel 即定义的各个 node 对应在不同运行设备上的计算逻辑。这个过程会将持有的各个 ExecutionProvider 上定义的所有 node 对应的 Kernel 注册到 session 中,session 通过 KernelRegistryManager 成员进行持有和管理
  • 然后 session 会对 Graph 进行图变换,包括插入 copy 节点,cast 节点等
  • 接下来是 model partition,也就是根运行设备对 graph 进行切分,决定每个 node 运行在哪个 provider 上
  • 最后,为每个 node 创建 ExecutePlan,运行计划主要包含了各个 op 的执行顺序,内存申请管理,内存复用管理等操作

ONNXRuntime 的三等级的图优化 (Graph Optimization)?

  • Basic (基础):平台无关的优化,在拆分子图之前进行。Basic 优化主要是冗余的节点和计算,包括 Constant Folding(常量折叠)、Redundant node eliminations(冗余节点消除)、Semantics-preserving node fusions(节点融合)
  • Extended (扩展):发生在拆分子图之后,实现更复杂的节点融合,目前只支持 CPU 和 CUDA 的 EP
  • Layout Optimizations (结构优化):需要改变数据的结构,以获得更高的计算性能提升,目前只支持 CPU 的 EP

ONNXRuntime 1.8 在 C++ 上的使用流程?

  • 模型初始化:基于 onnx 模型初始化 runtime Session
    1
    2
    3
    4
    5
    6
    7
    8
    // 创建会话参数、会话
    Env mEnv;
    SessionOptions sessionOptions = SessionOptions(); // SessionOptions;
    Ort::Session* mSession=nullptr;
    onnx_path="xxxx/xxx.onnx"

    // 使用文件的方式创建会话
    session = new Ort::Session(mEnv, onnx_path.c_str(), sessionOptions);
  • 图像处理:根据训练模型的前处理方式处理数据
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    # resize->RGB->归一化->标准化->CHW
    cv::Mat imageBGR = cv::imread(imageFilepath, cv::ImreadModes::IMREAD_COLOR);
    cv::Mat resizedImageBGR, resizedImageRGB, resizedImage, preprocessedImage;
    cv::resize(imageBGR, resizedImageBGR,
    cv::Size(inputDims.at(2), inputDims.at(3)),
    cv::InterpolationFlags::INTER_CUBIC);
    cv::cvtColor(resizedImageBGR, resizedImageRGB,
    cv::ColorConversionCodes::COLOR_BGR2RGB);
    resizedImageRGB.convertTo(resizedImage, CV_32F, 1.0 / 255);

    cv::Mat channels[3];
    cv::split(resizedImage, channels);
    // Normalization per channel
    // Normalization parameters obtained from
    // https://github.com/onnx/models/tree/master/vision/classification/squeezenet
    channels[0] = (channels[0] - 0.485) / 0.229;
    channels[1] = (channels[1] - 0.456) / 0.224;
    channels[2] = (channels[2] - 0.406) / 0.225;
    cv::merge(channels, 3, resizedImage);
    // HWC to CHW
    cv::dnn::blobFromImage(resizedImage, preprocessedImage);
  • 构建输入输出:将预处理后的数据整理成 onnxruntime 的格式
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    # 若要使用 ONNX 运行时运行推理,用户负责创建和管理输入和输出缓冲区
    # 这些缓冲区可以通过 std::vector 创建和管理。应将线性格式的输入数据复制到缓冲区
    # 输入缓冲区
    size_t inputTensorSize = vectorProduct(inputDims);
    std::vector<float> inputTensorValues(inputTensorSize);
    inputTensorValues.assign(preprocessedImage.begin<float>(),
    preprocessedImage.end<float>());
    # 输出缓冲区
    size_t outputTensorSize = vectorProduct(outputDims);
    assert(("Output tensor size should equal to the label set size.",
    labels.size() == outputTensorSize));
    std::vector<float> outputTensorValues(outputTensorSize);


    # 创建缓冲区后,它们将用于创建实例,其 Ort::Value 实例是 ONNX 运行时的张量格式
    # 有多少个输入,创建多少个Ort::Value
    std::vector<Ort::Value> inputTensors;
    std::vector<Ort::Value> outputTensors;
    Ort::MemoryInfo memoryInfo = Ort::MemoryInfo::CreateCpu(
    OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
    inputTensors.push_back(Ort::Value::CreateTensor<float>(
    memoryInfo, inputTensorValues.data(), inputTensorSize, inputDims.data(),
    inputDims.size()));
    outputTensors.push_back(Ort::Value::CreateTensor<float>(
    memoryInfo, outputTensorValues.data(), outputTensorSize,
    outputDims.data(), outputDims.size()));
  • 模型推理:使用 Session 推理
    1
    2
    3
    4
    // https://github.com/microsoft/onnxruntime/blob/rel-1.6.0/include/onnxruntime/core/session/onnxruntime_cxx_api.h#L353  
    session.Run(Ort::RunOptions{nullptr}, inputNames.data(),
    inputTensors.data(), 1, outputNames.data(),
    outputTensors.data(), 1);
  • 解析返回:解析推理后的结果
1
2
# 这里获取第一个输出口的结果
float* floatarr = outputTensors[0].GetTensorMutableData<float>(); // 第i个节点得结果

参考:

  1. YOLOv5 在 C++ 中通过 Onnxruntime 在 window 平台上的 cpu 与 gpu 推理_lazyoneguy 的博客 - CSDN 博客
  2. Deploying PyTorch Model into a C++ Application Using ONNX Runtime | by Huili Yu | Medium
  3. 一、ONNX Runtime 的设计理念_onnxruntime_丶 Shining 的博客 - CSDN 博客
  4. 模型部署入门教程(五):ONNX 模型的修改与调试 - 知乎
  5. ONNXRuntime 整体概览 - 知乎
  6. ONNXRuntime 源码之 OpKernel 注册 - 知乎
  7. AI 模型部署 (1) - ONNX - 知乎
  8. AI 模型部署 (2) - ONNX Runtime - 知乎
  9. [5. ONNXRuntime 概述 - 知乎 (zhihu. com)]([ONNX 从入门到放弃] 5. ONNXRuntime 概述 - 知乎
  10. [推理模型部署 (一):ONNX runtime 实践 - 知乎]( https://zhuanlan.zhihu.com/p/582974246 [[ONNX 从入门到放弃] 1. ONNX 协议基础 - 知乎 (zhihu. com)
  11. onnxruntime 的 C++ api 如何实现 session 的多输入与多输出? - 知乎
  12. lite.ai.toolkit/deeplabv3_resnet101.cpp at main · DefTruth/lite.ai.toolkit · GitHub
  13. YOLOv5+TensorRT+OnnxRuntime+Visual Studio+CmakeLists 实现推理: YOLOv5 在 C++ 中通过 TensorRT 或者 Onnxruntime 在 Visual Studio+CmakeLists 上实现推理,用了 spdlog 实现输出,需提前下好。
  14. ONNX Runtime C++ Inference - Lei Mao’s Log Book