Py学习  »  机器学习算法

深入解析深度学习框架动态Shape处理:从符号推导到内存规划

ai算法芯片与系统 • 昨天 • 10 次点击  

 

摘要:本文深入探讨深度学习推理引擎处理动态张量形状的核心机制。动态Shape已成为现代深度学习模型的常态,尤其在自然语言处理和计算机视觉领域广泛应用。文章首先聚焦NLP和CV中的典型动态场景,然后介绍在实际部署中如何为动态维度指定约束以优化性能。接着系统阐述符号推导的基本概念与原理,通过与数值计算的对比阐明符号表达式的意义。随后详细拆解主流推理引擎(如ONNX Runtime、TensorRT等)如何通过符号化表示、形状推导和内存分配三大环节应对动态性。重点阐述符号生成与部分数据传播机制,并探讨静态与动态结合的优化策略——包括按最大形状预分配内存的实用方案,以及如何利用符号比较生成带有符号表达式的静态内存规划。通过列表、表格和图示,帮助读者全面理解动态Shape推理的技术全景。

目录

  • • 📖 1. 引言
  • • 🎯 2. 动态Shape在NLP和CV中的典型应用
  • • 📦 3. 动态Shape模型的部署方法:约束指定与配置
  • • 🔍 4. 符号推导的概念与原理
  • • ⚙️ 5. 动态Shape处理核心机制
  • • 💡 6. 静态与动态结合的优化策略
  • • ⚖️ 7. 与其他框架的对比
  • • 🔮 8. 未来展望与总结

📖 1. 引言

传统的深度学习推理往往基于静态图假设:所有张量的形状在编译时完全已知,执行路径固定不变。

例如,一个图像分类模型可能固定输入为  的RGB图像,所有中间张量的尺寸都能在编译时精确计算。

然而,这一假设正被日益普及的动态模型所打破。

动态Shape模型的核心特征在于:张量的形状、尺寸乃至执行路径都可能随输入数据变化

例如,自然语言处理中的变长序列、计算机视觉中的多分辨率图像处理,都属于典型的动态场景。

动态性给推理优化带来严峻挑战。

在静态模型中行之有效的技术——如算子融合(依赖相同迭代空间)、执行顺序规划(需知张量大小)和内存复用(需确定生命周期)——在动态Shape下难以直接应用。

现有的推理引擎虽能支持动态模型,但常因过于保守的假设或运行时昂贵分析而产生高昂开销。

例如,若每次都重新分配内存,会导致性能下降和内存碎片。

本文将聚焦于主流推理引擎(如ONNX Runtime、TensorRT等)处理动态Shape的内部机制,并探讨符号化静态规划的可能路径。

特别是按最大形状预分配内存这一实用策略的可行性及其局限。

🎯 2. 动态Shape在NLP和CV中的典型应用

动态Shape并非理论上的特例,而是广泛存在于各类实际应用中。本节重点分析自然语言处理和计算机视觉领域的典型场景。

2.1 自然语言处理中的变长序列

NLP模型(如BERT、GPT系列)处理的输入句子长度天然可变。

例如,Wikipedia数据集的序列长度通常在32到512之间波动。

若将所有输入填充(padding)至固定最大长度,将导致大量无效计算和内存浪费。

  • • BERT模型:输入由 token_idsattention_mask 和 token_type_ids 组成。

    其中  attention_mask 用于指示哪些位置是有效token(而非填充)。

    在动态Shape支持下,模型可以仅计算有效部分的注意力,显著提升效率。

    例如,对于长度为  的序列,注意力矩阵的大小为 

    若填充至 ,则矩阵大小为 ,计算量随  增长。

  • • GPT系列:自回归生成时,每次迭代只生成一个token,序列长度逐步增加。

    若固定最大长度,需预先分配最大内存,造成浪费;动态Shape则允许逐次扩展。

  • • 推理服务:云端同时处理多个请求,每个请求的句子长度不同。

    若使用固定batch size和固定长度,需将所有句子填充到批次中最长句子的长度,导致计算浪费;动态batch和动态长度可减少填充。

下表展示了变长序列处理中动态Shape的优化效果(基于BERT-base,序列长度分布为32~512,平均128):

