ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

YOLOv7量化部署:PTQ/QAT选型与TensorRT INT8完整实践

YOLOv7量化部署:PTQ/QAT选型与TensorRT INT8完整实践 简介面向 YOLOv7 模型量化与 TensorRT 部署的完整工程包适合需要将目标检测模型落地到 GPU 实时推理场景的 C 开发者。内容涵盖 PTQ 与 QAT 两条量化训练路线从浮点权重转换到低精度整数再到 TensorRT 的推理实现可帮助理解量化精度与推理速度的平衡。包内共 134 个文件压缩后 35.14MB。35 个 Python 脚本和 14 个 Jupyter Notebook 覆盖训练与量化实验33 个 YAML 文件保存模型和训练配置5 个 C 源文件配合 CUDA 核心与头文件完成 TensorRT 部署另含 Dockerfile、CMake、Shell 脚本方便一键复现环境。目前已有 486 人学习下载。部署代码注释较完整覆盖模型加载、输入预处理、推理执行、结果解析与 NMS 后处理等环节并提供了 C 工程所需的构建脚本和容器环境。借助包内代码可以走通从 PyTorch 训练到 PTQ/QAT 量化、再到 TensorRT 高效部署的完整链路适合正在做模型压缩或嵌入式 GPU 部署的开发者直接参考。1. YOLOv7 量化部署从 PTQ、QAT 到 TensorRT 的完整链路yolov7 部署到 TensorRT 的时候我踩过最大的一个坑不是模型导出失败而是量化策略选错。PTQ 训后量化最快一个多小时就能把 PyTorch 权重换成 INT8 引擎但碰上遮挡密集的交通场景mAP 掉得让人怀疑模型是不是废了换 QAT 量化感知训练精度是保住了代价却是重新训练和校准的那几天时间。这套资源给的正是这条完整链路C 工程里从 common.cmake 编译配置、sampleOptions.cpp 参数解析到 yolo.cpp 推理逻辑、kernel_function.cu 的 CUDA 后处理再到 app_yolov7.cpp 的主程序入口一应俱全。适合正在把 YOLOv7 往边缘端或 GPU 服务上压的工程师尤其是想搞清楚 PTQ、QAT 怎么选、INT8 部署从哪下手的人。2. PTQ 与 QAT两条量化路线的原理、代价与选型2.1 PTQ不重训也能量化但校准集决定了精度上限PTQ 的思路很好懂训练出来的 FP32 权重已经收敛好了直接把它从 float 映射到 int8再用一小批校准数据统计每层激活值的动态范围把 scale 和 zero_point 固化下来。推理时 32 位乘加变成 8 位乘加浮点矩阵运算交给 TensorRT 的 INT8 kernel 跑模型体积压到原来的四分之一吞吐提升通常在 1~2 倍具体看层结构里卷积占比有多高。选 PTQ 的理由很简单工程上零训练成本不需要动训练流程权重和 BN 全部保持原样从权重文件到 INT8 engine 的转换在一个小时内搞定。它的问题出在激活分布的长尾上。如果某些层的激活值有离群点TensorRT 用 entropy 或 mse 方式估算 scale 时就会被拉偏量化误差逐层累积小目标密集的场景经常首当其冲。我见过最夸张的一次PTQ 后车载摄像头画面里行人框全部飘到天上查到最后就是某一层的 ReLU 输出有个 1000 倍离群点把整条量化链带崩了。实际操作里校准集的质量比数量重要。我习惯从训练集里抽 300~500 张图按场景覆盖均匀取样白天、夜晚、逆光、遮挡都要有绝对不做随机翻转或色彩抖动这类增强因为增强等于引入训练时没见过的分布校准出来的激活范围容易偏。批量大小主要看显存一次喂 8 张让 TensorRT 在跑完整个校准集的过程中收集激活直方图再算出最优 scale。下面是用 Python 准备 PTQ 校准数据的一个常见做法把图片处理后打包成 TensorRT 校准器能直接消费的 batchimport cv2 import glob import numpy as np from tqdm import tqdm def build_calib_set(image_dir, out_path, target_size640, batch_size8): # 从指定目录读取图片统一做 letterbox 后保存为 npy # letterbox 函数复用官方 repo 的 utils.py 实现即可 img_paths sorted(glob.glob(f{image_dir}/*.jpg))[:500] batch [] for i, p in enumerate(tqdm(img_paths)): img cv2.imread(p) img letterbox(img, (target_size, target_size), 114) img img[:, :, ::-1] # BGR - RGB img img.astype(np.float32) / 255.0 img np.ascontiguousarray(img.transpose(2, 0, 1)) batch.append(img) if len(batch) batch_size: # 固定 batch 是 TensorRT 校准器的最低要求 np.save(f{out_path}/calib_{i}.npy, np.stack(batch, 0)) batch [] # 不足一个 batch 的剩余数据丢弃避免 shape 不匹配这段代码里 target_size 必须和推理时一致letterbox 的补边值我用 114这是 YOLOv7 训练时的默认 pad 值。归一化只用 /255.0 缩放到 0~1不做减均值因为现在的导出脚本基本都在 ONNX 里自带了减均值逻辑喂进去的图已经是模型期待的张量分布。校准集选训练集而不是验证集是底线验证集在评测环节还要充当检验样本拿它做校准等于让模型提前见过答案精度看着虚高上线后立刻露馅。2.2 QAT伪量化训练用多几天训练换回精度QAT 的关键动作是在训练前向里插入 fake quant 节点把量化再反量化这一步在浮点图上完整模拟一遍。前向时 x 被四舍五入成 int8 再还原成浮点参与计算反向时用直通估计器STE让梯度跨过这个不可导的过程权重在微调中主动去适应量化带来的扰动。最后切到真 int8 时模型看到的数值分布和训练时基本一致精度损失就被压到了最小。YOLOv7 的 QAT 训练通常这么做先拿 PTQ 跑通验证一遍确认量化路径本身没问题再加载原来训练好的 FP32 预训练权重把主干和检测头的部分层替换成伪量化版本微调。常见做法是把学习率压到原来预训练的十分之一到二十分之一关闭大部分数据增强训练轮次控制在十几到几十个 epoch。训练完导出的模型和 PTQ 一样还是 ONNX只是它已经见过量化误差切到 INT8 后数值分布不容易漂移。训练入口我一般用一套封装好的命令把关键参数钉死避免每次手工确认python train.py \ --data data/coco.yaml \ --weights yolov7.pt \ --epochs 30 \ --batch-size 16 \ --img 640 640 \ --hyp data/hyp.scratch.qat.yaml \ --device 0--weights 直接指到 FP32 预训练权重--hyp 指向一套 QAT 专用超参文件里面学习率设成原训练方案十分之一数据增强项全部关闭。这个细节容易翻车学习率大了前几步梯度就会把已经收敛好的权重冲走后面等于重新训练一个低精度模型难度陡增还未必收敛。batch-size 设不下时优先降尺寸而不是降 batch640 输入降到 512 对精度影响小batch 减半对 BN 统计的影响反而大。QAT 的显存开销比普通训练略高因为伪量化节点在前向要同时保留量化前后的两份数值显存卡住就把 batch-size 降到 8 或 16不要硬抗。整套流程跑完mAP 通常能拉回到离 FP32 不到 0.5 个百分点的水平代价就是 GPU 多跑几天以及数据侧多一套标注和校验流程。这也是为什么我建议把 QAT 定位成保底方案而不是默认方案。2.3 选型判断先 PTQ 试水精度不够再上 QAT很多团队一上来就想上 QAT其实没必要。量化选型有个省事的判断路径PTQ 导出的引擎先在自己的测试集上完整刷一遍看 mAP 比 FP32 掉多少。业务容忍线通常在 1~2 个点以内PTQ 能压住就直接上线省下来的训练时间拿去调别的指标压不住再考虑 QAT而不是一开始就跳过 PTQ 去重训一个模型。这里有个容易被忽视的细节QAT 不是对任意模型都能救回来。如果 PTQ 精度掉得厉害是因为某个特定算子对量化敏感QAT 能通过微调让它适应但如果问题本身是输入分布太杂、小目标占比极高那 QAT 也只是在同一个分布上学到更稳的量化参数解决不了分布层面的缺陷。判断方法很简单跑 PTQ 后在验证集上按类别拆精度看看是不是只有少数类在掉如果所有类均衡地掉 2 个点QAT 大概率能救如果只有某个类掉 15 个点先查这个类的标注质量和样本量再决定是否上 QAT。对比项PTQQAT训练成本无只需校准集需要重训通常几天典型精度损失mAP 损失 1~3 个点可控制在 0.5 个点内数据需求300~500 张静态图完整训练集工程复杂度低一个通宵可上线需要改训练脚本和调参适用场景精度余量大、上线急精度敏感、需要逐类校准这个表把两条路线摆开就清楚PTQ 是快速试错工具QAT 是保底武器。资源包里的 sampleOptions.cpp 同时支持 --calib 校准和 QAT 两种精度模式的加载就是为了让同一套 C 工程能在两种量化路线之间切换部署不用为每种路线各写一套推理代码。3. 从 .pt 到 TensorRT 引擎ONNX 导出与 C 推理工程搭建3.1 模型导出把 PyTorch 权重转成 ONNX 并固化为引擎TensorRT 读不了 .pt 文件它吃的是 ONNX 或 TensorRT 自家的 engine 格式。所以第一步永远是 .pt → .onnx。导出时的关键参数不少输入尺寸我建议在导出时就固定成业务真实用到的尺寸不要留动态宽高否则后面 ONNX 里一旦有 NMS 插件TensorRT 的 optimization profiles 也得跟着动态化工作量成倍涨还容易踩坑。python export.py --weights yolov7.pt \ --img 640 \ --batch 1 \ --include onnx \ --simplify--simplify 会调用 onnx-simplifier 把冗余的 reshape 和 transpose 揉平转出来的计算图更干净TensorRT 解析时不容易遇到不支持的子图。很多人在导出时忽略这个参数后面转 engine 总告警说有一两个算子走了 CUDA kernel 而不是 TensorRT 层这部分算子拖慢的是每帧推理的尾部延迟积少成多。拿到 ONNX 后用 trtexec 验证转换和校准/usr/src/tensorrt/bin/trtexec --onnxyolov7.onnx \ --saveEngineyolov7_int8.engine \ --int8 --calibcalib.txt --calibCachecalib.cache \ --workspace2048 --verbose--int8 触发量化路径--calib 指定校准图片列表--calibCache 把本次校准结果缓存成文件。第二次转换直接读 cache不用重新跑校准省的时间非常可观。--workspace 单位是字节2048 表示 2GB显存小于这个值的卡要把数调小新版本 TensorRT 里这个参数改叫 --memPoolSize写法变了但作用一样。trtexec 只能验证转换是否成功业务上还是要用 C 接口动态加载 engine这也是下面这个工程存在的意义。3.2 C 工程结构从 app_yolov7.cpp 到 yolo.cpp 的数据流这套 C 推理工程是典型的分层结构。按数据流动的顺序解释一遍对照文件去读会快很多文件职责关键数据common.cmake查找 TensorRT/CUDA编译开关TENSORRT_DIR、CUDA_INCLUDE_DIRSsampleOptions.cpp命令行参数解析--weights、--input、--calibapp_yolov7.cpp主程序入口串起整条链路配置对象、循环读图yolo.cpp引擎创建与推理流程engine、context、buffersutils.cpp图像读取、letterbox 预处理输入张量、缩放系数logger.cppTensorRT 日志回调用实现ILogger 接口kernel_function.cu设备端后处理核函数检测框、得分app_yolov7.cpp 的逻辑很直白解析参数 - 用 yolo.cpp 里的接口创建 engine - 把图像经过 utils.cpp 预处理成张量 - 绑定显存 buffer - executeV2 执行 - 解析输出 - 打印或渲染结果。yolo.cpp 是核心它负责创建 TensorRT runtime、反序列化 engine并在每次推理时管理 device buffer 和 host buffer 的内存拷贝同时把原始输出矩阵 reshape 成候选框列表。我把完整流程压成最小化的推理片段忽略错误处理后的结构长这样// app_yolov7.cpp 内部简化后的核心推理流程 // 1. 创建 runtime 和 engine nvinfer1::IRuntime* runtime nvinfer1::createInferRuntime(logger); nvinfer1::ICudaEngine* engine runtime-deserializeCudaEngine(engineData, engineSize); // 2. 从 engine 拿输入输出 binding 信息 nvinfer1::IExecutionContext* context engine-createExecutionContext(); int inputBinding engine-getBindingIndex(images); int outputBinding engine-getBindingIndex(outputs); // 3. 分配 device 和 host 内存 cudaMalloc(inputBuffer, inputSize * sizeof(float)); cudaMalloc(outputBuffer, outputSize * sizeof(float)); float* hostOutput new float[outputSize]; // 4. CPU 侧图像处理完成拷到 device cudaMemcpy(inputBuffer, hostInput, inputSize * sizeof(float), cudaMemcpyHostToDevice); // 5. 交给 GPU 跑推理 context-executeV2(buffers); cudaMemcpy(hostOutput, outputBuffer, outputSize * sizeof(float), cudaMemcpyDeviceToHost);第 4 步的 hostInput 来自 utils.cpp 里的 letterbox 输出第 5 步 executeV2 是同步阻塞调用推理期间 CPU 完全空闲这也是为什么很多上线工程会把 executeV2 换成异步版本 enqueueV2 配合 cudaStream把前处理、推理、后处理做成三级流水线。参数上inputSize 对应 1×3×640×640 的 float 元素数outputSize 取决于输出 head 的个数常规 YOLOv7 配置下大致在 25200×(580) 这个量级实际以导出的 ONNX 输出 shape 为准不要拍脑袋写死。3.3 sampleOptions.cpp 与编译细节参数怎么设、依赖怎么接sampleOptions.cpp 是入口参数的统一收口--weights 指定 engine 路径--input 是图片或视频目录--precision 区分 fp32/fp16/int8--maxBatch 控制动态 batch。它的职责不在于保证这些参数正确而在于让同一个二进制在测试环境和生产环境之间切换时不需要重新编译。编译这块common.cmake 里通常这样接 TensorRT 和 CUDA 依赖# common.cmake 片段 find_package(CUDA REQUIRED) find_library(TENSORRT_LIB nvinfer HINTS ${TENSORRT_DIR}/lib) include_directories(${TENSORRT_DIR}/include ${CUDA_INCLUDE_DIRS}) target_link_libraries(yolov7_app PRIVATE ${TENSORRT_LIB} ${CUDA_LIBRARIES} cudnn)TENSORRT_DIR 一般通过环境变量传入不要在 find_package 里硬编码绝对路径。工程根目录用 cmake -DTENSORRT_DIR/path/to/tensorrt 从命令行注入方便 Docker 里多版本共存时快速切换。GPU 架构参数也要提前确认CMAKE_CUDA_ARCHITECTURES 设成 61 对应 GTX 1070 这类 Pascal 卡设成 75 对应 Turing设成 89 对应 Ada写错架构编译出来也能跑但 kernel 性能会差不少因为 CUDA 编译器生成的 SASS 是针对架构特化的。如果你用的 TensorRT 是 10.x还要额外注意计算能力边界GTX 1070 的 compute capability 是 6.1处于 10.x 支持范围的下限附近跑 FP32/FP16 没问题但部分 INT8 算子的覆盖度不如 Turing 之后的架构个别层会退到 FP32 执行量化加速效果打折扣。老卡部署时别期待所有 INT8 算子都提速先跑一遍基准测出真实收益再决定上不上量化。4. YOLOv7 部署避坑清单从乱码框到精度掉点4.1 推理结果乱码预处理参数不一致现象engine 转换成功、推理也正常跑通但输出框要么满天飞要么全是 0.999 置信度的空框完全没有语义。原因绝大多数情况是训练和推理两端的输入预处理不一致。YOLOv7 训练时输入是 letterbox 补边后的 640×640如果你的代码只做了 resize 压扁长宽比变形会让模型识别崩掉或者归一化多减了一次均值颜色分布漂移。TensorRT 引擎没有记忆功能它只认输入张量的数值分布喂进去的东西跟训练时不一样输出自然乱。解决把预处理参数固定成一列常量target_size640、pad_value114、归一化 /255.0 且不做减均值。改完生成 engine 之前先用一张已知图片在 PyTorch 里跑出预测框坐标在 C 侧读同一张图走完整链路比对输出框坐标误差在 1 个像素以内然后再继续调后续逻辑。这个对比动作虽然土但能挡住九成预处理问题。4.2 PTQ 精度暴跌校准集和校准方法都在背锅现象PTQ 转完 INT8 enginemAP 相比 FP32 掉了 10 个点以上模型基本不可用检测框全面漂移。原因校准集质量差是头号嫌疑。用测试集做校准会导致推理评测时精度虚高上线后暴露真实水平用增强过度的图片校准会让激活直方图失真校准集只含单一场景比如全是白天街景夜间小目标直接黑掉。另一个原因是校准方法选错默认的 entropy 在部分检测模型上不如 mse 或 percentile99.9 稳。解决校准集从训练集选 300~500 张覆盖目标出现的全部典型场景图片过一遍 letterbox 保持静态分布。校准方法三个方案各试一遍在验证集上取 mAP 最高的那个并把对应校准缓存保存下来。以后重新生成 engine 时直接复用缓存避免结果漂移。校准缓存文件最好和 engine 同名同版本归档否则版本管理混乱时你会分不清哪个引擎对应哪份校准。4.3 编译报错TensorRT 与 CUDA 版本错配现象cmake 配置能通过编译时却报找不到 nvinfer.h 或 libnvinfer.so或者链接成功但运行时报 undefined symbol。原因TensorRT 的 include 和 lib 来自不同版本或者 lib 路径没加进运行时搜索路径。常见情况是系统里同时装了两个 TensorRTcmake 找到的是 A 版本头文件运行时 LD_LIBRARY_PATH 又指向 B 版本库符号表对不上。解决编译和运行统一用同一份 TensorRT。编译时显式传 -DTENSORRT_DIR/opt/tensorrt 指定目录运行前用 export LD_LIBRARY_PATH/opt/tensorrt/lib:$LD_LIBRARY_PATH 把对应版本的库放到最前面。如果还报 undefined symbol用 ldd 查看可执行文件实际链接到的 libnvinfer.so 路径确认不是被系统里另一个老版本抢先。这个问题在 Docker 环境里更隐蔽容器里和宿主机各装一份 TensorRT 时非常容易踩。4.4 目标框重复NMS 被做了两次现象同一辆车出十几个框置信度都差不多nms_iou 阈值怎么调都压不掉框始终叠在一起。原因导出 ONNX 时模型内部已经带了 NMS 节点C 侧 yolo.cpp 的 decode 之后又做了一次 NMS两次叠加导致重复框无法被有效过滤。这个现象在导出脚本和推理逻辑来自不同作者、不同版本时非常常见尤其是直接拿别人仓库的导出脚本和自己的推理代码拼接时。解决先确认 ONNX 的最后一个节点是什么打开 Netron 可视化看最后一层是 NonMaxSuppression 还是普通的 unsqueeze 输出。如果 ONNX 已经带 NMSC 侧只做阈值过滤和输出整理如果没有C 侧用 kernel_function.cu 里的 NMS 核函数做掉。判断方法最省事的就是可视化一次 ONNX几十秒就能定下来不用猜。4.5 Docker 镜像构建失败GPU 基础镜像版本对不上现象docker build 过程中 TensorRT 安装成功但容器启动后运行 app_yolov7 直接崩掉或者报 CUDA driver version is insufficient。原因容器内的 CUDA 运行时库和宿主机的 NVIDIA 驱动版本不配套。TensorRT 本身不涉及驱动但它依赖的 CUDA kernel 需要设备驱动支持宿主机驱动比容器要求的版本旧时cudaMemcpy 到 device 的调用就直接失败错误信息还不一定指向驱动。解决先看本机 nvidia-smi 里的驱动版本分支选择对应 CUDA 版本的 nvidia/cuda 基础镜像并在 Dockerfile 里把 TensorRT 的版本锁死不要用 latest。容器内跑一次 nvidia-smi 能通过不代表一切正常真正可靠的验证是启动一个最小示例跑一次 executeV2这一步放在镜像构建的 CMD 里最稳能把启动失败的问题在构建期暴露出来。5. 输入预处理与后处理letterbox、坐标还原与 CUDA NMS 细节5.1 letterbox为什么不能直接 resizeYOLOv7 训练时用 letterbox 把图像按原始长宽比缩放到目标尺寸剩余区域用固定灰度值补边。这样物体不会被压扁模型学到的宽高比特征在推理时不会失效。直接 resize 到 640×640 的坏处很明显一辆 4:3 的车被拉成 1:1训练时没见过这种变形检测框的置信度和定位精度都会掉。C 侧我一般用 cv::resize 缩放后配合 cv::copyMakeBorder 补边避免手写像素复制循环。核心要记录两个值缩放系数 scale 和补边偏移 pad_x、pad_y它们是后处理坐标还原的唯一依据拿 int 去存会在 640 大图上累积出 3~5 个像素的误差。// utils.cpp 里的 letterbox 核心逻辑简化 cv::Mat letterbox(const cv::Mat src, int target_size, int pad_value, float scale, float pad_x, float pad_y) { int w src.cols, h src.rows; scale std::min((float)target_size / w, (float)target_size / h); int new_w (int)round(w * scale); int new_h (int)round(h * scale); pad_x (target_size - new_w) / 2.0f; pad_y (target_size - new_h) / 2.0f; cv::Mat resized; cv::resize(src, resized, cv::Size(new_w, new_h)); cv::Mat out(target_size, target_size, CV_8UC3, cv::Scalar(pad_value, pad_value, pad_value)); resized.copyTo(out(cv::Rect((int)pad_x, (int)pad_y, new_w, new_h))); return out; }这段代码里 scale 是全局缩放比例pad_x/pad_y 是补边偏移它们在后面坐标还原时必须原样传出去。补边值固定 114这一条和训练时的 padding 参数严格对齐如果训练时用的是 0 或 128推理侧也必须跟着改否则等效于给模型加了一层训练时没见过的噪声。很多工程只在预处理时算了 pad没有保留 scale 的 float 精度直接在 int 上算还原小目标框的 IoU 一下就掉了好几个点。5.2 输出解析与坐标还原从模型张量到原图坐标YOLOv7 导出的 ONNX 输出格式有两种常见形态一种是解码前的三个尺度分支原始张量通道数是 num_anchors×(41num_classes)要先做 sigmoid、anchor decode 才能拿到框另一种是已经解码的 1×(box_info)×7 格式前四位是归一化坐标 x1,y1,x2,y2第五位是得分后几位是类别索引。两种形态在 C 侧都要支持靠导出的 ONNX 结构判断不要写死成一种。解码完成的坐标用 letterbox 参数还原到原图坐标系公式非常短// 从 letterbox 坐标还原到原图坐标注意 clamp 是必须的 float x1_orig clamp((x1_letterbox - pad_x) / scale, 0, orig_w - 1); float y1_orig clamp((y1_letterbox - pad_y) / scale, 0, orig_h - 1); float x2_orig clamp((x2_letterbox - pad_x) / scale, 0, orig_w - 1); float y2_orig clamp((y2_letterbox - pad_y) / scale, 0, orig_h - 1);这里的 clamp 不是可选项。letterbox 补边后模型输出的框有一部分可能落在 pad 区域内映射回原图就会变成负坐标或超出边界不 clamp 轻则画框出界重则后续裁剪 ROI 时越界访问。阈值参数上我固定 conf_thresh0.25、nms_iou0.45 起步这两个值和 YOLOv7 官方验证时的默认值对齐。加大 nms_iou 会增加相邻目标被合并的风险下调 conf_thresh 会拉高召回但堆满小目标假阳框业务侧按场景微调前先记下这两组初始值。5.3 kernel_function.cu把 NMS 塞进 GPU在 CPU 上对 25200 个候选框做 NMS单帧耗时 5~10ms在 1080p 视频流多路并发时直接吃光推理管线的预算。kernel_function.cu 的核函数思路是把候选框按置信度排序在 GPU 上并行比较每个框与其高置信度框的 IoU把被抑制的标记为无效。这个过程不是全并行通常做法是 batch 内逐类别循环类别数越少并行度越高。// kernel_function.cu 中 NMS 核函数骨架 __global__ void nms_kernel(const float* boxes, const float* scores, int num_boxes, float iou_threshold, bool* keep) { int idx blockIdx.x * blockDim.x threadIdx.x; if (idx num_boxes) return; for (int j 0; j num_boxes; j) { if (scores[j] scores[idx]) { // 常规 IoU交集面积 / 并集面积 float ix1 max(boxes[idx*4], boxes[j*4]); float iy1 max(boxes[idx*41], boxes[j*41]); float ix2 min(boxes[idx*42], boxes[j*42]); float iy2 min(boxes[idx*43], boxes[j*43]); float inter max(0.0f, ix2 - ix1) * max(0.0f, iy2 - iy1); float area1 (boxes[idx*42] - boxes[idx*4]) * (boxes[idx*43] - boxes[idx*41]); float area2 (boxes[j*42] - boxes[j*4]) * (boxes[j*43] - boxes[j*41]); if (inter / (area1 area2 - inter) iou_threshold) { keep[idx] false; return; } } } keep[idx] true; }这个核函数假设 boxes 已经按置信度降序排好实际工程里要先做一次排序常见做法是 thrust::sort_by_key 或手写基数排序。blockDim 设 256 比较稳妥num_boxes 超过 65535 时 grid 维数要相应加大。CUDA 后处理和 CPU 后处理的结果差异主要来自排序稳定性最终输出框基本一致但吞吐差距明显这也是把后处理搬进 GPU 的最大理由。6. 量化部署的验证方法三份基准才能放心上线想确认量化后的模型是否真的能上线光看一两张图的结果远远不够。我的做法是固定三份基准FP32 引擎的 mAP、PTQ INT8 引擎的 mAP、QAT INT8 引擎的 mAP全部在同一个验证集上跑完再做速度测试。速度测试用 trtexec 直接量化出吞吐差异/usr/src/tensorrt/bin/trtexec --loadEngineyolov7_fp32.engine --shapesimages:1x3x640x640 /usr/src/tensorrt/bin/trtexec --loadEngineyolov7_int8.engine --shapesimages:1x3x640x640对比两行输出的 End-to-End 耗时就能把 INT8 的提速收益量化出来。精度侧我在 C 程序的输出端直接做了统计把还原后的框写入 JSON用 Python 脚本和标签文件算 AP50 和 mAP这样精度和速度都不依赖外部工具链生产环境也能复现。从那次被 PTQ 精度暴跌坑过之后我每次换 TensorRT 版本或换显卡都强制重新走一遍三份基准的流程校准缓存和 engine 文件按版本命名归档绝不凭感觉把新的 INT8 引擎直接推到生产。这套习惯帮我挡掉了至少两次无声的量化回归希望帮到你。本文还有配套的精品资源点击获取
返回列表