tensorrt:学习(2)

TensorRT 工作原理

对象的生命周期

TensorRT的API是基于类的,其中一些类作为一些类的工厂。对于用户拥有的对象,工厂对象的生存期必须跨越它创建的对象的生存期。例如,NetworkDefinition和BuilderConfig类是从Builder类创建的,这些类的对象应该在生成器工厂对象之前销毁。

此规则的一个重要例外是从生成器创建引擎。创建引擎后,可以销毁生成器、网络、解析器和生成配置,然后继续使用引擎。

错误句柄和日志

创建TensorRT顶级接口builderruntimerefitter)时,您必须提供Logger(C++、Python)接口的实现。Logger用于诊断和信息消息;其详细级别是可配置。由于记录器可用于在TensorRT生存期内的任何时间点传回信息,因此其生存期必须跨越应用中该接口的任何使用。实现还必须是线程安全的,因为TensorRT可能会在内部使用工作线程。

对对象的API调用将使用与相应顶级接口关联的Logger。例如,在调用ExecutionContext::enqueueV3()时,执行上下文是从引擎创建的,而引擎是从运行时创建的,因此TensorRT将使用与该运行时关联的日志记录器。

错误处理的主要方法是ErrorRecorder(C++,Python)接口。您可以实现此接口,并将其附加到API对象以接收与该对象关联的错误。对象的记录器也将传递给它创建的任何其它记录器-例如,如果将错误Logger附加到Engine,并从该引擎创建执行上下文,则它将使用相同的Logger。如果您接着将新的错误记录器附加至执行内容,它将只接收来自该内容的错误。如果生成错误但未找到错误记录器,则将通过关联的记录器发出该错误。


请注意,CUDA错误通常是异步的-因此,当在单个CUDA上下文中异步执行多个推理或其他CUDA工作流时,可能会在与生成该错误的上下文不同的执行上下文中观察到异步GPU错误。

Memory

TensorRT使用大量设备内存(即GPU可直接访问的内存,而不是连接到CPU的主机内存)。由于设备内存通常是受限资源,因此了解TensorRT如何使用它非常重要。

The Build Phase

build期间,TensorRT会为时序层实现分配设备内存。某些实现可能会消耗大量的临时内存,特别是在张量较大的情况下。您可以通过builder config.的内存池限制来控制最大临时内存量。工作区大小默认为设备全局内存的完整大小,但必要时可以对其进行限制。如果构建器发现由于工作空间不足而无法运行的可用内核,它将发出一条日志消息来指示这一点

然而,即使工作空间相对较小,实时性的需求为输入、输出和权重创建缓冲区。TensorRT对于操作系统(OS)返回此类分配的内存不足非常稳健。在某些平台上,操作系统可能会成功提供内存,然后内存不足杀手进程会发现系统内存不足,并终止TensorRT。如果发生这种情况,请在重试之前释放尽可能多的系统内存。

在build阶段,主机存储器中通常至少存在权重的两个副本:来自原始网络的那些,以及在构建引擎时作为引擎的一部分包括的那些。此外,当TensorRT组合权重时(例如卷积与批量归一化),将创建额外的临时权重张量。

The Runtime Phase

在运行时,TensorRT使用的主机内存相对较少,但可以使用大量设备内存
engine on  deserialization时分配设备存储器以存储模型权重。由于 serialized engine几乎包含所有权重,因此其大小与权重所需的设备内存量非常接近

ExecutionContext使用两种设备内存:

  • 一些层实现所需的持久性存储器-例如,一些卷积实现使用边缘掩码,并且该状态不能像权重那样在上下文之间共享,因为其大小取决于层输入形状,其可以跨上下文而变化。该内存在创建执行上下文时分配,并持续其生命周期。
  • 一些层实现所需的非持久性存储器-例如,一些卷积实现使用边缘掩码,并且该状态不能像权重那样在上下文之间共享,因为其大小取决于层输入形状,其可以跨上下文而变化。该内存在创建执行上下文时分配,并持续其生命周期。

您可以选择使用ICudaEngine::createExecutionContextWithoutDeviceMemory()创建一个没有临时内存的执行上下文,并在网络执行期间自己提供该内存。这允许您在不并发运行的多个上下文之间共享它,或者在推理不运行时用于其他用途。ICudaEngine::getDeviceMemorySize()返回所需的临时内存量。

关于执行上下文所使用的持久存储器和临时存储器的量的信息由构建器在构建网络时以严重性kINFO发出。检查日志,消息类似于以下内容:

[08/12/2021-17:39:11] [I] [TRT]主机持久内存总量:106528
[08/12/2021-17:39:11] [I] [TRT]设备持久性内存总量:29785600
[08/12/2021-17:39:11] [I] [TRT]暂存内存总量:9970688

TensorRT的依赖项(NVIDIA cuDNN和NVIDIA cuBLAS)可能会占用大量的设备内存。TensorRT允许您通过使用构建器配置中的IGpuAllocator(C++,Python)属性来控制这些库是否用于推理。请注意,一些插件实现需要这些库,因此如果排除它们,则网络可能无法成功编译。

TensorRT的依赖项(NVIDIA cuDNN和NVIDIA cuBLAS)可能会占用大量的设备内存。TensorRT允许您通过使用构建器配置中的TacticSources(C++,Python)属性来控制这些库是否用于推理。请注意,一些插件实现需要这些库,因此如果排除它们,则网络可能无法成功编译。

此外,PreviewFeature::kDISABLE_EXTERNAL_TACTIC_SOURCES_FOR_CORE_0805用于控制TensorRT核心库中cuDNN、cuBLAS和cuBLASLt的使用。当设置此标志时,TensorRT核心库将不会使用这些策略,即使它们由IBuilderConfig::setTacticSources()指定。如果设置了适当的策略源,此标志将不会影响使用IPluginV2Ext::attachToContext()传递给插件的cudnContext和cublasContext句柄。默认情况下设置此标志。

