3、TensorRT模型导出基础:从PyTorch导出ONNX、ONNX模型检查与优化、TensorRT解析ONNX、Engine序列化与反序列化

好,咱们进入正题。这一章是整个课程的地基,说白了就是搞清楚模型怎么从PyTorch一路跑到TensorRT上。我见过太多同学在ONNX导出这一步就卡住了,后面全白搭。所以咱们慢慢来,把每个环节都掰扯清楚。

3.1 从PyTorch导出ONNX

PyTorch导出ONNX,核心就一个函数——torch.onnx.export()。但参数怎么填,坑在哪里,我踩过不少。

先看一个最基础的例子:

import torch
import torchvision.models as models

# 加载一个预训练模型
model = models.resnet18(pretrained=True)
model.eval()

# 构造一个虚拟输入
dummy_input = torch.randn(1, 3, 224, 224)

# 导出ONNX
torch.onnx.export(
    model,               # 模型
    dummy_input,         # 输入张量
    "resnet18.onnx",     # 输出文件名
    export_params=True,  # 是否导出参数
    opset_version=11,    # ONNX算子集版本
    do_constant_folding=True,  # 常量折叠优化
    input_names=['input'],
    output_names=['output'],
    dynamic_axes={
        'input': {0: 'batch_size'},
        'output': {0: 'batch_size'}
    }
)
print("ONNX导出完成")

这里有几个关键点,我重点说一下:

  • opset_version:我个人习惯用11或13。版本太低有些算子不支持,版本太高某些推理框架可能不兼容。11是个稳妥的选择。
  • dynamic_axes:这个参数很关键。如果你希望模型支持动态batch,就得在这里指定。我在项目中遇到过,一开始没设这个,结果线上请求batch大小变化时,每次都得重新构建Engine,性能直接崩了。
  • do_constant_folding:建议打开。它能提前把一些固定计算算好,减少ONNX模型里的冗余节点。
注意:导出前一定要调用 model.eval()。我见过有人忘了这步,结果导出的ONNX里包含了Dropout和BatchNorm的训练行为,推理结果完全不对。

3.2 ONNX模型检查与优化

导出的ONNX文件,不能直接扔给TensorRT。你得先检查一下,看看有没有问题。我常用的工具是 onnxruntimeonnx 自带的检查器。

3.2.1 基础检查

import onnx

# 加载ONNX模型
onnx_model = onnx.load("resnet18.onnx")

# 检查模型结构是否合法
onnx.checker.check_model(onnx_model)
print("ONNX模型检查通过")

# 打印模型信息
print(onnx.helper.printable_graph(onnx_model.graph))

这一步能发现一些明显的问题,比如节点连接错误、类型不匹配等。但说实话,很多时候检查通过了,实际跑起来还是有问题。所以我会再用onnxruntime跑一遍推理,验证数值是否正确。

import onnxruntime as ort
import numpy as np

# 创建推理会话
ort_session = ort.InferenceSession("resnet18.onnx")

# 构造输入
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)

# 运行推理
outputs = ort_session.run(None, {'input': input_data})
print("ONNX推理结果:", outputs[0].shape)

3.2.2 模型优化

ONNX模型里经常有一些冗余操作,比如连续的Reshape、Cast等。我推荐用 onnx-simplifier 来清理:

pip install onnx-simplifier
python -m onnxsim resnet18.onnx resnet18_simplified.onnx

这个工具能合并一些节点,去掉不必要的操作。我在一个项目里,优化前模型有200多个节点,优化后只剩150个,推理速度提升了将近20%。

小技巧:优化后的ONNX文件,最好再用onnxruntime验证一次。我曾经遇到过简化器把某些自定义算子搞丢的情况,还好及时发现。

3.3 TensorRT解析ONNX

ONNX准备好了,接下来就是用TensorRT来解析它。TensorRT提供了一个叫 trtexec 的命令行工具,但咱们做工程化,还是用Python API更灵活。

import tensorrt as trt

# 创建logger
logger = trt.Logger(trt.Logger.WARNING)

# 创建builder
builder = trt.Builder(logger)

# 创建network定义
network = builder.create_network(
    1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)

# 创建ONNX解析器
parser = trt.OnnxParser(network, logger)

# 解析ONNX模型
with open("resnet18_simplified.onnx", "rb") as f:
    if not parser.parse(f.read()):
        for error in range(parser.num_errors):
            print(parser.get_error(error))
        raise RuntimeError("ONNX解析失败")

print("ONNX解析成功,网络包含 %d 层" % network.num_layers)

这里有个细节:EXPLICIT_BATCH 这个标志。如果你在导出ONNX时用了dynamic_axes,这里就必须加上它。否则TensorRT会认为batch是固定的,动态batch就失效了。

解析成功后,network里就包含了模型的计算图。但这时候还不能直接跑,得先构建Engine。

3.4 Engine序列化与反序列化

Engine是TensorRT优化后的最终产物。构建Engine很耗时,尤其是复杂模型,可能要好几分钟。所以咱们一般会把它序列化保存到磁盘,下次直接加载,省得重新构建。

3.4.1 构建并序列化Engine

# 创建配置
config = builder.create_builder_config()
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)  # 1GB

# 构建engine
serialized_engine = builder.build_serialized_network(network, config)

# 保存到文件
with open("resnet18.engine", "wb") as f:
    f.write(serialized_engine)

print("Engine序列化完成,保存为 resnet18.engine")

构建时,set_memory_pool_limit 控制显存使用上限。设得太小,构建可能失败;设得太大,显存不够用。我一般先设1GB,不够再加。

3.4.2 反序列化加载Engine

# 从文件加载
with open("resnet18.engine", "rb") as f:
    serialized_engine = f.read()

# 创建runtime
runtime = trt.Runtime(logger)

# 反序列化
engine = runtime.deserialize_cuda_engine(serialized_engine)

# 创建执行上下文
context = engine.create_execution_context()

print("Engine反序列化成功,准备推理")

反序列化很快,基本是毫秒级的。所以线上服务启动时,直接从文件加载Engine,比重新构建快得多。

核心要点:
  • 序列化保存的是优化后的Engine,不是原始ONNX
  • Engine是平台相关的,不同GPU架构(如T4、A100)的Engine不能混用
  • 反序列化时,runtime的logger级别要和构建时一致,否则可能报错

3.5 完整流程总结

咱们把整个流程串起来,就是下面这张表:

步骤 操作 输出 耗时
1 PyTorch导出ONNX .onnx文件 秒级
2 ONNX检查与优化 优化后的.onnx 秒级
3 TensorRT解析ONNX network对象 秒级
4 构建Engine并序列化 .engine文件 分钟级
5 反序列化加载Engine engine对象 毫秒级

嗯,到这里,模型导出的基础流程就讲完了。下一章咱们会深入Engine的构建配置,看看怎么调优才能让推理跑得更快。到时候我会分享一些实际项目中的调参经验,保证让你少走弯路。