想象你要准备一桌丰盛的年夜饭。菜单上有一道糖醋排骨,配料栏赫然写着“加 3 勺白糖,再额外加 2 勺白糖”。聪明的厨师绝不会等到下锅炒菜时才开始数糖,他会在备菜阶段直接改成“加 5 勺白糖”——反正都是固定要加的,提前算好天经地义。在 AI 的世界里,程序就像一份菜谱,编译器就是那个厨师。代码里经常写着 x = 3 * 4 + 2 这样的句子,如果每次运行都要重新算一遍 3×4,那实在太浪费了。编译器会在“开火”之前就把 3×4+2 算成 14,直接把 x 的位置摆好一个固定答案。这个“提前算好固定答案”的过程,就叫常量折叠(Constant Folding),它是图优化中最基础、也最常用的一招。看似简单,却贯穿了从 PyTorch 到 XLA 的整条 AI 编译链路,是理解所有编译器优化的第一块敲门砖。

岗位关联

掌握常量折叠,你就拿到了理解 AI 编译器优化日志的第一把钥匙。无论在 PyTorch 训练加速、XLA 编译调优还是推理部署瘦身中,都能迅速定位“哪些计算本该在编译期消失”。

目录
  1. 可视化演示
  2. 概念入门
  3. 折叠算法
  4. 实现机制
  5. XLA 实战
  6. PyTorch 实战
  7. 性能影响
  8. 工程挑战
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示常量折叠三步流程(识别→求值→替换)、不动点迭代收敛过程及标量/张量/Shape三类折叠,切换模式查看不同优化机制。

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

常量折叠流程:识别→求值→替换 识别所有输入为编译期常量的算子节点(如ADD的输入2和3) 编译期求值后替换为常量节点(2+3→5),消除运行时计算 fold(ADD, 2, 3) → Const(5) 折叠减少算子数量,简化图结构。

概念入门

年夜饭备菜时,厨师会把“3 勺糖 + 2 勺糖”改成“5 勺糖”。这个动作看似不起眼,却暗含一个深刻的道理:只要操作对象全是固定的,结果就是固定的,提前做掉天经地义。AI 程序同样如此。模型代码会被编译成一张“计算图”——你可以把它想象成一张描述数据如何流动的菜谱,每个节点是一次运算(加法、乘法、卷积),每条边是数据的传递。当某个节点所有的输入都是固定值(常量)时,编译器就看到了一个“备菜机会”:直接把这道菜做出来,把结果放冰箱里等着用。

这个机会就是常量折叠。在编译优化领域,它专指在编译期计算那些输入全部为常量的表达式,并用计算结果替换原子图的操作。比如:

在图优化 pass 管线中,常量折叠几乎总是最早执行的优化之一。它先把图中的“死肉”剔掉,让图变小,后续的算子融合、内存规划、指令调度才能在更清爽的图上大展拳脚。回到年夜饭的比喻:提前把糖称好,不仅炒菜时少两道工序,也让灶台更整洁、大厨更从容。这就是常量折叠在 AI 编译里的定位——用编译期的一点时间,换运行期一路顺畅。

e=3×4+2  →fold  e=14e = 3 \times 4 + 2 \;\xrightarrow{\text{fold}}\; e = 14
这个公式展示了常量折叠的缩影:一条含两个固定运算的表达式,在编译期被整体求值,变成一个不能再简单的常量。
  1. 原始表达式这个式子包含一次乘法和一次加法,三个操作数 3、4、2 都是字面量常量
    e=3×4+2e = 3 \times 4 + 2
  2. 内层乘法按运算优先级,3×4 是最内层子表达式,编译器可先对它求值
    3×4=123 \times 4 = 12
  3. 折叠符号箭头上的 fold 表示“编译器执行折叠变换”这一动作
    →fold\xrightarrow{\text{fold}}
  4. 折叠结果整个表达式收缩为单个常量 14,运行期只需读取,无需运算
    e=14e = 14

折叠算法

备了这么多年菜,厨师总结出一套固定检查流程:这道菜的原料是不是都来自“固定仓库”?如果是,当场做好放保鲜盒;如果不是,留在下锅时再处理。编译器对计算图节点的检查逻辑与此完全一致——这就是折叠算法的主干。

