2. Working With TensorRT Using The C++ API

以下部分重点介绍了可以使用C ++ API执行的TensorRT用户目标和任务。 进一步的细节在 Samples 部分提供,并在适当的情况下链接到下面。

假设您从一个已经训练好的模型开始。 本章将介绍使用TensorRT的以下必要步骤:

  • 从模型中创建 TensorRT 网络定义
  • 调用 TensorRT 构建器以从网络创建优化的运行时引擎
  • 序列化和反序列化引擎,以便在运行时快速重新创建
  • 喂入数据为引擎提供执行推理

C++ API vs Python API

从本质上讲,C ++ API 和 Python API 在支持您的需求方面应该完全相同。 C ++ API 应该用于任何性能关键场景,以及安全性很重要的场合,例如汽车行业。

Python API 的主要好处是数据预处理和后处理易于使用,因为您可以使用各种库,如 NumPy 和 SciPy。 有关 Python API 的更多信息,请参阅 Working With TensorRT Using The Python API.

2.1. Instantiating TensorRT Objects in C++

要运行推理,您需要使用 IExecutionContext 对象。 要创建 IExecutionContext 类型的对象,首先需要创建 ICudaEngine 类型的对象(引擎)。

可以通过以下两种方式之一创建引擎:

  • 通过用户模型的网络定义。 在这种情况下,可以选择将引擎序列化并保存以供以后使用。
  • 通过从磁盘读取序列化引擎。 在这种情况下,性能更好,因为绕过了解析模型和创建中间对象的步骤。

需要全局创建 iLogger 类型的对象。 它用作 TensorRT API 的各种方法的参数。 一个演示记录器创建的简单示例如下所示:

class Logger : public ILogger           
 {
     void log(Severity severity, const char* msg) override
     {
         // suppress info-level messages
         if (severity != Severity::kINFO)
             std::cout << msg << std::endl;
     }
 } gLogger;

名为 createInferBuilder(gLogger) 的全局 TensorRT API 方法用于创建 iBuilder 类型的对象,如下图所示。有关更多信息,请参阅 IBuilder class reference。

使用iLogger作为输入参数创建iBuilder

为 iBuilder 定义的名为 createNetwork 的方法用于创建 iNetworkDefinition 类型的对象,如下图所示。

createNetwork() 用于创建网络

使用 iNetwork 定义作为输入创建一个可用的解析器:

  • ONNX: parser = nvonnxparser::createParser(*network, gLogger);
  • NVCaffe: ICaffeParser* parser = createCaffeParser();
  • UFF: parser = createUffParser();

调用来自 iParser 类型的对象的名为 parse() 的方法来读取模型文件并填充 TensorRT 网络:

Parsing the model file

调用 iBuilder 的一个名为 buildCudaEngine() 的方法来创建一个 iCudaEngine 类型的对象,如图所示:

Creating the TensorRT engine

可以选择将引擎序列化并转储到文件中。

Creating the TensorRT engine

执行上下文用于执行推理。

Creating an execution context

如果序列化引擎被保留并保存到文件中,则可以绕过上述大多数步骤。

名为 createInferRuntime(gLogger)的全局 TensorRT API 方法用于创建 iRuntime 类型的对象,如图所示:

Creating TensorRT runtime

有关 TensorRT 运行时的更多信息,请参阅 IRuntime class reference。 通过调用运行时方法 deserializeCudaEngine() 来创建引擎。

对于这两种使用模型,其余推断是相同的。

尽管可以避免创建 CUDA 上下文(将为您创建默认上下文),但这是不可取的。 建议在创建运行时或构建器对象之前创建和配置 CUDA 上下文。

将使用与创建线程关联的 GPU 上下文创建构建器或运行时。 虽然如果默认上下文尚不存在,但会创建它,但建议在创建运行时或构建器对象之前创建和配置 CUDA 上下文。

2.2. Creating A Network Definition In C++

