读代码学习TensorRT
读代码学习TensorRT
代码仓库: https://github.com/wang-xinyu/tensorrtx/tree/yolov5-v4.0/yolov5s
TensorRT 7.0 documentation: https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-700/tensorrt-api/c_api/namespacemembers_func.html
Cuda documentation: https://docs.nvidia.com/cuda/cuda-runtime-api/modules.html#modules
TensorRT 做的工作
- 构建期
- 模型解析/建立
- 计算图优化
- 节点消除
- 多精度支持
- 优选kernel/format
- 导入plugin
- 显存优化
- 运行期
- 运行时环境
- 序列化反序列化
版本
- GTX1080 / Ubuntu16.04 / cuda10.0 / cudnn7.6.5 / tensorrt7.0.0 / nvinfer7.0.0 / opencv3.3
- Yolov5 v4.0
TensorRTX由于基于TensorRT7.0,与最新的8.5.2的API有较大不同
7.0 https://docs.nvidia.com/deeplearning/tensorrt/archives/tensorrt-700/tensorrt-api/c_api/index.html
latest(8.5.2 now) https://docs.nvidia.com/deeplearning/tensorrt/api/c_api/index.html
yolov5.cpp
首先是宏定义常量:
1 |
其中,USE_FP16用于指定量化精度;DEVICE用于指定GPU id,在单显卡状态下默认为0;NMS_THRESH是NMS算法中的筛选阈值;CONF_THRESH是置信度confidence筛选阈值;BATCH_SIZE。
1 | // stuff we know about the network and the input/output blobs |
通过const变量定义了一些常量。其中INPUT_H和INPUT_W指定了输入的尺寸;CLASS_NUM指定了输出标签的种类数量;OUTPUT_SIZE??。
从main()函数开始看起:
1 | int main(int argc, char** argv) { |
接下来先看看APIToModel()
1 | /** |
IBuilder是TensorRT标准类,作用是"Builds an engine from a network definition. ",需要#include <NvInfer.h>
。
createInferBuilder是tensorrt7.0的接口,8.5.2中已取消。作用是"Create an instance of an IBuilder class.“,返回的对象是"This class is the logging class for the builder.”。传入参数需要为ILogger对象,是"Application-implemented logging interface for the builder, engine and runtime",用于生成器、引擎和运行时的应用程序实现的日志记录接口。在TensorRTX中作者继承ILogger类实现了Logger类(在logging.h中),因此输入的是Logger对象。
builder->createBuilderConfig()的描述为"Create a builder configuration object.",即只进行engine的某些配置,不参与网络结构定义。
ICudaEngine是TensorRT运行时推理引擎,描述为"An engine for executing inference on a built network, with functionally unsafe features."。作者在函数build_engine通过包装原生API实现的网络各层堆叠生成engine。
接下来看build_engine()
1 | ICudaEngine* build_engine(unsigned int maxBatchSize, IBuilder* builder, IBuilderConfig* config, DataType dt, float& gd, float& gw, std::string& wts_name) { |
输入参数有Builder,BuilderConfig,然后在第一行又通过createNetworkV2()创建了一个network对象。createNetworkV2()的作用是"Create a network definition object.",即network对象的主要作用为定义网络结构。同时有另一个函数createNetwork()主要用于兼容早期版本的TensorRT。createNetwork()与createNetworkV2()最主要的不同为CreateNetworkV2支持动态形状dynamic shapes 和显式批处理维度explicit batch sizes。
1 | // Create input tensor of shape {3, INPUT_H, INPUT_W} with name INPUT_BLOB_NAME |
这段主要在定义输入张量大小和加载权重。
network->addInput()的描述为:
"输入张量的名称name用于查找从网络构建的引擎的缓冲区数组中的索引。尺寸的体积必须小于2^30个元素。对于具有隐式批次维度的网络,此卷包括长度设置为最大批次大小的批次维度。对于具有所有显式维度和通配符维度的网络,体积基于IOptimizationProfile指定的最大值。维度通常为正整数。例外的是,在具有所有显式维度的网络中,-1可以用作在运行时指定维度的通配符。具有此类通配符的输入张量必须在IOptimizationProfiles中具有相应的条目,指示允许的极值,并且输入维度必须由IExecutionContext:setBindingDimensions设置。不同的IExecutionContext实例可以具有不同的维度。只有EngineCapability::kDEFAULT支持通配符维度。它们在安全环境中不受支持。DLA不支持{C,H,W}维度中的通配符维度。
张量尺寸的指定与格式无关。例如,如果张量以“NHWC”或矢量化格式格式化,则维度仍按顺序{N,C,H,W}指定。对于具有通道维度的2D图像,最后三个维度总是{C,H,W}。对于具有通道维度的3D图像,最后四个维度总是{C,D,H,W}。"
TensorRTX从.wts文件中加载权重。加载后以map即元组的形式组织。
Weights也是Nvidia的标准类,描述为"An array of weights used as a layer parameter.",
接下来就是不断堆叠作者通过原生API实现的网络层。
首先是用具体权重实例化每一层对象。这一段类似于yolov5*.yaml
1 | /* ------ yolov5 backbone------ */ |
这是Yolov5 v4.0的网络结构
1 | # parameters |
其中以convBlock为例,对比yolov5 pytorch和TensorRTX中基于原生API的实现。
1 | ILayer* convBlock(INetworkDefinition *network, std::map<std::string, Weights>& weightMap, ITensor& input, int outch, int ksize, int s, int g, std::string lname) { |
1 | # Yolov5 v4.0 |
对比一下,可以看到并不能直接从pytorch翻译成TensorRT,需要添加很多细节。这应该是由于pytorch框架内部隐含实现了很多细节。但是只要网络是基于pytorch实现的,都可以参考别人的翻译来实现,因为在翻译时只需要以pytorch API为单位进行翻译即可。
注意到在common.hpp和yololayer.cu中作者自行实现了一个plugin,
1 | // common.hpp |
可以看到plugin是cuda编程实现的,具体是那一部分?估计大概率是多重检测头。对比回yolov5的common.py
1 | class DetectMultiBackend(nn.Module): |
看不懂,过。
1 | // Build engine |
builder和config设置了一些engine的参数,然后设置了计算精度是FP16或者0INT8。
1 | std::cout << "Building engine, please wait for a while..." << std::endl; |
build engine
1 | // Don't need the network any more |
收尾工作。build_engine()全部看完。
回到APIToModel。
1 | // Serialize the engine |
将engine序列化,然后收尾。
回到main()
1 | // create a model using the API directly and serialize it to a stream |
我们仍在这一段。在将engine序列化后,可以将其导出为.engine文件保存。
每一次构建完网络后,都会先将其导出为.engine文件。
1 | // deserialize the .engine and run inference |
这一段代码就是输入.engine文件,然后将其反序列化为engine。如果已有.engine文件,就不再需要重新构建网络。
1 | std::vector<std::string> file_names; |
TensorRTX的作者希望我们以图片的形式输入数据进行推理。当我们需要改造TensorRTX时,可以跟踪file_names的行为并将其改造为我们需要的数据流。
到这里为止,我们已经构建了一个用于推理的engine,接下来我们应该构建context,即申请显存。
1 | // prepare input data --------------------------- |
createInferRuntime()描述为"Create an instance of an IRuntime class.This class is the logging class for the runtime."。即createInferRuntime实例化的是一个logger,而ICudaEngine类描述的才是真正的engine。
前面从API构建网络的最后对engine进行了序列化,而读取.engine文件时也是序列化状态,因此需要反序列化deserializeCudaEngine。
IExecutionContext描述为"使用具有功能不安全特性的引擎执行推理的上下文。一个ICudaEngine实例可能存在多个执行上下文,允许同一个引擎同时执行多个批处理。如果引擎支持动态形状,则并发使用的每个执行上下文都必须使用单独的优化配置文件。"上下文实际上就是显存空间。
getNbBindings(),binding不知道是什么。描述为"获取绑定索引的数量。如果引擎是为K个配置文件构建的,那么第一个getNbBindings()/K绑定将由配置文件编号0使用,下面的getNbBinding()/KK绑定将由第1个配置文件使用。"后面用binding获取了输入和输出,猜测估计是网络中内存与显存需要进行交换的部分???因为除了输入输出,中间部分都可由GPU自动分配显存,但输入输出要实现约定好大小以进行数据交换。
cudaMalloc()和cudaStreamCreate()属于cuda编程部分,不属于TensorRT。cudaMalloc()的描述为"在设备上分配线性内存的大小字节,并在*devPtr中返回分配内存的指针。分配的内存适合于任何类型的变量。内存未清除。cudaMalloc()在失败时返回cudaErrorMemoryAllocation。“其中设备是在main()的第一行cudaSetDevice(DEVICE);
指定的。cudaStreamCreate()的描述为"Creates a new asynchronous stream.”,应该是内存与显存的传输流。
到了这部分,推理的准备已经全部完成,后面的部分就是推理部分。
1 | int fcount = 0; |
基本上与pytorch的实现类似,从推理到nms,还包含计算推理时间以及最后在图上画框的部分。第一部分应该是对输入的图像进一步处理成输入数据,除了整理数据组织形式外,其他的处理操作没看懂。对TensorRTX改造时主要的处理部分。
其中推理接口doInference()
1 | void doInference(IExecutionContext& context, cudaStream_t& stream, void **buffers, float* input, float* output, int batchSize) { |
可以看到基本上用cuda编程,将数据送上去排队,然后等待输出。
1 | // Release stream and buffers |
最后的收尾工作,包括传输流的回收,释放显存,最后释放内存。