YOLOv7量化部署:PTQ/QAT选型与TensorRT工程避坑指南 简介面向目标检测落地与模型压缩的开发者这份资料系统整理了YOLOv7的PTQ与QAT量化训练流程并配套基于TensorRT的C部署实现覆盖从模型训练、量化到GPU加速推理的完整链路。压缩包共134个文件约35MB其中包含Python训练/量化脚本、C推理源码、yaml配置、ipynb示例以及Dockerfile等环境配置便于快速搭建和对照学习。已有486人学习适合正在做YOLOv7部署优化或希望理解量化的工程师参考。代码中对模型加载、输入预处理、推理执行、NMS后处理等关键步骤提供了较详细注释并呈现了PTQ与QAT在精度和速度上的取舍能帮助开发者依据实际项目灵活调整部署参数、缩短调优周期。1. YOLOv7 量化部署入门PTQ 和 QAT 先想好走哪条路接手过几个 YOLOv7 的实时检测落地需求几乎每次都会在量化这一步卡住。FP32 的 TensorRT engine 跑起来延迟和显存都能接受但一旦要上 INT8模型、校准、C 工程三个环节只要有一个细节没对齐检测框要么全乱要么精度掉到没法看。这个场景有多常见做边缘盒子、Jetson、工控机推理的开发者十有八九都要走一遍“训练保精度、量化提速度”的路子。这里面的核心就是 PTQ 和 QAT 两条路线PTQ 快适合先验证可行性QAT 慢但能救回大量精度。再配合一套基于 TensorRT 的 C 部署代码把模型加载、预处理、推理、解码、NMS 全部衔接起来。血泪经验是很多人不是不会装而是版本选错、校准集乱配、预处理不一致最后问题全堆在推理结果上根本分不清是量化的问题还是部署的问题。这篇就把环境、训练、C 部署、验证和避坑一次说透。2. 搭建量化训练与部署环境CUDA / cuDNN / TensorRT 版本怎么选2.1 先根据 GPU 和驱动确认 CUDA 版本再选 TensorRT装环境最容易翻车的不是下载而是版本之间的兼容关系。TensorRT 不是一个独立软件它依赖 CUDA 版本和 cuDNN 版本配错了最常见的结果就是编译时报找不到nvinfer.h或者运行时直接段错误。我一般先把下面两条命令跑一遍确认当前机器状态# 查看 GPU 型号和驱动支持的最高 CUDA 版本 nvidia-smi # 查看当前环境里实际可用的 CUDA 编译器版本 nvcc --versionnvidia-smi右上角显示的是驱动本身支持的最高 CUDA 版本不代表你机器里装了对应的 CUDA Toolkit。真正决定编译行为的是nvcc --version输出的版本号。比如驱动显示 12.2但nvcc还是 11.8那编译时就得按 11.8 来链接库而不是按驱动显示的版本来。部署代码里如果直接用 CMake 链接 CUDA版本不对会在链接期报一堆undefined reference这个阶段看不出是驱动问题还是库版本问题。确定 CUDA 后TensorRT 的版本选择要看它对 CUDA 的兼容矩阵。常见做法是打开 TensorRT 的 Release Notes 找到 supported platforms 表格比对 CUDA 和 cuDNN 的要求。对于 C 部署来说装 TensorRT 其实只需要解压后的库文件和头文件路径包括libnvinfer.so、libnvinfer_plugin.so以及对应的 include 目录并不需要安装 Python 包。Python 的 TensorRT 更多是给 ONNX 导出、精度验证用的辅助工具两个可以共存但不要混用版本。2.2 用 Dockerfile 把环境锁死省掉版本错配的后悔药如果是在一台全新机器上部署我最推荐的做法是直接用 TensorRT 官方容器镜像作为底座把 CUDA、cuDNN、TensorRT 版本一次性对齐。项目里自带的 Dockerfile 思路也是这样省去每个机器重装一遍环境的痛苦。基础镜像选 TensorRT 官方的 NGC 镜像后面自己装编译工具和 OpenCV 就够了。# 基于 TensorRT 官方镜像tag 按你机器的 CUDA 版本到 NGC 页面选择 FROM nvcr.io/nvidia/tensorrt:版本-py3 # 安装部署所需的编译工具和图像处理库 RUN apt update apt install -y --no-install-recommends \ build-essential cmake libopencv-dev pkg-config # 把部署代码拷贝进容器直接编译成可执行程序 WORKDIR /workspace COPY . /workspace RUN mkdir build cd build cmake .. make -j$(nproc)libopencv-dev是为了在 C 里做 image decode、resize、letterbox 这些预处理操作YOLOv7 的部署流程离不开它。pkg-config帮助 CMake 自动找到 OpenCV 的 include 和 lib 路径。FROM里的 tag 是唯一需要你仔细确认的地方不要拿一个旧镜像硬配新版 TensorRT 的代码那样反而会把容器镜像的优势浪费掉。如果不用 Docker就单独把 TensorRT 的 tar 包解压到某个目录比如/opt/tensorrt后面通过cmake指定路径。无论是 Docker 还是物理机最终目标都是一个让 CMake 能同时找到 TensorRT、CUDA、OpenCV 三套依赖。2.3 看懂工程文件common.cmake 和各源码模块的职责拿到部署代码后先别急着编译花几分钟把工程结构理清后面的排查会快很多。这个工程里的核心文件和职责如下文件作用common.cmake公共 CMake 配置定义 TensorRT、CUDA 的查找路径和编译选项app_yolov7.cpp主程序入口负责 engine 加载、推理流程编排yolo.cppYOLOv7 模型封装包含输出解码和 NMS 逻辑utils.cpp公共工具函数常见的有读文件、图像预处理、engine 序列化logger.cpp / logging.hTensorRT 日志回调控制日志输出级别kernel_function.cu自研 CUDA 核函数多用于预处理或 GPU 上的解码加速sampleOptions.cpp命令行参数解析用于指定 engine 路径、输入图片、阈值等Dockerfile容器环境配置就是上面那段的基础common.cmake是编译的入口配置。我一般会在 CMakeLists 里通过include(common.cmake)引入它然后在 target 上链接 TensorRT 和 CUDA 库。一个可用的配置片段长这样include(common.cmake) # app_yolov7 是最终的可执行程序 add_executable(app_yolov7 app_yolov7.cpp yolo.cpp utils.cpp logger.cpp sampleOptions.cpp kernel_function.cu ) target_include_directories(app_yolov7 PRIVATE ${TensorRT_INCLUDE_DIR} ${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS} ) target_link_libraries(app_yolov7 PRIVATE nvinfer nvparsers cudart ${OpenCV_LIBS} ) # kernel_function.cu 需要 CUDA 标准 set_target_properties(app_yolov7 PROPERTIES CUDA_STANDARD 17 CUDA_STANDARD_REQUIRED ON )nvinfer是 TensorRT 的核心推理库nvparsers用于 ONNX 模型解析cudart是 CUDA 运行时。如果少了nvparsers在加载 ONNX 转 engine 时就会报符号找不到的错误。实际编译时如果提示找不到TensorRT_INCLUDE_DIR就去common.cmake里把它改成你的 TensorRT 实际路径通常这也就是唯一需要改的地方。3. 量化训练实操PTQ 校准器与 QAT 微调怎么配3.1 PTQ用 TensorRT 的 INT8 校准器做后训练量化PTQ 是在 FP32 模型训练完成后直接做量化不需要重新训练。常见做法是在 TensorRT 构建 engine 时挂一个IInt8EntropyCalibrator2校准器它负责在构建阶段读取一组校准图片统计每一层激活值的分布从而确定 INT8 的 scale 因子。实现这个校准器是 PTQ 落地的核心步骤class Int8EntropyCalibrator : public nvinfer1::IInt8EntropyCalibrator2 { public: Int8EntropyCalibrator(int batchSize, const std::vectorstd::string imagePaths); // TensorRT 构建时调用返回每次喂入的 batch 大小 int getBatchSize() const noexcept override { return batchSize_; } bool getBatch(void* bindings[], const char* names[], int nbBindings) noexcept override { // 依次读取校准图片做 letterbox 到 640x640 // 转成 RGB、除以 255 归一化再拷贝到 device buffer // 注意这里的预处理必须和推理时的预处理完全一致 for (int i 0; i nbBindings; i) { // bindings[i] 已经是 device 指针直接把图像数据填进去 cudaMemcpy(bindings[i], hostBuffer, size, cudaMemcpyHostToDevice); } return true; } private: int batchSize_; std::vectorstd::string imagePaths_; };这段代码里最关键的一点校准数据的前处理必须和推理时完全一致包括 padding 颜色、归一化方式、是否 BGR 转 RGB。很多人 PTQ 掉精度不是量化本身的问题而是校准图片走的是另一套预处理scale 学出来的分布和实际部署时对不上。getBatchSize一般取 8 或 16batch 太小校准不稳定太大则校准时间成倍增加。校准图片的数量和内容也很讲究。我一般从验证集里随机抽 300 到 500 张要求是覆盖多种光照条件、目标尺度和场景而不是从训练集里拿一批数据糊弄过去。如果部署场景是夜间监控校准集里却全是白天图片INT8 的检测精度在夜间大概率崩掉。校准集不要求带标注因为校准过程只统计激活值分布不计算 loss。提示校准完成后 TensorRT 会生成 calibration cache 文件。这个缓存可以复用下次构建 INT8 engine 时直接加载不用重新跑一遍校准部署时记得把它和 engine 一起管理。3.2 QAT训练感知量化让模型在低精度下保精度当 PTQ 掉点超过预期比如 mAP 掉了 2% 以上就得考虑 QAT。QAT 的核心思想是在训练阶段就在模型里插入模拟量化的节点让前向传播提前适应 INT8 的数值误差反向传播仍然用浮点梯度更新权重。常见做法是借助 pytorch_quantization 这个工具库在训练前把模型里的常规卷积层替换成带伪量化节点的版本。from pytorch_quantization import quant_modules # 全局替换模型里的可量化层 quant_modules.initialize() # 加载已经训练好的 YOLOv7 FP32 权重中途不要从零开始 model build_yolov7(weightsyolov7_fp32.pt) # 降低学习率对量化节点做微调 optimizer torch.optim.SGD(model.parameters(), lr1e-4, momentum0.9, weight_decay5e-4) # 只微调几个 epoch观察 mAP 不再下降后保存模型 for epoch in range(5): train_one_epoch(model, train_loader, optimizer) eval_after_quantize(model, val_loader)quant_modules.initialize()的作用是替换模型中所有nn.Conv2d和nn.Linear换成支持伪量化的版本。这里有一个很关键的细节QAT 必须加载 FP32 预训练权重直接从随机初始化开始训练量化模型几乎收敛不了。学习率也要比正常训练低常见做法是取原来基础学习率的十分之一到百分之一微调 5 到 10 个 epoch 就够了训练太久反而过拟合。QAT 训练完成后导出的 ONNX 模型里面会带上 QuantizeLinear 和 DequantizeLinear 节点也就是常说的 QDQ 节点。TensorRT 在构建 engine 时看到这些节点就知道该走量化路径且不再需要额外提供 PTQ 校准器因为模型已经“教会”了量化参数。这也是判断一个模型是否走对 QAT 流程的标准打开 ONNX 图看有没有 QDQ 节点没有就说明训练配置没生效。3.3 导出 ONNX 与生成 TensorRT engine无论 PTQ 还是 QAT最终都要把 PyTorch 模型转成 ONNX再交给 TensorRT 构建 engine。导出这一步的坑集中在输入维度和 opset 版本上。我一般会把输入固定成 1x3x640x640避免使用动态 shape这样 TensorRT 能做得更多的图优化部署时也少一类维度相关的问题。YOLOv7 的输出是三个 head对应 stride 8、16、32导出时要分别命名import torch model load_yolov7_model(yolov7_qat.pt) # 已加载 QAT 权重 model.eval() dummy_input torch.zeros(1, 3, 640, 640) torch.onnx.export( model, dummy_input, yolov7_qat.onnx, opset_version12, input_names[input], output_names[output_8, output_16, output_32], # 三个检测头 dynamic_axesNone, # 固定输入尺寸不要开动态维度 )opset_version12是一个兼容性比较好的选择TensorRT 对它的算子支持比较全。如果导出后构建失败并提示某个算子不支持可以用 onnx-simplifier 对模型做一次简化把一些冗余的 reshape 和 transpose 合并掉这种情况在 YOLOv7 上不算少见。三个输出头的顺序也要和 C 代码里的解析顺序对应起来输出名只影响 ONNX 图最终 C 端依赖的是 tensor 的顺序和维度。ONNX 准备好后用 trtexec 快速生成 engine# FP16 版本适合大多数 GPU 上的实时推理 ./trtexec --onnxyolov7_qat.onnx \ --saveEngineyolov7_qat_fp16.engine \ --fp16 # INT8 版本如果是 PTQ先指定校准缓存 ./trtexec --onnxyolov7_qat.onnx \ --saveEngineyolov7_qat_int8.engine \ --int8--fp16和--int8是两种精度模式。FP16 通常不掉精度INT8 需要模型走完量化流程才不掉点。trtexec构建完成后会打印 engine 的构建信息包括层数、显存占用和平均延迟这一步可以作为后续 C 部署的性能基线。4. 用 C 接 TensorRT从 engine 加载到结果解析4.1 主程序怎么加载 engine 并分配显存部署阶段的 C 代码不应该每次启动都从 ONNX 重新构建 engine正确做法是加载已经序列化好的.engine文件。反序列化比构建快了几个数量级适合生产环境快速启动。app_yolov7.cpp的主流程基本是读 engine 文件、创建 runtime、反序列化、创建 context、分配输入输出显存然后进入推理循环。#include NvInfer.h #include fstream #include vector // logger 别用全局默认的自定义一个控制日志输出 class Logger : public nvinfer1::ILogger { public: void log(Severity severity, const char* msg) noexcept override { if (severity Severity::kWARNING) { std::cout [TensorRT] msg std::endl; } } }; int main() { Logger logger; // 读取 engine 文件 std::ifstream file(yolov7_int8.engine, std::ios::binary); std::vectorchar data((std::istreambuf_iteratorchar(file)), std::istreambuf_iteratorchar()); // 反序列化 engine auto runtime nvinfer1::createInferRuntime(logger); auto engine runtime-deserializeCudaEngine(data.data(), data.size()); auto context engine-createExecutionContext(); // 输入输出显存空间 void* inputDevice; void* outputDevice; cudaMalloc(inputDevice, 1 * 3 * 640 * 640 * sizeof(float)); cudaMalloc(outputDevice, 1 * MAX_OUTPUTS * sizeof(float)); return 0; }deserializeCudaEngine是反序列化的关键 API它需要传入完整的数据和长度。这里有个容易犯的错文件读取用std::ifstream时没设二进制模式在 Windows 上会导致读取长度不对engine 反序列化直接失败。context负责实际的推理执行每个 context 保存独立的中间计算结果多线程推理时每个线程需要单独创建 context但 engine 可以共享。显存分配的大小取决于模型的输入输出 tensor。输入固定是1x3x640x640的 float 数组输出部分要看模型处理后的最大检测框数量一般代码里会预留一个上限比如 25200 个候选框乘每个框的坐标和类别信息。这个最大值要预留够否则推理时输出数据会被截断检测框莫名变少。createInferRuntime创建 runtime 对象负责 engine 的反序列化和内存管理。deserializeCudaEngine从字节流恢复 engine执行前必须保证 data 数据完整存活。createExecutionContext创建推理上下文后续enqueueV2都依赖它。4.2 预处理letterbox 和归一化细节YOLOv7 的 C 部署中预处理决定了一半的精度。所谓预处理就是把读进来的任意尺寸图片先等比例缩放到 640 尺寸的长边短边用灰边补齐再转成 RGB、归一化到 0 到 1 之间最后把 HWC 排列转成 CHW。这个流程和 PyTorch 训练时的处理必须严格一致特别是 padding 颜色。void letterbox(const cv::Mat src, cv::Mat dst, int targetSize 640) { float scale std::min(targetSize * 1.0f / src.cols, targetSize * 1.0f / src.rows); int newW static_castint(src.cols * scale); int newH static_castint(src.rows * scale); cv::Mat resized; cv::resize(src, resized, cv::Size(newW, newH)); int padW targetSize - newW; int padH targetSize - newH; int left padW / 2; int top padH / 2; cv::copyMakeBorder(resized, dst, top, padH - top, left, padW - left, cv::BORDER_CONSTANT, cv::Scalar(114, 114, 114)); }114这个数字不是随便写的对应 PyTorch 预处理里的 padding 值 0.45 乘回 255 的结果。如果两边 padding 颜色不一致模型输入分布偏移检测框坐标也会跟着偏移。resize插值方式也要固定常见的是双线性插值和 PyTorch 的F.interpolate保持一致。归一化和通道转换通常直接在 CUDA 核函数里做kernel_function.cu里可以看到类似这样的逻辑先把 uint8 的 BGR 数据转成 float 的 RGB再乘以1.0f / 255.0f最后写进 CHW 排布的输入 buffer。这个瓶颈如果在 CPU 上做一张 640 的图都要额外增加几毫秒GPU 上做则几乎可以忽略。推理本身的调用很简单难的点都在前后处理的对齐上// 将输入数据从 host 拷贝到 device cudaMemcpy(inputDevice, inputHost, inputSize, cudaMemcpyHostToDevice); // 异步推理结果写入 outputDevice context-enqueueV2(bindings.data(), stream, nullptr); cudaStreamSynchronize(stream); // 把结果拷回 host cudaMemcpy(outputHost, outputDevice, outputSize, cudaMemcpyDeviceToHost);enqueueV2是异步接口它把推理任务放进指定的 CUDA stream 后立即返回。如果后续代码直接读输出必须先cudaStreamSynchronize等待推理完成否则读到的全是旧数据。bindings数组的顺序和构建 engine 时输入输出的绑定顺序一致一般第 0 个是输入后面依次是三个输出头。4.3 解码与 NMSyolo.cpp 和 kernel_function.cuTensorRT 输出的原始数据并不是最终检测框需要经过解码。YOLOv7 的三个输出头分别对应不同 stride 的特征图每个特征图的每个格子有若干个 anchor原始输出经过 sigmoid 解码后才能得到相对坐标和置信度。解码这一步既可以在 CPU 上用yolo.cpp完成也可以写成 CUDA 核函数放到 GPU 上kernel_function.cu本质上就是为了做这件事。// 以 stride 8 的检测头为例逐个格子解码 for (int h 0; h gridH; h) { for (int w 0; w gridW; w) { for (int a 0; a anchors; a) { // 中心点偏移加网格坐标再乘以 stride float x (sigmoid(rawX) w) * stride; float y (sigmoid(rawY) h) * stride; // 宽高是 exp 后乘 anchor 再乘 stride float bw exp(rawW) * anchorW * stride; float bh exp(rawH) * anchorH * stride; // 置信度 目标分数 * 类别分数 float conf sigmoid(obj) * maxClassProb; // 过滤低置信度框 if (conf confThreshold) { detections.emplace_back(Box(x, y, bw, bh), conf, classId); } } } }解码公式里的sigmoid、exp、stride、anchor这几个要素缺一不可。其中 anchor 值必须和训练时用的保持一致不同数据集训练的 YOLOv7 anchor 并不一定相同代码里通常会写死一组默认值换模型时要记得同步修改。如果框的位置整体偏移在小半个格子以内多半就是 grid 和 stride 的对应关系没对好。NMS 的位置选择直接影响推理性能。候选框数量少的时候比如几百个框用 CPU 跑一个双重循环的 NMS 也够了但候选框上万时CPU 上跑一次可能就要十几毫秒这时候要么用 TensorRT 自带的 EfficientNMS plugin要么自己写 CUDA kernel 做并行抑制。项目里带kernel_function.cu就意味着作者倾向于把解码或 NMS 放到 GPU 上这类核函数通常只需要在解码和抑制循环里注意多线程的索引错位问题。最终输出结果通常是这样一个结构体数组每个元素包含x, y, w, h、confidence、classId。这个结果可以直接画框也可以转成 JSON 供上层业务使用。关键是置信度阈值和 IOU 阈值要单独暴露成可配置参数因为不同场景对误检和漏检的容忍度完全不同。5. 部署避坑YOLOv7 量化最容易翻车的五个现场量化部署这个环节一半的时间花在读报错另一半花在排查结果不对上。下面这五个问题是我自己踩过、以及帮别人排查时遇到最多的按“现象、原因、解决”的方式一条条说清楚。5.1 现象PTQ 构建成功但检测框全乱成一团这个现象最容易让人误判成 TensorRT 部署代码写错了。我遇到过校准图片只有三十张且全部来自训练集同一批场景的情况。原因是校准数据量太少TensorRT 统计到的激活值分布严重偏向部分层生成的 scale 因子失真。解决方法是至少准备 300 张覆盖不同光照、目标尺寸和背景的验证集图片重新跑一遍校准。校准集不要求标注但场景分布一定要贴近真实使用环境。5.2 现象INT8 比 FP32 掉点 3% 以上但校准集没问题如果校准集没毛病却依然掉点严重常见原因是校准预处理和推理预处理不一致。比如推理时用了 BGR 转 RGB校准器里却直接拿了原图或者推理 letterbox pad 值是 114校准时用了 0。这类问题肉眼几乎看不出来排查方法是把校准器的预处理函数和推理预处理函数逐行对比把每个像素值都打印出来确认。我一般会写一个测试用例同一张图分别走两套预处理对比输出数组是否完全一致。5.3 现象QAT 训练了几个 epochloss 一直不降QAT 的训练行为和普通训练不一样最常见原因是没加载 FP32 预训练权重就从随机初始化开始学习率也按普通训练的节奏在跑。量化节点的梯度信号天然带有噪声学习率太高会让损失在某个区间震荡。解决方法是先载入 FP32 权重把学习率调低到1e-4甚至1e-5只微调 5 到 10 个 epoch并且每个 epoch 都做一次量化模型的验证以验证集的 mAP 为准判断是否继续训练。5.4 现象engine 文件换一台机器后无法反序列化engine 文件与 TensorRT 版本、GPU 架构强绑定换机器后反序列化报错是正常现象。很多人把 engine 当普通模型文件拷来拷去结果换台机器或者换个容器镜像就直接崩溃。解决方法是把 ONNX 文件和构建脚本留着在新机器上用 trtexec 或 C 代码重新构建 engine不要跨环境复用。Dockerfile 里也不要缓存 engine 文件否则镜像换个 GPU 型号就白搭。5.5 现象NMS 之后重复框特别多重复框多通常不是量化的问题而是后处理参数跑偏。置信度阈值太低比如设成 0.05会把大量低质量框放进 NMS导致原来该被抑制的框因为分数稍高而保留下来IOU 阈值设得太高比如 0.9也会放宽抑制力度。解决方法是先把置信度阈值调到 0.25、IOU 阈值调到 0.45 作为基线再根据场景微调。如果重复框还是多检查解码时是否漏了 sigmoid输出值未做归一化会导致置信度整体虚高。6. 装完怎么验证跑通trtexec 与精度回归脚本6.1 用 trtexec 卡出性能和延迟基线engine 构建完第一件事就是跑一遍 trtexec确认这个 engine 在目标 GPU 上的真实性能。命令如下./trtexec --loadEngineyolov7_int8.engine \ --shapesinput:1x3x640x640 \ --iterations100 \ --avgRuns100--avgRuns100表示取 100 次推理的平均值避免单次运行时冷启动状态影响数据。关注输出的 Latency 列和显存占用这两个数值就是后续性能优化的基准。如果 INT8 反而比 FP16 慢先检查是否 GPU 不支持 INT8 加速或者模型里有些层没有被量化。另外准备几张真实图片跑一遍完整程序用画框保存的方式确认检测框数量和位置正常这一步是排除“engine 构建成功但推理结果错误”的有效手段。6.2 精度回归量化模型必须跑一遍 mAP 对比性能达标后还差最后一道验证精度。我平时习惯的做法是同一份测试集分别跑 FP32 engine 和 INT8 engine把检测结果导出成 COCO 格式然后计算两边 mAP 的差值。INT8 通常比 FP32 掉 0.5% 到 2%如果超过 3% 就得回查量化步骤。from pycocotools.coco import COCO from pycocotools.cocoeval import COCOeval coco_gt COCO(annotations/instances_val2017.json) # 分别加载 FP32 和 INT8 的检测结果 for run_name in [fp32, int8]: coco_dt coco_gt.loadRes(fresults_{run_name}.json) coco_eval COCOeval(coco_gt, coco_dt, bbox) coco_eval.evaluate() coco_eval.accumulate() coco_eval.summarize() print(f{run_name} mAP0.5:0.95 {coco_eval.stats[0]:.4f})loadRes加载的是模型输出的检测结果格式是标准的image_id, category_id, bbox, score。COCOeval.summarize()会打印从 AP 到 AR 的一整套指标主要看stats[0]也就是 mAP0.5:0.95。两个 json 的预处理和后处理必须完全一致否则差值不是量化造成的而是代码差异造成的。我现在每次拿到一个 YOLOv7 量化部署任务都会先把 FP32 engine 跑一遍建立基线再上 INT8精度对比不过直接回到量化步骤调整这套检查习惯已经救了我很多次希望帮到你。本文还有配套的精品资源点击获取