五. TensorRT API的基本使用-load-model

前言

自动驾驶之心推出的 《CUDA与TensorRT部署实战课程》,链接。记录下个人学习笔记,仅供自己参考

本次课程我们来学习课程第五章—TensorRT API 的基本使用,一起来学习手动实现一个 build

课程大纲可以看下面的思维导图

在这里插入图片描述

0. 简述

本小节目标:手动实现 build 完成模型的序列化

今天我们来讲第五章节第二小节—5.2-load-model 这个案例,上个小节我们主要是通过官方 MNIST 案例让大家熟悉 TensorRT 的一些 API 的使用,这个小节我们主要是模仿官方案例来自己手写一个 build

下面我们开始本次课程的学习🤗

1. 案例运行

在正式开始课程之前,博主先带大家跑通 5.2-load-model 这个小节的案例🤗

源代码获取地址:https://github.com/kalfazed/tensorrt_starter.git

首先大家需要把 tensorrt_starter 这个项目给 clone 下来,指令如下:

git clone https://github.com/kalfazed/tensorrt_starter.git

也可手动点击下载,点击右上角的 Code 按键,将代码下载下来。至此整个项目就已经准备好了。也可以点击 here 下载博主准备好的源代码(注意代码下载于 2024/7/14 日,若有改动请参考最新

整个项目后续需要使用的软件主要有 CUDA、cuDNN、TensorRT、OpenCV,大家可以参考 Ubuntu20.04软件安装大全 进行相应软件的安装,博主这里不再赘述

假设你的项目、环境准备完成,下面我们来一起运行 5.2 小节案例代码

开始之前我们需要创建几个文件夹,在 tensorrt_starter/chapter5-tensorrt-api-basics/5.2-load-model 小节中创建一个 models 文件夹,接着在 models 文件夹下创建 onnx 和 engine 文件夹,总共三个文件夹需要创建

创建完后 5.2 小节整个目录结构如下:

在这里插入图片描述

接着我们需要执行 python 文件创建一个 ONNX 模型,先进入到 5.2 小节中:

cd tensorrt_starter/chapter5-tensorrt-api-basics/5.2-load-model

执行如下指令:

python src/python/generate_onnx.py

Note:大家需要准备一个虚拟环境,安装好 torch、onnx、onnxsim 等第三方库

输出如下:

在这里插入图片描述

生成好的 onnx 模型文件保存在 models/onnx 文件夹下,大家可以查看

接着我们需要利用 ONNX 生成对应的 engine,在此之前我们需要修改下整体的 Makefile.config,指定一些库的路径:

# tensorrt_starter/config/Makefile.config
# CUDA_VER                    :=  11
CUDA_VER                    :=  11.6
    
# opencv和TensorRT的安装目录
OPENCV_INSTALL_DIR          :=  /usr/local/include/opencv4
# TENSORRT_INSTALL_DIR        :=  /mnt/packages/TensorRT-8.4.1.5
TENSORRT_INSTALL_DIR        :=  /home/jarvis/lean/TensorRT-8.6.1.6

Note:大家查看自己的 CUDA 是多少版本,修改为对应版本即可,另外 OpenCV 和 TensorRT 修改为你自己安装的路径即可

接着我们就可以来执行编译,指令如下:

make -j64

输出如下:

在这里插入图片描述

接着执行:

./trt-infer

输出如下:

在这里插入图片描述

可以看到输出了很多日志信息,该案例主要是通过自定义 build 构建一个 engine 并保存到 models/engine/sample.engine 中,最后打印输入和输出的维度信息

如果大家能够看到上述输出结果,那就说明本小节案例已经跑通,下面我们就来看看具体的代码实现

2. 代码分析

2.1 main.cpp

我们先从 main.cpp 看起:

#include <iostream>
#include <memory>

#include "model.hpp"
#include "utils.hpp"

using namespace std;

int main(int argc, char const *argv[])
{
    Model model("models/onnx/sample.onnx");
    if(!model.build()){
        LOGE("ERROR: fail in building model");
        return 0;
    }
    return 0;
}

通过传入 ONNX 模型文件路径创建一个 Model 实例,之后调用 build 函数构建 engine

2.2 model.hpp

我们来看 Model 类的定义:

class Model{
public:
    Model(std::string onnxPath);
    bool build();
private:
    std::string mOnnxPath;
    std::string mEnginePath;
    nvinfer1::Dims mInputDims;
    nvinfer1::Dims mOutputDims;
    std::shared_ptr<nvinfer1::ICudaEngine> mEngine;
    bool constructNetwork();
    bool preprocess();
};

Model 类中公有方法主要是 build,而其它的比如 mEngine,constructNetwork,preprocess 等都是私有方法,没有必要暴露给用户的

2.3 model.cpp

接着我们来看 Model 的构造函数:

Model::Model(string onnxPath){
    if (!fileExists(onnxPath)) {
        LOGE("%s not found. Program terminated", onnxPath.c_str());
        exit(1);
    }
    mOnnxPath   = onnxPath;
    mEnginePath = getEnginePath(mOnnxPath);
}

首先它会去检查传入的 onnxPath 文件是否存在,如果不存在则打印错误信息并退出,接着把 onnxPath 赋值给私有成员变量 mOnnxPath,通过 getEnginePath 函数拿到对应的 mEnginePath。另外这里的 LOGE 是通过宏定义实现的一个打印函数,它可以用来控制不同的输出日志等级

我们再来看 build 的实现,首先是检查 mEnginePath 是否存在:

if (fileExists(mEnginePath)){
    LOG("%s has been generated!", mEnginePath.c_str());
    return true;
} else {
    LOG("%s not found. Building engine...", mEnginePath.c_str());
}

如果存在则不用再重新 build,如果不存在则需要通过下面的流程进行 build

首先我们实例化一个 logger:

class Logger : public nvinfer1::ILogger{
public:
    virtual void log (Severity severity, const char* msg) noexcept override{
        string str;
        switch (severity){
            case Severity::kINTERNAL_ERROR: str = RED    "[fatal]:" CLEAR;
            case Severity::kERROR:          str = RED    "[error]:" CLEAR;
            case Severity::kWARNING:        str = BLUE   "[warn]:"  CLEAR;
            case Severity::kINFO:           str = YELLOW "[info]:"  CLEAR;
            case Severity::kVERBOSE:        str = PURPLE "[verb]:"  CLEAR;
        }
        if (severity <= Severity::kINFO)
            cout << str << string(msg) << endl;
    }
};

Logger logger;

我们上节课讲过在创建一个 builder 的时候需要绑定一个 logger,因此我们这里自己手动实现了一个 Logger 类,它继承自 nvinfer1::ILogger,在 Logger 类中我们必须自己手动来实现 log 虚函数。Severity 是一个枚举类,用于控制日志消息的等级,然后将不同的 str 附加不同颜色,如果 severity 级别小于或等于 kINFO,则会通过 cout 将带有前缀的 str 日志信息打印出来

创建完 logger 之后我们接着往下看:

auto builder       = make_unique<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(logger));
auto network       = make_unique<nvinfer1::INetworkDefinition>(builder->createNetworkV2(1));
auto config        = make_unique<nvinfer1::IBuilderConfig>(builder->createBuilderConfig());
auto parser        = make_unique<nvonnxparser::IParser>(nvonnxparser::createParser(*network, logger));

