☰
vs2015环境下yolov5 TensorRT加速部署实践
2026/10/17 12:27:50 网站建设 项目流程

前阵子接手一个老项目的性能升级需求,代码库和编译链路都被锁死在 vs2015 上,模型检测部分却想换用 yolov5。训练倒是简单,一到推理就头大:TensorRT 在 Windows 上的生态明显不如 Linux,老社区里的经验贴又大多是 Linux + CMake 的玩法,很少有正经讲 Windows 10 + vs2015 + yolov5 这条路该怎么走的。我硬着头皮啃了两周,前后踩了十几个坑,才把 yolov5 从 PyTorch 推理换成 TensorRT 加速引擎,在 vs2015 工程里把检测速度提了上去。这篇就把整个实践过程、配置组合和踩坑细节完整记下来,给同样被老工具集锁住的朋友一份可以直接照着抄的参考。

先说明一下这套方案适合谁:如果你的环境和我不一样,可以不用照搬版本号,但环境搭配思路、导出参数、推理代码骨架、以及最后那组最容易翻车的坑,基本都是通用的。尤其适合那种“编译环境动不了,只能往里面塞新模型”的存量工程场景。

1. 为什么在 vs2015 上做 TensorRT 加速:先把这套链路卡在哪讲清楚

很多人的第一反应是:vs2015 都什么年代的老古董了,直接升级成 vs2022 不是省事得多?能做我当然想做,问题是现实项目里经常存在“不能升级”的客观原因。可能是底层库的作者早就离职、遗留代码只敢拿原工具链编译;可能是第三方 SDK 只提供了 v140 向导生成的工程文件;也可能就是一个组织内部的规范问题——反正你只能在这个老工具集里折腾。这时候与其纠结“为什么不升级”,不如想想“怎么在现有条件下把它跑起来”。

这套链路最难受的地方有三点:

第一,TensorRT 官方在 Windows 上提供的示例工程基本都是给较新的 Visual Studio 准备的,直接拿 vs2015 打开官方 sample 工程,一般会报平台工具集不匹配,提示安装 v140 或提示升级。我一开始也以为这条路断了,后来才发现一个关键事实:官方 sample 打不开,不代表官方运行库在 vs2015 下不能链接。我们完全可以自己建一个空工程,只把 TensorRT 的 include 和 lib 指进来,用官方编译好的运行时库做推理。相当于我们只是“借用”TensorRT 预编译的 DLL,而不是自己从源码编译它,这样编译器版本限制就被绕开了。

第二,yolov5 本身是 PyTorch 生态,PyTorch 推理和 TensorRT 推理之间的交接要靠 ONNX 格式。这条交接链路在 Windows 下没有 Linux 那么顺滑,尤其是导出时 opset 版本、动态维度、是否 simplify 这些参数,稍有不对后面转引擎就报错。而且我们最终是给 C++ 用的,导出环节一旦图结构复杂化,后面不管怎么调整都很痛苦。

第三,也是很多人忽略的:TensorRT 推理不是“把模型加载进去就完事”,预处理、后处理、坐标映射这些全都要自己写。PyTorch 推理时代可以用 torchvision 的 transforms,换到 TensorRT 之后,图像缩放、归一化、NMS 这些操作都得用 OpenCV 和手写代码。如果对 letterbox 的缩放比和 padding 理解不到位,检测框的位置会整体偏移,而且是那种看起来没错、实际上框全歪的偏移。

所以这篇文章的主线就是:先搭出一套能互相兼容的版本环境,然后导出干净可用的 ONNX,再转成 TensorRT 引擎,最后在 vs2015 里写推理代码,把预处理、推理、后处理串起来。

2. 环境版本搭配:一份能稳定通过 vs2015 编译的组合

版本搭配是整个实践里最容易让人崩溃的部分。CUDA、cuDNN、TensorRT、OpenCV、vs2015 这五样东西之间的关系,比一般软件依赖要敏感得多。我最后用下来比较稳的组合是这样:

组件我用的版本说明
操作系统Windows 10建议 x64 系统
Visual Studio2015 Update 3平台工具集 v140,x64 配置
CUDA11.1对应显卡驱动不低于 456.x
cuDNN8.1.1解压后把 bin、include、lib 内容复制到 CUDA 对应目录
TensorRT7.2.3.2Windows x64 的 zip 包,自带 trtexec.exe
OpenCV3.4.x建议用 3.4 系列,别直接上 4.x 官方预编译包
Python3.8 或 3.9只用于导出 ONNX 和辅助脚本,不参与最终部署

