一. 环境配置
1.tensorRT安装
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。
Visual Studio配置
打开任意一个项目,打开项目属性->C/C++->常规->附加包含目录,将TensorRT\include加入附加包含目录;打开项目属性->链接器->附加库目录,将TensorRT\lib加入附加库目录;打开项目属性->链接器->输入->附加依赖项,将TensorRT\lib下的所有lib文件加入附加依赖项;
nvinfer.lib
nvinfer_plugin.lib
nvonnxparser.lib
nvparsers.lib
2. LibTorch安装
从Libtorch官网下载并解压,此后对VS进行配置。打开项目属性->C/C++->附加包含目录,将Libtorch\include和Libtorch\include\torch\csrc\api\include加入;
打开项目属性->链接器->常规->附加库目录,将Libtorch\lib加入;打开项目属性->链接器->输入->附加依赖性,将Libtorch\lib下的lib文件名全部加入;打开项目属性->配置属性->调试->环境,将D:\libtorch\bin;%PATH%加入其中;
二.TensorRT的使用
1.TensorRT的流程
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();
2.yolo的tensorRT部署
而对于本文的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函数,同时还需要将BGR转化为RGB格式。这是因为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);
需要注意的是,yolo本身并不是直接将图片进行resize的,而是进行了等比例缩放,其余地方进行填充的。即letterbox函数,在yolo中,网格的大小并不是固定为20*20,40*40,80*80的,,输入也不必固定为640*640,而是可以根据图像的长宽比自动缩放的。但是导出为trt以后失去了这个功能,网格大小被固定为20/40/80,且输入也要被固定。
本文的letterbox函数如下。
Mat latterBox(const Mat& img, Size new_Shape ,Scalar color,bool auto_size,bool scale_fill, bool scale_up,float& ratio) {
int rows = img.rows,cols = img.cols;
float Scale_ratio = min((float)new_Shape.height / rows, (float)new_Shape.width / cols);
if (!scale_up) //如果不允许扩大 仅仅允许缩小
Scale_ratio = min(Scale_ratio,1.0f);
int new_origin_width = cols * Scale_ratio, new_origin_height = rows * Scale_ratio;
int pad_width = new_Shape.width - new_origin_width,pad_height = new_Shape.height - new_origin_height;
if (auto_size) {
pad_width = pad_width % 32;
pad_height = pad_height % 32;
}
else if(scale_fill) {
pad_width = 0;
pad_height = 0;
new_origin_height = new_Shape.height;
new_origin_width = new_Shape.width;
}
ratio = Scale_ratio;
Mat resize_img;
//先转过来 对应比例转化且保证被32整除
resize(img, resize_img, Size(new_origin_width, new_origin_height), 0, 0, cv::INTER_LINEAR);
int top = pad_height / 2;
int bottom = pad_height - top;
int left = pad_width / 2;
int right = pad_width - left;
Mat temp;
copyMakeBorder(resize_img, temp, top, bottom, left, right, BORDER_CONSTANT, color);
//再进行填充 转到640*640
Mat result(new_Shape.height, new_Shape.width, CV_8UC3,color);
int y = (new_Shape.height - temp.rows) / 2;
int x = (new_Shape.width - temp.cols) / 2;
temp.copyTo(result(Rect(x,y,temp.cols,temp.rows)));
return result;
}
后处理包括对每一个检测框进行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;
}
3.部署效果
下图为yolov7的tensorRT部署效果。
在已有的项目中,使用TensorRT部署的yolov7,较部署前平均快了3-4倍,可见TensorRT的效率。