使用 TensorRT 进行推理的第一步是从您的模型创建 TensorRT 网络。 实现此目的的最简单方法是使用 TensorRT 解析器库导入模型,该解析器库支持以下格式的序列化模型:

  • sampleMNIST (both BVLC and NVCaffe)
  • sampleOnnxMNIST
  • sampleUffMNIST (used for TensorFlow)

另一种方法是使用 TensorRT API 直接定义模型。 这要求您进行少量 API 调用以定义网络图中的每个层,并为模型的训练参数实现自己的导入机制。

在任何一种情况下,您都明确需要告诉 TensorRT 需要哪些张量作为推断的输出。 未标记为输出的张量被认为是可由建造者优化的瞬态值。 输出张量的数量没有限制,但是,将张量标记为输出可能会禁止对张量进行一些优化。 输入和输出张量也必须给出名称(使用 ITensor :: setName() )。 在推理时,您将为引擎提供一个指向输入和输出缓冲区的指针数组。 为了确定引擎对这些指针的预期顺序,您可以使用张量名称进行查询。

TensorRT 网络定义的一个重要方面是它包含指向模型权重的指针,这些指针由构建器复制到优化引擎中。 如果网络是通过解析器创建的,则解析器将拥有权重占用的内存,因此在构建器运行之前,不应删除解析器对象。

2.2.1. Creating A Network Definition From Scratch Using The C++ API

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

在下面的示例中,我们将创建一个包含 Input,Convolution,Pooling,FullyConnected,Activation 和 SoftMax 层的简单网络。 要查看整体中的代码,请参阅位于 /usr/src/tensorrt/samples/sampleMNISTAPI 目录中的 sampleMNISTAPI。

  1. 创建构建器和网络:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. 使用输入维度的方式将输入层添加到网络。 网络可以有多个输入,但在此示例中只有一个:
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W});
  1. 添加具有隐藏层输入节点的 Convolution 图层,该层还带有过滤器和偏差的步幅和权重。 为了从图层中检索张量参考,我们可以使用:
layerName->getOutput(0)
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});

注意:传递给 TensorRT 层的权重在主机内存中。

  1. 添加 Pooling 层:
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});
  1. 添加全连接和激活函数层:
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]);
auto relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);
  1. 添加 SoftMax 层以计算最终概率并将其设置为输出:
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
  1. 标记输出:
network->markOutput(*prob->getOutput(0));

2.2.2. Importing A Model Using A Parser In C++

要使用 C ++ Parser API 导入模型,您需要执行以下高级步骤:

  1. Create the TensorRT builder and network.
IBuilder* builder = createInferBuilder(gLogger);
nvinfer1::INetworkDefinition* network = builder->createNetwork();
  1. Create the TensorRT parser for the specific format.

ONNX

auto parser = nvonnxparser::createParser(*network, gLogger);

UFF

auto parser = createUffParser();

NVCaffe

ICaffeParser* parser = createCaffeParser();
  1. Use the parser to parse the imported model and populate the network.
parser->parse(args);

具体的 args 取决于使用什么格式的解析器。 有关更多信息,请参阅 TensorRT API 中解析器的文档。

必须在网络之前创建构建器,因为它充当网络的工厂。 不同的解析器具有用于标记网络输出的不同机制。

2.2.3. Importing A Caffe Model Using The C++ Parser API

以下步骤说明了如何使用 C ++ Parser API 导入 Caffe 模型。 有关更多信息,请参阅 sampleMNIST。

  1. Create the builder and network:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. Create the Caffe parser:
ICaffeParser* parser = createCaffeParser();
  1. Parse the imported model:
const IBlobNameToTensor* blobNameToTensor = parser->parse("deploy_file" , "modelFile", *network, DataType::kFLOAT);

这将填充 Caffe 模型中的 TensorRT 网络。 最后一个参数指示解析器生成权重为 32 位浮点数的网络。 使用 DataType::kHALF 将生成具有 16 位权重的模型。

