☰
YOLOv7 INT8量化部署全链路:PTQ/QAT训练到TensorRT C++推理
2026/10/8 7:41:01 网站建设 项目流程

简介:YOLOv7目标检测模型的高效部署往往受限于模型体积与推理速度,这份资源正是面向算法工程师与C++开发者的量化部署完整方案,涵盖PTQ与QAT两种训练量化路径,并给出基于TensorRT的C++推理代码。包内共134个文件,包括35个Python脚本负责训练与量化流程,33个YAML配置用于模型与训练参数设定,C++源文件、头文件及CUDA kernel构成TensorRT部署主体,另有notebook示例、说明文档和Dockerfile便于环境复现,压缩包整体约35.14MB。资源细致拆解了模型加载、输入预处理、推理执行、结果解析和NMS后处理等环节,代码注释充分,可帮助开发者根据精度与速度需求调整量化参数,并快速集成到真实项目中。已有486人学习,适合希望在GPU环境下从训练、量化到TensorRT部署完整落地YOLOv7的开发者。

1. YOLOv7 量化部署:PTQ/QAT 训练到 TensorRT C++ 推理的完整链路

把 YOLOv7 从 PyTorch 搬到 TensorRT,卡点往往不在训练,而在量化到部署这一段。FP32 下好好的模型,一上 INT8 就漏检;ONNX 好不容易导出,构建又报算子不支持;engine 出来了,C++ 端前后处理跟 Python 对不上,结果全乱。

这套 C++ 部署工程就是冲这个痛点来的。压缩包里包含 common.cmake、sampleOptions.cpp、yolo.cpp、utils.cpp、app_yolov7.cpp、logger.cpp 和 kernel_function.cu,覆盖 PTQ 和 QAT 两条量化路线,从训练校准到构建 INT8 engine,再到推理解码的全链路。模型怎么量化、engine 怎么构建、C++ 怎么调,一条链条给全。

适合手上有 YOLOv7 权重、想压到 INT8 用 C++ 落生产代码的工程师。新手能顺着工程跑通,熟手直接看参数体系和量化选型,少踩坑。

2. 量化选型:PTQ 与 QAT 的精度代价、校准集与训练侧准备

2.1 PTQ 为什么快但容易掉点:量化误差从哪来

PTQ 的流程一句话讲完:把训练好的 FP32 权重在推理机上跑一遍校准数据,统计每层激活值分布,算出 scale 和 zero point,然后把权重和激活都从 FP32 量化到 INT8。因为不需要反向传播,几分钟到几十分钟就能出结果,这是它最大的优势。

代价就是精度不受控。量化本质是把连续的浮点分布映射到 256 个离散整数上,映射误差取决于两个因素:一是分布形状,二是动态范围。TensorRT 默认的熵校准(EntropyCalibrator2)会尝试最小化量化前后的 KL 散度,但如果某层激活值里有几个特别大的离群点,max 被拉高,剩下 99% 的数值只能挤在很低的量化分辨率里,精度就悄悄掉了。

YOLOv7 是 anchor 多尺度检测,小目标只出现在深层特征图的稀疏位置,这些位置的激活值分布和背景差异大,INT8 量化后信息损失比大目标更明显。所以 PTQ 翻车最常见的症状不是全盘掉点,而是小目标漏检、远处物体框不稳。这是结构问题,不是校准集不够就能解决的——当然校准集太烂会雪上加霜。

提示:如果你的任务里小目标占比高,直接默认走 QAT,别在 PTQ 上调半天校准集,省下的时间够你训练好几轮了。

2.2 QAT 训练:模拟量化与精度回血

QAT 的思路是让模型在训练时就适应量化误差。做法是在网络里插入 fake quant 算子,前向传播时先把 tensor 量化成 INT8 再反量化回 FP32,让梯度在训练过程中「看见」量化噪声,从而学到更鲁棒的权重。NVIDIA 官方配套的是 pytorch_quantization 库,和 TensorRT 的 INT8 完全对齐,QAT 导出的 ONNX 可以直接被 TensorRT 解析,不需要再手动校准。

