想象一家定制家具工坊。老师傅接到订单,不会立刻把木头往机器里塞。他先铺开图纸,把每一块木板的长、宽、厚度都标出来:这块板切完剩多长,那块板刨完剩多厚,最后组装的柜门用几个抽屉。只要所有中间尺寸预先算清,整个车间就能像上了发条般运转;若算不清,工人们就得反复停下来量尺寸、调机器。 AI 编译器面临的处境几乎一样。模型没法直接“跑”,先得被翻译成一张计算图——好比工艺图纸;图中的每个加工步骤都要吞进、吐出一块有确定“形体”的数据。形状推理(Shape Inference)就是编译器在开工之前,沿着计算图静态推算出每一步输出数据块的形状,让后续的内存分配、步骤合并、代码生成都可以提前拍板,不必等运行时反复“测量”,从而省下大量性能开销。 这套思想贯穿 XLA 与 PyTorch 2.0 的编译栈:先标尺寸,再干活;标得越全,干得越快。

岗位关联

理解形状推理,是看懂 PyTorch 2.0 / XLA 编译优化思路的钥匙,也是排查“编译后为什么变慢”“动态形状为何反复重编译”等性能问题的必备技能。

目录
  1. 可视化演示
  2. 概念与意义
  3. 静态推导原理
  4. 符号形状表达
  5. 编译优化收益
  6. PyTorch 2.0实践
  7. 局限与挑战
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

演示图优化阶段Shape Inference沿算子链静态推导各层输出形状,为内存分配、算子融合与代码生成提供精确依据。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Shape Inference 算子 形状推导 融合 静态推导避免运行时检查

概念与意义

老木匠的案头功夫,从来不在锯子上。订单进门,他先铺开一张空白图纸,把每一块原木的来料尺寸、每一道工序后的半成品尺寸都写上去。柜门要四块板,每块多厚;抽屉要三条轨,每根多长——这些数字在机器启动前就定下了。图纸上多写一行字,车间里就少一次手忙脚乱的测量。

深度学习模型走进编译器时也是如此。模型先被翻译成一张计算图:节点是算子,边是流动的张量。张量的“形体”由形状(shape)描述——一个维度元组,比如 [B, 3, H, W] 依次是批次、通道、高、宽。形状推理(Shape Inference)就是编译期的一项静态分析:不真正执行任何算子,只根据算子的语义与输入形状,推算出每个输出的形状。

在 XLA 与 PyTorch 2.0 的图优化阶段,形状推理紧跟在图构建之后、内存规划与内核生成之前。它输出的“形状事实表”被下游反复消费:内存分配器按形状算缓冲大小,算子融合器按形状判断合并可能性,代码生成器按尺寸特化循环。若形状只能等到运行时才知道,编译器就只能做最保守的安排——相当于每块料都现量现裁,工效大打折扣。正因如此,形状推理被视为编译优化链条上“承上启下”的关键前置步骤。

机器还没转,但老师的图纸已经写满数字。形状推理的价值正在于此:用一次编译期的“纸上标注”,换取运行期每一步的从容。图纸上多一行尺寸,车间里就少一次等待——这就是为什么它如此重要。

Shape(y)=fop(Shape(x1),Shape(x2),…,Shape(xn))\displaystyle \text{Shape}(y) = f_{\text{op}}(\text{Shape}(x_1), \text{Shape}(x_2), \ldots, \text{Shape}(x_n))
这条式子概括了形状推理的本质:算子的输出形状,是其输入形状经算子固有规则变换后的结果。
  1. 输出形状 Shape(y)表示算子输出张量 y 的形状,是一个由各维度长度组成的元组。这是整段推导的“待求答案”。
    Shape(y)\text{Shape}(y)
  2. 变换函数 f_op每个算子自带一条“尺寸规则”,描述输入形状如何映射到输出形状,好比机器铭牌上的进料/出料尺寸对照表。
    fop(⋅)f_{\text{op}}(\cdot)
  3. 输入形状参数 Shape(x_i)第 i 个输入张量的形状。卷积有两个输入(数据与权重),全连接同理;每个输入的形状都要代入规则。
    Shape(x1),…,Shape(xn)\text{Shape}(x_1), \ldots, \text{Shape}(x_n)
  4. 结构约束输出形状只由“算子语义 + 输入形状”决定,与张量里的具体数值无关——这正是可以静态推导的根本原因。
    y=op(x1,…,xn)⇒Shape(y)=fop(Shape(x1),…,Shape(xn))y = \text{op}(x_1, \ldots, x_n) \Rightarrow \text{Shape}(y) = f_{\text{op}}(\text{Shape}(x_1), \ldots, \text{Shape}(x_n))