策略内存占用计算量适用场景
固定长度
512
100%
简单实现,
但资源浪
动态长度
(每样本)
约25%
批量推理,
需动态内
动态batch+
动态长度
最优
最优
高性能服务

2.2 计算机视觉中的多分辨率输入

图像/视频处理任务常面临分辨率变化。例如:

  • • 移动端应用:相机输入分辨率随网络状况和用户设置动态调整,模型需适应不同大小的输入。
  • • 目标检测:模型如YOLO系列需处理不同尺寸的输入图像,以适应不同尺度的目标。

    YOLOv5支持在推理时动态调整输入尺寸(如从  到 ),只需在预处理时 resize

  • • 超分辨率/图像生成:Stable Diffusion等生成模型可生成可变分辨率输出(如从  到 ),用户可自由指定。
  • •  视频分析:视频帧分辨率可能变化(如从480p切换到720p),模型需无缝切换。

动态分辨率带来的好处:

  • • 避免图像缩放造成的信息损失(如拉伸失真)。
  • • 根据设备能力选择合适分辨率(低端设备用小图,高端用大图)。
  • • 支持任意宽高比,适应不同显示设备。

下表列出了常见CV模型对动态分辨率的支持情况:

模型输入尺寸
范围
处理方式备注
YOLOv5
预处理
resize + 
动态Shape
推理
输出特征
图尺寸相
应变化
Efficient
Det
任意
动态Shape
 + 自适应
锚框
需调整锚
框生成逻
Stable 
Diffusio
n
64的倍数
动态Shape
 + 位置编
码插值
需调整UN
et中的位
置编码

📦 3. 动态Shape模型的部署方法:约束指定与配置

在实际部署动态Shape模型时,推理引擎通常需要用户提供关于动态维度的约束信息,以便在编译或初始化阶段进行优化。

这些约束可以是范围(如最小值、最大值)或枚举值集合(如允许的几种尺寸)。

提供约束有助于引擎进行内存预分配、内核选择、执行计划生成等优化,从而在运行时获得接近静态模型的性能。

3.1 常见框架的约束指定方式

不同推理框架和硬件后端提供了各自的方式来描述动态维度的约束。

框架/后端
约束指定方式
示例
TensorRT
优化配置文件
(Optimization Profile),
为每个动态维度指定
minoptmax
三个值
input: [min=1, opt=4, max=8]
ONNX Runtime
通过配置参数覆盖
动态维度为具体值(非范围),
或借助硬件后端(如TensorRT EP)
间接支持范围
options.free_dimension_overrides = {"batch": 4}
Ascend CANN
配置文件中的 
dynamic_dims 参数,
指定枚举值列表
dynamic_dims=[1,2,4,8]
OpenVINO
使用 reshape 方法
设置新的输入形状,
可部分指定动态维度
model.reshape({"input": PartialShape([-1, 3, -1, -1])})
PyTorch Executorch
通过 ValueRange
 标注符号变量的取值范围
dim_order = DimOrder("batch", value_range=(1,8))

3.2 为什么要指定约束?

  • • 内存规划:知道最大尺寸后,可以预分配足够大的内存池,避免运行时动态分配。
  • • 内核选择:编译器可根据常见尺寸(opt)选择最优的kernel实现,同时确保覆盖整个范围。
  • • 执行计划生成:在动态范围内,引擎可以提前计算所有中间张量的最大大小,并生成静态的内存偏移量(如TensorRT的做法)。
  • • 硬件限制:某些硬件加速器(如NPU)要求输入尺寸固定,通过约束可将动态模型转换为多个静态模型(多档),运行时根据实际尺寸选择。

3.3 实际部署流程示例(TensorRT)

以TensorRT部署动态batch的BERT模型为例:

  1. 1. 导出ONNX模型,将batch维度标记为动态(如 -1)。
  2. 2. 创建TensorRT构建器,定义优化配置文件:
    profile = builder.create_optimization_profile()
    profile.set_shape("input_ids", (1, 128), ( 4, 128), (8, 128))
    profile.set_shape("attention_mask", (1, 128), (4, 128), (8, 128))
    config.add_optimization_profile(profile)
  3. 3. 构建引擎,TensorRT会根据 min=1opt=4max=8 进行内存规划和内核调优。
  4. 4. 运行时,通过 context.set_binding_shape 设置实际的batch大小(如5),引擎直接使用预计算的偏移量执行,无动态分配开销。

