
1. 为什么要把 YOLOv4-tiny 塞进 TensorRT做过边缘端视觉项目的人大概都有这种体会模型在服务器上跑得好好的一上嵌入式设备就原形毕露。YOLOv4-tiny 本身已经算是轻量级选手了权重文件才 20 多 MB在普通 CPU 上跑个十几帧也不是不行但一旦你对帧率有要求比如要稳定 30 FPS 以上或者要同时跑多路视频流纯 CPU 推理就完全不够看了。这时候 TensorRT 就派上用场了。它是专门为推理阶段做加速的框架核心思路是把训练好的网络重新编译、融合算子、量化精度最终生成一个针对特定硬件高度优化的推理引擎。我实测下来同一个 YOLOv4-tiny 模型从 Darknet 原始权重转成 TensorRT 引擎之后在同类 GPU 上的推理速度通常能提升 3 到 8 倍具体倍数取决于输入分辨率、精度模式FP32/FP16/INT8以及后处理是否也做了加速。这个内容适合谁看如果你手头有 Jetson 系列设备、或者带独立 GPU 的工控机正在做目标检测相关的落地项目又恰好选了 YOLOv4-tiny 这个模型那这篇经验基本可以直接抄作业。如果你只是想了解 TensorRT 的通用流程里面的算子融合思路、精度校准方法、后处理加速技巧同样有参考价值。我会把整个链路拆开讲从权重导出、ONNX 转换、引擎构建一直到后处理的 CUDA 加速和实际部署中的坑尽量让不同基础的人都能跟着走一遍。需要提前说明的是TensorRT 的版本兼容性是个老大难问题不同版本之间 API 差异不小。我下面讲的操作以 TensorRT 8.x 为主线如果你用的是 7.x 或者更早的版本部分接口需要对照官方文档做调整。另外整个流程涉及 Python 和 C 两种语言Python 用来做模型转换和验证C 用来做最终部署这个分工是比较常见的做法。2. 整体转换链路的设计思路2.1 为什么不能直接从 Darknet 权重转 TensorRT很多人第一反应是Darknet 的 .weights 文件能不能直接喂给 TensorRT答案是不行。TensorRT 的输入格式是它自己定义的网络描述或者 ONNX 这类中间表示它不认识 Darknet 的权重格式。所以中间必须有一个翻译环节而 ONNX 就是目前最通用的那个中间人。整个链路我习惯分成四段第一段是把 Darknet 的 .weights 和 .cfg 转成 ONNX 模型第二段是对 ONNX 做必要的图优化和检查第三段是用 TensorRT 的解析器把 ONNX 吃进去构建出序列化的引擎文件第四段是写推理代码加载引擎、做前处理、跑推理、做后处理。这四段里第一段和第三段是最容易出问题的后面会重点讲。为什么不直接用 TensorRT 自带的 Darknet 解析器早期版本确实有但它对 YOLOv4-tiny 这种带 LeakyReLU、上采样、route 层的结构支持得并不好经常解析失败或者结果对不上。用 ONNX 作为中间层虽然多了一步但可控性强得多出了问题也容易定位。2.2 ONNX 作为中间层的取舍选 ONNX 不是没有代价的。ONNX 的算子集和 Darknet 的层类型并不是一一对应的转换过程中需要做映射。比如 Darknet 里的 route 层特征拼接在 ONNX 里对应 Concatupsample 层对应 Resize这些映射大部分开源转换脚本已经处理好了但版本差异会导致某些算子属性对不上。另一个取舍是动态维度。ONNX 支持动态 batch 和动态输入尺寸但 TensorRT 在构建引擎时如果你开了动态 shape优化空间会受限性能可能不如固定 shape。我的建议是如果你的部署场景输入尺寸固定就老老实实写死别为了灵活性牺牲性能。如果确实需要多尺寸再考虑用 optimization profile 来配置。还有一个容易被忽略的点是 NMS非极大值抑制放不放进 ONNX 图里。放进去的好处是后处理也在 GPU 上跑省去了 CPU 和 GPU 之间的数据搬运坏处是 TensorRT 对 NMS 算子的支持在不同版本里差异很大而且调试起来麻烦。我个人的做法是先把 NMS 留在外面用 CPU 做等整条链路跑通了再考虑把 NMS 也搬进引擎里做加速。2.3 精度模式的选择逻辑TensorRT 支持 FP32、FP16、INT8 三种精度。FP32 最稳但速度最慢FP16 在支持它的硬件上几乎无损速度能翻倍INT8 最快但需要校准精度损失取决于校准集的质量。对于 YOLOv4-tiny 这种小模型我的经验是如果硬件支持 FP16直接上 FP16基本不用犹豫精度掉不了多少速度提升很明显。INT8 就要谨慎了小模型对量化误差更敏感尤其是检测小目标的时候mAP 可能掉好几个点。如果非要用 INT8校准集一定要覆盖实际场景的各种光照、角度、遮挡情况不能随便拿几百张图糊弄。下面这张表是我在不同精度模式下实测的对比硬件是某款带 Tensor Core 的嵌入式 GPU输入 416x416batch 为 1精度模式推理耗时(ms)mAP0.5引擎大小(MB)FP328.20.64124FP164.10.63913INT82.60.6127可以看到 FP16 几乎无损INT8 掉了将近 3 个点。这个差距在要求不高的场景可以接受但在安防、工业检测这类对漏检零容忍的场景我一般还是选 FP16。3. 从权重到 ONNX 的关键细节3.1 环境准备与版本对齐这一步最烦人的就是版本。我踩过最大的坑就是 ONNX 版本和 PyTorch 版本不匹配导致转换脚本报一堆莫名其妙的错。我的建议是固定一套经过验证的组合不要盲目追新。比如 PyTorch 1.8 配 ONNX 1.9或者 PyTorch 1.10 配 ONNX 1.11这些组合在社区里验证得比较多。安装的时候注意onnx 和 onnxruntime 是两个包转换只需要 onnx但验证转换结果对不对需要 onnxruntime。另外如果你要用 GPU 跑 onnxruntime 做对比验证还要装 onnxruntime-gpu这个包和 onnxruntime 不能共存装之前先卸干净。pip install torch1.10.0 pip install onnx1.11.0 pip install onnxruntime1.11.0提示如果你在转换时遇到 Unsupported ONNX opset version 之类的报错八成是 opset 版本设高了。YOLOv4-tiny 用 opset 11 或 12 就够了别设到 13 以上很多解析器还没跟上。3.2 转换脚本的核心改动点网上流传的 YOLOv4-tiny 转 ONNX 脚本大多基于某位开发者的开源实现但直接拿来用经常会遇到两个问题一是输出层名字对不上二是某些层的属性缺失。我一般会在脚本里做三处改动。第一处是明确指定输出节点。YOLOv4-tiny 有两个检测头对应两个输出。转换时要显式告诉脚本哪两个节点是最终输出否则 ONNX 图里会多出一堆无用的后处理节点。通常这两个输出节点的名字类似output_0和output_1但不同脚本命名不一样转换完要用 Netron 打开看一眼确认。第二处是处理 LeakyReLU 的 negative_slope。Darknet 里这个值是 0.1但有些转换脚本默认写成 0.01导致转换后精度对不上。这个细节很隐蔽因为模型能跑只是结果偏了不仔细对比根本发现不了。第三处是固定输入尺寸。在转换脚本里把输入 shape 写死成[1, 3, 416, 416]不要留动态维度。这样后面构建 TensorRT 引擎时省事很多。# 转换后验证输出节点 import onnx model onnx.load(yolov4-tiny.onnx) for node in model.graph.output: print(node.name, node.type.tensor_type.shape)3.3 转换结果的验证方法转完 ONNX 千万别急着往下走一定要先验证。验证分两步第一步是结构验证用 onnx.checker 检查模型是否合法第二步是数值验证用同一张输入图片分别跑原始 Darknet 和 ONNX 模型对比输出差异。数值验证这一步很多人跳过结果后面 TensorRT 推理结果不对回头排查浪费大量时间。我的做法是准备一张标准测试图记录 Darknet 推理出的检测框坐标和置信度然后跑 ONNX 推理逐元素对比。如果最大误差在 1e-3 以内说明转换没问题如果误差很大多半是某个层的属性映射错了。import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov4-tiny.onnx) input_name sess.get_inputs()[0].name # 假设 img 是预处理好的 NCHW 输入 outputs sess.run(None, {input_name: img}) print(outputs[0].shape, outputs[1].shape)注意ONNX 推理和 Darknet 推理的前处理必须完全一致包括归一化方式、通道顺序RGB 还是 BGR、letterbox 的填充值。这些细节不一致数值对比就没有意义。4. 构建 TensorRT 引擎的实操过程4.1 解析 ONNX 与网络构建TensorRT 构建引擎有两种方式一种是直接用 trtexec 命令行工具另一种是写 Python 或 C 代码调用 API。trtexec 适合快速验证代码方式适合集成到项目里。我一般先用 trtexec 跑一遍确认模型能正常解析再写代码。trtexec 的基本用法是这样trtexec --onnxyolov4-tiny.onnx \ --saveEngineyolov4-tiny-fp16.engine \ --fp16 \ --workspace1024 \ --verbose这里几个参数值得说一下。--workspace是给 TensorRT 做层融合和算法选择用的显存上限单位是 MB。设太小会导致某些优化无法进行设太大又浪费显存。对于 YOLOv4-tiny 这种小模型1024 MB 足够了。--verbose会打印每一层的构建信息第一次构建时建议开着方便看哪些层被融合了、哪些层回退到了 FP32。构建过程中你会看到 TensorRT 把 Conv BN LeakyReLU 融合成一个节点这就是它加速的核心手段之一。融合之后原本三次显存读写变成一次速度自然就上去了。YOLOv4-tiny 里这种可融合的结构很多所以加速比才这么明显。4.2 动态 Shape 的配置方法如果你的场景确实需要动态输入比如同时处理不同分辨率的视频流那就得配置 optimization profile。这个配置告诉 TensorRT 你的输入尺寸范围它会针对这个范围做优化。profile builder.create_optimization_profile() profile.set_shape(input, min(1, 3, 320, 320), opt(1, 3, 416, 416), max(1, 3, 608, 608)) config.add_optimization_profile(profile)min、opt、max 三个尺寸分别是最小、最优、最大。TensorRT 会以 opt 尺寸为主做优化所以 opt 要设成你最常用的那个尺寸。这里有个坑动态 shape 下某些依赖固定尺寸的算子比如某些版本的 Resize可能构建失败遇到这种情况要么换算子实现要么退回固定 shape。4.3 序列化与反序列化引擎构建好之后要序列化成文件保存这样下次启动直接加载不用重新构建。构建过程可能耗时几十秒到几分钟部署时肯定不能每次启动都重建。# 序列化 with open(yolov4-tiny.engine, wb) as f: f.write(engine.serialize()) # 反序列化 with open(yolov4-tiny.engine, rb) as f: runtime trt.Runtime(trt.Logger(trt.Logger.WARNING)) engine runtime.deserialize_cuda_engine(f.read())注意引擎文件和构建时的 TensorRT 版本、GPU 型号是绑定的。换设备或者升级 TensorRT 之后引擎必须重新构建不能直接拿来用。这个限制经常被忽略导致部署时莫名其妙报错。5. 推理代码与后处理加速5.1 内存绑定与异步推理TensorRT 推理的核心是绑定输入输出缓冲区然后调用 execute 或 execute_async。同步推理简单但效率低实际部署我推荐用异步配合 CUDA Stream 可以做到数据拷贝和计算重叠。内存分配要用cudaMallocHost分配页锁定内存pinned memory这样 CPU 和 GPU 之间的拷贝速度会快很多。普通内存拷贝要走 PCIe页锁定内存可以用 DMA 直接传输带宽能差好几倍。# 分配输入输出缓冲 h_input cuda.pagelocked_empty(trt.volume(engine.get_binding_shape(0)), dtypenp.float32) h_output cuda.pagelocked_empty(trt.volume(engine.get_binding_shape(1)), dtypenp.float32) d_input cuda.mem_alloc(h_input.nbytes) d_output cuda.mem_alloc(h_output.nbytes) bindings [int(d_input), int(d_output)]5.2 后处理的 CUDA 加速思路YOLOv4-tiny 的输出是两个尺度的特征图每个格点预测若干框后处理要做的是解码坐标、过滤低置信度、执行 NMS。这部分如果放在 CPU 上做当检测框数量多的时候会成为瓶颈。加速思路有两种一是把整个后处理写成 CUDA kernel二是用 TensorRT 的插件机制把 NMS 集成进引擎。前者灵活但开发量大后者省事但受版本限制。我一般先用 CPU 版本跑通测出后处理耗时占比如果超过总耗时的 30%再考虑上 CUDA。CPU 版本的后处理也有优化空间。比如用 NumPy 向量化代替 Python 循环用np.where一次性筛出候选框比逐个判断快得多。NMS 可以用cv2.dnn.NMSBoxes它底层是 C 实现比纯 Python 快。# 向量化筛选候选框 scores outputs[:, 4] mask scores conf_threshold boxes outputs[mask, :4] scores scores[mask] indices cv2.dnn.NMSBoxes(boxes.tolist(), scores.tolist(), conf_threshold, nms_threshold)5.3 前处理的 letterbox 实现前处理里 letterbox 是个容易被忽视的性能点。它的作用是把任意尺寸的输入图等比缩放到模型输入尺寸不足的部分用灰边填充保证不拉伸变形。如果用 Python 逐像素操作一张 1080p 的图可能要几十毫秒。优化方法是把 resize 和 padding 分开做。resize 用cv2.resize它底层是 SIMD 优化的很快padding 用np.full预分配一块固定尺寸的缓冲区然后把 resize 后的图贴进去。这样比一步到位做 letterbox 快不少。def letterbox(img, new_shape(416, 416), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw (new_shape[1] - new_unpad[0]) / 2 dh (new_shape[0] - new_unpad[1]) / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (left, top)6. 常见问题与排查技巧实录6.1 精度对不上的排查顺序转换之后精度对不上是最常见的问题排查要有顺序不能瞎试。我的排查顺序是这样的第一步确认前处理一致。Darknet 用的是 BGR、除以 255、letterbox 填充 0.5ONNX 和 TensorRT 推理时如果用了 RGB 或者填充 0结果肯定偏。这一步用同一张图对比预处理后的张量就能确认。第二步确认输出节点正确。用 Netron 打开 ONNX看输出节点是不是两个检测头有没有多出后处理节点。如果输出节点错了后面全错。第三步确认 LeakyReLU 的 slope。前面提过0.1 和 0.01 的差异在小模型上会被放大。第四步逐层对比。如果前三步都没问题那就得用 TensorRT 的逐层输出功能对比每一层的输出和 ONNX 的差异定位到具体哪一层开始偏。下面这张表是我整理的高频问题速查现象可能原因排查方法检测框位置整体偏移letterbox 参数不一致对比预处理后的张量置信度普遍偏低LeakyReLU slope 错误检查转换脚本中的 slope 值某些类别完全检测不到输出节点顺序颠倒交换两个输出头的解析顺序引擎构建失败opset 版本过高降到 opset 11 重新导出推理结果全为 NaN输入未归一化确认输入值在 0-1 范围速度没有提升层融合未生效查看 verbose 日志中的融合信息6.2 引擎构建失败的典型原因引擎构建失败的原因五花八门但高频的就那么几个。一是显存不够workspace 设太大或者 GPU 显存本身太小解决办法是调小 workspace 或者换设备。二是算子不支持某些 ONNX 算子 TensorRT 没有对应实现需要自定义插件或者改写网络结构。三是动态 shape 配置冲突min 和 max 跨度太大导致优化失败。我遇到过一次很隐蔽的失败ONNX 模型里有一个 Resize 算子的 coordinate_transformation_mode 属性设成了half_pixel但 TensorRT 那个版本只支持asymmetric构建时直接报错。解决办法是在导出 ONNX 时把这个属性改掉或者用 TensorRT 的插件替换这个算子。提示构建失败时一定要看 verbose 日志的最后几行TensorRT 会告诉你具体是哪一层、哪个算子出的问题。不要只看最后的 Engine building failed 就抓瞎。6.3 部署时的显存与功耗平衡在嵌入式设备上部署显存和功耗是两个硬约束。YOLOv4-tiny 的引擎本身不大但推理时的中间激活值会占不少显存。如果 batch 设大了或者输入分辨率高了显存很容易爆。我的经验是在 Jetson 这类设备上batch 设 1 到 4 比较稳妥输入分辨率 416 是甜点608 会明显吃紧。如果非要跑高分辨率可以考虑把模型切成两半分时推理或者用 INT8 省显存。功耗方面TensorRT 推理时 GPU 会跑满功耗自然高。如果设备是电池供电得在帧率和功耗之间找平衡。一个技巧是动态调频检测到画面静止时降频有运动时升频这样平均功耗能降不少。7. 我踩过的几个坑和一点心得第一个坑是引擎文件和设备绑定这件事。我有一次在开发机上构建好引擎直接拷到设备上跑结果报了一堆错。后来才知道引擎文件跟 GPU 架构、TensorRT 版本、甚至 CUDA 驱动版本都有关联跨设备必须重新构建。现在我都是把构建脚本一起打包部署时在目标设备上现场构建。第二个坑是 INT8 校准集的选择。我一开始图省事随便找了 200 张图做校准结果模型在夜间场景下漏检严重。后来把校准集扩充到覆盖白天、夜间、雨天、逆光等各种情况总共 800 张精度才回来。校准集的质量比数量重要一定要覆盖实际部署场景的分布。第三个坑是后处理的 NMS 阈值。YOLOv4-tiny 因为模型小同一个目标经常出多个框NMS 阈值设高了框去不干净设低了又容易把相邻目标误删。我最后是拿实际场景的视频跑了一遍统计不同阈值下的漏检和误检才定下来一个 0.45 的值。这个值没有标准答案得根据你的场景调。最后分享一个小技巧如果你要在同一台设备上跑多个模型可以把它们构建到同一个引擎里用 TensorRT 的 multi-engine 或者 batch 维度来区分。这样能省去重复的显存分配和上下文切换开销整体吞吐量能提升不少。不过这个做法对显存要求更高得先算好总账。这套流程我前后在三个项目里用过从工控机到嵌入式设备都跑通了。核心就是那几步权重转 ONNX 要验证、引擎构建要看日志、后处理要测耗时、精度要逐层对比。把这几个环节卡住剩下的就是调参和适配了。