CUDA结构和TensorRT的设备代码也会消耗设备内存。内存量因平台、设备和TensorRT版本而异。您可以使用cudaGetMemInfo来确定正在使用的设备内存总量。

TensorRT计算构建器和运行时的关键操作前后内存的使用量。这些内存使用统计信息被打印到TensorRT的信息记录器中。例如:

[MemUsageChange] Init CUDA: CPU +535, GPU +0, now: CPU 547, GPU 1293 (MiB)

它指示CUDA初始化导致的内存使用变化。CPU +535,GPU +0是运行CUDA初始化后增加的内存量。现在之后的内容:是CUDA初始化后的CPU/GPU内存使用快照。

注意:在多租户情况下,cudaGetMemInfo和TensorRT报告的内存使用情况容易出现争用情况,其中新的分配/释放由不同的进程或不同的线程完成。由于CUDA无法控制统一内存设备上的内存,因此cudaGetMemInfo返回的结果在这些平台上可能不准确。

CUDA延迟加载

CUDA延迟加载是CUDA的一项功能,可显著降低TensorRT的峰值GPU和主机内存使用率,并加快TensorRT初始化,而对性能的影响微乎其微(〈1%)。内存使用和初始化时间的节省取决于型号、软件堆栈、GPU平台等,通过设置环境变量CUDA_MODULE_LOADING=LAZY启用。有关详细信息,请参阅NVIDIA CUDA文档。

L2 持久缓存管理

NVIDIA Ampere和更高版本的体系结构支持二级高速缓存持久性,该功能允许在选择要清除的缓存线时优先保留二级高速缓存线。TensorRT可以使用此功能在缓存中保留激活,从而减少DRAM流量和功耗。

缓存分配是针对每个执行上下文的,使用上下文的setPersistentCacheLimit方法启用。所有上下文(以及使用此功能的其他组件)中的总持久性缓存不应超过cudaDeviceProp::persistingL2CacheMaxSize。有关详细信息,请参阅NVIDIA CUDA最佳实践指南。

Threading

通常,TensorRT对象不是线程安全的;从不同线程对对象的访问必须由用户进行串行化。
预期的运行时并发模型是不同的线程将在不同的执行上下文上操作。上下文包含执行期间的网络状态(激活值等),因此在不同线程中并发使用上下文会导致未定义的行为。

为了支持此模型,以下操作是线程安全的:

  1. 运行时或引擎上的非修改操作。
  2. 从TensorRT运行时反序列化引擎。
  3. 从引擎创建执行上下文。
  4. 注册和注销插件。

在不同线程中使用多个生成器不存在线程安全问题;然而,构建器使用时序来确定所提供参数的最快内核,并且使用具有相同GPU的多个构建器将扰乱时序和TensorRT构建最佳引擎的能力。使用多个线程来构建不同的GPU不存在这样的问题。

Determinism

TensorRT 的  build 使用计时来找到实现给定运算符的最快内核。定时内核会受到噪声的影响-GPU上运行的其他工作、GPU时钟速度的波动等。定时噪声意味着在构建器的连续运行中,可能不会选择相同的实现。
一般来说,不同的实现将使用不同顺序的浮点运算,从而导致输出的微小差异。这些差异对最终结果的影响通常非常小。然而,当TensorRT被配置为通过在多个精度上进行调整来优化时,FP 16和FP 32内核之间的差异可能会更加显著,特别是如果网络没有被很好地正则化或者对数值漂移敏感的话。
可能导致不同内核选择的其他配置选项包括不同的输入大小(例如,批处理大小)或输入配置文件的不同优化点(请参阅使用动态形状部分)。
AlgorithmSelector(C++,Python)接口允许您强制构建器为给定层选择特定的实现。您可以使用它来确保构建器从运行到运行选择相同的内核。有关详细信息,请参阅“算法选择和可复制构建”部分。
引擎构建完成后,除IFillLayer外,它都是确定性的:在相同的运行时环境中提供相同的输入将产生相同的输出。

Runtime Options

TensorRT提供多个运行时库,以满足各种用例。运行TensorRT引擎的C++应用程序应链接以下内容之一:

  • 默认的运行库是主库(libnvinfer.so/.dll)。
  • 精益运行时库(libnvinferlean.so/.dll)比默认库小得多,并且只包含运行版本兼容引擎所需的代码。它有一些限制;首先,它不能改装或序列化发动机。
  • 调度运行时(libnvinfer_dispatch.so/.dll)是一个小型shim库,可以加载精益运行时,并将调用重定向到它。调度运行时能够加载旧版本的精益运行时,并与构建器的适当配置一起,可以用于提供较新版本的TensorRT和较旧计划文件之间的兼容性。使用分派运行时几乎与手动加载精益运行时相同,但它检查API是否由加载的精益运行时实现,并执行一些参数映射以支持API更改(如果可能)

精益运行时包含的运算符实现比默认运行时少。由于TensorRT在构建时选择运算符实现,因此您需要通过启用版本兼容性来指定引擎应针对精益运行时构建。它可能比为默认运行时构建的引擎稍慢。
精益运行时包含分派运行时的所有功能,默认运行时包含精益运行时的所有功能。
TensorRT提供与上述每个库对应的Python包:

tensorrt
        一个Python包。它是默认运行时的Python接口。
tensorrt_lean
        一个Python包。它是精益运行时的Python接口。
tensorrt_dispatch
        一个Python包。它是分派运行时的Python接口。
运行TensorRT引擎的Python应用程序应该导入上述包之一,以加载适合其用例的库。

Advanced Topics

版本兼容性

默认情况下,TensorRT engines仅与构建它们的TensorRT版本兼容。通过适当的构建时配置,可以构建与主版本中的其他TensorRT次要版本兼容的引擎。使用TensorRT 8构建的TensorRT引擎也将与TensorRT 9运行时兼容,但反之亦然。

从版本8. 6开始支持版本兼容性;也就是说,计划必须使用至少8.6或更高的版本来构建,并且运行时必须是8.6或更高。

