多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

YOLOv8 TensorRT C++部署实战:从环境配置到推理优化

YOLOv8 TensorRT C++部署实战:从环境配置到推理优化 简介这份资源面向具备一定C与深度学习基础的开发者聚焦YOLOv8模型在TensorRT上的C部署实践尤其针对X射线检测场景。内容围绕模型导入、Engine构建、推理实现与输入输出处理等环节展开帮助读者理解如何借助TensorRT加速YOLOv8的推理性能适用于实时目标检测与工业图像分析方向。压缩包共85个文件约379.2MB以h头文件、cpp源文件、vcxproj工程文件为主辅以ipch、tlog等编译中间文件以及jpg、png测试图像和sln解决方案文件整体构成一个可参考的Visual Studio工程结构。目前已有3362人学习下载。资源中包含模型权重转换脚本、C推理代码与X射线测试图像读者可据此对照工程目录理解TensorRT集成流程掌握精度模式选择、工作区设置等性能调优思路为实际项目部署提供可复用的参考。1. YOLOv8 上 TensorRT 的 C 部署为什么它是产线推理的必经之路训练完一个 YOLOv8 模型best.pt在 PyTorch 里跑得挺欢可一旦要落到产线相机、边缘盒子或者工控机上Python 那套推理链路就开始拖后腿GIL 卡吞吐、依赖一堆、启动慢、显存占用还高。这时候把模型转成 TensorRT 引擎再用 C 接进业务代码基本是工业视觉项目的默认答案。TensorRT 会把卷积、BN、激活这些层做融合按你显卡的架构选最优 kernel再配合 FP16 或 INT8 量化同一张卡上推理延迟经常能压到 PyTorch 的一半甚至更低。C 侧则负责取流、预处理、推理、后处理、画框、推流这一整条流水线没有解释器开销部署到客户机器上就是一个可执行文件加几个动态库。这篇笔记面向的是已经跑通 YOLOv8 训练、准备把它塞进 C 工程的工程师从环境、导出、推理到后处理把能抄的代码和会翻车的地方都讲清楚。2. 环境与依赖把 TensorRT、CUDA、OpenCV 三件套对齐2.1 版本对齐为什么比装软件本身更重要TensorRT 不是一个独立运行的库它和 CUDA、cuDNN、显卡驱动是绑死的。你装 TensorRT 8.6就得配 CUDA 11.8 或 12.x 的对应版本驱动版本又决定了你能用的 CUDA 上限。最常见的翻车就是nvcc --version显示 CUDA 12.2但 TensorRT 装的是给 CUDA 11.8 编译的包链接阶段直接报一堆undefined reference。所以第一步不是急着写代码而是先把版本矩阵定下来。我一般会按这个顺序确认先看显卡驱动支持的最高 CUDA 版本nvidia-smi右上角那个 CUDA Version再选一个 TensorRT 官方明确支持的 CUDA 版本最后让 OpenCV 也用同一个 CUDA 编译如果你要用 GPU 预处理。下面这张表是我在几台机器上验证过的组合供参考组件推荐版本说明显卡驱动535 及以上决定 CUDA 上限CUDA11.8兼容性最好TensorRT 8.6 官方支持cuDNN8.9.x必须和 CUDA 11.8 对应TensorRT8.6.x与 CUDA 11.8 配套的 tar 包OpenCV4.8需要 dnn 模块可选 CUDA 支持CMake3.20用于组织工程提示TensorRT 的 tar 包解压后要手动把lib加进LD_LIBRARY_PATH把include加进编译器的搜索路径别指望它像 apt 包一样自动配好。2.2 从零搭一个可编译的 CMake 工程工程目录我习惯这样组织include/放头文件src/放实现models/放引擎文件CMakeLists.txt在根目录。核心是让 CMake 找到 TensorRT 和 OpenCV。下面是一个能直接用的最小CMakeLists.txtcmake_minimum_required(VERSION 3.20) project(yolov8_trt_deploy CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # TensorRT 路径按你解压的实际位置改 set(TENSORRT_ROOT /opt/TensorRT-8.6.1.6) set(CUDA_ROOT /usr/local/cuda-11.8) find_package(OpenCV REQUIRED) find_package(CUDA REQUIRED) include_directories( ${TENSORRT_ROOT}/include ${CUDA_ROOT}/include ${OpenCV_INCLUDE_DIRS} ${CMAKE_SOURCE_DIR}/include ) link_directories(${TENSORRT_ROOT}/lib ${CUDA_ROOT}/lib64) add_executable(yolov8_trt src/main.cpp src/yolov8.cpp src/preprocess.cpp src/postprocess.cpp) target_link_libraries(yolov8_trt nvinfer nvinfer_plugin nvonnxparser cudart ${OpenCV_LIBS} )这段 CMake 的逻辑很直白把 TensorRT 和 CUDA 的头文件、库目录显式指出来然后链接nvinfer推理核心、nvinfer_plugin插件层YOLOv8 的某些算子会用到、nvonnxparser解析 ONNX 用。参数上唯一要改的就是TENSORRT_ROOT和CUDA_ROOT两个路径改成你机器上的实际位置。编译时如果报找不到nvinfer.h八成是TENSORRT_ROOT写错了如果链接阶段报undefined reference to cudart检查CUDA_ROOT/lib64是否在link_directories里。2.3 验证环境是否真的可用装完别急着写推理代码先跑一个最小验证用trtexec这个 TensorRT 自带的命令行工具随便拿个 ONNX 转一下看能不能出引擎。trtexec --onnxtest.onnx --saveEnginetest.engine --fp16如果这条命令能跑通并生成.engine文件说明 TensorRT 本身没问题。然后再写一个只调用cudaGetDeviceCount的 C 小程序确认 CUDA runtime 链接正常。这两步过了再往下走会省很多事。3. 从 PyTorch 到 ONNX 再到 TensorRT 引擎导出链路与参数3.1 YOLOv8 导出 ONNX 的正确姿势Ultralytics 的库自带导出命令但默认参数不一定适合 TensorRT。我一般用 Python 脚本显式控制导出过程而不是直接命令行因为要固定输入尺寸和 opsetfrom ultralytics import YOLO # 加载训练好的权重 model YOLO(best.pt) # 导出 ONNX关键参数逐个说明 model.export( formatonnx, imgsz(640, 640), # 固定输入尺寸TensorRT 需要静态 shape opset12, # opset 12 对 TensorRT 8.6 兼容性最好 simplifyTrue, # 用 onnxsim 简化计算图去掉冗余节点 dynamicFalse, # 关闭动态 batch产线一般固定 batch halfFalse, # 这里不量化量化留给 TensorRT 做 )这段代码里每个参数都有讲究。imgsz必须是元组固定成 640x640因为 TensorRT 构建引擎时需要知道确切的输入维度如果你用动态 shape后面构建引擎要配 optimization profile复杂度上一个台阶产线固定分辨率场景没必要。opset12是我踩过坑之后定下来的opset 11 有时会在某些算子转换上出问题opset 13 以上又可能引入 TensorRT 还没支持的算子。simplifyTrue会调用 onnxsim 做图优化能减少后面 TensorRT 解析时的报错概率。dynamicFalse和halfFalse都是为了把量化这一步留给 TensorRT因为 TensorRT 的 FP16/INT8 校准比 PyTorch 侧的伪量化更贴近实际部署。导出完成后你会得到一个best.onnx可以用 Netron 打开看一眼确认输入是images输出是output0形状大概是[1, 84, 8400]84 4 个框坐标 80 类8400 是 80x8040x4020x20 三个尺度的 anchor 总数。3.2 用 trtexec 构建引擎并读懂日志ONNX 有了接下来转 TensorRT 引擎。最省事的方式是trtexectrtexec \ --onnxbest.onnx \ --saveEnginebest_fp16.engine \ --fp16 \ --workspace4096 \ --minShapesimages:1x3x640x640 \ --optShapesimages:1x3x640x640 \ --maxShapesimages:1x3x640x640 \ --verbose参数逐个解释--fp16开启半精度GTX1660Ti 这类图灵卡对 FP16 支持很好速度提升明显--workspace4096给 4GB 显存做构建时的工作空间太小会导致某些层无法用最优 kernel三个 shape 参数即使你固定 batch 也要写TensorRT 8.x 要求显式声明。--verbose会打印每一层的融合情况第一次构建建议开着能看到哪些层被融合、哪些层 fallback 到 CUDA 实现。构建日志里要重点看两个东西一是Total Host Walltime和Total GPU Compute Time如果 GPU 时间远小于 Host 时间说明瓶颈在数据搬运二是Layer列表里有没有大量Reformat层这些是精度转换层太多会拖慢推理。如果发现某个算子没被 TensorRT 支持日志里会有Unsupported字样这时候要么换 opset 重新导出要么自己写 plugin。3.3 在 C 里加载引擎并做一次推理引擎文件有了C 侧第一步是把它读进内存、反序列化成ICudaEngine再创建IExecutionContext。下面这段是加载和推理的核心骨架#include NvInfer.h #include fstream #include iostream // TensorRT 日志器必须实现否则报错看不到 class Logger : public nvinfer1::ILogger { void log(Severity severity, const char* msg) noexcept override { if (severity Severity::kWARNING) std::cout [TRT] msg std::endl; } }; nvinfer1::ICudaEngine* loadEngine(const std::string path, Logger logger) { std::ifstream file(path, std::ios::binary); if (!file.good()) { std::cerr 引擎文件打不开 std::endl; return nullptr; } file.seekg(0, file.end); size_t size file.tellg(); file.seekg(0, file.beg); std::vectorchar buffer(size); file.read(buffer.data(), size); file.close(); nvinfer1::IRuntime* runtime nvinfer1::createInferRuntime(logger); nvinfer1::ICudaEngine* engine runtime-deserializeCudaEngine(buffer.data(), size); runtime-destroy(); // runtime 用完即可销毁engine 独立存活 return engine; }逻辑说明先把.engine文件整个读进vectorchar然后createInferRuntime创建运行时deserializeCudaEngine把二进制反序列化成引擎。注意runtime在反序列化之后就可以销毁引擎对象不依赖它。参数上唯一要注意的是文件必须以二进制模式打开否则 Windows 下会出问题。拿到 engine 后用engine-getNbBindings()和engine-getBindingDimensions(i)遍历输入输出分配对应的 GPU 显存然后context-enqueueV2或executeV2发起推理。第一次跑通建议先用一张固定图片把输入输出维度打印出来核对确认和 ONNX 侧一致。4. 预处理与后处理C 侧最容易翻车的两段4.1 letterbox 预处理别让缩放比例毁掉精度YOLOv8 训练时用的是 letterbox 填充推理时如果直接 resize 到 640x640长宽比一变框的位置就会系统性偏移。C 侧必须复刻同样的 letterbox 逻辑按长边缩放短边补灰边114,114,114并记录缩放比例和填充偏移后处理时再映射回原图坐标。cv::Mat letterbox(const cv::Mat src, int targetSize, float scale, int padW, int padH) { int w src.cols, h src.rows; scale std::min(targetSize / (float)w, targetSize / (float)h); int newW (int)(w * scale); int newH (int)(h * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH)); padW (targetSize - newW) / 2; padH (targetSize - newH) / 2; cv::Mat out(targetSize, targetSize, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(out(cv::Rect(padW, padH, newW, newH))); return out; }这段代码的关键是scale取长边比例保证整张图都能塞进去padW和padH是左右、上下各补多少后处理映射时要减掉。补边颜色必须是 114和训练时一致否则边缘区域的激活值会有偏差。做完 letterbox 还要做归一化除以 255和 BGR 转 RGB再转成 CHW 排布最后cudaMemcpy到 GPU 输入 buffer。这一串操作如果顺序错了模型输出会完全乱掉我见过有人忘了 BGR 转 RGB结果所有类别置信度都低得离谱。4.2 解码输出84x8400 到底怎么读YOLOv8 的输出是[1, 84, 8400]84 维里前 4 维是cx, cy, w, h后 80 维是类别分数。注意这里没有单独的 objectness类别分数直接就是最终置信度。解码时对每个 anchor 取 80 类里的最大值超过阈值就保留再把cx, cy, w, h从 letterbox 坐标系映射回原图。struct Detection { int classId; float conf; cv::Rect box; }; std::vectorDetection decode(float* output, float confThres, float scale, int padW, int padH, int origW, int origH) { std::vectorDetection dets; const int numAnchors 8400; const int numClasses 80; for (int i 0; i numAnchors; i) { float* cls output 4 * numAnchors i; // 类别分数起始位置 int bestId 0; float bestScore 0; for (int c 0; c numClasses; c) { float s cls[c * numAnchors]; if (s bestScore) { bestScore s; bestId c; } } if (bestScore confThres) continue; float cx output[i]; float cy output[numAnchors i]; float w output[2 * numAnchors i]; float h output[3 * numAnchors i]; // 映射回原图 float x (cx - w / 2 - padW) / scale; float y (cy - h / 2 - padH) / scale; dets.push_back({bestId, bestScore, cv::Rect(x, y, w / scale, h / scale)}); } return dets; }这里最容易错的是内存排布。输出是[1, 84, 8400]在内存里是行优先所以第c个类别的第i个 anchor 的分数在output[(4 c) * 8400 i]我上面写成cls[c * numAnchors]是因为cls已经偏移了4 * numAnchors。坐标映射公式是(坐标 - padding) / scale顺序不能反。解码完还要做 NMS按类别分组做 IoU 抑制阈值一般 0.45。如果发现框整体偏移先检查padW/padH有没有传对如果框大小不对检查scale是不是用了短边比例。4.3 用 OpenCV 画框和推流验证解码出Detection之后用cv::rectangle和cv::putText画到原图上再用cv::imshow或cv::VideoWriter输出。验证阶段我建议先用一张图跑把每个框的坐标打印出来和 Python 侧同图推理的结果对比误差在 1-2 像素内算正常。如果偏差大回头查预处理和后处理的坐标映射。视频流场景下把取帧、预处理、推理、后处理放在一个循环里注意 GPU 推理是异步的cudaMemcpyAsync要配cudaStreamSynchronize否则会读到上一帧的结果。5. 避坑与排查那些让我加班到凌晨的细节5.1 引擎加载报错 “serialization version mismatch”现象C 程序加载.engine时报错提示序列化版本不匹配。原因引擎文件是用另一个版本的 TensorRT 构建的TensorRT 的引擎格式不跨版本兼容。解决引擎必须在目标机器上用相同版本的 TensorRT 重新构建或者用trtexec在目标机上转一遍。别想着把开发机的引擎拷到部署机上直接用除非两边 TensorRT 版本完全一致。5.2 推理结果全是一类或置信度异常现象所有框都预测成同一个类别或者置信度普遍很低。原因预处理时 BGR 没转 RGB或者归一化系数用错YOLOv8 是除以 255不是减均值除方差。解决检查cv::cvtColor(src, rgb, cv::COLOR_BGR2RGB)有没有调用归一化是不是pixel / 255.0f。这个坑我踩过两次每次都是因为复制了旧项目的预处理代码。5.3 显存泄漏导致跑几百帧后崩溃现象程序跑一段时间后cudaMalloc失败或直接段错误。原因每帧都创建IExecutionContext或分配新的 GPU buffer没有复用。解决IExecutionContext和输入输出 buffer 在初始化时创建一次循环里只做cudaMemcpy和enqueue。如果要用多线程每个线程一个 context但 buffer 可以共享。5.4 FP16 引擎精度下降明显现象转 FP16 后 mAP 掉了好几个点。原因某些层对精度敏感FP16 的动态范围不够。解决用trtexec的--layerPrecisions或--precisionConstraints把敏感层强制回 FP32或者改用 INT8 加校准集。GTX1660Ti 上 FP16 一般没问题但如果你的模型有自定义算子要单独测。5.5 Windows 下fopen报安全错误现象在 Visual Studio 里用fopen读引擎文件编译报 C4996 错误。原因MSVC 把fopen标记为不安全。解决在文件开头加#define _CRT_SECURE_NO_WARNINGS或者改用std::ifstream。我一般直接用ifstream跨平台还省心。6. 进阶技巧用 CUDA 预处理和 INT8 校准把延迟再压一截当你把基础链路跑通之后如果延迟还达不到要求有两个方向可以继续挖。第一个是把预处理也搬到 GPU 上。现在 CPU 做 letterbox、归一化、HWC 转 CHW 这一串在 1080p 输入下可能占掉好几毫秒。常见做法是写一个 CUDA kernel把 resize、归一化、通道转换一次做完输出直接就是 GPU 上的float*省掉一次 Host 到 Device 的拷贝。这个 kernel 不复杂但要注意双线性插值的边界处理以及和 letterbox 的 padding 逻辑对齐。第二个方向是 INT8 量化。FP16 已经能带来明显加速但 INT8 在支持 TensorRT 的图灵卡上还能再快一截。INT8 需要校准集一般从训练集里抽 500 到 1000 张图用trtexec --int8 --calibcalibration.cache或者 C API 里的IInt8EntropyCalibrator2。校准集的质量直接决定精度损失我一般会覆盖所有类别和不同光照条件。校准完之后用验证集跑一遍 mAP和 FP32 对比掉点控制在 1 个点以内可以接受。验证方法上我习惯用trtexec --loadEnginebest.engine --shapesimages:1x3x640x640 --iterations100 --avgRuns10来测纯推理延迟这个数字排除了预处理和后处理能看出引擎本身的性能。然后再在完整 C 程序里测端到端延迟两个数字一对比就知道瓶颈在 GPU 还是 CPU。如果 GPU 时间远小于端到端时间优化重点就放在预处理和后处理上比如用 OpenCV 的cv::cuda模块或者自己写 kernel。最后说个我自己的习惯每次改完预处理或后处理代码一定先用同一张图、同一个引擎把 C 输出和 Python 输出逐元素对比确认数值一致再往下走。这个「后悔药」比出了 bug 再回头查省太多时间。部署这件事玄学不多大部分问题都是版本、坐标、内存这三类耐心对齐就好。希望帮到你。本文还有配套的精品资源点击获取
返回列表