TensorRT 工作原理#

本节提供有关 TensorRT 工作原理的更多详细信息。

对象生命周期#

TensorRT 的 API 是基于类的,其中一些类充当其他类的工厂。对于用户拥有的对象,工厂对象的生命周期必须跨越它创建的对象的生命周期。例如,NetworkDefinitionBuilderConfig 类是从 Builder 类创建的,这些类的对象应在 Builder 工厂对象被销毁之前销毁。

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

错误处理和日志记录#

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

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

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

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

内存#

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

构建阶段#

在构建期间,TensorRT 会为计时层实现分配设备内存。某些实现会消耗大量临时内存,尤其是在使用大型张量时。您可以通过构建器配置的内存池限制来控制临时内存的最大量。工作区大小默认为设备全局内存的完整大小,但在必要时可以限制。如果构建器发现由于工作区不足而无法运行的适用内核,它将发出日志消息指示这一点。

然而,即使工作区相对较小,计时也需要为输入、输出和权重创建缓冲区。TensorRT 可以很好地应对操作系统 (OS) 返回内存不足的情况。在某些平台上,操作系统可能会成功提供内存,然后内存不足的终止进程会观察到系统内存不足并终止 TensorRT。如果发生这种情况,请在重试之前释放尽可能多的系统内存。

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

运行时阶段#

TensorRT 在运行时使用相对较少的主机内存,但可以使用大量的设备内存。

引擎在反序列化时分配设备内存以存储模型权重。由于序列化引擎几乎拥有所有权重,因此其大小近似于权重所需的设备内存量。

ExecutionContext 使用两种设备内存

  • 某些层实现需要持久内存 - 例如,某些卷积实现使用边缘掩码,并且此状态无法在上下文之间共享,因为权重是,因为其大小取决于层输入形状,这可能在不同上下文中有所不同。此内存分配给执行上下文的创建,并在其生命周期内持续存在。

  • 入队内存用于在处理网络时保存中间结果。此内存用于中间激活张量(称为激活内存)。它也用于层实现所需的临时存储(称为暂存内存),其边界由 IBuilderConfig::setMemoryPoolLimit() 控制。TensorRT 通过以下几种方式优化内存使用

    • 通过在具有不相交生命周期的激活张量之间共享设备内存块。

    • 在可行的情况下,允许瞬态(暂存)张量占用未使用的激活内存。因此,TensorRT 所需的入队内存范围为 {总激活内存,总激活内存 + 最大暂存内存}。

您可以选择在不使用入队内存的情况下创建执行上下文,方法是使用 ICudaEngine::createExecutionContextWithoutDeviceMemory() 并在网络执行期间提供该内存。这允许您在不并发运行的多个上下文之间共享它,或者在推理未运行时用于其他用途。所需的入队内存量由 ICudaEngine::getDeviceMemorySizeV2() 返回。

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

[08/12/2021-17:39:11] [I] [TRT] Total Host Persistent Memory: 106528
[08/12/2021-17:39:11] [I] [TRT] Total Device Persistent Memory: 29785600
[08/12/2021-17:39:11] [I] [TRT] Max Scratch Memory: 9970688

默认情况下,TensorRT 直接从 CUDA 分配设备内存。但是,您可以将 TensorRT 的 IGpuAllocator (C++, Python) 接口的实现附加到构建器或运行时,并自行管理设备内存。如果您的应用程序想要控制所有 GPU 内存并将其子分配给 TensorRT 而不是让 TensorRT 直接从 CUDA 分配,这将非常有用。

NVIDIA cuDNNNVIDIA cuBLAS 可能会占用大量设备内存。TensorRT 允许您使用构建器配置的 TacticSources (C++, Python) 属性来控制是否将这些库用于推理。某些插件实现需要这些库,因此当排除这些库时,网络可能无法成功编译。如果设置了适当的策略来源,则 cudnnContextcublasContext 句柄将使用 IPluginV2Ext:::attachToContext() 传递给插件。

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 初始化后内存增加的量。 now: 之后的内容是 CUDA 初始化后的 CPU/GPU 内存使用快照。

注意

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

CUDA 延迟加载#

CUDA 延迟加载是一项 CUDA 功能,可以显着减少 TensorRT 的峰值 GPU 和主机内存使用量,并通过可忽略不计(< 1%)的性能影响来加速 TensorRT 初始化。初始化的内存使用量和节省时间取决于模型、软件堆栈、GPU 平台等。它通过设置环境变量 CUDA_MODULE_LOADING=LAZY 来启用。有关更多信息,请参阅 NVIDIA CUDA 文档

L2 持久缓存管理#

