4、模型准备:PyTorch/TensorFlow/ONNX模型导出动态维度的方法、动态轴标记、batch维度设置

在RKNN模型转换与部署的完整链路中,模型导出是决定后续量化精度与推理灵活性的关键前置环节。许多开发者在完成模型训练后,直接使用静态shape导出模型,导致在部署阶段遇到batch size固定、输入分辨率不可变、多路视频流无法复用同一模型等问题。本章将系统讲解在PyTorch、TensorFlow、ONNX三种主流框架下导出动态维度模型的具体方法,重点剖析动态轴标记(dynamic axes)的语义、batch维度设置的工程陷阱,以及如何为RKNN工具链准备一份“既灵活又可控”的中间模型。

动态维度模型导出与RKNN适配链路 PyTorch torch.onnx.export dynamic_axes={"batch":{0:"N"}} opset_version ≥ 11 dummy_input 仅作shape参考 支持动态H/W(需算子兼容) TensorFlow tf.saved_model / Keras InputSpec(shape=[None,H,W,C]) converter.target_spec.supported_ops 动态维度用None占位 TFLite转换时需指定动态维度 ONNX onnx.helper / onnxruntime dim_param 命名动态轴 graph.input[0].type.tensor_type shape.dim[0].dim_param="N" 支持动态shape + 动态rank 动态轴标记验证 onnx.checker.check_model | onnxruntime 推理测试 | 对比静态/动态输出一致性 RKNN-Toolkit2 转换配置 rknn.config(mean_values, std_values, target_platform='rk3588') rknn.load_onnx(model='dynamic_model.onnx', input_size_list=[[1,3,640,640]])

上图展示了从三种主流框架导出动态维度模型,到ONNX中间表示,再到RKNN工具链适配的完整链路。核心要点在于:动态维度并非“无限自由”,而是通过显式标记告诉推理引擎哪些轴可以在运行时变化。RKNN工具链在加载模型时,会依据这些标记生成对应的动态shape处理逻辑,但实际可变的轴范围和步长受限于NPU硬件调度策略。因此,理解动态轴的声明方式与RKNN的约束条件,是避免“导出成功但转换失败”的关键。

4.1 核心原理:动态维度与静态维度的本质差异

深度学习模型在编译与部署时,张量的shape信息决定了内存分配、算子调度和流水线优化策略。静态shape模型在编译期即可确定所有中间张量的大小,NPU可以提前规划好缓冲区并应用最激进的优化;而动态shape模型则需要在运行时根据实际输入尺寸重新计算部分调度策略,这会引入额外的开销。但动态维度的价值在于:同一份模型可以适配不同batch size、不同分辨率的输入,无需为每种规格单独导出和存储模型文件,这在多路视频分析、动态分辨率检测等场景中能显著减少开发与维护成本。

从ONNX的视角看,动态维度通过dim_param(维度参数名)或dim_value(固定值)来区分。当某个维度被赋予一个字符串名称(如"N"、"batch")时,该维度即为动态轴;若使用整数,则为静态轴。RKNN-Toolkit2在解析ONNX模型时,会读取这些标记,并在内部构建动态shape的推理图。值得注意的是,动态轴并非所有算子都支持,例如某些硬件加速的卷积实现可能要求输入通道数为固定值,而Reshape、Transpose等算子对动态shape的传播也有额外限制。

核心认知:动态维度是“声明式”的,不是“绝对自由”的。导出的模型只是声明了哪些轴可变,实际能否在RKNN上高效运行,取决于NPU驱动对动态shape的支持程度以及算子的兼容性。建议在导出前先查阅RKNN-Toolkit2的算子支持列表,确认目标模型中的关键算子(如卷积、池化、全连接)是否支持动态输入。

4.2 PyTorch模型导出动态维度的方法

PyTorch通过torch.onnx.export函数导出ONNX模型时,dynamic_axes参数是控制动态维度的核心。该参数接受一个字典,键为输入/输出名称,值为一个子字典,子字典的键为维度索引,值为该维度的符号名称。以下是一个典型的动态batch导出示例:

import torch
import torchvision.models as models

model = models.resnet18(pretrained=True)
model.eval()

# 准备一个静态shape的dummy输入,仅用于确定网络结构
dummy_input = torch.randn(1, 3, 224, 224)

# 定义动态轴:batch维度(索引0)标记为'N',高度和宽度维度(索引2,3)标记为'H'和'W'
dynamic_axes = {
    'input': {0: 'N', 2: 'H', 3: 'W'},
    'output': {0: 'N'}
}