除了填充网络定义之外,解析器还返回一个字典,该字典从 Caffe blob 名称映射到 TensorRT 张量。 与 Caffe 不同,TensorRT 网络定义没有就地操作的概念。 当 Caffe 模型使用就地操作时,字典中返回的 TensorRT 张量对应于对该blob 的最后一次写入。 例如,如果卷积写入 blob 并且后跟就地 ReLU,则该 blob 的名称将映射到 TensorRT 张量,该张量是 ReLU 的输出。

  1. Specify the outputs of the network:
for (auto& s : outputs)
    network->markOutput(*blobNameToTensor->find(s.c_str()));

2.2.4. Importing A TensorFlow Model Using The C++ UFF Parser API

注意:对于新项目,建议使用 TensorFlow-TensorRT 集成 作为转换 TensorFlow 网络以使用 TensorRT 进行推理的方法。 有关集成说明,请参阅 Integrating TensorFlow With TensorRT 和 Release Notes。

从 TensorFlow 框架导入要求您将 TensorFlow 模型转换为中间格式 UFF(Universal Framework Format)。 有关转换的更多信息,请参阅 Converting A Frozen Graph To UFF。

以下步骤说明了如何使用C ++ Parser API导入TensorFlow模型。 有关UFF导入的更多信息,请参阅 sampleUffMNIST。

  1. Create the builder and network:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
  1. Create the UFF parser:
IUFFParser* parser = createUffParser();
  1. Declare the network inputs and outputs to the UFF parser:
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");

注意:TensorRT 期望输入张量为 CHW 顺序。 从 TensorFlow 导入时,请确保输入张量符合所需顺序,如果不是,请将其转换为 CHW。

  1. Parse the imported model to populate the network:
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);

2.2.5. Importing An ONNX Model Using The C++ Parser API

限制:由于 ONNX 格式正在快速开发,您可能会遇到模型版本和解析器版本之间的版本不匹配。 TensorRT 5.0.0 附带的 ONNX Parser 支持 ONNX IR(Intermediate Representation)版本 0.0.3,opset 版本 7。

通常,较新版本的 ONNX Parser 旨在向后兼容,因此,遇到早期版本的 ONNX 导出器生成的模型文件不应该导致问题。 当更改不向后兼容时,可能会有一些例外。 在这种情况下,将早期的 ONNX 模型文件转换为以后支持的版本。 有关此主题的更多信息,请参阅 ONNX Model Opset Version Converter。

用户模型也可能是由支持后来的 opset 的导出工具生成的,而不是 TensorRT 附带的 ONNX 解析器所支持的。在这种情况下,请检查发布到 GitHub onnx-tensorrt 的最新版本的 TensorRT 是否支持所需的版本。 支持的版本由 onnx_trt_backend.cpp 中的 BACKEND_OPSET_VERSION 变量定义。 从 GitHub 下载并构建最新版本的 ONNX TensorRT Parser。 有关构建的说明,请访问:TensorRT backend for ONNX。

以下步骤说明了如何使用 C ++ Parser API 导入 ONNX 模型。 有关 ONNX 导入的详细信息,请参阅 sampleOnnxMNIST。

  1. Create the ONNX parser. 解析器使用辅助配置管理 SampleConfig 对象将输入参数从示例可执行文件传递到解析器对象:
nvonnxparser::IOnnxConfig* config = nvonnxparser::createONNXConfig();
//Create Parser
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*config);
  1. Ingest the model:
parser->parse(onnx_filename, DataType::kFLOAT);
  1. Convert the model to a TensorRT network:
parser->convertToTRTNetwork();
  1. Obtain the network from the model:
nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();

2.3. Building An Engine In C++

下一步是调用 TensorRT 构建器来创建优化的运行时。 构建器的一个功能是搜索其 CUDA 内核目录以获得最快的可用实现,因此必须使用相同的 GPU 来构建优化引擎将运行的 GPU。

构建器具有许多属性,您可以设置这些属性以控制网络应运行的精度,以及自动调整参数,例如 TensorRT 在确定哪个最快时(多次迭代会导致更长的运行时间,但是对噪声的敏感性较低)应该为每个内核计时多少次 。您还可以查询构建器,以找出硬件本身支持的精简类型。