静态推导原理

工坊里每台机器都钉着铭牌:进料尺寸、出料尺寸、吃料深度,写得清清楚楚。老师傅按工序从第一台走到最后一台,翻一块铭牌,记一笔台账,把每个中间件的尺寸顺次填进表格。这趟“翻铭牌、记台账”的流程,就是形状推理的日常。

编译器里的每类算子同样自带“铭牌”——算子元数据中注册了一个形状规则(inference rule),描述输入形状如何映射到输出形状。推理器拿到图的输入形状后,按拓扑序遍历计算图(保证每个算子在推理时,它的输入形状都已被上游算好):遇到算子就取出它的规则,代入输入形状,算出输出形状,写进形状环境(shape env)——一张“张量名 → 形状”的映射表,供下游算子继续查用。

以卷积为例,其规则由填充(padding)p、卷积核(kernel)k 与步幅(stride)s 决定。三个参数都是编译期常量时,只要输入高宽已知,输出高宽立刻确定。若某算子的规则依赖运行时数据——比如“输出长度等于输入序列中非零元素个数”——规则返回未知(Unknown),该输出被标记为动态,不再参与静态推导。整个遍历自底向上、层层推进,形状像水一样从输入端流向输出端,直到整张图都被“标注”或标记为未知。

老师傅一路翻到底,台账越记越厚,图纸越来越清晰;遇到没有铭牌的机器,就在台账上画个问号。这个问号,就是编译器里的 Unknown——它预示了后面的工序没法全按图纸走,得留一手。

Hout=⌊Hin+2p−ks⌋+1\displaystyle H_{\text{out}} = \left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor + 1
这是卷积算子铭牌上最核心的一行:输出高度由输入高度、填充、核尺寸与步幅共同决定。
  1. 输入高度 H_in上一层输出的特征图高度,来自形状环境里已登记好的数值。它是整个公式的“被加工原料”。
    HinH_{\text{in}}
  2. 填充 2p卷积前在特征图上下各补 p 圈“假像素”,让边缘信息不被丢掉;上下各补 p,所以分子里加 2p。
    Hin+2pH_{\text{in}} + 2p
  3. 核尺寸与步幅减 k 是因为卷积核要完整落在图上;除以步幅 s 是因为每次滑动 s 个像素,能放下多少个核窗口,窗口数就是输出高度。
    Hin+2p−ks\frac{H_{\text{in}} + 2p - k}{s}
  4. 向下取整与加一尺寸必须是整数,所以向下取整;加 1 是因为从位置 0 开始计数,窗口个数 = 间隔段数 + 1。
    ⌊Hin+2p−ks⌋+1\left\lfloor \frac{H_{\text{in}} + 2p - k}{s} \right\rfloor + 1

符号形状表达

有的客户不把话说死:“柜子要三米到四米,你们看着办。”老师傅没法标一个死数字,只能在图纸上写一个大大的 H(待定),再补一行小字:H 必须是 4 的倍数,滑轨才装得上。这样的图纸依然能用,只是多了一层“带约束的弹性”。

模型部署里也常有“话不说死”的维度:批次大小随请求量浮动,序列长度随句子变化。若把所有维度都写成整数,batch 从 8 变成 16 就要重新编译一次,代价高昂。符号形状(Symbolic Shape)允许这类维度用符号变量表示——形状里的 S_b、S_h、S_w 不是具体数字,而是一个“待定但受约束”的记号。

符号变量照样参与传播:输入 [S_b, 3, S_h, S_w] 经过卷积得到 [S_b, 16, S_h-2, S_w-2]。遇到除以步幅时会引入整除要求,编译器便收集约束 S_h % 4 == 0。符号表达式既能像普通形状一样参与内存计算(表达式可化简),又给运行时留下适度的自由度——介于“完全静态”和“完全动态”之间,是工程上的经典折中。

