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的更多信息,请参阅 第三章。
2.1 用C++实例化TensorRT对象
要运行推理,您需要使用IExecutionContext对象。要创建IExecutionContext类型的对象,首先需要创建ICudaEngine类型的对象(引擎)。
可以通过以下两种方式之一创建引擎:
- 通过用户模型的网络定义进行创建。在这种情况下,可以选择将引擎序列化并保存以供以后使用。
- 通过从磁盘读取序列化引擎。在这种情况下,性能更好,因为绕过了解析模型和创建中间对象的步骤。
需要全局创建iLogger类型的对象。它用作TensorRT API的各种方法的参数。此处显示了一个演示logger创建的简单示例:
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类型的对象,如图5所示。有关更多信息,请参阅IBuilder类引用。
图 5 使用iLogger作为输入参数创建iBuilder
为iBuilder定义的名为createNetwork的方法用于创建iNetworkDefinition类型的对象,如图6所示。
图 6 createNetwork()函数用于创建网络
使用iNetwork定义作为输入创建一个可用的解析器:
ONNX: parser = nvonnxparser::createParser(*network, gLogger);
NVCaffe: ICaffeParser* parser = createCaffeParser();
UFF: parser = createUffParser();
调用来自iParser类型的对象的名为parse()的方法来读取模型文件并填充TensorRT网络。
图 7 解析模型文件
调用iBuilder的一个名为buildCudaEngine()的方法来创建一个iCudaEngine类型的对象,如图8所示:
图 8 创建TensorRT引擎
可以选择将引擎序列化并转储到文件中。
图 9 将引擎序列化到文件中
创建执行上下文用于执行推理。
图 10 创建执行上下文
如果序列化引擎被保留并保存到文件中,则可以绕过上述大多数步骤。
名为createInferRuntime(gLogger)的全局TensorRT API方法用于创建iRuntime类型的对象,如图11所示:
图 11 创建TensorRT runtime
有关TensorRT运行时的更多信息,请参阅IRuntime类引用。通过调用runtime的deserializeCudaEngine()函数来创建引擎。
对于这两种使用模型,其余推理过程是相同的。
尽管可以避免创建CUDA上下文(将为您创建默认上下文),但这是不可取的。建议在创建runtime或builder对象之前创建和配置CUDA上下文。
将使用与创建线程关联的GPU上下文创建builder或runtime。虽然如果缺省上下文尚不存在,但会创建它,但建议在创建runtime或builder对象之前创建和配置CUDA上下文。
2.2 在C ++中创建网络定义
使用TensorRT进行推理的第一步是从您的模型创建TensorRT网络。实现此目的的最简单方法是使用TensorRT解析器库导入模型,该解析器库支持以下格式的序列化模型:
- sampleMNIST (both BVLC and NVCaffe)
- sampleOnnxMNIST
- sampleUffMNIST (used for TensorFlow)
另一种方法是使用TensorRT API直接定义模型。这要求您进行少量API调用以定义网络图中的每个层,并为模型的训练参数实现自己的导入机制。
在任何一种情况下,您都明确需要告诉TensorRT需要哪些张量作为推理的输出。未标记为输出的张量被认为是可由builder优化的瞬态值。输出张量的数量没有限制,但是,将张量标记为输出可能会禁止对张量进行一些优化。输入和输出张量也必须给出名称(使用ITensor :: setName())。在推理时,您将为引擎提供一个指向输入和输出缓冲区的指针数组。为了确定引擎对这些指针的预期顺序,您可以使用张量名称进行查询。
TensorRT网络定义的一个重要方面是它包含指向模型权重的指针,这些指针由builder复制到优化引擎中。如果网络是通过parser创建的,则parser将拥有权重占用的内存,因此在builder运行之前,不应删除parser对象。
2.2.1 在c++中从头创建一个模型定义
您也可以通过网络定义API直接将网络定义到TensorRT,而不是使用parser。此方案假定在网络创建期间,每层权重已在主机内存中准备好传递给TensorRT。
在下面的示例中,我们将创建一个包含Input,Convolution,Pooling,FullyConnected,Activation和SoftMax图层的简单网络。要查看整体中的代码,请参阅位于/ usr / src / tensorrt / samples / sampleMNISTAPI目录中的sampleMNISTAPI。
1.创建builder和network:
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2.使用输入维将“输入”层添加到网络。网络可以有多个输入,但在此示例中只有一个:
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H,
INPUT_W});
3.添加具有隐藏层输入节点、过滤器和偏差的步幅和权重的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中的权重目前还存储在内存中。
4.添加Pooling层:
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX,
DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});
5.添加全连接层和激活函数:
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500,
weightMap["ip1filter"], weightMap["ip1bias"]);
auto relu1 = network->addActivation(*ip1->getOutput(0),
ActivationType::kRELU);
6.添加Softmax层计算最终的概率并将其设置为输出:
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
7.标记输出:
network->markOutput(*prob->getOutput(0));
整体代码:
#include "NvCaffeParser.h"
#include "NvInfer.h"
#include "common.h"
#include "cuda_runtime_api.h"
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstring>
#include <ctime>
#include <fstream>
#include <iostream>
#include <map>
#include <sstream>
#ifdef _MSC_VER
#include <direct.h>
#else
#include <sys/stat.h>
#endif
#include <vector>
// stuff we know about the network and the input/output blobs
static const int INPUT_H = 28;
static const int INPUT_W = 28;
static const int OUTPUT_SIZE = 10;
static int gUseDLACore{-1};
const char* INPUT_BLOB_NAME = "data";
const char* OUTPUT_BLOB_NAME = "prob";
using namespace nvinfer1;
using namespace nvcaffeparser1;
static Logger gLogger;
// Load weights from files shared with TensorRT samples.
// TensorRT weight files have a simple space delimited format:
// [type] [size] <data x size in hex>
std::map<std::string, Weights> loadWeights(const std::string file)
{
std::cout << "Loading weights: " << file << std::endl;
std::map<std::string, Weights> weightMap;
// Open weights file
std::ifstream input(file);
assert(input.is_open() && "Unable to load weight file.");
// Read number of weight blobs
int32_t count;
input >> count;
assert(count > 0 && "Invalid weight map file.");
while (count--)
{
Weights wt{DataType::kFLOAT, nullptr, 0};
uint32_t type, size;
// Read name and type of blob
std::string name;
input >> name >> std::dec >> type >> size;
wt.type = static_cast<DataType>(type);
// Load blob
if (wt.type == DataType::kFLOAT)
{
uint32_t* val = reinterpret_cast<uint32_t*>(malloc(sizeof(val) * size));
for (uint32_t x = 0, y = size; x < y; ++x)
{
input >> std::hex >> val[x];
}
wt.values = val;
}
else if (wt.type == DataType::kHALF)
{
uint16_t* val = reinterpret_cast<uint16_t*>(malloc(sizeof(val) * size));
for (uint32_t x = 0, y = size; x < y; ++x)
{
input >> std::hex >> val[x];
}
wt.values = val;
}
wt.count = size;
weightMap[name] = wt;
}
return weightMap;
}
// We have the data files located in a specific directory. This
// searches for that directory format from the current directory.
std::string locateFile(const std::string& input)
{
std::vector<std::string> dirs{"data/samples/mnist/", "data/mnist/"};
return locateFile(input, dirs);
}
// simple PGM (portable greyscale map) reader
void readPGMFile(const std::string& filename, uint8_t buffer[INPUT_H * INPUT_W])
{
readPGMFile(locateFile(filename), buffer, INPUT_H, INPUT_W);
}
// Creat the engine using only the API and not any parser.
ICudaEngine* createMNISTEngine(unsigned int maxBatchSize, IBuilder* builder, DataType dt)
{
INetworkDefinition* network = builder->createNetwork();
// Create input tensor of shape { 1, 1, 28, 28 } with name INPUT_BLOB_NAME
ITensor* data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W});
assert(data);
// Create scale layer with default power/shift and specified scale parameter.
const float scaleParam = 0.0125f;
const Weights power{DataType::kFLOAT, nullptr, 0};
const Weights shift{DataType::kFLOAT, nullptr, 0};
const Weights scale{DataType::kFLOAT, &scaleParam, 1};
IScaleLayer* scale_1 = network->addScale(*data, ScaleMode::kUNIFORM, shift, scale, power);
assert(scale_1);
// Add convolution layer with 20 outputs and a 5x5 filter.
std::map<std::string, Weights> weightMap = loadWeights(locateFile("mnistapi.wts"));
IConvolutionLayer* conv1 = network->addConvolution(*scale_1->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
assert(conv1);
conv1->setStride(DimsHW{1, 1});
// Add max pooling layer with stride of 2x2 and kernel size of 2x2.
IPoolingLayer* pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
assert(pool1);
pool1->setStride(DimsHW{2, 2});
// Add second convolution layer with 50 outputs and a 5x5 filter.
IConvolutionLayer* conv2 = network->addConvolution(*pool1->getOutput(0), 50, DimsHW{5, 5}, weightMap["conv2filter"], weightMap["conv2bias"]);
assert(conv2);
conv2->setStride(DimsHW{1, 1});
// Add second max pooling layer with stride of 2x2 and kernel size of 2x3>
IPoolingLayer* pool2 = network->addPooling(*conv2->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
assert(pool2);
pool2->setStride(DimsHW{2, 2});
// Add fully connected layer with 500 outputs.
IFullyConnectedLayer* ip1 = network->addFullyConnected(*pool2->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]);
assert(ip1);
// Add activation layer using the ReLU algorithm.
IActivationLayer* relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);
assert(relu1);
// Add second fully connected layer with 20 outputs.
IFullyConnectedLayer* ip2 = network->addFullyConnected(*relu1->getOutput(0), OUTPUT_SIZE, weightMap["ip2filter"], weightMap["ip2bias"]);
assert(ip2);
// Add softmax layer to determine the probability.
ISoftMaxLayer* prob = network->addSoftMax(*ip2->getOutput(0));
assert(prob);
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);
network->markOutput(*prob->getOutput(0));
// Build engine
builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
samplesCommon::enableDLA(builder, gUseDLACore);
ICudaEngine* engine = builder->buildCudaEngine(*network);
// Don't need the network any more
network->destroy();
// Release host memory
for (auto& mem : weightMap)
{
free((void*) (mem.second.values));
}
return engine;
}
void APIToModel(unsigned int maxBatchSize, IHostMemory** modelStream)
{
// Create builder
IBuilder* builder = createInferBuilder(gLogger);
// Create model to populate the network, then set the outputs and create an engine
ICudaEngine* engine = createMNISTEngine(maxBatchSize, builder, DataType::kFLOAT);
assert(engine != nullptr);
// Serialize the engine
(*modelStream) = engine->serialize();
// Close everything down
engine->destroy();
builder->destroy();
}
void doInference(IExecutionContext& context, float* input, float* output, int batchSize)
{
const ICudaEngine& engine = context.getEngine();
// Pointers to input and output device buffers to pass to engine.
// Engine requires exactly IEngine::getNbBindings() number of buffers.
assert(engine.getNbBindings() == 2);
void* buffers[2];
// In order to bind the buffers, we need to know the names of the input and output tensors.
// Note that indices are guaranteed to be less than IEngine::getNbBindings()
const int inputIndex = engine.getBindingIndex(INPUT_BLOB_NAME);
const int outputIndex = engine.getBindingIndex(OUTPUT_BLOB_NAME);
// Create GPU buffers on device
CHECK(cudaMalloc(&buffers[inputIndex], batchSize * INPUT_H * INPUT_W * sizeof(float)));
CHECK(cudaMalloc(&buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float)));
// Create stream
cudaStream_t stream;
CHECK(cudaStreamCreate(&stream));
// DMA input batch data to device, infer on the batch asynchronously, and DMA output back to host
CHECK(cudaMemcpyAsync(buffers[inputIndex], input, batchSize * INPUT_H * INPUT_W * sizeof(float), cudaMemcpyHostToDevice, stream));
context.enqueue(batchSize, buffers, stream, nullptr);
CHECK(cudaMemcpyAsync(output, buffers[outputIndex], batchSize * OUTPUT_SIZE * sizeof(float), cudaMemcpyDeviceToHost, stream));
cudaStreamSynchronize(stream);
// Release stream and buffers
cudaStreamDestroy(stream);
CHECK(cudaFree(buffers[inputIndex]));
CHECK(cudaFree(buffers[outputIndex]));
}
int main(int argc, char** argv)
{
gUseDLACore = samplesCommon::parseDLA(argc, argv);
// create a model using the API directly and serialize it to a stream
IHostMemory* modelStream{nullptr};
APIToModel(1, &modelStream);
assert(modelStream != nullptr);
// Read random digit file
srand(unsigned(time(nullptr)));
uint8_t fileData[INPUT_H * INPUT_W];
const int num = rand() % 10;
readPGMFile(std::to_string(num) + ".pgm", fileData);
// Print ASCII representation of digit image
std::cout << "\nInput:\n"
<< std::endl;
for (int i = 0; i < INPUT_H * INPUT_W; i++)
std::cout << (" .:-=+*#%@"[fileData[i] / 26]) << (((i + 1) % INPUT_W) ? "" : "\n");
// Parse mean file
ICaffeParser* parser = createCaffeParser();
assert(parser != nullptr);
IBinaryProtoBlob* meanBlob = parser->parseBinaryProto(locateFile("mnist_mean.binaryproto").c_str());
parser->destroy();
const float* meanData = reinterpret_cast<const float*>(meanBlob->getData());
// Subtract mean from image
float data[INPUT_H * INPUT_W];
for (int i = 0; i < INPUT_H * INPUT_W; i++)
data[i] = float(fileData[i]) - meanData[i];
meanBlob->destroy();
IRuntime* runtime = createInferRuntime(gLogger);
assert(runtime != nullptr);
if (gUseDLACore >= 0)
{
runtime->setDLACore(gUseDLACore);
}
ICudaEngine* engine = runtime->deserializeCudaEngine(modelStream->data(), modelStream->size(), nullptr);
assert(engine != nullptr);
modelStream->destroy();
IExecutionContext* context = engine->createExecutionContext();
assert(context != nullptr);
// Run inference
float prob[OUTPUT_SIZE];
doInference(*context, data, prob, 1);
// Destroy the engine
context->destroy();
engine->destroy();
runtime->destroy();
// Print histogram of the output distribution
std::cout << "\nOutput:\n\n";
float val{0.0f};
int idx{0};
for (unsigned int i = 0; i < 10; i++)
{
val = std::max(val, prob[i]);
if (val == prob[i])
idx = i;
std::cout << i << ": " << std::string(int(std::floor(prob[i] * 10 + 0.5f)), '*') << "\n";
}
std::cout << std::endl;
system("pause");
return (idx == num && val > 0.9f) ? EXIT_SUCCESS : EXIT_FAILURE;
}
2.2.2 使用c++ parser导入模型
要使用C ++ Parser API导入模型,您需要执行以下高级步骤:
1.创建TensorRT builder和network。
IBuilder* builder = createInferBuilder(gLogger);
nvinfer1::INetworkDefinition* network = builder->createNetwork();
有关如何创建logger的示例,请参阅 在C ++中实例化TensorRT对象。
2.为特定格式创建TensorRT解析器。
ONNX:
auto parser = nvonnxparser::createParser(*network,
gLogger);
UFF:
auto parser = createUffParser();
NVCaffe:
ICaffeParser* parser = createCaffeParser();
3.使用解析器解析导入的模型并填充网络。
parser->parse(args);
具体的args取决于使用什么格式的解析器。有关更多信息,请参阅TensorRT API中记录的解析器。
必须在创建network之前创建builder,因为它充当network的工厂。不同的解析器具有用于标记网络输出的不同机制。
2.2.3 使用c++ 解析器API导入caffe模型
以下步骤说明了如何使用C ++ Parser API导入Caffe模型。有关更多信息,请参阅sampleMNIST。
1.创建builder和network。
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2.创建caffe解析器。
ICaffeParser* parser = createCaffeParser();
3.解析导入的模型。
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解析器导入Tensorflow模型
对于新项目,建议使用TensorFlow-TensorRT集成作为转换TensorFlow网络以使用TensorRT进行推理的方法。有关集成说明,请参阅将TensorFlow与TensorRT及其发行说明集成。
从TensorFlow框架导入需要您将TensorFlow模型转换为中间格式UFF(通用框架格式)。有关转换的更多信息,请参阅8.2.3节将冻结图转换为UFF。
以下步骤说明了如何使用C ++ Parser API导入TensorFlow模型。有关UFF导入的更多信息,请参阅sampleUffMNIST。
1.创建 builder 和 network。
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2.创建 UFF 解析器。
IUFFParser* parser = createUffParser();
3.为UFF解析器指定网络的输入和输出。
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");
TensorRT期望输入张量为CHW顺序。从TensorFlow导入时,请确保输入张量符合所需顺序,如果不是,请将其转换为CHW。
4.解析导入的模型以填充网络。
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);
2.2.5 使用C++解析API导入ONNX模型
**限制:**由于ONNX格式正在快速开发,因此您可能会遇到模型版本和解析器版本之间的版本不匹配。 TensorRT 5.0.0附带的ONNX Parser支持ONNX IR(中间表示)版本0.0.3,opset版本7。
通常,较新版本的ONNX Parser旨在向后兼容,因此,遇到早期版本的ONNX导出器生成的模型文件不应该导致问题。当更改不向后兼容时,可能会有一些例外情况。在这种情况下,将早期的ONNX模型文件转换为以后支持的版本。有关此主题的更多信息,请参阅ONNX Model Opset版本转换器。
用户模型也可能是由支持后续opset的导出工具生成的,而不是由TensorRT附带的ONNX解析器支持的。在这种情况下,请检查发布到GitHub onnxtensorrt的最新版本的TensorRT是否支持所需的版本。支持的版本由onnx_trt_backend.cpp中的BACKEND_OPSET_VERSION变量定义。从GitHub下载并构建最新版本的ONNX TensorRT Parser。有关构建的说明,请访问:ONNX的TensorRT后端。
以下步骤说明了如何使用C ++ Parser API导入ONNX模型。有关ONNX导入的更多信息,请参阅sampleOnnxMNIST。
1.创建ONNX解析器。解析器使用辅助配置管理SampleConfig对象将输入参数从示例可执行文件传递到解析器对象:
nvonnxparser::IOnnxConfig* config = nvonnxparser::createONNXConfig();
//Create Parser
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*config);
2.输入模型:
parser->parse(onnx_filename, DataType::kFLOAT);
3.将模型转换为TensorRT网络:
parser->convertToTRTNetwork();
4.从模型获取网络:
nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();
2.3 使用C++创建引擎
下一步是调用TensorRT构建器来创建优化的运行时。builder的一个功能是搜索其CUDA内核目录以获得最快的可用实现,因此必须使用相同的GPU来构建优化引擎将运行的GPU。
builder具有许多属性,您可以设置这些属性以控制网络应运行的精度,以及自动调整参数,例如TensorRT在确定哪个最快时(多次迭代导致更长的运行时间)应该为每个内核计时多少次,同时对噪音的敏感性较小。)您还可以查询builder以查找硬件支持的精简类型。
两个特别重要的属性是最大批量(batch)大小和最大工作空间(workspace)大小。
- 最大批量大小指定TensorRT将优化的批量大小。在运行时,可以选择较小的批量大小。
- 层算法通常需要临时工作空间。此参数限制网络中任何层可以使用的最大大小。如果提供的scratch不足,则TensorRT可能无法找到给定层的实现。
1.使用builder对象创建引擎:
builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildCudaEngine(*network);
在构建引擎时,TensorRT会复制权重。
2.析构network、builder和parser:
engine->destroy();
network->destroy();
builder->destroy();
2.4 使用C++序列化模型
要进行序列化,您要将引擎转换为一种格式,以便以后存储和使用以进行推理。要用于推理,您只需反序列化引擎即可。序列化和反序列化是可选的。由于从网络定义创建引擎可能非常耗时,因此每次应用程序重新运行时都可以通过序列化一次并在推理时对其进行反序列化来避免重建引擎。因此,在构建引擎之后,用户通常希望将其序列化以供以后使用。
构建可能需要一些时间,因此一旦构建了引擎,您通常需要将其序列化以供以后使用。在将模型用于推理之前,并非绝对有必要对模型进行序列化和反序列化 - 如果需要,可以直接使用引擎对象进行推理。
序列化引擎不能跨平台或TensorRT版本移植。引擎特定于它们构建的精确GPU模型(以及平台和TensorRT版本)。
1.将builder作为先前的脱机步骤运行,然后序列化:
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上创建。
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流中enqueue内核:
context.enqueue(batchSize, buffers, stream, nullptr);
如果数据尚未存在,通常在内核从GPU移动数据的之前或之后进行异步memcpy()。enqueue()的最后一个参数是一个可选的CUDA事件,当输入缓冲区被消耗并且它们的内存可以安全地重用时,它将被发出信号。
要确定内核(以及可能的memcpy())何时完成,请使用标准CUDA同步机制(如事件)或等待流。
2.6 C++接口进行内存管理
TensorRT提供了两种机制,允许应用程序更好地控制设备内存。
默认情况下,在创建IExecutionContext时,会分配持久设备内存来保存激活数据。要避免此分配,请调用createExecutionContextWithoutDeviceMemory。然后应用程序负责调用IExecutionContext :: setDeviceMemory()来提供运行网络所需的内存。内存块的大小由ICudaEngine :: getDeviceMemorySize()返回。
此外,应用程序可以通过实现IGpuAllocator接口提供在构建和运行时使用的自定义分配器。实现接口后,调用setGpuAllocator(&allocator);
在IBuilder或IRuntime接口上。然后,将通过此接口分配和释放所有设备内存。