02-TensorRT 开发者指南
使用 TensoRT 带来的优势?
- 吞吐量: 单位时间内的推理数量更多
- 功率: 单位功率的吞吐率更高
- 推理时间: 毫秒级的推理时间
- 准确性: 训练过的神经网络可以提供正确的结果
替代 TensorRT 的方法有哪些?
- 使用训练框架本身进行推理:需占用更多的 GPU,训练框架倾向于高效的训练,而不是推断
- 专门设计用于执行自定义应用程序网络使用的低级库和数学运算:工作量大,兼容性不强
TensorRT 是如何工作的?
- 构建阶段: 根据已经训练好的网络,执行特定的网络、特定平台的优化,生成推理引擎的阶段
- 非跨平台: 生成的推理引擎不能跨平台或 TensorRT 版本移植。计划特定于构建它们的确切 GPU 模型(除了平台和 TensorRT 版本),并且必须重新针对特定 GPU,以防您想在不同的 GPU 上运行它们
- 优化概述: 通过消除死计算、折叠来优化网络图常量,以及重新排序和组合操作以更有效地运行图形处理器
- 选择精度: 自动将 32 位浮点计算减少到 16 位并支持浮点值的量化 ,甚至是 8 位整数
TensorRT 的核心功能?
- 网络定义: 网络定义接口为应用程序提供定义网络的方法。可以指定输入和输出张量,并可以添加和配置层
- 优化配置文件: 优化配置文件指定动态维度上的约束
- 构建器配置: 构建器配置接口指定创建引擎的详细信息。它允许应用程序指定优化配置文件,最大工作空间大小,最低可接受的精度水平,定时迭代计数的自动调整,和一个接口量化网络运行在 8 位精度
- 构建器: 接口允许从网络定义和构建器配置创建优化的引擎
- 引擎: Engine 接口允许应用程序执行推理。它支持同步和异步执行、分析和枚举,以及查询引擎输入和输出的绑定。一个单引擎可以有多个执行上下文,允许使用一组训练过的参数同时执行多个推理
- ONNX 解析器: 此解析器可用于解析 ONNX
TensorRT 的 版本号 (MAJOR.MINOR.PATCH) 说明?
- MAJOR:在进行不兼容的 API 或 ABI 时,主要版本会发生更改
- MINOR:以向后兼容的方式添加功能时的 MINOR 版本
- PATCH:补丁版本时进行向后兼容的错误修复
TensorRT 如何使用解析器 (IBuilder) 创建网络定义?
- 由于 TensorRT 7.0,ONNX 解析器只支持全维模式,这意味着必须使用 explicitBatch 拆分批处理
- 创建构建器和网络定义
1
2
3IBuilder* builder = createInferBuilder(gLogger);
const auto explicitBatch = 1U < < static_cast(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
INetworkDefinition* network = builder->createNetworkV2(explicitBatch); - 创建 ONNX 解析器并解析模型
1
2
3
4
5
6
7nvonnxparser::IParser* parser =
nvonnxparser::createParser(*network, gLogger);
parser->parseFromFile(onnx_filename, ILogger::Severity::kWARNING);
for (int i = 0; i < parser.getNbErrors(); ++i)
{
std::cout < < parser->getError(i)->desc() < < std::endl;
}
TensorRT 如何使用构建器创建引擎?
- 生成器 IBuilderConfig 有许多属性,一个特别重要的属性是最大工作空间大小
- 使用 builder 对象构建引擎
1
2
3
4
5IBuilderConfig* config = builder->createBuilderConfig();
// // 构建器(builder)的工作空间,越大表示占用GPU越多,构建速度越快
config->setMaxWorkspaceSize(16_MiB);
// 当引擎构造完成时,TensorRT会复制重量
ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config); - 销毁解析器、网络定义、配置、构建器
1
2
3
4parser->destroy();
network->destroy();
config->destroy();
builder->destroy();
如何加快 TensorRT 构建器的构建时间?
- onnx->trt 这一过程是构建器(builder)为每一层计算获选内核的过程,试想 onnx 是一个层级的算子序列,每层算子对应 1 个或多个 TensorRT 算子,这些算子的速度与硬件参数相关,为了构建最快的引擎,需要遍历 列表组合这些算子,统计其运行时间,最终获得最快的算子组合,所以这个过程尤其耗时
- 从 TensorRT 8.0 开始,运行在拥有相同 CUDA 设备属性和 CUDA/TensorRT 版本的设备上的构建器实例可以序列化和加载时间缓存
- 程序
- 创建一个新的空计时缓存
1
2IBuilderConfig* config = builder->createBuilderConfig();
ITimingCache* cache = config->createTimingCache(cacheFile.data(), cacheFile.size()); - 将全局计时缓存附加到构建器
1
2// 在拥有不同 CUDA 设备属性的设备之间共享缓存可能会导致功能/性能问题。建议是只有当设备型号相同但 UUID 不同时才这样做。但是,加载的缓存必须由与当前生成器实例相同的 TensorRT 版本生成
config->setTimingCache(*cache, false); - 序列化全局计时缓存
1
IHostMemory* serializedCache = cache->serialize();
- 设置生成器标志来关闭全局 / 本地缓存
1
config->setFlag(BuilderFlag::kDISABLE_TIMING_CACHE);
- 创建一个新的空计时缓存
TensorRT 如何序列化模型?
- 序列化引擎不能跨平台移植 TensorRT 版本,如,tensorrt8 序列化的引擎不能在 tensorrt7 上使用
- 不仅 tensorrt 的版本问题,高版本的 TensorRT 依赖于高版本的 CUDA 版本,而高版本的 CUDA 版本依赖于高版本的驱动,如果想要使用新版本的 TensorRT,更换环境是不可避免的;
- 程序
- 运行离线状态的构建器,然后序列化模型
1
2
3
4IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy(); - 创建运行时用于反序列化
1
2IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);
- 运行离线状态的构建器,然后序列化模型
如何反序列化 TensorRT 引擎并执行推理?
- 创建执行上下文来存储中间激活值
1
2// 一个引擎可以有多个执行上下文,允许为多个重叠推理任务使用一组权重。例如,您可以在并行 CUDA 流中使用一个引擎和一个上下文来处理图像。每个上下文都是在与引擎相同的 GPU 上创建的
IExecutionContext *context = engine->createExecutionContext(); - 使用输入和输出 blob 名称来获得相应的输入和输出索引
1
2int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME); - 设置一个指向 GPU 上输入和输出缓冲区的缓冲区数组
1
2
3void* buffers[2];
buffers[inputIndex] = inputBuffer;
buffers[outputIndex] = outputBuffer; - 执行推理
1
2
3
4
5// 异步推理
context->enqueueV2(buffers, stream, nullptr);
// 同步推理
bool status = context->executeV2(buffers.getDeviceBindings().data())
// 从同一个 IExecutionContext 对象调用 enqueue 或 enqueueV2,并同时调用不同 CUDA 数据流,将导致未定义行为/值。为了在多个 CUDA 流中并发执行推理,每个 CUDA 流使用一个 IExecutionContext
在 TensorRT 中如何对显存进行管理?
- 创建运行上下文时设置―
- 默认情况下,在创建 IExecutionContext 时,将分配持久设备内存来保存激活数据
- 要避免这种分配,请调用 createexecutioncontextwithoutdevicemory。然后,应用程序负责调用 IExecutionContext: : setdevicemory () ,以提供运行网络所需的内存。内存块的大小由 ICudaEngine: : getdevicemorysize () 返回
- 通过实现 IGpuAllocator 接口―
- 应用程序可以提供定制的分配器,以便在构建和运行时使用。如果您的应用程序希望控制所有的 GPU 内存并将子分配给 TensorRT,而不是直接从 CUDA 分配 TensorRT
- 接口实现后,在 IBuilder 或 IRuntime 接口上调用 setGpuAllocator (& allocator)。然后通过这个接口分配和释放所有设备内存
在 TensorRT 中如何定制化权重?
- 可以为发动机重新装配新的重量,而不需要重新装配
- 程序
- 在建造之前请求一个可定制权重的引擎
1
2
3...
config->setFlag(BuilderFlag::kREFIT)
builder->buildEngineWithConfig(network, config); - 创建一个 reftter 对象
1
2ICudaEngine* engine = ...;
IRefitter* refitter = createInferRefitter(*engine,gLogger) - 通过层名更新权重
1
2
3Weights newWeights = ...;
refitter->setWeights("MyLayer",WeightsRole::kKERNEL,
newWeights); - 通过权重名更新权重
1
2Weights newWeights = ...;
refitter->setNamedWeights("MyWeights", newWeights); - 获取必须提供的权重
1
2
3
4
5
6
7
8
9
10// 获取层名
const int n = refitter->getMissing(0, nullptr, nullptr);
std::vector layerNames(n);
std::vector weightsRoles(n);
refitter->getMissing(n, layerNames.data(),
weightsRoles.data());
// 获取权重名
const int n = refitter->getMissingWeights(0, nullptr);
std::vector weightsNames(n);
refitter->getMissingWeights(n, weightsNames.data()); - 按照任意顺序提供权重
1
2
3for (int i = 0; i < n; ++i)
refitter->setWeights(layerNames[i], weightsRoles[i],
Weights{...}); - 使用提供的所有权重更新引擎
1
2bool success = refitter->refitCudaEngine();
assert(success); - 销毁定制器
1
refitter->destroy();
- 在建造之前请求一个可定制权重的引擎
如何使用 C++ 设置 TensorRT 的 TF32 推理模式 ?
- 默认情况下,TensorRT 允许使用 TF32 TensoRT 内核
- 使用 TF32 TensoRT 内核通常不会丢失精度,比 FP16 更稳定,但需要更精细的权重,其引擎序列化后更大,推理时间更长
1
2
3
4
5
6// 检查平台是否支持TF32
bool hasTf32 = builder->platformHasTf32()
// 设置TensoRT使用TF32
config->setFlag(BuilderFlag::kTF32);
// 清除
config->clearFlag(BuilderFlag::kTF32);
如何使用 C++ 设置 TensorRT 的 FP16 推理模式 ?
1
2
3
4// 设置TensoRT使用FP16
config->setFlag(BuilderFlag::kFP16);
// 不保证在构建引擎时使用16位内核,使用以下标志强制16位精度
config-> setFlag(BuilderFlag :: kSTRICT_TYPES)- 注意: 唯一具有全速率 FP16 性能的 GPU 是 Tesla P100、Quadro GP100 和 Jetson TX1/TX2, 不支持全速率 **FP16,所以,在这些型号中使用 fp16 精度反而比 fp32 慢
如何使用 C++ 设置 TensorRT 的 INT8 推理模式 ?
- 为了执行 INT8 推断,需要对 FP32 激活张量和权重进行量化,为了表示 32 位浮点值和 INT 8 位量化值,TensorRT 需要了解每个激活张量的动态范围。动态范围用于确定适当的量化比例
- 设置构建器标志可启用 INT8 精度推断
1
config-> setFlag(BuilderFlag :: kINT8);
- 两种方式向网络提供动态范围
- 使用 setDynamicRange API 手动设置每个网络张量的动态范围
1
2ITensor *tensor = network-> getLayer(layer_index)-> getOutput(output_index);
tensor -> setDynamicRange(min_float,max_float); - 使用 INT8 校准使用校准数据集生成每个张量动态范围
- 使用 setDynamicRange API 手动设置每个网络张量的动态范围
TensoRT 如何使用动态 shape?
- 动态形状是将指定某些或所有张量维度推迟到运行时的能力
- 构建动态 shape 的引擎的步骤
- 网络必须被定义为隐式批处理大小
- 使用 - 1 表示不确定的维度
- 指定一个或多个优化配置文件在构建时,为带有运行时维度的输入指定允许的维度范围,以及自动调谐器应该优化的维度
- 使用带动态 shape 的引擎?
- 从引擎创建执行上下文,与没有动态形状相同
- 在运行时,您需要在设置输入尺寸之前设置优化配置文件,配置文件按照添加的顺序编号,从开始 0// 使用 executeV2 推理时 context->setOptimizationProfile (0); // 设置推断输入尺寸
- 指定执行上下文的输入维度。设置输入尺寸后,您可以获得 TensorRT 计算给定的输入尺寸 // 0 表示输入节点的 bindingIndex context.setBindingDimensions (0, Dims3 (3, 150, 250))
- 开始推理
如果在一个 GPU 上构建引擎,并在另一个 GPU 上运行引擎,这可行吗?
- 建议不要;但是,如果这样做了,需要遵循以下准则
- TensoRT 的 major, minor, patch 3 个层次的版本号必须和系统匹配,以确保 TensoRT core 存在
- CUDA 的 major, minor 2 个层次的版本号必须和系统匹配,以确保 相同的硬件特性存在,因此内核不会无法执行
- 软件设备参数需要匹配, 如果任何属性不匹配,将收到警告―
- 最大 GPU 时钟速度 (Maximum GPU graphics clock speed)
- 最大 GPU 内存时钟速度 (Maximum GPU memory clock speed)
- GPU 内存总线宽度 (GPU memory bus width)
- 总 GPU 内存 (Total GPU memory)
- GPUL2 缓存大小 (GPU L2 cache size)
- SM 处理器计数 (SM processor count)
- 异步引擎计数 (Asynchronous engine count)
如何使用 TensorRT 在多个 GPU 上 (mutiple GPUs)?
- 无论是由构建器还是反序列化生成的引擎,每个 ICudaEngine 对象在实例化时被绑定到特定的 GPU
- 如果要选择使用特定 GPU,请在调用生成器或反序列化引擎之前使用 cudaSetDevice ()
- 注意,每个 IExecutionContext 与引擎绑定到同一个 GPU
如何理解 IBuilderConfig 的 setMaxWorkspaceSize?
- 最大工作空间大小,寻找层实现通常需要一个临时工作区,这个参数限制了网络中任何层可以使用的最大大小。如果提供的工作空间不够,则 TensorRT 可能无法找到一个层的实现
- 给出模型中任一层能使用的内存上限。运行时,每一层需要多少内存系统分配多少,并不是每次都分 1 GB,但不会超过 1 GB
- 设置内存上限过小会报以下警告
1
[I] [TRT] Some tactics do not have sufficient workspace memory to run. Increasing workspace size may increase performance, please check verbose output
- 经过实践,设置内存上限很大,引擎构建时间也不一定增加或减少,推理速度有下降趋势,但是不是很明显 (可能某些操作提升明显)
如何选择最佳的工作空间大小 (workspace size)?
- 方法 IBuilderConfig: :setMaxWorkspaceSize () 控制可分配的最大工作空间,并防止构建器考虑需要更多工作空间的算法
- 运行时,在创建 IExecutionContext。即使在中设置了额度,分配的额度也不会超过 IBuilderConfig: :setMaxWorkspaceSize () 的要求
- 因此,应用程序应该允许 TensorRT 构建器尽可能多的工作空间;在运行时,TensorRT 分配的不超过这个数量,通常更少
TensorRT 的 engine 和校准表是否可以跨 TensorRT 版本?
- 不可以。内部实现和格式会不断优化,并且可以在不同版本之间进行更改
- 不能保证发动机和校准表与不同版本的二进制兼容 TensorRT。当使用新版本的时,应用程序应该构建新的引擎和 INT8 校准表 TensorRT
如何创建一个针对几种不同批量优化的引擎?
- TensorRT 允许针对给定批处理大小优化的引擎以任何较小的大小运行,但这些较小大小的引擎的性能却不能得到很好的优化
- 要针对多个不同的批次大小进行优化,请在分配给的维度上创建优化配置文件 optfilerselector: :KOpt