详解TensorRT的C++高性能部署
一. ONNX
1. ONNX的定位
ONNX是一种中间文件格式,用于解决部署的硬件
与不同的训练框架
特定的模型格式
的兼容性
问题。
ONNX本身其实是一种模型格式
,属于文本
,不是程序,因而无法直接在硬件设备上运行。因此,就需要ONNX Runtime、TensorRT等软件栈(推理框架
(引擎))来加载ONNX模型,从而使得它在硬件设备上能够高效地推理。
ONNX 使用的是 Protobuf
这个序列化数据结构去存储神经网络的权重信息。
Protobuf
是一种轻便高效的结构化数据存储格式
,可以用于结构化数据串行化,或者说序列化。它很适合做数据存储
或数据交换格式
。可用于通讯协议、数据存储等领域的语言无关、平台无关、可扩展的序列化结构数据格式。目前提供了 C++、Java、Python 三种语言的 API。
许多芯片厂商依托自研的推理框架,NVIDIA的TensorRT、Intel的OpneVINO等可以充分发挥自家芯片的能力,但是普适性较差
,你没有办法应用到其它的芯片上。
而,ONNX Runtime等通用性强
,可以运行在不同的软硬件平台。
所以,PyTorch模型的部署通用流程一般如下:
首先
,训练PyTorch等深度学习框架的网络模型;接着
,将模型转换为ONNX模型格式;最后
,使用推理框架把ONNX模型高效地运行在特定的软硬件平台上。
2. ONNX模型格式
ONNX (Open Neural Network Exchange)
一种针对机器学习所设计的开放式的文件格式
,用于存储训练好的模型
。不同
的训练框架可采用相同格式存储模型
并交互
。由微软,亚马逊,Facebook和BM等公司共同发起。
下图,是经典的LeNet-5由PyTorch框架转换ONNX中间格式后,netron.app
可视化的结构图。ONNX模型是一个有向无环图
,图中的每个结点
代表每个用于计算的算子
,所有算子的集合称之为算子集
,图中的边
表示结点的计算顺序
和数据的流向
。
模型属性,可以看到ONNX规范的第6个版本,PyTorch的版本,ONNX算子集的版本。
也可以点击结点,查看每个结点的信息。属性attributes记录的就记录超参数信息。1个输入,1个输出(名称为11)等等。
ONNX中定义的所有算子构成了算子集,访问网页,可以查看所有算子的定义。
算子在不同
的版本,可能会有差异
,比如这里的全局平均池化AveragePool,ONNX中AveragePool的属性中pads是个list,而PyTorch中是1个int,所以PyTorch导出ONNX时,会在AveragePool前面加上1个Pad结点
。
3. ONNX代码使用实例
这里以图像分类模型转ONNX为例,进行PyTorch模型转ONNX。
import torch
import torchvision
# 选择模型推理的设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
# 从pytorch官方实例化预训练模型,并转验证模型
model = torchvision.models.resnet18(pretrained=False)
model = model.eval().to(device)
# 构造一个输入图像的Tensor
# 该Tensor不需要任何的意义,只要在维度上匹配模型的输入即可
# 相当于构建一个输入,走一遍模型的推理过程
x = torch.randn(1, 3, 256, 256).to(device)
# 将x输入进模型推理
output = model(x)
print(output.shape) # 1x1000
# PyTorch模型转ONNX
with torch.no_grad():
torch.onnx.export(model, # 要转换的模型
x, # 模型的任意一个匹配的输入
"resnet18.onnx", # 导出的文件名
input_names=['input'], # 输入结点的名称列表(自定义名称)
output_names=['output'], # 输出结点的名称列表(自定义名称)
opset_version=11, # ONNX的算子集版本
)
加载导出的ONNX模型,并验证。
import onnx
# 验证是否导出成功
# 读取onnx模型
onnx_model = onnx.load('resnet18.onnx')
# 检查模型格式是否正确
onnx.checker.check_model(onnx_model)
# 以可读的形式打印计算图
print(onnx.helper.printable_graph(onnx_model.graph))
推理引擎ONNX Runtime的使用。
import onnxruntime
import torch
# 载入onnx模型,获取ONNX Runtime推理器
ort_session = onnxruntime.InferenceSession('resnet18.onnx')
# 构造随机输入
x = torch.randn(1, 3, 256, 256).numpy()
# ONNX Runtime的输入
# 这里构建的输入和输出的名称要和上面模型导出时自定义的名称一致。
ort_inputs = {
'input': x}
# ONNX Runtime的输出,是1个list,对应模型的forward输出多少个,这里就是1个
ort_output = ort_session.run(['output'], ort_inputs)[0]
pass
注意事项:
1.在转ONNX时,将模型转成.eval()验证模式
,因为模型在训练时,BN层、dropout都会起作用,而推理是不需要的。
2.这里导出onnx的api中第2个参数args
,必须和我们使用PyTorch定义的模型model中forward
函数中传入的参数一致
,因为模型是torch.nn.Module,只有再执行一遍
前向推理过程,也就是forward,才知道模型中有哪些算子
。这也就是torch.jit.trace过程
,trace得到的torch.jit.ScriptModule才是真正的计算图结构
。
3.我们在上述导出ONNX时,x = torch.randn(1, 3, 256, 256).numpy()
,代表着batch为1,每次模型的推理只能接受1张图
,这么做的效率就低了。可以在导出的时候设置dynamic_axes参数
,使得动态接受数据的数量
。
二、并行处理与GPU架构
1. 并行处理
1.1 串行处理与并行处理的区别
1.并行处理
:并行处理是一种计算方法,多个任务
或指令
在不同的处理单元(如多个CPU核心
或GPU流处理器
)上同时独立
运行。并行处理适用于数据密集型任务
和需要同时处理多个
任务的场景。
2.串行处理
:串行处理是一种计算方法,其中多个任务
或指令
按照顺序
依次执行,每个任务必须等待
前一个任务完成后才能开始执行。在串行处理中,每个任务都是在同一个处理单元上顺序执行,因此无法同时进行多个任务的处理。串行处理适用于简单
的计算任务和单一任务
的场景。
1.2 常见的并行处理
1.2.1 数据并行
将数据分成多个部分的数据片段
,分配给不同的处理单元进行并行处理。每个处理单元使用相同的模型
,但处理不同的数据片段。数据并行通常应用于分布式深度学习训练
,其中多个GPU或多台机器分别处理不同的训练样本。
1.2.2 任务并行
将计算任务分解成多个子任务
,并分配给不同的处理单元并行执行
。每个处理单元负责处理其中一个子任务,可以是相同或不同的模型。任务并行通常应用于大规模计算
,其中不同任务可以并行处理,从而加速整体计算过程。
1.2.3 模型并行
将大型模型拆分成多个部分
,分配给不同的处理单元并行计算
。每个处理单元负责计算模型的一个部分,然后将结果传递给其他处理单元进行进一步计算
。模型并行通常应用于处理大型深度学习模型,其中一个处理单元无法容纳整个模型
。
1.2.4 流水线并行
将计算流程分成多个阶段
,并分配给不同
的处理单元依次执行
。每个处理单元在完成自己的任务后,将结果传递
给下一个处理单元进行进一步计算
。流水线并行通常用于处理连续的计算任务,使得多个处理单元可以同时执行不同阶段的计算。
1.2.5 并发并行
将不同的任务同时执行
,而不必担心
它们的执行顺序。并发并行可以在单个处理单元上实现多任务并行处理,通常通过多线程或多进程来实现。
2. GPU 并行处理
2.1 特点
GPU并行处理: GPU(图形处理器)是专门设计用于处理图形渲染
和并行计算
任务的硬件。GPU具有大量的流处理器(CUDA核心
)和高带宽的内存
,能够同时执行大量计算任务,适合处理数据密集型任务和并行计算。主要的特点包括:
1.
高并行计算能力: GPU的设计目标之一就是实现高效的并行计算,它在单个芯片上集成了大量的流处理器,可以同时处理多个计算任务。
2.
高带宽内存: GPU拥有高带宽的全局内存,能够快速读取和写入大规模的数据和模型参数。
3.
适用于数据并行: GPU特别适合数据并行处理,即将数据分成多个部分,分配给不同的流处理器进行并行计算。
4.
深度学习加速: GPU广泛应用于深度学习训练和推理,通过并行计算加速神经网络的矩阵运算和卷积操作,提高训练和推理性能。
2.2 组成部分
GPU体系架构通常包含以下关键组件:
1.
流处理器(Streaming Processor): GPU中包含多个流处理器
,也称为CUDA核心
。每个流处理器负责执行计算任务,例如执行浮点运算
和向量操作
。
2.
多处理器(Multiprocessor): GPU中的流处理器分组成多个多处理器,每个多处理器负责管理多个流处理器,并调度并行任务。
3.
全局内存(Global Memory): GPU具有高带宽的全局内存,用于存储大规模的数据和模型参数。
4.
共享内存(Shared Memory): 共享内存是多个流处理器共享的高速缓存,用于加速多个流处理器之间的数据交换。
5.
纹理内存(Texture Memory): 纹理内存用于处理图像数据,适合对图像进行采样和滤波操作。
2.3 优势
1.
并行计算能力:GPU的并行结构使其能够同时执行多个计算任务,特别适用于数据密集型计算,如深度学习中的矩阵运算和卷积操作。
2.
高性能和吞吐量:GPU的高带宽内存和多处理器架构使其能够提供更高的计算性能和数据吞吐量,加速大规模数据处理和模型训练。
3.
加速深度学习:GPU广泛用于深度学习任务,如图像识别、目标检测和自然语言处理等,加速了模型训练和推理。
3. CPU 并行处理
CPU(中央处理器)是通用
计算设备,主要用于执行通用计算
任务和控制计算机
的操作。CPU具有多个核心和缓存,可以同时执行多个任务,但相比GPU,其并行计算能力较弱
。主要的特点包括:
1.
多核心处理: 现代CPU通常拥有多个核心,可以同时执行多个任务,实现任务级
的并行处理
。
2.
多线程处理: CPU支持多线程处理
,可以通过多线程编程实现并发计算。
3.
适用于任务并行: CPU适合任务并行处理,即将计算任务分解成多个子任务,分配给不同的核心进行并行执行。
4.
通用计算: CPU可以执行通用计算任务
,适用于各种应用场景,包括图形渲染、数据处理、编码解码等。
4. Memory Latency
Memory Latency(内存延迟)是指从发出内存请求到数据可供使用所需的时间
。在计算机系统中,内存延迟对于系统性能和计算速度至关重要。以下是Memory Latency的主要作用和影响:
系统性能: 内存延迟直接影响计算机系统的性能。如果内存延迟较高,CPU在等待数据时将会闲置,从而导致系统整体性能下降。
1.
指令执行: 内存延迟对于CPU的指令执行速度至关重要。在许多计算任务中,CPU需要频繁地从内存中读取数据和指令,如果内存延迟高,将会导致CPU等待数据的时间增加,从而降低指令执行速度。
2.
缓存命中率: 内存延迟直接影响缓存命中率。当CPU无法及时从内存中获取数据时,将会增加缓存未命中的可能性,从而导致CPU不得不从主内存中读取数据,进而增加内存延迟。
3.
内存带宽利用率: 内存延迟也影响内存带宽的利用率。当CPU等待数据时,内存带宽无法充分利用,导致内存带宽资源浪费。
4.
计算性能: 内存延迟对于计算任务的性能影响尤为重要。在数据密集型任务中,CPU需要频繁地访问内存,内存延迟会成为性能瓶颈
5. GPU特点
在GPU计算生态系统中,CUDA
、cuDNN
和TensorRT
是三个重要的组件,它们各自具有不同的特点和作用。
5.1 CUDA特点
1.
高并行计算能力: CUDA允许开发者使用大量的CUDA核心(流处理器)
进行并行计算,从而加速复杂的计算任务。
2.
丰富的并行库: CUDA提供了丰富的并行计算库,包括矩阵运算、图像处理、深度学习等,方便开发者进行高性能计算和深度学习任务。
3.
灵活性: 开发者可以直接在CUDA核心上编写代码,精细地控制计算过程,从而实现高度定制化的并行计算。
5.2 cuDNN特点
cuDNN是NVIDIA开发的深度学习计算库
,基于CUDA平台。它为深度学习任务提供了高性能的基本操作,如卷积
、池化
、归一化
等,以加速深度神经网络的训练和推理。cuDNN的特点包括:
1.
优化的深度学习操作: cuDNN实现了高度优化的深度学习操作,利用CUDA的并行计算能力加速神经网络的前向和后向计算。
2.
兼容性: cuDNN与各种深度学习框架(如TensorFlow、PyTorch、Caffe等)兼容,可与它们无缝集成,为模型训练和推理提供高性能支持。
5.3 TensorRT特点
TensorRT是NVIDIA开发的用于深度学习推理的高性能优化库
。它能够自动优化
深度学习模型,包括权重量化
、卷积融合
、内存优化
等技术,以提高模型在GPU上的推理性能。TensorRT的特点包括:
1.
推理性能优化: TensorRT通过深度学习模型的优化,充分利用GPU的并行计算能力,提高模型的推理速度。
2.
支持多种深度学习框架: TensorRT支持与多种深度学习框架兼容,可以与TensorFlow、PyTorch等框架无缝集成。
3.
支持多种精度: TensorRT支持FP32
、FP16
、INT8
等多种精度,可以根据需求选择合适的精度来平衡性能和精度要求。
三、CUDA编程
1. 简介
CUDA是由NVIDIA推出的并行计算架构
,可充分利用GPU的并行计算引擎,以更高效地解决复杂计算问题。
CUDA编程模型
是一种异构计算模型
,涉及CPU(主机)
和GPU(设备)
的协同工作
。
在CUDA中,主机代表了CPU及其内存,而设备代表了GPU及其内存。CUDA程序包含主机程序和设备程序,分别在CPU和GPU上执行。主机和设备之间可以进行数据通信
,以便在它们之间传输数据。
2. CUDA 编程入门
2.1 CUDA 概述
在目前主流使用的冯·诺依曼体系结构
的计算机中,GPU
属于一个外置设备
,因此即便在利用 GPU 进行并行计算的时候也无法脱离 CPU,需要与 CPU 协同工作。因此当我们在说 GPU 并行计算时,其实指的是基于 CPU+GPU
的异构计算架构
。在异构计算架构中,CPU 和 GPU 通过PCI-E
总线连接在一起进行协同工作,所以 CPU 所在位置称为 Host
,GPU 所在位置称为 Device
。
ps:PCI-E
(Peripheral Component Interconnect Express)是一种高速接口标准
,用于连接主板上的各种硬件设备
,如显卡、固态硬盘和网络卡等。它提供了高速的数据传输通道,使得 CPU(Host)与 GPU(Device)之间可以快速交换数据。
可以看出,GPU 中有着更多的运算核心
,非常适合数据并行的计算密集型任务
,比如大型的矩阵计算。
2.2 CUDA 编程模型基础
CUDA 模型是一个异构模型
,需要 CPU 和 GPU 协同工作,在 CUDA 中一般用Host
指代 CPU 及其内存
,Device
指代 GPU 及其内存
。CUDA 程序
中既包含在 Host 上运行的程序
,也包含在 Device 上运行的程序
,并且 Host 和 Device 之间可以进行通信
,如进行数据拷贝
等操作。一般的将需要串行
执行的程序放在 Host 上执行,需要并行执行
的程序放在 Device 上进行。
典型的CUDA程序执行流程
如下:
1.
分配主机内存并进行数据初始化。
2.
分配设备内存并将数据从主机复制到设备。
3.
调用CUDA核函数在设备上执行特定的计算。
4.
将设备上的计算结果复制回主机。
5.
释放设备和主机上分配的内存。
第 3 步中,CUDA Kernel 指的是在 Device 线程上并行执行的函数
,在程序中利用 __global__
符号声明,在调用时需要用<<<grid, block>>>
来指定 Kernel 执行的线程数量
,在 CUDA 中每一个线程都要执行 Kernel 函数,并且每个线程会被分配到一个唯一的 Thread ID
,这个 ID 值可以通过 Kernel 的内置变量 threadIdx
来获得。
__gloabl__ vectorAddition(float* device_a, float* device_b, float* device_c); // 定义 Kernel
int main()
{
/*
some codes
*/
vectorAddition<<<10, 32>>>(parameters); // 调用 Kernel 并指定 grid 为 10, block 为 32
/*
some codes
*/
}
由于GPU实际上是异构模型,所以需要区分host和device上的代码,在CUDA中是通过函数类型限定词
区别host和device上的函数,主要的三个函数类型限定词如下:
1.__global__
:在device上执行,从host中调用(一些特定的GPU也可以从device上调用),返回类型必须是void,不支持可变参数参数,不能成为类成员函数。注意用__global__定义的kernel是异步
的,这意味着host不会等待kernel执行完就执行下一步。
2.__device__
:在device上执行,单仅可以从device中调用,不可以和__global__同时用。
3.__host__
:在host上执行,仅可以从host上调用,一般省略不写,不可以和__global__同时用,但可和__device__,此时函数会在device和host都编译。
kernel在device上执行时实际上是启动很多线程,一个kernel所启动的所有线程称为一个网格(grid)
,同一个网格上的线程共享相同的全局内存空间
,grid是线程结构的第一层次,而网格又可以分为很多线程块(block)
,一个线程块里面包含很多线程,这是第二个层次
。
线程两层组织结构如下图所示,这是一个gird和block均为2-dim的线程组织。grid和block都是定义为dim3类型的变量,dim3可以看成是包含三个无符号整数(x,y,z)成员的结构体变量,在定义时,缺省值初始化为1。因此grid和block可以灵活地定义为1-dim,2-dim以及3-dim结构,对于图中结构(主要水平方向为x轴),定义的grid和block如下所示,kernel在调用时也必须通过执行配置<<<grid, block>>>
来指定kernel所使用的线程数及结构
。
dim3 grid(3, 2);
dim3 block(5, 3);
kernel_fun<<< grid, block >>>(prams...);
所以,一个线程需要两个内置的坐标变量(blockIdx,threadIdx)
来唯一标识
,它们都是dim3类型变量,其中blockIdx指明线程所在grid中的位置,而threaIdx指明线程所在block中的位置,如图中的Thread (1,1)满足:
threadIdx.x = 1
threadIdx.y = 1
blockIdx.x = 1
blockIdx.y = 1
四、TensorRT
TensorRT是由NVIDIA 提供的一个高性能深度学习推理(inference)引擎。用于提高深度学习模型在NVIDIA GPU上运行的的推理速度和效率。
1. 引言
要进行NVIDIA显卡的高性能推理,首推的还是自研发的推理引擎TensorRT
。使用TensorRT部署ONNX模型时,分为两个阶段:
1.
构建阶段。对ONNX模型转换和优化,输出优化后模型。
TensorRT会解析ONNX模型,并进行多项优化
:
(1)模型量化。分为:训练后量化、训练时量化,均支持。下图为将FP32量化为INT8。
(2)层融合
(3)自动选择最合适计算的kernel。
build阶段,支持C++
和Python
的API,也可以使用可执行程序trtexec
。
2.
运行阶段。加载优化后模型,执行推理。
注意事项:
1.如果导出ONNX时设置了动态batch,使用trtexec转换TensorRT时,就需要加上最小shape
、最优shape
和最大shape
的参数设置。这样得到的TensorRT模型就可以支持批处理
了。
2.FP16
、INT8
、INT4
等低精度
可以提升推理效率
,
2. 详解
2. 1 构建模型的方案
TensorRT构建模型的形式:如果有1个模型需要加速或者优化,肯定得告诉它模型长啥样,那其实就是告诉模型的权重是什么。
上述使用是不太方便或者工作量比较大的,那NVIDIA官方提供了3种便捷途径
来实现一个更加方便的封装。
如果使用pytorch,通常使用ONNX。
但是,ONNX在之前的老版本时,问题和坑很多,所以也有人不用上述方法,自己实现:
因为由pytorch到ONNX由pytorch官方维护,并且更新频率较快,由ONNX到TensorRT由TensorRT官方维护,所以采用下面的方案链接
2. 2 正确导出ONNX的注意力事项
如下图所示,写成size或shape返回的参数时,会造成pytorch对size的跟踪
,多出来了Shape
、gather
等结点其实是很没有必要的。
2.3 动态batch和动态宽高的处理方式
关于动态batch
,因为tensorRT对静态batch的处理,即使设的batch是32,如果输入的图片是1或者是32都是按32来
处理的,所以耗时
是固定
的,且缺少灵活性。
3. TensorRT安装配置
3.1 VS2019配置Onnxruntime
默认已经,安装好Anaconda、Pycharm和VS2019,配置好torch、torchvision、Cuda和Cudnn的安装,还有VS配置OpenCV的教程,可以看我之间的 博客。
接着,我们就来配置Onnxruntime
。
3.1.1 确认配置
首先确定cudnn的版本
nvcc --version
以我这里为例,我的是,进入链接中进行下载。
我这里的cudnn的版本为11.*
时,选择下载1.9.0
的gpu版本
。
3.1.2 配置vs
3.1.2.1 打开属性设置
打开vs,新建一个新项目,将上述下载的onnxruntime的zip文件解压到自己想放的磁盘位置。
左击这个项目名称;
随后右击,点击属性:
3.1.2.2 添加附加包含目录
首先在属性页上,先选择配置为Debug模式,以及x64的平台。
然后,在VC++目录中
中,将onnxruntime的安装包的include文件
路径添加到附加包含目录
中。
D:\program\onnxruntime-win-x64-gpu-1.9.0\include
3.1.2.3 添加附加库目录
接着,在VC++目录中
中,将onnxruntime的安装包的lib文件
路径添加到库目录
中。
D:\program\onnxruntime-win-x64-gpu-1.9.0\lib
3.1.2.4 附加依赖项
最后,在链接器
中的输入
中,将onnxruntime的安装包的lib文件路径下的onnxruntime.lib
添加到附加依赖项
中。
3.1.2.5 测试vs
先运行一个简单程序,生成Debug文件。
将onnxruntime.dll
放入到代码项目的Debug
或Release
下。
#include <iostream>
#include<onnxruntime_cxx_api.h>
using namespace std;
int main()
{
cout << "hello, onnx!" << endl;
system("pause");
return 0;
}
成功运行:
3.2 VS2019配置libtorch
3.2.1 下载libtorch
进入Pytorch官网进行libtorch下载,根据自己本地的CUDA版本
及Release版本
还是Debug版本
进行选择。
将下载好的zip文件
解压到自己想放的路径中。
3.2.2 配置系统环境变量
这里默认CUDA的环境变量
已经添加好。
D:\program\libtorch\bin
D:\program\libtorch\lib
3.2.3 配置vs环境变量
我们上述在配置onnxruntime的时候,最后一步是将解压的文件里的.dll文件
拷贝到代码所在的文件夹下,这些dll文件会复制很多份
,占据很大空间
,这里通过设置依赖于项目的环境变量
,少了大量拷贝,还不影响其他变量。
PATH=D:\program\libtorch\lib;%PATH%
3.2.4 配置头文件
将下面两个包含文件添加到vs中,以及CUDA的include
。
D:\program\libtorch\include
D:\program\libtorch\include\torch\csrc\api\include
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\include
3.2.5 配置库目录
将下面库目录添加到vs中,以及CUDA的lib
。
D:\program\libtorch\lib
C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\lib
3.2.6 配置链接器的依赖库
这里手动复制每个.lib文件名很繁琐,写了个自动化python脚本一键复制。
import os
# 指定要读取的文件夹路径
folder_path = r'D:\program\libtorch\lib' # 替换为你的文件夹路径
output_file = 'output.txt' # 输出的txt文件名
# 获取所有.lib文件名
lib_files = [f for f in os.listdir(folder_path) if f.endswith('.lib')]
# 将文件名逐行写入output.txt
with open(output_file, 'w') as f:
for file_name in lib_files:
f.write(file_name + '\n')
print(f"已将{
len(lib_files)}个文件名写入{
output_file}")
asmjit.lib
c10.lib
c10_cuda.lib
caffe2_nvrtc.lib
cpuinfo.lib
dnnl.lib
fbgemm.lib
fbjni.lib
fmtd.lib
kineto.lib
libprotobuf-lited.lib
libprotobufd.lib
libprotocd.lib
pthreadpool.lib
pytorch_jni.lib
sleef.lib
torch.lib
torch_cpu.lib
torch_cuda.lib
XNNPACK.lib
3.2.7 更改vs一些设置
点击c/c++,点击常规,点击附加包含目录,添加$(IncludePath)
,将SDL检查改为否
。
点击语言,点击c++语言标准,改为c++20
, 根据自己报错进行更改,我的是c++20,默认为c++14。
将符合模式
改为否
到这里你的libtorch的cpu版本
就已经配置完成
了,如果需要配置gpu版本
就跟以下这个链接器的命令行
有关系,在命令行的其他选型中输入:
/INCLUDE:"?ignore_this_library_placeholder@@YAHXZ"
不同的CUDA版本需要不同的命令行,根据自己的版本进行配置。
3.2.8 vs测试
#include <iostream>
#include <vector>
#include<torch/torch.h>
#include<torch/script.h>
using namespace std;
int main() {
auto a = torch::rand({
5, 3 });
if (torch::cuda::is_available()) {
cout << "cuda可用" << endl;
}
else
{
cout << "不可用" << endl;
}
cout << a << endl;
}
配置成功!
3.3 安装tensorRT
3.3.1 下载tensorRT
NVIDIA官网下载TensorRT,可以选择不同的版本。
这里,我以下载版本8.5为例:
我本地是CUDA11.8,所以下载第一个。
3.3.1 配置tensorRT
将下载好的zip文件
解压到自己想放的路径中,并将.lib库文件
添加到系统环境变量
中。
D:\program\TensorRT-8.6.1.6\lib
然后,将lib
下的文件拷贝到 cuda 安装目录下的 bin 文件夹
下,比如我这里的 C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.8\bin
。
3.3.2 配置头文件
将下面的包含文件添加到vs中。
D:\program\TensorRT-8.6.1.6\include
3.2.3 配置库目录
将下面库目录添加到vs中。
D:\program\TensorRT-8.6.1.6\lib
3.2.4 配置链接器的依赖库
nvinfer.lib
3.2.5 vs测试
一般通过中间表示的模型(如 ONNX)转换成TensorRT,是最常见的。
这里我们以为,通过 TensorRT 的 API 逐层搭建网络并序列化模型为例做个简单的测试:
TensorRT的C++实现,主要分为下述流程:
1.
首先初始化一个日志记录器,
ps:nvinfer1:: createInferBuilder 对应 Python 中的 tensorrt.Builder,需要传入 ILogger 类的实例,但是 ILogger 是一个抽象类,需要用户继承该类并实现内部的虚函数。不过此处我们直接使用了 TensorRT 包解压后的 samples 文件夹 …/samples/common/logger.h 文件里的实现 Logger 子类。
2.
通过日志记录器构建build(IBuilder类
)
!!!!除了2中build通过(IBuilder类),其余的比如构建网络、构建配置、构建引擎全部都是build->... 网络层就是network->...
3.
通过build构建config(IBuilderConfig类
)、network(INetworkDefinitio类
)、优化配置文件profile(IOptimizationProfile类
)、引擎engine(ICudaEngine类
)…
4.
通过network->add…来定义网络结构,卷积、池化(IPoolingLayer类
)…
5.
通过profile->setDimensions优化配置文件
6.
通过config->setMaxWorkspaceSize最大工作空间大小
7.
engine->serialize()序列化
#include <fstream>
#include <iostream>
#include<NvInfer.h>
#include <../samples/common/logger.h>
using namespace nvinfer1;
using namespace sample;
const char* IN_NAME = "input";
const char* OUT_NAME = "output";
static const int IN_H = 224;
static const int IN_W = 224;
static const int BATCH_SIZE = 1;
static const int EXPLICIT_BATCH = 1 << (int)(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
int main(int argc, char** argv)
{
// 1.创建 builder
sample::Logger m_logger; // 初始化一个日志记录器
// 使用日志记录器创建一个推理构建器 builder,并创建一个构建配置 config。
nvinfer1::IBuilder *builder = nvinfer1::createInferBuilder(m_logger);
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
// 2.创建网络
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(EXPLICIT_BATCH);
// 2.1 添加输入张量
nvinfer1::ITensor* input_tensor = network->addInput(IN_NAME, nvinfer1::DataType::kFLOAT, nvinfer1::Dims4{
BATCH_SIZE, 3, IN_H, IN_W });
// 2.2 添加最大池化层
// 操作在输入张量上进行,池化窗口大小为 2x2。
nvinfer1::IPoolingLayer* pool = network->addPooling(*input_tensor, nvinfer1::PoolingType::kMAX, nvinfer1::DimsHW{
2, 2 });
pool->setStride(nvinfer1::DimsHW{
2, 2 }); // 设置pool的stride
// 将池化层的输出张量命名为 OUT_NAME,并将其标记为网络的输出。
pool->getOutput(0)->setName(OUT_NAME);
network->markOutput(*pool->getOutput(0));
// 3. 构建引擎
// 创建优化配置文件:用于配置网络的优化参数。
// 优化网络,执行层次融合、内存优化等
nvinfer1::IOptimizationProfile* profile = builder->createOptimizationProfile();
// 为了确保模型在不同的输入条件下都能正常工作
// 为优化配置文件设置最小、最优和最大输入维度,确保引擎能够处理这些维度的输入。
profile->setDimensions(IN_NAME, nvinfer1::OptProfileSelector::kMIN, nvinfer1::Dims4(BATCH_SIZE, 3, IN_H, IN_W));
profile->setDimensions(IN_NAME, nvinfer1::OptProfileSelector::kOPT, nvinfer1::Dims4(BATCH_SIZE, 3, IN_H, IN_W));
profile->setDimensions(IN_NAME, nvinfer1::OptProfileSelector::kMAX, nvinfer1::Dims4(BATCH_SIZE, 3, IN_H, IN_W));
// 设置最大工作空间大小:为 TensorRT 设置工作空间大小(1MB),用于优化过程中的内存分配。
// 1 << 20:这是一个位移操作,表示将数字 1 向左移动 20 位,相当于 2~20,1,048,576 字节,等于 1 MB。
config->setMaxWorkspaceSize(1 << 20);
nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
// 4. 序列化引擎
// 4.1 创建一个指向主机内存的指针,初始化为 nullptr, 用于指向序列化后的模型数据
nvinfer1::IHostMemory* modelStream{
nullptr };
// 4.2 确保 engine 不为空,防止后续操作出错
assert(engine != nullptr);
// 4.3 调用引擎的序列化函数,将引擎数据存储到 modelStream 中
modelStream = engine->serialize();
// 4.4 以二进制模式打开文件 "model.engine" 以保存序列化的模型。如果打开失败,输出错误信息并返回 -1。
std::ofstream p("model.engine", std::ios::binary);
if (!p)
{
std::cout << "could not open output file to save model" << std::endl;
return -1;
}
p.write(reinterpret_cast<const char*>(modelStream->data()), modelStream->size());
std::cout << "generating file done!" << std::endl;
// 5. 手动释放资源
modelStream->destroy();
network->destroy();
engine->destroy();
builder->destroy();
config->destroy();
return 0;
}
4. TensorRT使用说明
TensorRT 部署流程主要有以下五步:
1.训练模型
2.导出模型为 ONNX 格式
3.选择精度
4.转化成 TensorRT 模型
5.部署模型
TensorRT 的 API 是基于类
的。
在TensorRT中,对象的生命周期可以概括为以下几个主要阶段:
1.
创建对象:在TensorRT中,可以创建多种不同类型的对象,例如IBuilder、INetworkDefinition、ICudaEngine等。这些对象用于构建、定义和优化神经网络模型。
2.
构建网络:在创建INetworkDefinition对象后,可以使用TensorRT提供的API来构建神经网络。这包括添加输入和输出层,定义中间层和操作,设置张量的维度和数据类型等。
3.
优化网络:在构建网络后,可以通过调用IBuilder对象的方法来优化网络。这些方法包括执行层次融合、内存优化、精度校准等技术,以减少推理时间和内存占用。
4.
构建引擎:在优化网络后,可以使用IBuilder对象的buildCudaEngine方法来构建ICudaEngine对象。这是TensorRT运行时使用的引擎对象,它包含了优化后的网络和执行推理所需的GPU代码。
5.
序列化引擎:构建引擎后,可以将ICudaEngine对象序列化为一个文件,以便以后加载和重用。这可以通过调用ICudaEngine对象的serialize方法来完成。
6.
加载引擎:当需要执行推理时,可以通过反序列化引擎文件来加载ICudaEngine对象。这将创建一个可以执行推理的运行时环境。
7.
执行推理:一旦引擎加载完成,可以使用ICudaEngine对象的方法将输入数据提供给模型,并获取输出结果。推理过程在TensorRT的运行时环境中进行,利用GPU的并行计算能力来加速推理速度。
8.
释放资源:在完成推理任务或不再需要TensorRT对象时,应该显式地释放和销毁TensorRT对象。这可以通过调用相应对象的析构函数或销毁方法来完成。
4.1 Layer Fusion(层融合)
Layer Fusion是一种优化技术,用于将多个神经网络层融合成一个更大的层,提高推理性能。Layer Fusion可以在TensorRT的优化过程中自动应用
,以减少内存访问
和计算开销
。通过Layer Fusion,TensorRT可以优化
和简化
模型的计算图
,可以在推理过程中显著提升性能。
在神经网络中,不同层之间的计算和数据传输可能涉及多次内存访问和操作。通过将多个层
融合为一个层
,可以减少这些内存操作,从而减少内存带宽和延迟,并提高计算效率。
五、C++部署Yolo模型实例
深度学习模型,以Yolo为例,通常在以Python和PyTorch框架训练模型后,整个推理过程分为:预处理、推理和后处理部分。而要进行模型的部署,需要把后处理的部分从模型里面摘出来。
OpenCV中的深度学习模块(DNN)只提供了推理功能,不涉及模型的训练,支持多种深度学习框架:Torch、TensorFlow、Caffe、Darknet。