Skip to content

TensorRT 与边缘部署:从 ONNX 到 Jetson

本页速览 把模型优化成 TensorRT engine 再部署到边缘设备(Jetson)的实战:ONNX→TRT 转换、FP16/INT8 精度权衡、动态 shape、边缘部署与功耗约束。

TensorRT 与边缘部署:从 ONNX 到 Jetson

一句话定义:TensorRT 是 NVIDIA 的 GPU 推理引擎,把训练好的模型(ONNX 等)编译成针对具体 GPU 高度优化的 engine——通过层融合、精度校准、内核自动选择把推理延迟再压一个数量级,然后部署到 Jetson 这类边缘设备

为什么值得动手:边缘设备上的 GPU 比云端小一个数量级,模型必须"抠"到极致才能实时跑。Jetson Orin Nano 只有 8~16GB 统一内存、功耗 7~25W,原生 PyTorch 在它上面跑 ResNet50 可能要 50ms+,TensorRT FP16 能压到 5ms 内、INT8 更快。本文从 ONNX 准备、trtexec 转换、INT8 校准到 Jetson 部署走完整条链路,并给出精度与性能的实测权衡。

一、TensorRT 是什么

TensorRT 对 GPU 推理做四件事:

  1. 层融合(kernel fusion):把 Conv+BN+ReLU 等算子融合成一个 kernel,减少 kernel 启动与显存读写;
  2. 精度校准(precision calibration):FP16/INT8 推理,减少带宽与计算量(INT8 计算快 2~4 倍、显存减半);
  3. 图优化与算子选择:按目标 GPU 架构自动选最优 kernel(如针对 Ada/Lovelace 的 tensor core 路径);
  4. 显存优化:静态显存池、减少中间张量分配。

先给结论:TensorRT 的收益在推理延迟上,典型是 PyTorch eager 的 2~8 倍;代价是编译产物绑定 GPU 型号与精度,且 INT8 需要校准数据。底层原理与推理:从前向传播到推理引擎中"推理引擎"一节对应。

二、优化流程总览

text
PyTorch 模型
   │  torch.onnx.export

model.onnx
   │  trtexec --fp16 / --int8 (+ 校准)

model.engine(绑定 GPU 型号与 TensorRT 版本)
   │  Triton 后端 / C++ Runtime / Python Runtime

Jetson 或数据中心 GPU 上运行

三、第一步:导出与检查 ONNX

python
import torch
import onnx
from torchvision.models import resnet50, ResNet50_Weights

model = resnet50(weights=ResNet50_Weights.DEFAULT).eval()
dummy = torch.randn(1, 3, 224, 224)

torch.onnx.export(
    model, dummy, "resnet50.onnx",
    input_names=["input"], output_names=["output"],
    # 动态 batch 与动态分辨率都要显式声明(边缘部署常用动态分辨率)
    dynamic_axes={"input": {0: "batch", 2: "height", 3: "width"},
                  "output": {0: "batch", 2: "height", 3: "width"}},
    opset_version=17,
)

# 用 onnx 库校验模型结构
onnx.checker.check_model(onnx.load("resnet50.onnx"))
print("onnx ok, ops:", len(onnx.load("resnet50.onnx").graph.node))

导出要点与兼容性检查见模型格式与转换导出失败/精度变化时优先怀疑自定义算子与 opset

四、第二步:trtexec 转换(FP16 / INT8 / 动态 shape)

trtexec 是 TensorRT 自带的转换与基准工具(装了 TensorRT 即自带):

bash
# FP16:最常用,零额外成本,速度约 2 倍
trtexec --onnx=resnet50.onnx --saveEngine=resnet50_fp16.engine \
        --fp16 \
        --minShapes=input:1x3x224x224 \
        --optShapes=input:1x3x224x224 \
        --maxShapes=input:16x3x224x224