图纸上的“H(待定)”配上“4 的倍数”的约束小字,就是符号形状的写照:自由要给,但不能无边无际。约束越清晰,编译器的优化空间就越大。

shape(x)=(Sb, 3, Sh, Sw),Sh mod 4=0\displaystyle \text{shape}(x) = (S_b,\ 3,\ S_h,\ S_w), \qquad S_h \bmod 4 = 0
符号形状 = 自由维度 + 约束条件。它让“待定尺寸”也能参与推理,同时用约束守住正确性底线。
  1. 符号批次 S_b批次维度用符号变量表示,每次调用可以是 1、8、32,只要是正整数,编译产物就能复用。
    Sb∈Z≥1S_b \in \mathbb{Z}_{\ge 1}
  2. 静态通道 3通道数在模型定义期就固定,写死成常量 3,因为它永远不会变,不需要符号化。
    33
  3. 空间符号 S_h、S_w图像高宽随输入变化,用符号表达。符号参与后续规则计算,形成符号表达式。
    Sh, SwS_h,\ S_w
  4. 整除约束S_h mod 4 = 0,保证后续步长为 4 的下采样能整除。约束即“自由度的边界”。
    Sh mod 4=0S_h \bmod 4 = 0

编译优化收益

尺寸标全的图纸,能同时喂饱车间里的三个角色:仓管按最大尺寸预订货架,裁切师傅按余料规划顺序,打磨工序直接并进切割机——半成品不必搬来搬去。尺寸,是车间运转的公共语言。

编译器里三位“角色”同样依赖形状。第一位是内存分配器:张量占用的字节数等于元素总数乘以每个元素的字节数,形状已知,分配器就能在运行前精确预留缓冲,甚至让多个中间张量复用同一块内存,而不是运行时“现租”。

第二位是算子融合器:两个算子能否融合,先看中间形状是否兼容。卷积后面跟一个逐元素激活函数,只要形状一致就能合并成一个内核,省去中间张量的写回与读入。第三位是代码生成器:固定形状可以特化内核——展开小循环、固定边界、为特定 batch 挑选最优的 GEMM 实现。再加上运行时形状检查被整个消除(每次调用都要比较尺寸的环节直接省掉),三重收益叠加,正是形状推理带来的性能红利。

图纸齐全,仓库、裁切、合并全部提前安排;形状齐全,内存、融合、特化全部胸有成竹。老师傅照图施工,编译器按形优化——“先标后做”的朴素智慧,在编译的世界里价值千金。

mem(T)=elemsize×∏i=1rdi\displaystyle \text{mem}(T) = \text{elemsize} \times \prod_{i=1}^{r} d_i
内存分配的“算账公式”:形状确定,字节数就是一目了然的乘积。
  1. 总字节数 mem(T)张量 T 占用的内存字节,是分配器拿到的最终数字。
    mem(T)\text{mem}(T)
  2. 每元素字节 elemsize与数据类型绑定,float32 为 4 字节、int8 为 1 字节。这是“单价”。
    elemsize\text{elemsize}
  3. 各维度乘积从第 1 维到第 r 维的边长全部相乘,得到元素总数。形状已知,乘积就是确定值。
    ∏i=1rdi\prod_{i=1}^{r} d_i
  4. 维度数 r张量的阶数,例如四维张量 r = 4。乘积按维度逐一展开,比如 [B, 3, 64, 64] 就是 B × 3 × 64 × 64。
    i=1,…,ri = 1, \ldots, r

PyTorch 2.0实践

工坊接了不少相似订单。老师傅把验证过的工序图挂在墙上,下次来了同尺寸的活,照着做就行。但每批木材到货,伙计仍要量一下尺寸——万一这批板子宽了两毫米,整套工序就得推翻重来。这道“到货核验”,就是形状守卫。

PyTorch 2.0 的 torch.compile 延续了同样的逻辑。Dynamo 把 Python 函数捕获成 FX 计算图,后端在图上完成形状推理与一系列优化,得到一份可复用的编译产物;产物上挂着一串形状守卫(guards)——对输入形状做出的编译期假设,例如“batch 等于 4 且高度等于 224”。编译产物上还记录了特化后的 TensorType(如 [4, 224, 224, 3]),它是“这张图纸对应哪批料”的精确签名。