当使用版本兼容性时,引擎在运行时支持的API是构建它的版本中支持的API和用于运行它的版本的API的交集。TensorRT仅在主要版本边界上删除API,因此这不是主要版本中的问题。但是,希望将TensorRT 8引擎与TensorRT 9一起使用的用户必须迁移弃用的API。

创建版本兼容引擎的建议方法是按如下方式构建:

c++
builderConfig.setFlag(BuilderFlag::kVERSION_COMPATIBLE);
IHostMemory* plan = builder->buildSerializedNetwork(network, config);
python
builder_config.set_flag(tensorrt.BuilderFlag.VERSION_COMPATIBLE)
plan = builder.build_serialized_network(network, config)

隐式批处理模式不支持此标志。必须使用NetworkDefinitionCreationFlag::kEXPLICIT_BATCH创建网络

这将导致精益运行时的副本被添加到计划中。当您随后反序列化计划时,TensorRT会识别出它包含运行时的副本。它加载运行库,并使用它来反序列化和执行计划的其余部分。由于这会导致代码在拥有进程的上下文中从计划加载和运行,因此您应该只以这种方式反序列化受信任的计划。要向TensorRT表明您信任该计划,请调用:

runtime->setEngineHostCodeAllowed(true);
runtime.engine_host_code_allowed = True

如果要在plan中打包插件,则还需要受信任计划的标志(请参阅插件共享库)。

Manually Loading the Runtime(手动加载runtime)

前面的方法(版本兼容性)为每个计划打包一Runtime的副本,如果应用程序使用大量模型,则这可能会令人望而却步。另一种方法是自己管理运行库加载。对于这种方法,构建版本兼容的计划,如前一节所述,但也设置了一个额外的标志来排除精益运行时。

builderConfig.setFlag(BuilderFlag::kVERSION_COMPATIBLE);
builderConfig.setFlag(BuilderFlag::kEXCLUDE_LEAN_RUNTIME);
IHostMemory* plan = builder->buildSerializedNetwork(network, config)
python
builder_config.set_flag(tensorrt.BuilderFlag.VERSION_COMPATIBLE)
builder_config.set_flag(tensorrt.BuilderFlag.EXCLUDE_LEAN_RUNTIME)
plan = builder.build_serialized_network(network, config)

要运行此计划,您必须能够访问用于构建此计划的版本的精益运行时。假设您已经使用TensorRT 8.6构建了计划,并且您的应用程序与TensorRT 9相链接,您可以按如下方式加载计划。

C++
IRuntime* v9Runtime = createInferRuntime(logger);
IRuntime* v8ShimRuntime = v9Runtime->loadRuntime(v8RuntimePath);
engine = v8ShimRuntime->deserializeCudaEngine(v8plan);
python
v9_runtime = tensorrt.Runtime(logger)
v8_shim_runtime = v9_runtime.load_runtime(v8_runtime_path)
engine = v8_shim_runtime.deserialize_cuda_engine(v8_plan)

运行时将为TensorRT 8.6运行时转换TensorRT 9 API调用,检查以确保支持调用并执行任何必要的参数重新映射。

Loading from Storage

在大多数操作系统上,TensorRT可以直接从内存加载共享运行时库。但是,在3.17之前的Linux内核上,需要临时目录。使用IRuntime::setTempfileControlFlagsIRuntime::setTemporaryDirectory API来控制TensorRT对这些机制的使用。

Using Version Compatibility with the ONNX Parser

当使用TensorRT的ONNX解析器从ONNX文件生成TensorRT网络定义时,建议指定解析器以版本兼容的方式解析ONNX模型。
为此,请使用IParser::setFlag()API。仅当使用InstanceNormalization运算符解析ONNX模型时,才需要此标志。此API仅在开源ONNX解析器中可用。

auto *parser = nvonnxparser::createParser(network, logger);
parser->setFlag(nvonnxparser::OnnxParserFlag::kVERSION_COMPATIBLE);
parser = trt.OnnxParser(network, logger)
parser.set_flag(trt.OnnxParserFlag.VERSION_COMPATIBLE)

此外,解析器可能需要使用插件,以便完全实现网络中使用的所有ONNX运算符。特别地,如果网络用于构建版本兼容的引擎,则一些插件可能需要与引擎一起包括(与引擎一起序列化,或者外部提供并显式加载)。
要查询实现特定解析网络所需的插件库列表,请使用IParser::getUsedVCPluginLibraries API:

auto *parser = nvonnxparser::createParser(network, logger);
parser->setFlag(nvonnxparser::OnnxParserFlag::kVERSION_COMPATIBLE);
parser->parseFromFile(filename, static_cast<int>(ILogger::Severity::kINFO));
int64_t nbPluginLibs;
char const* const* pluginLibs = parser->getUsedVCPluginLibraries(nbPluginLibs);
parser = trt.OnnxParser(network, logger)
parser.set_flag(trt.OnnxParserFlag.VERSION_COMPATIBLE)

status = parser.parse_from_file(filename)
plugin_libs = parser.get_used_vc_plugin_libraries()

Hardware Compatibility

默认情况下,TensorRT引擎仅与构建它们的设备类型兼容。通过构建时配置,可以构建与其他类型的设备兼容的引擎。目前,硬件兼容性仅支持Ampere和更高版本的设备架构,不支持NVIDIA DRIVE OS。
例如,要构建与所有Ampere和更新架构兼容的引擎,请按如下方式配置IBuilderConfig:

config->setHardwareCompatibilityLevel(nvinfer1::HardwareCompatibilityLevel::kAMPERE_PLUS);

在硬件兼容模式下构建时,TensorRT排除了不兼容硬件的策略,例如使用特定于架构的指令或需要比某些设备上可用的更多共享内存的策略。因此,硬件兼容引擎可以具有比其非硬件兼容对应引擎更低的吞吐量和/或更高的等待时间。这种性能影响的程度取决于网络架构和输入大小。

Compatibility Checks

TensorRT在plan中记录用于创建计划的库的主要,次要,补丁和构建版本。如果这些与用于反序列化计划的运行库版本不匹配,则反序列化计划将失败。当使用版本兼容性时,检查将由反序列化计划数据的精益运行时执行。默认情况下,该精益运行时包含在计划中,并且保证匹配成功。