算法的输入是一张计算图,输出是折叠后的新图。核心是对每个节点 n 做一次决策:

把这套决策落到工程实现上,经典流程分四步。第一步,常量子图识别:扫描全图,标记常量节点以及“所有输入都是常量”的可折叠节点。第二步,表达式求值:用编译期内置的求值器(类似一个迷你解释器)把子图算出结果。第三步,节点替换:用新的常量节点替换整棵子图。第四步,死代码消除:被替换掉的旧节点没有任何引用,顺手从图中删掉。这套流程像厨师把备好的菜放进保鲜盒后,把用过的砧板清洗归位——整个后厨焕然一新。

fold(n)={eval(n),∀ v∈inputs(n): isConst(v)n,otherwise\text{fold}(n) = \begin{cases} \text{eval}(n), & \forall\, v \in \text{inputs}(n):\ \text{isConst}(v) \\ n, & \text{otherwise} \end{cases}
这段公式是折叠算法的“决策开关”:只有当节点的每一个输入都是常量时,才调用求值器原地算出结果;否则原样保留,留给运行时处理。
  1. 折叠函数fold(n) 表示对节点 n 做折叠决策,返回值是替换后的节点
    fold(n)\text{fold}(n)
  2. 全称量词∀ 读作“对于每一个”,它遍历 n 的全部输入节点 v,一个不落
    ∀ v∈inputs(n)\forall\, v \in \text{inputs}(n)
  3. 常量判定isConst(v) 检查输入节点 v 是否为编译期可知的常量
    isConst(v)\text{isConst}(v)
  4. 求值分支当所有输入都通过判定,就用求值器算出常数值并替换原子图
    eval(n)\text{eval}(n)
  5. 保留分支否则保持节点 n 不动,等待运行时传入真实数据再计算
    otherwise: n\text{otherwise}:\ n

实现机制

后厨不是想什么时候备菜就什么时候备。主厨会把“提前备菜”写进每日流程表,固定在下午三点执行,风雨不改。编译器也一样,常量折叠作为一个独立优化 pass,被安排在优化管线的固定位置,严格遵守执行顺序。

在编译器架构中,pass 是“对中间表示做一次遍历变换”的最小优化单元。前端(负责把 Python 代码变成中间 IR)和后端(负责 IR 到机器码)各有自己的中间表示,也都各自实现了一份常量折叠。以常见的执行顺序为例:shape 推断确认每个张量的形状 → 常量折叠消除常量子图 → 算子融合把相邻算子合并成一个 kernel。折叠放在 shape 推断之后是刻意的——shape 推断能算出哪些节点的输出形状也是常量,这给折叠提供了更多线索;而折叠把图变小后,算子融合处理的候选节点变少,融合效率更高。

这个 pass 化的设计带来一个好处:优化策略可以自由组合、按需插拔。比如调试时跳过折叠 pass,就能保留中间节点;生产环境下开启它,就能获得极致性能。就像主厨的流程表上,“备菜”这一栏是可以根据当天情况勾选或跳过的——但一旦勾上,就会在固定时间、按固定标准执行,绝不手忙脚乱。

P: G↦G′,G′=fold(G)\mathcal{P}:\ G \mapsto G', \quad G' = \text{fold}(G)
这个公式把“一次优化 pass”抽象成“输入图到输出图的映射”:P 是一个优化动作,fold(G) 表示对图 G 逐节点应用折叠规则后得到新图 G'。
  1. 优化器 PP 表示一次具体的编译优化过程,这里是常量折叠 pass
    P\mathcal{P}
  2. 图映射箭头↦ 表示从输入图 G 得到输出图 G',强调“图到图的变换”
    G↦G′G \mapsto G'
  3. 折叠算子fold(G) 把折叠规则作用到图 G 的每个节点上,产出新图
    G′=fold(G)G' = \text{fold}(G)

XLA 实战

如果说 PyTorch 默认模式是“边看菜谱边炒菜”的小饭馆,那 XLA 就是大型中央厨房:把整张菜单(整个模型的计算图)拿过来统一规划,所有菜品在开张前就被模块化备好。XLA 是 Google 推出的 AI 编译器,它的中间表示叫 HLO(High Level Operations),可以理解为“中央厨房的食材清单与工序表”。

在 XLA 中,常量折叠是 HLO 优化管线里最勤快的帮厨。它做的第一件事是 HLO 折叠:扫描整张 HLO 图,把输入全为常量的子图(如 constant(3) × constant(4))在编译期算完。第二件事是跨算子优化:不只折叠单个算子,像 reshape、transpose、slice 这类“搬运型”算子,只要输入是常量,也会被整段算掉。第三件事是设备端常量:折叠后的常量直接编入设备二进制,TPU/GPU 运行时不再为这些常量准备额外内存。XLA 还配有编译缓存,相同形状的模型编译一次后,后续直接命中缓存。

正是这套“整图备菜”的流程,让 XLA 在 TPU 上能把模型启动时间压到极低。对于大模型训练,每次迭代都要把输入数据灌进设备,HLO 图越小、常量越少,数据传输与设备初始化就越快。中央厨房的哲学在这里体现得淋漓尽致:能在开张前做完的,绝不占开张后的灶台。

HLO→ConstFoldHLO′\text{HLO} \xrightarrow{\text{ConstFold}} \text{HLO}'
这个公式表示 XLA 的常量折叠 pass 对 HLO 图的整体改造:原图 HLO 经过 ConstFold 变换,得到折叠后的新图 HLO'。
  1. 输入 HLO 图折叠的对象是 XLA 内部的 HLO 中间表示,而非 Python 源码
    HLO\text{HLO}
  2. ConstFold 动作这是 XLA 优化管线中常量折叠 pass 的代号,作用在整张图上
    →ConstFold\xrightarrow{\text{ConstFold}}
  3. 输出新图HLO' 表示折叠后的图,图中所有可提前计算的常量子图已消失
    HLO′\text{HLO}'

PyTorch 实战

PyTorch 默认的 eager 模式就像“边看菜谱边炒菜”:每执行一行 Python 代码,就立刻调用底层算子算一次。这种方式灵活性极高,但也意味着同样的固定计算会被反复执行。TorchScript 和 Dynamo 的出现,就是为了给 PyTorch 补上“提前背菜谱”的能力:先把 Python 函数追踪成静态计算图,再对图做优化,其中就包括常量折叠。

TorchScript 是 PyTorch 的静态编译工具,通过 torch.jit.trace 或 torch.jit.script 把模型变成可序列化的图表示。JIT 追踪时,凡是直接由 Python 数值算出的中间量都会被标记为常量,进入折叠流程。Dynamo(torch.compile 的底层引擎)则更进一步,它拦截 Python 字节码,把子图抽出来编译,在编译 IR 上同样应用常量折叠。最经典的落地案例是推理期 BatchNorm 折叠:BatchNorm 在训练时均值、方差是统计量,但部署后这些参数全部冻结为常量,于是可以整体折叠进卷积层:

背过菜谱再去炒菜,虽然备菜阶段多花了一点时间,但真正上菜时一步到位。PyTorch 的开发者正是利用这种“动态转静态”的编译哲学,让 eager 模式的灵活性与编译优化兼顾,这也是 torch.compile 成为大模型训练标配的原因。

BN(x)=x−μcσc2+ϵ×γc+βc  ⇒  BN(x)=x×wc+bc\text{BN}(x) = \frac{x - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}} \times \gamma_c + \beta_c \;\Rightarrow\; \text{BN}(x) = x \times w_c + b_c
这个公式揭示了 BatchNorm 折叠的数学本质:推理时 μ、σ、γ、β 全是常量,除法和乘法可以合并成一个新权重 w,所有加减项合并成一个新偏置 b,归一化层就此隐形。
  1. BN 原始计算推理阶段均值 μ、方差 σ、缩放 γ、偏移 β 都已冻结为常量,下标 c 表示"constant"
    BN(x)=x−μcσc2+ϵ×γc+βc\text{BN}(x) = \frac{x - \mu_c}{\sqrt{\sigma_c^2 + \epsilon}} \times \gamma_c + \beta_c
  2. 权重合并常数除法与乘法结合成一个新权重 w_c,这是折叠中的“乘除项归并”
    wc=γcσc2+ϵw_c = \frac{\gamma_c}{\sqrt{\sigma_c^2 + \epsilon}}
  3. 偏置合并所有与 x 无关的加减项合并成偏置 b_c,这是“加减项归并”
    bc=βc−γcμcσc2+ϵb_c = \beta_c - \frac{\gamma_c \mu_c}{\sqrt{\sigma_c^2 + \epsilon}}
  4. 折叠结果整个 BN 变成一个线性变换,算子数减半,推理时少一次全张量遍历
    BN(x)=x×wc+bc\text{BN}(x) = x \times w_c + b_c