下一次调用时,运行时先逐条求值守卫:实测本次输入形状,与假设逐一比对。全部通过,直接复用编译内核,把编译开销摊薄到一次次调用里;任一守卫失败,则触发重编译,新形状对应的产物同样被缓存。这也解释了“第一次慢、后面快”的现象。对于动态形状场景,用户可把部分维度标记为符号,守卫从“等于某值”放宽为“满足符号约束”,从而减少重编译频率。

墙上的工序图是编译产物,到货核验是形状守卫。核验通过则照图施工,核验失败则重新画图。形状推理的质量,决定了图纸被复用的概率——推理与守卫,本就是一枚硬币的两面。

g(x)=⋀d∈dims(x.size(d)=vd)\displaystyle g(x) = \bigwedge_{d \in \text{dims}} \bigl( x.\text{size}(d) = v_d \bigr)
形状守卫是一串“到货核验”条件:所有维度都符合图纸,才允许复用编译产物。
  1. 守卫函数 g(x)以输入张量 x 为参数的布尔函数,返回“编译假设是否仍然成立”。
    g(x)g(x)
  2. 对全部维度做合取大写的 ∧(逻辑与)把每个维度的检查结果串起来——只要有一个维度不匹配,整体就是 false。
    ⋀d∈dims\bigwedge_{d \in \text{dims}}
  3. 运行时取维度长度 x.size(d)在每次调用时实际读取张量第 d 维的长度,这是“到货实测值”。
    x.size(d)x.\text{size}(d)
  4. 编译期记录值 v_d当初形状推理时使用的假设值,是“图纸标注值”。实测值等于 v_d,守卫才通过。
    =vd= v_d

局限与挑战

手艺再好的工坊,也会碰上难缠的单子:客户现场改尺寸,或要求“先看料再定方案”。这时图纸标不全,老师傅只能交代工人边做边量、做到哪算哪。不是老师傅不专业,而是这类活从来就没有“图纸上的答案”。

形状推理也有标不出的形状。最常见的来源是控制流:计算图里出现 if/while,两个分支的输出形状不一致,汇合处填什么?还有数据依赖形状:输出形状由输入张量的内容决定,例如“返回所有非零行”。这类情形在静态阶段无法确定,只能标记为 Unknown。

编译器对 Unknown 的应对是回退(fallback):放弃本段静态优化,退回解释执行,或在运行时做动态形状检查,甚至为每个新形状重新编译。回退带来性能损失,但保住正确性。另一重挑战是复杂度:符号约束越多,求解与化简越贵,编译本身可能变慢。因此工程实现普遍采用“先静态、再符号、后动态”的渐进策略,并以缓存缓解重编译的代价。

标不全的图纸配上一套“现场测量”的备案,订单照样能交付,只是慢一些。形状推理的局限不是失败——它替编译器划清了“哪些该静态做、哪些必须动态做”的边界,让回退变得有条不紊。图纸上的问号,同样是专业的一部分。

