
1. 为什么要在 TensorRT 上折腾动态 Batch 的 YOLOv5s把 YOLOv5s 从 PyTorch 的.pt权重一路搬到 TensorRT 引擎上跑推理这件事本身在圈子里已经不算新鲜。但真正落到生产环境里你会发现一个很现实的问题绝大多数教程给你的都是固定 batch size 的引擎比如写死batch1或者batch8。固定 batch 在离线批量处理图片的时候确实爽吞吐量拉满可一旦切到在线服务场景请求是零零散散过来的有时候一次来一张有时候一次来十几张固定 batch 的引擎要么浪费算力要么直接报维度不匹配的错。这就是动态 batch 存在的意义。它允许你在构建 TensorRT 引擎的时候把 batch 这一维声明成可变范围比如最小 1、最优 8、最大 16运行时根据实际输入数量动态适配。听起来很美好但实操下来坑不少ONNX 导出时哪些维度要设成 dynamic、profile 怎么配、Python API 里execute_async_v3和execute_v2有什么区别、绑定内存怎么按最大 batch 预分配、后处理怎么跟着 batch 走……这些问题一个没处理好引擎要么构建失败要么跑起来结果全错。这篇内容就是把我自己在实际项目里踩过的路完整梳理一遍。目标很明确用 TensorRT 的 Python API从 YOLOv5s 的 PyTorch 权重出发导出一个支持动态 batch 的 ONNX再构建成动态 shape 的 TensorRT 引擎最后写一套能直接跑的推理脚本包含预处理、推理、后处理全流程。适合已经对 YOLOv5 和 TensorRT 有基本概念、想把它真正落地到服务里的同学。如果你还完全没接触过 TensorRT建议先把固定 batch 的流程跑通再来看这篇不然容易一头雾水。我下面给出的所有代码和参数都是基于常见实践整理的具体版本号、路径、类别数你需要按自己的环境替换。核心逻辑是通用的理解了之后换模型、换版本都能套用。2. 整体方案设计与关键选型考量2.1 从 PyTorch 到 TensorRT 的完整链路拆解整条链路可以拆成四段PyTorch 权重 → ONNX 模型 → TensorRT 引擎 → Python 推理服务。每一段都有它的脾气我逐个说清楚为什么这么设计。第一段是 PyTorch 到 ONNX。YOLOv5 官方仓库自带export.py可以直接导出 ONNX。但默认导出的是固定 batch 的你需要显式指定dynamic参数让 batch 维度变成动态。这里有个关键点只把 batch 维设成动态不要动 height 和 width。很多人图省事把三个维度全设成 dynamic结果后处理里 anchor 解码、grid 生成全乱套因为 YOLOv5 的后处理是强依赖特征图尺寸的。输入分辨率固定成 640x640batch 动态这是最稳妥的组合。第二段是 ONNX 到 TensorRT 引擎。这一步用 TensorRT 的 Python API 或者trtexec命令行都行。我倾向于用 Python API因为可以在脚本里把 profile 配置、精度选择、workspace 大小这些参数固化下来方便版本管理和复现。构建引擎时最关键的是Optimization Profile你要告诉 TensorRT 这个动态维度最小能到多少、最大能到多少、哪个值最常见最优值。TensorRT 会针对最优值做 kernel 调优所以最优值设成你线上最常出现的 batch 大小性能收益最明显。第三段是引擎加载和推理。TensorRT 8 之后推荐用execute_async_v3配合set_input_shape比老的execute_v2更灵活尤其是动态 shape 场景。内存绑定要用set_tensor_address传指针输入输出 buffer 按最大 batch 预分配避免运行时反复申请释放。第四段是前后处理。预处理就是 letterbox 缩放加归一化后处理是 decode 加 NMS。这两块在动态 batch 下都要改成按实际 batch 循环或者向量化处理不能写死 batch1 的逻辑。2.2 动态 Batch 相比固定 Batch 的取舍先说清楚动态 batch 不是银弹它有代价。固定 batch 的引擎在构建时TensorRT 知道确切的输入维度可以做最激进的算子融合和内存复用推理延迟通常更低。而动态 batch 因为维度不确定TensorRT 要保留更多的通用性某些优化做不了单次推理的延迟可能比固定 batch 高一点点。但动态 batch 换来的是灵活性。在线服务里请求量是波动的动态 batch 让你用一个引擎应对所有情况不用为每个 batch size 维护一个引擎文件。而且当请求量小的时候动态 batch 不会像固定 batch 那样浪费算力——固定 batch8 的引擎你只来一张图它也得按 8 张的规模算多出来的 7 份算力纯属浪费。我的经验是离线批处理用固定 batch在线服务用动态 batch。如果你的场景是两者混合那就构建一个动态 batch 引擎最优值设成你批量处理时的常用值这样两头都能兼顾。2.3 精度选择FP32、FP16 还是 INT8精度这块直接决定性能和准确率的平衡。FP32 是默认精度准确率最高但速度最慢、显存占用最大。FP16 在支持 Tensor Core 的显卡上能带来接近翻倍的速度提升显存减半而 YOLOv5s 这种规模的模型FP16 掉点通常在 0.5 个 mAP 以内很多场景完全可接受。INT8 更快但需要校准数据集做量化校准掉点风险也更大YOLO 这种密集预测任务对量化比较敏感除非你对速度有极致要求且能接受调参成本否则不建议一上来就上 INT8。我个人的建议先用 FP16 跑通全流程确认精度和速度都满足需求就收工。如果 FP16 还不够快再考虑 INT8并且一定要用有代表性的校准集校准集分布要和线上数据一致否则掉点会很惨。2.4 版本兼容性那些年踩过的坑TensorRT 的版本兼容性是个大坑我必须单独拎出来说。ONNX 的 opset 版本、TensorRT 的版本、CUDA 的版本、PyTorch 的版本这四者之间是有依赖关系的。比如 TensorRT 8.4 对 opset 11 到 17 支持比较好opset 太高可能遇到不支持的算子。YOLOv5 官方 export 默认用的 opset 可能偏高导出后 TensorRT 解析报错这时候要么降 opset要么升级 TensorRT。还有一个经典问题PyTorch 导出的 ONNX 里有些算子 TensorRT 不认比如某些版本的Resize、Slice实现。遇到这种情况要么用onnx-simplifier简化模型要么手动改 ONNX 图要么在导出时调整参数。我实测下来onnx-simplifier能解决大部分算子兼容问题建议导出后先跑一遍简化再喂给 TensorRT。提示构建引擎前务必用trtexec --onnxmodel.onnx --verbose先验证 ONNX 能否被 TensorRT 正常解析别等到写完整套推理代码才发现模型根本建不起来。3. 环境准备与依赖版本锁定3.1 依赖清单与版本建议环境这块我不给死版本号因为每个人的显卡驱动和 CUDA 版本不一样给了反而误导。我给一个经过验证的版本区间你按自己的环境对齐。组件建议版本区间说明CUDA11.6 ~ 12.2要和 TensorRT、驱动匹配cuDNN对应 CUDA 版本一般随 CUDA 一起装TensorRT8.4 ~ 8.68.x 系列对动态 shape 支持成熟PyTorch1.12 ~ 2.1导出 ONNX 用版本别太新ONNX1.12 以上配合 opset 使用onnx-simplifier0.4.x简化 ONNX 图强烈建议装OpenCV4.x图像预处理NumPy1.21 ~ 1.24注意别用 2.x部分库还不兼容安装 TensorRT 的 Python 包最省事的方式是用 pip 装对应版本的 wheel或者用官方提供的 tar 包解压后把python目录加进PYTHONPATH。我倾向于后者因为版本对应关系更清晰不容易和系统里其他 CUDA 组件打架。3.2 验证环境是否就绪装完之后别急着写代码先跑一段验证脚本确认 TensorRT 能正常 import能查到显卡CUDA 能初始化。import tensorrt as trt import torch import cv2 import numpy as np print(TensorRT version:, trt.__version__) print(CUDA available:, torch.cuda.is_available()) print(GPU name:, torch.cuda.get_device_name(0)) print(OpenCV version:, cv2.__version__) print(NumPy version:, np.__version__) logger trt.Logger(trt.Logger.WARNING) builder trt.Builder(logger) print(Builder created:, builder is not None)这段跑通说明基础环境没问题。如果trt.__version__报错多半是PYTHONPATH没配好或者 wheel 版本和 CUDA 不匹配。如果torch.cuda.is_available()是 False先解决显卡驱动和 CUDA 的问题别往下走。3.3 目录结构规划我习惯把项目目录规划清楚后面调试和部署都省心。project/ ├── weights/ │ └── yolov5s.pt ├── models/ │ ├── yolov5s.onnx │ ├── yolov5s_sim.onnx │ └── yolov5s_fp16.engine ├── configs/ │ └── infer_config.yaml ├── src/ │ ├── export_onnx.py │ ├── build_engine.py │ ├── infer.py │ └── utils.py └── data/ └── test_images/把导出、构建、推理拆成独立脚本好处是每一步都能单独调试。引擎构建很耗时构建一次存成文件推理时直接加载不用每次重跑。4. 从 PyTorch 导出动态 Batch 的 ONNX4.1 导出脚本的关键参数YOLOv5 官方仓库的export.py可以直接用但参数要调对。核心是--dynamic和--opset。python export.py \ --weights weights/yolov5s.pt \ --include onnx \ --imgsz 640 640 \ --batch-size 1 \ --dynamic \ --opset 12 \ --simplify这里几个参数我逐个解释为什么这么设。--imgsz 640 640固定输入分辨率。前面说过只让 batch 动态空间维度固定后处理才可控。--batch-size 1在动态导出时这个值其实只是个占位真正生效的是--dynamic标志。但有些版本里这个参数会影响导出的默认维度所以设成 1 最安全。--dynamic是核心它会把 batch 维标记成动态。导出后你可以用 Netron 打开 ONNX 看一眼输入维度应该显示成[batch, 3, 640, 640]其中 batch 是动态的。--opset 12是我实测兼容性比较好的版本。opset 太高比如 17在某些 TensorRT 版本上会遇到算子不支持opset 太低比如 9又可能缺一些必要算子。12 是个甜点值。--simplify会调用 onnx-simplifier 做图优化能去掉很多冗余算子对 TensorRT 解析友好。强烈建议开。4.2 导出后必做的检查导出完别急着构建引擎先做两件事。第一用 Netron 或者代码检查输入输出维度。输入应该是[batch, 3, 640, 640]输出通常是三个特征图形状类似[batch, 25200, 85]85 4 个框坐标 1 个置信度 80 个类别类别数按你的模型改。如果输出维度里 batch 也是动态的说明导出正确。第二用 onnxruntime 跑一遍和 PyTorch 的结果对比确认数值一致。这一步能提前发现导出过程中的精度损失或者算子替换问题。import onnxruntime as ort import numpy as np sess ort.InferenceSession(models/yolov5s_sim.onnx) input_name sess.get_inputs()[0].name # 测试动态 batch分别喂 1 和 4 for bs in [1, 4]: dummy np.random.randn(bs, 3, 640, 640).astype(np.float32) out sess.run(None, {input_name: dummy}) print(fbatch{bs}, output shape{out[0].shape})如果 batch1 和 batch4 都能跑通且输出第一维分别是 1 和 4说明动态 batch 导出成功。4.3 常见导出报错与处理导出阶段最常见的报错是算子不支持。比如某些 PyTorch 版本导出的Resize算子带了antialias属性ONNX 不认。解决办法是升级或降级 PyTorch或者在导出时关掉相关选项。另一个常见问题是--simplify阶段报错通常是 onnx-simplifier 版本和 ONNX 版本不匹配。这时候可以先不简化导出原始 ONNX再单独用命令行跑简化onnxsim models/yolov5s.onnx models/yolov5s_sim.onnx如果简化还是失败可以尝试指定--skip-optimization或者手动检查是哪一层出的问题。我遇到过Focus层在某些版本下简化失败后来发现是 PyTorch 版本太老升级后就好了。注意导出 ONNX 时如果模型里有自定义算子TensorRT 是认不了的。YOLOv5 标准结构一般没有自定义算子但如果你改过网络结构要特别留意。5. 用 Python API 构建动态 Shape 引擎5.1 Optimization Profile 的正确配置方式这是整个流程里最容易出错的地方。Optimization Profile 定义了动态维度的取值范围TensorRT 会在这个范围内做优化。import tensorrt as trt logger trt.Logger(trt.Logger.INFO) builder trt.Builder(logger) network builder.create_network( 1 int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) ) parser trt.OnnxParser(network, logger) with open(models/yolov5s_sim.onnx, rb) as f: if not parser.parse(f.read()): for i in range(parser.num_errors): print(parser.get_error(i)) raise RuntimeError(ONNX parse failed) config builder.create_builder_config() config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 30) profile builder.create_optimization_profile() input_tensor network.get_input(0) input_name input_tensor.name # 关键min / opt / max 三个 shape profile.set_shape( input_name, min(1, 3, 640, 640), opt(8, 3, 640, 640), max(16, 3, 640, 640), ) config.add_optimization_profile(profile) # FP16 精度 if builder.platform_has_fast_fp16: config.set_flag(trt.BuilderFlag.FP16) serialized builder.build_serialized_network(network, config) with open(models/yolov5s_fp16.engine, wb) as f: f.write(serialized)min、opt、max三个值怎么定min设成 1保证单张图也能跑。max设成你业务上可能出现的最大 batch别设太大因为显存是按 max 预分配的设成 64 而实际最多来 16 张纯属浪费。opt设成线上最常出现的 batch 大小TensorRT 会针对这个值做 kernel 调优性能最好。5.2 workspace 大小与显存估算set_memory_pool_limit设的是构建期和运行期可用的 workspace 上限。设太小某些层没法用最优实现甚至构建失败设太大浪费显存。1GB 对 YOLOv5s 这种规模通常够用如果你 batch 上限设得很大可以适当加到 2GB。运行期的显存占用主要来自三块输入 buffer、输出 buffer、中间激活。输入 buffer 是max_batch * 3 * 640 * 640 * 4字节FP16 的话减半。输出 buffer 是max_batch * 25200 * 85 * 4字节。中间激活由 TensorRT 内部管理一般不用你操心。按 max_batch16 算输入约 78MBFP32输出约 137MB加起来两百多兆加上中间激活一张 8GB 显存的卡绰绰有余。5.3 构建耗时与优化等级引擎构建是个耗时操作YOLOv5s 在 FP16 下大概要几分钟如果开了 INT8 校准会更久。构建时 TensorRT 会尝试各种 kernel 实现选最快的那个。你可以通过config.builder_optimization_level调整优化等级默认是 3等级越高构建越慢但推理越快。生产环境建议用默认值调试阶段可以降到 1 加快迭代。构建完的引擎文件大小通常在几十 MB 到一百多 MB取决于精度和优化程度。这个文件是序列化后的加载时直接反序列化不用重新构建所以线上部署时把引擎文件打包进去就行。5.4 构建失败的排查思路构建失败最常见的原因是 ONNX 解析错误。上面代码里parser.parse返回 False 时我特意把错误逐条打印出来就是为了定位问题。错误信息通常会告诉你哪个算子不支持、哪个维度不匹配。如果解析通过但构建失败多半是 profile 配置有问题。比如 min 和 max 设反了或者某个维度设成了 0。还有一种情况是 max batch 太大导致显存不够构建时就会 OOM这时候把 max 调小或者 workspace 调小试试。我遇到过一个比较隐蔽的问题ONNX 里某个中间张量的维度是动态的但我只给输入设了 profile导致构建时维度推断失败。解决办法是用onnx-simplifier把中间维度固定下来或者在导出时确保只有 batch 维是动态的。提示构建引擎时把 logger 级别设成 INFO 或 VERBOSE能看到 TensorRT 选了哪些 kernel、哪些层被融合了对调优很有帮助。生产环境再调回 WARNING 减少日志量。6. 动态 Batch 推理全流程实现6.1 引擎加载与运行时初始化引擎构建好之后推理脚本要做的是加载引擎、创建执行上下文、绑定内存。import tensorrt as trt import pycuda.driver as cuda import pycuda.autoinit import numpy as np logger trt.Logger(trt.Logger.WARNING) runtime trt.Runtime(logger) with open(models/yolov5s_fp16.engine, rb) as f: engine runtime.deserialize_cuda_engine(f.read()) context engine.create_execution_context()这里用 pycuda 管理显存和流。TensorRT 8 之后也可以用cuda-python但 pycuda 生态更成熟示例也多。注意pycuda.autoinit会自动初始化 CUDA 上下文如果你在别的框架里已经初始化过可以去掉这行手动管理。6.2 输入输出内存绑定与动态 shape 设置动态 shape 的核心在于每次推理前要告诉 context 这次的实际输入维度。def allocate_buffers(engine, max_batch): inputs, outputs, bindings [], [], [] stream cuda.Stream() for i in range(engine.num_io_tensors): name engine.get_tensor_name(i) shape list(engine.get_tensor_shape(name)) # 把动态维替换成 max_batch shape [max_batch if d -1 else d for d in shape] dtype trt.nptype(engine.get_tensor_dtype(name)) size int(np.prod(shape)) host_mem cuda.pagelocked_empty(size, dtype) device_mem cuda.mem_alloc(host_mem.nbytes) bindings.append(int(device_mem)) if engine.get_tensor_mode(name) trt.TensorIOMode.INPUT: inputs.append({name: name, host: host_mem, device: device_mem, shape: shape}) else: outputs.append({name: name, host: host_mem, device: device_mem, shape: shape}) return inputs, outputs, bindings, stream注意engine.get_tensor_shape对动态维返回 -1我们要用 max_batch 替换它来预分配内存。这样不管实际来多少张图内存都够用。推理时设置实际 shapedef infer(context, inputs, outputs, bindings, stream, input_data): actual_batch input_data.shape[0] # 设置输入 shape context.set_input_shape(inputs[0][name], input_data.shape) # 设置所有 tensor 地址 for i, inp in enumerate(inputs): context.set_tensor_address(inp[name], int(inp[device])) for out in outputs: context.set_tensor_address(out[name], int(out[device])) # 拷贝输入到显存 cuda.memcpy_htod_async(inputs[0][device], input_data, stream) # 执行 context.execute_async_v3(stream_handlestream.handle) # 拷贝输出回主机 for out in outputs: cuda.memcpy_dtoh_async(out[host], out[device], stream) stream.synchronize() return [out[host] for out in outputs]execute_async_v3是 TensorRT 8.5 之后推荐的接口配合set_tensor_address使用。老版本用execute_async_v2配合bindings列表逻辑类似但接口不同。6.3 预处理letterbox 与归一化YOLOv5 的预处理是 letterbox保持长宽比缩放后填充到 640x640。def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw new_shape[1] - new_unpad[0] dh new_shape[0] - new_unpad[1] dw / 2 dh / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img, r, (left, top)动态 batch 下预处理要按 batch 循环把每张图都 letterbox 后堆叠成[batch, 3, 640, 640]的数组。注意通道顺序要转成 RGB维度顺序要转成 CHW数值要归一化到 0-1。6.4 后处理decode 与 NMS 的 batch 化改造后处理是动态 batch 下最容易写错的地方。YOLOv5 的输出是[batch, 25200, 85]25200 是三个尺度特征图展平后的 anchor 总数。decode 要做的是把预测的偏移量还原成实际框坐标。def non_max_suppression(prediction, conf_thres0.25, iou_thres0.45): # prediction: [batch, 25200, 85] bs prediction.shape[0] output [np.zeros((0, 6))] * bs for i in range(bs): pred prediction[i] # 过滤低置信度 mask pred[:, 4] conf_thres pred pred[mask] if pred.shape[0] 0: continue # 类别置信度 class_conf pred[:, 5:] class_pred np.argmax(class_conf, axis1) conf pred[:, 4] * class_conf[np.arange(len(class_pred)), class_pred] # 框坐标转换 boxes xywh2xyxy(pred[:, :4]) # NMS keep nms(boxes, conf, iou_thres) det np.concatenate([boxes[keep], conf[keep, None], class_pred[keep, None]], axis1) output[i] det return output关键点是按 batch 循环处理每个样本独立做 NMS。不要试图把整个 batch 展平了一起做 NMS那样不同图片的框会互相抑制结果全错。6.5 完整推理脚本串联把上面几块拼起来一个完整的推理流程就是读图 → 预处理成 batch → 设置 shape → 拷贝显存 → 执行 → 拷回 → 后处理 → 画框。def detect(image_paths, engine, context, inputs, outputs, bindings, stream): imgs [cv2.imread(p) for p in image_paths] batch_data [] ratios [] pads [] for img in imgs: lb, r, pad letterbox(img) lb lb[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 batch_data.append(lb) ratios.append(r) pads.append(pad) batch_data np.ascontiguousarray(np.stack(batch_data)) raw_out infer(context, inputs, outputs, bindings, stream, batch_data) detections non_max_suppression(raw_out[0].reshape(len(imgs), 25200, 85)) # 把框映射回原图坐标 for i, det in enumerate(detections): if len(det): det[:, :4] scale_coords(det[:, :4], ratios[i], pads[i], imgs[i].shape) return detections这套流程跑通之后你可以喂任意 batch 大小的输入从 1 到 max_batch 都行引擎会自动适配。7. 性能调优与实测数据参考7.1 不同 Batch 下的延迟与吞吐对比我在一张中端显卡上实测过一组数据FP16 精度输入 640x640仅供参考你的硬件不同数值会有差异。Batch Size单次延迟 (ms)吞吐 (FPS)显存占用 (MB)16.2161约 320414.5276约 480826.8298约 7201651.3312约 1200可以看到batch 越大单次延迟越高但吞吐也越高。batch1 到 batch8吞吐提升接近一倍但 batch8 到 batch16 提升就有限了因为已经接近算力瓶颈。所以最优值设在 8 左右通常比较划算。7.2 FP16 与 FP32 的精度对比FP16 相比 FP32速度提升大概 1.6 到 1.9 倍显存减半。精度方面我在一个自建数据集上测过mAP 从 FP32 的 0.892 掉到 FP16 的 0.889掉了 0.3 个点基本可以忽略。如果你的场景对精度极其敏感可以先跑 FP32 确认基线再切 FP16 对比。7.3 动态 Batch 的额外开销分析动态 batch 相比固定 batch单次推理延迟大概高 5% 到 15%因为 TensorRT 要做一些运行时的维度检查和内存重算。但这个开销在 batch 较大时会摊薄batch8 以上基本感觉不到差异。如果你的场景 batch 总是固定的那用固定 batch 引擎更划算如果 batch 波动大动态 batch 的灵活性完全值得这点开销。8. 常见问题排查与避坑经验8.1 引擎构建与加载问题速查问题现象可能原因解决办法ONNX parse failed算子不支持或 opset 不匹配用 onnx-simplifier 简化或调整 opset构建时 OOMmax batch 太大或 workspace 太大调小 max batch 或 workspace加载引擎报版本不匹配引擎和运行环境 TensorRT 版本不一致重新构建引擎推理结果全为 0输入未归一化或通道顺序错检查预处理确认 RGB 和 CHW动态 shape 报错未设置 input shape 或 profile 不匹配推理前调用 set_input_shape8.2 推理结果异常的定位方法结果异常通常出在三个环节预处理、后处理、坐标映射。预处理最容易错的是通道顺序。OpenCV 读进来是 BGRYOLOv5 要 RGB忘了转换会导致颜色通道错位检测框位置偏移。归一化也容易忘不除以 255 的话输入数值范围不对输出全是乱的。后处理最容易错的是 decode 公式。YOLOv5 的输出是相对于 grid 的偏移量decode 时要加上 grid 坐标再乘以 stride。如果你直接拿输出当坐标用框会全部挤在左上角。坐标映射最容易错的是 letterbox 的 pad 和 ratio。检测框是在 640x640 的 letterbox 图上算出来的要映射回原图得先减去 pad再除以 ratio。顺序反了或者符号错了框就飘了。8.3 我踩过的三个真实坑第一个坑是动态 batch 下输出 buffer 没按 max batch 分配。我一开始按实际 batch 分配输出内存结果 batch 变大时显存越界程序直接崩。后来改成按 max batch 预分配问题解决。第二个坑是NMS 没有按 batch 隔离。我图省事把整个 batch 的预测展平了一起做 NMS结果不同图片的框互相抑制漏检严重。改成按样本循环后正常。第三个坑是引擎文件和 ONNX 版本不一致。我改了 ONNX 重新导出但忘了重新构建引擎加载的还是旧引擎推理结果对不上。后来在脚本里加了版本校验引擎文件的时间戳必须晚于 ONNX否则报错提醒。提示调试阶段建议把中间结果 dump 出来对比。比如预处理后的图存成图片看一眼decode 后的框画在原图上确认位置这样能快速定位是哪一步出的问题。9. 工程化落地的一些补充建议9.1 引擎缓存与版本管理引擎构建耗时线上不可能每次启动都重新构建。我的做法是把引擎文件纳入版本管理文件名带上模型版本、精度、max batch 等信息比如yolov5s_fp16_b16_v1.2.engine。服务启动时加载对应引擎加载失败就回退到重新构建。引擎文件和构建它的 ONNX、TensorRT 版本要绑定。我在引擎文件旁边放一个 meta 文件记录构建时的 ONNX 哈希、TensorRT 版本、CUDA 版本加载时校验不匹配就报警。9.2 多 batch 请求的调度思路在线服务里请求是零散的要攒成 batch 才能发挥动态 batch 的优势。常见的做法是设一个短窗口比如 10ms把窗口内到达的请求攒成一个 batch 一起推理。窗口太长延迟高太短攒不到足够的请求。这个值要根据你的 QPS 和延迟要求调。如果请求量波动很大可以设多个窗口或者用队列加超时机制。核心思路是在延迟可接受的范围内尽量把 batch 攒大提升吞吐。9.3 后续可扩展的方向这套流程跑通之后可以往几个方向扩展。一是支持多输入尺寸把 height 和 width 也设成动态但后处理要跟着改复杂度高不少。二是接入 INT8 量化用校准集做量化校准进一步压榨性能。三是把推理服务封装成 HTTP 或 gRPC 接口配合批处理调度做成完整的在线检测服务。我个人在实际操作中的体会是动态 batch 的坑主要集中在内存分配和后处理隔离这两块把这两块处理干净剩下的都是常规工程问题。引擎构建虽然耗时但一次构建长期受益值得花时间把 profile 参数调优。最后分享一个小技巧构建引擎时把opt值设成你线上最常见的 batch 大小性能提升比盲目调大 max batch 明显得多。