1、动态形状问题概述

大家好,我是你们这堂课的主讲。咱们今天聊一个在模型推理里特别让人头疼的问题——动态形状。

说白了,动态形状就是模型输入数据的尺寸不是固定的。你想想看,一张图片可能是 224x224,也可能是 512x512;一句话可能是 10 个 token,也可能是 100 个 token。这在训练阶段还好说,大不了 padding 一下。但到了推理阶段,尤其是做线上服务的时候,这就成了个大麻烦。

什么是动态形状

动态形状,英文叫 Dynamic Shape,指的是模型输入张量的某些维度在运行时才能确定。比如:

  • Batch Size 动态:每次请求的图片数量不一样
  • 序列长度动态:NLP 模型里每个句子的长度不同
  • 图像尺寸动态:目标检测模型输入图片的宽高不固定

我举个例子你就明白了。假设你有一个 BERT 模型做文本分类,输入是 [batch_size, seq_len]。如果用户发来的是 "你好",那 seq_len 就是 2;如果发来的是 "今天天气真不错,我们出去走走吧",那 seq_len 就是 14。你看,这个 seq_len 就是动态的。

核心定义:动态形状 = 输入张量的某些维度在编译期未知,需要在运行时动态推导。

为什么动态形状是推理优化的难点

这个问题我深有体会。我在做模型部署的时候,遇到过好几次因为动态形状导致的性能翻车。说白了,推理框架最喜欢的是静态形状——所有维度都确定,内存分配、算子选择、计算图优化都能提前做好。

动态形状为什么难?主要有这几个原因:

  1. 内存分配没法提前做:静态形状下,推理框架可以一次性分配好所有中间张量的内存。动态形状呢?每次输入尺寸不一样,内存得动态申请,这本身就慢。
  2. 算子选择困难:很多算子对输入尺寸有最优实现。比如矩阵乘法,小矩阵和大矩阵用的算法不一样。动态形状下,框架得在运行时做决策,这就有开销。
  3. 计算图优化受限:像算子融合、常量折叠这些优化,在动态形状下很多都做不了。因为优化器不知道张量的具体大小,没法做精确的代价模型。
  4. JIT 编译失效:很多推理框架会用 JIT 编译来加速,比如 TensorRT 的 engine 编译。但动态形状意味着每次输入都可能触发重新编译,这开销你想想看。

避坑指南:我曾经在一个线上服务里用了动态 batch,结果发现每次请求的 batch size 都不一样,导致推理框架频繁重新编译 engine,延迟从 5ms 飙到了 200ms。后来我强制做了 batch 对齐,问题才解决。

常见动态形状场景

动态形状不是凭空出现的,它背后有真实的应用场景。我列几个最常见的:

场景 动态维度 典型模型
NLP 文本分类 序列长度 BERT、GPT、T5
目标检测 图像宽高 YOLO、Faster R-CNN
语音识别 音频帧数 Whisper、Conformer
推荐系统 特征数量 DIN、DIEN
视频理解 视频帧数 VideoMAE、I3D

嗯,这里要注意一点。动态形状和可变形状其实有区别。可变形状是输入尺寸在一个小范围内变化,比如 batch size 只能是 1、2、4、8。动态形状则是完全不确定,比如序列长度从 1 到 512 都有可能。后者更难优化。

个人经验:我建议你在设计模型服务时,尽量把动态形状转化为可变形状。比如限制序列长度范围,或者做 padding 到固定长度。虽然会浪费一点计算,但换来的是推理框架能更好地优化。

为什么会这样?说白了,推理框架对「有限种可能」的优化能力远强于「无限种可能」。你想想看,如果框架知道输入尺寸只能是 1、2、4、8,它可以提前编译好 4 个 engine,运行时直接选一个用。但如果尺寸是 1 到 512 任意整数,那就只能走动态路径了。

好了,这一节的内容就到这里。动态形状是个大话题,后面我们会深入讲怎么处理它。下一节我会聊聊动态形状对算子性能的具体影响,以及怎么用 profiling 工具来定位问题。