TensorRT还记录计划中的计算能力(主要和次要版本),并根据加载计划的GPU进行检查。如果它们不匹配,计划将无法反序列化。这确保了在构建阶段选择的内核存在并且可以运行。当使用硬件兼容性时,检查是宽松的;使用HardwareCompatibilityLevel::kAMPERE_PLUS,检查将确保计算能力大于或等于8.0,而不是检查精确匹配。

TensorRT还会检查以下属性,如果它们不匹配,则会发出警告,除非使用硬件兼容性:

  • 全局内存总线宽度
  • 二级缓存大小
  • 每个块和每个多处理器的最大共享内存
  • 纹理对齐要求
  • 多处理器数量
  • GPU设备是集成的还是独立的

如果GPU时钟速度在引擎串行化和运行时系统之间不同,则从串行化系统选择的策略对于运行时系统可能不是最佳的,并且可能导致一些性能降级。
如果反序列化期间可用的设备内存小于序列化期间的内存量,则反序列化可能会由于内存分配失败而失败。
如果要优化单个TensorRT引擎以在同一架构中的多个设备上使用,建议的方法是在最小的设备上运行构建器。或者,您可以在具有有限计算资源的较大设备上构建引擎(请参阅限制计算资源部分)。这是因为当在大型设备上构建小型模型时,TensorRT可能会选择效率较低但在可用资源上扩展得更好的内核。此外,TensorRT用于从cuDNN和cuBLAS选择和配置内核的API不支持跨设备兼容性,因此在构建器配置中禁用这些策略源。

安全运行时能够反序列化在TensorRT的主要、次要、补丁和构建版本在某些情况下不完全匹配的环境中生成的引擎。有关详细信息,请参阅NVIDIA DRIVE OS 6.0开发人员指南。

Refitting an Engine

TensorRT可以使用新的权重重新安装engine,而无需重新构建它,但是,必须在构建时指定这样做的选项:

config->setFlag(BuilderFlag::kREFIT) 
builder->buildSerializedNetwork(network, config);

Later, you can create a Refitter object:

ICudaEngine* engine = ...;
IRefitter* refitter = createInferRefitter(*engine,gLogger)

然后更新权重。例如,要更新名为“MyLayer”的卷积层的核权重:

Weights newWeights = ...;
refitter->setWeights("MyLayer",WeightsRole::kKERNEL,
                    newWeights);

新的权重应与用于构建engine的原始权重具有相同的计数。如果出现错误,例如图层名称或角色错误或权重计数更改,setWeights将返回false。
由于引擎的优化方式,如果您更改了某些权重,则可能还需要提供一些其他权重。界面可以告诉您必须提供哪些附加重量。
您可以使用INetworkDefinition::setWeightsName()在构建时命名权重-ONNX解析器使用此API将权重与ONNX模型中使用的名称相关联。然后,稍后您可以使用setNamedWeights来更新权重:

Weights newWeights = ...;
refitter->setNamedWeights("MyWeights", newWeights);

setNamedWeightssetWeights可以同时使用,也就是说,您可以使用setNamedWeights更新具有名称的权重,并使用setWeights更新那些未命名的权重。
这通常需要两次调用IRefitter::getMissing,首先获取必须提供的权重对象的数量,其次获取它们的层和角色。

const int32_t n = refitter->getMissing(0, nullptr, nullptr);
std::vector<const char*> layerNames(n);
std::vector<WeightsRole> weightsRoles(n);
refitter->getMissing(n, layerNames.data(), 
                        weightsRoles.data());

或者,要获取所有缺失权重的名称,请运行:

const int32_t n = refitter->getMissingWeights(0, nullptr);
std::vector<const char*> weightsNames(n);
refitter->getMissingWeights(n, weightsNames.data());

您可以按任何顺序提供缺少的权重:

for (int32_t i = 0; i < n; ++i)
    refitter->setWeights(layerNames[i], weightsRoles[i],
                         Weights{...});

返回的缺失权重集是完整的,在这个意义上,仅提供缺失权重不会产生对任何更多权重的需求。
提供所有权重后,您可以更新引擎:

bool success = refitter->refitCudaEngine();
assert(success);

如果refit返回false,则检查日志以获取诊断信息,可能是关于仍然缺少的权重。
然后可以删除改装程序:

delete refitter;

更新后的引擎表现得就像它是从用新权重更新的网络构建的。

要查看引擎中的所有可重调权重,请使用refitter-〉getAll(...)getAllWeights(...);类似于先前如何使用getMissing和getMissingWeights。

Creating a Network Definition from Scratch(从头创建网络定义)

您也可以使用Network Definition API直接将网络定义到TensorRT,而不是使用解析器。此场景假设每层权重已在主机内存中准备好,以便在网络创建期间传递给TensorRT。

以下示例创建了一个简单的网络,其中包含Input、Convolution、Pooling、MatrixMultiply、Shuffle、Activation和SoftMax层。

有关层的详细信息,请参阅NVIDIA TensorRT操作员参考。https://docs.nvidia.com/deeplearning/tensorrt/operators/docs/index.html

C++

在此示例中,权重被加载到以下代码中使用的weightMap数据结构中。
首先创建构建器和网络对象。请注意,在下面的示例中,使用所有C++示例通用的logger.cpp文件初始化记录器。C++示例帮助类和函数可以在common.h头文件中找到。

    auto builder = SampleUniquePtr<nvinfer1::IBuilder>(nvinfer1::createInferBuilder(sample::gLogger.getTRTLogger()));
    const auto explicitBatchFlag = 1U << static_cast<uint32_t>(nvinfer1::NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
    auto network = SampleUniquePtr<nvinfer1::INetworkDefinition>(builder->createNetworkV2(explicitBatchFlag));

有关kEXPLICIT_BATCH标志的详细信息,请参阅“显式与隐式批处理”部分。
通过指定输入张量的名称、数据类型和全维度,将输入图层添加到网络。一个网络可以有多个输入,尽管在这个例子中只有一个:

auto data = network->addInput(INPUT_BLOB_NAME, datatype, Dims4{1, 1, INPUT_H, INPUT_W});

添加具有隐藏层输入节点、步长以及过滤器和偏置权重的卷积层。

auto conv1 = network->addConvolution(
*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});

