赞
踩
TensorRT是英伟达公司出品的高性能的推断C++库,应用于边缘设备的推理,可以将训练好的模型分解再进行融合,融合后的模型具有高度的集合度,其直接利用cuda以在显卡上运行,所有的代码库仅仅包括C++和cuda,在利用此优化库运行代码时,运行速度和所占内存的大小都会大大缩减。
在官网下载tensorRT,本文选取版本8.4.2.4;
下载并解压后,将tensorRT/bin目录加至Path环境变量中;
D:\TensorRT-8.4.2.4\bin
将TensorRT\lib下的lib文件拷贝至CUDA的lib\x64路径,如C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\lib\x64;将TensorRT\lib下的dll文件拷贝至C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.2\bin。
打开任意一个项目,打开项目属性->C/C++->常规->附加包含目录,将TensorRT\include加入附加包含目录;打开项目属性->链接器->附加库目录,将TensorRT\lib加入附加库目录;打开项目属性->链接器->输入->附加依赖项,将TensorRT\lib下的所有lib文件加入附加依赖项;
- nvinfer.lib
- nvinfer_plugin.lib
- nvonnxparser.lib
- nvparsers.lib
从Libtorch官网下载并解压,此后对VS进行配置。打开项目属性->C/C++->附加包含目录,将Libtorch\include和Libtorch\include\torch\csrc\api\include加入;
打开项目属性->链接器->常规->附加库目录,将Libtorch\lib加入;打开项目属性->链接器->输入->附加依赖性,将Libtorch\lib下的lib文件名全部加入;打开项目属性->配置属性->调试->环境,将D:\libtorch\bin;%PATH%加入其中;
TensorRT包含解析器parser、构建器builder、引擎engine、执行上下文context等;解析器负责加载模型框架与权重,构建器负责构建网络,引擎与执行上下文负责在网络中进行前向传播。
一般而言,TensorRT的支持自定义构建TensorRT模型和从外界导入模型两种。
TensorRT支持从头创建网络,其一般流程如下。
创建logger->创建builder->创建Network->创建网络->标记输入输出->设置运行参数config->创建engine->创建上下文并推理;
- 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;
- //builder创建
- IBuilder* builder = createInferBuilder(gLogger);
- //network创建
- INetworkDefinition* network = builder->createNetworkV2(1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH));
上述代码创建了builder与network,可以使用network->add函数以在模型中增加模块。亦可以在增加模块的时候对齐命名,使用getOutput->setName即可命名。
- IBuilderConfig* config = builder->createBuilderConfig();
- config->setMaxBatchSize(maxBatchSize);//设置批量大小
- config->setMaxWorkspaceSize(1 << 30);//设置最大运行空间
-
- //创建engine
- ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
上述代码则实现了config的创建,以配置运行空间等参数,此后创建了引擎。此后可以使用引擎进行推理,也可以使用engine->serialize()函数以将模型序列化保存。
当使用完毕后,使用如下函数进行析构。
- serializedModel->destroy();
- engine->destroy();
- network->destroy();
- config->destroy();
- builder->destroy();
tensorRT可以直接解析caffe和TensorFlow的网络模型;而caffe2,pytorch,mxnet,chainer,CNTK等框架则是需要将模型转为 ONNX(开放式神经网络交换工具) ,再对ONNX模型做解析以转化为TensorRT模型。其支持的精度包括FP32/FP16/TF32等。
在得到导出的ONNX模型以后,应先使用onnxruntime包,在python上进行调用,以验证此模型的正确性,在得到了验证后再转化为TensorRT文件以进行后续部署。
得到onnx文件后,在/tensorRT/bin目录下,调用trtexec.exe文件即可将onnx转为tensorRT文件。其调用指令如下。待运行完毕后即可得到tensorRT文件。
- #640 批量固定为1
- ./trtexec.exe --onnx=yolov7.onnx --saveEngine=yolov7-tiny.trt --buildOnly
- # 640 动态维度 最大最小shape可更改
- ./trtexec.exe --onnx=yolov7.onnx --saveEngine=yolov7.trt --buildOnly --minShapes=images:1x3x640x640 --optShapes=images:2x3x640x640 --maxShapes=images:4x3x640x640
- # 1280 动态维度
- ./trtexec.exe --onnx=yolov7-w6.onnx --saveEngine=yolov7-w6.trt --buildOnly --minShapes=images:1x3x1280x1280 --optShapes=images:2x3x1280x1280 --maxShapes=images:4x3x1280x1280
而对于调用TRT模型而言,其一般流程如下:
读入trt文件->创建Iruntime->Iruntime反序列化trt文件以得到engine->engine创建context->为运行分配空间->将数据拷贝至分配的空间上->模型推理->后处理;
- class Logger : public ILogger {
- public:
- void log(Severity severity, const char* msg) noexcept override
- {
- // suppress info-level messages
- if (severity <= Severity::kWARNING)
- std::cout << msg << std::endl;
- }
- }gLogger;
-
- //假设模型已经被读入到vector<char> modeltrt中
- initLibNvInferPlugins(&gLogger, "");
- //模型导入
- Logger m_logger;
- IRuntime* runtime = createInferRuntime(gLogger);
- ICudaEngine* engine = runtime->deserializeCudaEngine(modeltrt.data(), modeltrt.size(), nullptr);
- IExecutionContext* context = engine->createExecutionContext();
-
- //分配空间
- void* buffers[2];
- buffers[0] = buffers[1] = NULL;
- cudaMallocManaged(&buffers[0], 3 * 640 * 640 * 4);
- cudaMallocManaged(&buffers[1], 1 * 25200 * 85 * 4);
- cudaStream_t stream;
- cudaStreamCreate(&stream);
-
- //假设输入数据已经复制到buffers上,运行
- context->enqueueV2(buffers, stream, nullptr);