config->setMaxWorkspaceSize(1<<28);

if (!parser->parseFromFile(mOnnxPath.c_str(), 1)){
    LOGE("ERROR: failed to %s", mOnnxPath.c_str());
    return false;
}

其实和上节课讲的流程一样,我们先创建一个 builder,然后通过 builder 创建 network、config,接着把 network 和 logger 丢到 nvonnxparser::createParser 函数中创建一个 parser

接着通过 config 设置了最大的 workspace size,其实 config 可以设置非常多的参数,包括 setCalibrationProfile 设置校准文件,setInt8Calibrator 设置校准器等等,这些都是跟模型创建相关的东西,大家自己可以看下

另外这些 API 的说明在官方文档中描述都比较详细,大家也可以参考:tensorrt/developer-guide

config 设置完成之后,通过 parserFromFile 函数将 onnx parser 到 network 里面去

上面这些都是准备工作,接着我们就可以来创建 engine:

auto engine        = make_unique<nvinfer1::ICudaEngine>(builder->buildEngineWithConfig(*network, *config));
auto plan          = builder->buildSerializedNetwork(*network, *config);
auto runtime       = make_unique<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger));

auto f = fopen(mEnginePath.c_str(), "wb");
fwrite(plan->data(), 1, plan->size(), f);
fclose(f);