性能影响

备菜的直接好处很直观:炒菜快、台面干净、食材不浪费。常量折叠在性能上的收益也可以逐一量化。

第一是启动提速。模型初始化阶段往往有大量只依赖超参数的固定计算,例如位置编码、attention mask、旋转位置编码的 cos/sin 表。eager 模式下每次启动都从头算一遍,折叠后这些计算在编译期完成,启动时间显著缩短。第二是内存节省。常量子图不再于运行时创建临时张量,设备内存的峰值占用随之下降。第三是模型瘦身。折叠后的常量可以序列化成紧凑的缓存文件,部署包变小、加载变快。第四是权衡代价:折叠本身消耗编译时间,但对单图而言复杂度近线性,且“一次编译、多次运行”的训练/推理场景下,这笔投入的回报极高。

这也解释了为什么大模型训练格外重视常量折叠:一个位置编码表在几万步训练中被反复使用,折叠后单步省下的几毫秒乘以几万步,就是几十分钟的净收益。主厨坚持下午三点备菜,多花半小时,换来的是饭点高峰期一小时的心不慌——这买卖太划算了。

ΔT=Tconst×Ncalls\Delta T = T_{\text{const}} \times N_{\text{calls}}
这个公式量化了常量折叠省下的总时间:把每个常量子图单次运行耗时,乘以它在训练/推理中实际执行的次数。
  1. 单次省时T_const 表示某个常量子图每次运行时消耗的时间
    TconstT_{\text{const}}
  2. 调用次数N_calls 表示该子图在整个训练或推理过程中被执行的次数
    NcallsN_{\text{calls}}
  3. 总节省两者相乘就是折叠省下的总时间;调用次数越多,收益越可观
    ΔT=Tconst×Ncalls\Delta T = T_{\text{const}} \times N_{\text{calls}}