为什么 OpenCV 这里要特别提醒?因为新版 OpenCV 官方预编译包基本是用更新的 Visual Studio 工具集编出来的,vs2015 的 v140 工具集直接链接时很容易在运行库层面出问题。我用 3.4.x 系列就省心很多,官方下载页里就有现成的 Windows 预编译包,直接配 include 和 lib 就行了。如果你的功能强依赖新版 OpenCV 的特性,也可以拿源码自己用 v140 编译,但那个工作量明显大不少,没必要为了一个 resize 和 NMS 去折腾。

再说 CUDA 和 TensorRT 的匹配。TensorRT 7.2.3 需要 CUDA 11.x,这个组合在 Windows 上兼容性比较好。注意不要在 7.2 上硬配 CUDA 8.x,也不要拿新 TensorRT 8.6 去配老 CUDA 10.2,这种不匹配最常见的报错是加载 engine 失败、提示找不到某个 CUDA 入口点,或者是 trtexec 转换时直接崩溃。具体对应关系在 TensorRT 下载页的 Release Notes 里有兼容表,安装前一定看一眼。

还有几个环境变量的坑值得提前记录:

  • 安装完 CUDA 后,确认CUDA_PATH环境变量指向C:\Program Files\NVIDIA GPU Computing Toolkit\CUDA\v11.1。
  • 把 TensorRT 解压目录里的lib路径加到系统的Path变量里,否则运行时会提示找不到nvinfer.dll。
  • cuDNN 处理好之后可以在终端里敲一下nvcc --version验证。

把这一层搞定之后,才算拿到一张能继续往下走的门票。我见过不少人在这一步就卡了三天,最后发现只是 Path 没配全,属实可惜。

3. 从 pt 权重到 TensorRT 引擎:yolov5 导出 ONNX 与 trtexec 转换细节

3.1 导出 ONNX:参数不是随便填的

yolov5 官方仓库里自带export.py,直接用就好。我当时的导出命令大致是:

python export.py --weights yolov5s.pt --img 640 --batch 1 --include onnx --opset 12 --simplify

几个参数为什么这么选:

  • --img 640:把输入分辨率固定到 640。TensorRT 最舒服的模式是固定 shape,动态 shape 虽然支持,但会引入额外的工作量,后面讲。
  • --batch 1:固定 batch 为 1。实际部署大多是单帧推理,没必要给动态 batch 留空间。
  • --opset 12:ONNX opset 版本。yolov5 的一些算子用低版本导出容易有问题,12 是一个比较合适的值,部分版本还支持 13,但 12 兼容性最稳。
  • --simplify:这个非常关键。simplify 会调用 onnx-simplifier 把计算图里的一些冗余节点合并掉,比如把常量折叠、把多余的 reshape 抹掉。经过 simplify 之后的模型在 TensorRT 里结构更干净,转换成功率明显更高。

导出成功之后,会得到yolov5s.onnx,大概几十 MB。可以顺手用onnxruntime跑一张测试图验证输出是否正常,能省后面不少排查时间。我那次导出完没有立刻验证,结果到 TensorRT 阶段才发现网络结构哪里不对劲,回头定位浪费了大半天。

需要留意 yolov5 输出的形状。以yolov5s为例,固定 640x640 输入时,ONNX 模型的输出是一个[1, 25200, 85]的张量。25200 等于三个尺度下 anchor 数量的总和,85 是4 个坐标 + 1 个置信度 + 80 个类别分数。后面的推理代码就是基于这个形状写的,不要记错。

3.2 trtexec:一条命令完成转换

TensorRT 装好之后,目录里自带一个trtexec.exe,这是最方便的转换工具,不需要写任何 C++ 代码就能把 ONNX 变成 engine 文件。我的转换命令是这样:

C:\TensorRT-7.2.3.2\bin\trtexec.exe --onnx=yolov5s.onnx --saveEngine=yolov5s_fp16.trt --fp16 --workspace=2048
  • --fp16:启用 FP16 精度。这是加速收益最大的一步,TensorRT 会把能做半精度计算的层全部转成 FP16,推理速度通常能提升 50% 到 100%。
  • --workspace=2048:给转换过程分配以 MB 为单位的显存工作空间,当时要 2GB 左右才够,太少了会报 workspace 不足。
  • --saveEngine:转换完成后的引擎文件路径。

转换成功的标志是终端输出里出现Engine built successfully之类的内容,并给出一个 build 耗时。失败的情况我遇到过几种,比较常见的是算子不支持的报错,提示某个节点无法找到对应的 TensorRT layer。这种时候优先检查 ONNX 是不是没有 simplify,再检查 opset 版本是不是太低。

如果你对 INT8 感兴趣,也可以试:

