如果把 TensorRT 引擎比作一家湘菜馆,那么“固定 Shape”时代的后厨只认一种桌型:八人桌、八人套餐。两位客人来,要么拼桌要么请走;六位客人来,也得按八人份备菜,多出的两份倒掉。真实世界显然不这么听话——NLP 的句子有时三五字,有时上百字。于是 TensorRT 推出动态 Shape(Dynamic Shape),相当于把餐馆改造成灵活包间:订餐系统(Optimization Profile,优化画像)预先记录“最小 2 人、最优 8 人、最大 20 人”三档桌型,客人到店报出实际人数(运行时输入维度),后厨按人数挑锅(内核选择),服务员按订单上菜(执行推理)。本文就带你走进这家“动态包间”的厨房,看清每一道流程。
学懂动态 Shape,你就能把 NLP 这类输入长度千变万化的模型高效部署上线,让推理服务在“灵活”与“性能”之间不再二选一——这是推理引擎工程师的核心技能之一。
可视化演示
演示TensorRT动态Shape机制:Optimization Profile展示min/opt/max三组维度约束;batch动态演示批量维度运行时变化;内核选择演示引擎按实际输入形状动态挑选最优执行策略。
本节配有交互动效(进入视口后自动加载,约 3KB)。
Optimization Profile界定 min/opt/max 三组形状 opt=最频繁尺寸 运行时按实际输入动态调整执行策略
固定Shape痛点
老张的湘菜馆开业三年,菜单上只有一道“八人套餐”。不是他不想接待两人约会,而是厨房只按八人份备菜备锅——这就是固定 Shape 的宿命:构建引擎(engine)时,输入张量的每一个维度都必须写死成具体数字,比如图像模型固定 224×224×3,NLP 模型固定 batch×96×768。
写死维度带来两个问题。第一,输入稍不留神就不匹配,引擎直接报错;第二,为了让变长输入“匹配”,只能做填充(padding)——把短句子硬补到最长的 96。假设一次请求里真实句子长度是 \(L_{\text{actual}}\),填充后固定为 \(L_{\text{padded}}\),计算浪费可以精确刻画:
六人桌摆八份菜,后厨炒了没人吃,还占着灶台。线上短文本占多数时,这种浪费会直接变成 GPU 账单上的数字。改,还是不改?答案是后者。
- 真实长度\(L_{\text{actual}}\) 表示本次输入序列的真实长度,例如一条只有 12 个字的客服问题。
- 填充长度\(L_{\text{padded}}\) 是固定 Shape 下必须补到的长度,通常取数据集最大值。
- 浪费率\(1 - \frac{L_{\text{actual}}}{L_{\text{padded}}}\) 计算被填充空白“白跑”的比例。
动态Shape概念
老张下了决心,挂出招牌:“本店承接 2 到 20 人宴席”。桌子还是那些桌子,但包间能伸缩——这就从固定 Shape 迈入了动态 Shape。
动态 Shape 不再锁死输入维度,而是声明一个允许的区间。TensorRT 构建引擎时知道“长度可能在 8 到 96 之间”,但不会为 8、9、10……96 每个长度各编译一遍引擎。在 TensorRT 中,把某个维度设成 -1 就表示“这一维是动态的”,公式化表达:
改造后的餐厅不用为每种人数单独装修,而是一套能伸能缩的桌椅。动态 Shape 的引擎也正是这样:它只编译一次,却能在运行时拥抱各种输入。
- 动态标记\(d_i = -1\) 表示张量第 \(i\) 维不是固定数字,而是交给运行时决定。
- 区间约束\(\Longleftrightarrow\) 表示这一标记等价于一组范围限制,而非任意取值。
- 上界收口\(D_i \le D_{\max,i}\) 防止动态维度无限膨胀,确保资源可预估。
优化画像设定
光说“能接待 2 到 20 人”不够,后厨得知道每一档怎么备货。老张在订餐系统里记下三档:至少 2 人、最常见 8 人、最多 20 人。这三档就是 TensorRT 的优化画像(Optimization Profile)。
每个优化画像由最小维度(KDT_MIN)、最优维度(KDT_OPT)、最大维度(KDT_MAX)组成,开发者调用 setDimensions 依次填写。最优维度通常设为线上最常出现的输入长度,它是后续内核选择和内存规划的“锚点”:
一个引擎还可以同时持有多个画像,比如“日常档”和“高峰档”,为不同负载模式各备一套预案。三档桌型像三份预案,后厨既不盲目铺张,也不会措手不及。
- 画像集合\(\mathcal{P}_k\) 是第 \(k\) 个 Optimization Profile,本质是三个维度值组成的有序组。
- 下界排序\(D_{\min} \le D_{\text{opt}}\) 保证最优维度不低于最小维度。
- 上界排序\(D_{\text{opt}} \le D_{\max}\) 保证最优维度不超过最大维度,整个三元组递增合法。
维度绑定
客人订了包间,前台要登记“哪个包间可调桌型”,并把订单和桌型预案绑在一起。TensorRT 里这个动作叫维度绑定(binding)。
网络定义阶段,调用输入张量的标记接口把动态维度写成 -1;引擎序列化时不知道具体数字,只知道“这几个维度是可变的”。执行时创建执行上下文(ExecutionContext),上下文绑定某个画像,由画像指导内存分配与内核调度。用集合描述哪些维度是动态的:
引擎是总厨,平时只管设计和菜谱;执行上下文才是值班领班——手里握着画像预案,客人报人数时才能指挥后厨。没有领班,总厨再厉害也开不了席。
- 动态下标集\(\mathbb{I}_{\text{dyn}}\) 收集所有被标记为 -1 的维度下标。
- 非空条件\(\left| \mathbb{I}_{\text{dyn}} \right| \ge 1\) 表明至少一个动态维度,否则就是固定 Shape。
- 绑定动作执行上下文在创建时与某个 \(\mathcal{P}_k\) 绑定,之后该上下文的所有推理都遵循这份画像。
运行时推理
客人到店报实际人数,前台核对“在 2 到 20 人范围吗?”,确认后才叫后厨下锅。每次推理都走这套“先验菜、后下锅”的流程。
运行时调用 setBindingDimensions 设置本次输入的真实形状,TensorRT 会检查每个动态维度是否落在绑定画像的区间内,越界则返回错误。设置完成后调用 enqueueV2 真正执行。合法性校验可写成:
报错人数就退单,报对人数才上菜。这样一单一 check,既灵活又安全,是动态 Shape 可靠运行的底线。
- 实际维度\(D_{\text{actual},i}\) 是本次请求在第 \(i\) 个动态维度的真实取值。
- 逐维校验\(\forall\, i \in \mathbb{I}_{\text{dyn}}\) 表示对所有动态维度逐一检查,缺一不可。
- 双端夹逼\(D_{\min,i} \le D_{\text{actual},i} \le D_{\max,i}\) 同时校验下界与上界。
内核选择机制
后厨不会为 2 到 96 人各备一口锅,而是备好炒锅、蒸锅、汤锅三档,客人报数后选最合适的一口。TensorRT 的内核选择也是这个思路。
编译期,引擎针对最优维度附近生成一批候选内核(tactic);运行时拿到实际输入形状后,在候选集合里做映射,挑一个代价最低的执行策略;形状相近时直接复用,避免逐形状重新编译。用数学表达这次“挑锅”:
相近人数共一口锅,既不用临时买锅(实时编译),也不会把菜炒糊(性能回退)。这正是动态 Shape 能保持接近固定 Shape 性能的秘密。
- 候选集合\(\mathcal{T}_{\text{opt}}\) 是编译期基于最优维度生成的内核候选集。
- 代价函数\(\operatorname{cost}\left( K, D_{\text{actual}} \right)\) 估算内核 \(K\) 处理该实际形状的开销。
- 取最小值\(\arg\min\) 返回代价最小的那个内核,即最终选中策略。
性能与注意
老张如果把接待范围吹成“2 到 200 人”,后厨就得囤两百人份的食材锅具,成本失控;包间从八人桌改成十人桌,翻台也要时间。动态 Shape 的代价同样藏在范围与切换里。
画像范围过宽时,工作区(workspace)内存按最大可能维度预留,候选内核数量随之膨胀;推理时切换画像还有额外开销。内存预留的原则可以近似写成:
聪明的餐馆不会为一年来一次的二十人包间把厨房扩一倍。成熟方案是统计线上句子长度分布,把最优维度设在众数附近,用“日常档 + 高峰档”两个画像覆盖绝大多数请求——灵活与性能,从此不必二选一。
- 工作区函数\(\operatorname{Workspace}(D)\) 是处理维度为 \(D\) 的输入时所需的临时显存。
- 取最大值\(\max_{D \in \mathcal{P}}\) 表示在画像覆盖的所有可能维度里取最坏情况。
- 近似预留\(\approx\) 说明实际预留通常会加上对齐、多流等因素,只会更多不会更少。
AI 应用场景
在线NLP推理服务
阶段:模型部署与推理优化
解决的问题:智能客服、搜索场景中 query 长度波动极大,固定 Shape 把短句全部填充到最大长度,短文本占多数时六成以上算力被白白浪费。动态 Shape 让引擎按实际长度执行指令,消除填充开销。
具体案例:某电商智能客服的 BERT 模型,线上 90% 的 query 长度在 8~40 个字之间,偶尔出现 80 字长句。部署时配置画像 min=8、opt=32、max=96,上线后 P99 延迟降低 38%,吞吐提升近一倍。
安防视频检测服务
阶段:推理服务弹性调度
解决的问题:多路摄像头的抽帧检测任务,batch 大小随白天黑夜剧烈波动。维护多个固定 Shape 模型版本既占显存又难同步更新,动态 Shape 让一份引擎同时服务大、小 batch。
具体案例:园区 64 路摄像头,夜间低峰 batch=1,白天高峰 batch=16。通过 batch 维度的动态化并配置两个画像(低峰档与高峰档),显存占用下降约 40%,切换时无需重载模型。
大模型服务的长上下文
阶段:LLM 推理引擎优化
解决的问题:大模型输入 token 数随用户提问长短变化,若按最大上下文固定分配,prefill 阶段的大量位置是空转。动态 Shape 让 prefill 只计算真实 token。
具体案例:企业内部知识库问答,平均提问 300 token、上限 2048。配置 min=64、opt=512、max=2048 后,prefill 阶段的无效计算减少约 70%,首 token 延迟明显下降。
Python 演示
# -*- coding: utf-8 -*-"""模拟 TensorRT 动态 Shape 的核心机制:Profile 校验与运行时内核选择。"""import random class OptimizationProfile: """优化画像:记录最小、最优、最大维度。""" def __init__(self, name, dmin, dopt, dmax): if not (dmin <= dopt <= dmax): raise ValueError("需满足 min <= opt <= max") self.name = name self.dmin = dmin self.dopt = dopt self.dmax = dmax def contains(self, length): return self.dmin <= length <= self.dmax class KernelTactic: """一个内核策略:擅长处理某个长度区间的序列。""" def __init__(self, name, good_for): self.name = name self.good_for = good_for def cost(self, length): center = sum(self.good_for) / 2.0 return abs(length - center) def select_kernel(kernels, length): """运行时内核选择:挑代价最低的内核。""" return min(kernels, key=lambda k: k.cost(length)) def main(): profile = OptimizationProfile("NLP默认画像", dmin=8, dopt=32, dmax=96) kernels = [ KernelTactic("短序列内核", (8, 24)), KernelTactic("标准内核", (24, 56)), KernelTactic("长序列内核", (56, 96)), ] print("=== TensorRT 动态 Shape 模拟 ===") random.seed(42) for step in range(1, 9): length = random.randint(10, 100) if not profile.contains(length): print("step %d: 输入长度 %d 超出 Profile 范围,拒绝推理" % (step, length)) continue kernel = select_kernel(kernels, length) waste = 1 - length / profile.dmax print("step %d: 实际长度 %3d | 选中 %s | 固定Shape填充浪费 %.0f%%" % (step, length, kernel.name, waste * 100)) if __name__ == "__main__": main()
if not (dmin <= dopt <= dmax):校验三档排序,相当于订餐系统拒绝“最小人数大于最大人数”的荒谬预案。def contains(self, length):判断本次输入是否落在画像区间内,对应 TensorRT 运行时的形状合法性检查。return min(kernels, key=lambda k: k.cost(length))运行时内核选择,等价于后厨从三口锅里挑最顺手的一口。if not profile.contains(length):越界即拒绝推理,模拟真实引擎返回错误而不是硬着头皮跑。waste = 1 - length / profile.dmax计算如果走固定 Shape 会产生的填充浪费比例,呼应正文的核心痛点。
常见误区
误区以为声明了 min~max 区间,TensorRT 就会为 8、9、10……96 每个长度分别编译一版内核,模型体积必然爆炸。
正解实际并非如此。编译期只针对最优维度附近生成预选内核集合;运行时把实际形状映射到集合中代价最低的策略,相近形状直接复用同一个内核。就像后厨只备炒锅、蒸锅、汤锅三档,而不是为 2 到 96 人的每一种人数各买一口锅。
为何常见:固定 Shape 是“一个形状一版内核”,人们自然推断动态就是“每个形状一版内核”,忽略了“区间声明 + 运行时映射”的工程设计。
误区既然动态 Shape 支持区间,那就把 min 设为 1、max 设为 4096,一劳永逸,再也不用担心输入超范围。
正解范围过宽会按最大维度预留 workspace 内存、扩大候选内核搜索空间,还可能让实际输入长期偏离最优维度导致性能回退。正确做法是统计业务分布,把 opt 设在众数附近,再用多个 profile 覆盖不同负载模式。
为何常见:工程直觉常把“更通用”当作“更好”,忽略了显存和调度开销的物理上限。
误区以为建好引擎、定好画像就能直接推理,运行时不需要再设置输入形状。
正解动态 Shape 要求运行时调用 setBindingDimensions 设置本次实际维度,并通过执行上下文绑定对应画像;未设置或越界都会出错。就像客人到店必须报人数,领班才知道怎么排桌,报错或漏报都会出乱子。
为何常见:静态 Shape 是“建完即跑”,把这种惯性带到动态场景,就漏掉了 setShape 和上下文绑定这两步关键动作。
误区觉得凡是动态的都是性能妥协,宁可把输入全部 pad 死,也要保住“确定性”。
正解只要实际输入围绕最优维度波动,动态引擎会选到编译期针对 opt 优化的内核,性能可逼近固定 Shape;而固定 Shape 的填充浪费在变长场景反而更慢。动态是“以范围换灵活性”,不是“以速度换灵活性”。
为何常见:把运行时映射的微小开销绝对化,忽略了填充带来的无效计算往往是更大的开销来源。
面试题
Q1. 在固定 Shape 的 TensorRT 引擎中,以下哪个是最主要的痛点?
- A.无法充分利用GPU显存
- B.输入变长时需填充浪费算力← 正确答案
- C.模型推理精度必然下降
- D.引擎编译时间过长
答案:B。固定 Shape 必须把所有输入规整到同一尺寸,变长输入只能靠 padding 补足,短文本越多浪费越严重。GPU 显存可以被利用,只是存在无效计算;精度与编译时间并非固定 Shape 的核心痛点。
Q2. Optimization Profile 中三个维度参数是什么?
- A.上下限与步长
- B.最小最优最大← 正确答案
- C.平均中位数众数
- D.输入输出权重
答案:B。Profile 由 KDT_MIN、KDT_OPT、KDT_MAX 三档组成,分别对应最小、最优、最大维度。最优维度用于引导内核选择,最小/最大划出合法边界。步长是卷积参数,平均/中位数是统计量,都与画像无关。
Q3. 动态 Shape 推理前,设置本次实际输入形状的正确动作是?
- A.直接执行推理指令
- B.设置本次输入维度← 正确答案
- C.创建执行上下文
- D.读取引擎的元数据
答案:B。运行时必须先调用 setBindingDimensions 设置实际维度并接受合法性校验,随后才能 enqueueV2。直接推理会因维度未定而出错;创建执行上下文是建引擎阶段的准备,不是每次推理的设置动作。
Q4. 关于引擎与执行上下文(ExecutionContext),下列说法正确的是?
- A.一个引擎可建多个上下文← 正确答案
- B.上下文创建时绑定画像← 正确答案
- C.上下文之间共享运行状态
- D.上下文各自持有执行状态← 正确答案
答案:ABD。一个引擎可以创建多个执行上下文,各自绑定一个 Optimization Profile 并独立持有运行状态(内存、中间结果),互不共享,因此可并行处理不同偏好的输入。C 说反了,上下文之间正是通过“独立状态”来隔离的。
Q5. 下列哪个场景最适合使用动态 Shape?
- A.输入尺寸固定的图片分类
- B.变长文本的NLP推理服务← 正确答案
- C.每次输出固定4像素的任务
- D.完全没有输入的单例模型
答案:B。NLP 句子长度天然变化,是动态 Shape 的典型受益者。输入尺寸固定的图片分类用固定 Shape 更简单高效;固定输出或无输入场景不需要维度动态化。
Q6. 动态 Shape 相比固定 Shape 的优势包括?
- A.免去手动填充到固定长度← 正确答案
- B.减少无效计算提升吞吐← 正确答案
- C.模型精度得到必然提升
- D.适配长度多变的输入请求← 正确答案
答案:ABD。动态 Shape 让每批请求按实际长度执行,省去 padding 和无效计算,吞吐随之提升,也能灵活适配多变输入。但精度取决于模型本身与量化设置,动态 Shape 并不改变数值精度,C 错。
Q7. 动态 Shape 下引擎内核选择的正确原理是?
- A.按输入大小实时编译新内核
- B.运行时逐一尝试所有内核
- C.编译期预选策略映射实际形状← 正确答案
- D.退化为CPU推理执行模式
答案:C。引擎在编译期针对最优维度生成候选内核集合,运行时把实际形状映射到这个集合里代价最低的策略,相近形状直接复用。实时编译代价过高,逐一尝试不现实,退化 CPU 更不是设计目标。
Q8. 关于 Optimization Profile,下列说法正确的是?
- A.最优维度用于引导内核选择← 正确答案
- B.最小最大维度框定合法范围← 正确答案
- C.一个引擎只能创建一个画像
- D.画像之间可切换互相配合← 正确答案
答案:ABD。opt 是内核选择与内存布局的锚点,min/max 决定合法区间,多个画像可并存并在运行时切换以覆盖不同负载。一个引擎完全可以创建多个画像,C 错误。
Q9. 若实际输入长度超出 Profile 声明的最大值,会发生什么?
- A.自动对输入做截断处理
- B.静默选用最长内核运行
- C.返回错误或行为未定义← 正确答案
- D.自动扩展出新的画像
答案:C。越界输入不满足画像约束,TensorRT 会返回错误,依赖未定义行为继续运行是危险的。引擎不会自动截断、也不会静默换内核,更不会在运行时自动扩展画像——扩展需要重新构建引擎。
Q10. 动态 Shape 下引擎工作区(workspace)内存的预留原则是?
- A.按最小维度预留内存空间
- B.按最大可能维度预留内存← 正确答案
- C.按最优维度预留内存空间
- D.推理时动态向系统申请
答案:B。引擎要为画像覆盖范围内最“大”的输入提前备好工作区,即按最大可能维度预留,保证任意合法输入都有足够临时显存。按最小或最优预留会导致大输入时内存不足,运行时临时申请则违背引擎“早规划、低抖动”的设计目标。