3、构建动态形状网络:使用Python API定义动态输入、设置动态维度、动态输出

好,咱们进入正题。这一章我带你手写一个动态形状网络。说白了,就是让TensorRT在推理时能接受不同大小的输入,而不是被绑死在一个固定尺寸上。

我个人习惯把动态形状网络比作「变形金刚」。你给它一个224x224的图,它能跑;给它一个512x512的图,它也能跑。这在实际项目中太重要了——比如你部署一个目标检测模型,用户上传的图片尺寸五花八门,总不能每张都resize到固定大小吧?

3.1 动态输入的定义方式

在Python API里,定义动态输入其实就一行代码的事。但这一行背后,藏着不少门道。

先看最基础的写法:

import tensorrt as trt

# 创建builder和network
builder = trt.Builder(trt.Logger(trt.Logger.WARNING))
network = builder.create_network(1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH))

# 定义动态输入
input_tensor = network.add_input(
    name="input",
    dtype=trt.float32,
    shape=(-1, 3, -1, -1)  # 关键在这里:-1表示动态维度
)

看到那个 (-1, 3, -1, -1) 了吗?这就是动态形状的核心。第一个-1是batch size,后面两个-1是高度和宽度。中间的3是通道数,固定不变。

重点理解: -1 不是「随便」,而是「待定」。它告诉TensorRT:「这个维度我现在不确定,等推理时再告诉你。」

我在项目中遇到过一个问题:有人把所有的维度都设成-1,结果引擎构建失败。为什么?因为TensorRT需要至少一个维度是确定的,才能做内存规划和算子选择。你想想看,如果连通道数都是动态的,那卷积核怎么选?

3.2 设置动态维度的范围

光写-1还不够。你得告诉TensorRT:「这个动态维度,最小是多少,最大是多少,常见是多少。」

这就用到 OptimizationProfile 了:

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

# 设置输入"input"的三个形状
profile.set_shape(
    "input",
    min=(1, 3, 224, 224),   # 最小尺寸:1张224x224的图
    opt=(4, 3, 512, 512),   # 常见尺寸:4张512x512的图
    max=(8, 3, 1024, 1024)  # 最大尺寸:8张1024x1024的图
)

# 把profile绑定到config
config = builder.create_builder_config()
config.add_optimization_profile(profile)

嗯,这里要注意三个值的含义:

  • min:推理时不会小于这个尺寸。我一般设成实际业务中最小的输入。
  • opt:TensorRT会针对这个尺寸做极致优化。选你最常用的尺寸。
  • max:不能超过这个尺寸,否则会报错。

我的经验: opt值一定要选对。我曾经有个项目,大部分请求是640x640的图,但我把opt设成了1024x1024。结果推理速度反而比固定尺寸慢了30%。因为TensorRT为了照顾大尺寸,选了更保守的算子。后来改成opt=(4, 3, 640, 640),速度直接翻倍。

3.3 动态输出的自动推导

输入设好了,输出怎么办?好消息是:大部分情况下,TensorRT会自动推导输出的形状。

比如一个简单的卷积网络:

# 添加一个卷积层
conv = network.add_convolution(
    input=input_tensor,
    num_output_maps=64,
    kernel_shape=(3, 3),
    kernel=weights,
    bias=bias
)

# 输出形状会自动变成 (-1, 64, -1, -1)
# 因为卷积不改变空间尺寸(padding=1的情况下)
output = conv.get_output(0)
output.name = "output"
network.mark_output(output)

但有些层会导致输出形状变化,比如池化层、步长卷积、上采样等。这时候你需要手动确认一下:

# 添加一个步长为2的卷积
conv_stride = network.add_convolution(
    input=input_tensor,
    num_output_maps=128,
    kernel_shape=(3, 3),
    stride=(2, 2),
    kernel=weights,
    bias=bias
)

# 输出形状会变成 (-1, 128, -1, -1)
# 但实际尺寸是输入的一半,TensorRT会自动处理

踩坑提醒: 我曾经遇到一个情况——网络里有Resize层,输出形状依赖于输入形状的倍数关系。结果推理时输入尺寸不是opt值,输出尺寸算错了。后来我加了一个assert,确保输入尺寸能被32整除。嗯,从那以后我再也不敢忽视对齐问题了。

3.4 完整示例:构建一个动态CNN

来,咱们把上面这些串起来,写一个完整的动态形状网络构建流程:

import tensorrt as trt
import numpy as np

def build_dynamic_network():
    # 1. 创建builder和network
    logger = trt.Logger(trt.Logger.WARNING)
    builder = trt.Builder(logger)
    network = builder.create_network(
        1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH)
    )
    
    # 2. 定义动态输入
    input_tensor = network.add_input(
        name="input",
        dtype=trt.float32,
        shape=(-1, 3, -1, -1)
    )
    
    # 3. 构建网络结构(这里用简单的卷积+池化)
    # 第一层卷积
    conv1 = network.add_convolution(
        input=input_tensor,
        num_output_maps=32,
        kernel_shape=(3, 3),
        padding=(1, 1),
        kernel=np.random.randn(32, 3, 3, 3).astype(np.float32),
        bias=np.zeros(32, dtype=np.float32)
    )
    conv1.name = "conv1"
    
    # ReLU激活
    relu1 = network.add_activation(
        input=conv1.get_output(0),
        type=trt.ActivationType.RELU
    )
    
    # 池化层
    pool1 = network.add_pooling(
        input=relu1.get_output(0),
        type=trt.PoolingType.MAX,
        window_size=(2, 2),
        stride=(2, 2)
    )
    
    # 4. 标记输出
    output = pool1.get_output(0)
    output.name = "output"
    network.mark_output(output)
    
    # 5. 设置动态profile
    config = builder.create_builder_config()
    profile = builder.create_optimization_profile()
    profile.set_shape(
        "input",
        min=(1, 3, 224, 224),
        opt=(4, 3, 512, 512),
        max=(8, 3, 1024, 1024)
    )
    config.add_optimization_profile(profile)
    
    # 6. 构建引擎
    engine = builder.build_engine(network, config)
    
    return engine

# 调用
engine = build_dynamic_network()
print("动态形状网络构建成功!")

这段代码跑通后,你就有了一个能处理224到1024任意尺寸输入的引擎。是不是很爽?

3.5 动态形状的注意事项

最后,我总结几个实战中容易翻车的地方:

问题 原因 解决方案
构建引擎超慢 动态形状需要探索更多算子组合 缩小min-max范围,或优化opt值
推理时形状不匹配 输入尺寸超出profile范围 加预处理检查,或resize到合法范围
显存占用飙升 TensorRT为最大尺寸预留了显存 使用动态显存分配,或限制max尺寸
某些层不支持动态 比如全连接层需要固定输入维度 改用卷积或GAP替代

一句话总结: 动态形状不是魔法,它需要你给TensorRT足够的「提示」——min、opt、max三个值就是你的提示。选对了,事半功倍;选错了,事倍功半。

下一章,我会带你看看如何在推理时动态绑定输入数据,以及如何处理多个profile的情况。到时候你会发现,动态形状的真正威力,在推理阶段才完全释放出来。