ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

YOLOv5推理打包TensorRT DLL:C++部署实战与避坑指南

YOLOv5推理打包TensorRT DLL:C++部署实战与避坑指南 简介针对实时目标检测在边缘设备上的部署需求这份YOLOv5结合TensorRT的DLL封装资源面向具备一定C与深度学习基础的计算机视觉开发者省去了自行转换、编译和链接模型的繁琐流程。压缩包共8个文件、仅18KB主要包含C实现源码、头文件、CMake构建配置、说明文档及许可证文件其中源码与头文件定义了核心推理接口构建配置便于快速编译集成文档则提供使用与环境说明适合直接嵌入Windows应用程序。目前已有79人学习下载适合作为轻量级参考实现。通过该DLL开发者无需修改原始YOLOv5模型代码即可将经过TensorRT优化的检测能力集成到自研软件中在实时交通监控、视频分析和自动紧急制动等对响应速度敏感的场景下显著降低延迟。整体结构简洁清晰阅读源码可理解模型加载、推理封装与接口导出的完整思路对希望将深度学习算法落地到实际项目的开发者具有切实的参考价值。1. 把 YOLOv5 推理打包成 TensorRT DLL部署卡壳时的另一个出口训练好的 YOLOv5 权重交到交付手上那一刻要求往往不是“跑个 demo”而是“把这个模型接进我们 C 的程序里不能带 Python 环境”。YOLOv5 的推理链路依赖 PyTorch、torchvision、一堆 pip 包部署机一换就翻车。tensorrt 的 dll 版本思路是把整套推理封装成动态链接库模型权重转成 TensorRT 引擎C 侧调 DLL 完成预处理、推理、后处理对外只暴露几个 C 接口。这个 zip 资源做的就是这件事——适合被 Python 环境折腾过、需要把 YOLOv5 以原生库形式交付的从业者。下面从选型逻辑、封装流程到踩坑记录完整拆一遍你看完能判断这份资源适不适合自己的项目也能照着把 DLL 接起来。2. 部署形态的三个抉择DLL 封装、TensorRT 版本与显卡算力边界2.1 从 Python 权重到 DLL推理链路四层变化与取舍把 YOLOv5 从 Python 代码变成 DLL不是简单用 pyinstaller 打包个 exe 就算完。推理链路涉及四层变化每一层都影响部署稳定性。第一层是解释器层的消失。Python 推理依赖 CPython 运行时、GIL、numpy 和 torch 的 C 扩展换个机器就要重装环境。DLL 方案把推理代码编译成原生二进制运行时不依赖 Python部署机只需要装好 CUDA、cuDNN、TensorRT 的运行时库。第二层是模型格式的转变。YOLOv5 原生权重是 PyTorch 的 .pt 文件里面保存的是网络结构和状态字典推理时还要经过 Python 侧的前向计算。TensorRT 方案先导出 ONNX再转成 .engine 引擎文件引擎文件里是经过层融合、权重量化、kernel 自动调优后的计算图前向执行路径短很多。第三层是预处理和后处理位置的迁移。Python 部署里letterbox 缩放到 640×640、BGR 转 RGB、归一化这些操作通常写在 detect.py 里用 numpy 和 OpenCV 实现。DLL 方案中这些必须全部收进 C 代码输入直接是原始图像字节输出直接是 NMS 之后的检测框数组上层调用方不接触任何 cv 细节。第四层是加速策略的差异。PyTorch CPU 推理慢GPU 推理有框架调度开销TensorRT 的 kernel 是构建引擎时针对具体显卡自动挑选的FP16 下小目标检测吞吐能拉开明显差距。代价是引擎文件与显卡绑定换 GPU 型号必须重新构建。部署形态运行时依赖推理加速调用方接入成本跨机器迁移Python 脚本Python PyTorch 一堆 pip 包PyTorch GPU需配环境、理解 detect.py差环境易碎TensorRT 引擎 C 程序CUDA TensorRT 运行时TensorRT 优化中等需写 C 推理代码尚可引擎绑显卡TensorRT DLLCUDA TensorRT 运行时TensorRT 优化低只调 C 接口尚可DLL 和引擎一起拷贝把推理封装成 DLL 的真正价值是隔离复杂度。调用方不需要知道 TensorRT API 怎么用不需要理解引擎反序列化只需要拿一个初始化函数、一个推理函数、一个释放函数。这个资源把这几层打通省掉的是自己啃 NvInfer.h 和踩 API 坑的时间。2.2 TensorRT 8.x 与 10.xPascal 老卡GTX 1070停在哪个版本选 TensorRT 版本最怕的不是新功能不会用而是下载了新版结果老显卡不支持构建引擎时直接报 “no kernel image available” 或者平台不支持。这个问题在热搜里经常出现TensorRT 版本如果是 10.x 是否支持 GTX 1070。GTX 1070 是 Pascal 架构计算能力 6.1。TensorRT 10.x 的官方支持列表里 Pascal 系已经不作为主推目标常见现象是能装上、能运行工具链但构建引擎或推理时报缺少可用 kernel。我一般建议 10 系显卡的机器固定用 TensorRT 8.5 或 8.6 搭配 CUDA 11.x这两个版本对 Pascal 支持完整推理性能和稳定性都有保障。显卡架构代表显卡计算能力推荐 TensorRT推荐 CUDAPascalGTX 1070 / 10806.18.5 / 8.611.4 ~ 11.8TuringRTX 2060 / 20807.58.5 或 10.x11.x / 12.xAmpereRTX 3060 / 30908.68.5 或 10.x11.x / 12.xAdaRTX 4060 / 40908.910.x 最佳12.x判断方法是查看显卡计算能力Windows 下用 nvidia-smi 确认显卡型号再对照上表选版本。如果你拿到手的 DLL 工程包在构建引擎时报缺少 kernel 相关的错误先检查是不是这个原因而不是怀疑编译选项写错了。2.3 拿到 zip 先核对一份 DLL 工程包的文件清单下载资源解压后不要急着跑先核对文件结构。一个合格的 YOLOv5 TensorRT DLL 工程包至少应该包含这几类东西。模型转换脚本是第一个必须有的一环通常是一个 convert.py 或 export_onnx.py负责把 .pt 权重导出为 ONNX 再转 .engine。DLL 源码是第二个核心部分包含封装 TensorRT 推理的 C 源文件和头文件比如 yolo_dll.cpp、yolo_infer.h。构建配置也需要有Windows 下一般是 CMakeLists.txt 或 Visual Studio 的 .vcxproj里面配置好了 include 路径、库路径和依赖项。最后是准备一个 README说明 TensorRT 版本要求、CUDA 版本要求、如何改类别数量和输入尺寸。对照清单检查的意义在于如果包里少了转换脚本说明你要自己补导出逻辑如果 DLL 源码的 API 设计和你的调用方不匹配需要提前改接口。拿到资源的头一两个小时先做结构梳理后面接入会顺很多。3. 从权重到 DLL 落地ONNX 导出、引擎构建与 C 接口封装实战3.1 导出 ONNX动态 batch、opset 与简化选项的取舍YOLOv5 官方仓库自带 export.py导出 ONNX 是最省事的路径。但参数设置直接决定后续 TensorRT 构建的难易。以 YOLOv5 v6.0 之后的版本为例常用导出命令如下。python export.py --weights yolov5s.pt --include onnx --opset 12 --dynamic --batch-size 1 --simplify参数含义--weights 指定训练好的权重文件--include onnx 表示只导出 ONNX 格式--opset 12 是 ONNX 算子集版本TensorRT 对 opset 12 的支持很成熟过高的 opset 反而可能遇到算子兼容问题--dynamic 让导出的模型支持动态 shape--batch-size 1 固定 batch 为 1推理场景通常单张输入即可满足需求固定 batch 能减少 TensorRT 优化时的 shape 分支构建出的引擎更精简。--simplify 是很多人的习惯操作它不是 YOLOv5 内置参数而是配合 onnx-simplifier 使用。这个库会折叠常量、删除冗余节点让计算图更干净。我一般会跑一遍简化再转 TensorRT因为简化后的 ONNX 在 TensorRT 解析器眼里更容易被识别为可融合的模式。导出完成后用 netron 打开 ONNX 文件检查输出节点。YOLOv5 的输出通常是三个尺度的检测头shape 分别是 1×(3×85)×80×80、1×(3×85)×40×40、1×(3×85)×20×20以输入 640 为例。如果你看到输出被写死成固定 batch说明 --dynamic 没生效构建引擎时 batch 维度会被锁死。3.2 构建 TensorRT 引擎精度模式与工作空间参数设置ONNX 拿到手后下一步是把模型转成 .engine 引擎文件。构建可以在 Python 里调用 TensorRT 的 Python API 完成也可以用 trtexec 命令行工具。我习惯写一个 py 脚本原因是可以把日志输出和后处理验证串在一起。import tensorrt as trt logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(yolov5s.onnx, rb) as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) exit(1) config builder.create_builder_config() config.set_memory_pool_size(trt.MemoryPoolType.WORKSPACE, 2 20) config.set_flag(trt.BuilderFlag.FP16) engine builder.build_serialized_network(network, config) with open(yolov5s_fp16.engine, wb) as f: f.write(engine)代码逻辑分三步第一步创建 network 和 OnnxParser把 ONNX 文件解析成 TensorRT 自己的网络结构这一步如果解析失败会逐条打印错误信息绝大多数情况是某个算子不受支持第二步创建 builder 配置设置工作空间大小和 FP16 精度第三步构建引擎并序列化保存。参数说明set_memory_pool_size 是 TensorRT 10.x 的写法老版 8.x 用 config.max_workspace_size效果一样都是控制 kernel 选择阶段允许使用的显存上限。220 是 2GB对于 YOLOv5s 足够模型更大时可以提高到 4GB。FP16 是半精度推理开关GTX 1070 上开启后推理速度提升明显检测精度下降通常在 0.5 个 mAP 点以内大部分场景可接受。INT8 量化精度损失更大需要校准数据集不是首选。构建完成后会得到 yolov5s_fp16.engine这个文件就是后续 DLL 加载的模型本体。注意引擎文件与显卡绑定在 GTX 1070 上构建的引擎换到 RTX 3060 上无法直接加载需要重新构建。3.3 DLL 内部三件套初始化、推理与释放的 C 接口设计DLL 的对外接口设计是整个封装的关键。调用方只知道三个函数InitEngine 加载引擎、Detect 执行推理、ReleaseEngine 释放资源。内部细节全部隐藏。#include Windows.h #include fstream #include vector #include opencv2/opencv.hpp #include NvInfer.h using namespace nvinfer1; static IRuntime* g_runtime nullptr; static ICudaEngine* g_engine nullptr; static IExecutionContext* g_context nullptr; extern C __declspec(dllexport) int InitEngine(const char* engine_path) { std::ifstream file(engine_path, std::ios::binary); std::vectorchar data((std::istreambuf_iteratorchar(file)), {}); if (file.fail() || data.empty()) return -1; g_runtime createInferRuntime(Logger()); g_engine g_runtime-deserializeCudaEngine(data.data(), data.size()); if (!g_engine) return -2; g_context g_engine-createExecutionContext(); if (!g_context) return -3; return 0; }InitEngine 的逻辑是读引擎文件到内存缓冲区然后调用 createInferRuntime 创建运行时实例再反序列化引擎最后创建执行上下文。返回值用负数区分错误类型-1 是文件读取失败-2 是引擎反序列化失败-3 是上下文创建失败。调用方拿到非零返回值就知道问题出在哪个环节。推理函数是核心负责把输入图像经预处理送入引擎再把输出做 NMS 后处理。extern C __declspec(dllexport) int Detect(unsigned char* src, int img_w, int img_h, float* out_boxes, int* out_count) { // 输入为 BGR 三通道原始字节输出格式为 x1,y1,x2,y2,score,class_id 连续排列 const int input_w 640, input_h 640; float* gpu_input; float* gpu_output; int output_size 25200 * 6; // 25200 (80*80 40*40 20*20) * 3 cudaMalloc(gpu_input, input_w * input_h * 3 * sizeof(float)); cudaMalloc(gpu_output, output_size * sizeof(float)); // letterbox 预处理保持宽高比缩放不足部分用 114 填充 cv::Mat img(img_h, img_w, CV_8UC3, src); float scale std::min((float)input_w / img_w, (float)input_h / img_h); int new_w (int)(img_w * scale), new_h (int)(img_h * scale); cv::Mat resized; cv::resize(img, resized, cv::Size(new_w, new_h)); cv::Mat canvas(input_h, input_w, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect((input_w - new_w) / 2, (input_h - new_h) / 2, new_w, new_h))); // BGR 转 RGB归一化到 0~1HWC 转 CHW std::vectorcv::Mat channels; cv::split(canvas, channels); for (int i 0; i 3; i) channels[i].convertTo(channels[i], CV_32F, 1.0 / 255.0, -0.5); cv::Mat chw; cv::vconcat(channels, chw); cudaMemcpy(gpu_input, chw.data, input_w * input_h * 3 * sizeof(float), cudaMemcpyHostToDevice); // 推理 void* buffers[] { gpu_input, gpu_output }; g_context-enqueueV2(buffers, 0, nullptr); // 后处理解析 1x25200x6 的输出做置信度过滤和 NMS结果写成 out_boxes std::vectorfloat output(output_size); cudaMemcpy(output.data(), gpu_output, output_size * sizeof(float), cudaMemcpyDeviceToHost); // NMS 实现省略按 score 阈值过滤后填充 out_boxes *out_count 0; // post_process_and_nms(output, scale, pad_x, pad_y, out_boxes, out_count); cudaFree(gpu_input); cudaFree(gpu_output); return 0; }这个函数的输入输出设计有三点要注意。第一输入是图像原始字节和宽高DLL 内部自己完成所有预处理调用方不需要依赖 OpenCV 也不会和 DLL 内的 OpenCV 符号冲突。第二输出是平铺的 float 数组每组六个值分别是 x1、y1、x2、y2、score、class_id坐标已经是映射回原始图像尺寸的绝对坐标调用方直接画框就行。第三显存缓冲在函数内部申请和释放调用方感知不到 GPU 显存活动接口边界干净。释放函数顺序很重要必须先销毁上下文再销毁引擎最后销毁运行时顺序颠倒会在某些驱动版本下触发访问冲突。extern C __declspec(dllexport) void ReleaseEngine() { if (g_context) { delete g_context; g_context nullptr; } if (g_engine) { delete g_engine; g_engine nullptr; } if (g_runtime) { delete g_runtime; g_runtime nullptr; } }封装 DLL 时另一个关键点是 extern C 和 __declspec(dllexport) 组合前者避免 C 名字修饰后者告诉链接器导出该符号。如果省略 extern CGetProcAddress 时找不到函数名DLL 加载成功但调用失败这个问题很隐蔽。4. 避坑指南DLL 加载失败、引擎异常与版本冲突排查4.1 WinError 1114初始化例程失败先把依赖链查明白现象用 LoadLibrary 加载你的 yolov5_tensorrt.dll 时返回 NULLGetLastError 报 WinError 1114 “动态链接库初始化例程失败”或者报 Error 126 “找不到指定的模块”。原因DLL 初始化失败通常不是你的导出函数有问题而是 DLL 的依赖项在系统里缺失或版本不匹配。TensorRT 相关 DLL 依赖链条很长你的 DLL 依赖 nvinfer.dll、nvinfer_plugin.dll这两个依赖 cudart64_xx.dllCUDA 运行时、cublas、cudnn 等一堆库。任何一环找不到或者版本不匹配加载到一半就失败。解决不要用网上的 dll 修复工具去修这个那些工具给的是一个单独 dll 文件解决不了版本链问题。我一般用开源工具 Dependencies就是当年的 Dependency Walker 的替代品打开你的 DLL它会把完整依赖树列出来红头符号就是缺失项。优先检查三件事CUDA 的 bin 目录是否在 PATH 中TensorRT 的 lib 目录里 nvinfer.dll 和 nvinfer_plugin.dll 是否和编译时用的是同一版本运行时 DLL 是否都拷贝到了和业务 exe 相同的目录。最省心的做法是部署机上装好和开发机一致的 CUDA 版本然后把 TensorRT 的 dll 和你的 dll 放在同一个目录。4.2 GTX 1070 构建引擎失败TensorRT 版本不是越新越好现象在一台 GTX 1070 机器上跑构建脚本TensorRT 10.x 环境下报错 “could not find any kernel image to launch on device”或者构建时直接提示平台不受支持。原因前面说过Pascal 架构计算能力 6.1在 TensorRT 10.x 的支持列表里已经边缘化。TensorRT 10 的 kernel 库主要针对 Turing、Ampere、Ada 架构优化Pascal 对应 kernel 缺失时构建过程不报错但实际推理阶段会卡死或崩溃。解决把 TensorRT 降级到 8.5 或 8.6CUDA 保持在 11.x。这组搭配在 GTX 1070 上跑 YOLOv5s FP16 推理单张 640 输入延迟通常能控制在 15ms 以内完全够用。降级时注意同时替换 nvinfer.dll、nvinfer_plugin.dll 和头文件三件套必须同版本。4.3 CUDA 多版本共存PATH 顺序决定加载的是哪一个现象开发机装了 CUDA 12 又装了 CUDA 11DLL 编译链接都正常运行时加载 nvinfer.dll 失败报 cudart 版本冲突。原因Windows 加载 DLL 时按 PATH 顺序搜索如果 PATH 里 CUDA 12 的 bin 目录排在 CUDA 11 前面TensorRT 8.x 对应的 nvinfer.dll 会尝试加载 CUDA 12 的 cudart64_12.dll但 TensorRT 8.6 编译时链接的是 CUDA 11 的接口动态符号不匹配就会加载失败。解决把 CUDA 11 的 bin 目录调整到 PATH 最前面。或者更彻底一点编译 DLL 时把 CUDA 的 cudart64_11.dll、cublas64_11.dll 直接拷贝到 DLL 输出目录。Windows 加载 DLL 时会优先搜索 exe 和 dll 所在目录比 PATH 优先级高这样能根治 PATH 顺序问题。4.4 释放顺序颠倒显存上下文与流销毁的次序问题现象程序退出时在 ReleaseEngine 之后、进程结束前出现访问冲突或者下一次 InitEngine 时初始化失败报显存不足。原因IExecutionContext 销毁前与显存缓冲区、CUDA stream 的关联没有被正确解除。如果先删除了 runtime再删除 context会访问已经释放的内存。另一个常见场景是 Detect 函数内的 cudaMalloc 没有和 cudaFree 成对出现显存泄漏到第二次初始化时耗尽。解决严格遵循 context → engine → runtime 的销毁顺序。每个 Detect 内部申请显存后必须在函数返回前释放不要指望调用方帮你清理。另外注意 cudaSetDevice 的调用时机如果机器有多个 GPU第一次 InitEngine 前先 cudaSetDevice(0)ReleaseEngine 前也先切到同一个设备避免上下文和设备绑定错乱。5. 验证与接入把 DLL 接进 C 工程并做一致性测试5.1 隐式链接还是动态加载部署程序我选后者DLL 接入方式有两种隐式链接和显式加载。隐式链接是编译时在工程里配置 .lib 导入库程序启动时 Windows 自动加载 DLL显式加载是运行时调用 LoadLibrary GetProcAddress 获取函数指针。我部署时一律选显式加载。原因有两个。第一隐式链接下如果 DLL 缺失或依赖不齐程序直接无法启动错误提示往往是通用弹窗用户根本不知道怎么回事。显式加载时我能主动捕获 LoadLibrary 的返回值弹出自己的错误提示“推理引擎缺失或版本不匹配”。第二隐式链接固定了 DLL 导出的签名升级 DLL 时接口一变就得重新编译调用方显式加载只要函数名和参数约定不变换 DLL 文件即可。typedef int (*InitEngineFunc)(const char*); typedef int (*DetectFunc)(unsigned char*, int, int, float*, int*); HMODULE hDll LoadLibraryA(yolov5_tensorrt.dll); if (!hDll) { /* 记录错误并提示 */ } InitEngineFunc init (InitEngineFunc)GetProcAddress(hDll, InitEngine); DetectFunc detect (DetectFunc)GetProcAddress(hDll, Detect);GetProcAddress 失败时用 GetLastError 记录错误码配合之前提的 Dependencies 工具确认导出符号是否被 C 名字修饰破坏。5.2 输出对比让 DLL 与 PyTorch 基线同时跑同一张图接口接通之后第一件事不是测速度而是验证输出一致性。拿同一张测试图分别用原始 PyTorch 权重和封装好的 DLL 跑一次对比检测框是否一致。对比标准我一般这么定同一目标两套输出的检测框 IOU 大于 0.9置信度差异不超过 0.05 就算通过。FP16 推理和 PyTorch FP32 之间存在数值精度差异严格逐位对比没有意义0.9 的 IOU 阈值足以覆盖工程应用。如果发现坐标差异明显重点检查两个地方。一是 letterbox 的填充参数YOLOv5 训练时的灰度填充值是 114如果 DLL 里写成了 0 或者 127输出框在小目标上会偏移。二是坐标映射逻辑模型输出的坐标是相对于缩放后且填充过的 640 图像空间的要映射回原图需要减去填充偏移量再除以缩放比例这个系数算错会出现整体偏移。从那以后我每次拿到这种打包好的推理 DLL 资源都不会先急着进业务代码而是花半小时写一个独立测试程序把加载、推理、释放和输出对比完整走一遍。这半小时省下的排错时间远比想象中多。希望帮到你。本文还有配套的精品资源点击获取
返回列表