3.4 约束与符号推导的关系

约束信息本质上是对符号变量的取值范围限定

例如,符号  表示batch size,约束  就为符号推导提供了边界,使得符号比较(如  与  谁大)可以在编译期确定,从而支持更精确的静态内存规划。

在后续的符号推导章节中,我们将看到这些约束如何与符号表达式结合,实现带符号的静态内存分配方案。

🔍 4. 符号推导的概念与原理

符号推导是处理动态Shape的核心技术。本节将详细解释符号变量、符号表达式,并通过与普通数值计算的对比,阐明其独特之处。

4.1 符号变量与符号表达式

符号变量是一个代表未知整数的占位符,通常用字母表示,如 

它们没有具体值,但可以带有约束(如  是整数)。

符号变量的引入使得我们可以在编译期描述形状的不确定性。

符号表达式是由符号变量、常数和运算符(加、减、乘、除、取模、最大值等)构成的数学表达式。

例如,经过一个步长为2的卷积后,输出高度可表示为:



如果  是符号变量,则  也是一个符号表达式。

4.2 符号推导 vs 数值计算

普通数值计算是在具体数值上进行的,结果也是具体数值。

而符号推导是在符号表达式上进行的,结果仍然是符号表达式。

两者的根本区别在于:数值计算需要具体输入才能得到具体输出,符号推导则可以在没有具体输入的情况下,推导出输出形状的表达式

考虑一个简单的计算图:输入  形状为 [N, 3, 224, 224],经过一个卷积层(核大小3,步长1,填充1)后,输出形状应为 [N, 64, 224, 224]

在数值计算中,如果我们知道 ,我们可以计算出输出形状为 [4, 64, 224, 224]

但在符号推导中,我们不知道  的具体值,我们只能推导出输出形状为 [N, 64, 224, 224],其中  仍然是符号变量。

更复杂的例子:假设有两个输入  和 ,形状分别为 [M, 256] 和 [N, 256],经过 Concat 操作(axis=0)后,输出形状为 [M+N, 256]

符号推导得到的表达式  保留了输入符号的关系,而数值计算则会给出具体的和。

下图用不同颜色标注了符号变量与具体数值在推导过程中的流动:

4.3 符号传播的规则

符号传播依赖于每个算子的形状推导函数。

这些函数定义如何根据输入形状(可能包含符号)计算输出形状。例如:

  • • Conv:输出空间维度是输入空间维度的线性函数(取决于核大小、步长、填充)。
  • • Add:要求两个输入形状相同,输出形状与输入相同。
  • • MatMul:输出形状为 [M, K],其中  来自第一个输入的行数, 来自第二个输入的列数。
  • • Reshape:需要根据目标形状表达式求解,可能涉及乘法关系。

对于 Reshape,假设输入形状为 [A, B],目标形状为 [X, Y],且要求 

如果  是符号变量 ,则  可以推导为 ,前提是  能整除 

4.4 部分数据传播

除了形状本身,某些算子的输出值也可能用于后续的形状计算。

例如,Shape 算子输出一个一维张量,包含输入张量的形状值。

如果这些值被后续的 GatherSlice 等算子使用,就需要传播这些值(即使它们还是符号)。

部分数据传播 就是指在编译期尽可能计算这些参与形状计算的张量数据,即使结果是符号表达式。

例如,考虑以下子图:




    
input: [N, 256]
shape = Shape(input)   # 输出 [N, 256] 的形状张量 [2]
dim = Gather(shape, indices=[0])  # 提取第一个维度,得到 [N]
output = Reshape(input, shape=[dim, 256])  # 实际上形状不变

通过部分数据传播,我们可以知道 dim 就是符号 ,从而 Reshape 的输出形状也是 [N, 256]

4.5 符号约束与取值范围

符号变量往往带有取值范围,这些约束可以从模型输入定义、用户配置或数据集中获得。

例如,NLP模型可能规定序列长度不超过512,即 ;图像模型可能要求输入尺寸是32的倍数,即 

这些约束对于符号比较和内存规划至关重要。