工程挑战

备菜不是没有风险:菜谱抄错、食材提前切好会氧化、客人临时改菜单。常量折叠在工程落地时同样横着四道坎。

第一道坎是精度风险。浮点运算不满足结合律,(a+b)+c 和 a+(b+c) 的结果可能有细微差异。编译期求值器与运行期 kernel 若采用不同的求值顺序或精度策略,折叠后的结果与折叠前会有毫厘之差。对训练影响尚可,对某些对数值敏感的推理场景则是致命伤。第二道坎是副作用问题。random()、print()、读取系统时间这类操作不能被折叠——提前算出结果与运行期实时调用在语义上完全不同。第三道坎是调试困境。折叠后中间节点从图中消失,开发者在排查数值问题时看不到过程值,只能看到最终常量。第四道坎是边界处理:动态 shape、控制流依赖的场景下,节点是否“真的是常量”需要格外谨慎,不能把运行时才能确定的值当成常量提前算掉。

好厨师会在备菜时留个心眼:能提前的提前,不能提前的绝不动手。常量折叠的工程分寸正是如此——每一种风险背后都对应一条设计红线:只折叠纯函数、对浮点运算做严格的 IEEE 兼容测试、为调试模式保留旁路开关。理解了这些边界,你才算真正握住了常量折叠这把工具,而不只是听过它的名字。

