YOLOv5模型TensorRT加速部署:从PyTorch到推理优化的完整流程
2026/9/15 22:27:34 网站建设 项目流程

把yolov5模型从PyTorch搬到TensorRT上做推理加速,是我接触过的部署优化里收益最直接、坑也最集中的一件事。同样的权重,在PC上用TensorRT跑FP16,通常能比PyTorch浮点前向推理快2到3倍;换到Jetson这类边缘设备上,优势还会更明显。这篇就聊我实际踩完坑之后的完整流程,我把它切成七步:环境准备、导出ONNX、构建引擎、推理管线、后处理、精度与性能验证、最后落到实际部署。

这篇文章不是训练教程,默认你手里已经有一个训练好或者下载来的yolov5权重文件,也大致知道模型是怎么用的。学完这套流程,你能把一个yolov5s甚至更大的模型,从.pt变成.engine,然后跑通端到端的目标检测推理。如果你项目里已经用上了yolov5,但总觉得推理速度不够、显存占用太高,那这篇就是给你准备的。

1. 项目概述与整体思路

1.1 为什么要用TensorRT做yolov5推理加速

yolov5本身是个结构清晰、效果稳定的检测模型,但直接用PyTorch跑推理有一个绕不开的问题:动态图的算子调度开销太大。每个卷积、BN、激活函数在运行时都要经过Python解释器、torch分发、CUDA kernel启动这几层,GPU的算力被切成了很多碎块,真正用在矩阵计算上的时间比例并不高。

TensorRT做的事情本质上是“静态化”。它拿到ONNX模型后,会做层融合、kernel自动调优、精度校准、内存复用这一整套优化。一个比较直观的比喻:PyTorch推理像是每次出门都临时查地图、现找路线,TensorRT则是在你出发前就把最优路线刻进导航里,你只管踩油门。我实测过的RTX 3060上,yolov5s的PyTorch FP32推理大概12ms左右,换TensorRT FP16能压到5ms上下,后处理如果也放到GPU上做,端到端延迟还能再往下走。

这套优化路线不止适用yolov5,分类、分割、OCR这些模型都能用同一套思路加速。只不过yolov5的部署链路相对标准,最适合拿来当TensorRT入门的第一个项目。

1.2 七步方案的完整路线图

先交代整体路线,省得你后面迷路。我总结的七步是这样的:

  • 第一步:环境准备与版本匹配,把CUDA、cuDNN、TensorRT、PyTorch这些底层依赖对齐。
  • 第二步:导出ONNX中间模型,把PyTorch权重转成TensorRT能吃的中间格式。
  • 第三步:构建TensorRT推理引擎,用trtexec或者自写脚本生成序列化的engine文件。
  • 第四步:搭建推理管线,包括图像预处理、数据拷贝、执行推理。
  • 第五步:实现后处理,解析模型输出、过滤候选框、做NMS、坐标还原。
  • 第六步:精度校验与性能调优,确认加速后没掉点、延迟数据可信。
  • 第七步:部署集成,把engine接进摄像头流程、Web服务或者Jetson等边缘设备。

这套顺序是有讲究的。我自己最早犯过的错就是跳步,还没验证ONNX正确性就急着上FP16,结果后端输出一团乱麻,排查了整整一个下午。正确做法永远是先在PC上把FP32流程跑通,再逐步上FP16、INT8;先在单张图片上验结果,再上视频流;先确认精度无损,再谈极致加速。

2. 第一步:环境准备与版本匹配

2.1 版本选型的三个判断标准

TensorRT加速yolov5的第一个大坑不是代码,是版本。CUDA、cuDNN、TensorRT、PyTorch、ONNX这五个东西的版本必须能互相咬合,否则你会在各种底层报错里浪费大量时间。

我的判断标准有三条。第一,TensorRT版本必须对应你的CUDA版本和显卡架构。比如老显卡用CUDA 10.2配TensorRT 7.x更稳妥,新显卡直接上CUDA 11.x配TensorRT 8.x。第二,PyTorch和ONNX导出工具链不要追求最新,稳定优先。第三,如果后续要部署到Jetson,直接用JetPack自带的TensorRT版本,别自己在板子上重编。