4.6 符号计算库(如SymPy)的作用

在实际实现符号推导系统时,可以借助现成的符号计算库来简化开发。

例如,SymPy 是Python的一个符号数学库,能够进行符号表达式的解析、简化、代入、比较等操作。

假设我们需要在编译器中对形状表达式  和  进行大小比较,并已知 ,可以用SymPy验证:

from sympy import symbols, Interval, S
N = symbols('N', integer=True)
expr1 = 2*N + 3
expr2 = N + 5
# 判断 expr1 >= expr2 是否在区间内恒成立

ineq = expr1 >= expr2
# 化简得到 N >= 2

# 检查区间 [1,10] 是否包含于解集

solution = ineq.as_set()
interval = Interval(1, 10)
is_true = interval.is_subset(solution)  # 返回 False,因为 N=1 时不成立

虽然主流推理引擎未必直接使用SymPy,但符号推导的原理与SymPy类似:将形状视为符号表达式,并通过代数规则进行传播和比较。

⚙️ 5. 动态Shape处理核心机制

本节以主流推理引擎(如ONNX Runtime、TensorRT等)为例,剖析其处理动态Shape的核心机制。核心哲学是:将“符号化”的模型描述与“具体化”的运行时执行分离

引擎不在编译期固定所有维度,而是在推理时根据真实输入动态推导、分配资源,或者通过编译期符号化规划实现运行时零开销。

5.1 符号维度表示法

在模型中间表示(如ONNX)中,动态维度通过两种方式标记:

  • • 符号参数:为动态维度命名,如将BatchSize定义为变量 "batch"

    这允许不同层之间的维度建立关联。例如,输入和输出的batch维度都表示为 "batch",表明它们始终相等。

  • • 匿名维度:直接用 -1 或 "?" 表示维度可变但未命名。

    这种方式无法建立维度间的关系,但足以表示形状可变性。

一些先进的推理引擎(如ONNX Runtime 1.10+)引入了符号生成机制,使得匿名维度也可以在传播过程中被赋予符号,从而建立关系。

例如,遇到 Concat 的两个输入分别为 [?, 256] 和 [?, 256],符号生成机制会为第一个 ? 生成符号 ,第二个生成符号 ,然后输出形状为 

5.2 形状推导(Shape Inference)流程

当输入具体数据(如Batch=4)后,推理引擎启动动态形状推导。

整个过程可分为节点级和图级两个层面:

节点级形状推导:每个算子(Op)都定义了自身的形状推导函数,根据输入形状计算输出形状。

推导函数的输入可能是具体数值,也可能是符号表达式。

例如,Concat 算子的推导函数会合并各输入在指定维度上的大小。

图级形状推导:高层逻辑遍历整个计算图,依次调用节点级推导函数,并将结果传递给下游节点。

若遇到无法完全确定的维度(如 Concat 的两个输入分别为  和 ),图级推导会生成新符号(如 )并继续传播,同时记录符号间的关系(如 )。

这一流程确保了整个计算图的形状在运行时变得“具体”,为后续的内存分配和执行奠定基础。

5.3 内存分配策略

基于推导出的具体形状,推理引擎通常采用分层内存分配策略。

5.3.1 静态部分内存分配

权重、常量等固定数据初始化时预分配

这部分内存不会随输入变化,可一次性分配到位。

例如,卷积层的卷积核参数、批归一化层的均值和方差,这些数据在模型加载后固定不变,因此可以在创建模型实例时分配并驻留。

5.3.2 动态部分内存分配

对于输入、输出和中间激活值,采用延迟分配策略:

  1. 1. 在执行到具体节点前,根据刚算出的具体形状动态计算所需内存大小 
  2. 2. 从内存池中申请相应大小的内存块(若池中有足够大且空闲的块,则复用)。
  3. 3. 执行计算,将结果写入分配的内存。
  4. 4. 张量生命周期结束后,内存归还池中。

详细示例:考虑一个BERT模型推理,输入序列长度  在运行时确定为128。

形状推导引擎计算出各个中间张量的具体形状:

  • • 嵌入层输出:[128, 768],大小  字节(假设float32)= 393,216 字节。
  • • 第一个注意力层的Q、K、V投影:各自大小也为 [128, 768],但生命周期不同。
  • • 注意力输出:[128, 768]