ϵrel=∣vfold−vruntime∣∣vruntime∣\epsilon_{\text{rel}} = \frac{|v_{\text{fold}} - v_{\text{runtime}}|}{|v_{\text{runtime}}|}
这个公式定义了折叠值与运行值的相对误差,是衡量精度风险的标尺。
  1. 两者之差先计算折叠结果 v_fold 与运行期真实结果 v_runtime 的差
    vfold−vruntimev_{\text{fold}} - v_{\text{runtime}}
  2. 取绝对值竖线 |·| 去掉符号,只保留误差大小
    ∣vfold−vruntime∣|v_{\text{fold}} - v_{\text{runtime}}|
  3. 相对误差除以运行值的绝对值做归一化,得到与量级无关的相对误差指标
    ϵrel=∣vfold−vruntime∣∣vruntime∣\epsilon_{\text{rel}} = \frac{|v_{\text{fold}} - v_{\text{runtime}}|}{|v_{\text{runtime}}|}

AI 应用场景

场景:大模型训练启动加速

阶段:训练前编译与图优化

解决的问题:GPT 类模型初始化时要生成旋转位置编码(RoPE)的 cos/sin 查找表、attention mask、因果掩码等,这些张量只依赖序列长度和超参数,是天然的常量。eager 模式下每次启动训练都要重新算一遍,浪费 GPU 空转时间,且每次输出的字节都一样,纯属重复劳动。

具体案例:某团队训练 70B 参数模型时发现,光 RoPE 预计算表每次启动就要花 40 秒。改用 torch.compile 并让编译期完成常量折叠后,启动时间降到 8 秒,且折叠后的常量表直接缓存在编译产物中,后续多次启动共享。

场景:推理引擎部署瘦身

阶段:推理图优化与部署

解决的问题:ONNX 模型导出后,图里残留大量输入为常量的 Transpose、Reshape、Concat 节点,运行时要逐个启动 kernel 处理,GPU 利用率低,部署包也臃肿。

具体案例:某推荐系统把排序模型导出为 ONNX 后用 ONNX Runtime 推理。开启 ConstantFolding 优化后,模型参数文件减少 18%,单次推理延迟从 3.2ms 降到 2.1ms。因为常量子图在编译期被算掉,运行时少执行了几十个算子,GPU 的 kernel 启动开销大幅下降。

场景:异构设备代码生成

阶段:编译器后端代码生成

解决的问题:TPU、NPU 等专用芯片不支持通用的 Python 逻辑,也不希望在设备端处理常量运算。所有常量计算必须在 host 端编译期完成,设备只负责真正的张量算子。

具体案例:XLA 在 TPU 上编译 Stable Diffusion 的 UNet 时,ConstantFolding pass 把 attention mask、时间步嵌入等常量子图在 host 端算完,设备端 HLO 图更小、执行时间更短。编译缓存让同一结构不同权重的模型只会重新折叠一次,后续编译毫秒级完成。

Python 演示

python60 行
class ConstantFolder:    """极简常量折叠器: 把计算图中所有输入均为常量的节点提前求值。"""     def __init__(self):        self.constants = {}        self.graph = None     def is_const(self, node):        return node['op'] == 'const'     def eval_op(self, op, args):        if op == 'add':            return args[0] + args[1]        if op == 'mul':            return args[0] * args[1]        if op == 'sub':            return args[0] - args[1]        raise ValueError("unsupported op: " + op)     def fold(self, graph):        self.graph = graph        for node in graph['nodes']:            if self.is_const(node):                self.constants[node['id']] = node['value']        changed = True        while changed:            changed = False            for node in graph['nodes']:                if node['id'] in self.constants:                    continue                if all(inp in self.constants for inp in node['inputs']):                    val = self.eval_op(                        node['op'],                        [self.constants[inp] for inp in node['inputs']]                    )                    self.constants[node['id']] = val                    changed = True        return self.constants  def build_demo_graph():    # 计算图: n0=3, n1=4, n2=2, n3=n0*n1, n4=n3+n2, n5=n4*n1    return {        'nodes': [            {'id': 'n0', 'op': 'const', 'value': 3},            {'id': 'n1', 'op': 'const', 'value': 4},            {'id': 'n2', 'op': 'const', 'value': 2},            {'id': 'n3', 'op': 'mul', 'inputs': ['n0', 'n1']},            {'id': 'n4', 'op': 'add', 'inputs': ['n3', 'n2']},            {'id': 'n5', 'op': 'mul', 'inputs': ['n4', 'n1']}        ]    }  if __name__ == '__main__':    graph = build_demo_graph()    folder = ConstantFolder()    result = folder.fold(graph)    for node_id in sorted(result):        print(node_id + ' = ' + str(result[node_id]))
目的:在极简计算图上实现一个完整的常量折叠器,演示“识别常量 → 编译期求值 → 迭代传播折叠结果”的完整流程。
  1. self.constants = {}用字典记录“节点 id → 折叠后的常量值”,相当于后厨的保鲜盒标签
  2. if self.is_const(node):第一遍扫描,登记所有天生就是常量的叶子节点,像先把固定食材清点入库
  3. all(inp in self.constants for inp in node['inputs'])折叠算法的核心判据:一个节点的所有输入都已进入保鲜盒,它自己也就可折叠了
  4. self.eval_op(node['op'], [...])用算子的语义当场做运算,这就是编译期求值器的最小实现
  5. changed = True只要这一轮有节点被折叠,上层节点的输入就变“干净”了,必须再扫一轮,直到图不再变化
  6. self.constants[node['id']] = val新算出的常量也放进保鲜盒,供上层节点继续使用