传递给TensorRT层的权重位于主机内存中。

增加Pooling层;注意,来自前一层的输出作为输入被传递。

auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});

添加一个Shuffle层以重塑输入,为矩阵乘法做准备:

int32_t const batch = input->getDimensions().d[0];
int32_t const mmInputs = input.getDimensions().d[1] * input.getDimensions().d[2] * input.getDimensions().d[3]; 
auto inputReshape = network->addShuffle(*input);
inputReshape->setReshapeDimensions(Dims{2, {batch, mmInputs}});

现在,添加一个MatrixMultiply层。这里,模型导出器提供了转置权重,因此为这些权重指定了kTRANSPOSE选项。

IConstantLayer* filterConst = network->addConstant(Dims{2, {nbOutputs, mmInputs}}, mWeightMap["ip1filter"]);
auto mm = network->addMatrixMultiply(*inputReshape->getOutput(0), MatrixOperation::kNONE, *filterConst->getOutput(0), MatrixOperation::kTRANSPOSE);

添加偏差,该偏差将在批处理维度中传播。

auto biasConst = network->addConstant(Dims{2, {1, nbOutputs}}, mWeightMap["ip1bias"]);
auto biasAdd = network->addElementWise(*mm->getOutput(0), *biasConst->getOutput(0), ElementWiseOperation::kSUM);

Add the ReLU Activation layer:

auto relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);
Add the SoftMax layer to calculate the final probabilities:
auto prob = network->addSoftMax(*relu1->getOutput(0));
Add a name for the output of the SoftMax layer so that the tensor can be bound to a memory buffer at inference time:
auto prob = network->addSoftMax(*relu1->getOutput(0));
Mark it as the output of the entire network:
network->markOutput(*prob->getOutput(0));

Python

Code corresponding to this section can be found in network_api_pytorch_mnist.
This example uses a helper class to hold some of metadata about the model:
class ModelData(object):
    INPUT_NAME = "data"
    INPUT_SHAPE = (1, 1, 28, 28)
    OUTPUT_NAME = "prob"
    OUTPUT_SIZE = 10
    DTYPE = trt.float32
