想象你经营一家中央厨房。过去你只接“固定套餐”的订单:4人餐就备4份菜,8人餐就备8份菜,每份菜的切配、火候、装盘步骤全在开工前设计好,后厨闭着眼睛执行,效率极高。可一旦来了一桌 5 人、一桌 7 人、一桌忽多忽少的团餐,固定套餐就全乱套了——要么不够吃,要么剩一堆。 AI 编译器也面临同样的烦恼。传统 TVM 走“静态 Shape”路线:模型输入尺寸固定,编译器可以把每个算子(比如矩阵乘法、卷积)在编译期就完全展开成机器码,极致压榨性能。但现实世界的输入尺寸千变万化:NLP 模型一句话长一句话短,视觉模型一张图 640、一张图 1280。TVM 必须学会“动态 Shape”——像央厨推出“单点菜单”一样,把维度写成“按人数 N 算”,等客人落座、人数确定后,再指挥后厨开工。而连接“菜单上抽象的 N”与“实际到店的 7 人”之间的桥梁,就是 SymbolicVar 机制。理解这套机制,你就掌握了 AI 编译器从“刻舟求剑”走向“随机应变”的核心智慧。
掌握动态 Shape 与 SymbolicVar 机制,是吃透 TVM 编译器架构的最后一里路,也是你在实际推理部署中应对多变输入尺寸的必备兵器,能让你的算子优化能力从“实验室能用”升级为“生产可用”。
目录
可视化演示
展示TVM中SymbolicVar机制如何支持动态维度,揭示编译时符号变量与运行时实际形状的映射关系。
本节配有交互动效(进入视口后自动加载,约 3KB)。
SymbolicVar机制 静态Shape:编译时固定所有维度 SymbolicVar:用符号变量表示未知维度 运行时绑定:实际形状在执行时确定 Shape = [N, C, H, W] where H, W ∈ SymbolicVar 动态形状的编译时抽象与运行时绑定
动态Shape概念:从固定套餐到单点菜单
回到央厨的比喻。传统“固定套餐”模式下,后厨在海报上写着“4 人餐 ¥198、8 人餐 ¥398”,提前把每道菜的料备好、工序排好。这套模式的好处是极致高效——备菜、切配、炒制环环相扣,一分钟都不浪费。可一旦来了 5 人、6 人或 10 人的散客,后厨就傻眼了:固定套餐不是多了就是少了,重新设计套餐又要耽误半天。AI 编译器里的“静态 Shape”就是这份固定套餐:(4, 3, 224, 224) 的输入尺寸写死在编译产物里,模型跑得飞快,但换一个分辨率立刻报错,只能重新编译。
“动态 Shape”则是央厨推出的新服务:菜单上不再写死人数,而是写“按实际就餐人数 N 配餐”。后厨提前准备好通用的切配流程,等客人报出人数(运行时给出真实 Shape),再按 N 调整每道菜的份量。在 TVM 中,这就意味着张量的维度可以是符号变量——N、H、W 不再绑定具体数值,而是以符号形式存在,编译一次,运行无数种尺寸。
动态 Shape 的典型应用场景遍地都是。NLP 模型里一句话有长有短,batch 大小随着服务端并发随时变化;视觉模型要适配不同分辨率的输入图片;推荐系统要拼接不同长度的用户行为序列。这些场景如果每个尺寸都单独编译一份“套餐”,光编译时间就让人崩溃。动态 Shape 让编译器只编译一次,运行时“按需上菜”,以少量性能损耗换取极大的通用性。
套餐思维的价值在于快,单点思维的价值在于活。动态 Shape 的诞生,正是 AI 编译器从“实验室理想环境”迈向“真实互联网服务”的必然选择——因为真实世界的人数和菜量,从来不会按海报上的套餐方案乖乖入座。
- 张量形状定义
shape(x)表示张量 x 的完整维度结构,比如图像张量的“批次、通道、高度、宽度”四个维度。 - 符号维度 N
N是编译期的符号,不绑定任何具体数值,如同菜单上的“按人数 N”。 - 正整数约束
N ∈ ℤ⁺表示 N 是任意正整数,编译产物的循环结构按 N 的最一般情况生成,运行时才知道具体取值。
SymbolicVar原理:菜单上的“按人数N”
央厨推出单点菜单后,菜单上出现了一个神奇的词——“按就餐人数 N”。这个 N 不是具体数字,而是一个符号:它告诉后厨“菜量要由 N 决定”,但 N 究竟是多少,要等客人落座、店员报数后才能确定。TVM 中的 SymbolicVar 就是菜单上这个 N——它是动态 Shape 的“灵魂符号”。
SymbolicVar 的本质是一个符号变量:在编译器的中间表示(IR)里,它是一个特殊的节点,代表“尚未确定但在运行时会明确的整数”。它不占用真实内存,不参与算术结果,却参与循环边界的推导。比如你写一个卷积算子,输入高度是符号 H,TVM 就能生成一个以 H 为上界的循环结构——H 是几,循环就跑几遍。这就是符号变量与普通 Python 变量的最大区别:普通变量在编译期就有确定值,符号变量把“确定”推迟到运行时。
TVM 中这个机制的核心是 tir.Var 与 tir.Any。tir.Var 是有名字的符号变量,比如 N = tir.Var("batch"),它就像菜单上的“按人数 N”;而 tir.Any 则是不关心名字的“任意维度”通配符,像菜单上写“配菜若干”。两者都可以表示动态维度,但 Var 允许你在不同算子之间引用同一个符号,从而保证“同一个 N”在整张计算图里始终是同一个值。
运行时绑定(bind)是符号变量大显身手的时刻。当真实输入 \(x\) 带着 shape (7, 3, 224, 224) 到来时,TVM 的运行时把 N 绑定为 7。此时编译器生成的通用 kernel——那个以 N 为循环上界的算子——便以 7 为具体循环次数开始执行。绑定不是重新编译,而是把符号替换成实际值,流程极快。
回到央厨的视角,SymbolicVar 就是那张贴在备菜间的纸条,上面写着“主菜 ×N、凉菜 ×N”。纸条本身没有菜,但整个厨房的运转都围着它转;等前厅报来“7 位客人”,所有工序瞬间按照 7 这个数字全部激活。没有这个符号,后厨就只能守着固定套餐,失去随机应变的灵魂。
- 定义符号变量
n = Var("batch")创建了一个名为 batch 的符号,它在编译期只是“命名字母”。 - 符号形状
shape(x) = [n, 3]表示张量的第一维由符号 n 决定,是动态的。 - 运行时绑定箭头表示绑定操作——运行期把 n 替换为实际值 7。
动态Shape表示:符号如何写进形状
有了符号变量这个“字母”,下一步是把它正式写进菜单。央厨里,单点菜单的格式是“主菜(按人数 N)”;TVM 里,动态 Shape 的标注方式也有一套严谨的“语法规则”。
TVM 中标注动态 Shape 的常见做法是使用 tvm.tir 中的符号表达式。你可以直接把一个 Var 放进 shape 列表:shape = [n, 3, 224, 224]。更灵活的方式是用 tvm.shape 或带条件约束的符号表达式,比如 H % 32 == 0 这样的整除约束,它告诉编译器“高度必须是 32 的倍数”——这就像央厨备注“人数必须是双数,方便分桌”。这些约束会在编译期参与合法性检查,也会在运行时用于快速校验输入是否合法。
TVM 内部区分两种动态语义:Named Var(命名变量)与 Any(通配符)。命名变量像契约——[n, 3] 要求流水线上所有算子共享同一个 n;Any 则像“随便”——它只声明“这一维可变”,但具体变的值不需要跨算子保持一致。这直接影响到 shape 传播:当多个算子串联时,命名变量可以沿数据流自动传递,保证整个流水线对同一批输入的解读完全一致,而 Any 往往在传播中被进一步收紧为具体符号或常量。
形状传播规则是动态 Shape 表示中最精妙的部分。TVM 的推理器(shape inference)会从输入 shape 出发,沿着算子的数学语义一步一步推导出每个中间张量的 shape。对于 elementwise 算子,输出 shape 等于输入 shape;对于矩阵乘法,shape 规则是“前维保留、内维匹配”;对于广播(broadcast),两个张量的维度按对齐规则逐位合并。这套规则在编译期执行,结果是每个中间张量都有一个用符号表达式表示的 shape——后厨的每一道工序,都知道自己该端出多大的盘子。
菜单的写法规矩,决定了后厨能不能准确备菜。动态 Shape 的表示形式不是随便定的——命名符号提供“契约一致”,通配符提供“下限承诺”,传播规则则像流水线作业指导书,保证从原料到成品的每一步都心中有数。
- 广播输入形状两个输入张量的 shape 分别为
[n, 1]和[n, 3],其中1那一维表示“待扩展”。 - 逐维对齐合并第一维两个都是 n,直接保留;第二维 1 和 3,取最大值 3。
编译期优化:央厨的预案库
单点菜单有了,可央厨如果真等客人报数再从头洗菜切菜,高峰期必然瘫痪。聪明的央厨会准备“预案库”:把最常用的菜提前切好备好、做成半成品。TVM 编译动态 Shape 时也遵循同样的智慧——在编译期设计好通用的“半成品”策略,运行期快速加工。
第一个策略是算子实例化(operator instantiation)。TVM 收到一个带有符号 Shape 的算子时,不会像静态 shape 那样直接生成针对固定维度的机器码,而是生成一个通用 kernel——循环边界是符号变量、内存偏移按符号表达式计算的机器码。这个 kernel 就像一个“万能模具”,任何 N 值填入都能跑,代价是循环体内多了一些下标计算的中间操作。
第二个策略是静态部分展开(partial unrolling)。动态 Shape 不等于所有维度都是动态的。比如输入是 (N, 3, 224, 224),只有 N 可变,其余三维全固定。TVM 在编译期会把固定维度的循环完全展开,只把 N 对应的最外层循环保留为动态循环。这就好比央厨知道“人数变,但菜单不变”——每道菜的程序照旧,只是总量乘上 N。这种“半动态半静态”的混合展开,是动态 Shape 编译期优化最实用的手段。
第三个策略是常量折叠(constant folding)。在符号表达式的推导过程中,凡是能算出结果的静态计算,编译器都提前算完。比如 shape 表达式里 [n, 3] 做 transpose 后变成 [3, n],这个交换本身在编译期完成;再比如循环边界里的 n * 3 * 4 会折叠成一个乘法表达式,避免运行期重复计算。常量折叠让符号表达式在编译期被充分“化简”,运行期只剩下最必要的符号代入。
预案库思维的精髓是“把能提前做的事情全部提前”。央厨不会等客人来了才洗菜,TVM 也不会等输入到了才生成代码——编译期把所有静态部分打磨成半成品,运行期只做“报人数、开火、上菜”三件事,这就是动态 Shape 编译器高效运转的底气。
- 通用 kernel 生成
C_gen是编译期生成一份能适配任意 N 的通用算子的代价——只做一次。 - 运行时绑定成本
C_bound是每次实际输入到达时把符号替换成具体值的开销——极其微小。 - 静态维度展开求和符号表示对每个静态维度独立展开循环,这部分在编译期完成,运行期零成本执行。
运行时调度:高峰期如何出菜
当第一批客人落座、前厅报来“7 位、4 个凉菜、3 个热菜”时,央厨的后厨系统立刻进入高峰调度模式。TVM 运行时的工作与此别无二致:拿到真实 Shape 后,如何快速把符号替换、选择最合适的 kernel、分配好内存并执行完毕,这就是运行时调度的艺术。
第一步是形状绑定(shape binding)。真实输入张量携带 shape 到达运行时,TVM 将符号表达式 [n, 3] 中的 n 替换为实际值(比如 7),得到完全具体的张量形状。这一步极其轻快,本质只是整数赋值,像前厅报一句“7 位”。
第二步是形状分发(shape dispatch)。如果编译期生成了多个 kernel 变体——比如 n=4 专用 kernel、n=8 专用 kernel、以及万能 kernel——运行时需要一个“查表”逻辑:把实际 n 值作为 key,去 kernel 表中查找最匹配的实例。这就像央厨的传菜窗口:看到 7 人桌,就直接走“7 人套餐快速通道”,而不是把所有菜从头做一遍。查表命中则直接执行,未命中则退回万能 kernel。
第三步是动态内存分配。静态 Shape 的内存在编译期就能算出总量,一次分配终身使用;动态 Shape 每次可能不同,必须在运行时按实际大小分配缓冲区。TVM 采用内存池(memory pool)复用策略:上一次分配的缓冲区如果够大就复用,不够再扩容,避免反复 malloc 带来巨大开销。这对应央厨的“备用餐具柜”——盘子共用,谁需要谁取,不够了才补充新盘子。
调度开销的控制是动态 Shape 运行时性能的命门。TVM 通过尽量在编译期完成 kernel 选择逻辑、把 dispatch 收敛到 O(1) 查表、以及复用内存池,将调度总开销压缩在微秒级。对于动辄毫秒级的算子计算,这点额外成本在大多数场景下完全可接受。
高峰期的央厨最怕慌乱无序。TVM 的运行时调度就像一位经验丰富的前厅经理:接到人数、手一指对应通道、顺手递上餐具、一声“上菜”,全程流畅不卡壳。把调度税的每一分钱都算得清清楚楚,动态 Shape 才能在实践中真正落地。
- 绑定开销
T_bind—— 把符号变量 n 赋值为实际数值,常数时间。 - 分发开销
T_dispatch—— 按 n 查 kernel 表,理想情况下是 O(1) 查找。 - 计算开销
T_kernel—— kernel 真实执行计算的时间,与输入规模成正比。 - 内存开销
T_alloc—— 动态分配/复用缓冲区的时间,用内存池压低。
性能差异分析:定制菜vs标准菜的成本账
食客都知道,现炒的单点菜通常比中央厨房配送的标准化套餐贵一点——因为人工灵活了,效率却不那么极致了。动态 Shape 与静态 Shape 的性能差异,也正是这份“灵活的成本账”。
静态 Shape 为什么快?因为编译器可以做激进优化。循环上界已知,编译器能完全展开循环、消除分支、将数组访问优化为寄存器操作、甚至跨算子融合。这相当于央厨为 4 人餐设计了一条全自动流水线,从洗菜到装盘,中间没有任何等待和判断,每一秒都在产出。动态 Shape 的万能 kernel 因为循环上界是变量,无法完全展开,循环内必须保留边界检查和下标计算,性能自然打折扣。实测中,同样的算子,动态 Shape 往往比静态 Shape 慢 5%~20%,具体取决于动态维度的占比——动态维度占比越低,差距越小。
但动态 Shape 的优势在于编译次数。静态 Shape 每来一个新尺寸都要重新编译,一次编译可能耗时几十秒;动态 Shape 编译一次,运行期任何尺寸直接绑定执行。把编译时间也算进总账,动态 Shape 的“综合成本”反而低得多——尤其是推理服务中 batch size 频繁波动时,静态方案会陷入“不断重编译”的泥潭,动态方案毫不在意地照单全收。
真正聪明的央厨不会只做纯标准套餐或纯单点,而是混合策略:把最常见的人数(如 4 人、8 人)做成标准化套餐快速出餐,其余人数走单点通道。TVM 实践中的调优也遵循同样的逻辑——对高频使用的 shape 做静态专用化(specialization),对低频 shape 保留动态通用 kernel。编译期甚至可以根据历史 profiling 数据,自动决定为哪些 shape 生成专用版本。
这份成本账教给我们的,不是非黑即白地选边站,而是精准评估场景、按需下注。如果你的模型永远只有固定尺寸,静态 Shape 是最优解;如果你的服务面向真实世界、输入千变万化,动态 Shape 加上混用专用化,才是真正能吃下高并发客流又不亏本的大厨智慧。
- 静态执行时间
T_static = T_kernel—— 静态方案省去调度开销,只剩纯计算。 - 动态执行时间分母把动态方案的绑定、分发、计算、分配四项全部计入。
- 性能比
η越接近 1 说明动态方案损失越小;若 η 明显小于 1,则需要用专用化来弥补。
AI 应用场景
场景一:NLP 推理服务中的动态 Batch 优化
阶段:模型推理部署
解决的问题:真实线上服务中,每秒钟到达的请求数量是波动的。传统静态 Shape 方案为每个 batch size 单独编译一个版本,请求一到 17 就得专门编译一次,延迟飙升;而固定小 batch 又浪费 GPU 算力。TVM 动态 Shape 让 batch 维度绑定为符号 N,运行时按实际并发量动态调整 batch 大小,像央厨按到店人数灵活配餐,既吃满算力又不卡响应。
具体案例:某文本分类服务部署在 GPU 上,请求到达率在 50~500 QPS 间波动。使用 TVM 动态 Shape 后,服务端将 10 个请求聚合为一个 batch 推理,batch 维度以符号 N 编译一次、运行期任意绑定。相比每次重编译的静态方案,p99 延迟从 38ms 降至 15ms,GPU 利用率从 41% 提升到 76%。
场景二:视觉大模型的多分辨率输入适配
阶段:模型编译与算子优化
解决的问题:视觉模型(如目标检测、分割)要处理不同分辨率的输入图,画面长宽比五花八门。传统做法要么把所有图 resize 到固定尺寸(损失信息),要么为每个分辨率单独编译(工程爆炸)。TVM 用 SymbolicVar 把高和宽设为符号 H、W,配合 H % 32 == 0 之类的对齐约束编译一次,运行期直接绑定任意分辨率。
具体案例:某自动驾驶感知模型需要处理 1280×720 与 1920×1080 两种主流分辨率,偶尔还有 800×600 的辅助摄像头输入。TVM 动态 Shape 方案编译一份通用硬件 kernel,运行时按实际分辨率绑定符号,配合内存池复用,整网推理只比静态最优版本慢 8%,却省去了 3 份独立编译与切换的工程成本。
场景三:ONNX 模型的跨框架动态维度迁移
阶段:模型转换与格式适配
解决的问题:ONNX 模型中的动态维度(如 batch_size 轴标为 "None")在不同框架间迁移时经常被误解为“固定为 1”,导致推理维度受限。TVM 的 SymbolicVar 机制能在导入 ONNX 时把 "None" 正确映射为符号变量,保留其动态语义,让 PyTorch 训练、ONNX 导出、TVM 部署的整条链路支持任意 batch。
具体案例:某电商推荐模型最初在 PyTorch 中训练,导出 ONNX 时 batch 轴标注为动态。团队原本在 TVM 中将其固定为 64 编译,结果双 11 大促流量翻倍后服务直接撑爆。改用 SymbolicVar 导入后,batch 从 32 到 256 连续可调,弹性扩容后服务器数量反而减少了 30%。
Python 演示
import timeimport random class SymbolicVar: """模拟 TVM 的符号变量: 编译期是名字, 运行期 bind 得到具体值.""" def __init__(self, name): self.name = name def bind(self, value): return BoundValue(self.name, value) class BoundValue: """符号变量完成绑定后的结果.""" def __init__(self, name, value): self.name = name self.value = value class StaticKernel: """静态 shape kernel: 编译期完全展开, 循环次数固定.""" def __init__(self, rows, cols): self.rows = rows self.cols = cols self.iterations = rows * cols def run(self, data): return sum(data[: self.iterations]) * 2 class DynamicKernel: """动态 shape kernel: 编译期只有符号 N, 运行期 bind 后按 N 循环.""" def __init__(self, cols): self.cols = cols self.symbol = SymbolicVar("N") self.bound = None def bind_and_run(self, rows, data): self.bound = self.symbol.bind(rows) n = self.bound.value return sum(data[: n * self.cols]) * 2 class ShapeDispatcher: """运行时调度器: 按实际 shape 选择专用 kernel, 否则退回动态 kernel.""" def __init__(self, cols): self.cols = cols self.specialized = {} self.generic = DynamicKernel(cols) def add_specialized(self, rows, kernel): self.specialized[rows] = kernel def dispatch(self, rows, data): kernel = self.specialized.get(rows, self.generic) if isinstance(kernel, StaticKernel): return kernel.run(data) return kernel.bind_and_run(rows, data) if __name__ == "__main__": data = list(range(100000)) dispatcher = ShapeDispatcher(cols=64) dispatcher.add_specialized(8, StaticKernel(8, 64)) dispatcher.add_specialized(16, StaticKernel(16, 64)) target_rows = [4, 8, 16, 8, 4, 4, 16, 8] start = time.time() for rows in target_rows: dispatcher.dispatch(rows, data) dynamic_cost = time.time() - start static_start = time.time() for _ in range(8): StaticKernel(8, 64).run(data) static_cost = time.time() - static_start print("动态调度总耗时: %.6fs" % dynamic_cost) print("纯静态(固定8行)总耗时: %.6fs" % static_cost)
N 的定义与运行时绑定、静态专用 kernel 的快速通道、以及动态通用 kernel 的兜底调度,直观对比两种方案的性能账。class SymbolicVar模拟 TVM 的tir.Var:编译期只保存名字"batch",不占具体数值。def bind(self, value)对应运行期绑定操作:符号 N 得到实际值,返回BoundValue对象。class StaticKernel模拟静态 Shape 编译产物:iterations在构造时就定死,运行期零判断。class DynamicKernel模拟动态 Shape 万能 kernel:只有符号symbol,每次运行都要现绑定。bind_and_run把“拿到 N → 以 N 为上界执行循环”封装成一步,模拟动态 kernel 的真实执行。class ShapeDispatcher模拟运行期形状分发:specialized字典保存高频专用 kernel,查不到走通用。add_specialized(8, StaticKernel(8, 64))模拟混合策略:为高频出现的 8 行提前准备静态专用版本。dispatch查表命中直接跑静态 kernel,未命中退回动态 kernel,模拟 TVM runtime 的分发逻辑。
常见误区
误区动态 Shape 不就是把 shape 设成 None,然后推理时每次想传什么形状就传什么,TVM 会自动重新编译、自动优化吗?
正解动态 Shape 的本质是编译期就为符号维度生成通用 kernel,运行时只做值绑定与分发,不是“重新编译”。TVM 在编译期看到符号 N 时,就生成了以 N 为循环上界的通用机器码,运行时把 N 换成 7 只是赋值操作。凡是想“换个形状就自动重新优化编译”的,那是 JIT 友好重编译,不是动态 Shape 的本义。
为何常见:很多人在 PyTorch 里“动态图”用惯了,以为动态 shape = 动态重编译,混淆了前端框架的 tracing 机制与编译器后端的设计理念。
误区动态 Shape 性能差,所有维度都必须想办法固定死,只有固定 shape 才能做高性能算子优化。
正解动态 Shape 的实际开销来自三处小项:绑定、分发、动态内存管理。TVM 通过编译期静态部分展开、kernel 查表分发、内存池复用,可以把总开销压到微秒级;而动态维度在整张 shape 中通常是少数,计算主体仍在静态维度上。5%~20% 的性能差换来的却是免去所有新尺寸重编译的巨大收益。盲目全静态,等于把“来 5 人就拒绝营业”当作经营策略。
为何常见:性能基准测试往往拿“静态最优 kernel”打“动态万能 kernel”,忽略了静态方案遇到新尺寸要重新编译乃至服务不可用的真实代价。
误区动态维度嘛,用什么符号表达不重要,`Any` 也是动态,命名 Var 也是动态,哪个顺手用哪个。
正解两者语义截然不同。命名 Var(如 N)代表“同一个符号贯穿全图”,不同算子间的 N 被保证是同一个值,shape 传播不会走样;Any 则是“占位通配”,表示“这一维可变但不需要跨算子记账”——它的传播能力弱,容易被收紧成具体值。用错了 Any,跨算子维度关联就会断裂,导致运行时 shape 校验失败或生成错误 kernel。
为何常见:动态 shape 概念初学时只看“可变”这一共同点,忽略了两者在 shape 传播和一致性保障上的深层分工。
误区反正编译一次跑所有尺寸,那这个万能 kernel 应该对所有 shape 都接近最优吧?
正解恰恰相反,万能 kernel 是“最一般”的代码——循环不能完全展开、内存访问模式保守、没有针对特定 N 的向量化优化。对某个高频固定 shape(如 N=8),一个静态专用 kernel 往往还能再快 10%~30%。所以 TVM 的工程实践是混合策略:高频 shape 做专用化加速,低频 shape 用万能 kernel 兜底,而不是把宝全押在万能型上。
为何常见:把“通用”误解为“全优”,忘了编译优化本质上是在“已知信息”上做文章,信息越少,优化空间越窄。
面试题
Q1. 动态 Shape 与静态 Shape 的本质区别是什么?
- A.动态 Shape 会在运行时重新编译代码
- B.静态 Shape 的维度在编译期完全确定← 正确答案
- C.动态 Shape 只能用于 CPU 推理
- D.静态 Shape 不支持 GPU 加速
答案:B。静态 Shape 在编译期就确定了全部维度,可以做循环展开等激进优化;动态 Shape 的维度是符号,运行时才绑定具体值。A 错在动态 Shape 的关键不是重编译,而是编译一次、运行期绑定;C、D 都是无依据的臆断。
Q2. 关于 SymbolicVar 机制,下列说法正确的是?
- A.符号变量编译期有确定数值
- B.符号变量运行时 bind 为具体值← 正确答案
- C.符号变量和 Python 变量无任何区别
- D.符号变量不参与算术运算
答案:B。SymbolicVar 编译期只是“名字 + 约束”,运行时通过 bind 得到具体值。A 反了,编译期符号恰恰没有确定数值;C 错在符号变量以代数形式参与推导而非持有值;D 错在实际计算时符号会被替换后参与运算。
Q3. TVM 中 tir.Var 和 tir.Any 的关键差异是什么?
- A.Any 适用于小 shape,Var 适用于大 shape
- B.Var 有名字可跨算子保持一致,Any 是通配符← 正确答案
- C.Var 只能用于动态 shape,Any 只能用于静态
- D.Var 是运行期概念,Any 是编译期概念
答案:B。命名 Var 用名字建立跨算子的维度契约,保证同一个符号处处同值;Any 只是“可变”占位,不传递一致性。其他选项都是概念错位:Var 和 Any 都服务于动态 shape,且都是编译期实体。
Q4. 动态 Shape 比静态 Shape 的额外开销主要来自哪方面?
- A.编译时间更长
- B.绑定、分发与动态内存管理← 正确答案
- C.模型精度下降
- D.算子数量增加
答案:B。动态方案编译一次即可,差异不在于编译时间;模型精度与算子数量不因 shape 策略改变。真实开销是运行期的符号绑定(bind)、kernel 查表分发(dispatch)以及每次可能变化的缓冲区分配——合称“调度税”。
Q5. 静态 Shape 编译能进行哪类激进优化?
- A.运行时动态内存分配
- B.循环完全展开与常量折叠← 正确答案
- C.每次请求重新编译代码
- D.动态修改内核代码
答案:B。静态 Shape 维度已知,编译器可以把循环完全展开、把常量表达式提前算完,消除分支与下标计算。A 是动态方案的产物,C 是伪需求,D 是运行时行为,均不是静态编译期优化。
Q6. 动态 Shape 编译期可以完成哪些优化?
- A.固定维度的循环展开← 正确答案
- B.符号表达式的常量折叠← 正确答案
- C.生成适配任意 N 的通用 kernel← 正确答案
- D.把动态维度全部替换为固定值
答案:ABC。编译期能做三类事:静态部分展开(A)、常量折叠(B)、生成通用 kernel 模板(C)。D 是错误选项——编译期根本不知道 N 的值,谈不上替换为固定值,运行时 bind 才做替换。
Q7. 混合策略中,为高频 shape 生成专用 kernel 的依据通常是什么?
- A.运行时随机选择
- B.历史 profiling 与 shape 频次统计← 正确答案
- C.算子参数的数量
- D.模型层数大小
答案:B。工程实践会根据线上 profiling 数据统计各 shape 出现的频次,对高频 shape 做静态专用化以获得最优性能,低频 shape 退回通用 kernel。这是以“数据驱动”决定编译资源投向,而非随机或按算子规模拍脑袋。
Q8. 运行时 shape 分发(dispatch)的理想时间复杂度是?
- A.O(N) 线性扫描
- B.O(1) 哈希或表查找← 正确答案
- C.O(N²) 二重循环
- D.O(log N) 的折半查找
答案:B。优秀的分发器把 shape 映射为索引,用哈希表或直接查表实现 O(1) 命中,将调度税压到微秒级。线性扫描在大规模 kernel 库下不可接受;折半查找虽可行但通常不必要。B 是工程上的最优约定。
Q9. 动态 Shape 在哪些真实场景中不可或缺?
- A.NLP 模型变长序列输入← 正确答案
- B.视觉模型多分辨率推理← 正确答案
- C.服务端 batch 大小动态波动← 正确答案
- D.固定尺寸图像分类单 batch 离线测试
答案:ABC。NLP 序列长度不定、视觉分辨率多样、服务端并发引发 batch 波动,三者都天然需要动态 Shape。D 恰好是固定尺寸 + 单 batch 的静态场景,用静态 Shape 最优,不需要动态机制。
Q10. 动态 Shape 万能 kernel 相对静态专用 kernel 性能略低,最核心的原因是?
- A.模型精度损失导致重算
- B.循环上界为变量,无法完全展开← 正确答案
- C.算子被拆分成多个小算子
- D.GPU 显存容量受限
答案:B。万能 kernel 的循环上界是符号变量,编译器无法在编译期展开全部循环,循环内必须保留边界判断与动态下标计算,阻碍了向量化和寄存器优化。这是动态方案性能折扣的最核心来源;精度、算子拆分、显存容量均与此无关。