常见误区

误区初学编译器时,我总把“常量折叠”和“常量传播”当成同一件事,觉得反正都是把常量提前算好。

正解两者是不同层级的兄弟优化。常量折叠把“表达式”算成值,比如 3×4+2 变成 14;常量传播是把“变量的常量约束”沿着数据流传递,比如 a=5; b=a+1 时,编译器先知道 a 是 5,再算出 b 是 6。二者经常在同一 pass 里配合出现,但前者消灭计算,后者消灭变量引用。

为何常见:教材和源码里它们总是成对出现、共用遍历框架,初学者看到一个优化 pass 同时做这两件事,就误以为是一个概念。

误区既然输入都是常量,那不管什么操作,提前算掉不就行了吗?

正解只有“纯函数”才能安全折叠。random()、print()、读取系统时间、访问全局可变状态这类操作,即使没有输入,也不能提前求值——它们的结果每次运行都不同,或者有输出副作用。折叠会让程序语义被篡改,好比把“客人点餐时才决定放不放辣”提前改成“一律不放辣”。

为何常见:教科书示例永远用 3×4、a+b 这种纯算术,给人造成“只要输入是常量就能折叠”的错觉。

误区编译期算出来的 14,和运行期算出来的 14,一定一模一样。

正解对于整数通常是安全的,但浮点运算不满足结合律和分配律。编译期求值器用某种顺序把 (a+b)+c 算完,运行期 kernel 可能用 FMA 指令或不同精度把 a+(b+c) 算出来,结果在小数点后几位可能不同。相对误差可能小到 1e-7,但对某些数值敏感的算法会连锁放大。

为何常见:人脑默认数学运算是精确且可结合的,忽略了 IEEE 754 浮点表示的实际规则。

误区`2+3` 这种标量才叫常量折叠,大模型里都是 Tensor,用不上。

正解张量同样可以被折叠。常量张量之间的 add、matmul、reshape、transpose、slice 都能在编译期算完,XLA 甚至会折叠整段跨算子的常量子图。PyTorch 的 BatchNorm 折叠就是对整张特征图做的事,省下的内存和时间远大于标量场景。

为何常见:教学材料习惯用标量举例子,而真实编译器的折叠实现是面向任意形状的张量数据,两者形成了认知落差。

面试题

Q1. 常量折叠发生在哪个阶段?

单选题 · easy

  • A.源码编写阶段
  • B.编译优化阶段← 正确答案
  • C.运行时计算阶段
  • D.模型部署阶段

答案:B。常量折叠是编译器的优化动作,发生在编译优化阶段,而非源码编写或运行时。编译器拿到中间表示后,在优化管线的固定位置执行折叠,对应公式中 fold 变换作用在编译期图上。

Q2. 表达式 3×4+2 经常量折叠后结果是什么?

单选题 · easy

  • A.原式保留给运行期
  • B.变为 12 再加 2
  • C.折叠成常量 14← 正确答案
  • D.变为 3 乘以 6

