六、深入学习TensorRT,Developer Guide篇(五)

这两天在忙其他的事情,过两天要去厦门旅游了,利用换工作的间隙放松一下,还没有坐过飞机,没有到过真正的南方,期待ing。

上面两篇文章介绍了常规的C++和Python的API,虽然了解了很多常用的API,但是总感觉不够透彻,我们继续前进,坚决啃掉TensorRT这块硬骨头。

5. How TensorRT Works

这一节主要讲的是官方文档的第五节:How TensorRT Works,这一节更详细的讲解了TensorRT是怎么工作的,为啥这么厉害,通过这章的学习,我们对TensorRT的整个工作细节有更全面的认识,让你了解更深入。我觉得这些部分只有官方文档会降到,当你遇到一些疑难杂症的时候,我相信答案就在这篇文章。

5.1 Object Lifetimes

TensorRT的API是基于类的,其中部分类是作为其他类的工厂存在的(就是设计模式中的工厂模式,设计模式挺重要,大型工程必须要懂设计模式)。对于用户拥有的对象,工厂对象的生命周期必须跨越它创建的对象的生命周期。例如,NetworkDefinitionBuilderConfig类是由Builder类创建的,这些类的对象应该在builder factory object销毁之前被销毁。有一个例外是从builder创建的engine。创建engine后,你可以销毁构建器、网络、解析器和构建配置并继续使用该引擎,而不受上面锁描述的生命周期的影响。

5.2 Error Handling and Logging

当创建一个 TensorRT 顶级接口(top-level interfaces )(builder, runtime or refitter)时,必须提供Logger接口的实现(C++Python)。logger用于记录一些诊断和信息性的消息;其日志输出的详细级别(verbosity level)是可配置的。由于logger可用于在 TensorRT 生命周期中的任何时刻传回信息,因此其生命周期必须大于所有使用到的logger的应用程序。因为 TensorRT 可能在内部使用多线程,所以该实现还必须是线程安全的。

对对象的 API 调用将使用与相应顶级接口关联的logger。例如,在调用ExecutionContext::enqueueV3()的时候,execution context是从engine创建的,而引擎是从runtime创建的,因此 TensorRT 将使用与该运行时关联的logger(也就是说,TensorRT使用的是和对象关联的顶层接口实现的logger)。

错误处理的主要方法是ErrorRecorder接口(C++Python)。你可以实现此接口,并将其附加到一个对象中以接收与该对象关联的错误。对象的recorder也将传递给它创建的任何其他recorder。例如,如果你将错误recorder附加到引擎,并从该引擎创建execution context,它将使用相同的recorder。如果后面将新的错误recorder附加到execution context,它将仅接收来自该上下文的错误。如果生成错误但未找到recorder,则会通过关联的logger发出错误(大白话就是一个recorder你让它attach到谁身上,它就记录谁的错误,如果一个错误没有recorder,那就传给logger来记录)。

请注意,CUDA 错误通常是异步的 。 因此,当在单个 CUDA 上下文中异步执行多个推理或其他 CUDA 工作流时,可能会在与生成该错误的执行上下文不同的执行上下文中观察到异步 GPU 错误(这告诉我们在写日志的时候,一些关键要素如发生时间、行号、类型等一定要记清楚,不然由于异步的原因,你日志混乱很难做后期分析)。

5.3 Memory

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

5.3.1. The Build Phase

在构建过程中,TensorRT 为耗时layer的实现分配设备内存。某些实现可能会消耗大量临时内存,尤其是对于大张量。你可以通过构建器配置的内存池限制来控制临时内存的最大数量(这个我们之前有讲过builder config)。工作区大小默认为设备全局内存的完整大小,但可以在必要时进行限制。如果builder发现由于工作空间不足而无法运行使用内核,它将发出一条日志消息来指示你。

然而,即使工作空间相对较小,也需要为输入、输出和权重创建缓冲区计时。TensorRT 对于此类分配返回内存不足的操作系统 (OS) 具有很强的鲁棒性。在某些平台上,操作系统可能会成功提供内存,然后内存不足杀手进程会观察到系统内存不足,并杀死 TensorRT(系统会主动kill掉一些进程保留必要的系统内存,防止系统崩溃)。如果发生这种情况,请在重试之前释放尽可能多的系统内存。

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

5.3.2. The Runtime Phase

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

