Pytorch导出onnx模型,C++转化为TensorRT并实现推理

活动地址:毕业季·进击的技术er

Pytorch导出onnx模型,C++转化为TensorRT并实现推理

本篇为学习笔记,与参考文中有出入的地方,用黄色标记出来。

主要参考
1. Pytorch导出onnx模型,C++转化为TensorRT并实现推理过程
2. onnxruntime安装与使用(附实践中发现的一些问题)
3. TensorRT_Test

一. Pytorch导出onnx模型

  1. 新建一个export_onnx.py文件,全部内容如下。ResNet50_wSoftmax自定义模型,是在官方原有的基础上,添加了softmax操作。

  2. 将一些必要后处理添加到模型中一起导出,这样做有两个优点:
    1)可以直接得到端到端的 onnx/tensorrt 模型,不必在外面再做后处理操作
    2)之后我们会将 onnx 模型转换为 tensorrt 模型,在转换过程中 tensorrt 会对我们的模型进行一些针对特定的 Nvidia GPU 的推理优化,我们将后处理一起合并到 onnx 模型中,可能可以使得一些算子操作再转换为 tensorrt 的过程中同样得到优化。

# export_onnx.py
import torch
import torchvision.models as models
import cv2
import numpy as np

class ResNet50_wSoftmax(torch.nn.Module):
    # 将softmax后处理合并到模型中,一起导出为onnx
    def __init__(self):
        super().__init__()
        self.base_model = models.resnet50(pretrained=True)
        self.softmax = torch.nn.Softmax(dim=1)

    def forward(self, x):
        y = self.base_model(x)
        prob = self.softmax(y)
        return prob

def preprocessing(img):
    # 预处理:BGR->RGB、归一化/除均值减标准差
    IMAGENET_MEAN = [0.485, 0.456, 0.406]
    IMAGENET_STD = [0.229, 0.224, 0.225]
    img = img[:, :, ::-1]
    img = cv2.resize(img, (224, 224))
    img = img / 255.0
    img = (img - IMAGENET_MEAN) / IMAGENET_STD
    img = img.transpose(2, 0, 1).astype(np.float32)
    tensor_img = torch.from_numpy(img)[None] # 此处增加一维, 从[3,224,224] 到 [1, 3,224,224] 
    return tensor_img

if __name__ == '__main__':
    # model = models.resnet50(pretrained=True)
    image_path = 'test.jpg'
    img = cv2.imread(image_path)
    tensor_img = preprocessing(img)
    model = ResNet50_wSoftmax()   # 将后处理添加到模型中
    model.eval()
    pred = model(tensor_img)[0]
    max_idx = torch.argmax(pred)
    print(f"test_image: {image_path}, max_idx: {max_idx}, max_logit: {pred[max_idx].item()}")

    dummpy_input = torch.zeros(1, 3, 224, 224)  # onnx的导出需要指定一个输入,这里直接用上面的tenosr_img也可
    torch.onnx.export(
            model, dummpy_input, 'resnet50_wSoftmax.onnx',
            input_names=['image'],
            output_names=['predict'],
            opset_version=11,
            dynamic_axes={'image': {0: 'batch'}, 'predict': {0: 'batch'}}		# 注意这里指定batchsize是动态可变的
    )
  1. 执行脚本:
python export_onnx.py

注意:操作之前需要准备一张图片,名为test.jpg,随便从网上下载一个即可,可以看到如下的类似的输出结果:

Downloading: "https://download.pytorch.org/models/resnet50-0676ba61.pth" to /home/cui/.cache/torch/hub/checkpoints/resnet50-0676ba61.pth
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 97.8M/97.8M [07:36<00:00, 225kB/s]
test_image: test.jpg, max_idx: 285, max_logit: 0.5382498502731323

二. onnxruntime推理测试

我们将刚刚得到的resnet50_wSoftmax.onnx ,用onnxruntime来进行推理测试,检测结果是否相同。

此处新建一个名为onnxruntime_test.py的文件,全部内容如下。此文件中复用了export_onnx.py中的preprocessing预处理操作,但是原函数输出结果为torch,需要将其转化为numpy类型

# onnxruntime_test.py
import onnxruntime as ort  #若缺少,使用pip install onnxruntime-gpu -i https://pypi.tuna.tsinghua.edu.cn/simple高速安装 
import numpy as np
import cv2
from export_onnx import preprocessing

