TensorRT笔记(3)使用C++ API

2.1 在C ++中实例化TensorRT对象

为了运行推理,请使用界面 IExecutionContext。为了创建一个类型的对象IExecutionContext,首先创建一个类型的对象 ICudaEngine (the engine)。
通过以下两种方式之一创建引擎:

  • 通过用户模型中的网络定义。在这种情况下,可以选择对引擎进行序列化并保存以供以后使用。
  • 通过从磁盘读取序列化的引擎。与创建网络定义并从中构建引擎相比,这可以节省大量时间。

创建类型的全局对象 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;

使用TensorRT API独立功能 createInferBuilder(gLogger) 创建一个iBuilder类型的对象 。有关更多信息,请参见IBuilder类参考
使用方法 iBuilder :: createNetworkV2 创建一个INetworkDefinition类型的对象 。
使用以下INetwork 定义作为输入创建其中一个可用的解析器(Caffe,ONNX或UFF):

  • ONNX: auto parser = nvonnxparser::createParser(*network, gLogger);
  • Caffe: auto parser = nvcaffeparser1::createCaffeParser();
  • UFF: auto parser = nvuffparser::createUffParser();
    调用IParser :: parse()方法读取模型文件并填充TensorRT网络。
    调用iBuilder :: buildEngineWithConfig()方法创建一个ICudaEngine类型的对象。
    可以选择将引擎序列化并转储到文件中。
    创建并使用执行上下文执行推理。
    如果保留了序列化引擎并将其保存到文件中,则可以跳过上述大多数步骤。
    使用TensorRT API独立功能 createInferRuntime(gLogger) 创建一个runtime类型的对象,查阅IRuntime参考文献。

通过调用 IRuntime :: deserializeCudaEngine()方法创建引擎。有关TensorRT运行时的更多信息,请参见参考IRuntime
对于引擎是直接从网络构建还是从文件反序列化,其余推论是相同的。
即使可以避免创建CUDA上下文(将为您创建默认上下文),也不建议这样做。建议在创建runtime或生成器对象之前创建和配置CUDA上下文。
将使用与创建线程关联的GPU上下文来创建构建器或运行时。尽管将创建默认上下文(如果尚不存在),但建议在创建运行时或生成器对象之前创建和配置CUDA上下文。

2.2 在C ++中创建网络定义

使用TensorRT进行推理的第一步是根据您的模型创建一个TensorRT网络。
最简单的方法是使用TensorRT 解析器库导入模型,该库在以下示例中支持序列化模型:

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

无论哪种情况,您都需要明确告诉TensorRT哪些张量需要作为推理输出。没有标记为输出的张量被认为是瞬态值,可以由建造者优化。输出张量的数量没有限制,但是,将张量标记为输出可能会禁止对该张量进行一些优化。

输入和输出张量也必须给定名称(使用 ITensor :: setName())。在推论时,您将为引擎提供指向输入和输出缓冲区的指针数组。为了确定引擎期望这些指针的顺序,您可以使用张量名称进行查询。

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

2.2.1 使用C ++ API从头开始创建网络定义

除了使用解析器之外,您还可以通过网络定义API将网络直接定义到TensorRT。该场景假设在网络创建过程中主机内存中的每层权重已准备好传递给TensorRT。
关于此任务
在下面的示例中,我们将创建一个具有输入,卷积,池化,完全连接,激活和SoftMax层的简单网络。要整体查看代码,请参阅位于GitHub库中的opensource/ sampleMNISTAPI目录下网址的逐层构建简单MNIST网络(sampleMNISTAPI)

程序

  1. 创建构建器和网络:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetworkV2(1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
  1. 将输入层(包括动态批处理)和输入维添加到网络中。一个网络可以有多个输入,尽管在此示例中只有一个:
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{
    
    -1, 1, INPUT_H, INPUT_W});
  1. 添加具有隐藏层输入节点,步幅和权重的卷积层以进行滤波和偏置。为了从图层中检索张量参考,我们可以使用:
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{
    
    5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{
    
    1, 1});

注意:传递到TensorRT层的权重在主机内存中。
4. 添加池层:

auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{
    
    2, 2});
pool1->setStride(DimsHW{
    
    2, 2});
  1. 添加FullyConnected和Activation层:
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 在C ++中使用解析器导入模型

必须在网络之前创建该构建器,因为它是网络的工厂。不同的解析器具有不同的机制来标记网络输出。
要使用C ++ Parser API导入模型,您将需要执行以下高级步骤:
1.创建TensorRT 构建器。

IBuilder* builder = createInferBuilder(gLogger);

有关如何创建记录器的示例,请参见在C ++中实例化TensorRT对象
2.为特定格式创建TensorRT网络和解析器。
ONNX

nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)); auto parser = nvonnxparser::createParser(*network, gLogger);

