前言:
- tensorrt的工作流程如下图:
- - 首先定义网络
- - 优化builder参数
- - 通过builder生成engine,用于模型保存、推理等
- - engine可以通过序列化和逆序列化转化模型数据类型(转化为二进制byte文件,加快传输速率),再进一步推动模型由输入张量到输出张量的推理。![avatar](./figure/2.tensortr-workflow.jpg)
code structure:
code structure
- 1. 定义 builder, config 和network,其中builder表示所创建的构建器,config表示创建的构建配置(指定TensorRT应该如何优化模型),network为创建的网络定义。
- 2. 输入,模型结构和输出的基本信息
- 1. 生成engine模型文件
- 2. 序列化模型文件并存储
- - [官方文档参考部分 C++ API](https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#:~:text=the%20Polygraphy%20repository.-,3.%C2%A0The%20C%2B%2B%20API,-This%20chapter%20illustrates)
头文件和定义log类
// tensorRT include
#include <NvInfer.h>
#include <NvInferRuntime.h>
// cuda include
#include <cuda_runtime.h>
// system include
#include <stdio.h>
//第一件事,log类,获得msg。然后可以根据级别决定要不要输出
class TRTLogger : public nvinfer1::ILogger{
public:
virtual void log(Severity severity, nvinfer1::AsciiChar const* msg) noexcept override{
if(severity <= Severity::kVERBOSE){
printf("%d: %s\n", severity, msg);
}
}
};
第一件事,log类,获得msg。然后可以根据级别决定要不要输出
定义配置
// ----------------------------- 1. 定义 builder, config 和network -----------------------------
// 这是基本需要的组件
//形象的理解是你需要一个builder去build这个网络,网络自身有结构,这个结构可以有不同的配置
nvinfer1::IBuilder* builder = nvinfer1::createInferBuilder(logger);
// 创建一个构建配置,指定TensorRT应该如何优化模型,tensorRT生成的模型只能在特定配置下运行
nvinfer1::IBuilderConfig* config = builder->createBuilderConfig();
// 创建网络定义,其中createNetworkV2(1)表示采用显性batch size,新版tensorRT(>=7.0)时,不建议采用0非显性batch size
// 因此贯穿以后,请都采用createNetworkV2(1)而非createNetworkV2(0)或者createNetwork
nvinfer1::INetworkDefinition* network = builder->createNetworkV2(1);
// 构建一个模型
/*
Network definition:
image
|
linear (fully connected) input = 3, output = 2, bias = True w=[[1.0, 2.0, 0.5], [0.1, 0.2, 0.5]], b=[0.3, 0.8]
|
sigmoid
|
prob 期望的输出
*/
输入输出,模型的基本信息
// ----------------------------- 2. 输入,模型结构和输出的基本信息 -----------------------------
const int num_input = 3; // in_channel
const int num_output = 2; // out_channel
float layer1_weight_values[] = {1.0, 2.0, 0.5, 0.1, 0.2, 0.5}; // 前3个给w1的rgb,后3个给w2的rgb
float layer1_bias_values[] = {0.3, 0.8};
//输入指定数据的名称、数据类型和完整维度,将输入层添加到网络
nvinfer1::ITensor* input = network->addInput("image", nvinfer1::DataType::kFLOAT, nvinfer1::Dims4(1, num_input, 1, 1));
nvinfer1::Weights layer1_weight = make_weights(layer1_weight_values, 6);
nvinfer1::Weights layer1_bias = make_weights(layer1_bias_values, 2);
//添加全连接层 pytorch 叫linear
auto layer1 = network->addFullyConnected(*input, num_output, layer1_weight, layer1_bias); // 注意对input进行了解引用
//添加激活层
auto prob = network->addActivation(*layer1->getOutput(0), nvinfer1::ActivationType::kSIGMOID); // 注意更严谨的写法是*(layer1->getOutput(0)) 即对getOutput返回的指针进行解引用
// 将我们需要的prob标记为输出
network->markOutput(*prob->getOutput(0));
printf("Workspace Size = %.2f MB\n", (1 << 28) / 1024.0f / 1024.0f); // 256Mib
config->setMaxWorkspaceSize(1 << 28);
builder->setMaxBatchSize(1); // 推理时 batchSize = 1
输入通道为3,输出通道为2
将image读为nvinfer1::ITensor的格式(1、3、1、1)的tensor
将weight和bias读为nvinfer1::Weights的格式
利用addFullyConnected创建全链接层。
添加激活层addActivation,这一层的input就是layer1层的输出,激活函数选用kSIGMOID。
得到prob这个layer,用markoutput标记为最终的输出。
生成模型
// ----------------------------- 3. 生成engine模型文件 -----------------------------
//TensorRT 7.1.0版本已弃用buildCudaEngine方法,统一使用buildEngineWithConfig方法
nvinfer1::ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
if(engine == nullptr){
printf("Build engine failed.\n");
return -1;
}
builder->buildEngineWithConfig这个过程就是合并算子,查找特定支持,各种优化等。
如果失败了,那engine就为空指针。
存储文件
// ----------------------------- 4. 序列化模型文件并存储 -----------------------------
// 将模型序列化,并储存为文件
nvinfer1::IHostMemory* model_data = engine->serialize();
FILE* f = fopen("engine.trtmodel", "wb");
fwrite(model_data->data(), 1, model_data->size(), f);
fclose(f);
这段代码是将TensorRT引擎序列化为二进制文件的过程。
首先,engine->serialize()
用于将TensorRT引擎对象engine
序列化为一块内存。返回的值是IHostMemory
类型指针,它包含了序列化后的模型数据和序列化后的模型大小。
接下来,通过打开一个文件指针f
,使用fopen
函数创建一个名为"engine.trtmodel"的二进制文件,以进行写入操作。
最后,使用fwrite
函数将model_data
中的模型数据写入到文件中。model_data->data()
表示获取模型数据的起始地址,model_data->size()
表示模型数据的大小(以字节为单位),1
表示每次写入一个字节。
拿到engine这个实例过后,要将其存储为文件,这时候就可以通过serialize()方法对其序列化进行存储,得到IHostMemory类型的文件。
到这里主要流程就结束拉!
释放
// 卸载顺序按照构建顺序倒序
model_data->destroy();
engine->destroy();
network->destroy();
config->destroy();
builder->destroy();
printf("Done.\n");
return 0;
一定要按照反顺序来做!
重点总结:
- 必须使用createNetworkV2,并指定为1(表示显性batch)。createNetwork已经废弃,非显性batch官方不推荐。这个方式直接影响推理时enqueue还是enqueueV2
- builder、config等指针,记得释放,否则会有内存泄漏,使用ptr->destroy()释放
- markOutput表示是该模型的输出节点,mark几次,就有几个输出,addInput几次就有几个输入。这与推理时相呼应。只保存输入输出不保存中间的内容,这也是为了节省内存。
- workspaceSize是工作空间大小,某些layer需要使用额外存储时,不会自己分配空间,而是为了内存复用,直接找tensorRT要workspace空间。指的这个意思。比如一个layer出现了三次,如果每一次都要给他分配一块内存的话,那就会占用很大的空间,就浪费掉了,因为别人又不能用。所以实际上他不需要这样做,有需要直接去trt要woorkspace空间就可以了。这样内存就可以复用。
- 保存的模型只能适配编译时的trt版本、编译时指定的设备。也只能保证在这种配置下是最优的。如果用trt跨不同设备执行,有时候可以运行,但不是最优的,也不推荐