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

前言

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

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

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

在这里插入图片描述

0. 简述

本小节目标:手动实现 infer 完成模型的推理

今天我们来讲第五章节第三小节—5.3-load-model 这个案例,上个小节我们自己手动实现了一个 build,这个小节我们主要是自己手动实现一个 infer 完成模型的推理过程

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

1. 案例运行

在正式开始课程之前,博主先带大家跑通 5.3-infer-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.3 小节案例代码

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

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

在这里插入图片描述

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

cd tensorrt_starter/chapter5-tensorrt-api-basics/5.3-infer-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

输出如下:

在这里插入图片描述

在这里插入图片描述

我们这里读取一个模型,然后将输入数据打印出来,通过模型的推理拿到对应的输出,将输出数据也打印出来,我们的模型是使用的上节课的 onnx,就是一个 Linear 层

我们在执行下 python 的推理,对比二者是否相同:

在这里插入图片描述

可以看到 C++ 和 Python 的推理结果都是一致的,说明我们的 infer 是没有问题的

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

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("fail in building model");
        return 0;
    }
    if(!model.infer()){
        LOGE("fail in infering model");
        return 0;
    }
    return 0;
}

与上节 build 案例相比 main 函数中多了一个 infer 的接口,这个也是仿照着官方 MNIST 案例去写的

2.2 model.cpp

我们来看 infer 接口中具体做了些什么操作,其实 infer 需要做的事情主要包括以下几个部分:

  • 1. 读取 model,创建 runtime,engine,context
  • 2. 把数据进行 host-> device 传输
  • 3. 使用 context 推理
  • 4. 把数据进行 device->host 传输

首先我们来看第一部分:

/* 1. 读取model => 创建runtime, engine, context */
if (!fileExists(mEnginePath)) {
    LOGE("ERROR: %s not found", mEnginePath.c_str());
    return false;
}

/* 反序列化从文件中读取的数据以unsigned char的vector保存*/
vector<unsigned char> modelData;
modelData = loadFile(mEnginePath);

Logger logger;
auto runtime     = make_unique<nvinfer1::IRuntime>(nvinfer1::createInferRuntime(logger));
auto engine      = make_unique<nvinfer1::ICudaEngine>(runtime->deserializeCudaEngine(modelData.data(), modelData.size()));
auto context     = make_unique<nvinfer1::IExecutionContext>(engine->createExecutionContext());

auto input_dims   = context->getBindingDimensions(0);
auto output_dims  = context->getBindingDimensions(1);

LOG("input dim shape is:  %s", printDims(input_dims).c_str());
LOG("output dim shape is: %s", printDims(output_dims).c_str());

我们先查看文件是否存在,接着通过 loadFile 函数加载文件中的数据保存到 modelData 中,然后实例化一个 logger,再通过 nvinfer1::createInferRuntime 实例化一个 runtime 对象,接着从 runtime 反序列化创建 engine,再从 engine 创建 context,到此为止准备工作就已经完成了

接着我们通过 context 的 getBindingDimensions 这个 API 获取到输入和输出的 dims,接着将其打印出来

Note:在 TensorRT 中,Binding 是指模型输入和输出张量与内存的关联,在实际使用中,我们需要确保为每个输入和输出绑定正确分配内存,并通过索引管理这些绑定。

下一部分开始做数据传递:

/* 2. host->device的数据传递 */
cudaStream_t stream;
cudaStreamCreate(&stream);

/* host memory上的数据*/
float input_host[] = {0.0193, 0.2616, 0.7713, 0.3785, 0.9980, 0.9008, 0.4766, 0.1663, 0.8045, 0.6552};
float output_host[5];

/* device memory上的数据*/
float* input_device = nullptr;
float* weight_device = nullptr;
float* output_device = nullptr;

int input_size = 10;
int output_size = 5;

/* 分配空间, 并传送数据从host到device*/
cudaMalloc(&input_device, sizeof(input_host));
cudaMalloc(&output_device, sizeof(output_host));
cudaMemcpyAsync(input_device, input_host, sizeof(input_host), cudaMemcpyKind::cudaMemcpyHostToDevice, stream);

和之前 MNIST 案例一样,我们先定义一个 stream,接着定义下 input 数据,这里是为了方便测试所以与 python 中的数据保持一致,定义 output 变量,接着我们在 device 上定义一些变量,包括 input_device、weight_device、output_device,然后通过 cudaMalloc 分配空间,通过 cudaMemcpyAsync 将 host 上的 input 数据拷贝到 device 上

接着第三步开始推理:

/* 3. 模型推理, 最后做同步处理 */
float* bindings[] = {input_device, output_device};
bool success = context->enqueueV2((void**)bindings, stream, nullptr);

我们将输入和输出绑定起来,然后送到模型中去执行推理

最后我们做数据传递,将 device 上的推理结果传递到 host 上:

/* 4. device->host的数据传递 */
cudaMemcpyAsync(output_host, output_device, sizeof(output_host), cudaMemcpyKind::cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);

LOG("input data is:  %s", printTensor(input_host, input_size).c_str());
LOG("output data is: %s", printTensor(output_host, output_size).c_str());
LOG("finished inference");

数据传递通过 stream 来做一个通过,接着将得到的推理结果打印出来

整个 infer 比较简单,因为没有涉及到预处理和后处理,大家简单了解下就行

2.3 其它

