外观
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 推理做四件事:
- 层融合(kernel fusion):把 Conv+BN+ReLU 等算子融合成一个 kernel,减少 kernel 启动与显存读写;
- 精度校准(precision calibration):FP16/INT8 推理,减少带宽与计算量(INT8 计算快 2~4 倍、显存减半);
- 图优化与算子选择:按目标 GPU 架构自动选最优 kernel(如针对 Ada/Lovelace 的 tensor core 路径);
- 显存优化:静态显存池、减少中间张量分配。
先给结论: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 统计每层激活值的分布来定缩放因子。直接结论:
- 校准集必须代表线上真实输入分布——用线上抽样的 500~2000 张真实图片,不要用训练集随机抽(分布可能与线上漂移);
- 校准后必须做精度校验:跑一个验证集,对比 FP32/FP16 的指标(Top-1 或任务指标),INT8 掉点超过阈值(如图像分类 >0.5%)就要考虑用"逐层 INT8 + FP16 混合"(
--layer-precision)只量化敏感度低的层; - 精度 vs 速度权衡表(ResNet50,Jetson Orin Nano):
| 精度 | 延迟 (ms) | 相对速度 | Top-1 变化 |
|---|---|---|---|
| FP32 | 约 20 | 1.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/16GB | 7~25W | 摄像头端实时检测 |
| Jetson Orin NX | 约 100 TOPS (INT8) | 16GB | 10~40W | 边缘多路视频 |
| Jetson AGX Orin | 约 275 TOPS (INT8) | 64GB | 15~60W | 边缘 LLM/重模型 |
部署三件事:
- 换 Jetson 专用镜像:
nvcr.io/nvidia/l4t-pytorch、l4t-tensorrt或 JetPack SDK(自带匹配的 TensorRT 版本)——不要在 Jetson 上装数据中心版 TensorRT,架构不符; - 交叉编译 vs 板上编译:engine 必须在目标 Jetson 上重新编译(或使用
trtexec离线板编译),容器镜像按aarch64构建; - 功耗与温度约束: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 device | engine 绑定 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 版本 |
延伸阅读
- 推理:从前向传播到推理引擎 —— TensorRT 层融合、kernel 选择的底层原理
- 模型格式与转换 —— ONNX 导出、opset、算子兼容的完整检查清单
- 量化 —— INT8 校准、PTQ/QAT 与精度权衡的理论
- NVIDIA Triton 多模型服务 —— 用 Triton 的 TensorRT 后端托管 engine,免手写显存管理
- GPU 与硬件选型 —— Jetson 系列、功耗模式与边缘硬件决策
- 常见陷阱与反模式 —— 边缘部署的运维坑(OTA、开机自启、崩溃拉起)
参考资料
- NVIDIA TensorRT 官方文档:https://docs.nvidia.com/deeplearning/tensorrt/
- trtexec 使用说明:https://docs.nvidia.com/deeplearning/tensorrt/developer-guide/index.html#trtexec
- NVIDIA Jetson 模块:https://developer.nvidia.com/embedded/jetson-modules
- NVIDIA L4T / JetPack:https://developer.nvidia.com/embedded/jetpack