merge(d1,d2)={d1if d1=d2⊥otherwise\displaystyle \text{merge}(d_1, d_2) = \begin{cases} d_1 & \text{if } d_1 = d_2 \\ \bot & \text{otherwise} \end{cases}
控制流汇合处的“形状仲裁”:两个分支给出的尺寸一致就通过,不一致就只能标记为未知。
  1. 合并函数 merge控制流汇合处把两个分支的输出形状合并成单一形状。它必须给出一个“两全”的答案。
    merge(d1,d2)\text{merge}(d_1, d_2)
  2. 两个分支维度 d_1、d_2来自 if 的两个出口。如果两者相等则没有问题,直接取该值。
    d1=d2d_1 = d_2
  3. 未知符号 ⊥两者不等时,没有任何一个静态数字能同时满足两个分支,输出标记为未知(bottom)。
    ⊥\bot
  4. 回退语义merge 结果为 ⊥ 时,整个算子链的形状推断失败,编译器进入运行时回退路径。
    otherwise⇒⊥\text{otherwise} \Rightarrow \bot

AI 应用场景

推理引擎部署优化(TensorRT / ONNX Runtime)

阶段:图优化 / 部署优化

解决的问题:传统推理服务中,形状信息缺失导致每次调用都要动态分配内存、执行形状检查,GPU 空转率高。形状推理让引擎预先知道所有中间张量的尺寸,可以一次性规划显存池、实施算子融合与内核特化,把“边跑边量”变成“照图施工”。

具体案例:某视觉团队把 YOLO 模型导出为 ONNX 并部署到 ONNX Runtime,开启形状推理与内存优化后,固定分辨率下的请求延迟下降约 30%;在动态 batch 服务中用符号维度覆盖常见批次,吞吐量提升约 2 倍。

torch.compile 训练加速

阶段:图捕获与编译优化

解决的问题:训练循环逐迭代执行 Python 解释器,开销巨大。torch.compile 通过 Dynamo 捕获计算图、形状推理与后端优化,把迭代编译成高效内核;然而输入形状一变就重编译,是动态形状训练的核心痛点。

具体案例:NLP 训练中句子长度不等,团队将序列维度设为符号维度并开启动态形状支持,使同一批编译内核覆盖多种长度,重编译次数从每步一次降到数百步一次,训练吞吐显著提升。

大模型服务端批量调度(vLLM 风格)

阶段:服务端图缓存与运行时调度

解决的问题:大模型推理时每个请求的 token 数不同,batch 内序列长度持续变化。若每次新长度都重新编译,首 token 延迟会不可接受。形状守卫配合符号形状,让一组预编译内核覆盖一段长度区间,用轻量核验代替整轮编译。

具体案例:类 vLLM 的服务端在启动时为常见长度区间预编译 attention 内核,运行时通过形状守卫快速匹配,动态 batch 场景下的 TTFT(首 token 时延)大幅下降,GPU 利用率显著提升。

Python 演示

python96 行
"""极简形状推理引擎: 按拓扑序遍历计算图,用算子注册的形状规则静态推断中间张量形状."""  class ShapeEnv:    """形状环境: 一张'张量名 -> 形状'的台账"""     def __init__(self):        self.shape_map = {}     def bind(self, name, shape):        self.shape_map[name] = shape     def lookup(self, name):        return self.shape_map.get(name)  def infer_conv2d(out_ch, kh, kw):    """卷积规则: 高宽按 (H - k + 1) 收缩"""     def _rule(in_shape):        n, _, h, w = in_shape        return (n, out_ch, h - kh + 1, w - kw + 1)     return _rule  def infer_reshape(target):    """reshape 规则: -1 表示按总数与其余维度反推"""     def _rule(in_shape):        if target[0] != -1:            return tuple(target)        total = 1        for d in in_shape:            total *= d        rest = 1        for d in target[1:]:            rest *= d        return (total // rest,) + tuple(target[1:])     return _rule  def infer_linear(out_features):    """全连接规则: 保留前导维, 末维变为 out_features"""     def _rule(in_shape):        return tuple(in_shape[:-1]) + (out_features,)     return _rule  class MiniCompiler:    """迷你编译器: 注册规则 + 编译图并执行形状推理"""     def __init__(self):        self.rules = {}     def register(self, op_name, rule_factory):        self.rules[op_name] = rule_factory     def compile_graph(self, graph, input_shape):        env = ShapeEnv()        env.bind(graph["input"], input_shape)        for node in graph["nodes"]:            rule = self.rules[node["op"]](*node["params"])            in_shape = env.lookup(node["inputs"][0])            out_shape = rule(in_shape)            env.bind(node["output"], out_shape)            print(node["name"], "->", out_shape, "已标注")        return env  def main():    compiler = MiniCompiler()    compiler.register("conv2d", infer_conv2d)    compiler.register("reshape", infer_reshape)    compiler.register("linear", infer_linear)    graph = {        "input": "img",        "nodes": [            {"name": "conv1", "op": "conv2d", "params": (16, 3, 3),             "inputs": ["img"], "output": "conv1"},            {"name": "flatten", "op": "reshape", "params": (-1, 16),             "inputs": ["conv1"], "output": "flat"},            {"name": "fc", "op": "linear", "params": (10,),             "inputs": ["flat"], "output": "fc"},        ],    }    print("=== 形状推理: 开工前标注尺寸 ===")    compiler.compile_graph(graph, (1, 3, 32, 32))  if __name__ == "__main__":    main()
目的:实现了一个微型形状推理引擎,模仿编译器“注册算子规则 + 按拓扑序传播 + 登记形状环境”的完整流程,输入一张三节点计算图与初始形状,输出每一层算子的标注结果。
  1. self.shape_map = {}台账初始化:张量名到形状的映射,起步为空。
  2. def register(self, op_name, rule_factory):给算子贴铭牌:把规则注册进编译器。
  3. rule = self.rules[node["op"]](*node["params"])查铭牌取规则,并注入卷积核尺寸等参数。
  4. in_shape = env.lookup(node["inputs"][0])从台账读出输入形状,作为推导原料。
  5. out_shape = rule(in_shape)套用规则算出输出形状,即铭牌上的进料-出料对照。
  6. env.bind(node["output"], out_shape)把新形状登记进台账,供下游算子查询。
  7. compiler.compile_graph(graph, (1, 3, 32, 32))给出输入形状,启动整张图纸的标注。

常见误区

误区编译器已经把每个张量的形状都静态推出来了,运行时就不该再有任何形状检查;看到检查代码就说明没优化好。

正解形状推理产生的是编译期假设,不是运行时事实。PyTorch 2.0 的 guards、TensorRT 的 binding 校验都是必须的“到货核验”——假设可能被打破,核验能防止内存越界或错误结果,代价极低。图纸标得再全,材料到货也得量一下。

为何常见:只看到静态分析“神机妙算”的一面,忽略了任何静态假设都必须靠运行时校验兜底,这是编译器安全设计的常识。

误区我用 Python 随时能打印 tensor.shape,运行时看一眼不就知道了?形状推理大概就是这么回事。

正解两者阶段完全不同。tensor.shape 是运行时查询已存在张量;形状推理发生在编译期、算子尚未执行时,靠算子规则“预判”未来的输出形状,结果写入编译产物。打印是“量已经做好的料”,推理是“还没开工就把图纸标好”。

为何常见:日常开发接触 eager 模式多,对编译期静态分析缺少直观感受,容易把“运行时查形状”与“编译期推形状”混为一谈。

误区形状推理这么重要,编译器肯定能把图里所有算子的输出形状都精确推出来,不然怎么叫优化。

正解控制流分支导致形状分叉、数据依赖形状(如按内容裁剪)等都无法静态确定,只能标 Unknown 并回退。符号形状虽然扩大覆盖,但本质边界仍在。推理能力是“尽可能”而非“全部”,局限源于问题的内在难度,不是实现偷懒。

为何常见:教材多讲形状推理的成功案例,很少聊失败路径;初学者把“大多数场景有效”放大成“所有场景必然”。

误区batch 大小变就变,反正显存够用,为什么编译器非要纠结形状?

正解显存够用只保证“能跑”,不保证“跑得快”。形状未知时无法预分配内存、无法判断算子融合、无法特化内核,性能可能损失数倍。形状推理的意义就在于把这些隐藏损失提前消除——省下的不是内存,是时间。

为何常见:纯应用视角下正确性立即可见,性能损耗藏在 profiling 数据里,容易被“能跑就行”的思维忽略。

面试题

Q1. 形状推理(Shape Inference)在图优化阶段主要解决什么问题?

单选题 · easy

  • A.静态推断每个算子输出的形状← 正确答案
  • B.压缩模型权重参数量
  • C.将浮点运算转换为整数运算
  • D.自动选择最优的 GPU 设备

答案:A。形状推理是编译期的静态分析,核心是推断算子输出张量的形状。B 属于剪枝/压缩,C 属于量化,D 属于设备调度,都不是形状推理的目标。类比:形状推理像在图纸上标尺寸,而不是决定用哪种木材。

Q2. 在 PyTorch 2.0 编译栈中,形状守卫(Shape Guard)的作用是什么?

单选题 · easy

  • A.验证输入形状匹配编译假设← 正确答案
  • B.防止模型参数被意外修改
  • C.限制网络模型的最大层数
  • D.限制 GPU 显存占用上限

答案:A。守卫是挂在编译产物上的一组形状断言,每次调用先检查,全部通过则复用编译产物,否则触发重新编译。B 是参数保护,C 是结构限制,D 是显存管理,都与守卫无关。

Q3. 卷积层输出高度公式中 H_out = floor((H_in + 2p - k)/s) + 1,符号 p 代表什么?

单选题 · easy

  • A.卷积核的高度
  • B.卷积的滑动步幅
  • C.边界的填充宽度← 正确答案
  • D.空洞的扩张比率

答案:C。p 是 padding(填充)宽度,上下各补 p 个像素,所以公式里是 2p。k 是卷积核高度,s 是步幅,dilation 是扩张率。类比:填充相当于在木板边缘加保护条,保证边缘信息不被一刀切掉。

Q4. 下列哪种情况最适合用符号形状(符号维度)表达?

单选题 · mid

  • A.输入图片分辨率固定不变
  • B.批次维度在不同调用间可变← 正确答案
  • C.模型输出概率分布的数值
  • D.权重矩阵数值始终保持恒定

答案:B。符号形状用于“运行时才确定、但始终满足某些约束”的维度,典型就是可变 batch。A 是固定维度,用常量表达即可;C/D 是数值而非形状,与符号形状无关。类比:图纸上写“H(待定)”正是为可变尺寸准备的。

Q5. 静态形状推理给编译器带来的收益不包括下列哪项?

单选题 · mid

  • A.实现精确的内存预分配
  • B.支撑算子融合的决策
  • C.消除运行时的形状动态检查
  • D.直接提升浮点计算的精度← 正确答案

答案:D。形状推理影响内存、融合、代码生成与运行时检查,但它不改变数值计算本身,浮点精度由数据类型与算法决定。D 是把“性能优化”误当成了“数值优化”。类比:尺寸标全能让车间跑得快,但改变不了木料本身的材质。

Q6. 当两个控制流分支的输出形状不一致时,merge 的结果通常是?

单选题 · mid

  • A.取两个形状的并集
  • B.标记为未知并触发回退← 正确答案
  • C.自动填充成两者最大值
  • D.随机选择其中一个形状

答案:B。merge 的语义是“找不到同时满足两个分支的静态答案就返回 ⊥(未知)”。编译器随后回退到动态检查或解释执行,而不是强行填充。类比:两个方案尺寸不统一,图纸上只能打问号,等现场决定。

Q7. 形状传播中产生整除约束(如 S_h % s == 0)的根本原因是?

单选题 · hard

  • A.算子规则要求维度必须被步幅整除← 正确答案
  • B.数据加载器随机采样的结果
  • C.梯度裁剪阈值带来的限制
  • D.学习率调度策略引入的约束

答案:A。卷积/池化等算子在计算输出尺寸时用步幅做除法,符号维度下无法预知整除是否成立,于是把 S_h % s == 0 作为约束条件收集起来。B/C/D 与算子几何语义无关。类比:滑轨必须等分安装,尺寸约束来自装配工艺而非客户喜好。

Q8. 静态形状推理与下列哪些编译优化环节密切相关?

多选题 · mid

  • A.内存分配过程← 正确答案
  • B.算子融合决策← 正确答案
  • C.权重初始设定
  • D.内核代码生成过程← 正确答案

答案:ABD。内存分配需要形状算字节数,融合需要判断相邻算子形状兼容,代码生成需要形状特化循环与内核。权重初始化发生在模型定义/训练阶段,与编译期的形状推理无关。类比:尺寸齐全后,仓库、合并工位、机器编程都能提前安排。

Q9. PyTorch 2.0 中缓解动态形状重编译开销的机制包括哪些?

多选题 · hard

  • A.形状守卫的缓存复用机制← 正确答案
  • B.动态维度的符号化表示← 正确答案
  • C.强制所有维度静态化处理
  • D.编译图结果的多次复用← 正确答案

答案:ABD。守卫决定缓存能否复用;符号化让一个产物覆盖多个相似形状;按形状缓存可以命中历史编译结果。C 恰好相反——强制静态化会让动态输入频繁触发重编译。类比:合同里写“高 H(待定),H 为 4 的倍数”,比写死每个数字更灵活。

Q10. 下列属于形状推理典型挑战的有哪些?

多选题 · extreme

  • A.控制流分支导致形状不一致← 正确答案
  • B.符号约束求解的复杂度上升← 正确答案
  • C.静态形状完全覆盖所有算子
  • D.未知维度触发编译回退损失← 正确答案

答案:ABD。控制流让 merge 返回未知;符号约束越多求解越贵;未知维度导致回退到动态/重编译。C 是理想化设想而非现实挑战。类比:现场改尺寸的订单,图纸标不全,只能边做边量——这就是局限的本质。