原创作品,转载时请务必以超链接形式标明文章原始出处: http://www.dapalm.com/?p=199,作者:大数据,怕了么?
本手册为TensorRT 4.0.1.6 GA版英文手册翻译而来,博主英文水平一般般,主要作为备忘所用,分享出来以供更多开发者使用。TensorRT Developer Guide手册一共分为四个章节,主要内容在第二、三章,看懂这两章,写代码够用了。第一章为TensorRT综述,就是自吹有多牛逼。第四章为示例,介绍demo的代码结构及功能。开篇是目录,前三章每章为两到三篇,最后第四章示例,会拆分几个关键示例进行详细说明。
第二章,分别介绍了C++和Python两种语言接口下,TensorRT如何实现推理。下一篇文章再介绍Python接口如何实现推理。
本章主要内容是用户能使用TensorRT实现的目标和任务。假设已有训练好的模型,本章将覆盖使用TensorRT的必要步骤:
‣导入模型创建TensorRT网络定义
‣通过TensorRT网络定义调用TensorRT构建器来创建优化运行时引擎
‣通过序列化和反序列化引擎实现快速重新创建运行时引擎
‣喂数据到引擎并执行推理
在实际使用中更重要的主题是:
‣使用自定义层扩展TensorRT功能
‣混合精度
TensorRT的两种初始化方法:
‣创建IBuilder对象去优化网络(创建后可生成序列化文件)
‣创建IRuntime对象去执行优化网络(从序列化文件导入)
在任何一种情况下,你都必须实现一个日志记录接口,TensorRT通过该接口报告错误,警告和信息性消息。 以下代码显示了如何实现日志记录接口。 在这种用例中,我们已经抑制了信息性消息,只报告警告和错误消息。
class Logger : public ILogger
{
void log(Severity severity, const char* msg) override
{
//不打印信息性消息
if (severity != Severity::kINFO)
std::cout << msg << std::endl;
}
} gLogger;
日志记录接口可用于创建多个运行时和构建器实例,但是日志记录接口是一个单件
(整个应用程序中只有一个类实例且这个实例所占资源在整个应用程序中是共享的),所以你必须为每个对象使用同一个日志记录实例。
创建构建器或运行时时,将创建线程关联的GPU上下文。 虽然默认上下文如果不存在,会自动创建它,但还是建议创建构建器或运行时实例之前,创建和配置CUDA上下文。
使用TensorRT进行推理的第一步是导入你的模型并创建TensorRT网络。 实现此目的的最简单方法是使用TensorRT解析器库导入模型,目前支持以下格式的序列化模型:
‣Caffe
‣ONNX(Pytorch、Caffe2等)
‣UFF(TensorFlow)
另一种方法是使用TensorRT API直接定义模型。 这要求你进行少量(感觉应该是大量调用
)API调用以定义网络图中的每个层,并为模型的训练参数实现自己的导入机制。
在任何一种情况下,你都需要明确告诉TensorRT哪些张量是推理的输出。 未标记为输出的张量被认为是可由构建器优化的瞬态值。 输出张量的数量没有限制,但是将张量标记为输出可能会禁止对张量进行一些优化。 输入和输出张量必须给出名称(ITensor:: setName()
)。 在推理时,你需要为引擎提供一个指向输入和输出缓冲区的指针数组。 为了确定引擎对应的缓冲区指针顺序,你可以使用张量名称进行查询(ICudaEngine::getBindingIndex(const char* name)
)。
TensorRT网络定义的一个重要方面是它包含指向模型权重的指针,这些指针由构建器复制到优化引擎中。 如果网络是通过解析器创建的,则解析器将拥有权重内存的句柄,因此在构建器运行结束之前,不应删除解析器对象。
使用C++解析器API导入模型,需要执行以下主要操作步骤:
1. 创建构建器和网络
2. 针对指定格式,创建相应的解析器
3. 使用解析器解析导入的模型并填充网络。
先创建构建器(作为网络工厂),再创建网络。 不同的解析器具有用于标记网络输出的不同机制。
下面的步骤说明了如何使用c++解析器API导入Caffe模型。更多信息,请参见示例sampleMNIST。
1. 创建构建器和网络
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网络,最后一个参数指示解析器生成fp32权重的网络。 使用DataType :: kHALF将生成fp16权重的网络。除填充网络定义之外,解析器还将返回一个Blob字典,该字典从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()));
导入TensorFlow框架,要求你将TensorFlow模型转换为中间格式UFF(通用框架格式)。 有关转换的更多信息,请参阅将冻结图(FrozenGraph)转换为通用框架格式(UFF)。
1. 创建构建器和网络
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2. 创建UFF解析器
IUFFParser* parser = createUffParser();
3. 声明网络的输入输出
parser->registerInput("Input_0", DimsCHW(1, 28, 28), UffInputOrder::kNCHW);
parser->registerOutput("Binary_3");//如果用uff.from_tensorflow_frozen_model(frozen_file, output_nodes=None, preprocessor=None, **kwargs)转换,输出节点会默认为MarkOutput_0,MarkOutput_1......MarkOutput_N
注:TensorRT默认输入张量是CHW,从TensorFlow(默认NHWC)导入时,确保输入张量也是CHW,如果不是先转换为CHW。
4. 解析模型并填充网络定义
parser->parse(uffFile, *network, nvinfer1::DataType::kFLOAT);
下面的步骤说明如何使用C ++ Parser API导入ONNX模型。 有关ONNX导入的更多信息,请参考示例sampleOnnxMNIST。
1. 创建ONNX解析器。 解析器使用辅助配置管理文件来将输入参数传递给解析器对象:
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. 从模型获取TensorRT网络
nvinfer1::INetworkDefinition* trtNetwork = parser->getTRTNetwork();
你也可以通过TensorRT网络定义API直接定义网络,而不是使用解析器。 此方案假设在网络创建期间,保存主机内存每层权重分别传递给TensorRT。
在下面的示例中,我们将创建一个包含Input
,Convolution
,Pooling
,FullyConnected
,Activation
和SoftMax
层的简单网络。 有关更多信息,请参考示例sampleMNISTAPI。
1. 创建构建器和网络
IBuilder* builder = createInferBuilder(gLogger);
INetworkDefinition* network = builder->createNetwork();
2. 添加输入层,并传入参数(输入维度)。一个网络可以有多个输入。
auto data = network->addInput(INPUT_BLOB_NAME, dt, Dims3{1, INPUT_H,INPUT_W});
3. 添加卷积层,并传入参数(input tensor、strides、weights)
layerName->getOutput(0)
auto conv1 = network->addConvolution(*data->getOutput(0), 20, DimsHW{5, 5}, weightMap["conv1filter"], weightMap["conv1bias"]);
conv1->setStride(DimsHW{1, 1});
4. 添加池化层
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));
下一步是调用TensorRT构建器来创建优化的运行时。 构建器的一个功能是搜索CUDA kernel目录以获得最快的可用实现,因此必须使用相同的GPU来运行优化后的引擎。
构建器具有许多属性,你可以设置这些属性以控制网络运行的精度(一般有fp32、fp16、int8),以及自动调整参数,例如TensorRT在确定哪个实现最快时,需要迭代每个内核多少次(多次迭代会导致更长的运行时间,但是会降低噪声的敏感性)。你还可以查询构建器,以找出硬件本身支持的哪些精度类型。
两个特别重要的属性是最大批大小和最大工作空间大小。
‣最大批大小指定TensorRT将优化的批大小。 在运行时,可以选择较小的批大小。
‣层算法通常需要临时工作空间。 此参数限制网络中任何层可以使用的最大空间大小。 如果提供的空间(scratch)不足,则TensorRT可能无法搜索到给定层的优化实现。
1. 使用构建器实例构建引擎:
在构建引擎时,TensorRT会复制权重。
2. 如果需要使用,请先分配构建器、网络和解析器
构建需要一些时间,因此一旦构建了引擎,你通常需要序列化引擎以供后后续使用。 在将模型用于推理之前,对模型进行序列化和反序列化不是必要的 - 如果需要,可以直接使用引擎实例进行推理。
注:序列化引擎不能跨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);
最后一个参数nullptr是使用自定义图层的应用程序的插件层工厂。 有关更多信息,请参考使用自定义图层扩展TensorRT。
一旦你有了引擎,就能执行推理了。
1. 创建空间存储中间激活值。由于引擎包含网络定义和训练参数,因此需要额外的空间。这些都保存在执行上下文中:
IExecutionContext * context = engine-> createExecutionContext();
引擎可以具有多个执行上下文(每个上下文异步执行一路
),允许一组权重用于多个并行推理任务。例如,你可以使用一个引擎和一个上下文(每个CUDA流异步执行一路
),上下文使用多个并行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流异步执行核函数kernel:
context.enqueue(batchSize,buffers,stream,nullptr);
通常先从主机内存异步拷贝数据到GPU显存(前面提到的输入缓冲区),之后enqueue函数将执行核函数。 enqueue()
的最后一个参数是一个可选的CUDA事件,当输入缓冲区的数据已经消费并且可以安全地重用这片显存时,它将被发出信号(异步中断系统中很有用,即生产者-消费者模型)。要确定核函数以及异步拷贝何时完成,请使用标准CUDA同步机制(如事件
)或等待流
。
TensorRT提供两种机制,允许应用程序对设备内存进行更多控制。
默认情况下,在创建IExecutionContext
时,会分配保存激活数据的GPU设备内存。 要避免此分配,可调用ICudaEngine::createExecutionContextWithoutDeviceMemory
。 然后应用程序负责调用IExecutionContext::setDeviceMemory()
来提供运行网络所需的内存。 ICudaEngine::getDeviceMemorySize()
返回内存块的大小。
此外,应用程序可以通过实现IGpuAllocator接口提供在构建和运行时使用的自定义分配器。 实现接口后,调用
setGpuAllocator(&allocator);
然后在IBuilder或IRuntime接口上,所有的设备内存申请释放都由IGpuAllocator接口。(没研究手动分配有什么优势,TensorRT瓶颈其实是在GPU计算资源不足,而不在显存
)