TRT亦可直接解析ONNX/CAFFE模型等,其流程类似,即:
创建Ibuilder->创建InetworkDefiniton->创建IParser->parser调用parsefromFile函数解析文件->标记网络输出->创建IbuilderConfig以配置运行环境->创建engine->序列化或进行推理;
- class Logger : public ILogger {
- public:
- void log(Severity severity, const char* msg) noexcept override
- {
- // suppress info-level messages
- if (severity <= Severity::kWARNING)
- std::cout << msg << std::endl;
- }
- }gLogger;
-
- IBuilder* builder = createInferBuilder(gLogger);
- //创建网络
- const auto explicitBatch = 1U << static_cast<uint32_t>(NetworkDefinitionCreationFlag::kEXPLICIT_BATCH);
- INetworkDefinition* network = builder->createNetworkV2(explicitBatch);
-
- //创建解析器
- nvonnxparser::IParser* parser = nvonnxparser::createParser(*network, gLogger);
-
- //onnx_filename为onnx路径
- parser->parseFromFile(onnx_filename, ILogger::Severity::kWARNING);
-
- //设置运行参数
- IBuilderConfig* config = builder->createBuilderConfig();
- config->setMaxBatchSize(maxBatchSize);//设置最大batchsize
- config->setMaxWorkspaceSize(1 << 30);//2^30 ,这里是1G
-
- //创建engine
- ICudaEngine* engine = builder->buildEngineWithConfig(*network, *config);
- //序列化
- IHostMemory *serializedModel = engine->serialize();
-