In this example, the weights are imported from the PyTorch MNIST model.
weights = mnist_model.get_weights()
Create the logger, builder, and network classes.
TRT_LOGGER = trt.Logger(trt.Logger.ERROR)
builder = trt.Builder(TRT_LOGGER)
EXPLICIT_BATCH = 1 << (int)(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
network = builder.create_network(EXPLICIT_BATCH)

Refer to the Explicit Versus Implicit Batch section for more information about the kEXPLICIT_BATCH flag.

Next, create the input tensor for the network, specifying the name, datatype, and shape of the tensor.
input_tensor = network.add_input(name=ModelData.INPUT_NAME, dtype=ModelData.DTYPE, shape=ModelData.INPUT_SHAPE)
Add a convolution layer, specifying the inputs, number of output maps, kernel shape, weights, bias, and stride:
conv1_w = weights['conv1.weight'].numpy()
    conv1_b = weights['conv1.bias'].numpy()
    conv1 = network.add_convolution(input=input_tensor, num_output_maps=20, kernel_shape=(5, 5), kernel=conv1_w, bias=conv1_b)
    conv1.stride = (1, 1)
Add a pooling layer, specifying the inputs (the output of the previous convolution layer), pooling type, window size, and stride:
pool1 = network.add_pooling(input=conv1.get_output(0), type=trt.PoolingType.MAX, window_size=(2, 2))
    pool1.stride = (2, 2)
Add the next pair of convolution and pooling layers:
conv2_w = weights['conv2.weight'].numpy()
    conv2_b = weights['conv2.bias'].numpy()
    conv2 = network.add_convolution(pool1.get_output(0), 50, (5, 5), conv2_w, conv2_b)
    conv2.stride = (1, 1)

    pool2 = network.add_pooling(conv2.get_output(0), trt.PoolingType.MAX, (2, 2))
    pool2.stride = (2, 2)
Add a Shuffle layer to reshape the input in preparation for a matrix multiplication:
batch = input.shape[0]
mm_inputs = np.prod(input.shape[1:])
input_reshape = net.add_shuffle(input)
input_reshape.reshape_dims = trt.Dims2(batch, mm_inputs)
Now, add a MatrixMultiply layer. Here, the model exporter provided transposed weights, so the kTRANSPOSE option is specified for those.
filter_const = net.add_constant(trt.Dims2(nbOutputs, k), weights["fc1.weight"].numpy())
mm = net.add_matrix_multiply(input_reshape.get_output(0), trt.MatrixOperation.NONE, filter_const.get_output(0), trt.MatrixOperation.TRANSPOSE);
Add bias, which will broadcast across the batch dimension:
bias_const = net.add_constant(trt.Dims2(1, nbOutputs), weights["fc1.bias"].numpy())
bias_add = net.add_elementwise(mm.get_output(0), bias_const.get_output(0), trt.ElementWiseOperation.SUM)
Add the ReLU activation layer:
 relu1 = network.add_activation(input=fc1.get_output(0), type=trt.ActivationType.RELU)
Add the final fully connected layer, and mark the output of this layer as the output of the entire network:
    fc2_w = weights['fc2.weight'].numpy()
    fc2_b = weights['fc2.bias'].numpy()
    fc2 = network.add_fully_connected(relu1.get_output(0), ModelData.OUTPUT_SIZE, fc2_w, fc2_b)

    fc2.get_output(0).name = ModelData.OUTPUT_NAME
    network.mark_output(tensor=fc2.get_output(0))

The network representing the MNIST model has now been fully constructed. Refer to sections Building an Engine and Performing Inference for how to build an engine and run inference with this network.

精度降低

网络级精度控制

默认情况下,TensorRT以32位精度工作,但也可以使用16位浮点和8位量化浮点执行操作。使用较低的精度需要较少的内存,并且可以实现更快的计算。

降低精度支持取决于您的硬件(请参阅硬件和精度)。您可以查询构建器以检查平台上支持的精度支持:

if (builder->platformHasFastFp16()) { … };
if builder.platform_has_fp16:

在构建器配置中设置标志会通知TensorRT它可以选择较低精度的实现:

config->setFlag(BuilderFlag::kFP16);
config.set_flag(trt.BuilderFlag.FP16)

有三个精度标志:FP 16、INT 8和TF 32,它们可以独立使能。请注意,如果TensorRT导致整体运行时间较低,或者如果不存在低精度实现,TensorRT仍然会选择更高精度的内核。
当TensorRT为层选择精度时,它会根据需要自动转换权重以运行层。
虽然使用FP 16和TF 32精度相对简单,但使用INT 8时会有额外的复杂性。有关更多详细信息,请参阅“使用INT 8”一章。
请注意,即使启用了精度标志,引擎的输入/输出绑定也默认为FP 32。有关如何设置输入/输出绑定的数据类型和格式,请参阅I/O Formats部分。

Layer-Level Control of Precision

Build标志提供许可的、粗粒度的控制。然而,有时网络的一部分需要更高的动态范围或对数值精度敏感。您可以约束每层的输入和输出类型:

layer->setPrecision(DataType::kFP16)
layer.precision = trt.fp16

这为输入和输出提供了一个首选类型(这里是DataType::kFP16)。您可以进一步设置层输出的首选类型:

ayer->setOutputType(out_tensor_index, DataType::kFLOAT)
layer.set_output_type(out_tensor_index, trt.fp32)

计算将使用与输入首选类型相同的浮点类型。大多数TensorRT实现的输入和输出具有相同的浮点类型;然而,卷积、去卷积和全连接可以支持量化的INT 8输入和未量化的FP 16或FP 32输出,因为有时需要使用来自量化输入的更高精度输出来保持精度。
将精度约束设置为TensorRT,提示它应该选择一个输入和输出与首选类型匹配的层实现,如果上一层的输出和下一层的输入与请求的类型不匹配,则插入重新格式化操作。请注意,TensorRT只能选择具有这些类型的实现,如果它们也使用构建器配置中的标志启用。
默认情况下,TensorRT只有在实现更高性能的网络时才会选择这样的实现。如果另一个实现更快,TensorRT会使用它并发出警告。您可以通过首选生成器配置中的类型约束来重写此行为。

config->setFlag(BuilderFlag::kPREFER_PRECISION_CONSTRAINTS)
config.set_flag(trt.BuilderFlag.PREFER_PRECISION_CONSTRAINTS)

如果约束是首选的,TensorRT会遵守它们,除非没有带有首选精度约束的实现,在这种情况下,它会发出警告并使用最快的实现。
要将警告更改为错误,请使用OBEY而不是PREFER:

config->setFlag(BuilderFlag::kOBEY_PRECISION_CONSTRAINTS);
config.set_flag(trt.BuilderFlag.OBEY_PRECISION_CONSTRAINTS);

精度约束是可选的--你可以通过C++中的 layer->precisionIsSet()或Python中的layer.precision_is_set来查询是否设置了一个约束。如果未设置精度约束,则从C++中的layer->getPrecision()返回的结果或阅读Python中的精度属性没有意义。输出类型约束同样是可选的。
如果未使用ILayer::setPrecision或ILayer::setOutputType API设置约束,则忽略BuilderFlag::kPREFER_PRECISION_CONSTRAINTS或BuilderFlag::kOBEY_PRECISION_CONSTRAINTS。层可以根据允许的构建器精度自由选择任何精度或输出类型。
请注意,ITensor::setType()API不会设置张量的精度约束,除非它是网络的输入/输出张量之一。此外,在layer-〉setOutputType()和layer->getOutput(i)->setType()之间存在区别。前者是一个可选类型,它限制了TensorRT将为层选择的实现。后者指定网络输入/输出的类型,如果张量不是网络输入/输出,则忽略。如果它们不同,TensorRT将插入一个转换,以确保遵守两个规范。因此,如果要为生成网络输出的层调用setOutputType(),则通常还应将相应的网络输出配置为具有相同的类型。

TF32

默认情况下,TensorRT允许使用TF32张量核。在计算内积时,例如在卷积或矩阵乘法期间,TF32执行以下操作:

  • 将FP32被乘数舍入为FP16精度,但保持FP32动态范围。
  • 计算舍入被乘数的精确乘积。
  • 将乘积累加到FP32总和中。

TF 32张量核可以使用FP 32加速网络,通常不会损失准确性。对于需要HDR(高动态范围)进行权重或激活的模型,它比FP 16更强大。
不能保证TF 32 Tensor核心实际上被使用,也没有办法强制实现使用它们- TensorRT可以随时回退到FP 32,如果平台不支持TF 32,则始终福尔斯退到FP 32。但是,您可以通过清除TF 32构建器标志来禁用它们的使用。

config->clearFlag(BuilderFlag::kTF32);
config.clear_flag(trt.BuilderFlag.TF32)

在构建引擎时设置环境变量NVIDIA_TF32_OVERRIDE=0将禁用TF 32的使用,尽管已设置BuilderFlag::kTF 32。此环境变量设置为0时,将覆盖NVIDIA库的任何默认值或编程配置,因此它们不会使用TF 32张量核加速FP 32计算。这只是一个调试工具,NVIDIA库之外的任何代码都不应更改基于此环境变量的行为。除0之外的任何其他设置都保留供将来使用。
警告:在引擎运行时将环境变量NVIDIA_TF32_OVERRIDE设置为其他值可能会导致不可预知的精度/性能影响。当发动机运转时,最好不要设置。
注:除非您的应用需要TF 32提供的更高动态范围,否则FP 16将是更好的解决方案,因为它几乎总是产生更快的性能。

I/O Formats

TensorRT使用许多不同的数据格式优化网络。为了允许在TensorRT和客户端应用程序之间有效地传递数据,这些底层数据格式在网络I/O边界处公开,即标记为网络输入或输出的Tensor,以及在向插件传递数据和从插件传递数据时。对于其他张量,TensorRT会选择整体执行速度最快的格式,并可能插入重新格式化以提高性能。
您可以通过分析可用的I/O格式,并结合TensorRT前后操作最有效的格式,来组装最佳数据管道。
要指定I/O格式,请以位掩码的形式指定一种或多种格式。
以下示例将输入张量格式设置为TensorFormat::kHWC 8。请注意,此格式仅适用于DataType::kHALF,因此必须相应地设置数据类型。

auto formats = 1U << TensorFormat::kHWC8;
network->getInput(0)->setAllowedFormats(formats);
network->getInput(0)->setType(DataType::kHALF);

formats = 1 << int(tensorrt.TensorFormat.HWC8)
network.get_input(0).allowed_formats = formats
network.get_input(0).dtype = tensorrt.DataType.HALF

请注意,在不是网络输入/输出的张量上调用setAllowedFormats()或setType()没有效果,并且被TensorRT忽略。
通过设置构建器配置标志DIRECT_IO,可以使TensorRT避免在网络边界插入重新格式化。此标志通常会适得其反,原因有二:

  • 与允许TensorRT插入重新格式化相比,生成的引擎可能会更慢。重新格式化可能听起来像是浪费的工作,但它可以允许耦合最有效的内核。
  • 如果TensorRT无法在不引入此类重新格式化的情况下构建引擎,则构建将失败。失败可能只发生在某些目标平台上,因为这些平台的内核支持的格式不同。

 注意,对于矢量化格式,通道维度必须被零填充到矢量大小的倍数。例如,如果输入绑定的维度为[16,3,224,224],数据类型为kHALF,格式为kHWC 8,则绑定缓冲区的实际所需大小为16*8*224*224*sizeof(半)字节,即使引擎-〉getBindingDimension()API将返回张量维度为[16,3,224,224]。填充部分中的值(即,在此示例中C= 3,4,...,7)必须用零填充。
有关这些格式的数据在内存中的实际布局,请参阅数据格式说明。

Explicit Versus Implicit Batch(显式与隐式batch)

TensorRT支持两种指定网络的模式:显式批和隐式批
在隐式批处理模式中,每个张量都有一个隐式批处理维度,所有其他维度必须具有恒定长度。早期版本的TensorRT使用此模式,现在已弃用,但为了向后兼容,仍将支持此模式。
在显式批处理模式中,所有维都是显式的,并且可以是动态的,也就是说,它们的长度可以在执行时更改。许多新功能(如动态形状和循环)仅在此模式下可用。它也是ONNX解析器所必需的。
例如,考虑以NCHW格式处理具有3个通道的大小为HxW的N个图像的网络。在运行时,输入张量具有维度[N,3,H,W]。这两种模式的不同之处在于INetworkDefinition指定张量维度的方式:

  • 在显式批处理模式中,网络指定[N,3,H,W]。
  • 在隐式批处理模式中,网络仅指定[3,H,W]。批维度N是隐式的。

“跨批处理对话”的操作不可能在隐式批处理模式中表示,因为无法在网络中指定批处理维度。隐式批处理模式中无法表达的操作示例:

  • 在批维度上减少
  • 调整批维度的形状
  • 将批处理维度与另一个维度调换

例外情况是,张量可以通过网络输入的ITensor::setBroadcastAcrossBatch方法在整个批处理中广播,并隐式广播其他张量。

显式批处理模式消除了限制-批处理轴是轴0。显式批处理的更准确术语是“batch oblivious”,因为在此模式下,TensorRT不会为前导轴附加任何特殊语义,除非特定操作需要。实际上,在显式批处理模式中,甚至可能没有批处理维度(例如,仅处理单个图像的网络),或者可能存在长度不相关的多个批处理维度(例如,比较从两个批处理中提取的所有可能对)。
在创建INetworkDefinition时,必须使用标志指定显式批处理与隐式批处理的选择。下面是显式批处理模式的C++代码:

IBuilder* builder = ...;
INetworkDefinition* network = builder->createNetworkV2(1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)))

