
简介面向嵌入式设备与边缘计算场景中需要加速实时目标检测的开发者这套基于Python的TensorRT INT8量化YOLOv5 ONNX模型实现方案围绕YOLOv5推理阶段的性能优化需求覆盖从ONNX模型导出、校准数据准备到INT8引擎构建与加载运行的完整流程适合具备一定深度学习部署基础、希望提升模型吞吐量的工程师。资源包共10个文件压缩后约6.95MB主要包含3个Python脚本、3个pyc文件、2个TensorRT引擎文件与2个校准缓存文件脚本实现校准器和量化转换逻辑trt文件为可直接加载的INT8引擎cache文件保存校准阶段确定的量化参数整体结构便于对照学习和二次开发。当前已有4470人学习下载。借助convert_trt_quant.py、calibrator.py等源码可以理解自定义校准器的编写方法、校准数据集的构造方式以及量化参数调节策略结合随包提供的yolov5s_int8.trt等产物还能快速验证INT8量化前后的性能差异为在资源受限设备上部署高效目标检测模型提供了可复用的脚本、参数配置与完整思路。1. 为什么偏偏选TensorRT INT8来跑YOLOv5 ONNX先把结论说在前面用Python训练完YOLOv5导出一个ONNX很容易难的是让这个ONNX在GPU上跑出接近实时的速度又不丢精度。很多人试过ONNX Runtime、试过OpenVINO最后发现部署到N卡上TensorRT还是绕不过去的坎。而TensorRT最常见的加速组合就是INT8量化把模型权重和激活从FP32压到8位整数推理速度能比FP16再快一截显存占用也几乎减半。这篇笔记要讲的就是从你手里的YOLOv5 PyTorch权重出发完整走一遍「导出ONNX → 准备校准集 → TensorRT INT8量化 → 拿到engine推理」的落地路径。适合那些已经把模型训练完、正在为产线GPU选推理框架的人也适合刚把TensorRT装好却不知道第一步该干什么的新手。我会把参数、代码和翻车点全部摊开。2. 从PyTorch导出YOLOv5的ONNX模型这一步决定后续能不能量化2.1 导出ONNX之前先确认TensorRT版本与GPU算力匹配很多人在导出ONNX这一步就开始出错根本原因是TensorRT版本和GPU算力对不上。TensorRT是一个绑定CUDA和GPU架构的推理引擎不是随便pip安装就能通用。常见做法是先在NVIDIA官网查一下你的显卡算力再决定装哪个版本的TensorRT。以GTX 1070为例它是Pascal架构算力6.1支持INT8但不支持Tensor Core。而TensorRT 10.x版本是否支持GTX 1070这个问题严格说不是「支持与否」而是「10.x的某些算子实现可能不再为Pascal优化甚至直接拉不起engine」。我遇到过很多次同样的代码在RTX 3090上构建engine成功换到1070上报错MISC_THREAD_LOCAL_INIT或者INVALID_VALUE最后只能降级到TensorRT 8.4 / 8.5。所以第一步先查算力再定版本。# 检查GPU算力Linux下直接跑nvidia-smi的查询模式 nvidia-smi --query-gpuname,compute_cap,memory.total --formatcsv # 或者用Python的pynvml读取更详细的信息 python -c import pynvml; pynvml.nvmlInit(); hpynvml.nvmlDeviceGetHandleByIndex(0); print(pynvml.nvmlDeviceGetName(h))上面两条命令输出的是显卡型号和显存算力还需要对照NVIDIA官方表格。Pascal架构是6.xVolta是7.0Turing是7.5Ampere是8.xAda是8.9。如果你手里是Turing以上的卡TensorRT 8.x和10.x都没问题如果是Pascal建议直接选TensorRT 8.5 LTS版本省得后面跟算子叫板。Python环境下TensorRT版本可以用tensorrt.__version__查看安装时注意CUDA版本和cuDNN版本要匹配否则import阶段就会报Could not load library libnvinfer.so。2.2 用官方export.py导出带动态shape的ONNXYOLOv5仓库里自带export.py导出ONNX最省事的方式就是直接调用它。但有一个关键点INT8量化时的校准数据集尺寸和最终推理时的输入尺寸可以不一样所以导出的ONNX最好带动态shape否则后面改输入分辨率就得重新导出。常见做法是给export.py传--dynamic --include onnx --opset 12三个参数。opset尽量不低于12TensorRT对低opset的某些算子比如Resize、ScatterND兼容性较差。# 在YOLOv5仓库根目录执行 python export.py --weights yolov5s.pt --include onnx --dynamic --opset 12 --simplify # 如果只想固定尺寸可以显式指定 python export.py --weights yolov5s.pt --include onnx --imgsz 640 640 --opset 12--simplify会调用onnx-simplifier把一些冗余的shape运算和常量折叠掉这一步强烈建议加上。因为TensorRT解析ONNX时遇到太复杂的Slice和Concat组合很容易报Plugin不支持或Unsupported layer。--dynamic参数导出的ONNX在三个维度上是动态的batch、height、width对应的动态轴名称通常是batch、height、width后面在TensorRT构建engine时需要显式指定optimization profile。导出完成后用onnx.shape_inference检查一下输出节点。YOLOv5的ONNX输出一般有三个对应三种尺度的检测头shape分别是[batch, 80, 6400]、[batch, 80, 1600]、[batch, 80, 400]80是类别数5个属性具体数值由你的模型决定。如果你看到的是[batch, 25200, 85]这种shape说明你导出的ONNX已经带了后处理拼接这种结构在TensorRT里不仅难量化还慢建议用不带后处理的尾椎。2.3 导出后必须用onnxruntime检查一遍别急着进TensorRTONNX导出成功不代表ONNX是对的。很多人在这一步跳过验证直接扔给TensorRT结果构建engine时报Node (Resize) requires 4 inputs这种错误回头找半天才发现在PyTorch里用了F.interpolate而导出时opset版本不够。正确姿势是先拿onnxruntime跑一遍对比ONNX的输出和PyTorch原模型的输出。注意onnxruntime和ONNX是两个概念ONNX是模型格式onnxruntime是推理引擎TensorRT也没法直接读ONNX格式它只是通过解析器把ONNX转换成自己的engine所以先用onnxruntime验证ONNX本身是否可用是很有必要的。# 先装onnxruntime注意这里只需要CPU版做验证就够了 pip install onnxruntime python - EOF import cv2, numpy as np, torch import onnxruntime as ort # 用一张真实图片测试不要用全零的假数据 img cv2.imread(test.jpg) img cv2.resize(img, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1) # BGR-RGB, HWC-CHW img np.ascontiguousarray(img).astype(np.float32) img / 255.0 img img[None, ...] sess ort.InferenceSession(yolov5s.onnx, providers[CPUExecutionProvider]) out sess.run(None, {images: img}) print([o.shape for o in out]) # 对比PyTorch原模型的输出 model torch.hub.load(ultralytics/yolov5, custom, pathyolov5s.pt) model.eval() with torch.no_grad(): torch_out model(torch.from_numpy(img)) print(torch_out.shape) EOF这个验证脚本要关注的是输出数值量级是否接近而不是完全一致。因为PyTorch原模型的后处理和ONNX裸输出本来就不一样只要没有NaN、没有inf并且数值范围在[-1, 10]内波动基本就能进下一步。这里最容易翻车的是图像预处理YOLOv5在训练时用的是RGB、0-1、BGR转换你导出ONNX后输入节点名是images它期望的输入就是标准化后的RGB千万不要把0-255的原始值塞进去否则后面INT8校准全在错误的数据分布上做精度直接崩掉。3. TensorRT INT8量化原理与校准数据准备3.1 INT8量化不是简单截断熵校准才是关键FP32、FP16、INT8这几种精度在实际部署里的区别用一句话讲就是FP32是基准FP16是半精度INT8是整型定点。FP16相比FP32速度快一倍上下精度损失肉眼几乎看不见INT8能把速度再翻一倍但模型权重分布和激活值分布会被粗暴地映射到256个整数刻度上。TensorRT的INT8量化不是拿着权重直接做round(weight * scale)而是要统计每一层激活值的分布然后在尽量少丢信息的前提下找一个合适的缩放因子。TensorRT提供了三种校准器IInt8LegacyCalibrator、IInt8EntropyCalibrator和IInt8MinMaxCalibrator。绝大多数YOLOv5部署场景用熵校准就够了原理是让量化前后的信息熵损失最小化。概念上可以理解为把FP32激活值的直方图压缩到256个bin时来回调整阈值边界让KL散度最小。MinMax校准最简单粗暴直接用绝对值的最大最小值做缩放速度快但容易受离群点影响YOLOv5检测头里偶尔会有一个极大值的logits用MinMax会压缩掉正常的分布精度掉得厉害。Legacy是早期版本遗留的不建议碰。# 校准器核心逻辑继承tensorrt.IInt8EntropyCalibrator2 import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np import os class YOLOv5EntropyCalibrator(trt.IInt8EntropyCalibrator2): def __init__(self, calib_imgs_dir, batch_size8, input_size(640, 640)): super().__init__() self.batch_size batch_size self.input_size input_size self.calib_files [os.path.join(calib_imgs_dir, f) for f in os.listdir(calib_imgs_dir) \ if f.endswith(.jpg) or f.endswith(.png)] self.current_idx 0 self.batches self._load_batches() self.device_input cuda.mem_alloc(self.batch_size * 3 * input_size[0] * input_size[1] * 4) def _load_batches(self): import cv2 batches [] for i in range(0, len(self.calib_files), self.batch_size): batch_files self.calib_files[i:i self.batch_size] batch_imgs [] for f in batch_files: img cv2.imread(f) img cv2.resize(img, self.input_size) img img[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) img / 255.0 batch_imgs.append(img) batches.append(np.stack(batch_imgs, axis0).ravel()) return batches def get_batch_size(self): return self.batch_size def get_batch(self, names): if self.current_idx len(self.batches): return None batch self.batches[self.current_idx] cuda.memcpy_htod(self.device_input, np.ascontiguousarray(batch)) self.current_idx 1 return [int(self.device_input)] def read_calibration_cache(self): if os.path.exists(calib.cache): with open(calib.cache, rb) as f: return f.read() return None def write_calibration_cache(self, cache): with open(calib.cache, wb) as f: f.write(cache)这个校准器类里get_batch返回的是GPU显存里的地址不是CPU数据。TensorRT在校准过程中会反复跑前向计算把激活值统计到直方图里所以数据必须提前拷到GPU。read_calibration_cache和write_calibration_cache是让校准结果缓存到磁盘下次构建engine直接读缓存省得重新跑一遍校准图片。get_batch返回None表示校准结束注意current_idx的递增逻辑。3.2 校准集怎么选数量、分布、图像尺寸这是INT8量化里最玄学但也最决定成败的一步。校准集不是越多越好常见经验是500到1000张就足够稳定再多也不会带来明显精度提升反而拉长校准时间。关键是覆盖模型在真实场景下会遇到的光照、角度、目标大小分布。如果你只是拿COCO验证集里的图片做校准部署时面对的是工业视觉里的暗光小目标那校准分布和推理分布不一致INT8精度会掉到没法看。常见做法是从训练集里随机抽样或者从线上日志里收集一段时间的真实推理帧。图像尺寸方面校准图片必须和最终推理尺寸一致。比如你打算部署时用640x640那校准也用640x640。这里有个坑TensorRT在INT8校准时会根据optimization profile里的input shape来分配校准数据的shape如果你前面导出的ONNX是动态shape但校准器里固定把图片resize成640x640那get_batch返回的batch张量空间不够或者多余都会导致构建engine报错。我一般会在_load_batches里把resize后的shape和self.input_size写成同一个元组并且确保batch张量在ravel()之前是contiguous的否则cuda.memcpy_htod会拿到一个非连续的ndarray可能静默拷贝错误数据。校准图片建议用JPEG不要用PNG因为YOLOv5在训练时的数据增强里会随机调整亮度饱和度JPEG的压缩噪声其实代表了一部分真实分布。另外校准集不要去重尽量保留场景多样性。如果目标场景中暗光图片很少可以人为把部分校准图片做gamma校正后再放进校准集这样能提高模型对亮度变化的鲁棒性。3.3 从ONNX到INT8 engine的完整构建流程构建engine的流程就是把ONNX解析、设置INT8标志、挂上校准器、设置优化profile这几件事串起来。这里给一个最小可跑的构建脚本注意里面有几个参数是必须调的。import tensorrt as trt import os TRT_LOGGER trt.Logger(trt.Logger.WARNING) def build_int8_engine(onnx_path, calib, engine_path, batch_size1, input_h640, input_w640): 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: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) return None 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 calib # 动态shape优化profile profile builder.create_optimization_profile() input_tensor network.get_input(0) profile.set_shape(input_tensor.name, (1, 3, input_h, input_w), (batch_size, 3, input_h, input_w), (batch_size, 3, input_h, input_w)) config.add_optimization_profile(profile) # 序列化engine serialized_engine builder.build_serialized_network(network, config) if serialized_engine is None: print(build engine failed) return None with open(engine_path, wb) as f: f.write(serialized_engine) return serialized_engineset_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30)这个1GB是给TensorRT计算时暂存激活值用的太小会导致INT8校准和tactic选择失败。batch_size在INT8量化时建议设为1或2因为校准器每次喂给网络的batch大小就是你在校准器类里定义的self.batch_size两者必须一致。build_serialized_network返回的是字节流直接写文件就行这个文件就是engine以后推理直接加载它不用再解析ONNX。4. 用TensorRT Python API构建INT8 engine并跑推理4.1 构建engine的完整脚本与参数说明上面3.3里给的是构建函数实际调用时还需要处理一个细节TensorRT 8.x和10.x的Python API在int8_calibrator的赋值方式上有变化。旧版本是builder.int8_calibrator calib新版本是config.int8_calibrator calib。如果你用的是TensorRT 10.x直接给builder赋值会报AttributeError。所以这里建议统一用config.int8_calibrator calib这是两个版本都兼容的写法。另一个容易踩的坑是create_network时是否加EXPLICIT_BATCH标志。YOLOv5导出的ONNX如果不加--dynamic用的是隐式batch模式此时不需要这个标志但加了--dynamic后解析ONNX必须用EXPLICIT_BATCH否则TensorRT把batch维当成隐式维profile.set_shape就没法设置batch维了。我建议你无论是否动态导出都加上这个标志然后统一用set_shape处理这样代码通用。# 实际构建调用 calibrator YOLOv5EntropyCalibrator(calib_images, batch_size4, input_size(640, 640)) engine_bytes build_int8_engine(yolov5s.onnx, calibrator, yolov5s_int8.engine, batch_size4, input_h640, input_w640)这里input_size(640, 640)和input_h, input_w都是640如果你部署时想用1280的分辨率校准和profile都要改成1280。顺便说一句INT8量化一个很常见的加速技巧是同时把输入分辨率调大来提高小目标召回但分辨率越大校准集需要越大的覆盖度否则量化误差会被放大。4.2 推理时要注意输入输出的排序NHWC与NCHW和preprocess对齐TensorRT的engine拿到手后推理时最烦的就是输入输出的内存布局。TensorRT内部默认使用NCHW或NDHWC但对YOLOv5这种CNN模型输入必须是NCHW即[batch, channel, height, width]。很多人在PyTorch里操作习惯了[batch, height, width, channel]到了TensorRT这里直接reshape就会得到一团乱码框。# 推理引擎的加载与执行 class TRTInference: def __init__(self, engine_path): import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit self.logger trt.Logger(trt.Logger.WARNING) with open(engine_path, rb) as f: self.runtime trt.Runtime(self.logger) self.engine self.runtime.deserialize_cuda_engine(f.read()) self.context self.engine.create_execution_context() self.stream cuda.Stream() self._alloc_buffers() def _alloc_buffers(self): import numpy as np # 获取binding信息 self.input_idx self.engine.get_binding_index(images) self.output_idxs [self.engine.get_binding_index(foutput{i}) for i in range(3)] self.input_shape (1, 3, 640, 640) self.input_size trt.volume(self.input_shape) * np.float32(0).itemsize # 分配GPU内存 self.d_input cuda.mem_alloc(self.input_size) self.d_outputs [] self.output_shapes [] for idx in self.output_idxs: shape self.engine.get_binding_shape(idx) self.output_shapes.append(shape) size trt.volume(shape) * np.float32(0).itemsize self.d_outputs.append(cuda.mem_alloc(size)) # 分配CPU端输出缓冲区 self.h_outputs [np.empty(shape, dtypenp.float32) for shape in self.output_shapes] def infer(self, img_nchw): import pycuda.driver as cuda cuda.memcpy_htod_async(self.d_input, img_nchw.ravel(), self.stream) self.context.execute_async_v2(bindings[int(self.d_input)] [int(o) for o in self.d_outputs], stream_handleself.stream.handle) for i, d_out in enumerate(self.d_outputs): cuda.memcpy_dtoh_async(self.h_outputs[i], d_out, self.stream) self.stream.synchronize() return [out.copy() for out in self.h_outputs]execute_async_v2的bindings列表必须和engine中的binding顺序一致。顺序不是按名字排的而是按ONNX解析时的输入输出顺序。上面代码里用get_binding_index(images)先获取索引再排就避免了手写顺序出错。注意输入图像做memcpy_htod_async之前必须先np.ascontiguousarray(img_nchw)因为execute_async_v2要求数据在显存里是连续的一块否则会崩。4.3 把engine跑起来绑定buffer、执行execute_async上面的TRTInference类已经能跑一次推理了但实战中还要处理YOLOv5的三个检测头输出。YOLOv5的ONNX输出通常是三个feature map每个包含目标框坐标、置信度和类别概率。这三个输出在TensorRT里是三个独立的binding需要分别拷贝出来再拼接。常见做法是在GPU上先拼接再拷贝或者在CPU上把三个np.ndarray先np.concatenate再进入NMS。我一般倾向于后者因为三个输出的数据量不大640x640输入下总共25200个anchorCPU拼接耗时也就零点几毫秒换来的是代码调试方便。# 推理主循环 def detect(trt_engine, img_bgr): import cv2, numpy as np img cv2.resize(img_bgr, (640, 640)) img img[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) img / 255.0 img np.ascontiguousarray(img[None, :]) outs trt_engine.infer(img) # 拼接三个输出: [batch, 80, anchor_num] - [batch, anchor_num, 80] preds np.concatenate([np.transpose(o, (0, 2, 1)) for o in outs], axis1) boxes preds[0, :, :4] scores preds[0, :, 4:5] * preds[0, :, 5:].max(axis1, keepdimsTrue) # 阈值过滤 keep scores.ravel() 0.5 return boxes[keep], scores[keep]这里拼接后的preds和YOLOv5原始输出的[batch, 25200, 85]结构一致。注意三个检测头对应不同尺度的anchor但它们输出的box坐标都是相对640x640的像素坐标直接过滤阈值就行。实际项目中推荐把NMS换成TensorRT的NMSPlugin或自己写CUDA核但在Python原型验证阶段用torchvision.ops.nms处理几百个框也够用。性能瓶颈不在NMS而在前向网络本身。5. 避坑INT8量化YOLOv5最容易翻车的五个地方5.1 现象量化后输出全为0或检测框漂移得离谱这是INT8量化最恶性的故障常见于校准器赋值错误或校准数据没进GPU。原因往往不是量化本身而是get_batch里返回的显存地址指向已经释放的临时缓冲区。我在写YOLOv5EntropyCalibrator时曾经把batch先ravel()再memcpy_htod但batch是多个np.stack的视图ravel()后仍然不是连续内存导致cuda.memcpy_htod拷了错误数据。解决方法是先np.ascontiguousarray(batch)再ravel()并且在校准器类里self.batches预先加载好所有校准图片的内存副本不要在get_batch里临时读文件否则IO延迟会让构建engine卡住。5.2 现象校准数据图像尺寸和原模型不一致构建时静默失败如果你导出ONNX时用了--dynamic --imgsz 640 640但校准器里把图片resize成416x416TensorRT会报Binding index 0 size mismatch或者直接让engine构建失败。原因是动态shape的profile必须覆盖校准器实际喂进去的shape。建议校准器、profile的min/max、推理时代码里的input_shape三者始终保持同一个分辨率。如果确实想在部署时切换分辨率那就为每种分辨率单独构建一个INT8 engine不要在同一个engine里做多profile的INT8因为不同profile的量化scale会打架。5.3 现象TensorRT 10.x在GTX 1070上构建engine直接报错GTX 1070Pascal以及更老的Maxwell系列跑TensorRT 10.x经常碰到Cannot deserialize engine或Unsupported op。这不一定是你的代码问题而是TensorRT 10.x默认针对Ampere及以上架构做算子优化旧卡上某些tactic不被支持。解决路径很直接装TensorRT 8.4或8.5并且在安装时确认配套的CUDA 11.x和cuDNN 8.x。如果你手里有1070同时又必须用新Pytorch那就让PyTorch只负责导出ONNX推理完全交给TensorRT 8.5两者不冲突。另外1070的FP16性能很弱用INT8反而更快这个选择是合理的但不要指望它能达到RTX 3060的INT8吞吐。5.4 现象INT8后小目标检测率下降明显原来的召回全没了这是熵校准的典型副作用。小目标对应到特征图上就是很小的激活值当分布直方图中大值和小值差异巨大时熵校准会优先保全大值的精度小值被压到同一格子里小目标框就丢了。常见解法有三个第一校准集里多放小目标图片把分布拉平第二改用IInt8MinMaxCalibrator不按熵来直接按绝对值范围缩放小目标的值能保留多一些代价是整体精度略有波动第三把检测头的输出单独固定为FP16只量化主干网络不过这需要自定义TensorRT网络节点复杂度较高。我一般先试第一种和第二种成本最低。5.5 现象构建engine时显存不足或卡死重启后又能跑了这个坑几乎人人会遇到。原因是config.set_memory_pool_limit(WORKSPACE, 1 30)设得太大而你的GPU显存只有6G的话构建时为了尝试所有tactic会把workspace撑爆。解决方法是把1GB改成256MB起步如果构建报CUBLAS_STATUS_ALLOC_FAILED逐渐下调。另一个原因是校准器get_batch里没有做显存回收每次调用都cuda.mem_alloc一次校准几百次后显存碎片化。正确做法是在__init__里只分配一次self.device_input每次get_batch复用同一块内存并在__del__里调用self.device_input.free(). 卡死还有一个玄学原因校准过程中stream.synchronize没做TensorRT的校准前向计算是异步的如果你在一个stream上校准下一次迭代的memcpy_htod可能覆盖上一次还没算完的输入。给校准器单独建一个cuda.Stream()并且在get_batch返回前调用stream.synchronize()能避免这种间歇性死锁。6. 把INT8 engine塞进自己的推理服务一些能省时间的小技巧6.1 序列化engine避免每次重复构建构建INT8 engine要跑校准集耗时可能从几分钟到几十分钟绝不能每次启动服务都重建。build_serialized_network输出的字节流直接落盘以后用trt.Runtime.deserialize_cuda_engine加载。这里有个细节engine文件和硬件、TensorRT版本强绑定换GPU型号或升级TensorRT后必须重新构建不要拿2080的engine放到3090上跑会报Engine is incompatible。加载engine时可以做一次校验把engine里的GPU device name打出来跟nvidia-smi结果比对不一致就自动触发重建逻辑。6.2 用binding index而不是名称硬编码TensorRT的binding名称在ONNX转engine时可能被自动改写尤其当你用了--simplify后输入输出节点名不一定还是原来的images。我在4.2里用get_binding_index(images)如果找不到会返回-1直接用就会崩。保险做法是遍历engine.num_bindings把所有binding的name打印出来你核对后再写死索引。更稳的做法是创建一个name - idx的映射字典放在配置类里这样换ONNX重导时只需改配置文件不用改推理代码。6.3 与ONNX Runtime / FP16 / INT8 的实际取舍ONNX Runtime用的NPU或CPUExecutionProvider在标准GPU上跑YOLOv5s速度大概是TensorRT FP16的三分之一到二分之一。TensorRT FP16的精度损失在1%以内对检测任务几乎无感知。INT8在Turing以上架构能比FP16再快1.5倍左右但精度波动可能让mAP掉2到3个点。我的习惯是先在FP16上跑通全流程确认功能和精度没问题再切INT8做速度优化。如果INT8掉点太多就保留FP16毕竟INT8省下的延迟在多数业务场景里不足以牺牲检测召回。在校准集质量足够的前提下INT8是值得做的但别把它当银弹。等你在自己的业务数据上把INT8跑通回过来看整个流程最值钱的不是那一套代码而是校准集和踩坑记录。我自己线上踩过最深的一个坑是校准图片没有做exif旋转纠正导致所有校准图都是横着的构建出来的engine对竖版图片检得特别差。从那以后我在校准脚本里加了一步读图后先除以255再减均值除方差用cv2.imdecode的IMREAD_COLOR处理EXIF。这些细节没人替你写遇到了只能自己记。希望这篇笔记能帮你少走前面那几段弯路也希望你的INT8 engine一次构建成功别再为校准缓存反复折腾。本文还有配套的精品资源点击获取