我这边一个比较稳的组合是:

组件建议版本说明
CUDA11.8兼容30系、40系显卡
cuDNN8.6与CUDA 11.8配套
TensorRT8.5.2.2对ONNX算子支持较全
PyTorch1.13.1yolov5官方仓库测试过
ONNX1.12导出格式稳定

这套组合不一定是性能最好,但胜在资料多、踩坑记录全。你如果非要全上最新版,我不拦你,但建议做好心理准备,很多报错在中文社区里搜不到。

2.2 安装TensorRT并验证

安装TensorRT我用的是tar包方式,比deb包更好控制路径。从NVIDIA官网下载对应CUDA版本的TensorRT压缩包后,解压到固定目录:

tar -xzvf TensorRT-8.5.2.2.Linux.x86_64-gnu.cuda-11.8.tar.gz sudo mv TensorRT-8.5.2.2 /opt/

然后设置环境变量,建议写进~/.bashrc,避免每次开终端都要重新export:

export LD_LIBRARY_PATH=/opt/TensorRT-8.5.2.2/lib:$LD_LIBRARY_PATH

接着安装Python接口:

pip install /opt/TensorRT-8.5.2.2/python/tensorrt-8.5.2.2-cp38-none-linux_x86_64.whl

装完别急着往下走,先验证一下:

import tensorrt as trt print(trt.__version__)

如果这里报错libnvinfer.so.8: cannot open shared object file,九成是LD_LIBRARY_PATH没配对,回去检查一下路径。这个验证看起来简单,但能帮你把“TensorRT没装好”和“代码写错了”这两个问题彻底分开。

3. 第二步:导出ONNX中间模型

3.1 导出前的模型检查

TensorRT不直接吃PyTorch权重,它吃的是ONNX。导出ONNX这一步,重点检查三件事。

第一,确认你手上的yolov5版本。6.0版本移除了Focus结构,换成了6x6卷积,网络结构和5.0之前完全不同。如果你找的教程还在讲Focus,对应的是老版本,导出出来的ONNX节点结构也不一样,别混着看。

第二,加载权重后一定记得调model.eval()。要不然BN层的running mean和running variance可能还会被当成训练模式处理,导出算图时会有奇怪的额外分支。

第三,想清楚要导出单输出还是三输出。官方yolov5仓库的export.py在6.0之后默认会把三个检测头合并成一个(1, 25200, 85)的输出张量,后处理写起来更简单。如果你改过detect头,或者用了老版本导出,可能是三个独立的特征图输出,后处理逻辑完全不同。导出完用Netron看一眼最靠谱。

3.2 导出方法与常见报错

如果你用的是官方yolov5仓库,命令很简单:

python export.py --weights yolov5s.pt --include onnx --opset 12

如果模型是自己魔改过的,直接用torch.onnx.export:

import torch model.load_state_dict(torch.load("你的权重.pth", map_location="cuda")) model.eval() dummy_input = torch.randn(1, 3, 640, 640).cuda() torch.onnx.export( model, dummy_input, "yolov5_custom.onnx", opset_version=12, input_names=["images"], output_names=["output"], dynamic_axes={"images": {0: "batch"}, "output": {0: "batch"}} )

这里有个细节:opset_version不要选太低,太低会导致TensorRT解析时遇到不支持的算子;也不要盲目选最新,有些新的opset对旧版本TensorRT反而有兼容问题。12到14这个区间最稳。

常见报错是Unsupported operator,比如遇到某些上采样或自定义算子。解决办法一般是升级yolov5版本,或者在导出前把自定义结构改掉。还有朋友遇到导出成功但TensorRT解析失败,这种情况多半是ONNX里有动态shape没固定,先把输入尺寸固定成1x3x640x640再导出试一次。

4. 第三步:构建TensorRT推理引擎

4.1 用trtexec快速生成engine

拿到ONNX之后,最快验证这条路能不能走通的方式就是用trtexec。它是TensorRT自带的命令行工具,不需要写一行代码。

/opt/TensorRT-8.5.2.2/bin/trtexec \ --onnx=yolov5s.onnx \ --saveEngine=yolov5s_fp16.engine \ --fp16 \ --minShapes=images:1x3x640x640 \ --optShapes=images:1x3x640x640 \ --maxShapes=images:4x3x640x640