UFF

nvinfer1::INetworkDefinition* network = builder->createNetworkV2(0U);auto parser = nvuffparser::createUffParser();

Caffe

nvinfer1::INetworkDefinition* network = builder->createNetworkV2(0U);auto parser = nvcaffeparser1::createCaffeParser();

3.使用解析器解析导入的模型并填充网络。

parser->parse(args);

具体的 args取决于使用哪种格式的解析器。有关更多信息,请参阅TensorRT API中记录的解析器。

2.2.3 使用C ++ Parser API导入Caffe模型

以下步骤说明了如何使用C ++ Parser API导入Caffe模型。
关于此任务
有关更多信息,请参见 位于GitHub存储库中的TensorRT的“ Hello World”(sampleMNIST)。
程序

  1. 创建构建器和网络:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetworkV2(0U);
  1. 创建Caffe解析器:
ICaffeParser* parser = createCaffeParser();
  1. 解析导入的模型:
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的输出。
4. 指定网络的输出:

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

2.2.4 使用C ++ UFF Parser API导入TensorFlow模型

以下步骤说明了如何使用C ++ Parser API导入TensorFlow模型。
关于此任务
注意:对于新项目,建议使用TF-TRT集成作为将TensorFlow网络转换为使用TensorRT进行推理的方法。有关集成说明,请参阅《TF-TRT用户指南》中的“加速推理”
从TensorFlow框架导入需要将TensorFlow模型转换为中间格式UFF(通用框架格式)。有关转换的更多信息,请参见将冻结图转换为UFF

有关UFF导入的更多信息,请参阅位于GitHub存储库中的导入TensorFlow模型并运行推理(sampleUffMNIST)

程序

  1. 创建构建器和网络:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetworkV2(0U);
  1. 创建UFF解析器:
IUFFParser* parser = createUffParser();
  1. 向UFF解析器声明网络输入和输出:
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");
  1. 解析导入的模型以填充网络:
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);

2.2.5 使用C ++ Parser API导入ONNX模型

以下步骤说明了如何使用C ++ Parser API导入ONNX模型。
注意:
通常,ONNX Parser的较新版本被设计为向后兼容直至opset7。当更改不向后兼容时,可能会有一些例外。在这种情况下,请将早期的ONNX模型文件转换为更高支持的版本。有关此主题的更多信息,请参见ONNX模型Opset版本转换器

用户模型也可能是由导出工具生成的,该工具支持比TensorRT随附的ONNX解析器支持的操作集晚的操作集。在这种情况下,请检查TensorRT的最新版本是否已发布到GitHub,onnx-tensorrt,支持所需的版本。受支持的版本由BACKEND_OPSET_VERSION 可变的 onnx_trt_backend.cpp。从GitHub下载并构建最新版本的ONNX TensorRT Parser。可以在以下位置找到构建说明:用于ONNX的TensorRT后端

关于此任务
有关ONNX导入的更多信息,请参阅位于GitHub存储库中的ONNX的TensorRT的“ Hello World”(sampleOnnxMNIST)
注意:在TensorRT 7.0中,ONNX解析器仅支持全尺寸模式,这意味着您的网络定义必须使用显式批处理标志集。有关更多信息,请参见 Working With Dynamic Shapes
UFF不支持 explicitBatch 或dynamic shape networks。

程序

  1. 创建构建器和网络。
IBuilder* builder = createInferBuilder(gLogger);
const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);  
INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
  1. 创建ONNX解析器:
nvonnxparser::IParser* parser = 
nvonnxparser::createParser(*network, gLogger);

有关更多信息,请参考NvOnnxParser.h文件。
3. 解析模型:

parser->parseFromFile(onnx_filename, ILogger::Severity::kWARNING);
	for (int i = 0; i < parser.getNbErrors(); ++i)
	{
    
    
		std::cout << parser->getError(i)->desc() << std::endl;
	}

2.3 用C ++构建引擎

下一步是调用TensorRT构建器以创建优化的运行时间。构建器的功能之一是搜索其CUDA内核目录,以获取可用的最快实现,因此有必要使用与运行优化引擎的GPU相同的GPU进行构建。
关于此任务
的 IBuilderConfig您可以设置许多属性来控制诸如网络运行的精度以及自动调整参数,例如确定最快的时间(例如,更多的迭代会导致更长的运行时间,TensorRT为每个内核计时的次数)。还可以查询构建器,以找出硬件固有支持的降低精度的类型。

一个特别重要的属性是最大工作空间大小。

  • 层算法通常需要临时工作空间。此参数限制网络中任何层可以使用的最大大小。如果提供的刮擦不足,则TensorRT可能无法找到给定层的实现。

程序

  1. 使用构建器对象构建引擎:
IBuilderConfig* config = builder->createBuilderConfig();
config->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);