对于隐式批处理,请使用createNetwork或向createNetworkV2传递0。
下面是显式批处理模式的Python代码:

builder = trt.Builder(...)
builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))

Sparsity

NVIDIA Ampere架构GPU支持结构化稀疏。为了利用该特征来实现更高的推理性能,卷积核权重和全连接权重必须满足以下要求:
对于核权重中的每个输出通道和每个空间像素,每四个输入通道必须至少有两个零。换句话说,假设内核权重具有形状[K,C,R,S]且C % 4 == 0,则使用以下算法验证要求:

hasSparseWeights = True
for k in range(0, K):
    for r in range(0, R):
        for s in range(0, S):
            for c_packed in range(0, C // 4):
                if numpy.count_nonzero(weights[k, c_packed*4:(c_packed+1)*4, r, s]) > 2 :
                    hasSparseWeights = False

要启用稀疏特性,请在构建器配置中设置kSPARSE_WEIGHTS标志,并确保启用kFP16或kINT8模式。例如:

config->setFlag(BuilderFlag::kSPARSE_WEIGHTS);
config->setFlag(BuilderFlag::kFP16);
config->setFlag(BuilderFlag::kINT8);
config.set_flag(trt.BuilderFlag.SPARSE_WEIGHTS)
config.set_flag(trt.BuilderFlag.FP16)
config.set_flag(trt.BuilderFlag.INT8)

在构建TensorRT引擎时,TensorRT日志结束时,TensorRT报告哪些层包含满足结构化稀疏性要求的权重,以及TensorRT在哪些层中选择使用结构化稀疏性的策略。在某些情况下,具有结构化稀疏性的策略可能比正常策略慢,在这些情况下,TensorRT将选择正常策略。以下输出显示了一个TensorRT日志的示例,其中显示了有关稀疏性的信息:

[03/23/2021-00:14:05] [I] [TRT] (Sparsity) Layers eligible for sparse math: conv1, conv2, conv3
[03/23/2021-00:14:05] [I] [TRT] (Sparsity) TRT inference plan picked sparse implementation for layers: conv2, conv3

Empty Tensors

TensorRT支持空张量。一个张量是空张量,如果它有一个或多个长度为零的维度。零长度尺寸通常没有特殊处理。如果一个规则适用于长度为L的维度,且L为任意正值,那么它通常也适用于L=0。
例如,当沿着最后一个轴连接两个维度为[x,y,z]和[x,y,w]的张量时,结果的维度为[x,y,z+w],而不管x,y,z或w是否为零。
隐式广播规则保持不变,因为只有单位长度维度是广播的特殊维度。例如,给定两个维度为[1,y,z]和[x,1,z]的张量,由IElementWiseLayer计算的它们的和具有维度[x,y,z],而不管x、y或z是否为零。
如果引擎绑定是一个空的张量,它仍然需要一个非空的内存地址,不同的张量应该有不同的地址。这与C++规则一致,即每个对象都有一个唯一的地址,例如,new float[0]返回一个非空指针。如果使用的内存分配器可能返回零字节的空指针,则要求至少一个字节。
请参阅NVIDIA TensorRT操作员参考,了解空张量的每层特殊处理。

Engine Inspector

TensorRT提供了ICengineInspector API来检查TensorRT引擎内部的信息。从反序列化引擎调用createEngineInspector()创建引擎检查器,然后调用getLayerInformation()或getEngineInformation()检查器API,分别获取引擎中特定图层或整个引擎的信息。您可以打印出给定引擎的第一层信息,以及引擎的整体信息,如下所示:

auto inspector = std::unique_ptr<IEngineInspector>(engine->createEngineInspector());
inspector->setExecutionContext(context); // OPTIONAL
std::cout << inspector->getLayerInformation(0, LayerInformationFormat::kJSON); // Print the information of the first layer in the engine.
std::cout << inspector->getEngineInformation(LayerInformationFormat::kJSON);
inspector = engine.create_engine_inspector();
inspector.execution_context = context; # OPTIONAL
print(inspector.get_layer_information(0, LayerInformationFormat.JSON); # Print the information of the first layer in the engine.
print(inspector.get_engine_information(LayerInformationFormat.JSON); # Print the information of the entire engine.

请注意,引擎/图层信息的详细程度取决于构建引擎时的ProfilingVerbosity构建器配置设置。默认情况下,“ProfilingVerbosity”设置为kLAYER_NAMES_ONLY,因此仅打印图层名称。如果ProfilingVerbosity设置为kNONE,则不会打印任何信息;如果设置为kDETAILED,则将打印详细信息。

下面是getLayerInformation()API根据ProfilingVerbosity设置打印的图层信息的一些示例:

kLAYER_NAMES_ONLY

"node_of_gpu_0/res4_0_branch2a_1 + node_of_gpu_0/res4_0_branch2a_bn_1 + node_of_gpu_0/res4_0_branch2a_bn_2"
{
  "Name": "node_of_gpu_0/res4_0_branch2a_1 + node_of_gpu_0/res4_0_branch2a_bn_1 + node_of_gpu_0/res4_0_branch2a_bn_2",
  "LayerType": "CaskConvolution",
  "Inputs": [
  {
    "Name": "gpu_0/res3_3_branch2c_bn_3",
    "Dimensions": [16,512,28,28],
    "Format/Datatype": "Thirty-two wide channel vectorized row major Int8 format."
  }],
  "Outputs": [
  {
    "Name": "gpu_0/res4_0_branch2a_bn_2",
    "Dimensions": [16,256,28,28],
    "Format/Datatype": "Thirty-two wide channel vectorized row major Int8 format."
  }],
  "ParameterType": "Convolution",
  "Kernel": [1,1],
  "PaddingMode": "kEXPLICIT_ROUND_DOWN",
  "PrePadding": [0,0],
  "PostPadding": [0,0],
  "Stride": [1,1],
  "Dilation": [1,1],
  "OutMaps": 256,
  "Groups": 1,
  "Weights": {"Type": "Int8", "Count": 131072},
  "Bias": {"Type": "Float", "Count": 256},
  "AllowSparse": 0,
  "Activation": "RELU",
  "HasBias": 1,
  "HasReLU": 1,
  "TacticName": "sm80_xmma_fprop_implicit_gemm_interleaved_i8i8_i8i32_f32_nchw_vect_c_32kcrs_vect_c_32_nchw_vect_c_32_tilesize256x128x64_stage4_warpsize4x2x1_g1_tensor16x8x32_simple_t1r1s1_epifadd",
  "TacticValue": "0x11bde0e1d9f2f35d"

此外,当引擎用动态形状构建时,引擎信息中的动态维度将被示出为-1,并且张量格式信息将不被示出,因为这些字段取决于推断阶段的实际形状。要获取特定推断形状的引擎信息,请创建一个IExecutionContext,将所有输入维度设置为所需的形状,然后调用inspector-〉setExecutionContext(context)。设置上下文后,检查器将打印上下文中设置的特定形状的引擎信息。
trtexec工具提供--profilingVerbosity、--dumpLayerInfo和--exportLayerInfo标志,可用于获取给定引擎的引擎信息。有关详细信息,请参阅trtexec部分。
目前,引擎信息中仅包括绑定信息和层信息,包括中间张量的维度、精度、格式、策略索引、层类型和层参数。在未来的TensorRT版本中,可以将更多信息作为输出JSON对象中的新键添加到引擎检查器输出中。还将提供关于检查器输出中的键和字段的更多规范。
此外,一些子图由尚未与引擎检查器集成的下一代图优化器处理。因此,当前未示出这些层内的层信息。这将在未来的TensorRT版本中得到改进。

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值