两个特别重要的属性是最大批量大小和最大工作空间大小。

  • 最大批量大小指定 TensorRT 将优化的批量大小。 在运行时,可以选择较小的批量大小。
  • 层算法通常需要临时工作空间。 此参数限制网络中任何层可以使用的最大大小。 如果提供的划痕不足,则TensorRT可能无法找到给定层的实现。
  1. Build the engine using the builder object:
builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildCudaEngine(*network);

在构建引擎时,TensorRT会复制权重。

  1. Dispense(分发、分配) with the network, builder, and parser if using one.
engine->destroy();
network->destroy();
builder->destroy();

2.4. Serializing A Model In C++

要进行序列化,您要将引擎转换为一种格式,以便以后存储和使用以进行推理。 要用于推理,您只需反序列化引擎即可。 序列化和反序列化是可选的。 由于从网络定义创建引擎可能非常耗时,因此每次应用程序重新生成时都可以通过序列化一次并在推理时对其进行反序列化来避免重建引擎。 因此,在构建引擎之后,用户通常希望将其序列化以供以后使用。

构建可能需要一些时间,因此一旦构建了引擎,您通常需要将其序列化以供以后使用。 在将模型用于推理之前,并非绝对有必要对模型进行序列化和反序列化 - 如果需要,可以直接使用引擎对象进行推理。

注意:序列化引擎不能跨平台或TensorRT版本移植。 引擎特定于它们构建的精确GPU模型(除了平台和TensorRT版本)。

  1. Run the builder as a prior offline step and then serialize:
IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();
  1. Create a runtime object to deserialize:
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

最后一个参数是使用自定义图层的应用程序的插件层工厂。 有关更多信息,请参阅 Extending TensorRT With Custom Layers。

2.5. Performing Inference In C++

以下步骤说明了如何使用引擎在C ++中执行推理。

  1. 创建一些空间来存储中间激活值。 由于引擎保持网络定义和训练的参数,因此需要额外的空间。 这些都保存在执行上下文中:
IExecutionContext *context = engine->createExecutionContext();

引擎可以具有多个执行上下文,允许一组权重用于多个重叠推理任务。 例如,您可以使用一个引擎和每个流一个上下文在并行 CUDA 流中处理图像。 每个上下文将在与引擎相同的 GPU 上创建。

  1. Use the input and output blob names to get the corresponding input and output index:
int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME);
  1. Using these indices, set up a buffer array pointing to the input and output buffers on the GPU:
void* buffers[2];
buffers[inputIndex] = inputbuffer;
buffers[outputIndex] = outputBuffer;
  1. TensorRT execution is typically asynchronous, so enqueue the kernels on a CUDA stream:
context.enqueue(batchSize, buffers, stream, nullptr);

通常在内核之前和之后将异步 memcpy() 排入队列以从 GPU 移动数据(如果尚未存在)。 enqueue() 的最后一个参数是一个可选的 CUDA 事件,当输入缓冲区被占用并且可以安全地重用它们的内存时,它将被发出信号。

要确定内核(以及可能的 memcpy() )何时完成,请使用标准 CUDA 同步机制(如事件)或等待流。

2.6. Memory Management In C++

TensorRT 提供了两种机制,允许应用程序更多地控制设备内存。

默认情况下,在创建 IExecutionContext 时,会分配持久设备内存来保存激活数据。 要避免此分配,请调用 createExecutionContextWithoutDeviceMemory。 然后应用程序负责调用IExecutionContext :: setDeviceMemory() 来提供运行网络所需的内存。 ICudaEngine :: getDeviceMemorySize() 返回内存块的大小。

此外,应用程序可以通过实现 IGpuAllocator 接口提供在构建和运行时使用的自定义分配器。 实现接口后,请调用 setGpuAllocator(分配器);

IBuilderIRuntime 接口上。 然后将通过此接口分配和释放所有设备内存。

你可能感兴趣的:(2. Working With TensorRT Using The C++ API)