构建引擎后,TensorRT会复制权重。
2. 如果使用网络,构建器和解析器,则免除它。

parser->destroy();
network->destroy();
config->destroy();
builder->destroy();

2.3.1 构建器层定时缓存

构建引擎可能很耗时,因为构建者需要为每个层的候选内核计时。为了减少构建者时间,TensorRT设置了一个层时序缓存,以在构建者阶段保留层概要信息。
如果存在其他具有相同输入/输出张量配置和层参数的层,那么TensorRT构建器将跳过概要分析,并将缓存的结果重新用于重复的层。默认情况下,层定时缓存处于打开状态。可以通过设置builder标志将其关闭。

...
config->setFlag(BuilderFlag::kDISABLE_TIMING_CACHE);

2.4 在C ++中序列化模型

在将模型用于推理之前,不一定非要序列化和反序列化模型–如果需要,引擎对象可以直接用于推理。
关于此任务
要进行序列化,您需要将引擎转换为一种格式,以便以后存储和使用以进行推理。为了进行推断,您只需反序列化引擎即可。序列化和反序列化是可选的。由于根据网络定义创建引擎非常耗时,因此您可以避免每次应用程序重新运行时都对其进行序列化并在推理时反序列化来重新构建引擎。因此,在构建引擎之后,用户通常希望对其进行序列化以供以后使用。

注意:序列化引擎不可跨平台或TensorRT 版本移植。引擎特定于它们所构建的确切GPU模型(除了平台和TensorRT版本)。
程序
1.作为先前的离线步骤运行构建器,然后进行序列化:

IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();

2.创建运行时对象以反序列化:

IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

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

2.5 在C ++中执行推理

有了引擎,以下步骤说明了如何在C ++中执行推理。
程序
1.创建一些空间来存储中间激活值。由于引擎保留了网络定义和训练有素的参数,因此需要额外的空间。这些是在执行上下文中保存的:

IExecutionContext *context = engine->createExecutionContext();

引擎可以具有多个执行上下文,从而允许将一组权重用于多个重叠的推理任务。例如,您可以使用一个引擎和每个流一个上下文来处理并行CUDA流中的图像。每个上下文将在与引擎相同的GPU上创建。
有关更多信息,请参见 setBindingDimension()setOptimizationProfile() 用于动态形状模型。
2.使用输入和输出Blob名称获取对应的输入和输出索引:

int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME);

3.使用这些索引,设置一个缓冲区数组,该数组指向GPU上的输入和输出缓冲区:

void* buffers[2];
buffers[inputIndex] = inputBuffer;
buffers[outputIndex] = outputBuffer;

4.TensorRT执行通常是异步的,因此将内核排入CUDA流:

context->enqueueV2(buffers, stream, nullptr);

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

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

有关更多信息,请参阅 enqueue() 用于隐式批处理网络和 enqueueV2()用于显式批处理网络。如果不需要异步,请参见execute()executeV2()

2.6 C ++中的内存管理

TensorRT提供两种机制,以允许对设备内存进行更多控制。
默认情况下,创建 IExecutionContext,分配了持久性设备内存来保存激活数据。为避免这种分配,调用 createExecutionContextWithoutDeviceMemory。然后,应用程序有责任调用 IExecutionContext :: setDeviceMemory()提供运行网络所需的内存。内存块的大小由以下方式返回 ICudaEngine :: getDeviceMemorySize()。

此外,应用程序可以通过实现以下操作来提供自定义分配器,以便在构建和运行时使用: IGpuAllocator 接口。如果您的应用程序希望控制所有GPU内存并向TensorRT进行子分配,而不是直接从CUDA分配TensorRT,这将非常有用。

接口实现后,调用setGpuAllocator(&allocator);
在 iBuilder的 要么 运行时接口。然后,将通过此接口分配和释放所有设备内存。

2.7 Refitting An Engine

TensorRT可以为引擎配上新的砝码,而无需对其进行改造。发动机必须制造为“可改装的”。由于优化引擎的方式,如果更改某些权重,则可能还必须提供其他权重。该界面可以告诉您需要提供哪些其他权重。
程序

  1. 在构建引擎之前,请先请求可改装引擎:
...
config->setFlag(BuilderFlag::kREFIT) 
builder->buildEngineWithConfig(network, config);
  1. 创建一个引用对象:
ICudaEngine* engine = ...;
IRefitter* refitter = createInferRefitter(*engine,gLogger)
  1. 更新您要更新的权重。例如,要更新名为“ MyLayer”的卷积层的内核权重:
Weights newWeights = ...;
refitter->setWeights("MyLayer",WeightsRole::kKERNEL,newWeights);

新权重的计数应与用于制造engine的原始权重的计数相同。