引擎在反序列化时分配设备内存来存储模型权重。由于序列化引擎几乎都是权重,因此其大小非常接近权重所需的设备内存量。

一个ExecutionContext使用两种设备内存:
某些层实现所需的持久内存(Persistent memory)。例如,某些卷积实现使用边缘掩码,并且此状态不能像权重一样在上下文之间共享,因为其大小取决于层输入形状,而该形状可能因上下文而异。该内存在创建执行上下文时分配,并在其生命周期内持续有效。
暂存存储器(Scratch memory),用于在处理网络时保存中间结果。该内存用于中间激活张量。它还用于层实现所需的临时存储,其大小由IBuilderConfig::setMemoryPoolLimit()进行控制。

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

有关执行上下文使用的持久内存和暂存内存量的信息由构建器在构建网络时以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] Total Scratch Memory: 9970688

默认情况下,TensorRT 直接从 CUDA 分配设备内存。但是,你可以使用 TensorRT 的IGpuAllocator( C++Python )自行管理设备内存。当你想要控制所有 GPU 内存并重新分配给 TensorRT,而不是让 TensorRT 直接从 CUDA 分配的时候,上面的函数非常有用。

TensorRT的依赖(NVIDIA cuDNN and NVIDIA cuBLAS)可能占用大量的设备内存,TensorRT允许你使用builder configuration中的TacticSources属性(C++Python)在推理的时候控制是否使用这些库。注意,如果你自己实现的插件依赖这些库,你的库可能会编译失败。

此外,PreviewFeature::kDISABLE_EXTERNAL_TACTIC_SOURCES_FOR_CORE_0805也可以用来控制cuDNN, cuBLAS, 和 cuBLASLt的内存使用。当设置了这个标志时,TensorRT的和辛苦将不会使用这些策略,即使他们使用了IBuilderConfig::setTacticSources()进行配置,如果设置了合理的 tactic sources,那么这个标志不会影响使用IPluginV2Ext::attachToContext()函数传递到plugin的cudnnContextcublasContext对象(这个地方有点迷糊,我们后面学习插件的时候再回头研究下),这个标志时默认被设置的。

CUDA 基础设施和 TensorRT 的设备代码也会消耗设备内存。内存量因平台、设备和 TensorRT 版本而异。你可以使用 cudaGetMemInfo确定正在使用的设备内存总量。另外,TensorRT将在builder和runtime过程中的操作前后时间都记录到logger中去了,日志信息如下:

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

上面的信息表示使用CUDA初始化的内存变化,前面的表示使用量变化,后面的表示当前的CPU和GPU的占用量。
注意,对于多用户的使用的情况,使用cudaGetMemInfo评估的分配释放信息可能不准确,因为在多个进程或线程中,CUDA无法控制统一内存设备上的内存,这样会导致信息不够准确。

5.3.4 CUDA Lazy Loading

CUDA的延迟加载技术,通过延迟加载可以显著降低 TensorRT 的 GPU 峰值和主机内存使用率,并加快 TensorRT 初始化速度,而对性能的影响可以忽略不计(< 1%)。内存使用和初始化时间的节省取决于模型、软件堆栈、GPU平台等。通过设置环境变量CUDA_MODULE_LOADING=LAZY来启用延迟加载功能。有关更多信息,请参阅lazy-loading 文档。

5.3.4. L2 Persistent Cache Management

NVIDIA Ampere 及更高版本的架构支持 L2 缓存持久性,当选择要删除的L2 cache lines时,允许对二级缓存liens进行优先级排序以保留。TensorRT 可以使用它来保留缓存中的激活,从而减少 DRAM 流量和功耗。

缓存分配是每个执行上下文使用setPersistentCacheLimit来进行设置的。所有上下文(以及使用此功能的其他组件)之间的总持久缓存不应超过 cudaDeviceProp::persistingL2CacheMaxSize。有关更多信息, 请参阅 NVIDIA CUDA Best Practices Guide

5.4. Threading

一般来说,TensorRT对象不是线程安全的;从不同线程对对象的访问必须由客户端序列化。

预期的runtime concurrency model是不同的线程,将在不同的execution contexts中进行操作。上下文包含执行期间的网络状态(激活值等),因此在不同线程中同时使用上下文会导致未定义的行为。

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

  • 运行时或引擎上的只读操作。
  • 从 TensorRT 运行时反序列化引擎。
  • 从引擎创建执行上下文。
  • 注册和取消注册插件。

