简介:面向需要在边缘端或GPU服务上高效部署YOLOv7的开发者,这份压缩包同时涵盖PTQ与QAT两种量化训练方案,并配套基于TensorRT的C++推理实现。它将模型压缩原理与可运行的落地代码放在一起,既适合正在做实时目标检测、需要降低显存占用或提升推理吞吐的工程师,也适合刚接触量化训练的初学者对照学习。资源共134个文件,约35.14MB,包含35个Python脚本、5个C++源文件、8个头文件、CUDA内核以及CMake/Dockerfile等构建配置,另有YAML参数文件和Notebook说明,覆盖数据配置、量化训练、模型导出与部署调用的主要环节。预览中的yolov7.cpp、utils.cpp、kernel_function.cu等文件,从模型加载、图像预处理、推理执行到NMS后处理均有注释可循,便于理解TensorRT构建选项、输入输出组织及量化参数调整等关键细节。目前已有484人学习下载,对于想绕开重复踩坑、快速跑通全流程的读者来说,这套打包内容能节省大量检索和调试时间。
1. 用 PTQ 和 QAT 把 YOLOv7 压进 TensorRT,精度掉点怎么兜回来
YOLOv7 在边缘设备上的部署几乎绕不开 TensorRT,但直接转 INT8 会遇到一个很现实的落差:70 层往上的网络结构里,检测头对噪声极其敏感,FP32 模型转 INT8 后 mAP 掉 3 到 5 个点是常态。PTQ 只用几百张校准图片就能完成量化,省时间但精度不可控;QAT 把量化误差模拟进前向传播,训练成本高但能把掉点压到 1 个点以内。这篇博文就顺着这两条路线讲清楚:什么时候用 PTQ 快速出包,什么时候必须上 QAT,以及最终如何在 TensorRT 里把量化模型稳定跑起来。适合正在做边缘推理、车端感知或工业质检的同学,尤其是已经被 TensorRT INT8 掉点折磨过的人。
2. 量化原理与误差来源:先弄懂 PTQ 和 QAT 差在哪
2.1 PTQ 与 QAT 的量化差异:校准器选错是第一个精度杀手
PTQ(Post-Training Quantization)是在模型训练完成后直接对权重和激活做量化,核心是找一组合理的 scale 和 zero point。这一步依赖校准数据集来统计激活值的分布。YOLOv7 的检测头输出是 bbox 回归和类别概率的联合张量,数值分布跨度大,校准器如果只选了简单的 min/max,很容易被离群点带偏。
常见做法是用 NVIDIA 的pytorch-quantization库,校准器一般用HistogramCalibrator并配合 percentile 策略。YOLOv7 这类单阶段检测器我建议直接用percentile=99.99而不是默认的mse,原因是检测头的输出张量里绝大多数数值集中在 0 附近,但少量高分框的回归值对最终 NMS 结果影响极大,MSE 会为了照顾整体分布而牺牲这些关键值。量化公式是每个张量独立计算 scale:
from pytorch_quantization import nn as quant_nn from pytorch_quantization.calib import HistogramCalibrator calibrator = HistogramCalibrator( num_bits=8, axis=None, # 按张量维度做校准 percentile=99.99 # 忽略极端离群点 )这里的axis=None表示 per-tensor 校准,percentile=99.99的意思是截掉分布尾部的 0.01% 极值。这样做的目的是避免某个通道的极端激活值把整个张量的 scale 撑大,导致其他通道的量化分辨率被压缩。YOLOv7 的 backbone 经过多层 Concate 后特征图通道数很大,如果出现个别异常激活值,对后续检测头影响会被放大。
2.2 QAT 的误差模拟:为什么伪量化节点能让网络自己学会补偿
QAT(Quantization-Aware Training)的做法是在网络中插入伪量化节点(FakeQuantize),前向传播时把浮点张量量化再反量化回浮点,产生和真实推理一致的精度损失;反向传播时使用直通估计器(STE)绕开量化函数的不可导问题。这样网络在训练过程中就能感知到量化误差,并主动调整权重分布来补偿。
YOLOv7 做 QAT 的常见路径是在 PTQ 校准得到的 scale 基础上继续微调,而不是从随机初始化开始训练。原因是 YOLOv7 的检测头使用了 RepConv 结构,反量化误差在重参数化合并后会被放大,直接从零训练 QAT 模型很难收敛到和原模型相当的精度。
我一般会在 QAT 阶段把学习率调到原训练计划的 1/10,并且冻结 backbone 前几层的 BN 参数,只让检测头和靠近检测头的层去适配量化噪声。伪量化节点会让梯度变得粗糙,如果 BN 也跟着更新,整个网络的数值稳定性会差很多。
2.3 量化粒度:per-tensor 和 per-channel 对 YOLOv7 检测头的影响
量化的另一个关键参数是粒度。per-tensor 是整个张量共享一个 scale,实现简单;per-channel 是每个输出通道单独一个 scale,精度更好但硬件支持有限。TensorRT 对卷积权重默认使用 per-channel,但激活值通常是 per-tensor,因为激活值的通道间分布差异不像权重那么稳定。
YOLOv7 的 Detect 层里,分类分支和回归分支的通道维度不同,如果使用 per-tensor 量化,两个分支的数值范围差异会被平均掉,导致小目标回归精度明显下降。实测中我一般只对 backbone 和 neck 做 INT8 量化,检测头保留 FP16,这样在 TensorRT 里的精度损失只有 0.5 个点上下。需要检测头也量化时就得依赖 QAT,而且必须给回归分支更小的量化步长,也就是用更大的 bit 数或者独立的 scale。
3. YOLOv7 的 PTQ 与 QAT 训练实操:从模型导出到微调回精度
3.1 环境准备与依赖安装
做 PTQ 和 QAT 训练需要依赖pytorch-quantization和torch。关键点是pytorch-quantization会替换torch.nn中的部分模块,所以要在加载 YOLOv7 模型之前完成 import。YOLOv7 仓库里提供了导出 ONNX 的脚本,但量化训练需要在其模型定义上直接做替换。
pip install pytorch-quantization --index-url https://pypi.org/simple pip install onnx onnxruntime-gpu注意这里必须先装pytorch-quantization再启动任何训练脚本。它会 monkey-patchtorch.nn里的Conv2d、Linear等模块。如果反过来先加载了 YOLOv7 的 checkpoint,模型里的算子已经完成实例化,替换就会失败。
3.2 用几个步骤完成 YOLOv7 的 PTQ
PTQ 流程分三步:加载 FP32 预训练权重、跑校准集、导出量化模型。YOLOv7 的模型定义里大量使用nn.Conv2d,需要把卷积层替换为quant_nn.TensorQuantizer支持的类型。
from pytorch_quantization import nn as quant_nn from pytorch_quantization.tensor_quant import QuantDescriptor # 设置全局量化参数:INT8,per-tensor,校准方式为直方图 quant_desc = QuantDescriptor(num_bits=8, calib_method='histogram') quant_nn.QuantConv2d.set_default_quant_desc_input(quant_desc) quant_nn.QuantConv2d.set_default_quant_desc_weight(quant_desc) # 加载 YOLOv7 模型后替换卷积层 def quantize_model(model): for name, module in model.named_modules(): if isinstance(module, torch.nn.Conv2d): quant_conv = quant_nn.QuantConv2d( in_channels=module.in_channels, out_channels=module.out_channels, kernel_size=module.kernel_size, stride=module.stride, padding=module.padding ) quant_conv.weight.data = module.weight.data if module.bias is not None: quant_conv.bias.data = module.bias.data # 替换父模块中的子模块 parent_name = name.rsplit('.', 1)[0] child_name = name.rsplit('.', 1)[-1] parent = model if parent_name: parent = dict(model.named_modules())[parent_name] setattr(parent, child_name, quant_conv) return model这段代码的主要逻辑是在保持 YOLOv7 原有权重不变的前提下,把普通卷积替换成带量化节点的卷积。替换之后需要喂入校准数据来统计激活值分布。校准集不需要标注,选取 500 张左右覆盖各类目标的图片即可。
跑校准的时候要留意torch.no_grad()必须开启,否则校准器会统计到梯度的值。校准完成后,把每个量化节点的 scale 提取出来并导出为 ONNX。导出的 ONNX 里虽然还带着伪量化节点,但 TensorRT 的解析器会自动识别并直接跳过。
3.3 QAT 微调:在 PTQ 基础上回精度
QAT 训练直接从 PTQ 模型的 checkpoint 开始,微调 10 到 20 个 epoch 即可。YOLOv7 的损失函数包含 box、cls 和 obj 三个部分,量化带来的误差主要影响 obj 分支——因为置信度预测对数值范围变化最敏感。微调时建议把 obj 分支的损失权重加大 1.5 倍。
from pytorch_quantization import nn as quant_nn # 加载 PTQ 后的模型权重 model.load_state_dict(torch.load('yolov7_ptq.pth')) model.train() # 冻结 backbone 前几层的 BN for name, module in model.named_modules(): if 'backbone' in name and isinstance(module, torch.nn.BatchNorm2d): module.eval() # BN 层固定,不再更新 running_mean 和 running_var # 降低学习率到原有计划的 1/10 optimizer = torch.optim.SGD(model.parameters(), lr=0.0001, momentum=0.937) loss_fn = build_loss_with_weight(obj_weight=1.5) for epoch in range(15): for images, targets in train_loader: optimizer.zero_grad() preds = model(images) loss = loss_fn(preds, targets) loss.backward() optimizer.step()微调完成后需要将 QAT 模型转换回普通浮点模型才能导出 ONNX。用quant_nn.TensorQuantizer的disable_quantization()方法关闭量化节点,再导出。如果不关闭就直接导出,ONNX 里会带上 FakeQuantize 算子,TensorRT 解析时虽然兼容,但画蛇添足。
需要注意的坑是 QAT 训练中 BN 的统计量会和量化噪声耦合。BN 层在训练模式下会更新running_mean和running_var,这些统计量对 INT8 的 scale 计算有直接影响。冻结 BN 是业界常见做法,但冻结后检测头 SE 层里的 BN 需要保留在训练模式,否则启用了 TTA 时精度会下降。
3.4 训练与校准的常见错误排除
YOLOv7 的Detect层在训练时输出的是三组不同尺度的特征图,但量化校准只统计单组特征图的分布。如果校准集的图片分辨率与推理状态不一致,校准出的 scale 会把检测头激活值分布统计歪。此时先检查校准集是否做了和训练一致的 letterbox 预处理,注意这一步影响远大于校准器选型。
4. TensorRT 部署整条路径:从 ONNX 到 engine 的踩坑记录
4.1 把 QAT 模型导出成带 QDQ 节点的 ONNX
TensorRT 8.x 及以上版本对 QAT 模型的原生支持是通过 ONNX 中的 QDQ(Quantize-Dequantize)节点实现的。QDQ 表达的好处是让 TensorRT 自己决定层融合策略,而不是用户手动指定哪些层用 INT8。YOLOv7 导出 ONNX 时要加上opset_version=13,低于这个版本的 QDQ 算子兼容性不好。
import torch # 关闭量化节点,让模型以 fp32 图结构导出 for module in model.modules(): if hasattr(module, 'disable_quantization'): module.disable_quantization() # 固定输入形状为动态 batch,具体尺寸保持动态 dummy_input = torch.randn(1, 3, 640, 640).cuda() torch.onnx.export( model, dummy_input, 'yolov7_qat.onnx', opset_version=13, do_constant_folding=True, input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch'}, 'output': {0: 'batch'}} )导出后建议先用 ONNX Runtime 跑一遍推理,确认输出和 PyTorch 模型一致。ONNX 里 QDQ 节点是否保留需要用 Netron 查看。如果看不到 QDQ 节点,说明disable_quantization()调用时机太早,量化信息没有进入导出图。
4.2 用 trtexec 一行命令构建 INT8 引擎
TensorRT 构建引擎的常见做法有两种:直接用trtexec命令行验证,或者在代码里用TensorRT.BuilderAPI 构建。trtexec适合快速验证模型能否转换成功,但生产环境建议用 API 构建以便接上自定义的 calibration cache。
trtexec \ --onnx=yolov7_qat.onnx \ --saveEngine=yolov7_qat.engine \ --int8 \ --calib=yolov7_calib_cache \ --workspace=4096 \ --verbose这里--int8指示构建器启用 INT8 精度。对于 QAT 模型,TensorRT 会直接读 QDQ 节点里携带的 scale,不需要再提供额外的校准集;对于 PTQ 模型,需要--calib参数指向校准 cache 文件。--verbose输出每条层融合的日志,转换失败时能用它定位是哪一层不支持 INT8。
构建成功的 engine 可以用trtexec --loadEngine=yolov7_qat.engine做一次推理耗时验证,注意Trtexec 默认输入形状是 1x3x640x640,如果导出时指定的动态 batch 没设好,会报input size mismatch。
4.3 Orin 部署时 TensorRT 版本回退的取舍
在 Jetson Orin 上部署时,可能遇到不同 TensorRT 版本解析 QDQ 节点行为不一致的问题。较新版本对 QDQ 的优化更强,但可能改变层融合方式导致精度波动。部分厂商在 Orin 上把 TensorRT 从 8.5 降级到 8.2 是为了兼容旧版 TensorRT 的量化 API,这会让 QAT 导出的 QDQ 节点被部分回退成普通 INT8 层,精度会损失。
我的习惯是先保留原厂 TensorRT 版本构建 engine,再在代码里手动指定一些敏感层为 FP16。这样既保留新版 TensorRT 的融合优化,又用更保守的精度去保护 Detect 层的输出。涉及到模型感知精度的语义时,可通过回退到低版本 TensorRT 来获得更保守的层融合行为。
// 在 TensorRT 构建时对指定算子类型强制使用 FP16 config->setFlag(BuilderFlag::kFP16); // 对敏感层单独设置精度 layer->setPrecision(DataType::kFP16); layer->setOutputType(0, DataType::kFP16);setPrecision让该层在被 INT8 量化时强制回退到 FP16。我倾向于在 Detect 层之前的所有层保持 INT8,从第一个 Detect 层输出的 concat 节点开始全部 FP16。这样模型整体计算量下降约 40%,检测精度和 FP32 对比差距控制在 0.2 个点以内。
4.4 部署后的延时与精度验证清单
部署完成后,建议同时跑一遍精度和性能测试。精度测试用验证集 1000 张图片,记录 INT8 engine 和 FP32 engine 在 mAP@0.5 和 mAP@0.5:0.95 的差值;性能测试用trtexec的--threads选项跑多轮取平均延迟。注意首次构建 engine 时有较长的算子自动调优时间,这部分不算在推理延迟里。
5. 用一个掉点追查技巧:每层输出分布对比快速定位精度回退
当 QAT 后的 INT8 engine 还有掉点问题,我一般不会急着调 QAT 训练超参,而是先做一次以层为单位的数值分布对比。TensorRT 支持IEngineInspector接口,按层打印每个 tensor 的统计信息。用它对 INT8 engine 和 FP16 engine 的输出分布做对比,能快速找到哪个层在 INT8 下分布变形最严重。
inspector->executionContext = context; inspector->printLayerInformation(0, TensorFormat::kLINEAR);打印出每层的output tensor数值统计后,对 YOLOv7 的网络结构按 layer ID 排序,找出分布跨度最大的层。最常出问题的是主干网络里的Concat层,它的输出是将不同尺度特征图拼接后的结果,数值范围差异极大。定位到具体层后,在TensorRT的配置里单独把这一层设为 FP16,重新构建 engine。
这个追查方法的实际价值在于给 QAT 的量化配置提供反馈:如果发现某个层总掉点,它的输出分布通常超出正常范围两到三倍,说明激活值量化时的 scale 取得过大。这时把 QAT 训练里的find_effective_scale替换成对该层单独校准的 scale,再重新微调两三个 epoch,精度就能恢复到可接受范围。整个过程比重新训练整网省时不少,也是在资源有限条件下最直接的兜底手段。
本文还有配套的精品资源,点击获取