image_path = 'test.jpg'
ort_session = ort.InferenceSession("resnet50_wSoftmax.onnx", providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider']) # 创建一个推理session

img = cv2.imread(image_path)
input_img = preprocessing(img)[None]
input_img = input_img.numpy() ## 新添加

pred = ort_session.run(None, { 'image' : input_img } )[0][0]
max_idx = np.argmax(pred)
print(f"test_image: {image_path}, max_idx: {max_idx}, probability: {pred[max_idx]}")

执行:

python onnxruntime_test.py

结果如下,基本与pytorch模型推理的一致,证明转换没问题。

test_image: test.jpg, max_idx: 285, probability: 0.5382504463195801

注意
1)此文件ort.InferenceSession中参数与参考文献略有变动,直接执行原网站的脚本会出现如下错误。
解决参考:[ValueError: This ORT build has ‘TensorrtExecutionProvider‘, ‘CUDAExecutionProvider‘, ‘CPUExecutionP

扫描二维码关注公众号,回复: 15915797 查看本文章
ValueError: This ORT build has ['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'] enabled. Since ORT 1.9, you are required to explicitly set the providers parameter when instantiating InferenceSession. For example, onnxruntime.InferenceSession(..., providers=['TensorrtExecutionProvider', 'CUDAExecutionProvider', 'CPUExecutionProvider'], ...)

2)原文直接运行推理会出现RuntimeError: Input must be a list of dictionaries or a single numpy array for input ‘image’
解决参考:onnx推理时报错RuntimeError: Input must be a list of dictionaries or a single numpy array for input ‘image

3)原文中导出的onnx模型是int64的weights,TensorRT不支持64位,但是不影响后续的使用。目前只支持32位,下一步的转换时,会报出相关类型警告。

Your ONNX model has been generated with INT64 weights, while TensorRT does not natively support INT64. Attempting to cast down to INT32

参考:onnx-typecast 位数转换工具

三. onnx模型转换为tensorrt模型

1. 环境安装

CUDA10.2+Cudnn8.4.1+TensorRT7.1.3.4+Onnx-tensorrt7.1,详细的安装可以参考环境配置安装的文章,此处不多展开,例如:

注意版本之间要对应,否则会出现莫名的错误。全部安装完成后,使用下面的命令进行版本检查。

nvcc -V    # 查看cuda版本,10.2
cat /usr/local/cuda-10.2/include/cudnn_version.h | grep CUDNN_MAJOR -A 2    # 查看cudnn8.4版本,与之前版本稍有不同,可参考第一个链接安装cudnn
find / -name NvInferVersion.h   # 查看tensorRT版本号

2. 文件夹建立

新建一个build_model.cc的C++文件,主要包含头文件,logger类,build_model函数等。

  • logger类:来打印构建 tensorrt 模型过程中的一些错误或警告。按照指定的严重性程度 (severity),来打印信息。
  • build_model函数:利用onnx构建,导出engine模型(等效于trt模型,都是序列化的二进制文件)

2.1 完整文件内容如下:

// tensorrt相关
#include <NvInfer.h>
#include <NvInferRuntime.h>
#include <NvInferRuntimeCommon.h>  //新添加,否则找不到nvinfer1::AsciiChar

// onnx解析器相关
#include <NvOnnxParser.h>  // 与原文不同,onnx-tensorrt build后,sudo make install

// cuda_runtime相关
#include <cuda_runtime.h>

// 常用头文件
#include <math.h>
#include <stdio.h>
#include <unistd.h>
#include <chrono>
#include <fstream>
#include <functional>
#include <iostream>
#include <memory>
#include <string>
#include <vector>
#include <dirent.h>

// opencv
#include <opencv2/opencv.hpp>

inline const char* severity_string(nvinfer1::ILogger::Severity t) {
  switch (t) {
    case nvinfer1::ILogger::Severity::kINTERNAL_ERROR:
      return "internal_error";
    case nvinfer1::ILogger::Severity::kERROR:
      return "error";
    case nvinfer1::ILogger::Severity::kWARNING:
      return "warning";
    case nvinfer1::ILogger::Severity::kINFO:
      return "info";
    case nvinfer1::ILogger::Severity::kVERBOSE:
      return "verbose";
    default:
      return "unknown";
  }
}

class TRTLogger : public nvinfer1::ILogger {
 public:
  virtual void log(Severity severity, const char* msg) noexcept override {
    if (severity <= Severity::kWARNING) {
      if (severity == Severity::kWARNING)
        printf("\033[33m%s: %s\033[0m\n", severity_string(severity), msg);
      else if (severity == Severity::kERROR)
        printf("\031[33m%s: %s\033[0m\n", severity_string(severity), msg);
      else
        printf("%s: %s\n", severity_string(severity), msg);
    }
  }
};

