Skip to content

30. Dynamic Shape Handling | 动态 Shape 处理

难度: Medium-Hard | 环境: CPU-first | 标签: 系统工程, 动态 Shape, 推理服务 | 目标人群: 编译与系统入门者

🚀 云端运行环境

本章节的实战代码可以点击以下链接在免费 GPU 算力平台上直接运行:

Open In ColabOpen In Studio (国内推荐:魔搭社区免费实例)


本节导读

输入长度一旦不固定,问题就不再只是 padding 多一点少一点,而是 batching、缓存组织、kernel 选择和执行路径都会一起变复杂。静态 shape 下顺手成立的很多假设,到了动态输入场景里都可能失效。

这一页在整个教程的纵向主线里属于 Part 01 的服务约束基础页,优先服务 推理优化路线 的请求编排与执行稳定性判断,也给后续 编译与图优化专题 补一层动态输入前置。学完这里,后面再看 34 / 37 / 66 以及服务侧调度相关的页时,你会更容易判断一个动态输入问题是在影响算子选择、缓存管理,还是整条服务执行链;如果这里没学明白,后面很容易把吞吐抖动简单归因到 batch 不稳定,却忽略 dynamic shape 已经同时改变了编译假设、缓存组织和调度路径。按专题归类,这一页主要属于 推理优化专题,并和 编译与图优化专题 共享一部分系统约束视角。

关键词: dynamic shape, batching, padding


前置阅读

导语: 这一页先把动态 batching、缓存复用和执行路径的关系接上,再看输入长度变化为什么会让系统更难稳定优化。

相关阅读

导语: 如果想继续把动态输入和后续的调度、虚拟化、成本判断串起来,可以接着看这些页。


Q1:动态 shape 为什么会让复用变差?

点击展开查看解析

固定 shape 时,batch 形状、缓存布局、kernel 形态和 graph 回放都更容易复用。

一旦输入长度不固定,这些复用都会变差:不同请求要重新分桶,padding 会引入无效计算,graph / kernel 复用率下降,执行路径也更容易分叉。

所以动态 shape 的难点,不是长度变化本身,而是它让原本能重复利用的优化结果变得不稳定。

### Q1小验证:固定假设为什么会失效

先记住固定 shape 下哪些结果能复用,再看动态输入会打断哪些复用。

python
def reuse_gap(shape_fixed=True, cached_batches=4, dynamic_lengths=1):
    # 复用是否成立,不是看一句 fixed / dynamic,而是看缓存和 kernel 能复用多少次。
    if not shape_fixed:
        return {'reuse_ratio': round(cached_batches / (cached_batches + dynamic_lengths), 2), 'stable': False}
    return {'reuse_ratio': 1.0, 'stable': True}

for case in [(True, 4, 1), (False, 4, 1), (False, 2, 4)]:
    print(case, '->', reuse_gap(*case))
print('dynamic shape hurts when cached shapes stop being reusable')

Q2:padding 和 bucketing 分别在减少什么浪费?

点击展开查看解析
  • padding 减少的是形状不一致带来的拼 batch 难题,它用填充把不同长度的输入拼成更规则的张量;
  • bucketing 减少的是无效 padding,它把相近长度的请求放到同一桶里,让浪费更少。

两者的目标都不是“消灭长度差异”,而是在吞吐、显存浪费和 kernel 稳定性之间找折中。

如果没有这层处理,动态 batching 很容易同时损失计算效率和资源利用率。

### Q2小验证:什么时候更适合 bucketing

长度分布越分散,bucketing 对减少 padding 浪费越有价值。

python
def padding_overhead(lengths, bucket_size=None):
    # padding 的代价在于把短序列补到同一长度时浪费了多少 token。
    if bucket_size is None:
        max_len = max(lengths)
        wasted = sum(max_len - l for l in lengths)
    else:
        wasted = sum((bucket_size - (l % bucket_size)) % bucket_size for l in lengths)
    return wasted

batches = [[64, 128, 256], [240, 248, 256], [512, 520, 800]]
for lengths in batches:
    print(lengths, '-> waste no bucket:', padding_overhead(lengths), 'bucket128:', padding_overhead(lengths, 128))
print('bucketing helps when the no-bucket waste is much larger than the bucket padding')

Q3:动态 shape 为什么会让性能更容易抖动?

点击展开查看解析

动态 shape 会同时影响两层决策:

  1. 执行路径:不同长度可能触发不同 kernel、不同 graph 或不同调度分支。
  2. 缓存策略:原本在固定形状下可复用的编译结果、块大小、缓存布局和调度参数,在动态场景里可能要按形状重新选择。

所以动态 shape 的本质,是把“编译期可确定的执行链”变成“依赖 runtime 形状判断的执行链”,性能也就更容易抖动。

### Q3小验证:为什么性能会抖动

长度变化导致路径和缓存都跟着变时,性能就更容易抖。

python
def path_stability(lengths):
    # 形状一变就换路径,说明 runtime 选择带来的抖动会更大。
    unique_paths = len(set('short' if l < 256 else 'long' if l < 1024 else 'very_long' for l in lengths))
    return {'unique_paths': unique_paths, 'stable': unique_paths == 1}

for lengths in [[128, 128, 128], [128, 512, 128], [128, 512, 2048]]:
    print(lengths, '->', path_stability(lengths))
print('more path variety means more performance jitter')

⚠️ 常见误区

  • 动态 shape 不是“只是输入长度不同”。
  • padding 能解决对齐,但不等于没有浪费。
  • bucketing 不是万能,只是减少浪费的办法之一。
  • 动态输入场景里,执行路径和缓存复用都可能变差。

Released under the MIT License.