setWeights 如果出现错误(例如layer name错误或role或一个在权重计数的改变),则返回false。

  1. 找出必须提供的其他权重。这通常需要两次致电 IRefitter ::
    getMissing,先获取必须提供的权重对象的数量,其次是获取其层和role。
const int n = refitter->getMissing(0, nullptr, nullptr);
std::vector<const char*> layerNames(n);
std::vector<WeightsRole> weightsRoles(n);
refitter->getMissing(n, layerNames.data(), weightsRoles.data());
  1. 按任何顺序提供缺少的权重:
for (int i = 0; i < n; ++i)
    refitter->setWeights(layerNames[i], weightsRoles[i],Weights{
    
    ...});

仅提供缺少的重量将不会产生任何更多的重量需求。提供任何其他重量可能会触发对更多重量的需求。
6. 使用提供的所有权重更新引擎:

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

如果 成功 是错误的,请检查日志以进行诊断,也许是关于仍然缺少的砝码。

7.销毁refitter:

refitter->destroy();

结果
更新的引擎的行为就像是从使用新权重更新的网络构建的一样。

要查看发动机中所有可改装的配重,请使用 refitter-> getAll(…); 类似于 getMissing 在步骤3中使用。

2.8 算法选择

TensorRT提供了一种机制来控制网络中不同层的算法选择。TensorRT的默认行为是选择全局最小化引擎执行时间的算法。
IAlgorithmSelector
该应用程序可以通过实现以下功能来提供自定义算法选择器,以便在引擎构建期间使用: IAlgorithmSelector接口。接口实现后,请调用:

config.setAlgorithmSelector(&selector);

其中config是将传递给IBuilder :: createBuilderConfig来构建引擎的createBuilderConfig,选择器是从IAlgorithmSelector派生的类的实例。

IAlgorithmSelector :: selectAlgorithms
该方法允许应用程序指导算法选择。 为该方法提供了层的算法上下文以及适用于该上下文的IAlgorithm选项列表。 您可以使用此方法的替代来根据您喜欢的启发式方法指示TensorRT应该考虑哪些选择,或者如果TensorRT应该进行所有选择,则返回所有选择。

从selectAlgorithm返回的选择限制了某些层允许的算法范围。 构建器将使用允许的选择进行全局最小化。 如果没有选择返回,TensorRT将恢复其默认行为。 您可以取消设置BuilderFlag :: kSTRICT_TYPES,以避免此后退,如果覆盖返回空列表,则会出错。 如果替代项返回一个选项,则可以保证使用该替代项。

IAlgorithmSelector :: reportAlgorithms
覆盖reportAlgorithms可用于记录TensorRT对每一层所做的最终选择。 对于给定的优化配置文件,TensorRT在对selectAlgorithms的所有调用之后都会调用reportAlgorithms。 要在较早版本中重放较早版本中的选择,请使方法selectAlgorithms返回与较早版本所报告的方法reportAlgorithms相同的选择,如“生成器中的确定性和可再现性”部分中所述。

注意:

  • 算法选择中的“层”概念与INetworkDefinition中的ILayer不同。 由于融合优化,前者中的“层”可能等效于多个ILayer的聚集。
  • 在selectAlgorithms中选择最快的算法可能无法获得整个网络的最佳性能。TensorRT优化了整个网络的最小时序,可能会偏离本地贪婪的选择,以换取更少的重新格式化开销。
  • 方法reportAlgorithms不提供IAlgorithm的时间和工作空间要求。方法selectAlgorithms可用于查询该信息。
  • IAlgorithmContext和IAlgorithm的顺序,以及每个IAlgorithm的时序,可能对于每个构建都不相同。

2.8.1 构建器中的确定性和可重复性

TensorRT的默认行为是选择全局最小化引擎执行时间的层实现。
一种算法的试用执行时间几乎永远不会相同,但是,如果两种算法具有相似的时序,则同一算法可能不会每次都更快。 因此,即使网络和构建配置未更改,TensorRT选择的算法对于每个构建可能也会有所不同。

但是,算法选择API可用于确定性地构建TensorRT引擎。 有关更多信息,请参见算法选择。 使用方法IAlgorithmSelector :: selectAlgorithms,您可以从选项列表中选择图层的算法。 通过始终返回相同的选择,可以强制该层进行确定性选择。

IAlgorithmSelector还允许您重现相同的实现。 IalgorithmSelector :: reportAlgorithms可以用于缓存TensorRT根据selectAlgorithms设置的默认行为或规则选择的算法。 然后,可以使用selectAlgorithms选择此高速缓存中记录的算法。 如果为layerName,实现,策略和输入/输出格式的每种组合返回相同的算法选择,那么您将始终获得相同的引擎。

sampleAlgorithmSelector类演示如何使用算法选择器在构建器中实现确定性和可重复性。

猜你喜欢

转载自blog.csdn.net/qq_33287871/article/details/113714723