当执行到第一个投影时,内存分配器检查内存池中是否有足够大的空闲块。

由于这是第一个动态分配,池为空,因此向系统申请393,216字节,并返回地址。

执行完投影后,该张量不再使用,内存归还池中。

后续的K、V投影和注意力输出可能复用同一块内存,因为它们大小相同且生命周期不重叠。

这种策略避免了为未知形状预留过大内存造成的浪费,但相比纯静态模型会有少量运行时分配开销。

动态Shape内存分配时序

💡 6. 静态与动态结合的优化策略

用户观察到的关键点在于:即使形状是动态的,某些比较(如  一定比  大)依然可以在编译期进行,从而生成带有符号的内存规划方案。

当前主流引擎虽未完全实现这一理想,但确实包含部分相关机制,并且可以通过静态与动态结合的方式进一步优化。

6.1 符号化静态规划的可行性

符号化静态规划的核心思想是:在编译阶段,使用符号表达式描述每个张量所需的内存大小和生命周期,然后进行符号化的内存复用分析,生成一个带有符号表达式的内存布局方案。

运行时只需代入符号的具体值,即可快速确定每个张量的偏移地址。

这一方案依赖于两个前提:

  1. 1. 符号传播的准确性:能够推导出所有中间张量大小的符号表达式。
  2. 2. 符号比较的能力:能够判断两个表达式的大小关系,以决定内存是否可以复用。

在常见模型中,许多算子的输出形状都可以表示为输入形状的线性表达式(如卷积、池化、全连接),这为符号传播提供了良好基础。

对于非线性操作(如 Reshape),可能需要求解方程,但通常仍可得到符号表达式。

符号比较可以通过数学性质或约束求解实现。例如:

  • • 若表达式均为多项式且系数非负,则大小关系可由系数和常数比较得出。

    例如,对于  和 ,要判断  对所有  是否成立,可化简为 ,因此并非总是成立,需要结合  的范围。

  • • 若符号变量有明确范围(如从配置中读取),则可在此范围内验证不等式是否恒成立。

6.2 符号比较在内存分配算法中的应用

经典的内存分配算法(如 "Efficient Memory Management for Deep Neural Net Inference" 中提出的算法)需要比较张量大小以决定内存复用。

该算法通过分析张量的生命周期,将不重叠的张量分配到同一内存区域,从而最小化总内存使用。

在静态形状下,大小是具体数值,比较直接;在动态形状下,如果大小是符号表达式,仍然可以进行符号比较

6.2.1 符号比较的基本原理

假设两个张量  和 ,其大小分别为  和 ,且生命周期不重叠。

那么我们可以确定  始终成立(对于 )。

因此可以分配一块大小为  的内存,并让  和  分时复用。

这样,内存总需求就是 ,而不是 

这个分配方案本身带有符号 ,运行时只需代入  的值即可计算出实际偏移量。

6.2.2 应用于内存复用的示例

考虑以下三个张量,它们的生命周期不重叠,大小分别为:

  • • 
  • • 
  • • 

假设  的取值范围为 

我们需要确定能否通过符号比较找到一块足以容纳三者中最大者的内存,让三者复用。

首先,分析两两之间的大小关系:

  • • 比较  和 ,即当   时 ;当  时 
  • • 比较  和 
  • • 比较  和 

因此,最大值表达式为:


如果  的范围事先已知,我们可以直接使用这个分段表达式作为内存大小。

运行时根据实际  值选择对应分支计算偏移量。

这仍然是静态计划,只是带有条件分支。

如果符号比较能够确定某个表达式恒大于等于其他表达式(如  恒大于 ),则无需分支,直接使用该表达式即可。

6.3 中间张量形状确定时的静态分配

在动态Shape模型中,并非所有中间张量都是动态的。

有些张量尽管输入是动态的,但其形状可能变为常数。例如:

  • • 经过全局平均池化后,空间维度变为1。
  • • 经过 Flatten 后,如果输入通道数是固定的,则输出大小固定。
  • • 某些 Reshape 操作可能将动态维度与固定维度合并,产生新的动态表达式,但其他维度固定。