这个命令我展开解释一下。--fp16是开启半精度推理,这是加速的大头。--minShapes--optShapes--maxShapes是动态shape的边界,分别指最小batch、最常用batch和最大batch。实际推理时,输入尺寸必须落在这个区间内。

需要注意,不同版本的trtexec参数名有差异。8.x早期版本用的还是--workspace=1024,新版改成了--memPoolSize=workspace:1024,如果你执行的时候提示参数无效,先查一下当前版本的帮助文档。我当初在这个地方卡了快半小时,亏得打印了--help

trtexec跑通的意义在于,它把ONNX、TensorRT、显卡驱动这条路整体验证了一遍。如果trtexec都构建失败,那就是模型或环境问题,不用急着写工程代码。

4.2 自写构建脚本的核心逻辑

trtexec适合验证,但工程上我更建议自己写构建脚本。原因有三个:一是可以在构建时打印详细日志,方便排查;二是可以批量生成FP32、FP16、INT8多种engine,方便后续对比;三是可以把构建逻辑封装进部署系统,在目标机器上自动完成。

核心代码不长,大概这样:

import tensorrt as trt logger = trt.Logger(trt.Logger.WARNING) builder = trt.Builder(logger) network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)) parser = trt.OnnxParser(network, logger) with open("yolov5s.onnx", "rb") as f: assert parser.parse(f.read()), "ONNX解析失败" config = builder.create_builder_config() # TensorRT 8.x早期版本用 config.max_workspace_size = 1 << 30 # 新版本推荐下面这种写法,以实际版本API为准 config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) config.set_flag(trt.BuilderFlag.FP16) engine = builder.build_engine(network, config) with open("yolov5s.engine", "wb") as f: f.write(engine.serialize())

EXPLICIT_BATCH这个flag必须加,因为现在的ONNX导出默认就是显式batch模式。set_memory_pool_limit是给构建过程分配的工作空间上限,太小可能导致构建失败,太大可能爆显存,1GB是个比较保守的值。

engine构建完成之后一定要序列化到磁盘。engine文件里包含了优化后的网络结构和权重,以后部署直接加载它,不需要重新构建。这里要提醒一个点:engine构建慢是正常的,yolov5s大概几十秒到一两分钟,你以为卡死了,其实它还在调优kernel。

4.3 动态shape与显存分配注意

用动态shape有几个容易踩的细节。第一,engine里的binding名称必须和ONNX里的输入输出名一致,我习惯先打印一遍确认:

for i in range(engine.num_bindings): print(i, engine.get_binding_name(i), engine.get_binding_shape(i))

第二,动态batch模式下,创建执行上下文时要设定实际的输入shape,不设或设错了都会报错。第三,显存分配要按照实际输入尺寸来算,别拿maxShapes去分,白白浪费显存。

另一个重要认知:engine文件和GPU架构强相关。你在自己电脑上构建的engine,拷到别人的同型号显卡上一般能用,但拷到Jetson或者另一代架构的显卡上大概率直接报错。最稳妥的做法是目标设备上重新构建。

5. 第四、五步:推理管线与后处理

5.1 预处理必须与训练一致

推理管线里最容易出问题、但很多人不重视的就是预处理。TensorRT本身不管你怎么缩放图像,它只会把你给的数据送进网络。如果你的预处理和训练时不一致,出来的检测框就会整体偏移,甚至完全检测不到目标。

yolov5官方的预处理顺序是:读图 -> letterbox缩放 -> BGR转RGB -> 除以255归一化 -> 从HWC转成CHW -> 转成float32。这里letterbox是关键,不能直接resize拉伸,要按比例缩放后再填充到640x640,否则目标的长宽比会变形,小目标很容易丢失。

我常用的letterbox代码:

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]) / 2 dh = (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] != new_unpad: img = cv2.resize(img, new_unpad, interpolation=cv2.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, value=color) return img

注意这里的填充是在两边均匀分配的,和yolov5训练时保持一致。有的老版本教程是右下填充,你要看自己用的yolov5版本对应的是哪种,两个混用会让所有框偏半个padding的距离。