我一般会这么做:

from pytorch_quantization import quant_modules from pytorch_quantization import calib from pytorch_quantization.tensor_quant import QuantDescriptor # 1. 全局替换标准算子:nn.Conv2d / nn.Linear 变为带 fake quant 的版本 quant_modules.initialize() model = create_yolov7(weights="yolov7.pt", device="cuda") # 2. 设置量化描述符:per-tensor 对称量化,校准方式用 max quant_desc = QuantDescriptor(calib_method="max", axis=None) for name, module in model.named_modules(): if hasattr(module, "input_quantizer"): module.input_quantizer.set_desc(quant_desc) module.weight_quantizer.set_desc(quant_desc) # 3. 冻结 backbone,只训练检测头,学习率压到 1e-5 量级 for name, param in model.named_parameters(): if name.startswith("model.0"): # YOLOv7 backbone 前缀 param.requires_grad = False

三段逻辑分别是:替换算子、统一量化描述符、限制训练范围。quant_modules.initialize()会把标准卷积和全连接换成带 fake quant 的版本,之后你只需要按正常流程训练。两个细节必须注意:学习率比正常训练低一到两个数量级,否则量化尺度还没稳定权重先震掉了;BN 层尽量在前几个 epoch 冻结,等统计量稳定了再放开。epoch 数不用多,10~20 轮通常就够把精度拉回来。

QAT 的代价是训练时间长,还得维护训练管线。但如果目标是长期跑生产,这笔投入是值的——QAT 模型在 INT8 下精度可以做到和 FP32 几乎持平,省掉的不只是重新训练的时间,还有上线后反复调 bug 的精力。

2.3 校准集与预处理:PTQ 精度的命门

不管是 PTQ 还是 QAT 后转 INT8,校准集都绕不开,只是 PTQ 对校准集更敏感。校准集三条铁律:数量 100~500 张、不重复训练集、分布贴近部署场景。

数量太少,统计出的分布不够稳;用训练集会高估精度,因为模型对这些图过度熟悉;部署场景里是夜晚路灯下的车,校准集却全是白天拍的,量化出来的 scale 自然不对。校准集要覆盖光照变化、目标尺度变化和类别均衡,宁可图多而杂,不要图少而纯。

校准器的选择也会影响结果,TensorRT 里常见的三种:

校准器原理适用场景
EntropyCalibrator2最小化量化前后 KL 散度默认首选,分布复杂时稳
MinMaxCalibrator直接用激活 min/max 定范围分布简单、无离群点
PercentileCalibrator截断 99.x% 再定范围离群点把 max 拉高时

预处理一致性是另一个高频翻车点。模型训练时如果是 BGR、归一化到 0~255,TensorRT 推理就必须完全一致;letterbox 的填充值、缩放方式也要对齐。TensorRT 的 INT8 校准器在getBatch里反复读校准图,走的就是这条链:

bool Int8EntropyCalibrator2::getBatch(void* bindings[], const char* names[], int nbBindings) noexcept { if (m_next_idx + m_batch_size > m_calib_images.size()) return false; for (int i = 0; i < m_batch_size; ++i) { cv::Mat img = cv::imread(m_calib_images[m_next_idx + i]); cv::Mat resized = letterbox(img, m_net_w, m_net_h, m_scale, m_pad_x, m_pad_y); resized.convertTo(resized, CV_32FC3, 1.0 / 255.0); cv::cvtColor(resized, resized, cv::COLOR_BGR2RGB); cudaMemcpy(m_device_buf + i * m_input_vol, resized.data, m_input_vol * sizeof(float), cudaMemcpyHostToDevice); bindings[0] = m_device_buf; } m_next_idx += m_batch_size; return true; }