对于这些形状确定的中间张量,我们可以静态分配内存,即将其纳入预分配内存池中,而不必在运行时动态申请。

这可以减少运行时分配次数,提高缓存局部性。

一些推理引擎(如ONNX Runtime)提供了内存模式优化(Memory Pattern Optimization),通过热身运行记录张量大小和生命周期,生成一个针对该具体形状的静态模式。

但该模式依赖于热身时使用的具体形状,一旦形状变化,模式失效。

理想情况下,如果能通过符号推导判断某些张量在所有可能输入下形状都相同,就可以直接生成不依赖于具体值的静态分配方案。

6.4 按最大形状预分配内存的实用策略

在实际部署中,一种简单而有效的静态分配策略是:按照可能出现的最大输入形状预分配内存,然后让所有较小形状的推理共享这份内存

这种策略的出发点是:对于大多数模型,中间张量的大小通常随输入形状单调增长。

例如,卷积层的输出特征图尺寸一般随输入尺寸增大而增大;全连接层的输出大小与batch size成正比。

因此,如果按最大输入形状分配内存,那么对于任何较小的输入,所需内存都不会超过已分配的大小,从而可以安全复用。

6.4.1 详细例子:NLP变长序列服务

假设一个BERT-based意图分类服务需要处理用户查询,句子长度在32到512之间变化。

我们预先分析模型:所有中间张量的大小都是输入长度  的单调递增函数(如注意力矩阵大小 ,全连接输出大小  等)。

因此,我们可以:

  1. 1. 在服务启动时,按最大长度  预分配所有中间张量所需的内存块。

    例如,注意力矩阵需要  字节 ≈ 1MB。

  2. 2. 当实际请求到来,长度为  时,我们仍然使用已分配的1MB内存块,但只使用其中的前  部分(通过偏移量计算)。
  3. 3. 所有中间张量都复用这些预分配的内存块,运行时零动态分配。

优点

  • • 运行时零动态分配,性能最佳。
  • • 实现简单,无需复杂的符号比较。

缺点

  • • 内存利用率可能不高,尤其当最大形状远大于常见形状时。

    例如,若大部分请求长度仅为32,则内存浪费严重。

  • • 需要预先知道最大形状的范围,这通常由业务场景决定(如最大句子长度、最大图像尺寸)。
  • • 存在反例:某些算子的输出大小可能随输入形状非单调变化。

6.4.2 反例分析

考虑一个模型,输入形状为 [N, 10],经过一个 Gather 操作,根据输入数据动态选择部分行。

Gather 的输出形状为 [k, 10],其中  取决于数据内容,可能与输入  无直接单调关系。

例如,模型可能根据某种条件只保留满足条件的行,当  较大时, 可能反而较小(如果满足条件的行少)。

在这种情况下,最大输入  对应的  未必最大。

因此,按最大输入形状预分配可能无法覆盖所有情况,需要结合符号推导进行分析。

尽管如此,对于许多实际场景(如NLP的变长序列、CV的变分辨率),中间张量大小通常是输入大小的单调函数,因此按最大形状预分配是一种行之有效的优化手段。

6.5 静态与动态结合的混合规划