在 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)

        for m in self.modules():
            if isinstance(m, nn.Conv2d):
                nn.init.kaiming_normal_(m.weight, mode='fan_out', nonlinearity='relu')
            elif isinstance(m, nn.Linear):
                nn.init.normal_(m.weight, mean=0., std=1.)
            elif isinstance(m, (nn.BatchNorm2d, nn.GroupNorm)):
                nn.init.constant_(m.wdight, 1)
                nn.init.constant_(m.bias, 0)
    
    def forward(self, x):
        x = self.linear(x)
        return x

def setup_seed(seed):
    torch.manual_seed(seed)
    torch.cuda.manual_seed_all(seed)
    torch.backends.cudnn.deterministic = True

def export_norm_onnx(input, model):
    current_path = os.path.dirname(__file__)
    file = current_path + "/../../models/onnx/sample.onnx"
    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 eval(input, model):
    output = model(input)
    print("from infer------")
    print(input)
    print(output)

if __name__ == "__main__":
    setup_seed(1)

    input = torch.tensor([[0.0193, 0.2616, 0.7713, 0.3785, 0.9980, 0.9008, 0.4766, 0.1663, 0.8045, 0.6552]])
    model = Model()

    export_norm_onnx(input, model)
    eval(input, model)

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

在这里插入图片描述

然后准备了一些输入数据进行验证测试

总结

本次课程我们主要在 5.2 小节的案例上增加了 infer 接口完成了模型的推理,整个过程还是比较简单的,首先读取 model 创建 runtime、engine、context,接着把输入数据从 host 上传输到 device 上,然后使用 context 进行推理,最后把推理好的数据从 device 上传输到 host 上打印显示。这里并没有涉及到复杂的预处理和后处理所以整个过程比较清晰,大家了解下就行

OK,以上就是 5.3 小节案例的全部内容了,下节我们来学习 5.4 小节来打印观察下经过 TensorRT 优化前后模型的结构

下载链接

参考

### RangeNet与TensorRT优化实现 对于RangeNet模型而言,在嵌入式设备上部署时,性能是一个重要的考量因素。为了提高推理速度并减少资源消耗,可以采用NVIDIA的TensorRT工具来优化该模型。 #### 准备工作 确保安装了必要的依赖库,包括但不限于CUDA、cuDNN以及TensorRT本身。这些组件能够提供底层硬件加速支持,从而显著提升网络运行效率[^1]。 #### 导出ONNX格式模型 由于TensorRT主要接受ONNX作为输入文件之一,因此首先需要将原始训练好的PyTorch版本RangeNet转换成ONNX格式: ```python import torch.onnx from model import RangeNet # 假设这是定义RangeNet的地方 dummy_input = torch.randn(1, 3, 640, 960).cuda() model = RangeNet().eval().cuda() torch.onnx.export(model, dummy_input, "rangenet.onnx", export_params=True, opset_version=11, do_constant_folding=True, input_names=['input'], output_names=['output']) ``` 此部分代码展示了如何利用`torch.onnx.export()`函数完成从PyTorch到ONNX的转换过程。 #### 使用TensorRT构建引擎 一旦获得了`.onnx`文件之后,则可以通过Python API加载它,并创建相应的TRTEngine用于实际推断操作: ```cpp #include <NvInfer.h> #include <onnx_parser.hpp> // ...省略其他初始化设置... nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger); builder->setMaxBatchSize(batch_size); auto network = builder->createNetworkV2(1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)); parser.parseFromFile(onnx_file_path.c_str(), verbosity); // 构建engine... ICudaEngine *engine = builder->buildCudaEngine(*network); assert(engine != nullptr && "Failed to build engine"); // 序列化engine以便后续重用 IHostMemory* modelStream{nullptr}; if (engine) { modelStream = engine->serialize(); } ``` 上述C++片段说明了通过解析器读取ONNX描述符后建立计算图的过程;接着调用`buildCudaEngine()`方法得到最终可用于执行的任务调度程序——即所谓的“引擎”。 #### 执行推理任务 最后一步就是编写简单的接口去驱动这个已经准备完毕的推理管道: ```c++ void infer(nvinfer1::ICudaEngine& engine){ // 获取输入/输出张量信息 const int inputIndex = engine.getBindingIndex("input"); const int outputIndex = engine.getBindingIndex("output"); void* buffers[] = {d_input, d_output}; context->executeV2(buffers); } int main(){ // 创建上下文对象并与特定平台绑定 IExecutionContext* context = engine.createExecutionContext(); // 分配GPU内存空间给inputs & outputs cudaMalloc(&d_input, sizeof(float)*batch_size*channel*height*width); cudaMalloc(&d_output,sizeof(float)*num_classes*grid_height*grid_width); // 将host端数据复制至device端 cudaMemcpy(d_input,h_inputs,cudaMemcpyHostToDevice); // 开始预测流程 infer(*engine); // 把结果搬回CPU处理 cudaMemcpy(h_outputs,d_output,num_classes*grid_height*grid_width*sizeof(float),cudaMemcpyDeviceToHost); return 0; } ``` 这段伪代码概括了一个完整的前向传播周期:先准备好所有必需的数据结构(比如指针数组),再启动一次性的同步调用来触发内核运算,直至获取到预期的结果集为止。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

爱听歌的周童鞋

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

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

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

打赏作者

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

抵扣说明:

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

余额充值