bool build_model() {
  if (access("resnet50.engine", 0) == 0) {
    printf("resnet50.engine already exists.\n");
    return true;
  }

  TRTLogger logger;

  // 下面的builder, config, network是基本需要的组件
  // 形象的理解是你需要一个builder去build这个网络,网络自身有结构,这个结构可以有不同的配置
  nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
  // 创建一个构建配置,指定TensorRT应该如何优化模型,tensorRT生成的模型只能在特定配置下运行
  nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
  // 创建网络定义,其中createNetworkV2(1)表示采用显性batch
  // size,新版tensorRT(>=7.0)时,不建议采用0非显性batch size
  nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1);

  // onnx parser解析器来解析onnx模型
  auto parser = nvonnxparser::createParser(*network, logger);
  if (!parser->parseFromFile("../resnet50_wSoftmax.onnx", 1)) {
    printf("Failed to parse resnet50_wSoftmax.onnx.\n");
    return false;
  }

  // 设置工作区大小
  printf("Workspace Size = %.2f MB\n", (1 << 28) / 1024.0f / 1024.0f);
  config->setMaxWorkspaceSize(1 << 28);

  // 需要通过profile来使得batchsize时动态可变的,这与我们之前导出onnx指定的动态batchsize是对应的
  int maxBatchSize = 10;
  auto profile = builder->createOptimizationProfile();
  auto input_tensor = network->getInput(0);
  auto input_dims = input_tensor->getDimensions();

  // 设置batchsize的最大/最小/最优值
  input_dims.d[0] = 1;
  profile->setDimensions(input_tensor->getName(),
                         nvinfer1::OptProfileSelector::kMIN, input_dims);
  profile->setDimensions(input_tensor->getName(),
                         nvinfer1::OptProfileSelector::kOPT, input_dims);

  input_dims.d[0] = maxBatchSize;
  profile->setDimensions(input_tensor->getName(),
                         nvinfer1::OptProfileSelector::kMAX, input_dims);
  config->addOptimizationProfile(profile);

  // 开始构建tensorrt模型engine
  nvinfer1::ICudaEngine* engine =
      builder->buildEngineWithConfig(*network, *config);

  if (engine == nullptr) {
    printf("Build engine failed.\n");
    return false;
  }

  // 将构建好的tensorrt模型engine反序列化(保存成文件)
  nvinfer1::IHostMemory* model_data = engine->serialize();
  FILE* f = fopen("resnet50.engine", "wb");
  fwrite(model_data->data(), 1, model_data->size(), f);
  fclose(f);

  // 逆序destory掉指针
  model_data->destroy();
  engine->destroy();
  network->destroy();
  config->destroy();
  builder->destroy();

  printf("Build Done.\n");
  return true;
}

int main() {
  if (!build_model()) {
    printf("Couldn't build engine!\n");
  }
  return 0;
}

2.2 Cmakelists.txt文件
完整工程参考:TensorRT_Test

2.3 编译运行

mkdir build && cd build
cmake ..
make -j8
./build_model

2.4 结果
出现如下情况,且生成了rsenet50.engine文件,即为成功转换。

warning: [TRT]/home/cui/workspace/tools/onnx-tensorrt/onnx2trt_utils.cpp:220: Your ONNX model has been generated with INT64 weights, while TensorRT does not natively support INT64. Attempting to cast down to INT32.
Workspace Size = 256.00 MB
Build Done.

2.5 可能遇到的问题
Q1:error: ‘nvinfer1::AsciiChar’ has not been declared
解决:tensorRT版本问题,将其改为const char*。

Q2: error: Could not open file resnet50_wSoftmax.onnx
解决:修改build_model.cc文件中78行,resnet50_wSoftmax.onnx 模型文件的对应路径


四. TensorRT模型推理测试

目的:验证与之前的Pytorch模型和onnx模型推理结果是否一致。

  1. 完整文件内容,请参考工程中的model.infer.cc文件,与原文中稍有改动。增加了checkRuntime实现:
#ifndef checkRuntime
#define checkRuntime(callstr)\
    {\
        cudaError_t error_code = callstr;\
        if (error_code != cudaSuccess) {\
            std::cerr << "CUDA error " << error_code << " at " << __FILE__ << ":" << __LINE__ << std::endl;\
            assert(0);\
        }\
    }
#endif  // checkRuntime

工程地址:TensorRT_Test

  1. 编译执行
mkdir build && cd build
cmake ..
make -j8
./model_infer
  1. 结果
test_image: ../test.jpg, max_idx: 285, probability: 0.538250(learning3d)
  1. 可能遇到的问题
    Q1: error while loading shared libraries: libcudnn.so.8: cannot open shared object file: No such file or directory
    解决:未找到cuda安装环境,可以进入conda配置好的某个环境下,在执行相应的操作。或者参看CUDA安装环境,将其配置到环境变量中。

猜你喜欢

转载自blog.csdn.net/weixin_36354875/article/details/125596653