训练
这个部分不属于TensorRt的内容范畴。通常训练网络模型使用高性能的服务器,使用的框架常见的有Tensorflow、pytorch、caffe、mxnet等。之后被TendorRt加载或转成换能够加载的模型格式。
编译
加载第三方模型(目前仅支持onnx、caffe、uff)进行模型编译并从多个方面优化,生成序列化的engine模型,以plan形式保存。通常模型加载转换通常有三种方式:
(1)使用 TF-TRT(部署中介绍),提供模型转换和高级运行时 API,并且能够回退到 TensorRT 不支持特定运算符的 TensorFlow 实现。
(2)自动模型转换和部署的一个更高效的选项是使用 ONNX 进行转换。ONNX是一个与框架无关的选项,可与 TensorFlow、PyTorch 等中的模型一起使用。TensorRT 支持使用 TensorRT API 从 ONNX 文件自动转换,或者使用工具 trtexec
。ONNX 转换的结果是单一的engine,比使用 TF-TRT 开销更少。
(3)使用TensorRT 网络定义 API手动构建 TensorRT engine,直接使用TensorRT 操作构建与目标模型相同的网络,之后从框架导出模型的权重并将它们加载到这个TensorRT 网络中。这种方式性能较高,可定制化,但是操作可能比较复杂,适合小型网络。
部署
加载优化后的模型到TensorRt runtime部署进行inference。
(1)TF-TRT 是 TensorRT 的高级 Python 接口,可直接与 TensorFlow 模型一起使用。TF-TRT 转换会生成一个 TensorFlow 图,其中插入了 TensorRT 操作。可以像运行任何其他使用 Python 的 TensorFlow 模型一样运行 TF-TRT 模型。
(2)TensorRT运行时 API允许最低的开销和最细粒度的控制,但 TensorRT 本身不支持的运算符必须作为插件实现(此处提供了预编写的插件库)。使用运行时 API 进行部署的最常见路径是使用框架中的 ONNX 导出,本指南的下一节将对此进行介绍。
(3)NVIDIA Triton 推理服务器是一款开源推理服务软件,使团队能够从任何框架(TensorFlow、TensorRT、PyTorch、ONNX Runtime 或自定义框架)、本地存储或谷歌云平台或任何 GPU 上的 AWS S3 部署训练有素的 AI 模型,或基于 CPU 的基础架构(云、数据中心或边缘)。它是一个灵活的项目,具有几个独特的功能 - 例如异构模型的并发模型执行和同一模型的多个副本(多个模型副本可以进一步减少延迟)以及负载平衡和模型分析。如果您必须通过 HTTP 为您的模型提供服务,这是一个不错的选择 - 例如在云推理解决方案中。您可以找到 NVIDIA Triton 推理服务器主页这里和这里的文档。
ONNX 转换通常是将 ONNX 模型自动转换为 TensorRT engine的最高效方式。本节将在部署预训练 ONNX 模型的背景下介绍 TensorRT 转换的五个基本步骤。
使用 ONNX 格式从 ONNX 模型库转换预训练的 ResNet-50 模型;一种与框架无关的模型格式,可以从大多数主要框架中导出,包括 TensorFlow 和 PyTorch。
准备一个onnx模型,可以来自网络框架直接导出(见后面章节 3.1 ),也可以是onnx model zoo中直接下载:
wget https://s3.amazonaws.com/download.onnx/models/opset_8/resnet50.tar.gz
tar xzf resnet50.tar.gz
解压得到预训练的模型resnet50/model.onnx。
batch size会对 TensorRT 在我们的模型上执行的优化产生很大影响。在推理优先考虑延迟时,选择小批量batch size;有线考虑吞吐量时,会考虑更大的batch size。较大的batch size需要更长的时间来处理,但由于并行操作会降低每个样本的平均处理时间。
TensorRt能够动态的处理只有运行时才确定的batch size,即固定batch size大小能够被优化。当前示例中指定为64,那么需要在tensorflow或pytorch中指定导出onnx的batch size为64。
BATCH_SIZE=64
这里下载的model.onnx预训练模型的batch size已经是被设置为64。
推理通常需要比训练更少的数值精度,较低的精度可以为您提供更快的计算和更低的内存消耗,而不会牺牲有意义的准确性。支持TF32, FP32, FP16, 和 INT8。
FP32是大多数网络框架训练的默认精度,因此在推理前进行指定
import numpy as np
PRECISION = np.float32
有多种工具可将模型从 ONNX 转换为 TensorRT engine,常见是使用附带工具trtexec
,它还能对engine进行其他分析。
通过以下命令,可以将onnx格式转换为engine格式
trtexec --onnx=resnet50/model.onnx --saveEngine=resnet_engine.trt
成功创建engine后,可以选择 python 或 c++ 运行时api部署运行。这里使用python简化的包装器ONNXClassifierWrapper
并用随机数据输入进行测试。
from onnx_helper import ONNXClassifierWrapper
import numpy as np
PRECISION = np.float32
N_CLASSES = 1000 # Our ResNet-50 is trained on a 1000 class ImageNet task
trt_model = ONNXClassifierWrapper("resnet_engine.trt", [BATCH_SIZE, N_CLASSES], target_dtype = PRECISION)
BATCH_SIZE=64
dummy_input_batch = np.zeros((BATCH_SIZE, 224, 224, 3))
predictions = trt_model.predict(dummy_input_batch)
注意,wrapper构造时并不会加载engine,只有第一次进行推理时才会进行。因此,第一次处理会有模型初始化、内部构造、硬件资源分配等耗时操作,称为warmup。
在 1、使用ONNX部署的示例 基础上, 增加其他框架转换为)ONNX模型再部署的内容。
ONNX 交换格式提供了一种从许多框架(包括 PyTorch、TensorFlow 和 TensorFlow 2)导出模型的方法,以便与 TensorRT 运行时一起使用。TensorRt 使用 ONNX 导入模型需要其模型中的运算符是被支持的,否则需要提供 不支持的任何运算符的插件实现。(可以在此处连链接找到 TensorRT 的插件库)。
使用 ONNX 项目中的 keras2onnx 和 tf2onnx 工具,能够轻松地将 TensorFlow 模型转换导出为 ONNX 模型。
# 从keras.applications中导入 ResNet-50 模型 . 这将加载具有预训练权重的 ResNet-50 副本。
from tensorflow.keras.applications import ResNet50
model = ResNet50(weights='imagenet')
# 将 ResNet-50 模型转换为 ONNX 格式。
import tf2onnx
model.save('my_model')
!python -m tf2onnx.convert --saved-model my_model --output temp.onnx
onnx_model = onnx.load_model('temp.onnx')
# 在 ONNX 文件中设置明确的批量大小。
# 笔记:默认情况下,TensorFlow 不会设置明确的批量大小。
import onnx
BATCH_SIZE = 64
inputs = onnx_model.graph.input
for input in inputs:
dim1 = input.type.tensor_type.shape.dim[0]
dim1.dim_value = BATCH_SIZE
# 保存 ONNX 文件。
model_name = "resnet50_onnx_model.onnx"
onnx.save_model(onnx_model, model_name)
# 从torchvision中导入 ResNet-50 模型. 这将加载具有预训练权重的 ResNet-50 副本。
import torchvision.models as models
resnext50_32x4d = models.resnext50_32x4d(pretrained=True)
#从 PyTorch 保存 ONNX 文件。
#注意:我们需要一批数据来保存来自 PyTorch 的 ONNX 文件。我们将使用虚拟批次。
import torch
BATCH_SIZE = 64
dummy_input=torch.randn(BATCH_SIZE, 3, 224, 224)
#保存 ONNX 文件。
import torch.onnx
torch.onnx.export(resnext50_32x4d, dummy_input, "resnet50_onnx_model.onnx", verbose=False)
存在两种转换方式, trtexec
和 TensorRT API
, 这里以后者为例,将 resnet50_onnx_model.onnx 转换导出为 resnet_engine.trt :
trtexec --onnx=resnet50_onnx_model.onnx --saveEngine=resnet_engine.trt
TensorRT 有许多可用的runtime运行时。当性能很重要时,TensorRT API 是运行 ONNX 模型的好方法。在下一节中,将使用 C++ 和 Python 中的 TensorRT 运行时 API 部署更复杂的 ONNX 模型。
对于模型转换和部署而言,性能最高、可自定义选项的方式是使用TensorRT的运行时api。
TensorRT 包括一个带有 C++ 和 Python 绑定的独立运行时,与使用 TF-TRT 集成并在 TensorFlow 中运行相比,它们通常具有更高的性能和更可定制性。C++ API 的开销较低,但 Python API 可以很好地与 Python 数据加载器和库(如 NumPy 和 SciPy)配合使用,并且更易于用于原型设计、调试和测试。
下面使用具有 ResNet-101 主干的完全卷积模型,对任意输入分辨率的图像进行语义分割为例,说明c++和python两种情况下的runtime api使用。使用主要包含以下三个步骤:
1、从TensorRT 开源软件存储库下载此快速入门教程的源代码。
$ git clone https://github.com/NVIDIA/TensorRT.git
$ cd TensorRT/quickstart
2、将torch.hub中FCN-Resnet-101预训练模型转换为ONNX
使用教程自带的导出脚本生成一个ONNX模型保存到fcn-resnet101.onnx,还会生成一个大小为 1282x1026 的.ppm测试图像。
$ docker run --rm -it --gpus all -p 8888:8888 -v `pwd`:/workspace -w /workspace/SemanticSegmentation nvcr.io/nvidia/pytorch:20.12-py3 bash
$ python export.py
注: FCN-ResNet-101只有一个输入维度[batch,3,height,weight],一个输出维度 [batch,21,height,weight],包含对应于 21 个类别标签的预测的非标准化概率。将模型导出到 ONNX 时,附加一个 argmax在输出层产生最高概率的每像素类标签。3、使用 trtexec 工具从ONNX模型构建为TensorRT engine
trtexec 能够将onnx模型构建为tensorRt engine,并使用 运行时api部署。通过TensorRt ONNX parser加载ONNX模型构建一个TensorRT network graph网络图,之后利用TensorRT Builder API生成优化后的engine。构建过程可能耗时,通常是离线进行的。
trtexec --onnx=fcn-resnet101.onnx --fp16 --workspace=64 --minShapes=input:1x3x256x256 --optShapes=input:1x3x1026x1282 --maxShapes=input:1x3x1440x2560 --buildOnly --saveEngine=fcn-resnet101.engine
成功执行将生成一个engine文件、控制台输出一些有关成功的信息。在 TensorRT Developer Guide 中介绍了trtexec工具构建engine的各种参数。
4、可选的,可以使用trtexec工具对随机输入进行engine验证
trtexec --shapes=input:1x3x1026x1282 --loadEngine=fcn-resnet101.engine
–shapes参数用于设定推力时要求输入的动态尺寸,当成功还行时,会有类似如下输出
&&&& PASSED TensorRT.trtexec # trtexec --shapes=input:1x3x1026x1282 --loadEngine=fcn-resnet101.engine
如果提示输入名称不正确,例如minst的测试
[11/27/2022-13:42:54] [I] [TRT] [MemUsageChange] TensorRT-managed allocation in IExecutionContext creation: CPU +0, GPU +0, now: CPU 0, GPU 0 (MiB)
[11/27/2022-13:42:54] [E] Cannot find input tensor with name "input" in the engine bindings! Please make sure the input tensor names are correct.
[11/27/2022-13:42:54] [E] Invalid tensor names found in --shapes flag.
[11/27/2022-13:42:54] [E] Inference set up failed
&&&& FAILED TensorRT.trtexec [TensorRT v8403] # trtexec.exe --shapes=input:64x1x28x28 --loadEngine=minst.trt
说明网络的输入名称不正确,可以通过Netron查看对应onnx模型的输入和输出等名称和尺寸,
将输入参数修改为 --shapes=Input3:64x1x28x28
即可正常执行。
使用测试容器编译运行c++的语义分割示例,
$ make
$ ./bin/segmentation_tutorial
后面步骤秒数如何使用 反序列化Plan (加载优化后的engine文件对象)进行前线推理inference。
1、从文件中发序列化读取TensorRT engine
std::ifstream engineFile(engine, std::ios::binary)
engineFile.seekg(0, engineFile.end);
long int fsize = engineFile.tellg();
engineFile.seekg(0, engineFile.beg);
std::vector<char> engineData(fsize);
engineFile.read(engineData.data(), fsize);
util::UniquePtr<nvinfer1::IRuntime> runtime{nvinfer1::createInferRuntime(sample::gLogger.getTRTLogger())};
util::UniquePtr<nvinfer1::ICudaEngine> mEngine(runtime->deserializeCudaEngine(engineData.data(), fsize, nullptr))
注意:TensorRT中对象销毁使用成员函数destroy()
,在示例程序中使用自定义删除器deleter的智能指针管理生命周期。
struct InferDeleter
{
template <typename T>
void operator()(T* obj) const {
if (obj) obj->destroy();
}
};
template <typename T>
using UniquePtr = std::unique_ptr<T, util::InferDeleter>;
2.TensorRT 执行上下文 context封装了执行状态,例如用于在推理期间保存中间激活张量的持久设备内存。
由于分割模型是在启用动态形状的情况下构建的,因此必须为推理执行指定输入的形状。可以查询network输出形状以确定输出缓冲区的相应维度。
auto input_idx = mEngine->getBindingIndex("input");
assert(mEngine->getBindingDataType(input_idx) == nvinfer1::DataType::kFLOAT);
auto input_dims = nvinfer1::Dims4{1, 3 /* channels */, height, width};
context->setBindingDimensions(input_idx, input_dims);
auto input_size = util::getMemorySize(input_dims, sizeof(float));
auto output_idx = mEngine->getBindingIndex("output");
assert(mEngine->getBindingDataType(output_idx) == nvinfer1::DataType::kINT32);
auto output_dims = context->getBindingDimensions(output_idx);
auto output_size = util::getMemorySize(output_dims, sizeof(int32_t));
注意:network中 I/O 的绑定索引可以按名称查询。
3. 在准备推理时,为所有输入和输出分配 CUDA 设备内存,处理图像数据并将其复制到输入内存中,并生成引擎绑定列表
对于语义分割,输入图像数据通过使用均值归一化[0.485, 0.456, 0.406]和标准偏差 [0.229, 0.224, 0.225] 拟合到[0, 1]。 请参阅输入预处理要求火炬视觉模型在这里。当前模型输入的预处理要求可以参看torchvision models。 此操作由实用程序类RGBImageReader抽象实现。
void* input_mem{nullptr};
cudaMalloc(&input_mem, input_size);
void* output_mem{nullptr};
cudaMalloc(&output_mem, output_size);
const std::vector<float> mean{0.485f, 0.456f, 0.406f};
const std::vector<float> stddev{0.229f, 0.224f, 0.225f};
auto input_image{util::RGBImageReader(input_filename, input_dims, mean, stddev)};
input_image.read();
auto input_buffer = input_image.process();
cudaMemcpyAsync(input_mem, input_buffer.get(), input_size, cudaMemcpyHostToDevice, stream);
4. 使用上下文context的启动推理执行 executeV2
或者enqueueV2
方法
执行完成后,我们将结果复制回主机缓冲区并释放所有设备内存分配。
void* bindings[] = {input_mem, output_mem};
bool status = context->enqueueV2(bindings, stream, nullptr);
auto output_buffer = std::unique_ptr<int>{new int[output_size]};
cudaMemcpyAsync(output_buffer.get(), output_mem, output_size, cudaMemcpyDeviceToHost, stream);
cudaStreamSynchronize(stream);
cudaFree(input_mem);
cudaFree(output_mem);
5. 使用伪彩色可视化并保存到.ppm文件
此操作由实用程序类ArgmaxImageWriter抽象实现。
const int num_classes{21};
const std::vector<int> palette{
(0x1 << 25) - 1, (0x1 << 15) - 1, (0x1 << 21) - 1};
auto output_image{util::ArgmaxImageWriter(output_filename, output_dims, palette, num_classes)};
output_image.process(output_buffer.get());
output_image.write();
安装python依赖包
$ pip install pycuda
启动 Jupyter 并使用提供的令牌通过浏览器登录 http://
$ jupyter notebook --port=8888 --no-browser --ip=0.0.0.0 --allow-root
打开tutorial-runtime.ipynb笔记本并按照其步骤操作。
Python环境下的运行时 runtime api 是 c++ api的隐射,参见Running an Engine in C++。