通过 builder->buildEngineWithConfig 把 network 和 config 丢进去创建 engine,之后把创建好的 network 做一个序列化保存到 plan 中去,其中 plan 是一个 IHostMemory 的指针,然后我们创建了一个 runtime 方便后续反序列化测试

接着我们把序列化好的 plan 文件通过 fwrite 写入保存到指定路径,方便下次加载使用

下面我们打印了模型的一些基本信息:

mEngine            = shared_ptr<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(plan->data(), plan->size()));
mInputDims         = network->getInput(0)->getDimensions();
mOutputDims        = network->getOutput(0)->getDimensions();
LOG("Input dim is %s", printDims(mInputDims).c_str());
LOG("Output dim is %s", printDims(mOutputDims).c_str());
return true;

通过 runtime->deserializeCudaEngine 来反序列化拿到我们的 engine,其中的 mEngine 是 ICudaEnigne 的指针,是一个推理引擎,然后我们可以通过 network 将输入输出的一些维度信息打印出来

这里有一个小技巧,大家在学习 API 的时候可以通过一些名字大概猜测其主要实现的功能,比如 network 它其中的以 getXXX 为例的 API 一般来说都是去获取网络的一些信息,比如 getLayer、getName 等等,再比如 engine 也有类似于 getXXX 的 API,比如 getDeviceMemorySize、getNbOptimizationProfiles 等等

2.4 其它

在 src/python 文件夹下还有一个 generate_onnx.py 的脚本文件,其内容如下:

import torch
import torch.nn as nn
import torch.onnx
import onnxsim
import onnx
import os

class Model(torch.nn.Module):
    def __init__(self):
        super().__init__()
        self.linear = nn.Linear(in_features=10, out_features=5, bias=False)
    
    def forward(self, x):
        x = self.linear(x)
        return x

def setup_seed(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)

def export_norm_onnx():
    current_path = os.path.dirname(__file__)
    file = current_path + "/../../models/onnx/sample.onnx"

    input   = torch.rand(1, 10)
    model   = Model()
    torch.onnx.export(
        model         = model, 
        args          = (input,),
        f             = file,
        input_names   = ["input0"],
        output_names  = ["output0"],
        opset_version = 15)
    print("Finished normal onnx export")

    # check the exported onnx model
    model_onnx = onnx.load(file)
    onnx.checker.check_model(model_onnx)

    # use onnx-simplifier to simplify the onnx
    print(f"Simplifying with onnx-simplifier {onnxsim.__version__}...")
    model_onnx, check = onnxsim.simplify(model_onnx)
    assert check, "assert check failed"
    onnx.save(model_onnx, file)

def infer():
    setup_seed(1)
    model  = Model()
    input  = torch.tensor([[0.0193, 0.2616, 0.7713, 0.3785, 0.9980, 0.9008, 0.4766, 0.1663, 0.8045, 0.6552]])
    output = model(input)
    print(input)
    print(output)

if __name__ == "__main__":
    export_norm_onnx()
    infer()

它就是创建了一个非常简单的 ONNX 模型,其中包含一个 Linear 节点,如下所示:

在这里插入图片描述

总结

本次课程我们主要模仿官方案例自己手动实现了一个 builder,和官方流程类似,先创建一个 logger,然后创建 builder,通过 builder 创建 network、config,然后创建 parser,通过 parseFromFile 将 ONNX parser 到 network 中,接着创建完 engine,通过 buildSerializedNetwork 进行序列化生成 plan,并将 plan 保存下来,最后调用一个 API 来打印一些输入输出维度信息。总的来说,实现还是比较简单的,关于一些 API 的使用大家可以多尝试尝试

OK,以上就是 5.2 小节案例的全部内容了,下节我们来学习 5.3 小节自己构建一个 infer 来推理模型,敬请期待😄

下载链接

参考

