4. TensorRT Python API入门:创建Builder与Config、构建Network定义、设置优化策略、生成Engine文件

好,咱们今天来聊聊TensorRT的Python API。说实话,很多同学一上来就急着调inference,结果连Engine怎么生成的都搞不清楚。我个人习惯是,先把这几个核心对象摸透:Builder、Config、Network,最后才是Engine。你想想看,盖房子总得先有图纸、有施工队、有监理吧?TensorRT里也是这个道理。

4.1 创建Builder与Config

Builder是TensorRT的“总指挥”。它负责协调整个模型优化和编译的过程。我刚开始用的时候,以为Builder就是个工厂类,直接new一下就行。其实没那么简单。

核心要点:Builder是单例的,一个进程中通常只创建一个。Config是Builder的配置对象,用来控制优化行为。

import tensorrt as trt

# 创建Builder - 这是入口
logger = trt.Logger(trt.Logger.WARNING)
builder = trt.Builder(logger)

# 创建Config - 这是配置中心
config = builder.create_builder_config()

# 设置工作空间大小(显存上限)
# 我曾经遇到过显存爆炸的问题,就是因为没设这个
config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30)  # 1GB

这里有个坑:trt.Logger的级别。我建议开发阶段用INFO,生产环境用WARNING。为什么?因为INFO会打印每一层的详细信息,调试时很有用,但线上跑起来日志量太大。

小技巧:如果你在Jupyter Notebook里调试,可以用trt.Logger(trt.Logger.ERROR),只显示错误信息,避免刷屏。

4.2 构建Network定义

Network定义就是你的模型“图纸”。TensorRT支持两种方式构建网络:一种是直接解析ONNX或Caffe模型,另一种是手动搭建网络层。咱们先讲最常用的——从ONNX解析。

# 创建Network对象
# 显式指定batch维度,这样后续可以动态调整batch size
network = builder.create_network(
    1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
)

# 加载ONNX模型
parser = trt.OnnxParser(network, logger)

with open("model.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(f"网络输入数量: {network.num_inputs}")
print(f"网络输出数量: {network.num_outputs}")

嗯,这里要注意。ONNX解析失败是家常便饭。我遇到过最离谱的一次,是因为模型里有个自定义算子,ONNX标准里没有。后来我手动用Plugin实现了那个算子才搞定。

避坑指南:我曾经花了一整天排查一个bug,最后发现是ONNX版本和TensorRT版本不兼容。建议你使用ONNX opset 11或13,这两个版本TensorRT支持得最好。

4.3 设置优化策略

优化策略说白了就是告诉TensorRT:“嘿,你帮我试试这几种配置,看哪个跑得最快。”Config里有个optimization_profile的概念,专门干这个的。

# 创建优化profile
profile = builder.create_optimization_profile()

# 设置输入张量的形状范围
# 格式: (最小形状, 最优形状, 最大形状)
input_name = network.get_input(0).name
profile.set_shape(input_name, 
                  min_shape=(1, 3, 224, 224),
                  opt_shape=(8, 3, 224, 224),
                  max_shape=(32, 3, 224, 224))

config.add_optimization_profile(profile)

# 设置精度模式
# 我建议先跑FP32验证正确性,再切FP16或INT8
if builder.platform_has_fast_fp16:
    config.set_flag(trt.BuilderFlag.FP16)
    print("支持FP16加速,已启用")

# 设置严格类型约束(默认关闭)
# config.set_flag(trt.BuilderFlag.STRICT_TYPES)

你可能会问:为什么要设三个形状?说白了,TensorRT需要知道你的输入范围,它才能针对性地做内存规划和算子选择。如果你只给一个固定形状,那动态batch就用不了了。

参数 含义 我的建议
min_shape 最小输入尺寸 设为实际业务的最小值
opt_shape 最常见尺寸 设为batch size的典型值
max_shape 最大输入尺寸 不要超过显存上限

个人经验:opt_shape设为你线上最常用的batch size。TensorRT会针对这个尺寸做深度优化,效果最明显。

4.4 生成Engine文件

终于到这一步了。Engine就是TensorRT编译后的“可执行文件”。它包含了优化后的网络结构、权重、以及CUDA kernel代码。说白了,Engine就是你的模型“成品”。

# 序列化Engine
# 这一步可能会比较慢,取决于模型大小
engine_bytes = builder.build_serialized_network(network, config)

if engine_bytes is None:
    raise RuntimeError("Engine构建失败")

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

print(f"Engine文件大小: {len(engine_bytes) / 1024 / 1024:.2f} MB")

# 或者直接创建runtime可用的Engine
# runtime = trt.Runtime(logger)
# engine = runtime.deserialize_cuda_engine(engine_bytes)

这里有个关键点:build_serialized_network返回的是序列化的字节流,不是直接可用的Engine对象。你需要用Runtime来反序列化。我习惯把Engine保存成文件,这样下次启动时直接加载,省去重新编译的时间。

重要提醒:Engine文件是跟CUDA版本、GPU架构绑定的。你在A100上生成的Engine,拿到T4上可能跑不了。我曾经因为这个被坑过,部署时忘了检查GPU型号,结果线上报错。

4.5 完整流程示例

把上面的步骤串起来,就是一个完整的Engine生成流程。我一般会封装成一个函数,方便复用。

def build_engine(onnx_path, engine_path, batch_size=8):
    """
    从ONNX生成TensorRT Engine
    
    Args:
        onnx_path: ONNX模型路径
        engine_path: 输出Engine路径
        batch_size: 默认batch size
    """
    logger = trt.Logger(trt.Logger.WARNING)
    builder = trt.Builder(logger)
    config = builder.create_builder_config()
    
    # 设置工作空间
    config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 2 << 30)  # 2GB
    
    # 创建网络
    network = builder.create_network(
        1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
    )
    
    # 解析ONNX
    parser = trt.OnnxParser(network, logger)
    with open(onnx_path, "rb") as f:
        if not parser.parse(f.read()):
            for i in range(parser.num_errors):
                print(parser.get_error(i))
            return None
    
    # 设置优化profile
    profile = builder.create_optimization_profile()
    input_name = network.get_input(0).name
    profile.set_shape(input_name,
                      (1, 3, 224, 224),
                      (batch_size, 3, 224, 224),
                      (batch_size * 4, 3, 224, 224))
    config.add_optimization_profile(profile)
    
    # 启用FP16(如果支持)
    if builder.platform_has_fast_fp16:
        config.set_flag(trt.BuilderFlag.FP16)
    
    # 构建Engine
    print("开始构建Engine,这可能需要几分钟...")
    engine_bytes = builder.build_serialized_network(network, config)
    
    if engine_bytes:
        with open(engine_path, "wb") as f:
            f.write(engine_bytes)
        print(f"Engine已保存到: {engine_path}")
        return engine_bytes
    else:
        print("Engine构建失败")
        return None

# 使用示例
build_engine("resnet50.onnx", "resnet50.engine", batch_size=8)

你看,整个流程其实不复杂。但每个环节都有它的门道。我个人觉得,最容易被忽视的就是优化profile的设置。很多人随便填个形状就完事了,结果线上性能差一大截。

好了,这一章的内容就到这里。下一章咱们会讲如何加载Engine并执行推理,到时候你会看到,前面这些准备工作都是值得的。