
简介面向深度学习部署与边缘计算场景这套资源聚焦YOLOv5 ONNX模型的TensorRT INT8量化实现适合需要提升推理速度、降低内存占用的算法工程师、嵌入式开发者及模型优化学习者。通过将FP32模型转换为8位整数计算可显著减小模型体积并加速推理在精度损失可控的前提下提升实际运行效率。资源包共10个文件包含3个Python源脚本及对应编译缓存用于实现校准器与模型转换逻辑2个TensorRT引擎文件提供YOLOv5s与NanoDet的INT8版本2个校准缓存文件对应量化校准数据。压缩包约6.95MB覆盖校准器定义、ONNX模型导入、引擎构建与序列化的完整工具链。已有4470人学习下载。内容提供可直接加载的TRT引擎和校准缓存便于快速复现或二次开发脚本中清晰展示了设置校准批次、逐样本调用add_input、完成Calibration及build_engine的典型写法对理解动态形状设置、校准数据分布和量化精度平衡很有帮助。1. TensorRT INT8 量化 YOLOv5先算清楚这笔账再动手目标检测模型部署到生产环境速度永远是绕不开的坎。YOLOv5 在 PyTorch 里跑得再欢到了 GPU 服务器上面对实时视频流帧率上不去就是废纸。TensorRT INT8 量化是英伟达官方钦定的加速路线之一把 YOLOv5 的权重从 FP32 压到 INT8用 8 位整数做推理算力开销直接砍半甚至更多。但这条路不是跑一条命令就能通的——量化校准、精度回退、算子兼容、版本匹配每一步都能让模型精度从 0.89 掉到 0.4也能让推理速度翻三倍。本文面向的是想把 YOLOv5 部署到 TensorRT 上的工程人员尤其是手里有 ONNX 模型、被高延迟卡住的人。我会按「pt 转 onnx → 环境评估 → INT8 量化 → 构建 engine → 精度验证」这条链路把能复现的步骤、参数和坑全部摆到台面上。2. 从 YOLOv5 到 ONNX导出参数与两点必查项2.1 pt 转 onnx官方导出命令怎么改才不挖坑YOLOv5 官方仓库里自带export.py但直接跑默认参数导出的 ONNX 往往在 TensorRT 那里吃闭门羹。我一般用下面这组参数python export.py \ --weights yolov5s.pt \ --include onnx \ --opset 14 \ --imgsz 640 \ --batch-size 1 \ --simplify \ --dynamic逐项拆开说--opset 14是 ONNX 算子集的版本。TensorRT 8.x/10.x 对 opset 14 以上的兼容性最好低于 13 容易在Slice、Split这些算子上报不支持--simplify会调用 onnx-simplifier 把常量折叠掉、把冗余节点合并这一步在导出时顺手做掉省得后面跑polygraphy时被一堆Shape算子干扰--dynamic让导出的模型带动态轴后面在 TensorRT 里才有资格配 profile否则 batch 固定死线上并发稍微波动就崩。导完先别急着拿去量化做两个检查。第一用onnxruntime跑一遍确认 ONNX 输出结果和 PyTorch 原始模型差距在 1e-3 级别。第二用onnx.checker校验模型结构再用onnxruntime的get_providers()看能不能正常加载。这两个检查加起来不超过五分钟但能提前拦住八成后续问题——比如某个算子导出后 shape 变成了动态TensorRT 转换时直接报InvalidNode。2.2 opset、动态轴与半精度残差三个参数决定后续能不能量化很多人忽略--opset和量化之间的关系。INT8 量化在 TensorRT 内部本质上是把算子替换成 INT8 实现但替换的前提是算子本身能被解析成标准计算图。opset 太低导出时 ONNX 里会出现一堆兼容性算子比如SliceConcat拼出来的PaddingTensorRT 的 parser 不认识这些组合量化无从谈起。opset 提高之后算子更原子化TensorRT 更容易匹配到自己的 INT8 kernel。动态轴这里有个容易被忽视的点--dynamic导出的模型输入 shape 是[-1, 3, 640, 640]。在 ONNX 阶段无所谓但进了 TensorRT你就必须为动态轴指定一个 profile否则构建时直接报Profile was not specified for dynamic input。我在第 4 章会专门讲这个配置先记住结论导出时开--dynamic是对的但后面要为它补一个显式的 range 声明否则等于给自己埋雷。还有一个「半精度残差」的概念值得说清楚某些算子比如Exp、Pow、Sigmoid在 FP16 下计算会有精度损失ONNX 模型里这些节点的输出如果直接拿去做 INT8 量化误差会被放大。常见做法是导出一个 FP32 的 ONNX 做量化基准不要用 FP16 ONNX 去校准。也就是说--half这个参数在导出时先不要加等 TensorRT 构建时再决定要不要开 FP16——这两个阶段是独立的混在一起会导致精度排查时不知道锅该甩给谁。2.3 用 onnxruntime 做一次推理基线量化前后才有对照在做任何量化之前先把 ONNX 模型在 onnxruntime 下的表现记录下来这是后面判断量化「有没有变差」的唯一依据。import onnxruntime as ort import numpy as np sess ort.InferenceSession(yolov5s.onnx, providers[CUDAExecutionProvider]) input_name sess.get_inputs()[0].name x np.random.rand(1, 3, 640, 640).astype(np.float32) outputs sess.run(None, {input_name: x}) print([o.shape for o in outputs])这里有个容易自信过头的环节随机噪声输入测出来的延迟没有参考价值现代 GPU 对全零或随机数据容易走 cache / 不再做实际计算。要做延迟测试请使用真实视频帧或至少一张有纹理的图否则量化前后的帧率对比会被系统性低估。3. TensorRT 环境与 INT8 原理算力、版本和校准算法怎么选3.1 TensorRT 10.x 对 GTX1070 这类老卡的兼容边界先说结论RTX 30 系以下的卡GTX 10 系、20 系对 TensorRT INT8 的收益要打折扣。原因是 INT8 推理在 TensorRT 里有两条实现路径一条是走 Tensor Core需要 Turing 架构及以上另一条是走 CUDA Core 的 DP4A 指令Pascal 架构GTX 1070 属于这一代。Tensor Core 的 INT8 吞吐量是 FP16 的两倍而 DP4A 模式下 INT8 往往只比 FP32 快 30%60%并且对精度损失更敏感。TensorRT 10.x 官方安装包要求 CUDA 12.x如果你的生产环境还在 CUDA 11.x要么升系统驱动和 CUDA要么退回 TensorRT 8.6 GA 版。我踩过的最典型的坑是TensorRT 10.0 的.whl装上了import tensorrt也成功了但 build engine 时报Error: could not find a suitable global execution context查了半天发现是显卡驱动版本过低导致的 CUDA runtime 不匹配。结论在 GTX1070 上想用 TensorRT 10.x请先核验nvidia-smi里的 CUDA Driver Version 是否 ≥ 12.0否则老老实实用 TensorRT 8.6 CUDA 11.8 组合。另外注意 TensorRT 的 pip 包tensorrt-x.x.x-cp38-cp38-linux_x86_64.whl不包含trtexec你需要单独安装TensorRT本体deb/rpm/tar 包。很多人到import tensorrt成功就以为环境齐了结果跑trtexec找不着命令这算是最不等的坑——但先装好 pip 包再装本体顺序反了可能导致两个版本的 Python bindings 冲突。3.2 INT8 校准的三件事校准集、校准算法、可复现性INT8 量化在 TensorRT 里的做法是先给定输入数据分布即校准集让模型跑一遍 FP32 推理记录每层激活值的分布再用校准算法确定「FP32 到 INT8」的缩放系数。这个环节是精度损失的真正来源。校准集选取有一条硬规则必须覆盖真实业务场景的输入分布。做车检就用车的图做人脸就用脸的图拿 COCO 的通用图片去校准一个专门检测钢材表面缺陷的模型大概率量化后 mAP 直接崩掉几个点。数量上 500 张以内足够我一般用 300500 张真实样本超过 1000 张边际收益几乎为零只会拖慢校准时间。校准算法在 TensorRT 里有三个选择LegacyCalibrator、EntropyCalibratorV2、MinMaxCalibrator。工程上默认选EntropyCalibratorV2它对分布尾部不敏感能兼顾大部分场景的精度MinMax在极端值较多的场景比如小目标占比极高下表现得更好但代价是整体精度可能被拖低。没有一个算法能通吃所有模型所以做量化时留一手把EntropyCalibratorV2和MinMaxCalibrator都跑一遍各构建一个 engine对比精度后再选这个时间成本值得花。3.3 FP32 vs FP16 vs INT8算力需求差异到底怎么影响部署先给一张对比表后面所有决策都围绕它展开精度模式存储占用相对 FP32推理延迟相对 FP32适用架构精度风险FP321x1x基准全架构无FP160.5x0.60.8xTuring极低INT8 (Tensor Core)0.25x0.30.5xTuring中高INT8 (DP4A)0.25x0.50.9xPascal/Volta中高先说背景TensorRT 生成 engine 时会把整个模型按层分派给不同精度的 kernelINT8 量化真正快在哪卷积、全连接、矩阵乘法这类计算密集型算子受益最大而Resize、Concat、Sigmoid这类内存/函数型算子基本吃不到红利。YOLOv5s 的 backbone 里 80% 的计算量集中在卷积层所以 INT8 化之后整个模型的速度提升明显但如果你是跑一个几乎全由ElementWise算子组成的轻量分类模型INT8 收益就很有限甚至因为反量化开销而变慢——这就是「算力需求差异」的实际含义。落到部署上如果你用的是 GTX 1070 这类 Pascal 卡INT8 的收益大概率不会到 2 倍此时更推荐用 FP16 做首版稳定后再评估是否值得付出校准和调优的成本去换 INT8 的增量。RTX 3060 以上的卡才值得优先把 INT8 作为目标方案。树莓派 5 这类 ARM 平台不建议硬上 TensorRTINT8 的量化和部署链路更适合放到 Jetson 系列。4. 用 Python API 构建 INT8 engine最小可跑脚本与参数说明4.1 从 ONNX 到 INT8 engine完整 build 脚本TensorRT 提供两种构建路径trtexec命令行和 Python API。生产环境里我建议用 Python API原因是校准器Int8Calibrator必须自己写trtexec虽然方便但没法自定义数据源。下面这份脚本是完整可跑的 INT8 构建流程import tensorrt as trt import numpy as np import os TRT_LOGGER trt.Logger(trt.Logger.WARNING) class EntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, calib_imgs, input_shape(1, 3, 640, 640)): super().__init__() self.calib_imgs calib_imgs # 校准图片路径列表比如 300 张 jpg self.input_shape input_shape self.batch_size input_shape[0] self.calib_data self._load_calib_data() self.calib_idx 0 def _load_calib_data(self): import cv2 data [] for path in self.calib_imgs: img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img cv2.resize(img, (self.input_shape[2], self.input_shape[3])) img img.astype(np.float32) / 255.0 data.append(img) return np.stack(data).astype(np.float32) def get_batch_size(self): return self.batch_size def get_batch(self, names, p_int): if self.calib_idx self.batch_size len(self.calib_data): return None batch self.calib_data[self.calib_idx:self.calib_idx self.batch_size] batch np.ascontiguousarray(batch) self.calib_idx self.batch_size return [batch] # 注意返回的是 list顺序要匹配 names def read_calibration_cache(self): if os.path.exists(calib.cache): return open(calib.cache, rb).read() return None def write_calibration_cache(self, cache): with open(calib.cache, wb) as f: f.write(cache) def build_int8_engine(onnx_path, calib_imgs, engine_path): builder trt.Builder(TRT_LOGGER) network builder.create_network(1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser trt.OnnxParser(network, TRT_LOGGER) with open(onnx_path, rb) as f: assert parser.parse(f.read()) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) # 1GB workspace config.set_flag(trt.BuilderFlag.INT8) config.int8_calibrator EntropyCalibrator(calib_imgs) profile builder.create_optimization_profile() # 动态 shape 的最小/最优/最大 batch必须按这个顺序 profile.set_shape(images, (1, 3, 640, 640), (1, 3, 640, 640), (8, 3, 640, 640)) config.add_optimization_profile(profile) engine builder.build_serialized_network(network, config) with open(engine_path, wb) as f: f.write(engine) print(f[OK] INT8 engine saved to {engine_path}) if __name__ __main__: build_int8_engine(yolov5s.onnx, [calib/img_001.jpg, ...], yolov5s_int8.engine)这段代码里有几个地方是血泪经验换来的。get_batch返回的数据必须是np.ascontiguousarrayTensorRT 要求连续内存否则在校准循环中会出现Segmentation fault返回值必须是一个 list元素顺序要跟names参数一致通常只有一个输入直接return [batch]即可read_calibration_cache和write_calibration_cache一定要实现前者让第二次构建直接跳过校准阶段后者把校准结果持久化否则每次 build 都要重新校准一遍时间白白烧掉。4.2 profile 与动态 shape四个必调参数上一节脚本里出现了set_shape这里的参数直接影响引擎能在什么输入尺寸下工作参数含义推荐值踩坑说明min shape最小输入尺寸(1, 3, 640, 640)不能低于 1YOLOv5 的 stride 是 32所以宽高必须是 32 的倍数opt shape最优输入尺寸(1, 3, 640, 640)实际业务中最高频的 shape写错了性能会严重退化max shape最大输入尺寸(8, 3, 640, 640)超过这个值 build 会直接报错线上并发打满时就直接崩workspaceGPU 显存上限1GB 起步2GB 会让 TensorRT 自动选择更多融合策略但显存小就别硬撑关于opt shape多说一句TensorRT 在构建时会对opt形状做全图优化如果你的线上输入大多是 480x480而这里填了 640x640构建出来的 engine 在这些小尺寸输入上的性能会明显差于「按 480x480 做 opt」的版本。所以拿到别人给的 engine 或者网上下的现成 engine先确认它的 opt shape 和你的输入分布匹不匹配。4.3 序列化 engine 并保存到磁盘反序列化后还能不能直接用engine 构建完成后不是直接用engine builder.build_serialized_network(...)的结果在内存里跑而是要先保存成.engine文件部署时加载。加载方式如下import tensorrt as trt TRT_LOGGER trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(TRT_LOGGER) with open(yolov5s_int8.engine, rb) as f: engine_bytes f.read() engine runtime.deserialize_cuda_engine(engine_bytes)注意.engine文件严格绑定 TensorRT 版本、CUDA 版本、GPU 架构。同一份 engine 在 A 机器上 build拷到 B 机器上哪怕 B 的显卡型号完全一样也可能 load 失败报错通常是Engine is incompatible with current runtime或者直接segfault。这不是玄学是因为 engine 里包含了针对特定硬件架构的 kernel 选择结果。所以生产环境的正确做法是在每台目标机器上各自执行一次 build可用缓存校准表加速或者用容器镜像把 build 环境整个复制过去保证版本一致。另一个序列化相关的常见错误是deserialize_cuda_engine返回的 engine 对象是空指针但 Python 端不抛异常。出现这种情况先检查.engine文件大小——如果只有几百字节那大概率是 build 失败但异常被吞了。解决办法是在 build 端加builder_network.is_valid()校验代码里可以提前调用这个接口以及设置环境变量TF_CPP_MIN_LOG_LEVEL0看完整日志。5. INT8 量化避坑笔记5 个让 engine「翻车」的常见原因5.1 现象量化后 mAP 掉 10 个点以上 → 原因校准集和业务数据分布不匹配 → 解决换校准集这是我遇到的最高频问题。有个项目做布匹瑕疵检测第一次量化用了 COCO 的图做校准结果是 mAP 从 0.92 掉到 0.71完全不可用。排查了半天最后发现是校准集里没有一张是布匹纹理图EntropyCalibratorV2计算出的激活值分布完全不贴合真实输入的分布。解决方式是从真实产线抽了 400 张代表性图片重新校准mAP 回到 0.88。血的教训校准集不是「随便找 500 张图」而是「最能代表线上输入的那批图」。5.2 现象INT8 engine 推理速度没提升反而变慢 → 原因小算子反量化开销大于收益 → 解决用层级精度控制或换 FP16YOLOv5s 里有一些Sigmoid、Mul、Add算子这些算子本身计算量极小但 INT8 推理前需要做反量化dequantize引入额外开销。如果整个模型都强制 INT8计算密集型卷积省下来的时间可能被这些额外开销吃掉。遇到这种情况查看trtexec --dumpLayerInfo的输出找出耗时占比 top10 的算子中哪些其实没吃到 INT8 红利通常是非卷积算子然后在的network层设置layer.precision trt.float32把这些层踢出 INT8 范围。也可以用config.set_flag(trt.BuilderFlag.FP16)做一版 FP16 engine 对比很多场景 FP16 更「够用」。5.3 现象构建 engine 时报InvalidNode指向ScatterND或NonZero→ 原因ONNX opset 太低或算子不兼容 → 解决重新导出 ONNX 并 open--simplifyYOLOv5 的 NMS 后处理如果写在模型里导出时--include onnx默认不含 NMS一般不会碰到但如果自定义的网络里用了ScatterND、NonZero这类算子TensorRT 的解析器在 INT8 模式下支持不全。查看 ONNX 模型中是否有这些算子用onnx-graphsurgeon移除后处理节点只保留conf和bbox输出。也可以换 opset 再导出一次试试——同一种结构在 opset 12 和 14 下解析路径完全不同。5.4 现象校准过程卡死get_batch只被调用一次 → 原因校准数据没做np.ascontiguousarray或 batch 返回方式不对 → 解决检查内存连续性并正确实现get_batch返回 list前面脚本里那段np.ascontiguousarray(batch)不是白写的。TensorRT 的校准器在遍历校准集时要求每次get_batch返回的内存必须是连续且对齐的如果返回了非连续数组它不会报错而是直接返回None同步表现为「只校准了一次就结束了」。排查这个问题的标准手段在get_batch里打印batch.shape和batch.flags[C_CONTIGUOUS]确认每个 batch 都正常返回。5.5 现象部署到新机器后 engine load 出现Segmentation fault→ 原因TensorRT/CUDA 版本不一致 → 解决卸载重装确切版本或用 Docker 固化环境.engine文件天生是版本敏感的。生产环境最常见的问题就是「测试机上 build 好了拷到生产机直接崩」。解决路径有两条一是严格统一镜像开发机、CI、生产机都装同一个 Ubuntu CUDA TensorRT 组合二是写一个启动脚本每次服务上线先在目标机上构建 engine再把 engine 缓存到本地磁盘利用read_calibration_cache避免重复校准。后者虽然多花几分钟启动时间但彻底消除了版本不匹配的玄学问题。6. 验证与调优用一张图把 INT8 的精度损失量化出来6.1 简化版 mAP 验证脚本用自己的数据跑一遍就够很多人对 INT8 量化的顾虑集中在「精度到底掉了多少」总想找个标准数据集系统性评估。真到了工程落地阶段其实不需要跑完整 COCO mAP——把你自己业务里最典型的一批测试图100~200 张拿出来统计模型在 INT8 和 FP32 两种 engine 下的检测结果算出 mAP0.5 即可。import numpy as np from pathlib import Path # 伪代码结构用同一个 NMS 后处理逻辑分别喂给两个 engine def infer_all(engine_path, test_imgs, label_map): results [] runtime trt.Runtime(TRT_LOGGER) engine runtime.deserialize_cuda_engine(open(engine_path, rb).read()) for img in test_imgs: # preprocess 后推理收集 dets [x1,y1,x2,y2,conf,cls] dets engine_infer(engine, img) results.append(dets) return results fp32_dets infer_all(yolov5s_fp32.engine, test_imgs) int8_dets infer_all(yolov5s_int8.engine, test_imgs) map_fp32 compute_map(fp32_dets, gt_annots) map_int8 compute_map(int8_dets, gt_annots) print(fFP32 mAP0.5: {map_fp32:.4f} | INT8 mAP0.5: {map_int8:.4f})这里的核心认知是对比的是「同一条后处理链路下的两个 engine」而不是和 PyTorch 源码比。PyTorch 的 NMS 阈值和 TensorRT 里你自定义的 NMS 阈值如果设得不一样精度差异根本没法定位。调优技巧是先用torch.hub.load(ultralytics/yolov5)的默认参数跑一遍同批图片把它的 mAP 作为上限参照再调 TensorRT 后处理的 conf-thres 和 iou-thres让 INT8 engine 的 mAP 尽量逼近 FP32。6.2 给生产环境的 INT8 落地建议什么时候该上 INT8什么时候别硬上INT8 量化的收益和成本在不同硬件架构上差距很大。我们用一组实际数据说话场景GPU 架构CPU 主干耗时msINT8 vs FP16 收益建议视频流检测1080p 25fps 单路Ampere/RTX3012快 50%优先上 INT8大批量离线推理batch16Turing20快 40%上 INT8校准集按业务选多路低分辨率320x320 8路Pascal/GTX10705几乎无收益用 FP16边缘盒子树莓派5 / JetsonARM40Jetson 上显著Jetson 可上树莓派不推荐如果拿不准我的习惯是先做一次 20 分钟的「短跑验证」用 FP32 和 INT8 各构建一版 engine在同样输入上跑 500 帧测平均延迟、用 100 张业务图测 mAP算出「帧率提升 / mAP 损失」这个比值。比值大于 1.5比如帧率快了一倍精度只掉了 0.3%就值得推上线比值小于 1比如只快了 20%精度掉 2%就果断退回 FP16。这个习惯帮我避开了至少三个「看起来很美、上线就翻车」的 INT8 方案。最后补一个 pybind 层面的技巧build engine 时给config.set_flag(trt.BuilderFlag.REFIT)留个后门线上发现精度异常时可以增量更新 engine 的部分层不用重新全量 build。这个 flag 只在 Turing 以上架构可用但它是我列出来的所有「后悔药」里最实用的一颗——量化精度这事谁都不敢保证一次到位给自己留条回退路才敢放心把 INT8 推上生产。希望这些踩出来的经验能帮你把 INT8 量化这条路走顺。本文还有配套的精品资源点击获取