3. TensorRT核心概念与工作流程
TensorRT 并不是一个“直接运行 ONNX 的推理框架”,而是一套把训练好的模型编译成针对特定 GPU 高度优化的推理引擎的工具链。理解它的关键,是先把几个核心概念串成一条线:计算图、层与张量、精度模式、构建器与运行时、序列化与反序列化,以及 ONNX 解析与模型导入。只有把这条线走通,后面在 Jetson 上做 FP16/INT8 加速、显存优化和吞吐调优时,才知道每一步在改什么、为什么有效。
计算图、层与张量:TensorRT 眼中的模型
一个神经网络在 TensorRT 内部被表示为一张有向无环计算图。图中的节点是层(Layer),边是张量(Tensor)。层负责运算,例如卷积、矩阵乘、激活、池化、归一化;张量负责承载数据,它带有名称、维度(shape)、数据类型(dtype)和内存布局。以常见的 ResNet 类分类网络为例,输入张量形如 [N, 3, 224, 224],经过若干卷积层后张量维度逐步缩小、通道数增大,最后接一个全连接或全局池化层输出 [N, 1000] 的 logits。TensorRT 在构建阶段会遍历这张图,做层融合、常量折叠、精度选择和张量内存复用,最终生成一个引擎。
这里要区分两个概念:网络定义(INetworkDefinition)是构建期的可变图,可以添加、修改层;引擎(ICudaEngine)是构建后的固化产物,结构、精度和内存规划都已确定,运行时只负责按绑定(binding)喂数据、取结果。张量在引擎中通常分为输入、输出和内部张量,内部张量由 TensorRT 自行分配和复用,用户一般只关心输入输出绑定。
精度模式:FP32、FP16 与 INT8 的取舍
精度模式是 TensorRT 优化中最直接的“加速旋钮”。FP32 是默认精度,数值稳定、精度最高,但计算和显存开销最大。FP16 把权重和激活降到 16 位浮点,在 Jetson 的 GPU 上通常能获得约 1.5~2 倍的速度提升,显存占用减半,而多数视觉模型的精度损失很小,因此是 Jetson 部署的首选档位。INT8 进一步把数据压到 8 位整型,速度可达 FP32 的 2~4 倍,但它不是简单地把浮点截断,而是需要校准(calibration):用一批有代表性的输入数据统计每层激活的数值分布,确定缩放因子(scale)和零点(zero point),从而在量化后尽量保持输出一致。
在代码层面,精度通过构建配置指定。例如启用 FP16 只需设置一个标志位:
config = builder.create_builder_config()
config.set_flag(trt.BuilderFlag.FP16) # 启用 FP16
# config.set_flag(trt.BuilderFlag.INT8) # 启用 INT8,需配合校准器
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) # 1GB workspace
engine = builder.build_engine(network, config)
需要注意的是,精度标志是“允许”而非“强制”。TensorRT 会逐层判断该层是否支持目标精度,若某层不支持则自动回退到更高精度,因此最终引擎可能是混合精度。判断是否真正生效,可以查看构建日志中类似 Conv_0 ... FP16 的层信息,或对比不同精度下引擎的推理耗时与输出误差。
构建器与运行时:两个阶段、两种角色
TensorRT 的工作流明确分为构建期和运行期。构建期由 IBuilder 主导,负责解析模型、优化图、选择精度、分配工作空间并生成引擎。这个过程计算密集、耗时较长,在 Jetson 上构建一个中等规模模型可能需要数十秒到数分钟,因此不应在每次启动时都重新构建。运行期由 IRuntime 和 IExecutionContext 主导,负责加载已序列化的引擎、绑定输入输出显存、执行推理。运行时不做图优化,启动快、开销小,适合部署场景。
一个常见的工程做法是:在开发机或 Jetson 上一次性构建引擎并序列化保存,部署时只做反序列化。序列化把引擎保存为 .plan 文件,反序列化则从该文件恢复引擎。需要特别注意的是,引擎与构建时的 GPU 架构、TensorRT 版本、精度配置强绑定,换设备或升级 TensorRT 后通常需要重新构建,否则反序列化可能失败或行为异常。
ONNX 解析与模型导入
TensorRT 原生不直接读取 PyTorch 或 TensorFlow 模型,主流路径是先把模型导出为 ONNX,再用 ONNX Parser 导入。导入时 Parser 会遍历 ONNX 图的节点,把支持的算子映射为 TensorRT 层,不支持的算子则报错或需要自定义插件(Plugin)。因此模型导入阶段最常见的两类问题是:算子不支持和动态维度处理不当。
以 PyTorch 导出为例,典型流程如下:
# 1. 导出 ONNX(在 PyTorch 侧执行)
torch.onnx.export(model, dummy_input, "model.onnx",
input_names=["input"], output_names=["output"],
opset_version=13, dynamic_axes={"input": {0: "batch"}})
# 2. TensorRT 导入并构建
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("model.onnx", "rb") as f:
if not parser.parse(f.read()):
for i in range(parser.num_errors):
print(parser.get_error(i))
如果解析失败,parser.get_error(i) 会给出具体算子或维度问题,这是排查导入错误的第一手信息。对于动态 batch,需要在构建配置中设置优化 profile,指定最小、最优、最大 shape,TensorRT 会针对该范围生成可复用的引擎。若模型包含 Parser 不支持的算子,则需要编写自定义插件并在网络中手动添加该层,这是进阶部署中绕不开的一环。
把上述概念串起来看:ONNX 提供模型描述,Parser 把它翻译成 TensorRT 的网络定义,Builder 在指定精度下优化并生成引擎,序列化把引擎落盘,Runtime 在部署时反序列化并执行推理。理解这条链路中每一步的输入、输出和约束,才能在 Jetson 上做出既快又稳的推理部署。