而对于本文的yolo,首先从YOLOv7官网将项目克隆,从Release下载yolov7.pt权重后,使用以下指令导出动态维度的yolov7;
亦可去除dynamic参数以导出不具有动态维度的onnx模型,此时批量大小为1。下文使用不带有动态维度的onnx模型。
python export.py --weights yolov7.pt --dynamic --grid
通过onnxruntime调用验证其正确性后,调用上文的trtexec文件生成yolov7.engine文件。首先将trt文件读入至vector中;
- string TRTpath = "your trtpath";
- ifstream trtinput(TRTpath, ios_base::binary);
- int trtsize = 0;
- if (trtinput.is_open()) {
- trtinput.seekg(0, ios::end);
- trtsize = trtinput.tellg();
- trtinput.seekg(0, ios::beg);
- }
- vector<char> modeltrt(trtsize);
- trtinput.read(modeltrt.data(), trtsize);
- trtinput.close();
此后创建Iruntime、IcudaEngine、IExecutionContext,ICudaEngine使用deserializeCudaEngine函数以反序列化模型;
- Logger m_logger;
- IRuntime* runtime = createInferRuntime(gLogger);
- ICudaEngine* engine = runtime->deserializeCudaEngine(modeltrt.data(), modeltrt.size(), nullptr);
- IExecutionContext* context = engine->createExecutionContext();
此时已经创建好了对应的engine与context,接下来所需要的是分配对应的空间,由于在GPU上运行,需要使用cudaMallocManaged函数以进行分配。yolov7的输出为1*25200*85,使用float类型,故分配1*25200*85*4的空间,输入为三通道的640*640图像,故输入为3*640*640*4。
- void* buffers[2];
- buffers[0] = buffers[1] = NULL;
- cudaMallocManaged(&buffers[0], 3 * 640 * 640 * 4);
- cudaMallocManaged(&buffers[1], 1 * 25200 * 85 * 4);
- cudaStream_t stream;
- cudaStreamCreate(&stream);
此后将数据复制到buffer数组上并推理。需要注意的是,yolo的输入为3*640*640,图像为640*640*3,需要调用permute函数交换维度的同时,还需要调用reshape函数。这是因为permute函数只是改变了张量解释的方法,并没有改变张量在内存中的存储,因而直接调用cudamemcpy函数以后仍然是没有调整维度的原张量,而reshape函数会重新分配一块内存,此时维度才能在内存上得到交换。最后将输出结果进行后处理即可。
- tensorimage1 = tensorimage1.permute({ 2,0,1 }).toType(torch::kFloat).unsqueeze(0).to(at::kCUDA);
- tensorimage1 = tensorimage1 / 255.0;
- tensorimage1 = tensorimage1.reshape(-1);
- cudaMemcpy(buffers[0], tensorimage1.data_ptr(), 4 * 3 * 640 * 640, cudaMemcpyHostToDevice);
- context->enqueueV2(buffers, stream, nullptr);
- torch::Tensor output = torch::ones({ 1 ,25200 ,85 }, at::kFloat);
- cudaMemcpy(output.data_ptr<float>(), buffers[1], 25200 * 85 * 4, cudaMemcpyDeviceToHost);
后处理包括对每一个检测框进行IOU计算与NMS抑制。yolov7的输出为1*25200*85,本质上是由3*20*20*85+3*40*40*85+3*80*80*85,其含义为将图片划分为20*20或40*40或80*80的网格,每一个网格中检测3个检测框;而最后一维的85由四个坐标+置信度+80种类别的可能性组成,其含义为检测框的左上角和右下角的坐标+置信度+检测框的物体类别,80基于COCO80数据集。
通过设置置信度阈值,选出大于置信度的所有检测框,再对每一个检测框进行NMS抑制,即可得到最终的检测框。
在本文中,NMS和IOU计算函数如下。
- class Bbox
- {
- public:
- int x;
- int y;
- int h;
- int w;
- float score;
- int type_number;//类型编号
- };
- //iou计算
- float calculateIOU(Bbox& box1, Bbox& box2) {
- int min_x = max(box1.x - box1.h / 2, box2.x - box2.h / 2); // 找出左上角坐标哪个大
- int max_x = min(box1.x + box1.h / 2, box2.x + box2.h / 2); // 找出右上角坐标哪个小
-
- int min_y = max(box1.y - box1.w / 2, box2.y - box2.w / 2);
- int max_y = min(box1.y + box1.w / 2, box2.y + box2.w / 2);
-
- if (min_x >= max_x || min_y >= max_y) // 如果没有重叠
- return 0;
- float over_area = (max_x - min_x) * (max_y - min_y); // 计算重叠面积
- float area_a = box1.h * box1.w;
- float area_b = box2.h * box2.w;
- float iou = over_area / (area_a + area_b - over_area);
- return iou;
- }
- vector<Bbox> NMS(vector<Bbox>& boxes, float threshold) {
- sort(boxes.begin(), boxes.end(), cmp);
- vector<Bbox> result;
- while (boxes.size() > 0) {
- result.push_back(boxes[0]);
- int index = 1;
- while (index < boxes.size())
- {
- float iou_value = calculateIOU(boxes[0], boxes[index]);
- if (iou_value > threshold)
- boxes.erase(boxes.begin() + index);
- else
- index++;
- }
- boxes.erase(boxes.begin());
- }
- return result;
-
- }

下图为yolov7的tensorRT部署效果。
在已有的项目中,使用TensorRT部署的yolov7,较部署前平均快了3-4倍,可见TensorRT的效率。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。