这个getBatch就是把校准图按推理前处理的标准流程走一遍:letterbox、归一化、BGR 转 RGB,最后拷进 device buffer。bindings[0]对应网络的输入 tensor,返回 false 表示数据用完,TensorRT 停止校准并生成 INT8 scale 表。校准链和推理链必须共用同一套预处理函数,我最开始就是在这里各写了一份,结果校准出来的 scale 和实际推理输入对不上,精度掉了 4 个点还找不到原因。

3. 构建 INT8 Engine:common.cmake、sampleOptions.cpp 与 Dockerfile 环境

3.1 工程结构:common.cmake 里到底配了什么

先看文件清单,这套工程的分工很清晰:

文件职责
common.cmake编译配置:CUDA 架构、TensorRT/OpenCV 依赖、C++ 标准
sampleOptions.cpp命令行参数解析:构建精度、workspace、batch 等
yolo.cppYOLOv7 特有逻辑:输出解码、anchor、NMS 参数
utils.cpp图像预处理、device buffer 分配与拷贝
app_yolov7.cpp主程序:检测流程、性能统计
logger.cpp / logging.hTensorRT 日志回调封装
kernel_function.cuCUDA kernel:GPU 上的 sigmoid / 解码加速
Dockerfile部署环境:TensorRT、CUDA、编译依赖

common.cmake 是所有目标的公共底座,关键配置就这几项:

# common.cmake 核心配置段 set(CMAKE_CXX_STANDARD 17) set(CMAKE_CUDA_STANDARD 17) find_package(CUDA REQUIRED) find_package(OpenCV REQUIRED) find_package(TensorRT REQUIRED) # 通常手动指定 TensorRT_ROOT set(CMAKE_CUDA_ARCHITECTURES "70;75;80;86") # T4=75, A100=80, 3090=86 include_directories(${TensorRT_INCLUDE_DIR} ${CUDA_INCLUDE_DIRS} ${OpenCV_INCLUDE_DIRS}) link_libraries(nvinfer nvonnxparser cudart ${OpenCV_LIBS})

CMAKE_CUDA_ARCHITECTURES决定了 kernel_function.cu 编译出的 cubin 目标架构。T4 是 75,A100 是 80,3090 是 86。如果编译机和部署机架构不一致,kernel 会在运行时直接失效或者 JIT 失败,这是典型的「编译能过、跑起来报 invalid device function」场景。我的习惯是编译参数里把目标机型架构写死,不要用默认值。

3.2 sampleOptions.cpp:构建与推理参数一栏读懂

sampleOptions.cpp 的作用是把命令行参数转成内部配置结构。YOLOv7 的 INT8 部署参数可以分成三组:模型与精度、构建选项、运行时选项。

参数含义典型值
--precision构建精度fp32 / fp16 / int8
--workspace构建时显存上限1G ~ 4G
--batch_size固定 batch1 / 4 / 8
--calib_dirINT8 校准图目录./calib
--engine输出 engine 路径yolov7_int8.engine
--conf / --iou检测阈值0.25 / 0.45

代码里的结构大概长这样:

struct Options { std::string precision = "int8"; size_t workspace = 1ULL << 30; // 1 GiB int batch_size = 1; std::string calib_dir = "./calib"; std::string engine_path = "yolov7_int8.engine"; }; bool parseArgs(int argc, char** argv, Options& opt) { for (int i = 1; i < argc; ++i) { if (std::string(argv[i]) == "--precision") opt.precision = argv[++i]; else if (std::string(argv[i]) == "--workspace") opt.workspace = std::stoull(argv[++i]); else if (std::string(argv[i]) == "--batch_size") opt.batch_size = std::stoi(argv[++i]); else if (std::string(argv[i]) == "--calib_dir") opt.calib_dir = argv[++i]; else if (std::string(argv[i]) == "--engine") opt.engine_path = argv[++i]; } return true; }

解析完参数后,构建阶段的核心代码是创建 builder、network 和 config:

auto builder = createInferBuilder(logger); auto network = builder->createNetworkV2(1U << static_cast<int>( NetworkDefinitionCreationFlag::kEXPLICIT_BATCH)); auto parser = nvonnxparser::createParser(*network, logger); parser->parseFromFile("yolov7.onnx", 1); auto config = builder->createBuilderConfig(); config->setMemoryPoolSize(MemoryPoolType::kWORKSPACE, opt.workspace); if (opt.precision == "int8") { config->setFlag(BuilderFlag::kINT8); config->setInt8Calibrator(new Int8EntropyCalibrator(opt.batch_size, opt.calib_dir)); } else if (opt.precision == "fp16") { config->setFlag(BuilderFlag::kFP16); } auto engine = builder->buildSerializedNetwork(*network, *config);

kEXPLICIT_BATCH必须开,YOLOv7 的 ONNX 导出带显式 batch 维度,不开它动态 shape 和 batch>1 都玩不了。workspace给太小,builder 会跳过部分层融合,engine 性能打折;给太大,构建时显存不够直接崩。1G 起步、4G 封顶是我常用的区间,具体按目标 GPU 显存调。

3.3 Dockerfile 部署环境与版本对齐

生产部署我强烈建议容器化。TensorRT 的版本必须和宿主机 CUDA 驱动兼容,否则 runtime 报 libnvinfer.so 找不到或者 CUDA driver too low。如果不想用容器,在 Ubuntu 上装 TensorRT 最常见的坑是 deb 包装完以后dpkg -L找不到库路径,实际是因为没把/usr/local/tensorrt/lib加进LD_LIBRARY_PATH,跑 trtexec 直接报 shared library 加载失败。

用官方容器镜像能省掉这一整轮版本地狱:

FROM nvcr.io/nvidia/tensorrt:23.08-py3 RUN apt-get update && apt-get install -y libopencv-dev cmake g++ COPY . /app WORKDIR /app RUN mkdir build && cd build && cmake .. && make -j$(nproc)

版本对齐有一条硬规矩:构建 engine 的 TensorRT 主版本和推理运行时必须一致。8.x 构建的 engine 不能拿到 9.x 跑,跨 minor 版本有时能跑但不保证。我生产环境都是 build 和 run 用同一镜像,engine 文件名里带 TRT 版本号,比如yolov7_int8_trt85.engine,防止同事误用。

注意:kernel_function.cu 编译出的 cubin 依赖 CUDA 架构,Dockerfile 里对应CMAKE_CUDA_ARCHITECTURES要和目标 GPU 对齐,容器换机型后必须重新 build 一次,不能直接拷 engine 过去用。

4. C++ 推理流水线:yolo.cpp、utils.cpp 与 kernel_function.cu 的完整配合

4.1 前处理:letterbox resize 与 BGR 归一化

推理第一步是把任意尺寸图像转成网络输入。YOLOv7 训练时用的是 letterbox:按比例缩放,短边不足的地方用 114 填充,不能直接拉伸,否则检测框坐标会失真。

// utils.cpp: letterbox 保持宽高比的 resize + 填充 cv::Mat letterbox(const cv::Mat& src, int dst_w, int dst_h, float& scale, int& pad_x, int& pad_y) { float r = std::min(static_cast<float>(dst_w) / src.cols, static_cast<float>(dst_h) / src.rows); int new_w = std::round(src.cols * r); int new_h = std::round(src.rows * r); scale = r; pad_x = (dst_w - new_w) / 2; pad_y = (dst_h - new_h) / 2; cv::Mat resized; cv::resize(src, resized, cv::Size(new_w, new_h), 0, 0, cv::INTER_LINEAR); cv::Mat canvas(dst_h, dst_w, CV_8UC3, cv::Scalar(114, 114, 114)); resized.copyTo(canvas(cv::Rect(pad_x, pad_y, new_w, new_h))); return canvas; }

这里的 scale 和 pad 值必须保存下来,后处理把检测框映射回原图时要反向去掉。常见翻车点是直接用cv::resize把图拉成 640x640,框虽然能检测出来,但坐标映射回原图时整体偏移,尤其画面边缘的物体偏差明显。r是最小缩放比,pad_x/pad_y是居中填充偏移,这三个值在yolo.cpp的坐标反算里都要用到。

预处理第二段是格式转换:BGR→RGB、uint8→float、再除以 255,和训练脚本里 DataLoader 的 transform 一字不差:

// utils.cpp: 预处理输入并拷入 device buffer cv::Mat rgb; cv::cvtColor(letterboxed, rgb, cv::COLOR_BGR2RGB); rgb.convertTo(rgb, CV_32FC3, 1.0 / 255.0); cudaMemcpy(device_input, rgb.data, input_vol * sizeof(float), cudaMemcpyHostToDevice);

float 输入的好处是可以和训练完全对齐。如果你用 INT8 engine 且输入不是 float,拿到的是量化后的 raw 值语义,通道顺序上很容易出错。我习惯一律用 float 输入,量化层的事交给 TensorRT 内部处理。

4.2 推理执行:输入输出绑定与 enqueueV2

Engine 构建完成后,推理侧是反序列化 engine、创建 context、绑定输入输出 buffer、执行推理:

// app_yolov7.cpp: engine 加载与推理执行 std::vector<char> blob = loadFile("yolov7_int8.engine"); auto runtime = createInferRuntime(logger); auto engine = runtime->deserializeCudaEngine(blob.data(), blob.size()); auto context = engine->createExecutionContext(); int in_idx = engine->getBindingIndex("images"); int out_idx = engine->getBindingIndex("output"); int in_vol = 1 * 3 * 640 * 640; int out_vol = 1 * 25200 * 85; void* buffers[2]; cudaMalloc(&buffers[in_idx], in_vol * sizeof(float)); cudaMalloc(&buffers[out_idx], out_vol * sizeof(float)); // 动态 shape 需要先设置输入维度 context->setInputShape("images", Dims4{1, 3, 640, 640}); cudaStream_t stream; cudaStreamCreate(&stream); // 每帧:拷入输入 → enqueueV2 → 拷出输出 context->enqueueV2(buffers, stream, nullptr); cudaMemcpyAsync(host_output, buffers[out_idx], out_vol * sizeof(float), cudaMemcpyDeviceToHost, stream); cudaStreamSynchronize(stream);

bindings 数组下标必须用getBindingIndex拿,不能自己按 0、1 写死,ONNX 导出时的输入输出顺序不一定是你以为的顺序。25200 是 YOLOv7 在 640 输入下三个检测头的 anchor 总数:3 × (80×80 + 40×40 + 20×20),85 是 4 个坐标 + 1 个置信度 + 80 个 COCO 类别。类别数变了这个 out_vol 要跟着改。

4.3 后处理:解码、NMS 与 kernel_function.cu 的 GPU 加速

YOLOv7 的原始输出是 (1, 25200, 85) 的裸 tensor,这 85 个数是 logits,要过 sigmoid,坐标要按 anchor 解码成 xyxy。CPU 上对 25200 个框做 sigmoid 和解码,640 输入大概多花 2~3 ms/帧,多路并发就顶不住了。所以 kernel_function.cu 把 sigmoid + decode 挪到了 GPU:

// kernel_function.cu: 单 kernel 完成 sigmoid + decode + conf 过滤 __global__ void decode_kernel(const float* raw, float* out, int num_boxes, float conf_thresh, int num_classes) { int idx = blockIdx.x * blockDim.x + threadIdx.x; if (idx >= num_boxes) return; const float* p = raw + idx * (5 + num_classes); float obj = 1.0f / (1.0f + expf(-p[4])); if (obj < conf_thresh) { out[idx * 6] = 0; return; } float cx = 1.0f / (1.0f + expf(-p[0])); float cy = 1.0f / (1.0f + expf(-p[1])); float w = expf(p[2]); float h = expf(p[3]); // anchor stride 换算回输入图像坐标后写入 out out[idx * 6 + 0] = (cx - 0.5f * w) * stride; // x1 out[idx * 6 + 1] = (cy - 0.5f * h) * stride; // y1 out[idx * 6 + 2] = (cx + 0.5f * w) * stride; // x2 out[idx * 6 + 3] = (cy + 0.5f * h) * stride; // y2 out[idx * 6 + 4] = obj; out[idx * 6 + 5] = best_class_id; }

sigmoid用expf直接算就行,没必要引__expf的快速近似,decode 阶段精度差异会传导到框坐标上。真正要小心的是 anchor 和 stride 的换算关系,yolo.cpp 里配置的 anchor 表必须和训练时一致,YOLOv7 训练配置里三组 anchor 错一个数字,所有框整体偏移,而且这种错误不报错、肉眼还不容易发现。

NMS 这块我也留了 GPU 版本,思路是先用 conf 过滤把候选框压到几百个,再按 score 降序并行抑制。如果 batch 不大、候选框少,CPU 的std::sort+ 循环 NMS 也就 1 ms 不到,这时候 GPU NMS 反而因为 kernel 启动开销更慢。我的做法:候选框少于 2000 走 CPU,多于 5000 走 GPU,阈值设在 3000 左右,两边留了同一套接口,切换只改一个参数。

5. 部署避坑:量化掉点、动态 shape 与推理异常的排查记录

5.1 PTQ 后小目标大面积漏检

现象:INT8 engine 在验证集上 mAP 比 FP32 掉 5 个点以上,漏的基本都是小目标,大目标框倒还挺稳。

原因:小目标对应深层特征图,激活值分布稀疏,量化误差在低响应区域被放大;校准集里小目标样本又太少,scale 被大目标主导。

解决:先换校准集,把部署场景里的小目标样本加进来重新校准,数量凑到 300 张以上;如果还掉,直接上 QAT,用 fake quant 训练 10~20 轮,INT8 精度通常能回到 FP32 的 98% 以上。血泪经验:小目标任务别在 PTQ 上死磕,校准集换三轮不如 QAT 跑一轮。

5.2 QAT 模型导出 ONNX 后构建报算子不支持

现象:parser->parseFromFile返回 false,日志里出现 Unsupported Operator,通常指向 QuantizeLinear / DequantizeLinear 节点。

原因:pytorch_quantization 导出的 ONNX 带量化节点,TensorRT 版本太老不支持;或者导出时 opset 版本和 TRT 兼容性差。

解决:TensorRT 8.5 以上才完整支持 QAT ONNX 自动解析。导出时固定opset=13,先关掉所有额外优化,再用 trtexec 验证:

trtexec --onnx=yolov7_qat.onnx --int8 --saveEngine=yolov7_qat.engine

如果 trtexec 能过,C++ 端基本也能过,问题就定位在工程代码而不是模型。这一步能省大量调试时间。

5.3 动态 shape 下输入尺寸变化导致 RuntimeError

现象:固定 batch 的 engine 一切正常,一开动态 batch 就报binding "images" is incompatible。

原因:动态 engine 必须设置 optimization profile,每次推理前setInputShape的维度必须在 profile 的 min/opt/max 范围内。

解决:构建时加 profile,min 和 opt 设成 1x3x640x640,max 按实际批量设:

auto profile = builder->createOptimizationProfile(); profile->setDimensions("images", OptProfileSelector::kMIN, Dims4{1,3,640,640}); profile->setDimensions("images", OptProfileSelector::kOPT, Dims4{1,3,640,640}); profile->setDimensions("images", OptProfileSelector::kMAX, Dims4{8,3,640,640}); config->addOptimizationProfile(profile);

注意动态维度下输入尺寸不能变,YOLOv7 的 letterbox 必须固定 640x640,动态的只有 batch。

5.4 推理输出全零或全部低于阈值

现象:engine 构建成功、推理不报错,但输出数组全为 0,或者所有 score 都小于 conf_thresh。

原因:最常见是输入 buffer 没拷对——device 指针填错,或者预处理通道顺序和训练不一致;其次是 bindings 下标写死,输入输出顺序和 engine 实际不符。

解决:第一步用 trtexec 跑同一张图,如果输出正常就说明模型没问题,问题在工程代码;第二步打印引擎里真实的绑定名字和下标:

for (int i = 0; i < engine->getNbBindings(); ++i) { std::cout << engine->getBindingName(i) << " " << engine->getBindingDataType(i) << std::endl; }

确认images和output的下标,再逐行核对预处理是否和训练完全一致,通常这两处查完就能定位。

5.5 多路并发推理时结果错乱

现象:单线程推理稳定,多线程各自开 context 后,结果出现交叉错乱,A 路的框混进了 B 路的输出。

原因:context 是线程独立的,但enqueueV2里传的 cudaStream 如果共用,且 kernel_function.cu 的解码 kernel 没做 stream 隔离,两个流的输出 buffer 会互相覆盖。

解决:每路独立 context 加独立 cudaStream,拷贝和 enqueue 都走各自的 stream,每个流分别同步:

cudaMemcpyAsync(host_out_0, dev_out_0, vol, cudaMemcpyDeviceToHost, stream_0); cudaStreamSynchronize(stream_0); // 每路分别同步,不要统一同步

不要图省事共用一个大 stream,这是多路并发里最容易埋雷的地方。

6. 验证与进阶:app_yolov7.cpp 端到端测试与性能基线

6.1 跑通一条验证路径

资源里 app_yolov7.cpp 是现成的验证入口。推荐路径:先 FP32 构建 engine 跑通流程,再 INT8 对比,最后才上 QAT。每一步都留日志和中间产物:

./app_yolov7 --engine yolov7_fp32.engine --image test.jpg --out fp32.jpg ./app_yolov7 --engine yolov7_int8.engine --image test.jpg --out int8.jpg ./app_yolov7 --engine yolov7_qat.engine --image test.jpg --out qat.jpg

对比三张图的检测框数量和坐标,再算一遍验证集 mAP 差异,形成一个基线表:

模型mAP单帧延迟显存占用
FP32基线参考基准高
PTQ INT8可能掉 1~3%快 2~3 倍低
QAT INT8接近 FP32同 INT8低

数值按实际机器来,关键是流程要对齐。三张图如果框的数量和位置对不上,回头查预处理,别先怀疑量化。

6.2 性能调优的三个抓手

第一个是 workspace,它影响构建时的层融合质量,间接决定延迟上限;第二个是 batch,服务端场景 batch=4~8 的吞吐比 batch=1 高不少,但延迟会略涨;第三个是 stream 并发,多路视频流常见的提问是「T4 上 1080p 25fps、640 分辨率能撑几路」,我的经验值是 INT8 引擎单帧延迟在几毫秒量级,25fps 每帧预算 40ms,推理本身余量很大,瓶颈通常出在视频解码和预处理,不在模型推理。先测单路延迟,再按延迟预算反推路数,比拍脑袋靠谱。

6.3 一个养成的习惯

从那以后我每次接触新模型,都强制走一遍「FP32 基线 → PTQ 对比 → 需要再上 QAT」的三步流程,每步都留 engine 和日志,绝不让校准集预处理和推理预处理各写一套代码。这个习惯帮我少踩了很多坑,希望也能帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询