首页 / 资讯中心 / 文章详情

TensorRT加速YOLO部署:从ONNX到engine的推理优化全攻略

TensorRT加速YOLO部署:从ONNX到engine的推理优化全攻略 ★ FEATURED ARTICLE
简介这是一套基于TensorRT加速引擎的YOLO部署项目同时实现实例分割与目标检测功能并提供C和Python两种开发语言版本支持在Linux与Windows系统上运行。资源包共含1523个文件主要内容有C头文件与源文件、Python脚本、工程构建配置、动态与静态库、可执行程序以及技术文档等压缩包整体约314MB目录结构清晰便于定位所需代码和资料。目前已有307人学习下载。这套资源适合需要部署目标检测能力的开发者、研究人员及企业工程人员可应用于实时监控、工业产品质检、智能交通、医疗图像辅助分析等场景。除完整工程外还附带了依赖库、运行环境和部署说明能够帮助使用者快速搭建TensorRT推理环境直接验证分割与检测效果并为后续跨平台集成和性能优化提供基础。1. TensorRT 部署 YOLO 值不值从 50ms 到 3ms 的差距与适合的人一条产线上用 YOLO 做瑕疵检测同样的 640×640 输入CPU 推理要 50ms 一帧换到 T4 上用 TensorRT 压一遍能到 3ms 左右。这不是模型变了是部署方式变了。这类问题在监控、工业质检、智能交通里几乎天天遇到模型训出来了跑不动或者跑起来的延迟撑不住多路视频流。这个资源解决的就是这一段——把 YOLO 的目标检测和实例分割同时部署到 TensorRT 上C 和 Python 两套代码都给好Linux 和 Windows 都能跑拿过来改改路径就能复现。适合三类人要把检测功能集成进现有项目的开发者需要验证算法落地性能的研究人员以及想把 YOLO 塞进监控和质检系统的实施工程师。2. 从 YOLO 权重到 TensorRT engineONNX 这层转换避不开2.1 为什么必须经过 ONNXYOLO 的权重一般是 PyTorch 的 .pt 文件TensorRT 不能直接消费它必须先把模型转成 ONNX再交给 TensorRT 的 parser 解析成 engine。这个过程看起来多了一层实际是给两端解耦PyTorch 只管训练TensorRT 只管推理ONNX 是中间语言。很多第一次接触的人试图跳过 ONNX 直接用 .pt 文件去 build engine浪费一晚上最后还是要回来走 ONNX 这条路。导出这一步通常用官方脚本完成。以 YOLOv8 为例导出检测模型和分割模型分别是yolo export modelyolov8s.pt formatonnx opset12 yolo export modelyolov8s-seg.pt formatonnx opset12opset 建议保持在 12 到 17 之间。TensorRT 对过新的 opset 支持不一定及时太旧的又可能丢失一些算子语义。如果你是 YOLOv5导出路径是python export.py --include onnx --opset 12原理一样只是脚本入口不同。导出完成后先用onnx库快速检查一下输出节点确认输出名和 shape这一步能省掉后面很多定位问题的时间。2.2 检测和分割的输出头不是一回事这是整个项目里最容易翻车的地方。目标检测模型转出的 ONNX 通常是一个输出形状是[1, 84, 8400]。这个 84 由 4 个框坐标cx, cy, w, h加 80 个 COCO 类别的置信度组成8400 是三个尺度特征图上的候选框总数80×80 40×40 20×20。实例分割模型的 ONNX 则有两个输出一个是[1, 116, 8400]其中 4 是框坐标80 是类别置信度多出来的 32 是每个候选框对应的 mask 系数另一个是[1, 32, 160, 160]这是原型 mask。最终掩码等于原型 mask 和 mask 系数的加权和后面 Python 和 C 后处理里都要用这个关系。拿到的资源包里如果同时有检测和分割两个任务建议先用 Netron 打开 onnx 文件看一眼输出头两个任务的输出个数和维度不同后处理代码也完全不同。写推理脚本前先确认输出头的名字和顺序不要盲信代码里的变量名。2.3 构建 enginetrtexec 和 Python API 两条路TensorRT 的 engine 构建有两种常见方式。第一种是直接用 trtexec 命令行适合快速验证转换链路通不通trtexec --onnxyolov8s-seg.onnx --saveEngineyolov8s-seg.engine --fp16 --memPoolSizeworkspace:2G--fp16开启半精度推理对 YOLO 这类模型一般能带来 30% 到 50% 的提速精度损失很小--memPoolSizeworkspace:2G限制构建时的工作空间显存小的卡建议调低到 1G。如果后续要在多路视频流场景下跑建议在 trtexec 命令里加上--minShapesimages:1x3x640x640 --optShapesimages:4x3x640x640 --maxShapesimages:8x3x640x640让 engine 支持动态 batch。注意动态 batch 的 engine 在 Python 和 C 代码里都要手动调用set_input_shape否则推理时直接报错。第二种是用 Python API 在程序里构建适合要做批量化处理或需要精细控制构建参数的场景import tensorrt as trt logger trt.Logger(trt.Logger.INFO) builder trt.Builder(logger) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, logger) with open(yolov8s-seg.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 20) config.set_flag(trt.BuilderFlag.FP16) engine builder.build_serialized_network(network, config) with open(yolov8s-seg.engine, wb) as f: f.write(engine)重点是EXPLICIT_BATCH这个标志YOLO 系列模型的 ONNX 导出默认是动态维度不加这个标志没办法解析输入维度。构建完的 engine 是一个序列化文件里面包含推理时的 kernel 调度方案和权重但注意它绑定当前 GPU 架构和 TensorRT 版本换机器换驱动就要重建这是后面坑章里要展开的事。3. 环境搭建Linux 与 Windows 的版本矩阵与两种工程的骨架3.1 CUDA、cuDNN、TensorRT 的版本对应关系TensorRT 对版本敏感这个项目的坑一半埋在环境里。老手会先看 NVIDIA 官方兼容矩阵这里给几个我实际跑通过的组合方便你对照自己的显卡驱动来选组合CUDAcuDNNTensorRT适合场景组合 A11.68.48.4.1Ubuntu 20.04老卡 T4 / V100组合 B11.88.68.5.3我常用的组合稳定性最高组合 C12.08.98.6.140 系新卡Windows 原生跑一个原则如果已经装了 CUDA 12.x就别硬塞 TensorRT 8.4版本不匹配的典型症状是 engine 节点报错或者构建到一半崩掉。T4 这类数据中心卡用组合 A 或 B 都行消费级 30 系、40 系卡优先考虑 CUDA 11.8 或 12.0。3.2 Linux 端Ubuntu 上的安装与路径配置Linux 下推荐用 tar.gz 包解压安装不用 deb因为 deb 会把库文件散到系统目录里后续找路径麻烦。TensorRT 的 tar.gz 解压后核心库在lib/下头文件在include/下把这两个目录加到环境变量即可export TRT_RELEASE/opt/TensorRT-8.5.3.1 export LD_LIBRARY_PATH$TRT_RELEASE/lib:$LD_LIBRARY_PATH export CUDA_HOME/usr/local/cuda-11.8装好之后用python3 -c import tensorrt; print(tensorrt.__version__)验证 Python 端用trtexec --version验证命令行工具。如果 import 报错基本是LD_LIBRARY_PATH没包含 TensorRT 的 lib 目录这是 ubuntu 安装 tensorrt 最常见的状况。CMake 工程里要做的事很简单把 TensorRT 的 include 和 lib 路径显式指出来不要指望系统自动找到。set(TRT_INCLUDE_DIR /opt/TensorRT-8.5.3.1/include) set(TRT_LIB_DIR /opt/TensorRT-8.5.3.1/lib) target_include_directories(yolo_infer PRIVATE ${TRT_INCLUDE_DIR}) target_link_libraries(yolo_infer PRIVATE nvinfer nvonnxparser)注意链接的是nvinfer和nvonnxparser两个库只链nvinfer的话OnnxParser 相关代码会链接失败。3.3 Windows 端VS 工程与 DLL 依赖Windows 上跑这个项目建议别用 WSL 绕TensorRT 的 Windows 原生库表现更稳定而且资源包里 C 工程本来就是按 Windows 目录结构组织的。Visual Studio 2019 或 2022 的工程里需要配置三个地方VC 目录 → 包含目录指向 TensorRT 的include库目录指向lib然后在链接器输入里加上nvinfer.lib、nvonnxparser.lib、cudart.lib。Windows 下最隐蔽的坑是 DLL 路径。TensorRT 的bin目录和 CUDA 的bin目录必须加到系统 PATH否则编译通过但运行时提示找不到nvinfer.dll或者找不到cudart64_*.dll。项目代码没问题、报错却发生在启动阶段优先查这个。Python 端在 Windows 下需要额外注意 pycuda 的版本要和 CUDA 匹配pip install pycuda装出来的默认版本可能不匹配你装的 CUDA建议用源码编译或者直接找对应 CUDA 版本的预编译包。如果只是跑 Python 脚本不碰 pycuda用 tensorrt 自带绑定也行。4. Python 推理实现engine 加载、letterbox 预处理与后处理解码4.1 engine 加载与输入输出 buffer 分配Python 端的推理骨架不复杂核心就三步反序列化 engine、创建 context、分配 host 和 device 端的 buffer。import tensorrt as trt import numpy as np import ctypes logger trt.Logger(trt.Logger.INFO) runtime trt.Runtime(logger) with open(yolov8s-seg.engine, rb) as f: engine runtime.deserialize_cuda_engine(f.read()) context engine.create_execution_context() context.set_input_shape(images, (1, 3, 640, 640))这里set_input_shape是必须的因为前面导出时模型是动态维度不在推理前显式指定execute_v2会直接抛错。绑定名称不要写死用engine.get_tensor_name(i)遍历拿不同版本导出时输出节点命名可能不一样写死字符串在换模型文件时容易出问题。4.2 letterbox 预处理比例、填充和坐标还原YOLO 系列模型的预处理要求把任意尺寸原图等比缩放后填充成 640×640这个操作叫 letterbox。缩放比例和填充尺寸必须在预处理时记录后处理还原框坐标时要用同一套参数。def letterbox(img, new_shape(640, 640), color(114, 114, 114)): h, w img.shape[:2] ratio min(new_shape[0] / h, new_shape[1] / w) new_h, new_w int(h * ratio), int(w * ratio) dw, dh (new_shape[1] - new_w) // 2, (new_shape[0] - new_h) // 2 resized cv2.resize(img, (new_w, new_h), interpolationcv2.INTER_LINEAR) canvas np.full((new_shape[0], new_shape[1], 3), color, dtypenp.uint8) canvas[dh:dh new_h, dw:dw new_w] resized return canvas, ratio, (dw, dh) img, ratio, (dw, dh) letterbox(original_img) blob img.transpose(2, 0, 1)[None].astype(np.float32) / 255.0最后一步除以 255 的归一化不可省YOLO 在训练时的输入就是归一化后的推理时不归一化置信度输出会整体漂移。ratio和(dw, dh)先存好后处理时每个框都要用它换算回原图坐标这就是新手最容易漏掉的地方。4.3 检测后处理把 8400 个候选框裁成最终结果推理执行后拿到输出形状是[1, 84, 8400]维度顺序是[C, N]不是[N, C]。常规做法是先转置成[8400, 84]再做解算忽略这个布局的后果是置信度和坐标混在一起画出来的框全是乱的。outputs [context.get_tensor(name) for name in output_names] det_out outputs[0][0].transpose(1, 0) # [8400, 84] class_ids det_out[:, 4:].argmax(axis1) confidences det_out[:, 4:].max(axis1) mask_conf outputs[0][0][4 80:4 80 32] # [32, 8400] keep np.where(confidences 0.25)[0] boxes det_out[keep, :4] scores confidences[keep] cls class_ids[keep]YOLOv8 输出的框坐标是cx, cy, w, h需要换算成x1, y1, x2, y2再按 letterbox 的 ratio 和 pad 还原回原图。还原之后用torchvision.ops.nms或cv2.dnn.NMSBoxes做非极大值抑制NMS 的 IoU 阈值一般取 0.45 到 0.5置信度阈值取 0.25 是起步值实际部署按误检和漏检的容忍度去调。4.4 实例分割 mask 还原原型和系数的矩阵乘法实例分割多出来的工作是把 mask 系数和原型 mask 组合起来。这一步的公式是掩码等于 sigmoid 后的原型矩阵与系数矩阵相乘得到一个[160, 160]的掩码再缩放到原始尺寸。proto outputs[1][0] # [32, 160, 160] coeff mask_conf[:, keep] # [32, num_keep] coeff coeff.transpose(1, 0) # [num_keep, 32] masks (proto.reshape(32, -1).T coeff.T).T masks 1.0 / (1.0 np.exp(-masks)) # [num_keep, 160*160]masks每一行是一个候选框的 mask注意 reshape 成[160, 160]后只是 letterbox 画布上的结果需要按前面记录的 ratio 和 pad 裁剪出原图中的有效区域。实例分割的 mask 是低分辨率的160×160边缘会有锯齿如果项目对边缘质量要求高可以在还原后做一次轻微的高斯模糊再二值化。这一步放在 Python 端做没问题C 端就是把这个矩阵乘法用原生循环实现后面一章拆开讲。5. C 推理实现IRuntime、IExecutionContext 与双语言对齐的细节5.1 C 端加载 engine 的标准骨架C 端的推理流程和 Python 完全对应只是所有显存操作要手动管理。加载 engine 用IRuntime接口反序列化得到ICudaEngine再从 engine 创建IExecutionContextIRuntime* runtime createInferRuntime(logger); std::ifstream file(yolov8s-seg.engine, std::ios::binary); std::vectorchar data(std::istreambuf_iteratorchar(file), {}); ICudaEngine* engine runtime-deserializeCudaEngine(data.data(), data.size()); IExecutionContext* context engine-createExecutionContext(); const char* inputName engine-getIOTensorName(0); context-setInputShape(inputName, Dims4{1, 3, 640, 640});getIOTensorName的作用是遍历拿到输入输出节点的名字不要假设第 0 个一定是输入某些模型导出后输出排在前面。setInputShape和 Python 端一样只对导出时声明了动态轴的 ONNX 模型才需要调用如果你的 engine 是从静态 batch 的 onnx 构建的这一步可以跳过。5.2 解码循环和 NMSC 端的布局与性能账C 端的解码要比 Python 多一层思考输出 buffer 是连续内存你要自己算清楚每个元素的位置。YOLOv8 检测头输出布局是[C, N]第 4 到第 83 行是类别分数遍历时按列读数据void decodeYolo(const float* output, int numBoxes, int numClasses, float confThres, std::vectorBox boxes) { for (int i 0; i numBoxes; i) { const float* ptr output i * (4 numClasses); float bestClass 0.0f; int bestIdx -1; for (int j 0; j numClasses; j) { float score ptr[4 j]; if (score bestClass) { bestClass score; bestIdx j; } } if (bestClass confThres) continue; Box b; b.cx ptr[0]; b.cy ptr[1]; b.w ptr[2]; b.h ptr[3]; b.score bestClass; b.label bestIdx; boxes.push_back(b); } }注意这段代码里索引的计算方式因为内存是连续的第 i 个候选框的起始位置是i * (4 numClasses)类别分数从4开始。配好缓存命中和循环展开这个解码循环在 8400 个候选框规模下耗时一般不到 1ms属于可忽略的开销。NMS 在 C 端不推荐自己写 O(n²) 循环工程上我一般直接用std::sort按置信度排序再做线性的重叠剔除数据量小逻辑清晰调试也方便。追求极致性能可以后续换成 GPU 端 NMS 插件但先跑通链路更重要。5.3 与 Python 对齐时的三个检查点C 和 Python 两套代码如果跑出来的结果不一样优先排查三个点。第一是预处理参数ratio、dw、dh的计算方式两边必须完全一致比如填充值的默认颜色是 114 还是 0两边不一致会导致同一张图推理结果完全不同。第二是输出 buffer 的数据精度。如果构建 engine 时开了--fp16输出端的数据类型可能是FP16C 端如果没有把半精度转成 float 就直接按float*去解析拿到的是垃圾数据这种情况 log 完全没有报错但检测结果全乱。处理方式是在拿到输出 buffer 后先查engine-getTensorFormat(outputName)是 FP16 就做一次 half 转 float 的转换。第三是坐标还原的逻辑。C 端还原坐标时x (cx - dw) / ratio这行公式要写在 decode 之后、NMS 之前还是之后直接影响了画的框在原图上的位置。我的习惯是在 decode 阶段就把坐标还原回原图再进 NMS这样后续不管接的是 OpenCV 画框还是传给别的模块拿到手的都是原图坐标也方便调试。6. 部署避坑与验证trtexec 基准、三类高频报错的排查顺序先把验证这一步放在最前面。任何一次改造之后先用 trtexec 跑一个基准拿到整机延迟和吞吐的锚点再判断自己的代码写的对不对trtexec --loadEngineyolov8s-seg.engine --shapesimages:1x3x640x640输出里的GPU Compute Time是单帧推理延迟Throughput是吞吐。如果你的程序整体延迟比 trtexec 高了一倍以上先怀疑前处理或后处理耗时不要盯着推理代码改。常见问题排查顺序按我实际踩过的频率排第一engine 换机器就起不来。现象是deserializeCudaEngine返回空指针或者推理时输出全 0。原因是 TensorRT 的 engine 文件和 GPU 架构、TensorRT 版本、驱动版本都绑定不是跨平台通用的可执行文件。解决方法是每台机器各自用 trtexec 重新构建不要把构建好的 engine 文件直接拷到别的机器上这点在 Linux 和 Windows 之间尤其明显。第二检测框整体偏移但置信度正常。现象是框能框住物体但位置偏左上或偏右下主要是 letterbox 的 pad 计算和后处理还原时用的不是同一组dw、dh。解决方式是把预处理参数和还原公式放到同一个公共段代码里两边共用不要在 Python 里写一遍、C 里再重新推导一遍。从那以后我每次部署都强制走一遍 trtexec 基准验证然后单独检查预处理参数是否双语言一致这个习惯帮我省了不少排查时间。第三显存缓慢增长和推理耗时抖动。现象是程序跑几个小时之后显存溢出或者每隔一段时间单帧耗时翻倍。原因一般是循环里反复创建IExecutionContext或者 host 端 buffer 用了普通内存导致拷贝阻塞。解决方法是 context 创建一次复用显存 buffer 也提前分配好输入输出走 pinned memory推理结束后记得释放 engine 和 context 对象别偷懒不写清理。另外一个容易被忽略的陷阱是动态 shape 下没设置最优 batch。多路视频流场景建议把检测输入做成[N, 3, 640, 640]的 batch 推理N 根据显存来定T4 上常见做法是 4 路或 8 路一个 batch。配合 RTSP 拉流四路的耗时通常只比单路多 20% 到 30%吞吐却接近线性上涨。如果后续要在 Jetson 这类嵌入式设备上跑DLA 核的调度和 INT8 量化校准是另一套门道但前提是先把这套 C / Python 双端跑通。资源包里已经帮这两套语言写好了对应代码拿到手后建议先看 docs 目录里的部署文档把版本矩阵里的组合三选一配好再从 Python 推理开始验证最后切到 C 做集成这条路走完基本能把 70% 的坑提前避开。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站