NVIDIA Ampere 及更高版本架构支持 L2 缓存持久性,此功能允许在选择驱逐行时优先保留 L2 缓存行。TensorRT 可以使用它来保留缓存激活,从而减少 DRAM 流量和功耗。

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

线程#

TensorRT 对象通常不是线程安全的;客户端必须序列化来自不同线程的对象的访问。

预期的运行时并发模型是不同线程在不同的执行上下文中运行。上下文在执行期间包含网络状态(激活值等),因此在不同线程中并发使用上下文会导致未定义的行为。

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

  • 运行时或引擎上的非修改操作。

  • 从 TensorRT 运行时反序列化引擎。

  • 从引擎创建执行上下文。

  • 注册和注销插件。

在不同线程中使用多个构建器没有线程安全问题;但是,构建器使用计时来确定为提供的参数提供的最快内核,并且将多个构建器与同一 GPU 一起使用会扰乱计时和 TensorRT 构建最佳引擎的能力。使用多个线程与不同的 GPU 构建没有此类问题。

确定性#

TensorRT 构建器使用计时来查找最快的内核来实现给定的层。计时内核容易受到噪声的影响,例如 GPU 上运行的其他工作、GPU 时钟速度波动等。计时噪声意味着在连续的构建器运行中可能不会选择相同的实现。

通常,不同的实现将使用不同的浮点运算顺序,从而导致输出的细微差异。这些差异对最终结果的影响通常非常小。但是,当 TensorRT 配置为通过在多个精度上进行调整来优化时,FP16 和 FP32 内核之间的差异可能更显着,特别是如果网络未得到良好正则化或对数值漂移敏感。

可能导致不同内核选择的其他配置选项是不同的输入大小(例如,批大小)或输入配置文件的不同优化点(请参阅 使用动态形状 部分)。

可编辑计时缓存机制允许您强制构建器为给定层选择特定实现。您可以使用它来确保构建器从一次运行到另一次运行选择相同的内核。有关更多信息,请参阅 算法选择和可重现的构建 部分。

在构建引擎之后,除了 IFillLayerIScatterLayer 之外,它是确定性的:在相同的运行时环境中提供相同的输入将产生相同的输出。

IFillLayer 确定性#

当使用 RANDOM_UNIFORMRANDOM_NORMAL 操作将 IFillLayer 添加到网络时,上述确定性保证不再有效。在每次调用时,这些操作都会根据 RNG 状态生成张量,然后更新 RNG 状态。此状态存储在每个执行上下文的基础上。

IScatterLayer 确定性#

如果将 IScatterLayer 添加到网络,并且输入张量 indices 具有重复条目,则上述确定性保证对于 ScatterMode::kELEMENTScatterMode::kND 模式无效。此外,将任意选择来自输入更新张量的值之一。

运行时选项#

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

  • 默认运行时是主库 (libnvinfer.so/.dll)。

  • 精简运行时库 (libnvinfer_lean.so/.dll) 比默认库小得多,并且仅包含运行版本兼容引擎所需的代码。它有一些限制;主要是,它不能重定或序列化引擎。

  • 调度运行时 (libnvinfer_dispatch.so/.dll) 是一个小型的 shim 库,可以加载精简运行时并重定向调用。调度运行时可以加载旧版本的精简运行时,并且与构建器的适当配置一起使用,可以用于在新版本的 TensorRT 和旧版本的 plan 文件之间提供兼容性。使用调度运行时几乎与手动加载精简运行时相同。尽管如此,它还是会检查 lean 运行时是否实现了 API,并在可能的情况下执行一些参数映射以支持 API 更改。

精简运行时包含的操作符实现少于默认运行时。由于 TensorRT 在构建时选择运算符实现,因此您必须通过启用版本兼容性来指定应为精简运行时构建引擎。它可能比为默认运行时构建的引擎慢。

精简运行时包含调度运行时的所有功能,默认运行时包含精简运行时的所有功能。

TensorRT 提供了与上述每个库对应的 Python 包

tensorrt

一个 Python 包。它是默认运行时的 Python 接口。

tensorrt_lean

一个 Python 包。它是精简运行时的 Python 接口。

tensorrt_dispatch

一个 Python 包。它是调度运行时的 Python 接口。

运行 TensorRT 引擎的 Python 应用程序应导入上述包之一,以加载适合其用例的库。

兼容性#

默认情况下,序列化引擎仅保证在使用与用于序列化引擎的相同操作系统、CPU 架构、GPU 型号和 TensorRT 版本时才能正常工作。请参阅 版本兼容性硬件兼容性 部分,以放宽对 TensorRT 版本和 GPU 型号的约束。