TensorRT是一个高性能的深度学习推理库,可以用来加速模型的推理过程。要在C++中使用TensorRT部署Real-RGAN模型,需要进行以下步骤: 1. 将Real-ESRGAN模型转换为ONNX格式。 2. 使用TensorRT API将ONNX模型转换为TensorRT引擎。 3. 使用TensorRT引擎进行推理。 下面是一个简单的C++代码示例,用于将ONNX模型转换为TensorRT引擎,并进行推理: ```c++ #include <iostream> #include <fstream> #include <vector> #include <opencv2/opencv.hpp> #include <NvInfer.h> #include <NvOnnxParser.h> using namespace std; using namespace nvinfer1; using namespace nvonnxparser; int main() { // Step 1: Load the ONNX model const string onnx_model_path = "real_esrgan.onnx"; ifstream onnx_file(onnx_model_path, ios::binary); onnx_file.seekg(0, ios::end); const size_t onnx_size = onnx_file.tellg(); onnx_file.seekg(0, ios::beg); vector<char> onnx_buf(onnx_size); onnx_file.read(onnx_buf.data(), onnx_size); // Step 2: Create the TensorRT engine IRuntime* runtime = createInferRuntime(logger); ICudaEngine* engine = runtime->deserializeCudaEngine(onnx_buf.data(), onnx_buf.size(), nullptr); IExecutionContext* context = engine->createExecutionContext(); // Step 3: Prepare input and output buffers const int batch_size = 1; const int input_channels = 3; const int input_height = 256; const int input_width = 256; const int output_channels = 3; const int output_height = 1024; const int output_width = 1024; // Allocate memory for input and output buffers float* input_data = new float[batch_size * input_channels * input_height * input_width]; float* output_data = new float[batch_size * output_channels * output_height * output_width]; // Create input and output tensors ITensor* input_tensor = engine->getBindingTensor(0); ITensor* output_tensor = engine->getBindingTensor(1); // Create CUDA memory for input and output tensors void* input_cuda_mem, *output_cuda_mem; cudaMalloc(&input_cuda_mem, batch_size * input_channels * input_height * input_width * sizeof(float)); cudaMalloc(&output_cuda_mem, batch_size * output_channels * output_height * output_width * sizeof(float)); // Step 4: Run inference cv::Mat input_image = cv::imread("input.png"); cv::Mat input_resized; cv::resize(input_image, input_resized, cv::Size(input_width, input_height)); // Copy input data to CUDA memory cudaMemcpy(input_cuda_mem, input_data, batch_size * input_channels * input_height * input_width * sizeof(float), cudaMemcpyHostToDevice); // Set input tensor data input_tensor->setLocation(CUDA, input_cuda_mem); input_tensor->setDimensions({ batch_size, input_channels, input_height, input_width }); input_tensor->setType(DataType::kFLOAT); // Set output tensor data output_tensor->setLocation(CUDA, output_cuda_mem); output_tensor->setDimensions({ batch_size, output_channels, output_height, output_width }); output_tensor->setType(DataType::kFLOAT); // Run inference context->enqueue(batch_size, &input_cuda_mem, &output_cuda_mem, nullptr); // Copy output data from CUDA memory cudaMemcpy(output_data, output_cuda_mem, batch_size * output_channels * output_height * output_width * sizeof(float), cudaMemcpyDeviceToHost); // Convert output data to OpenCV Mat cv::Mat output_image(output_height, output_width, CV_32FC3, output_data); output_image.convertTo(output_image, CV_8UC3, 255.0); // Step 5: Save output image cv::imwrite("output.png", output_image); // Step 6: Cleanup cudaFree(input_cuda_mem); cudaFree(output_cuda_mem); delete[] input_data; delete[] output_data; context->destroy(); engine->destroy(); runtime->destroy(); return 0; } ``` 在这个示例中,我们首先加载了Real-ESRGAN模型的ONNX文件,然后使用TensorRT API将其转换为TensorRT引擎。接下来,我们准备了输入和输出缓冲区,并使用CUDA内存分配函数为它们分配了内存。然后,我们将输入数据复制到CUDA内存中,并设置了输入和输出Tensor的相关属性。最后,我们启动了推理过程,并将结果保存到输出文件中。最后,我们清理了使用的资源。 需要注意的是,该示例仅适用于输入和输出都是Mat的情况。如果你的输入和输出数据类型不同,请相应地修改代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱听歌的周童鞋

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值