在不同线程中使用多个builders不存在线程安全问题;然而,builders使用计时来确定所提供参数的最快内核,并且使用具有相同 GPU 的多个构建器将扰乱计时和 TensorRT 构建最佳引擎的能力。使用多个线程使用不同的 GPU 进行构建就不存在此类问题(就是说一个GPU使用多个builder进行kernel计时会导致紊乱和构建能力减弱,多GPU没问题,每个GPU都在做自己的事情,这个我们之前也提到过)。

5.5. Determinism

TensorRT builder使用计时来查找最快的内核来实现给定的运算符。内核的计时会受到噪声的影响: GPU 上运行的其他工作、GPU 时钟速度的波动等。时序噪声意味着在构建器的连续运行中,同一个实现可能不会完全相同。
一般来说,不同的实现会使用不同的浮点运算顺序,从而导致输出存在微小差异。这些差异对最终结果的影响通常很小。然而,当 TensorRT 配置为通过调整多个精度进行优化时,FP16 和 FP32 内核之间的差异可能会更加显着,特别是在网络尚未良好正则化或对数值漂移敏感的情况下。

可能导致不同内核选择的其他配置选项是不同的输入尺寸(例如batch大小)或输入配置文件的不同优化点(请参阅Working with Dynamic Shapes这个地方我们之前也有提到过)。

算法选择器( AlgorithmSelector, C++Python ) 接口允许你强制builder为给定层选择特定的实现。你可以使用它来确保构建器每次运行都选择相同的内核。有关更多信息,请参阅 Algorithm Selection and Reproducible Builds

engine构建完成后,除了IFillLayer,它是确定性的:在相同的运行时环境中提供相同的输入将产生相同的输出。

5.5.1. IFillLayer Determinism

IFillLayer使用RANDOM_UNIFORM或者RANDOM_NORMAL操作时,上面的确定性保证就不再有效了。每次调用时,这些操作都会根据 RNG 状态生成张量,然后更新 RNG 状态。该状态存储在每个执行上下文的基础上。

其实这个地方的不确定性就是说每次构建由于一些噪声等其他因素的印象,导致TensorRT在给算子寻找最快的kernel的时候,出现了不确定性,因为是通过计时来找的,计时操作每次运行因为硬件状态不一样会出现细微差异。

5.6. Runtime Options

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

  • default runtime库是主要的库(libnvinfer.so/.dll)。
  • lean runtime库(libnvinfer_lean.so/.dll)比默认库小得多,并且仅包含运行版本兼容引擎所需的代码。它有一些限制;最主要的事,它不能改装或序列化engine。
  • dispatch runtime库(libnvinfer_dispatch.so/.dll)是一个小型加载库,可以加载lean runtime,,并将调用重定向到它。dispatch runtime加载旧版本的lean runtime,再与合理配置的builder一起,可用于提供新版本的 TensorRT 和旧plan文件之间的兼容可能性。使用dispatch runtime与手动加载lean runtime几乎相同,但它会检查 API 是否由加载的lean runtime实现,并在可能的情况下执行一些参数映射以支持 API 更改。
    lean runtime包含比默认runtime更少的算子实现。由于 TensorRT 在构建时选择算子的实现,因此你需要通过启用版本兼容性来指定使用 lean runtime去构建引擎。它可能比为默认runtime构建的引擎稍慢。

lean runtime包含dispatch runtime的所有功能,默认runtime包含lean runtime的所有功能。

TensorRT提供了与上述每个库对应的Python包:
tensorrt: default runtime的 Python 接口 。
tensorrt_lean: lean runtime的 Python 接口 。
tensorrt_dispatch: dispatch runtime的 Python 接口 。
运行 TensorRT 引擎的 Python 应用程序应导入上述包之一,以加载适合其用例的库。

5.7. Compatibility

默认情况下,只有与用于序列化engine的相同操作系统、CPU 架构、GPU 模型和 TensorRT 版本一起使用时,才能保证序列化引擎正常工作(就是说你序列化好的engine,只适合于你序列化这个engine同软硬件配置的平台,其他平台可能会出现兼容性问题,这个我们之前也有提到过)。 如何放宽对 TensorRT 版本和 GPU 型号的限制,请参阅 Version CompatibilityHardware Compatibility部分。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值