答案:C。常量折叠会把整条只含常量的表达式在编译期算完,而不是只做一步。3×4 先算成 12,12+2 再算成 14,最终替换为单个常量节点。选项 B 只是中间态,不是折叠终点。

Q3. 以下哪个操作不能被安全折叠?

单选题 · mid

  • A.x = 3 乘 4 的乘积
  • B.y = 2 加 5 的和值
  • C.z = random 乘 2 值← 正确答案
  • D.w = 10 除 2 的商

答案:C。random() 是带随机副作用的非纯函数,即使乘以常量 2 也不能提前求值,否则每次运行结果都被固定,程序语义被篡改。其余选项都是纯算术,可以安全折叠。

Q4. 常量折叠能带来哪些收益?

多选题 · mid

  • A.减少运行期计算量← 正确答案
  • B.加速模型启动过程← 正确答案
  • C.直接提升模型精度
  • D.减少运行内存占用← 正确答案

答案:ABD。折叠把常量计算移到编译期,运行时少执行算子,计算量和内存占用都下降,启动也更快。但折叠不改变数值精度(反而可能引入微小误差),所以不能提升模型精度,C 错误。

Q5. XLA 中常量折叠直接作用的对象是什么?

单选题 · mid

  • A.TensorFlow Python 源码
  • B.XLA 的 HLO 计算图← 正确答案
  • C.Keras 模型配置文件
  • D.训练数据批处理流

答案:B。XLA 的优化都发生在 HLO 中间表示上。Python 源码、Keras 配置在进入 XLA 前已被前端翻译成 HLO 图,折叠 pass 遍历的是 HLO 节点,对应公式 HLO → HLO'。

Q6. 常量折叠后遗留的无用节点由谁清理?

单选题 · mid

  • A.算子融合优化器
  • B.死代码消除步骤← 正确答案
  • C.内存规划器
  • D.梯度裁剪器

答案:B。被替换的旧节点无人引用,属于死代码,由死代码消除(DCE)清理。算子融合做的是合并相邻算子,内存规划管的是张量复用,梯度裁剪是训练技术,都与清理无关。

Q7. PyTorch 中哪个模块会对图执行常量折叠?

单选题 · easy

  • A.torch.nn 模块函数
  • B.TorchScript 编译器← 正确答案
  • C.torch.optim 优化器
  • D.torch.utils 工具集

答案:B。TorchScript 的 JIT 编译器会把 Python 函数追踪成静态图,并在图优化阶段执行常量折叠。torch.nn 是网络层库,torch.optim 是训练优化器,torch.utils 是数据工具,都不做图优化。

Q8. 下列哪些情况应避免常量折叠?

多选题 · mid

  • A.操作带有随机副作用← 正确答案
  • B.浮点精度敏感场景← 正确答案
  • C.调试想保留中间节点← 正确答案
  • D.表达式全是整数算术

答案:ABC。有副作用的操作不能折叠;浮点精度敏感时折叠可能引入细微偏差;调试时保留中间节点有助于观察数值。纯整数算术恰恰是最适合折叠的场景,D 错误。

Q9. 常量折叠与常量传播的主要区别是什么?

单选题 · hard

  • A.折叠提前求值表达式← 正确答案
  • B.两技术本质完全相同
  • C.传播只处理浮点数
  • D.折叠只能在 GPU 执行

答案:A。折叠是把常量表达式算成值(3×4 → 12),传播是把变量的常量约束沿数据流传递(a=5 传给 b=a+1)。两者常配合使用但职责不同,B、C、D 的描述都不成立。

Q10. 关于常量折叠精度问题的说法,哪些正确?

多选题 · hard

  • A.浮点加法满足结合律
  • B.编译期与运行期结果可能有差异← 正确答案
  • C.整数运算也有溢出风险← 正确答案
  • D.折叠后必须做 IEEE 校验

答案:BC。IEEE 754 浮点加法不满足结合律,(a+b)+c 与 a+(b+c) 可能有细微差异,所以 B 对、A 错。整数运算可能溢出,极端大数相乘结果超范围,故 C 也对。“必须做 IEEE 校验”说法过于绝对,实际是策略选择,D 错。