预处理完的数据要一次性转成连续内存块的float32数组,再拷到GPU显存。别在numpy里切来切去然后直接传给TensorRT,底层容易报错或者拿到乱七八糟的数据。

5.2 后处理:解码、过滤、NMS、坐标还原

yolov5模型的输出,单输出版本是(1, 25200, 85)。25200 = 3个尺度 × (80×80 + 40×40 + 20×20)个格子,每个格子有3个anchor;85 = 4个框坐标 + 1个目标置信度 + 80个类别分数。

第一步要把这个二维矩阵拆开。坐标部分是x、y、w、h的归一化值,需要先转换成x1、y1、x2、y2格式。目标置信度和类别分数是分开的,最终的得分要把两者乘起来:

boxes = output[..., :4] obj_conf = output[..., 4:5] cls_conf = output[..., 5:] cls_scores, cls_ids = cls_conf.max(-1) scores = obj_conf.squeeze(-1) * cls_scores # 过滤低置信度候选框 mask = scores > 0.25 scores = scores[mask] boxes = boxes[mask] cls_ids = cls_ids[mask] # 中心点格式转xyxy boxes_xyxy = torch.zeros_like(boxes) boxes_xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 boxes_xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 boxes_xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 boxes_xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 # NMS keep = torchvision.ops.nms(boxes_xyxy, scores, iou_threshold=0.45)

NMS这里一定要用GPU版本,也就是torchvision的nms接口。用纯Python写双层循环做NMS,一秒视频流根本扛不住,直接成为整个链路的新瓶颈。我见过有人模型推理才5ms,NMS花了30ms,还跑来问为什么TensorRT加速没效果,一查全是后处理拖后腿。

最后一步是坐标还原。因为之前做了letterbox,模型输出的是在640×640填充图上的归一化坐标,要映射回原图,需要把padding和缩放比例代进去。这个公式不对,框就会全部偏到一边:

# ratio, dw, dh 是letterbox时保存下来的参数 boxes_xyxy[:, [0, 2]] = (boxes_xyxy[:, [0, 2]] - dw) / ratio boxes_xyxy[:, [1, 3]] = (boxes_xyxy[:, [1, 3]] - dh) / ratio

6. 第六步:精度校验与性能调优

6.1 性能指标怎么测才可信

很多人跑完TensorRT就发个“快了3倍”,但实际测速方法有问题:要么只跑了一次,要么把GPU预热时间也算进去了,数据完全不可信。我自己的测速流程已经固定下来了。

第一步,先把模型跑几十次做warmup,让GPU频率和TensorRT内部缓存都稳定下来。第二步,用CUDA事件或者trtexec的报告来计时。Python里用time.time()计时受系统调度和CPU负载影响太大,我用CUDA事件:

start = torch.cuda.Event(enable_timing=True) end = torch.cuda.Event(enable_timing=True) start.record() # 推理代码 end.record() torch.cuda.synchronize() print(f"延迟: {start.elapsed_time(end):.2f} ms")

第三步,分别统计纯模型推理时间和端到端时间。纯模型推理衡量的是TensorRT本身的优化效果,端到端时间才是用户实际感知到的延迟。我遇到过纯推理很快但端到端很慢的情况,后来发现瓶颈居然在cv2读取图片上。

下面是一个我在RTX 3060上实测过的数据,仅供参考,不同显卡和batch大小差异很大:

推理方式平均延迟(ms)相对加速备注
PyTorch FP3212.51x纯模型推理
TensorRT FP328.11.5x纯模型推理
TensorRT FP165.22.4x纯模型推理
TensorRT FP1612.81x(端到端)含预处理+后处理

看到没有,端到端一加进来,其他环节的比重就上来了。所以优化不能只盯着模型,整条链路都要管。

6.2 FP16与INT8的取舍

FP16是最划算的优化手段,构建engine时加一个--fp16或者set_flag(trt.BuilderFlag.FP16)就行,代码改动几乎为零,yolov5的精度损失通常在0.5%以内,肉眼基本看不出来。

INT8的收益更大,显存占用更低,但需要做校准。TensorRT需要通过校准集统计每一层激活值的分布,才能确定哪些地方可以用8位整数近似。校准集的选择非常关键,我习惯从验证集里随机抽500张图,保证覆盖不同光照、不同目标尺寸、不同场景类型。