C:\TensorRT-7.2.3.2\bin\trtexec.exe --onnx=yolov5s.onnx --saveEngine=yolov5s_int8.trt --int8 --calib=calib_cache.bin --workspace=2048

INT8 需要校准数据。简单来说就是需要准备一批代表性图片,让 TensorRT 统计各层激活值的分布,从而确定量化范围。没有校准数据的话,也可以让 TensorRT 使用默认的熵校准器自动生成 cache,但效果没有针对性校准好。第一次做建议先用 FP16,跑通了再考虑 INT8。

3.3 动态 shape 要不要碰

TensorRT 支持动态 shape,也就是让引擎在运行时接受多种输入尺寸,这个特性听起来很香,但代价也很明显:推理时需要设置 optimization profile,绑定输入维度时也要在 Runtime 里额外配置,稍有不慎就报Assertion failed或者维度不匹配。如果你的业务是固定摄像头分辨率,用固定 640x640 就好,完全没必要上动态。

动态 shape 还有一个隐藏缺点:TensorRT 为了支持动态尺寸,build 出来的引擎内部会做一些妥协,可能损失一点点性能。固定 shape 版本是最简单高效的选择。我在项目里就是固定 640x640,预处理的时候把任意尺寸的图拉成 letterbox 形式,效果稳定得多。

4. vs2015 工程里的推理代码:预处理、NMS 与坐标映射

4.1 工程创建和依赖配置

在 vs2015 里创建一个空的控制台工程,然后把平台工具集设置成 v140,配置管理器选 x64。这一步没问题之后,依次配置:

  • C/C++ -> 常规 -> 附加包含目录:填入 TensorRT 的 include、CUDA 的 include、OpenCV 的 include。
  • 链接器 -> 常规 -> 附加库目录:填入 TensorRT 的 lib、CUDA 的 lib、OpenCV 的 lib。
  • 链接器 -> 输入 -> 附加依赖项:添加入口的库文件,主要是nvinfer.lib、cudart.lib、opencv_world3410.lib之类的。

另外记得把 OpenCV 的bin目录和 TensorRT 的bin目录加进系统Path,否则 exe 启动时会提示找不到 DLL。Release 模式下编译,运行库选择多线程/MD,别用 Debug 模式的/MDd,不然链接 TensorRT 这种以 Release 库为主的第三方库时会出现各种符号不一致的问题。

4.2 letterbox 预处理:这一步做错,检测框就全歪了

yolov5 的数据增强里有一个关键操作叫 letterbox,就是把原始图像等比缩放后,在四周填充灰色(一般填充 114)。为什么不能直接cv::resize到 640x640?因为直接拉伸会破坏物体的长宽比,检测框虽然能出来,但回归精度会明显下降。letterbox 的核心代码如下:

cv::Mat letterbox(const cv::Mat& src, int targetSize, float& scale, float& padx, float& pady) { int h = src.rows, w = src.cols; scale = std::min((float)targetSize / w, (float)targetSize / h); int nw = (int)std::round(w * scale); int nh = (int)std::round(h * scale); padx = ((float)targetSize - nw) / 2.0f; pady = ((float)targetSize - nh) / 2.0f; cv::Mat resized; cv::resize(src, resized, cv::Size(nw, nh), 0.0, 0.0, cv::INTER_LINEAR); cv::Mat canvas(targetSize, targetSize, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect((int)padx, (int)pady, nw, nh))); return canvas; }

这里返回的scale、padx、pady必须保存下来,因为后处理要把模型输出的坐标换算回原图,全靠这三个值。

预处理除了 letterbox,还要把 BGR 转成 RGB,把 HWC 布局换成 CHW,再除以 255。yolov5 导出 ONNX 时默认就是按0~1的浮点输入做的,所以这一步不能少:

std::vector<float> inputTensor(1 * 3 * 640 * 640); float* data = inputTensor.data(); for (int c = 0; c < 3; c++) { for (int i = 0; i < 640 * 640; i++) { int h = i / 640; int w = i % 640; // 注意 BGR -> RGB,然后把 HWC 的数据按 CHW 顺序填入 if (c == 0) data[c * 640 * 640 + i] = letterboxed.at<cv::Vec3b>(h, w)[2] / 255.0f; if (c == 1) data[c * 640 * 640 + i] = letterboxed.at<cv::Vec3b>(h, w)[1] / 255.0f; if (c == 2) data[c * 640 * 640 + i] = letterboxed.at<cv::Vec3b>(h, w)[0] / 255.0f; } }

这段代码的效率不是最好的,因为单像素访问很慢,但对单帧推理来说完全够用。想更快可以考虑用cv::dnn::blobFromImage一步到位,它会做 resize、转换通道顺序和归一化,不过它不做 letterbox,所以还是得先手动处理完 letterbox 再调用。

4.3 TensorRT 引擎加载和推理骨架

加载 engine 的代码骨架大概是这样的:

std::ifstream file("yolov5s_fp16.trt", std::ios::binary | std::ios::ate); size_t size = file.tellg(); std::vector<char> buffer(size); file.seekg(0, std::ios::beg); file.read(buffer.data(), size); IRuntime* runtime = createInferRuntime(gLogger); ICudaEngine* engine = runtime->deserializeCudaEngine(buffer.data(), size); IExecutionContext* context = engine->createExecutionContext();

其中gLogger来自 TensorRT 自带的logging.h,需要把它包含进来。不同的 TensorRT 版本 API 有差异,比如新版把getBindingDimensions改成了getTensorShape,我这边用的旧版 API 还是getNbBindings这套,建议按你的版本查一下对应函数名。

推理执行的部分:

void* buffers[2]; size_t inputSize = 1 * 3 * 640 * 640; size_t outputSize = 1 * 25200 * 85; cudaMalloc(&buffers[0], inputSize * sizeof(float)); cudaMalloc(&buffers[1], outputSize * sizeof(float)); cudaMemcpy(buffers[0], inputTensor.data(), inputSize * sizeof(float), cudaMemcpyHostToDevice); cudaStream_t stream; cudaStreamCreate(&stream); context->enqueueV2(buffers, stream, nullptr); cudaStreamSynchronize(stream); std::vector<float> output(outputSize); cudaMemcpy(output.data(), buffers[1], outputSize * sizeof(float), cudaMemcpyDeviceToHost); cudaFree(buffers[0]); cudaFree(buffers[1]);

输出张量[1, 25200, 85]在 float 下占用约 8.5MB 显存,不算大,但如果你做的是批量或多路推理,这个内存分配和释放的频率就要注意了,最好一次性把 buffer 申请好,复用着用。

4.4 后处理:置信度过滤 + NMS + 坐标逆映射

拿到output之后,先遍历所有 25200 个候选框。每一个候选框的 85 个数据排列是:cx, cy, w, h, obj_conf, cls0, cls1, ..., cls79。先用obj_conf * cls_score算出最终置信度,过滤掉低于阈值的框,然后做 NMS。NMS 我用的是 OpenCV 自带的函数,省事不少:

std::vector<cv::Rect> rects; std::vector<float> scores; std::vector<int> classIds; for (int i = 0; i < 25200; i++) { float* row = output.data() + i * 85; float objConf = row[4]; float bestScore = 0.0f; int bestCls = -1; for (int c = 0; c < 80; c++) { float score = objConf * row[5 + c]; if (score > bestScore) { bestScore = score; bestCls = c; } } if (bestScore < confThresh) continue; float cx = row[0], cy = row[1], w = row[2], h = row[3]; float x1 = (cx - w / 2 - padx) / scale; float y1 = (cy - h / 2 - pady) / scale; float x2 = (cx + w / 2 - padx) / scale; float y2 = (cy + h / 2 - pady) / scale; rects.emplace_back(x1, y1, x2 - x1, y2 - y1); scores.push_back(bestScore); classIds.push_back(bestCls); } std::vector<int> keep; cv::dnn::NMSBoxes(rects, scores, confThresh, iouThresh, keep);

这一步是离“能跑”最近的地方,也是最容易出幺蛾子的地方。很多人在后处理里忘了把坐标减掉 padding 再除以 scale,结果就是框虽然能框住目标,但位置会系统性偏到左上或右下。我第一次跑出来的结果就是这样,检测置信度很高,但框就是不贴边,后来重新捋了一遍 letterbox 的逆变换才修好。

5. 性能结果与误差检查:到底加速了多少

跑通之后第一件事当然是看性能。我把三个阶段的推理方式对比了一下,量化一下这个加速到底有多大价值。测试环境就是我手头那台老平台的机器,同一张 1920x1080 测试图,同一份 yolov5s 权重,统计单帧推理耗时。

推理方式单帧耗时(毫秒,约)说明
PyTorch CPU500 以上CPU 跑,几乎不可用
PyTorch GPU FP3222 左右已经能实时,但显存和功耗偏高
TensorRT FP3213 左右相比 PyTorch GPU 有明显提升
TensorRT FP168 左右日常部署最推荐
TensorRT INT86 左右需要校准,精度有轻微下降

不同显卡之间差异非常大,具体数字不用太纠结,更重要的是观察几个趋势:

第一,TensorRT 的加速在 CPU 侧瓶颈明显时收益最大。如果你的系统里预处理、解码之类的操作很吃 CPU,那一帧的端到端耗时可能依然很高,加速效果会被掩盖。所以部署优化时不要只看模型推理时间,预处理和后处理也要一起算进去。

第二,FP16 到 INT8 的提升幅度没有 FP32 到 FP16 那么夸张。INT8 还需要额外处理校准数据、精度回调等一堆问题,对 yolov5 这种本身对量化不算特别友好的模型,如果只是普通监控场景,FP16 往往已经足够了。

第三,要验证加速后检测结果和 PyTorch 原版有没有明显偏差。我习惯是选取同一段视频,分别用 PyTorch 推理和 TensorRT 推理跑一遍,统计每一帧输出框的坐标差和置信度差。一般 FP16 下坐标偏移在 2~3 个像素以内,置信度差在 0.01 以内都算正常;如果出现某个类别大量丢失,就要考虑是不是量化或算子替换导致精度下降,此时可以退回 FP32 做对比排查。

我那次实际项目里最终采用的是 FP16,推理耗时从 PyTorch GPU 的约 22 毫秒降到 8 毫秒左右,端到端 FPS 从不到 30 提升到 60 以上。对一个老平台来说,这个提升已经完全够用了。

6. 踩坑清单:vs2015 + TensorRT 最容易翻车的几个点

6.1 平台工具集冲突和官方 sample 工程的误导

我最早尝试直接打开 TensorRT 官方 sample 工程,VS 立刻提示需要升级工具集,当时差点以为这条路走不通。后来才明白,官方 sample 只是参考代码,不是部署方案。正确做法是建一个全新的空工程,使用 v140 工具集,然后把 TensorRT 的 include、lib 手动配置进来,只链接官方编译好的导入库。

6.2 运行库模式不一致

Debug 配置默认用的是/MDd多线程调试 DLL 运行库,而 TensorRT 官方库是 Release 的/MD。Debug 工程里强行链接官方库,会出现大量LNK2038运行时库不匹配的错误,提示mismatch detected for RuntimeLibrary。解决方案是直接全用 Release 配置,或者在 Debug 配置里手动把运行库改成/MD。当然最省心的还是全程 Release 模式跑。

6.3 显存和内存泄漏

TensorRT 引擎加载之后,engine、context、runtime都需要释放。我一开始图省事没管,跑了几个小时后显存直接吃满,然后新的推理请求全部失败。建议用 RAII 或者显式封装,在结束的时候依次调用context->destroy()、engine->destroy()、runtime->destroy(),顺便把cudaFree的 buffer 也收干净。旧版 TensorRT 的 API 名字就是这样的,新版可能有智能指针包装,按版本调整。

另外cudaMalloc不要反复调用。工程里写一个简单的内存池,只分配一次,复用它,能明显减少端到端延迟抖动。

6.4 检测框坐标偏移

这个坑我在 4.4 里说过,其实它就是 letterbox 逆变换没做好。症状很典型:模型置信度和类别都对,但框的位置整体往左上偏移或者比例不对。排查方法也简单,找一张并没有缩放的接近 640x640 的图,看看框是不是正常;再用一张 1080p 的图对比,如果框出现规律性偏移,基本可以确定是坐标映射公式里漏了padx、pady或者除错了scale。

6.5 引擎文件和运行环境强绑定

TensorRT 生成的 engine 文件不是通用的,它依赖你 build 时用的 TensorRT 版本、CUDA 版本、显卡型号,甚至驱动版本。换一台机器或者升级驱动后,老 engine 可能直接加载失败,提示the engine plan file is generated by an incompatible version。要做跨机器部署的话,最稳妥的方式是每台机器现场用 trtexec 重新转一次,或者写一个建引擎的初始化流程,第一次启动时自动生成 engine 文件。

6.6 OpenCV 预编译包的版本坑

我一开始下载了当时最新的 OpenCV 4.5.4 预编译包,结果 vs2015 链接阶段各种报错,后来换成 3.4.x 系列的预编译包就顺利通过了。如果你必须在 vs2015 下用新版 OpenCV,就去下载源码自己编译,但这个成本一般不太值得。能在 3.4.x 里解决的问题,就别在编译工具链上浪费时间。

最后分享一个我自己的习惯:在调试阶段不要一上来就上 FP16 或 INT8,先用 FP32 把整个链路跑通,确认检测框正常、坐标映射正确、内存没有泄漏,再切换到 FP16 对比性能。这样排查问题时能少三分之二的干扰因素。等你把整套流程摸熟之后,再考虑 INT8 和各种自定义插件,心里就有底了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询