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
    3
    IBuilder* builder = createInferBuilder(gLogger);
    const auto explicitBatch = 1U < < static_cast(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
    INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
  • 创建 ONNX 解析器并解析模型
    1
    2
    3
    4
    5
    6
    7
    nvonnxparser::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
    5
    IBuilderConfig* config = builder->createBuilderConfig();
    // // 构建器(builder)的工作空间,越大表示占用GPU越多,构建速度越快
    config->setMaxWorkspaceSize(16_MiB);
    // 当引擎构造完成时,TensorRT会复制重量
    ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
  • 销毁解析器、网络定义、配置、构建器
    1
    2
    3
    4
    parser->destroy();
    network->destroy();
    config->destroy();
    builder->destroy();

如何加快 TensorRT 构建器的构建时间?

  • onnx->trt 这一过程是构建器(builder)为每一层计算获选内核的过程,试想 onnx 是一个层级的算子序列L1={A,B,C,}L1=\{A,B,C ,…\},每层算子对应 1 个或多个 TensorRT 算子L2={A1,A2,A3,},{B1,B2,B3,},L2=\{A_1,A_2,A_3,…\},\{B_1,B_2,B_3,…\},…,这些算子的速度与硬件参数相关,为了构建最快的引擎,需要遍历L1,L2L1,L2 列表组合这些算子,统计其运行时间,最终获得最快的算子组合,所以这个过程尤其耗时
  • 从 TensorRT 8.0 开始,运行在拥有相同 CUDA 设备属性和 CUDA/TensorRT 版本的设备上的构建器实例可以序列化和加载时间缓存
  • 程序
    • 创建一个新的空计时缓存
      1
      2
      IBuilderConfig* 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
      4
      IHostMemory *serializedModel = engine->serialize();
      // store model to disk
      // <…>
      serializedModel->destroy();
    • 创建运行时用于反序列化
      1
      2
      IRuntime* runtime = createInferRuntime(gLogger);
      ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

如何反序列化 TensorRT 引擎并执行推理?

  • 创建执行上下文来存储中间激活值
    1
    2
    // 一个引擎可以有多个执行上下文,允许为多个重叠推理任务使用一组权重。例如,您可以在并行 CUDA 流中使用一个引擎和一个上下文来处理图像。每个上下文都是在与引擎相同的 GPU 上创建的
    IExecutionContext *context = engine->createExecutionContext();
  • 使用输入和输出 blob 名称来获得相应的输入和输出索引
    1
    2
    int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME);
    int outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME);
  • 设置一个指向 GPU 上输入和输出缓冲区的缓冲区数组
    1
    2
    3
    void* 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
      2
      ICudaEngine* engine = ...;
      IRefitter* refitter = createInferRefitter(*engine,gLogger)
    • 通过层名更新权重
      1
      2
      3
      Weights newWeights = ...;
      refitter->setWeights("MyLayer",WeightsRole::kKERNEL,
      newWeights);
    • 通过权重名更新权重
      1
      2
      Weights 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
      3
      for (int i = 0; i < n; ++i)
      refitter->setWeights(layerNames[i], weightsRoles[i],
      Weights{...});
    • 使用提供的所有权重更新引擎
      1
      2
      bool 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
      2
      ITensor *tensor = network-> getLayer(layer_index)-> getOutput(output_index);
      tensor -> setDynamicRange(min_float,max_float);
    • 使用 INT8 校准使用校准数据集生成每个张量动态范围

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