torch.onnx.export(
    model,
    dummy_input,
    'resnet18_dynamic.onnx',
    input_names=['input'],
    output_names=['output'],
    dynamic_axes=dynamic_axes,
    opset_version=12,
    do_constant_folding=True
)
print("动态模型导出完成")

在上述代码中,dynamic_axes将输入张量的第0维(batch)、第2维(高度)、第3维(宽度)均声明为动态轴。这意味着导出的ONNX模型可以接受任意batch size和任意H×W分辨率的输入(只要满足算子的约束)。但需要注意:opset_version的选择会影响动态shape的表达能力,建议使用opset 11及以上版本,因为早期opset对动态shape的支持不完整,可能导致某些算子无法正确传播动态维度。

对于更复杂的模型,如果存在多个输入或中间张量需要动态化,可以在dynamic_axes中为每个张量单独声明。此外,PyTorch 2.x版本引入了torch.export和torch.onnx.dynamo_export等新接口,它们对动态shape的支持更加完善,但API略有不同。以下是一个使用dynamo_export的示例:

from torch.onnx import dynamo_export

# 使用动态shape的输入,通过dynamic_shapes参数指定
dynamic_shapes = {
    'input': {0: torch.export.Dim("batch", min=1, max=16), 2: torch.export.Dim("height"), 3: torch.export.Dim("width")}
}
dynamo_export(model, dummy_input, dynamic_shapes=dynamic_shapes).save("resnet18_dynamo.onnx")

这里torch.export.Dim允许指定动态维度的范围(如batch从1到16),这为后续RKNN转换提供了更明确的约束信息,有助于工具链生成更高效的调度策略。

4.3 TensorFlow模型导出动态维度的方法

TensorFlow中动态维度的处理方式与PyTorch不同。在Keras或tf.keras中,定义模型输入时使用None作为维度占位符即可表示动态轴。例如:

import tensorflow as tf

# 定义动态输入:batch维度为None,高度和宽度也为None
inputs = tf.keras.Input(shape=(None, None, 3), batch_size=None, name='input')
# 构建模型...
model = tf.keras.Model(inputs, outputs)

# 保存为SavedModel格式
model.save('dynamic_model_savedmodel')

在转换为TFLite或ONNX时,需要额外指定动态维度。对于TFLite,转换器默认会保留动态维度,但某些优化可能会将其固定。推荐使用以下方式:

converter = tf.lite.TFLiteConverter.from_saved_model('dynamic_model_savedmodel')
converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS, tf.lite.OpsSet.SELECT_TF_OPS]
converter.optimizations = [tf.lite.Optimize.DEFAULT]
# 允许动态shape
converter._experimental_disable_per_channel = False
tflite_model = converter.convert()

如果希望将TensorFlow模型导出为ONNX,推荐使用tf2onnx工具。在转换时,需要显式指定动态维度:

import tf2onnx

# 指定输入签名,将batch维度标记为动态
spec = [tf.TensorSpec([None, None, None, 3], tf.float32, name='input')]
onnx_model, _ = tf2onnx.convert.from_keras(model, input_signature=spec, opset=13, output_path="model.onnx")

在tf.TensorSpec中,None即表示该维度为动态。tf2onnx会自动将这些None转换为ONNX的dim_param。需要注意的是,TensorFlow的None维度在转换后可能被命名为None或自动生成名称,建议在转换后使用onnx库检查并重命名动态轴,以便后续RKNN配置时清晰识别。

4.4 ONNX模型动态轴标记与手动修改

有时我们拿到的ONNX模型已经是静态shape,或者需要手动调整动态轴的名称和范围。此时可以使用onnx库直接修改模型文件。以下是一个典型的操作流程:

import onnx

model = onnx.load("static_model.onnx")

# 获取输入张量
input_tensor = model.graph.input[0]
print("原始shape:", input_tensor.type.tensor_type.shape)

# 修改batch维度为动态,命名为'N'
dim = input_tensor.type.tensor_type.shape.dim[0]
dim.dim_param = 'N'  # 将固定值改为动态参数名

# 修改高度和宽度维度为动态
dim_h = input_tensor.type.tensor_type.shape.dim[2]
dim_h.dim_param = 'H'
dim_w = input_tensor.type.tensor_type.shape.dim[3]
dim_w.dim_param = 'W'

# 检查模型合法性
onnx.checker.check_model(model)
onnx.save(model, "dynamic_model_modified.onnx")
print("动态轴标记完成")

除了修改输入张量,还需要检查输出张量以及中间可能存在的Shape相关算子。如果模型中有Reshape、Flatten等操作,它们可能会将动态维度传播到其他位置,需要确保这些算子支持动态shape。此外,onnxruntime提供了动态shape的推理验证功能,可以在导出后立即测试:

import onnxruntime as ort
import numpy as np

sess = ort.InferenceSession("dynamic_model_modified.onnx", providers=['CPUExecutionProvider'])
# 使用不同的batch size进行推理
for batch in [1, 4, 8]:
    dummy = np.random.randn(batch, 3, 224, 224).astype(np.float32)
    outputs = sess.run(None, {'input': dummy})
    print(f"batch={batch}, output shape={outputs[0].shape}")

如果所有batch size均能正常推理且输出shape正确,说明动态轴标记成功。若出现错误,通常是因为某些算子不支持动态shape,需要回到原始框架中修改模型结构或使用onnx-simplifier等工具进行优化。

4.5 案例拆解:动态batch模型在RK3588上的部署实战

某安防企业需要在RK3588平台上部署一个行人检测模型,要求同一模型能够处理1路、4路、8路视频流,且输入分辨率在960×540到1920×1080之间动态变化。初始方案是分别导出三个静态模型(batch=1、4、8),但这样会导致模型文件冗余且切换时需要重新加载,延迟较高。

关键约束与目标:模型为YOLOv5s,PyTorch训练,需转换为RKNN格式。目标是在不损失精度的前提下,实现动态batch和动态分辨率支持,且单次推理延迟不超过15ms(NPU)。

实施步骤:

  1. 导出动态ONNX模型:使用PyTorch的dynamic_axes将batch、高度、宽度均设为动态轴,opset=12。
  2. 验证动态性:使用onnxruntime测试batch=1、4、8以及不同分辨率下的推理,确认输出shape正确。
  3. RKNN转换配置:在rknn.config中设置target_platform='rk3588',并在load_onnx时指定input_size_list为[[1,3,640,640]]作为基准shape。
  4. 动态shape范围设置:在RKNN-Toolkit2中,通过rknn.build(do_quantization=True, dataset='dataset.txt')时,工具会自动识别ONNX中的动态轴。但需要额外设置dynamic_input参数,指定batch的可选范围(如[1,4,8])和分辨率范围。
  5. 板端部署:在RK3588上使用rknn_init时传入动态shape配置,运行时根据实际输入调用rknn_run并传入对应的shape信息。

结果对比:动态模型在batch=1时推理延迟为8ms,batch=4时为12ms,batch=8时为18ms(略超目标,但通过NPU调度优化后降至14ms)。相比静态多模型方案,模型文件从3个减少到1个,内存占用降低约40%,切换视频流时无需重新加载模型,延迟降低90%以上。

风险与代价:动态shape会导致NPU无法完全预分配缓冲区,首次推理时会有额外的内存分配开销。此外,某些算子(如YOLOv5的Detect层中的Reshape)在动态shape下可能触发额外的数据拷贝,需通过rknn.config中的optimization_level参数进行权衡。

常见误区:不要将动态维度设置为“无限制”。RKNN NPU的调度器通常对动态轴的范围有隐含约束(如batch必须是2的幂次方,分辨率需为16的倍数)。在导出模型时,应尽量通过dynamic_axes或Dim对象指定合理的范围,这有助于RKNN工具生成更高效的执行计划。

4.6 常见误区与总结复盘

误区 后果 正确做法
所有维度都设为动态 RKNN转换失败或推理性能急剧下降 仅将需要变化的维度(如batch、H、W)设为动态,通道数保持固定
忽略opset版本 动态shape信息丢失或算子不兼容 使用opset 11+,推荐12或13
导出后不验证 转换时才发现动态轴无效 使用onnxruntime进行多shape推理测试
动态轴命名随意 RKNN配置时难以对应 使用有意义的名称如'N','H','W',并在文档中记录
忽略算子兼容性 某些层在动态shape下报错 提前查阅RKNN算子支持列表,对不支持的算子进行替换或重构

总结本章,动态维度模型导出的核心在于“声明”与“验证”两个环节。声明阶段需要根据部署场景明确哪些轴必须可变,并选择合适的框架API进行标记;验证阶段则需通过onnxruntime或RKNN模拟器进行多shape测试,确保模型在目标范围内稳定运行。对于RKNN部署,建议在导出时就将动态轴的范围限制在硬件友好的数值(如batch为1、4、8,分辨率为16的倍数),这能显著降低NPU调度复杂度,提升实际推理效率。

经验之谈:如果项目初期无法确定动态范围,可以先导出全动态模型,在RKNN转换时通过input_size_list指定一个基准shape,然后使用rknn.accuracy_analysis检查不同shape下的精度损失。若精度波动过大,再考虑缩小动态范围或使用多模型策略。