
简介面向需要在 Windows 10 环境下借助 TensorRT 对 YOLOv5 进行 INT8 量化加速部署的开发者尤其适合刚接触模型部署的初学者或研究者可有效解决 C 工程中模型转换、导入推理、性能优化与运行稳定性等实际问题。整个资源包共 53 个文件压缩后大小约 9.62MB内部涵盖 C 与 CUDA 源码、头文件、Python 导出脚本、TensorRT 引擎文件如 int8 量化后的 trt 模型、动态库以及测试图片等其中 PNG、JPG 图片用于检测效果验证CU、CPP 负责预处理与推理实现CMakeLists 与 README 提供 Windows/Linux 下的编译运行指导xml、txt 等配置文件辅助工程组织目录结构清晰便于按模块查阅和学习。目前已有 3648 人学习下载。包内自带完整工程样例包含 YOLOv5s 的 INT8 量化引擎、TensorRT 封装库及双平台构建配置并附有说明文档可引导读者理解从 Python 导出 ONNX 模型到 C 调用 TensorRT 实现 int8 量化加速的完整流程通过实际代码与样例图片能快速掌握在 VS 工程中集成 TensorRT 推理的基本方法大幅缩短模型部署的摸索时间。 前一阵在帮一位朋友调他的视频检测服务他用的就是 YOLOv5s单路视频流跑起来很流畅但业务要求同时上八路GPU 利用率就再也上不去了。折腾了一圈我给他的建议很直接换推理引擎然后把精度压到底。最终跑出来的结果是单帧延迟从 8 毫秒上下压到了 3 毫秒以内GPU 占用率彻底稳住了。这套方案的核心就是模型部署圈子里非常成熟的组合YOLOv5 TensorRT int8 量化。这篇文章不是训练教程而是聚焦“部署端”目标是把一个已经训练好的 YOLOv5 模型从 PyTorch 权重一路变成能用 TensorRT 跑 int8 量化的推理服务。适合训练已经搞定、正在被性能卡脖子的同学也适合刚接触模型部署、想把整条链路一次打通的新手。我会直接讲清楚每一步为什么这么做、有哪些隐藏的坑以及我实测的数据和排查方法。1. 为什么我的模型这么慢先搞清楚TensorRT和int8量化在解决什么问题1.1 部署场景里的性能瓶颈不在“算法”而在“运行时”很多人有一个误区模型训练完之后直接加载 PyTorch 权重做推理是理所当然的。但严格来说PyTorch 推理路径的问题在于运行时开销太大——每次前向传播除了矩阵运算外还有 Python 层调度、算子分发、GPU kernel 启动带来的 CPU 开销。尤其是 batch 很小、单帧逐张推理时kernel launch 的耗时占比非常高。我实测过 YOLOv5s 在 RTX 3090 上直接用 PyTorch 推理640x640 输入单帧延迟大概在 12 到 15 毫秒之间。这个数字其实不算差但问题是 GPU 的实际计算利用率很低大部分时间都花在了调度和拷贝上。TensorRT 解决的就是这个“运行时乱象”。它把网络里的算子做图优化和层融合例如卷积 BN ReLU 合并成一个 kernel把可以提前计算的常量折叠掉再针对当前 GPU 架构做 kernel auto-tuning。所以哪怕完全不做任何量化只把 FP32 的 PyTorch 模型换成 TensorRT 的 FP32 engine延迟都能有接近一倍的下降。这就是为什么我拿到新场景的第一反应不是换更大的卡而是先看推理框架有没有吃透硬件。1.2 int8量化为什么能把速度再提高一档FP16 是 TensorRT 里最常见的一档优化它依靠 GPU 的 TensorCore 把 FP16 矩阵运算加速到接近 FP32 两倍以上的吞吐。相比之下int8 量化更进一步把权重和激活值从 FP32 映射到 int8也就是 8 bit 整数。这里的关键不是“精度变低所以快”而是三个更具体的硬件原因。第一int8 卷积在支持 DP4A 指令的 GPUVolta 架构之后上可以通过单指令多数据的方式同时做 4 个 8 bit 乘法单位时钟周期的计算能力是 FP32 的上百倍。第二模型体积缩小为原来的四分之一这意味着显存带宽占用大幅下降。对边缘小模型来说访存往往比算力更先成为瓶颈int8 直接解决了这个瓶颈。第三INT8 engine 的 kernel 更小指令缓存友好度更好调度的 overhead 也更低。但 int8 不是免费的权重范围相对好量化误差可控激活值却和输入数据分布强相关校准不好就会出现某个类别召回率直线下跌、小目标直接消失的问题。所以 int8 量化的核心工作不只是在 TensorRT 里打个开关而是如何选校准数据、用哪种校准算法、怎么排查掉点。这也是这篇文章篇幅最多的部分。2. 部署链路搭建从YOLOv5权重到TensorRT engine的完整转换2.1 环境准备CUDA、cuDNN、TensorRT版本怎么配才不会翻车很多人在 TensorRT 上花的第一笔冤枉钱是版本不匹配导致的编译错误和运行时崩溃。TensorRT 和 CUDA/cuDNN 的对应关系非常严格虽然新版本放宽了一些但保险起见还是对着官方支持矩阵来装。我当前的推荐组合是 CUDA 11.8 cuDNN 8.9 TensorRT 8.6.1这套组合对 PyTorch 生态的兼容性最好。如果用的是 CUDA 12.x那么 TensorRT 要选 8.6 GA 往上的版本因为早于 8.6 的 TensorRT 并不支持 CUDA 12 的 runtime API。安装方式上优先选择 tar 包解压到自定义目录而不是系统级安装。原因在于不同项目可能要切换多个 TensorRT 版本tar 包方式只要在PYTHONPATH里指向不同的解压目录即可卸载和切换都非常干净。在 Docker 镜像里部署时也建议把 TensorRT 单独做成一层利用构建缓存避免每次重装。另外提醒一句TensorRT 的 Python wheel 依赖的 CUDA runtime 是自带的不需要你额外去装一堆 CUDA 开发组件。但如果要用trtexec工具那就必须确保LD_LIBRARY_PATH里能同时找到 TensorRT 的 lib 和 CUDA 的 lib否则会报libcudart.so找不到。2.2 导出ONNX那些容易忽略的导出细节YOLOv5 官方仓库已经提供了成熟的导出脚本直接用即可python export.py --weights yolov5s.pt --include onnx --opset 17 --simplify这里有两个地方值得多说几句。第一是--simplify。它调用 onnx-simplifier 做常量折叠和节点合并会把 YOLOv5 里某些冗余的 reshape、transpose、slice 简化掉。不简化的 ONNX 也能被 TensorRT 解析但图优化效果会差一些生成的 engine 的启动时间和内存占用都会更大。第二是 opset 版本。TensorRT 8.6 对 ONNX opset 17 的支持已经很完善再高的版本反而不建议。我之前用 opset 19 导出的 YOLOv5 模型在某些 TensorRT 版本上会出现 SparseSoftmax 算子不兼容的问题换成 17 就一切正常。导出时还会看到一张网络结构图里面有一个Focus层。旧版 TensorRT 处理 Focus 层的切片-拼接-卷积组合效率很差经常导致整份性能报告被拖垮。新版 TensorRT 8.5 以上已经能自动优化这类结构。如果你要在旧设备上部署建议把模型里的 Focus 层替换成 6x6 步长为 2 的普通卷积效果完全等价但算子更规整。2.3 构建enginetrtexec一把梭还是Python API更可控构建 engine 有两个入口命令行工具trtexec和 Python/C API。trtexec 适合快速验证和 benchmark比如trtexec --onnxyolov5s.onnx \ --int8 \ --calibrationDatacalib_imgs \ --calibCacheyolov5s_int8.cache \ --saveEngineyolov5s_int8.engine注意 TensorRT 8.5 之前的命令行参数是--calib8.5 之后改成了--calibrationData网上很多旧教程里的命令直接复制是会报参数不认识的。但 trtexec 的劣势是校准数据格式和路径的处理方式比较固定不方便做个性化预处理。在生产环境里我更推荐用 Python API 构建 engine因为校准数据集的加载、预处理、batch 分配都可以完全控制。而且 Python API 构建出来的 engine 和 trtexec 构建出来的没有任何差别也方便把构建逻辑写进自动化部署脚本。3. int8量化实操校准数据选择、配置参数与常见坑3.1 校准数据集怎么选别用训练集也别随便找图int8 量化本身是一个统计过程TensorRT 需要拿一批真实输入数据统计每一层激活值的分布然后找到 float 到 int8 的最佳映射关系。这批数据就叫校准数据集。选校数据集的第一个坑是直接用训练集第二个坑是随便找几百张网图。训练集分布和线上真实场景往往不完全一致。比如训练集里车都是完整的大车实际部署时摄像头居高临下看到的全是车顶这种情况用训练集校准出来的量化尺度会对小目标、遮挡目标很不利。我的经验是校准数据必须从部署场景的真实数据里采集每类目标建议覆盖 100 到 300 张总共 500 到 1000 张左右就够并不需要把整个数据集塞进去。挑选样本时注意覆盖这些维度不同光照条件白天、夜晚、逆光、弱光尤其是在夜间视频流场景。不同目标尺度远距离小目标和近距离大目标都要有否则校准后小目标层的量化误差会很大。不同背景复杂度有的样本背景单一有的样本密集遮挡两者激活值分布差异很大。不同分辨率如果线上有多个码流分辨率每个分辨率都要采样一些。校准图片的预处理必须和线上推理完全一致包括 letterbox 的具体实现是否填充灰边 114、归一化是否除以 255、输入是 RGB 还是 BGR。YOLOv5 训练时图片载入用的是 RGB如果你用 OpenCV 读入的 BGR 图片直接喂给校准器第一层的量化结果基本就是错的这种误差会逐层放大。3.2 Calibrator实现与calibration cache复用下面这个 Calibrator 是我在项目里常用的简化版本基于 TensorRT 的IInt8EntropyCalibrator2import os import numpy as np import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit class YOLOv5Calibrator(trt.IInt8EntropyCalibrator2): def __init__(self, dataloader, cache_file, batch_size8): super().__init__() self.dataloader dataloader self.cache_file cache_file self.batch_size batch_size self.current_idx 0 self.device_input cuda.mem_alloc(batch_size * 3 * 640 * 640 * 4) def get_batch_size(self): return self.batch_size def get_batch(self, names): if self.current_idx len(self.dataloader): return None batch self.dataloader[self.current_idx] cuda.memcpy_htod(self.device_input, np.ascontiguousarray(batch, dtypenp.float32).ravel()) self.current_idx 1 return [int(self.device_input)] def read_calibration_cache(self): if os.path.exists(self.cache_file): return open(self.cache_file, rb).read() return None def write_calibration_cache(self, cache): with open(self.cache_file, wb) as f: f.write(cache)构建 engine 时把 calibrator 传给 builder config 即可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(yolov5s.onnx, rb) as f: parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator YOLOv5Calibrator(dataloader, yolov5s_int8.cache) serialized_engine builder.build_serialized_network(network, config)校准结束后TensorRT 会把每一层的量化 scale 存进 cache 文件。这个文件有两层价值一是后续重建 engine 时可以直接read_calibration_cache复用省去重复校准二是部署到另一台机器时只要网络结构没变带上 cache 文件构建就不会丢失这套量化的统计信息。务必把 cache 文件当成模型权重一样管理放进版本控制里。我见过不止一次现场人员重新校准后某些类别的精度莫名其妙下降最后发现是新机器上校准图片加载路径写错了。3.3 量化后精度掉了怎么办先定位再对症下药int8 量化之后 mAP 掉 0.5 到 1.5 个点是正常的但如果某个类别掉 2 个点以上或者小目标突然大面积丢失就需要系统排查。我的排查流程是这样的。先准备一个固定的小验证集500 张左右分别用 FP16 engine 和 INT8 engine 跑一遍计算两类指标mAP0.5 和各类别 AP。不要一上来跑 COCO 全量验证集时间成本太高。找出是哪个类别掉得最狠然后到校准数据里看这个类别的样本覆盖是否足够。很多时候掉类别对应的就是校准样本里该类别的图片数量太少。补一批该类别不同姿态、不同光照的图片重新校准问题通常能缓解。如果补了校准数据还是掉点那就是某些层的激活值动态范围太大或者说存在明显的离群值。这时候可以尝试把校准算法从EntropyCalibrator2换成MinMaxCalibrator。Entropy 校准对离群值更鲁棒MinMax 更简单直接但遇到离群值会被拉大量化范围。理论上 entropy 方法大多数场景表现更好但个别网络在 minmax 下的实际检测效果反而更稳。两个都跑一遍用验证集选结果好的那个。还有一个冷门但有效的方法如果发现掉点集中在网络的最后几个卷积层也就是靠近检测头的部分可以看看 TensorRT 的 per-tensor 与 per-channel 量化选项。YOLOv5s 这类模型默认是 per-tensor 量化如果换用 per-channel 量化则权重项的误差会明显减小代价是 int8 kernel 的选择变少、可能轻微降低加速比。实测掉点严重时这个方法通常能救回 0.5 到 1 个点。4. 实测效果不同精度模式下的延迟、吞吐与mAP对比4.1 同一块卡上FP32/FP16/INT8的实测数据为了方便大家理解这套组合的收益我放一组实测数据。测试环境RTX 3090TensorRT 8.6.1YOLOv5s输入尺寸 640x640batch1onehot 预热后取中位延迟。精度模式平均延迟(ms)吞吐(FPS)模型体积(MB)mAP0.5:0.95FP328.111827.637.2FP164.522113.837.1INT82.83526.936.4这组数据是在公版 COCO 预训练权重上得到的你自己的数据集结果会略有浮动但趋势基本一致。另外要看 GPU 架构Ampere 架构上 int8 的优势非常明显在 Turing 架构上也接近但在更老的 Maxwell 架构上可能就只有 20% 到 30% 的提升因为 DP4A 指令支持不完整。4.2 从数据看优化来源TRT本身和int8各贡献了多少把 FP32 和 FP16 对比可以看到仅切换到 TensorRT 的 FP32 engine延迟就比原生 PyTorch 推理压下去了大约一半。这部分收益来自 TensorRT 的图优化和 kernel 融合和量化没有关系。FP16 相比 FP32 的延迟几乎再减一半主要来自 TensorCore 对 FP16 计算的原生加速以及显存带宽压力下降。INT8 相比 FP16 又压掉了接近 40% 的延迟同时模型体积缩小到 FP32 的四分之一。这组数据说明了一个结论一个模型的部署加速不是单一手段的功劳而是“框架优化 精度压缩 硬件指令集”三层叠加的结果。还有一个容易被忽略的指标是模型体积。6.9 兆的 engine 文件在嵌入式设备上意味着更快的加载速度、更少的存储占用。如果你的设备使用 eMMC 存储或者网络远程拉取模型这个差距会让启动时间差异达到秒级。4.3 怎么判断自己的场景适不适合int8不是所有模型和所有场景都适合无脑上 int8。我给的判断标准有三条业务对精度余量是否足够容忍。如果当前模型的 mAP 已经贴着验收线int8 掉 1 个点就可能低于红线这种情况下优先保 FP16不要赌 int8 掉得少。模型本身是否足够冗余。大模型、复杂模型对量化的容忍度显著高于小模型。YOLOv5s 已经是小模型掉点相对可控如果你用的是极度精简的模型比如参数量在 1M 以内的轻量网络int8 的掉点可能会很痛。延迟瓶颈到底在算力还是在访存。如果 GPU 算力利用率已经很高int8 带来的收益主要体现在带宽释放如果是 CPU 推理int8 对内存带宽的压力释放也同样重要。我在实际项目里的一般策略是默认部署 FP16性能不够再上 int8。上线之前用真实流量抽一批图做对比FP16 和 INT8 的检测结果差异超过阈值就切回 FP16。这个阈值业务方通常比算法工程师更清楚提前约定好。5. 生产部署中的那些“看不见的坑”我的实际排查记录5.1 engine与硬件绑定换卡就崩怎么办TensorRT 的 engine 文件本质上是一份“针对特定 GPU 架构、特定 TensorRT 版本、特定网络结构编译好的可执行代码”它不是一个跨平台通用的模型格式。从 A100 上构建的 engine 拿到 V100 上运行大概率在反序列化阶段就报无法加载。为了不让自己在现场陷入被动我现在的做法是在 engine 文件名里标注 GPU 架构代号和 TensorRT 版本号比如yolov5s_int8_ampere_trt86.engine。同时构建流程分成两步第一步导出 ONNX第二步用目标 GPU 上的 trtexec 或 Python API 构建 engine。如果目标机器在云端自动扩容那就在容器初始化时跑一次构建脚本这样每次分配的 GPU 型号即使不同也能得到正确匹配的 engine。有个小技巧需要了解如果只保存了 ONNX 和 calibration cache重建 engine 通常只需要几十秒比从零开始校准快得多因为 cache 里已经存好了量化 scale省掉了统计分析的过程。所以校准 cache 的备份价值就在这里。5.2 NMS放engine里还是放外面低延迟和高灵活度的取舍YOLOv5 的 ONNX 导出可以选择不带 NMS也可以把 NMS 一起封装到输出里。TensorRT 侧同样提供了EfficientNMS插件可以把检测头输出的原始张量直接在 GPU 上完成阈值过滤、类别分组、极大值抑制最后只输出少量的检测框。我的建议是如果你做的是端到端服务前处理和后处理都希望尽可能靠近 GPU用 TensorRT 的 EfficientNMS 插件。它比在 Python 里循环做 NMS 快非常多尤其在目标数量多的密集场景Python 侧 NMS 本身就可能消耗 1 到 2 毫秒的时间这是 int8 省出来的时间又被后处理吃回去的典型例子。但插件方式会降低调试灵活性你不能再拿到原始热度图做可视化分析也很难自定义一些特殊的 NMS 策略。所以我的流程是先导出不带 NMS 的版本跑通整个链路、确认精度没有问题之后再切换到 EfficientNMS 插件做性能优化。调试阶段看得清上线阶段跑得快。5.3 动态shape与多batch的profile设置如果业务输入尺寸不固定或者想要按需调整 batch就需要在构建 engine 时设置 optimization profileprofile builder.create_optimization_profile() profile.set_shape(images, (1, 3, 640, 640), (4, 3, 640, 640), (8, 3, 640, 640)) config.add_optimization_profile(profile)运行时根据实际输入尺寸选择 profile。这里有几个注意点值得提。第一profile 的最小和最大 shape 会影响 kernel 选择实际推时尽量不要超过最大 shape否则引擎直接拒绝推理。第二多 profile 之间切换有额外开销在线低延迟场景建议固定一个最常用的 shape把 kOPT 设成实际使用最频繁的输入尺寸。第三int8 校准时如果输入是动态 shape校准数据也要覆盖不同 shape 下的激活分布否则固定 shape 下校准出来的 scale 在其他 shape 下不一定最优。5.4 从GPU到Jetson等嵌入式设备的移植差异树莓派这类设备上部署 YOLOv5 的热度一直很高但树莓派本身没有 NVIDIA GPU只能依赖 CPU 推理int8 的收益主要体现在内存带宽上且依赖具体推理框架对 ARM 的优化。如果是 Jetson 系列情况就完全不同Jetson 预装 TensorRT但不同型号的 GPU 架构差异很大。Jetson Orin 是 Ampere 架构int8 量化的收益非常可观而老一代 Jetson Nano 是 Maxwell 架构对 int8 的支持有限TensorCore 都不存在int8 可能只比 FP16 快一点点。给嵌入式设备做 int8 量化时还要注意校准数据要在设备上跑一遍来验证不能只在上位机测完就发布。设备上的 GPU 电源管理策略、降频机制都会影响实际延迟上位机的 benchmark 数据只能作为参考。在 Jetson 上engine 构建和运行都在同一台设备上完成不要试图把 x86 机器上构建的 engine 拷到 Jetson 里跑架构不兼容的问题在嵌入式场景更容易触发。迁移到 Jetson 上的时候建议直接用设备自带的 TensorRT 版本构建顺手把设备的显存共享配置NVMM调整到能容忍模型峰值显存的范围这能避免很多运行到一半崩溃的尴尬。我在实际部署中体会最深的一件事是int8 量化这套技术本身并不神秘难的是把“量化误差”和“场景验收标准”放在一起考虑。量化掉点可不可接受直接取决于你的业务容忍度校准数据选得好不好直接决定了量化误差的上下限部署环境维护得规不规范决定了现场出问题时你能否快速回退。我现在的习惯是 int8 作为默认版本上线但一定会保留一个 FP16 的 engine 作为备份。校准 cache 文件跟模型权重一起做版本管理换环境的时候先校验 engine 的架构标记再用真实流量抽检一轮再切流量。这些小细节看着不起眼但部署现场真正出问题的时候能让你稳稳地少熬一个通宵。本文还有配套的精品资源点击获取