想象你经营着一家既能办百人婚宴、又能做街边小吃的复合餐厅。传统做法是为宴席准备固定尺寸的圆桌、为快餐准备固定的单人餐台——客人一多一少,桌椅就派不上用场,只能不停地重新布置。TensorRT 早先的“静态 Shape”模式正是这样:模型输入尺寸固定为 32、128、256,就得分别建三套完全独立的引擎,像三间不同规格的厨房一样各占显存、各出人力。而这个教程的主角——动态 Shape,则像把餐厅改造成了灵活的自助餐线:餐台宽度可以随客流伸缩,备料仓库统一调度,主厨根据当下客人数量动态挑选最合适的灶台与炒菜节奏。客人(输入数据)换了一拨又一拨,长度变了又变,厨房不需要重新装修,开张一次就能接待所有尺寸的客人。这个“自助餐厅”不仅省了空间,还保证了出餐速度,正是 NLP 等变长输入场景梦寐以求的推理能力。
对 AI 从业者而言,掌握动态 Shape 意味着你交付的推理服务不再是“只能吃固定套餐”的僵化模型,而是能从容应对真实世界长短不一的数据流——这是从“能跑通 demo”迈向“能上生产环境”的关键一步,也是面试部署工程师岗位时绕不开的核心考点。
可视化演示
展示TensorRT动态Shape机制:变长输入张量、优化Profile定义维度范围、运行时自动选择最优内核,通过交互理解动态Shape兼顾灵活性与推理性能的原理。
本节配有交互动效(进入视口后自动加载,约 3KB)。
动态Shape:变长输入处理 引擎处理变长输入无需逐尺寸构建 适合NLP等变长输入场景 Shape ∈ [Min, Max] 动态Shape兼顾灵活与性能
动态Shape概念
传统宴席上,每位客人的餐盘大小是统一的,厨房提前把菜分好,盘子端上去就是那个分量——这就是 TensorRT 的静态 Shape 模式。可自助餐厅的客人饭量不一:有人只想尝两口,有人要满满一盘。如果厨房还是按固定的盘子配菜,要么给每桌备上好几套规格的餐具(为每个尺寸单独建引擎),要么干脆把所有人的菜都堆成大份(padding 到最大长度)。前者管理成本爆炸,后者浪费大量算力。动态 Shape 让餐台本身能伸缩:输入数据的尺寸每次都可以不同,同一套厨房流程照常运转。
这个“伸缩的餐台”落到技术上,就是输入张量的 shape 中可以出现动态维度。在 TensorRT 里,我们通常用 -1 标记一个允许变化的维度。例如 NLP 模型处理句子序列时,批量大小固定为 1,隐藏层特征固定为 64,只有序列长度每次请求都不同,于是把输入定义为 (1, -1, 64)。
没有动态 Shape 时,线上句子长度在 8 到 256 之间浮动,你只能选两个方案:按最大长度 padding,GPU 上四分之三的算力在算“空菜”;或者为每个常见长度各建一个引擎,部署文件多到运维崩溃。动态 Shape 让一个引擎通吃整个区间,像一间自助厨房服务所有饭量的客人。
-1 是一个“活口”,允许每次推理时填入不同的序列长度。- N 固定批次大小批量大小在构建引擎时就确定,本例中固定为 1,代表一次只处理一条序列。
- -1 动态维度占位负一不是真的负一,而是告诉 TensorRT:“这个维度别锁死,运行时我会告诉你具体数值”。
- D 固定特征维度每个 token 的向量长度是模型结构决定的,始终不变。
优化Profile
自助餐要想开得顺畅,得先划好“取餐区间”:凉菜区备 3 到 8 种、热菜区备 5 到 12 种。客人少了就少上菜,客人多了就多补货,但总得有个下限和上限。TensorRT 的优化 Profile(Optimization Profile) 干的正是这件事——为每个动态维度声明一段合法区间,并且还要在这段区间里标出一个“黄金档位”。
Profile 用三个值描述一个动态维度:min 是下界,表示引擎接受的最小长度;max 是上界,表示显存和内核设计能支撑的最大长度;opt 是“最常出现的长度”,相当于主厨最拿手的分量。构建引擎时,TensorRT 会重点围绕 opt 调优内核;运行时,每次输入的实际长度只要落在 区间内就能正常运行。
划 Profile 有点像给自助餐线定“餐台容量区间”:下限定得太低、上限定得太高,主厨要兼顾的范围太广,每个尺寸都难做到极致;区间贴合真实客流量分布,才能既灵活又高效。这也是为什么部署前统计线上输入尺寸分布至关重要。
- minDim 下界保证最小输入也能正常推理,通常取线上可能出现的最短序列,比如一句话的十几个 token。
- optDim 最优档位这个值应该取实际部署中最常见的形状,TensorRT 会重点为这个尺寸选择最得意的内核。
- maxDim 上界超过这个值的输入会直接报错,它同时也是构建时显存预留的天花板。
引擎构建流程
自助餐厅开业前要做的不是每天临时摆桌子,而是把整个厨房动线设计成一张蓝图:哪里放灶台、哪里备菜、传菜路径怎么走。TensorRT 构建动态 Shape 引擎同样遵循“先设计、后营业”的思路——构建(Build)阶段把网络结构、Profile 区间、工作空间配置全部固化,产出一个序列化引擎文件,之后部署就靠加载这个“菜谱蓝图”。
构建流程可以拆成四步:先创建 Builder 构建器,它相当于总负责人;再创建 Network 定义模型的计算图,即每道菜的加工步骤;接着创建 Optimization Profile 并绑定到 Builder Config 配置上,相当于把“取餐区间”写进营业手册;最后调用构建接口,产出序列化的引擎字节流。
构建好的引擎像一本“中央厨房手册”:里面的 Profile 区间、内核选择策略、显存规划统统写死。部署时只需加载这本手册,不必重新建厨房——这保证了动态 Shape 引擎在启动后能快速响应不同尺寸的请求。
- network 网络定义模型层与张量连接的完整描述,是“菜谱”本身。
- profile 动态区间声明把每个动态维度的 min/opt/max 写入配置,相当于划定餐台的伸缩范围。
- config 构建配置包含 Profile 列表、workspace 显存上限等构建期选项。
- Serialize 序列化输出把构建好的引擎压缩成二进制字节流,便于存盘和加载。
运行时绑定
客人落座后开始点菜,服务员把具体需求报给后厨。动态 Shape 引擎在运行时也需要这样一次“点菜”:每次推理前,你必须通过执行上下文(Execution Context) 告诉引擎当前这次请求的实际输入形状,这一步叫作维度绑定。
执行上下文可以理解为引擎的“当前工作状态实例”。同一份引擎可以创建多个上下文,就像一本菜谱可以供多个厨师同时照做。每个上下文有自己独立的一组绑定缓冲区:set_input_shape 指定输入张量的实际维度,get_tensor_shape 查询输出张量的形状。关键约束是:实际维度必须落在构建时声明的 Profile 区间内。
绑定维度不是每次都要从零开始——如果下一次请求的长度和上一次相同,引擎会复用现成的内部状态,就像熟客“老样子”一样省事。一旦换了个新长度,引擎内部会自动切换对应的内核与显存布局,整个过程对上层透明。这就是动态 Shape“一个引擎、百变输入”的奥秘所在。
- 实际维度变量 d指第 i 个输入张量本次推理的真实尺寸。
- 区间约束实际值不得小于 min 也不得大于 max。
- 绑定动作通过上下文的 set 接口把实际维度写到引擎指定位置。
内存池管理
自助餐厅的备料仓库不会按最大客流量给每道菜都备满,而是根据时间段统筹:凉菜区和热菜区的备料高峰错开,同一批食材在不同时段被不同餐台使用。TensorRT 构建动态 Shape 引擎时,对显存的管理也遵循同样的“复用哲学”——Workspace 内存池。
模型推理时会产生大量中间张量,这些张量在计算图中各有各的“存活时间”:有的在层 A 算完就死了,有的要活到层 Z。如果每个张量都独享一块显存,内存占用会随动态 Shape 的最大尺寸爆炸式增长。TensorRT 在构建期做了一次“仓库规划”:把生命周期不重叠的张量安排到同一块显存区域轮流使用。
这套复用策略让动态 Shape 引擎的显存占用不再随最大维度线性放大:只要中间张量寿命错开,就能共享仓库空间。实际部署中,你可以通过 set_memory_pool_limit 为 workspace 设置上限,TensorRT 会在上限内自动压缩和复用——就像一个贴着预算经营的厨房主厨,用巧妙的排班代替买更多的仓库。
- t 时刻截面想象在计算过程的每一瞬间都拍一张快照。
- 存活张量集合 B_t这张快照里还“活着”(仍被后续层需要)的全部张量。
- 求和与取最大每个时刻都算一次内存总量,再取所有时刻中的峰值。
性能优化策略
同样是炒一盘青菜,大厨会用猛火灶快炒,炖汤则换文火炉慢煨。TensorRT 内部维护着大量针对不同形状、不同硬件优化的内核(Kernel),构建动态 Shape 引擎时,它会在 Profile 区间上做的事比你想的更聪明:不是含糊地用一套通用实现凑合,而是针对 opt 维度进行基准测试,为不同尺寸区间挑选不同的内核策略。
当某个输入形状恰好等于 opt 时,引擎直接使用那套精挑细选的最快内核;当形状偏离 opt 时,引擎会退而求其次,从备用内核池中选一个对该尺寸相对高效的选择。这个选择过程被建模为一个最大化问题:从内核集合里,挑出在给定 shape 和硬件下推理速度最快的那个。
实践中的调优秘诀是:把 opt 设成线上出现频率最高的输入形状,让最快内核服务于最常见的场景;同时不要让 Profile 区间过宽,否则引擎需要在“照顾少数派形状”上花费过多优化预算。正如好主厨不会为了偶尔的百人宴把整间厨房都换成大锅。
- 候选内核集合 KTensorRT 为每层算子准备的多套实现,包括不同向量化宽度、不同分块策略。
- 速度函数 v_k衡量内核 k 在特定 shape 和 GPU 型号下的吞吐,单位通常是 ms 或 samples/s。
- argmax 选择在所有候选中挑选速度值最大的那个。
部署应用场景
自助餐厅开业后,客流高峰时百人排队,闲时三三两两——但厨房只要开一次门,就能服务所有客人。动态 Shape 在真实部署中扮演的正是这个“一次构建、长时服役”的角色。以 NLP 推理服务为例,用户的 query 短则几个 token、长则上千 token,静态引擎要么按上限 padding 浪费显存,要么为各种长度维护一大堆引擎文件。
动态 Shape 让一个引擎覆盖全部长度区间,服务端只需在收到每个请求后调用上下文绑定接口,把实际 seq_len 传进去即可。对于 LLM 这类 token 逐次增长的解码场景,动态 Shape 还能配合 KV Cache 的增量更新,让显存利用率跟随实际序列长度平滑伸缩,而不是永远按最坏情况预留。
在实际推理集群中,动态 Shape 更带来资源调度的灵活性:多个模型的引擎可以共享 GPU 上的显存池,因为每个引擎的内存需求会随输入尺寸动态伸缩;不同租户的请求长短不一,也被自然地平均到了资源曲线上。就像自助餐线根据客流自动补菜,后厨始终忙碌而从容——这就是动态 Shape 从“能用”走向“好用”的工程价值。
- 完成的请求数 N在一个统计窗口里,推理服务成功返回结果的次数。
- 时间窗口 T统计持续的时间长度,比如 1 分钟或 1 小时。
- 两者相除得到单位时间吞吐率,是推理服务最核心的运维指标。
AI 应用场景
NLP 变长请求的在线推理服务
阶段:模型推理与部署
解决的问题:传统静态引擎要求输入固定长度,但真实用户 query 长短不一。若按最大长度 padding,Batch 内大部分是无效算力;若为每种长度建引擎,文件与显存开销不可管理。动态 Shape 用一个引擎覆盖所有长度,请求到达时按实际 seq_len 绑定维度,显著降低延迟与显存占用。
具体案例:某智能客服系统每天处理百万级用户 query,长度分布从 8 到 200 token 不等。部署动态 Shape 引擎后,GPU 显存占用降低约 40%,平均延迟降低约 25%,且不再需要为不同长度维护多个引擎版本。
多租户共享 GPU 的智能调度
阶段:资源管理与性能优化
解决的问题:多个业务方共用一块 GPU,各自输入尺寸差异极大。静态引擎需要为每个租户预留最大显存,导致资源碎片化。动态 Shape 引擎的显存需求跟随实际输入伸缩,让调度器更精准地装箱和超卖。
具体案例:某云厂商的推理平台让搜索、推荐、OCR 三个模型共享 L4 GPU。改用动态 Shape 后,同块 GPU 上可同时驻留的模型实例数从 2 个提升到 5 个,整体吞吐提升约 60%。
Python 演示
import tensorrt as trtimport numpy as np LOGGER = trt.Logger(trt.Logger.WARNING) def build_dynamic_engine(min_len, opt_len, max_len): builder = trt.Builder(LOGGER) network = builder.create_network( 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) ) profile = builder.create_optimization_profile() # 输入 x = (batch=1, seq_len=动态, hidden=64),NLP场景的典型布局 x = network.add_input("x", trt.float32, (1, -1, 64)) identity = network.add_identity(x) out = identity.get_output(0) network.mark_output(out) # 关键:为动态维度声明min/opt/max三段区间 profile.set_shape("x", (1, min_len, 64), (1, opt_len, 64), (1, max_len, 64)) config = builder.create_builder_config() config.add_optimization_profile(profile) config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, 1 << 30) return builder.build_serialized_network(network, config) def infer_with_dynamic_shape(engine_bytes, seq_len): runtime = trt.Runtime(LOGGER) engine = runtime.deserialize_cuda_engine(engine_bytes) context = engine.create_execution_context() # 运行时按请求实际长度绑定维度,无需重建引擎 context.set_input_shape("x", (1, seq_len, 64)) # 实际项目中还需用pycuda分配device显存并拷贝输入数据 shape = context.get_tensor_shape("x") return "引擎就绪,当前绑定输入维度:%s" % (shape,) if __name__ == "__main__": engine = build_dynamic_engine(min_len=8, opt_len=64, max_len=256) print("动态引擎构建成功,大小(MB):%.2f" % (len(engine) / 1e6)) print(infer_with_dynamic_shape(engine, seq_len=48)) print(infer_with_dynamic_shape(engine, seq_len=128))
network.add_input("x", trt.float32, (1, -1, 64))定义输入张量,用-1声明序列长度为动态维度。profile.set_shape("x", (1, min_len, 64), (1, opt_len, 64), (1, max_len, 64))为动态维度指定 min/opt/max 三段区间,这是动态 Shape 的核心配置。config.add_optimization_profile(profile)把 Profile 挂到构建配置上,引擎构建时才会识别动态区间。builder.build_serialized_network(network, config)执行构建并直接产出序列化引擎字节流。context.set_input_shape("x", (1, seq_len, 64))运行时绑定本次请求的实际维度,引擎内部自动切换内核。context.get_tensor_shape("x")查询绑定后的张量形状,确认动态维度生效。
常见误区
误区很多初学者以为把输入定义为 `(-1, -1, 64)` 或者什么都不用做,TensorRT 就会自动支持任意形状的输入。
正解动态 Shape 必须显式做两件事:一是构建前定义 Optimization Profile 的 min/opt/max 区间,二是运行时用 set_input_shape 绑定实际维度。少了 Profile,引擎根本不知道动态维度的边界;少了运行时绑定,引擎无法得知本次输入到底多长。就像自助餐必须先划好餐台范围、客人来了还得报饭量。
为何常见:PyTorch 等框架中动态维度几乎是“默认行为”,很容易让人误以为推理引擎也会自动跟随。
误区有人认为动态引擎要兼容各种尺寸,内核肯定是“大而全”的通用实现,性能一定不如专门为固定尺寸调优的静态引擎。
正解通过合理设置 opt 维度,TensorRT 会为最常出现的形状专门挑选最优内核;当输入恰好等于 opt 时,性能与静态引擎几乎一致。其他尺寸虽然略逊,但省去了多引擎切换和重建的开销。多个静态引擎频繁切换的额外损耗,往往比动态引擎的微小性能差距更致命。
为何常见:只看到“动态=通用=妥协”的直觉,忽略了 TensorRT 在构建期针对 opt 做过定向优化。
误区为了让模型“什么输入都能接”,把 min 设到 1、max 设到 1 万,觉得区间越宽越安全。
正解Profile 区间过宽会带来两个问题:一是显存按 max 预留,过大的 max 导致空闲时也占着大量显存;二是内核优化预算被摊薄,远离 opt 的形状性能会下降。正确的做法是统计线上输入分布,让 min/max 恰好覆盖真实数据,opt 对准高频中心。
为何常见:“覆盖全部可能性”的防御心态,忽略了显存与性能是需要用数据说话的工程权衡。
误区认为动态 Shape 只能在 NLP 场景用,动态维度只能放在 seq_len 上。
正解任何维度都可以声明为动态——包括 batch 维度、图片宽高、通道数。视频流检测中输入分辨率可能变化,推荐系统的 batch 大小可能随流量波动,这些都可以通过动态 Shape 统一承载。-1 只是一个占位符,放在了哪个位置,哪个位置就获得了伸缩能力。
为何常见:教程与案例多围绕 NLP 展开,容易把“常见用法”误当成“唯一用法”。
面试题
Q1. 动态Shape相比静态Shape的最大价值是什么?
- A.减少模型参数量
- B.免每种尺寸建引擎← 正确答案
- C.提升单次推理精度
- D.减少训练数据量
答案:B。动态Shape的核心价值是让一个引擎服务所有输入尺寸,避免为每个形状单独构建和部署引擎,就像一间自助厨房服务所有饭量的客人。它不改变模型参数、不提升精度,也与训练数据无关。
Q2. 优化Profile的三个关键维度是什么?
- A.low/mid/high
- B.min/opt/max← 正确答案
- C.min/avg/max
- D.start/peak/end
答案:B。Profile 用 min(下界)、opt(最优档位)、max(上界)三段区间描述动态维度的合法范围,其中 opt 是内核调优的基准。其他三组词都不是 TensorRT 中 Profile 的标准术语。
Q3. 关于优化Profile的说法正确的有哪些?
- A.opt维度用于指导内核选择← 正确答案
- B.输入必须落在min到max区间内← 正确答案
- C.引擎只能绑定一个Profile
- D.运行时可以重新绑定维度← 正确答案
答案:ABD。opt 是内核挑选的基准,实际维度必须在 [min, max] 内,运行时每次推理都可以重新绑定——这三点都对。但一个引擎可以绑定多个 Profile(例如不同 batch 范围),C 错在“只能绑定一个”。
Q4. 输入定义为(1, -1, 64)时,-1表示什么?
- A.该维度输入为空
- B.该维度大小可变← 正确答案
- C.该维度无任何限制
- D.该维度固定为零
答案:B。-1 是动态维度占位符,表示这个维度在构建时不定死,运行时通过 set_input_shape 填入具体值。它并非“空”或“零”,也并非完全无限制——必须受 Profile 的 min/max 约束。
Q5. 运行时输入长度超过Profile的max会怎样?
- A.自动扩容引擎
- B.报错或行为不正确← 正确答案
- C.自动裁剪到max值
- D.静默重建引擎
答案:B。超出 max 的输入无法被引擎正确处理,通常会导致错误。引擎构建时显存与内核都是按区间规划的,超界输入无法保证安全性;TensorRT 不会悄悄裁剪或重建引擎。
Q6. 动态Shape的显存优化包含哪些?
- A.workspace统一配置上限← 正确答案
- B.生命周期不重叠的张量共享显存← 正确答案
- C.每个输入独立分配大块显存
- D.构建期规划全局内存复用← 正确答案
答案:ABD。TensorRT 通过 workspace 上限约束总显存,并在构建期把生命周期不重叠的中间张量安排到同一区域复用。C 恰恰是静态引擎的做法,违背显存复用哲学,会导致占用随动态尺寸爆炸。
Q7. 动态Shape引擎的构建流程正确顺序是什么?
- A.定义网络→创建Profile→构建→序列化← 正确答案
- B.序列化→定义网络→创建Profile
- C.创建Profile→序列化→定义网络
- D.构建→定义网络→序列化→创建Profile
答案:A。构建动 Shape 引擎必须先定义网络计算图,再创建并绑定 Profile,然后执行构建,最后序列化保存。其他顺序在逻辑上不通,因为序列化必须发生在构建完成之后。
Q8. opt维度最合适的取值策略是什么?
- A.取min和max的几何中点
- B.取线上最频繁出现的形状← 正确答案
- C.永远取区间的最大值
- D.任意值,不影响性能
答案:B。opt 是 TensorRT 内核调优的靶心,应设为线上分布的高频中心,让最常见请求跑在最快内核上。中点和最大值都不符合数据分布,任意值更是会错失优化机会。
Q9. 一个引擎绑定多个Profile相比单个Profile的优势是?
- A.减少构建时间
- B.同时服务多段不同的尺寸区间← 正确答案
- C.完全消除运行时绑定
- D.自动增加模型精度
答案:B。多个 Profile 可以分别覆盖不同区间的形状,例如一个覆盖小 batch 高延迟敏感、一个覆盖大 batch 高吞吐场景,运行时按需选择。它不减少构建时间,也不消除维度绑定,更与模型精度无关。
Q10. 部署动态Shape引擎时应用层需要做哪些事?
- A.按请求实际维度调用绑定接口← 正确答案
- B.确保维度落在Profile区间内← 正确答案
- C.关闭显存池配置以省内存
- D.为并发请求创建多个执行上下文← 正确答案
答案:ABD。应用层要负责把每个请求的真实形状绑定到上下文,并保证它合法;高并发时通常创建多个上下文并行执行,让每个请求独立绑定。C 恰恰相反,合理配置 workspace 显存池正是动态 Shape 省显存的关键手段。