# INT8:需要校准集(见第五节),速度约 3~4 倍
trtexec --onnx=resnet50.onnx --saveEngine=resnet50_int8.engine \
        --int8 \
        --calib=/data/calib/calib.txt \
        --calibCache=calib.cache \
        --minShapes=input:1x3x224x224 \
        --optShapes=input:1x3x224x224 \
        --maxShapes=input:16x3x224x224

动态 shape 的三个参数先给结论:

  • --minShapes / --optShapes / --maxShapes 定义 batch/分辨率的变化范围:min 用于显存预分配下限,opt 是内核调优的优化目标(最常用形态),max 决定显存上限。范围越大编译越慢、显存预留越多,尽量贴近真实使用范围;
  • --calibCache:把校准结果缓存成文件,第二次编译不用重跑校准(否则每次 --int8 都慢);
  • 转换产物 .engine 绑定具体 GPU 型号与 TensorRT 版本:在 A10 上编的 engine 不能直接跑 Jetson,换卡必须重新编译。

五、INT8 校准与精度校验

INT8 量化需要一组有代表性的校准数据,让 TensorRT 统计每层激活值的分布来定缩放因子。直接结论:

  1. 校准集必须代表线上真实输入分布——用线上抽样的 500~2000 张真实图片,不要用训练集随机抽(分布可能与线上漂移);
  2. 校准后必须做精度校验:跑一个验证集,对比 FP32/FP16 的指标(Top-1 或任务指标),INT8 掉点超过阈值(如图像分类 >0.5%)就要考虑用"逐层 INT8 + FP16 混合"(--layer-precision)只量化敏感度低的层;
  3. 精度 vs 速度权衡表(ResNet50,Jetson Orin Nano):
精度延迟 (ms)相对速度Top-1 变化
FP32约 201.0×基准
FP16约 8约 2.5×无损
INT8约 4.5约 4.4×约 -0.2%~-0.5%

量化的通用权衡(不止 TensorRT)见量化

逐层精度控制:INT8 掉点太多时的第二招

全图 INT8 掉点不可接受时,退一步做逐层混合精度:用 --layer-precision 指定哪些层保持 FP16。命令示例:

bash
trtexec --onnx=resnet50.onnx --saveEngine=resnet50_mixed.engine \
        --int8 --fp16 \
        --calib=/data/calib/calib.txt \
        # 指定敏感层(如最后的全连接、归一化层)保持 FP16
        --layer-precision=fc:PFP16 \
        --layer-precision=conv_5: PFP16

先给结论:先全图 INT8 看掉点,掉得少就用;掉得多再逐层调,调不动再考虑 QAT(量化感知训练)。多数 CNN 全图 INT8 掉点 <0.5%,Transformer/检测类模型敏感层更集中,逐层控制收益明显。--layer-precision 的层名清单可以用 trtexec --dumpLayerInfo 先导出。

六、运行时推理:Python / C++ 加载 engine

python
# trt_runtime.py
import tensorrt as trt
import numpy as np
from cuda import cudart  # 或 pycuda

logger = trt.Logger(trt.Logger.WARNING)
runtime = trt.Runtime(logger)

# 反序列化 engine(二进制,与转换时的 GPU 绑定)
with open("resnet50_fp16.engine", "rb") as f:
    engine = runtime.deserialize_cuda_engine(f.read())
context = engine.create_execution_context()

# 动态 shape:推理前必须显式设置输入 shape
context.set_input_shape("input", (1, 3, 224, 224))

# 分配输入输出显存(示例只列流程,真实代码要处理流与同步)
input_buf, output_buf, stream = allocate_buffers(engine, context)
# 拷入输入 -> context.execute_async_v2(bindings, stream) -> 拷出输出

要点:动态 shape 的 engine 每次推理都要 set_input_shape 并重新分配 buffer;生产上多用 Triton 的 TensorRT 后端托管 engine,省去手写显存管理——与 Triton 无缝衔接。C++ 路径用 InferRuntime 同理,边缘部署(Jetson)常用 C++ 或 Python + TensorRT 的 Jetson 专用镜像。