结合以上讨论,我们可以设计一种混合静态-动态内存规划方案:

  1. 1. 符号推导阶段:通过符号传播,得到每个张量大小的符号表达式。
  2. 2. 分类阶段
  • • 对于表达式可化简为常数的张量,标记为静态张量,直接分配固定内存。
  • • 对于表达式为符号的张量,进一步分析其单调性。

    如果符号表达式是输入符号的单调递增函数(例如线性函数且系数为正),则可按最大输入形状计算其最大可能大小,并预先分配。

  • • 对于非单调或无法确定最大值的张量,保留动态分配。
  • 3. 生成混合计划:最终的内存布局包含静态区域、按最大预分配区域和动态区域。

    运行时,动态区域仍采用延迟分配,但静态和预分配区域无需调整。

  • 4. 运行时:对于预分配区域,只需在首次使用时根据实际形状计算偏移量(可能涉及符号代入),但内存块本身已经固定。
  • 这种方案结合了静态分配的效率和动态形状的灵活性,是未来推理引擎优化的重要方向。

    6.6 主流引擎的当前能力与局限

    以ONNX Runtime为例,其内存模式优化可以看作是一种有限的静态规划:它针对特定形状生成静态模式,但无法处理形状变化。

    符号生成和部分数据传播的引入,为更高级的静态规划奠定了基础。

    目前,主流引擎尚未实现完整的符号化内存复用,但通过硬件后端(如TensorRT)可以获得类似能力。

    用户也可以通过配置将动态维度临时覆盖为具体值,从而将动态模型转为静态,但这需要为每个输入形状单独创建模型实例,不够灵活。

    ⚖️ 7. 与其他框架的对比

    将ONNX Runtime与其他框架对比,有助于理解其在动态Shape处理生态中的定位。

    框架表示方式推导机制内存规划需要用户
    指定范围
    ONNX 
    Runtime
    符号/匿名
    运行时具
    体化
    动态分配+
    内存模式
    优化
    可选(覆
    盖动态维
    度)
    Tensor
    RT
    符号+范
    编译期符
    号化
    预分配+
    偏移量计
    是(优化
    配置)
    PyTorc
    h Execut
    orch
    符号+范
    编译期上
    限特化
    贪心算法+
    自定义区
    可选(ValueRange
    SoD2
    (学术)
    符号表达
    静态RDP
    分析
    符号化静
    态计划
    否(自动
    推导范围
    Ascen
    d CANN
    符号+枚
    举值
    编译期多
    档生成
    预分配多
    档内存
    是(dynamic_dims

    从上表可以看出,ONNX Runtime的核心设计偏向于运行时灵活性,将动态Shape的复杂性推迟到执行阶段,通过高效的形状推导和内存池来应对。

    而TensorRT等编译型后端则倾向于在编译期尽可能多地静态规划,换取运行时零开销。

    两者各有优劣:ONNX Runtime支持任意动态形状,无需预知范围;而TensorRT在已知范围内能提供更优的性能。

    🔮 8. 未来展望与总结

    动态Shape处理技术仍在快速发展。

    结合符号推导和静态规划的混合方法有望成为主流。

    未来主流推理引擎可能在以下方面演进:

    1. 1. 增强符号推理能力:在核心中引入更强大的符号表达式系统,支持表达式的生成、简化和比较,不仅局限于符号生成。

      这将为静态内存规划提供基础。

    2. 2. 范围信息的一等支持:目前范围信息主要通过厂商特定配置传入,未来模型标准(如ONNX)可能原生支持维度范围标注,让所有后端都能利用这一信息进行优化。
    3. 3. 部分静态规划:对于可静态确定的部分(如某些中间张量形状固定),提前规划内存布局;对于完全动态的部分,保留运行时灵活性。

      这可以通过混合编译+解释的执行模式实现。

    4. 4. 单调性分析:自动识别张量大小与输入符号的单调关系,从而支持按最大形状预分配,同时检测反例并采取相应措施。
    5. 5. 与编译器生态融合:借鉴TVM、MLIR等编译器基础设施的符号化优化能力,将模型转换为带符号的中间表示,进行联合优化。

    总结:主流推理引擎处理动态Shape的机制体现了务实的设计哲学:在不引入过度复杂性的前提下,通过符号生成、部分数据传播和延迟分配等策略,为动态模型提供灵活而高效的推理支持。

    同时,通过内存模式优化和各类硬件后端的后端能力,实现了静态与动态结合的优化路径。

    按最大形状预分配内存作为一种实用策略,在多数场景下有效,但需要警惕非单调反例。

    理解这些机制,有助于开发者根据自身应用特点选择合适的动态Shape处理策略,构建高效的推理系统。


    参考文献

    1. 1. ONNX Proposal - Symbolic Shape Inference And Partial Data Propagation
    2. 2. SoD2: Statically Optimizing Dynamic Deep Neural Network Execution (ASPLOS‘24)
    3. 3. NVIDIA TensorRT Developer Guide
    4. 4. ONNX Runtime Documentation
    5. 5. PyTorch Executorch Memory Planning Documentation
    6. 6. "Efficient Memory Management for Deep Neural Net Inference" (相关算法讨论)

     


    Python社区是高质量的Python/Django开发社区
    本文地址:http://www.python88.com/topic/193490