TensorRT学习(二)通过C++使用

  本文源于学习TensorRT文档《TensorRT-Developer-Guide》第2章“WORKING WITH TENSORRT USING THE C++ API”的理解。

一、TensorRT实例化对象

TensorRTAPI
  使用TensorRT进行推理需要创建的对象:

  • IExecutionContext,用于推理的对象,通过ICudaEngine对象获取;
  • ICudaEngine,TensorRT的引擎,通过对模型序列化buildCudaEngine(*network)、读取已经序列化的文件deserializeCudaEngine()两种方式产生,后者在前者基础上通过serialize()获得,因此使用更方便;
  • ILogger,全局变量,在大多数TensorRT的API中都会作为参数使用;
  • IBuilder,通过 createInferBuilder(gLogger) 创建;
  • iNetworkDefinition,在IBuilder对象中调用createNetwork()获取;
  • iParser,以INetwork作为输入来创建的网络解释器,其内部的parse()方法可以读取模型文件,这里要注意的是在Caffe、Uff、ONNX中使用时不同的,具体可看TensorRT的相关示例;
  • IRuntime,通过 createInferRuntime(gLogger) 创建。

  ICudaEngine可以通过IBuilder的buildCudaEngine()、IRuntime的deserializeCudaEngine()产生。CUDA的context建议在IRuntime、IBuilder前创建并配置,因为IRuntime和IBuilder在创建时会使用到context,在context为手动创建时会使用默认的context。

二、创建网络

  使用TensorRT进行推理需要创建TensorRT的网络,有两种方法:

  • 通过iParser序列化已有模型,主要针对Caffe、ONNX、Uff(TensorFlow);
  • 通过TensorRT的API直接定义每一层网络结构,并填充自己训练的参数。

  无论是哪种方法,都需要指定输入输出节点并命名,非输出节点会被优化。网络的权重和模型的优化会被保存在IBuilder的ICudaEngine中,当网络模型是通过iParser创建时,iParser会有内存管理权,因此iParser的释放应在IBuilder运行结束之后。

2.1 通过API定义网络

  sampleMNISTAPI中有关于手动创建每层网络的示例,其过程为:

// 定义IBuilder和iNetworkDefinition
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

// 添加输入层
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H, INPUT_W});

// 添加卷积层(输入节点、strides、权重、偏置)
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});

// 添加池化层
auto pool1 = network->addPooling(*conv1->getOutput(0), PoolingType::kMAX, DimsHW{2, 2});
pool1->setStride(DimsHW{2, 2});

// 添加全连接层和激活层
auto ip1 = network->addFullyConnected(*pool1->getOutput(0), 500, weightMap["ip1filter"], weightMap["ip1bias"]);
auto relu1 = network->addActivation(*ip1->getOutput(0), ActivationType::kRELU);

// 添加Softmax层计算最后的概率并设置节点名称
auto prob = network->addSoftMax(*relu1->getOutput(0));
prob->getOutput(0)->setName(OUTPUT_BLOB_NAME);

// 设定输出层
network->markOutput(*prob->getOutput(0));

2.2 通过Parser载入已有网络模型

  该方法也需要先创建IBuilder、INetworkDefinition,然后创建对应Caffe、UFF、ONNX的iParser,然后读取解析模型。IBuilder是INetworkDefinition的工厂模式需要优先创建,不同的网络框架对应的iParser输出机制不同。

  对于Caffe:

// 定义IBuilder和iNetworkDefinition
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

// 创建Caffe的解析器
ICaffeParser* parser = createCaffeParser();

// 解析已有模型,DataType::kFLOAT替换为DataType::kFLOAT能使用半精度
const IBlobNameToTensor* blobNameToTensor = parser->parse("deploy_file" , "modelFile", *network, DataType::kFLOAT);

// 指定网络输出
for (auto& s : outputs)
	network->markOutput(*blobNameToTensor->find(s.c_str()));

  对于TensorFlow模型使用UFF解析器(另有 TensorFlow-TensorRT 方法):

// 定义IBuilder和iNetworkDefinition
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

// 创建UFF的解析器
IUFFParser* parser = createUffParser();

// 声明网络输入与输出,对TensorFlow输入非CHW顺序的需要先做转换
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");

// 解析已有模型,DataType::kFLOAT替换为DataType::kFLOAT能使用半精度
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);

  对于ONNX:

// 定义IBuilder和iNetworkDefinition
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();

// 创建ONNX的解析器
nvonnxparser::IONNXParser* parser = nvonnxparser::createONNXParser(*network, gLogger);

// 获取模型
parser->parseFromFile(onnx_filename, ILogger::Severity::kWARNING);

三、创建引擎

  因为引擎的优化是针对GPU硬件的,IBuilder需要放到同一个GPU上。IBuilder可以设置网络运行的精度,也可以优化参数使性能最优,其主要的属性是设置最大BatchSize和最大工作空间:

  • maximum batch size,设定TensorRT对一批多大的数据进行优化,运行时可选择较小的批大小;
  • maximum workspace size,限定网络中各层占用存储的最大空间,如果设置的太小,可能导致TensorRT无法实现某些层的优化。
// 创建引擎
builder->setMaxBatchSize(maxBatchSize);
builder->setMaxWorkspaceSize(1 << 20);
ICudaEngine* engine = builder->buildCudaEngine(*network);

// 完成处理后释放
parser->destroy();
network->destroy();
builder->destroy();

四、序列化模型

  上一步建立的引擎可以直接使用,但考虑到创建引擎耗时过长,通常会选择通过序列化创建二进制文件和反序列化读取引擎,需要注意的是序列化后的文件不能跨平台和TensorRT版本使用,因为引擎是针对它们优化的。

// 通过引擎创建序列化文件
IHostMemory *serializedModel = engine->serialize();
// store model to disk
// <…>
serializedModel->destroy();

// 通过RunTime对象反序列化
IRuntime* runtime = createInferRuntime(gLogger);
ICudaEngine* engine = runtime->deserializeCudaEngine(modelData, modelSize, nullptr);

五、推理

// 创建context来开辟空间存储中间值,一个engine可以有多个context来并行处理
IExecutionContext *context = engine->createExecutionContext();

// 指定输入和输出节点名来获取输入输出索引
int inputIndex = engine->getBindingIndex(INPUT_BLOB_NAME);
int outputIndex = engine->getBindingIndex(OUTPUT_BLOB_NAME);

// 设置GPU上的缓存区阵列
void* buffers[2];
buffers[inputIndex] = inputbuffer;
buffers[outputIndex] = outputBuffer;

// 使用enqueue方法对CUDA内核在流上排队,进行异步处理
context->enqueue(batchSize, buffers, stream, nullptr);
// 最后的参数是个可选CUDA事件,用于输入缓存区被使用并可安全复用时发出信号

六、显存管理

  TensorRT有两种设备显存管理机制:

  • 默认方法,直接创建IExecutionContext会分配固定的显存大小来保存激活数据,如果要避免直接分配,可以使用createExecutionContextWithoutDeviceMemory(),然后通过IExecutionContext::setDeviceMemory() 设置网络运行需要的空间,通过ICudaEngine::getDeviceMemorySize()可以获取显存块的大小;
  • IGpuAllocator接口,使用IBuilder或IRuntime的setGpuAllocator(&allocator)来分配和释放空间。

七、重置引擎

  TensorRT的引擎可以通过新的权重参数重置而不需要重新去创建,但引擎必须创建为“refittable”,并且由于优化的原因更改一个参数可能需要连带更新其它的参数。更新的参数可以通过refitter->getAll(...)查看

// 创建引擎前设置为可重置类型的
...
builder->setRefittable(true);
builder->buildCudaEngine(network);

// 创建重置对象
ICudaEngine* engine = ...;
IRefitter* refitter = createInferRefitter(*engine,gLogger)

// 更新参数,以节点名为“MyLayer”的卷积层为例,新参数需要和原参数属性相同
Weights newWeights = ...;
refitter->setWeights("MyLayer",WeightsRole::kKERNEL, newWeights);

// 查找关联参数,先找到关联参数数量,再查找它们的属性
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());

// 按属性补充关联参数,不补充额外的参数不会影响到更多的关联参数
for (int i = 0; i < n; ++i)
	refitter->setWeights(layerNames[i], weightsRoles[i], Weights{...});

// 参数补充完成后,更新引擎,如果失败则需查阅日志看是否缺少参数未补充
bool success = refitter->refitCudaEngine();
assert(success);

// 释放重置器
refitter->destroy();

  

  

  

发布了24 篇原创文章 · 获赞 8 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/yangjf91/article/details/97912773
今日推荐