用 Triton 托管 engine:免手写显存管理

在数据中心或 Jetson 上跑多个 engine 时,Triton 的 tensorrt 后端是最省心的托管方式,config.pbtxt 里把 backend 换成 TensorRT 即可:

protobuf
# models/resnet50/config.pbtxt
name: "resnet50"
backend: "tensorrt"
max_batch_size: 16
input  [ { name: "input",  data_type: TYPE_FP32, dims: [3, 224, 224] } ]
output [ { name: "output", data_type: TYPE_FP32, dims: [1000] } ]
instance_group [ { kind: KIND_GPU, count: 1 } ]

优点:动态批处理、并发调度、指标输出全部白拿,还把"engine 文件管理"收进模型仓库。与 NVIDIA Triton 多模型服务 无缝衔接;Triton 的 --backend-config=tensorrt,coalesce-request-input=true 等参数可进一步调 batch。

七、边缘设备部署:Jetson

Jetson 是 NVIDIA 的边缘计算平台,常用型号对比(注意统一内存 shared memory):

型号算力统一内存功耗典型场景
Jetson Orin Nano约 40 TOPS (INT8)8/16GB7~25W摄像头端实时检测
Jetson Orin NX约 100 TOPS (INT8)16GB10~40W边缘多路视频
Jetson AGX Orin约 275 TOPS (INT8)64GB15~60W边缘 LLM/重模型

部署三件事:

  1. 换 Jetson 专用镜像nvcr.io/nvidia/l4t-pytorchl4t-tensorrt 或 JetPack SDK(自带匹配的 TensorRT 版本)——不要在 Jetson 上装数据中心版 TensorRT,架构不符
  2. 交叉编译 vs 板上编译:engine 必须在目标 Jetson 上重新编译(或使用 trtexec 离线板编译),容器镜像按 aarch64 构建;
  3. 功耗与温度约束:Jetson 有功耗模式(nvpmodel -m 0 最大性能 / 低功耗模式),跑重模型要选对模式并监控温度,过热降频会让延迟突增。硬件选型与功耗权衡详见GPU 与硬件选型

边缘部署的真问题

边缘和云端不同:网络可能断、设备可能重启、没有人在旁边重启进程。engine 文件要本地持久化(避免每次启动重新编译,编译一次可能几分钟);服务要支持开机自启与崩溃拉起;模型更新要靠 OTA 而不是现场刷机。这些"运维代价"常常被忽略,却在边缘场景占了大头。

八、性能对比实测表

同一张 Jetson Orin Nano、ResNet50、批量 1、动态输入 224×224:

部署方式延迟 (ms)吞吐 (FPS)显存占用
PyTorch eager (FP32)约 22约 45约 500MB
TensorRT FP16约 8约 125约 300MB
TensorRT INT8约 4.5约 220约 250MB

结论:FP16 是性价比最优起点,INT8 在精度可接受时再上。对移动端(无 NVIDIA GPU)场景,替代路线是 TFLite/ML Kit 的 TFLite + 量化,策略相同(精度校准 + 精度校验)。

常见坑与排查

现象排查
engine 换卡报错engine file is generated on an incompatible deviceengine 绑定 GPU/版本,目标设备重新编译
动态 shape 没设推理报 invalid shapes每次推理前 set_input_shape
INT8 精度崩Top-1 掉 5%+校准集不具代表性;换逐层混合精度
转换失败trtexec 报不支持算子换更高 opset、替换自定义算子、加 --onnx-normalization
显存 OOM动态 shape 范围设太大收窄 min/maxShapes,别给 max 留余量
Jetson 跑不动延迟比预期高很多检查 nvpmodel 功耗模式、温度降频
版本不匹配运行时报 trt 版本 API 缺失engine 与 runtime 用同一 TensorRT 版本

延伸阅读

参考资料