校准集的预处理必须和训练时完全一致,很多人INT8掉点严重都是因为这里用错了归一化方式。构建INT8 engine时需要用自定义calibrator,TensorRT的Python接口里要继承IInt8EntropyCalibrator2这一类,实现校准数据的读取逻辑。这个代码量不小,所以我建议顺序是:FP32先跑通 -> FP16确认精度 -> 最后再考虑INT8。如果你的部署环境对显存极其敏感,或者模型帧率就是差那最后一点,再上INT8不迟。

INT8下yolov5的mAP一般能保持FP32的95%以上,但要注意小目标的漏检率会有所上升。如果你的业务场景是无人机视角、远处小物体检测这类,INT8要格外谨慎评估。

7. 第七步:部署集成与服务化

7.1 常见集成场景

engine跑通之后,最后一步是把它放进真实业务环境。最常见的三种场景,我这里分别说一下做法。

第一种是本地视频流分析。推荐用多线程加双缓冲:一个线程专门处理图像解码和预处理,一个线程做推理。这样CPU和GPU能并行工作,不要串行地一帧一帧走。

更进一步的优化是用CUDA Stream做异步拷贝和执行,让数据搬运和计算重叠:

stream = cuda.Stream() cuda.memcpy_htod_async(gpu_input, h_input, stream) engine.execute_async_v2(bindings, stream.handle) cuda.memcpy_dtoh_async(h_output, gpu_output, stream) stream.synchronize()

第二种是封装成服务。engine在进程内加载一次,做成全局单例,每个请求来了直接复用。千万别在每个请求里重新加载engine,那会慢到怀疑人生。HTTP服务可以用FastAPI包一层,更追求性能就上gRPC。

第三种是Jetson等边缘设备。这里有个特别重要的建议:直接用JetPack系统自带的TensorRT,不要自己在板子上重新编译CUDA或装最新版TensorRT。Jetson的GPU架构和PC不同,强行替换系统库很容易把整个系统搞坏,我见过不少人在这上面翻车。在Jetson上的工程流程是:先在PC上把ONNX导出好,拷到板子上,再用板子上的TensorRT重新构建engine。

7.2 常见问题速查表

把我在实际部署中遇到的问题整理成了一张速查表,希望能帮你少走弯路。

现象可能原因解决方法
加载engine报错TensorRT版本不一致确认构建和加载用的是同一版本,必要时重新构建
输出全为0或乱码输入未做归一化或通道顺序错误检查是否除以255、是否BGR转RGB、内存是否连续
检测框偏移letterbox还原公式错误确认padding和scale参数传递正确
构建engine时报OOMworkspace过大或batch过大减小workspace、降低max batch、关掉其他显存占用程序
动态shape推理报错输入尺寸超出min/max范围把输入尺寸控制在构建时设置的区间内
加速效果不明显后处理或图片解码成为瓶颈用GPU NMS、用双缓冲、考虑把预处理放到GPU上
换个显卡后engine不能用engine与GPU架构强绑定在目标设备上重新运行构建流程
FP16结果和FP32差异明显某些层对精度敏感对该层单独回退FP32,或改用INT8校准方案

最后再补充一个容易忽略的小问题:如果部署机上有多张显卡,执行推理时一定要显式指定用哪张卡,cuda.set_device(0)或者设置CUDA_VISIBLE_DEVICES。否则生产环境里偶尔会碰到“模型时而正常时而报错”的诡异问题,其实就是多显卡环境下context绑定错乱了。

踩过几次坑之后,我现在的习惯是每到一个新环境,第一件事不是急着装最新版,而是先把CUDA、cuDNN、TensorRT、PyTorch这四者的版本组合在纸上列出来,对照官方兼容表确认一遍。模型能不能顺利部署,六成取决于版本匹配,三成在前处理对齐,剩下的一成才是NMS这类细节。建议你也按这套七步流程来,先在PC上把FP32跑通,再上FP16,最后再考虑INT8和目标设备。你会在第一次看到延迟曲线往下掉的时候,觉得前面这些折腾都值了。

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

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

立即咨询