老周经营一家川菜馆,后厨墙上贴满花花绿绿的工序卡。同样一盘回锅肉,切配、焯水、爆炒、回锅、装盘,五道工序环环相扣;宫保鸡丁则是切丁、滑油、调汁、爆炒。那晚生意爆火,老周却没有手忙脚乱——因为他心里装着“调度”:哪道菜先备料、哪个灶头先空出来、哪道工序可以两锅并一锅,全部井井有条。 AI 编译器里的 Pass 调度,正是老周这套功夫的镜像。编译器拿到一个深度学习模型或程序片段,并不会一步到位生成机器码,而是要经过一连串“工序”——每个变换步骤在 MLIR 里叫一个 Pass(处理步骤)。决定这些 Pass 谁先谁后、何时重复、如何嵌套的机制,就是“Pass 调度”。MLIR 正是以多级中间表示(IR)为食材、以 Pass 调度为烹饪流程的厨房。这篇教程将从基本概念讲到高级策略,穿插后厨比喻,帮你彻底看懂“编译器的后厨”如何运转。

岗位关联

对 AI 从业者:Pass 调度是 AI 编译器(MLIR、TVM、XLA)的核心引擎,搞懂它,等于拿到了模型优化与推理加速的钥匙。

目录
  1. 可视化演示
  2. Pass调度概述
  3. Pass管道设计
  4. Pass管理器
  5. 依赖分析管理
  6. 调度策略算法
  7. 性能优化实践
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

动态展示PassManager驱动的Pass流水线调度、全局vs局部调度粒度、训练图编译优化案例(常量传播→算子融合→内存重用)、以及强化学习自动Pass调度四个核心视角。

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

Pass调度概述

后厨的工序卡上,每道菜都写着一句“先做什么、再做什么”。焯水必须在爆炒前,腌制必须在油炸前——这些顺序不是拍脑袋定的,是后厨多年踩坑总结出的铁律。Pass 调度也一样,它是编译器里一张看不见的“工序卡”,规定 IR(中间表示)在哪个阶段接受哪道变换。

在 MLIR 的世界里,Pass 是一个独立、可复用的 IR 变换操作,比如“常量折叠”“死代码消除”“算子降级”。调度的基本职责,就是把这些 Pass 按合理顺序排成管道,再逐级施加到 IR 的每个结构单元上。正如工序卡要平衡口味与出菜速度,Pass 调度要在优化收益与编译开销之间取平衡。

调度首先面临“在哪个尺度上管事”的问题。MLIR 提供三种优化粒度:模块级 Pass 作用于整个 ModuleOp,适合做跨函数的内联与去重;函数级 Pass 进入每个函数体单独执行,最常用于循环变换与指令精简;区域级 Pass 则针对函数内部的嵌套区域做精细手术。PassManager 在运行时会自动遍历 IR 结构,把函数级 Pass 一一施用到每个函数上。

回到老周的后厨:工序卡不是越厚越好,每多一道工序就多一份等待与风险。调度开销模型提醒我们——每一道“验证”和“备料”都要计入时间账本,只有砍掉冗余、复用高汤,后厨才能在晚市高峰里游刃有余。

Tschedule=∑i=1n(trun(i)+tverify(i))−tcached\displaystyle T_{schedule} = \sum_{i=1}^{n} \left( t_{run}(i) + t_{verify}(i) \right) - t_{cached}
这个公式是说:整个调度的总时间,等于每个 Pass 的执行时间和验证时间累加起来,再减去缓存省下的时间。它提醒我们,Pass 管道中的每一道工序都有成本。
  1. 累计求和Σ 表示对管道中第 1 个到第 n 个 Pass 逐一累加开销,就像把每道工序的耗时全部记进后厨台账。
    ∑i=1n\displaystyle \sum_{i=1}^{n}
  2. Pass 执行时间$t_{run}(i)$ 是第 i 个 Pass 真正变换 IR 所花的时间,类似每道工序的烹饪时间。
    trun(i)\displaystyle t_{run}(i)
  3. 验证开销$t_{verify}(i)$ 是 Pass 执行后验证 IR 合法性所花的时间,类似出品前试味质检。
    tverify(i)\displaystyle t_{verify}(i)
  4. 缓存收益$t_{cached}$ 是分析缓存节省的总时间,类似提前熬好的高汤省下的炖煮时间。
    tcached\displaystyle t_{cached}

Pass管道设计

有经验的厨师不会把工序任意堆砌——焯水一定在爆炒之前,腌制一定在油炸之前。把多道工序按正确顺序串联成一条完整的制作流程,就是“管道设计”。MLIR 的 Pass 管道(Pipeline)正是这样一串经过编排的 Pass 序列,它定义了编译器从输入 IR 到输出 IR 的完整路径。

开发者通过命令行表达式描述管道,典型语法如 --pass-pipeline='builtin.module(func.func(convert-to-llvm))'。这行配置表示:对每个内置模块,依次进入函数级子管道,在每个函数上运行 convert-to-llvm 这个降级 Pass。管道还可以嵌套——函数级子管道可以再包含区域级管道,调度器按深度优先遍历 IR 树,将对应 Pass 应用到匹配的结构上。

更灵活的是管道重写(Pipeline Rewriting):Pass 在运行中发现某种 IR 模式后,可以动态启动一个子管道进行局部处理。这就像老周看到今天的鱼特别新鲜,临时加一道“快速过油”的工序,让鱼更酥脆——管道的骨架是固定的,但局部工序可以即兴发挥。

管道嵌套就像后厨的“分区作业”——凉菜间有凉菜间的流程,热灶间有热灶间的工序,互不干扰又统一受总调度管辖。设计管道时,把重复的工序提取为公共子管道,就是老周口中的“中央厨房”思维。

Tnested=Touter+∑k=1mTinner(k)\displaystyle T_{nested} = T_{outer} + \sum_{k=1}^{m} T_{inner}^{(k)}
嵌套管道的总时间 = 外层管道的时间 + 所有内层子管道的时间之和。理解这个拆分,才能准确判断“分区作业”到底省不省时间。
  1. 外层管道时间$T_{outer}$ 是外层模块级 Pass 的执行总时间,类似后厨热灶区的总工时。
    Touter\displaystyle T_{outer}
  2. 内层管道求和$\sum_{k=1}^{m} T_{inner}^{(k)}$ 把第 1 个到第 m 个嵌套子管道的运行时间逐个相加,类似把凉菜间、面点间的工时全部汇总。
    ∑k=1mTinner(k)\displaystyle \sum_{k=1}^{m} T_{inner}^{(k)}
  3. 嵌套总时间$T_{nested}$ 表示嵌套调度的总耗时,等于外层加上所有内层的总和。
    Tnested\displaystyle T_{nested}

Pass管理器

后厨除了掌勺的厨师,还有一位“厨师长”——自己不动手,但盯流程、查火候、看出品。编译器里,PassManager 就是这位厨师长。它不亲自变换 IR,却掌控着整个 Pass 管道的运行节奏。

PassManager 是 MLIR 中负责 Pass 调度执行的核心对象。它的第一项职责是执行控制:调用 run 方法启动整个管道,遍历 IR 的模块、函数、区域层级,把每个 Pass 精准投放到对应结构上。第二项职责是验证机制:每当一个 Pass 修改 IR 后,管理器会在下一次 Pass 执行前自动插入验证器,确认 IR 依然合法——相当于每道菜上桌前先试味,防止坏菜进入下一步。

第三项职责是失败处理:Pass 可以返回 failure 表示“此路不通”,管理器捕获后按策略回滚或终止,而不是让整个编译进程崩溃。这就像厨师长发现今天的鱼不新鲜,果断换菜而不是硬着头皮做下去。整个过程贯穿着对验证开销的敏感:验证是必要的,但过度验证会让厨师长变成“事事亲力亲为”的独裁者。

厨师长的价值在于“严丝合缝的流程管理”——用一点点质检开销,换来整条流水线的稳定出品。PassManager 同样如此:它把“担心”变成制度,把“万一出错”变成可回滚的预案。

Rverify=TverifyTrun+Tverify≤θ\displaystyle R_{verify} = \frac{T_{verify}}{T_{run} + T_{verify}} \leq \theta
验证开销占比 = 验证时间 ÷(执行时间 + 验证时间),工程上应把它控制在一个阈值 θ 以内。厨师长要做的,是让质检恰好够用,而不是把每道工序都验上三遍。
  1. 验证时间$T_{verify}$ 是所有验证步骤的总耗时,类似每道菜试味、摆盘检查的时间。
    Tverify\displaystyle T_{verify}
  2. 执行时间$T_{run}$ 是所有 Pass 真正执行变换的总耗时,类似厨师切配爆炒的时间。
    Trun\displaystyle T_{run}
  3. 占比约束$R_{verify}$ 表示验证开销在总时间中的占比,我们希望它不超过阈值 $\theta$。
    Rverify≤θ\displaystyle R_{verify} \leq \theta

依赖分析管理

后厨最有价值的资产,是那锅熬了六个小时的高汤——所有菜都能用它提鲜,不必每道菜重新熬制。但高汤也有保鲜期,一旦变质就不能再用。Pass 调度里的“高汤”,就是分析(Analysis)结果。

Pass 在执行时往往需要前置分析的结论,比如活跃变量分析、依赖分析。多个 Pass 可以复用同一个分析结果,避免重复计算。MLIR 中,Pass 通过 getAnalysis<A>() 请求分析结果:管理器先查缓存,命中就直接返回,否则立即计算并缓存。这就像厨师做菜时喊一句“来勺高汤”,不用重新炖。

但 IR 一经修改,部分分析就开始“变质”。失效传播是依赖管理的核心:一个 Pass 修改了 IR 的某个区域,该区域的局部分析应失效;若修改了整体结构,所有依赖它的分析都要作废。MLIR 通过 Pass 声明“我保留了哪些分析”(preserve)来精确控制缓存的生命周期。

高汤的新鲜度由厨师长把关——不是所有汤都一锅炖到尾,哪个菜用了、哪个步骤改了,都要在台账上划掉相关依赖。Pass 调度里的失效传播,就是这本台账的数字化版本。

Invalidate(A)⟺Killed(A)∨Modified(Deps(A))\displaystyle \text{Invalidate}(A) \Longleftrightarrow \text{Killed}(A) \lor \text{Modified}(\text{Deps}(A))
这条逻辑是说:分析结果 A 之所以失效,要么是某个 Pass 明确声明“我改了它依赖的东西”(Killed),要么是 A 所依赖的 IR 区域真的被修改了(Modified)。两个原因任一成立,A 就不能再用。
  1. 分析被撤销$\text{Invalidate}(A)$ 表示分析对象 A 的缓存结果被作废,相当于这锅高汤被贴上了“不可用”的标签。
    Invalidate(A)\displaystyle \text{Invalidate}(A)
  2. 显式失效$\text{Killed}(A)$ 表示 Pass 主动声明“我改了东西,A 别用了”,就像厨师长喊“高汤里下了辣椒,之前的清淡菜谱作废”。
    Killed(A)\displaystyle \text{Killed}(A)
  3. 依赖变更$\text{Modified}(\text{Deps}(A))$ 表示 A 所依赖的底层数据真的被修改了,相当于熬汤的底料换了。
    Modified(Deps(A))\displaystyle \text{Modified}(\text{Deps}(A))
  4. 或逻辑$\lor$ 表示“或者”——两种原因只要有一个成立,A 就必须失效。
    ∨\displaystyle \lor

调度策略算法

有些菜要“回锅”才香——比如回锅肉,煮了切、切了炒、炒了还要再回锅调味。Pass 调度也经常需要“回锅”:一轮 Pass 跑完,IR 可能还未收敛,需要再跑几轮。MLIR 支持两种基本调度策略:固定调度和动态调度。

固定调度下,每个 Pass 在管道中恰好执行一轮,按静态顺序依次跑完。动态调度则依据 IR 的实时状态决定是否继续执行——通常通过模式重写驱动,比如“不断折叠常量直到不再出现新的可折叠模式”。后者更智能,但也更需要谨慎设计终止条件,否则程序可能陷入“回锅肉越炒越黑”的死循环。

收敛判定是动态调度的核心算法。调度器通过比较 Pass 执行前后的 IR 变化量,决定是否继续循环;同时设置最大迭代次数作为安全阀。这两把锁缺一不可:前者保证“变不动了就停”,后者保证“就算一直变,也不能无限跑下去”。

好厨师知道“回锅肉最多回几次”——收敛判定就是那把尺子,防止一道菜炒到天荒地老。设计动态调度时,永远同时带上收敛阈值与最大迭代次数这两个安全阀。

d(IRk+1,IRk)<ε⇒Stop\displaystyle d(IR_{k+1}, IR_k) < \varepsilon \Rightarrow \text{Stop}
这条公式说的是:当相邻两轮调度后 IR 的变化距离 $d$ 小于阈值 $\varepsilon$ 时,调度器宣布收敛并终止循环。变化距离可以是指令条数差,也可以是重写次数差——就像回锅肉不再变色、不再出油,就说明火候到位了。
  1. IR 序列$IR_{k+1}, IR_k$ 分别表示第 k 轮和第 k+1 轮调度后的中间表示,像回锅肉第一遍和第二遍出锅的样子。
    IRk+1,IRk\displaystyle IR_{k+1}, IR_k
  2. 变化距离$d(IR_{k+1}, IR_k)$ 是 IR 变化量的度量函数,比如指令数量差异或重写事件计数。
    d(IRk+1,IRk)\displaystyle d(IR_{k+1}, IR_k)
  3. 判定阈值$\varepsilon$ 是工程预设的容忍阈值,变化量低于它就认为“没什么可改的了”。
    ε\displaystyle \varepsilon
  4. 停止结论$\Rightarrow \text{Stop}$ 表示条件满足则终止循环,防止无休止的重复执行。
    ⇒Stop\displaystyle \Rightarrow \text{Stop}

性能优化实践

晚市高峰,后厨最怕的不是菜多,而是“等”——等水开、等烤箱、等汤冷却。Pass 调度同样要消灭“等”:剔除不必要的验证、复用缓存数据、并行执行独立的工序。性能优化的本质,是让每一分钟编译时间都花得值。

三个优化方向中最立竿见影的是缓存复用。分析缓存命中率越高,重复计算越少,整体调度就越快。工程中常通过 --mlir-print-ir-after-all 观察每步执行后的 IR,结合 --debug 日志定位那些“反复重新计算”的分析,就像查看后厨台账找出哪道菜老是在重熬高汤。

并行调度则是把互不依赖的 Pass 分配到不同线程同时执行——后厨里的“两锅并一锅”。但要注意,并行会引入新的竞争条件,分析失效跨线程传播时尤其要小心。MLIR 的调度器在生成并行计划时会先做依赖分析,确保没有数据竞争才放心开火。

把“炖高汤”的次数降到最低,把“直接用高汤”的次数提到最高——后厨与编译器的效率哲学殊途同归。性能优化不是一招鲜,而是在缓存、验证、并行三者之间找平衡。

H=NhitNhit+Nmiss\displaystyle H = \frac{N_{hit}}{N_{hit} + N_{miss}}
缓存命中率 = 命中次数 ÷(命中次数 + 未命中次数)。命中率越接近 1,说明“直接舀高汤”的次数越多,“重新炖高汤”的次数越少。
  1. 命中次数$N_{hit}$ 是分析请求命中缓存的次数,相当于高汤直接舀来用。
    Nhit\displaystyle N_{hit}
  2. 未命中次数$N_{miss}$ 是缓存未命中、需要重新计算的次数,相当于高汤用完了只能重新熬。
    Nmiss\displaystyle N_{miss}
  3. 命中率$H$ 是两者的比值,衡量缓存策略的有效程度。
    H=NhitNhit+Nmiss\displaystyle H = \frac{N_{hit}}{N_{hit} + N_{miss}}

AI 应用场景

深度学习框架的图优化管线

阶段:模型编译与图优化

解决的问题:深度学习模型的原始计算图包含大量冗余运算,如连续 reshape、未融合的激活函数、重复的常量计算。手工优化费时费力且极易出错,AI 编译器用 Pass 管道自动完成算子融合、内存布局转换和常量折叠。

具体案例:PyTorch 的 TorchInductor 将 Torch IR 经一系列 Pass 降级为 Triton 底层 IR。调度器先做“模式匹配与融合”,再做“并行拆分”,最后做“代码生成”,每个阶段都是 Pass 调度的产物。开发者只需注册新 Pass,编译器会像老周安排新菜品一样自动排入流程。

AI芯片后端的IR降级调度

阶段:硬件代码生成

解决的问题:把深度学习模型部署到定制 AI 芯片上,需要把高层 IR 逐级降级为芯片指令。芯片的存储层次、算子库接口各不相同,手写一遍只能服务一款芯片,复用性极差。

具体案例:使用 MLIR 的 Dialect 转换管道,调度器依次执行“高电平降级 → 负载均衡 → 存储分配 → 指令选择”等 Pass,最终生成目标芯片可执行的代码。开发者在管道中插入自定义 Pass,如同给老周的后厨增加一道“秘制酱料”工序。

大模型推理引擎的算子融合调度

阶段:推理性能优化

解决的问题:大模型推理时 KV Cache、Attention 等算子反复调用,内存带宽成为瓶颈。推理引擎通过 Pass 调度将多个小算子融合成一个大算子,减少核启动开销与显存往返。

具体案例:vLLM、TensorRT-LLM 的 Graph Fusion Pass 把 Transformer 层中十几个算子融合成三四个 Fused Kernel。调度器决定融合顺序——先融合非线性部分再融合矩阵乘,以最大化缓存复用。这就像老周把“切配、调味、爆炒”合成一个连贯动作,减少交接等待。

Python 演示

python75 行
"""Pass 调度模拟器: 用后厨工序类比编译器 Pass 管道的执行过程"""  class Pass:    """一个编译器 Pass, 对应后厨的一道工序"""     def __init__(self, name, transform, requires=()):        self.name = name        self.transform = transform        self.requires = requires  class PassManager:    """调度中心: 按顺序执行 Pass, 管理分析缓存与结果验证"""     def __init__(self, verify=True):        self.pass_list = []        self.verify = verify        self.cache = {}        self.hit_count = 0     def register(self, p):        self.pass_list.append(p)     def load_analysis(self, req, ir):        if req in self.cache:            self.hit_count += 1            return self.cache[req]        result = len(ir.split())        self.cache[req] = result        return result     def run(self, ir):        for p in self.pass_list:            for req in p.requires:                self.load_analysis(req, ir)            ir = p.transform(ir)            if self.verify:                assert ir, "IR 验证失败: " + p.name            self.cache.clear()        return ir  def remove_comments(ir):    return "\n".join(line for line in ir.split("\n") if not line.startswith("//"))  def constant_fold(ir):    return ir.replace("a * 1", "a")  def dead_code_eliminate(ir):    return "\n".join(line for line in ir.split("\n") if "unused" not in line)  def lower_to_llvm(ir):    return ir.replace("add", "llvm.add").replace("mul", "llvm.mul")  def main():    pm = PassManager()    pm.register(Pass("删除注释", remove_comments))    pm.register(Pass("常量折叠", constant_fold, requires=("loads",)))    pm.register(Pass("死代码消除", dead_code_eliminate, requires=("loads",)))    pm.register(Pass("后端降级", lower_to_llvm))     source = "// 原始IR\na = 1\nb = a * 1\nunused = 2\nc = a + b"    final_ir = pm.run(source)    print(final_ir)    print("缓存命中次数:", pm.hit_count)    print("缓存命中率:", pm.hit_count / 2)  if __name__ == "__main__":    main()
目的:用 50 行代码模拟 MLIR 的 Pass 调度器——展示 Pass 如何按管道顺序执行、依赖分析如何命中缓存、IR 变化后缓存为何必须清空。
  1. class PassManager:定义调度中心类,相当于后厨的厨师长角色。
  2. self.pass_list.append(p)注册 Pass,把新工序挂到工序卡末尾。
  3. self.load_analysis(req, ir)先查缓存再计算,模拟分析结果的“高汤复用”。
  4. self.cache.clear()每个 Pass 跑完就清空缓存,模拟 IR 变化后的失效传播。
  5. pm.register(Pass("常量折叠", constant_fold, requires=("loads",)))注册带依赖的 Pass,必须先有 loads 分析结果才能执行。
  6. assert ir, "IR 验证失败: " + p.name每个 Pass 执行后验证 IR 合法性,对应 PassManager 的验证机制。

常见误区

误区编译器不开个上百个 Pass 真不好意思出门,每个 Pass 都是一层优化,跑得越多 IR 肯定越小,最后性能一定越好。我把能加的 Pass 全加上,总不会吃亏吧?

正解Pass 并非免费午餐。每个 Pass 都有执行与验证开销,而且可能相互抵消——比如“循环展开”Pass 展开的循环,可能被后面的“循环合并”Pass 重新合并,白忙一场。Pass 调度的价值恰恰在于选择“少而精”的管道,按需组合。就像老周不会给每道菜都上十道工序,工序越多反而越容易翻车。

为何常见:人们直观地认为“优化 = 多次变换”,忽略了 Pass 之间的相互作用与调度开销的累积效应。

误区反正所有 Pass 都会跑一遍,先执行哪个后执行哪个,结果不都一样吗?我把管道顺序随便排一下,应该没什么影响吧。

正解顺序决定结果。先做死代码消除,可能把后续 Pass 需要的元数据删掉;先做内联再做常量折叠,折叠机会会更多。MLIR 的开发者必须像厨师排工序一样认真设计管道顺序——先焯水再爆炒与先爆炒再焯水,出来的完全是两道菜。

为何常见:把 Pass 看成相互独立的滤镜,没意识到 IR 是流动的——前一个 Pass 的输出,正是后一个 Pass 的输入。

误区只要分析结果算出来,就放在缓存里,之后所有 Pass 都能直接用,省时间。我清了缓存反而变慢了吧?

正解IR 是“活”的。任一个 Pass 修改了 IR,依赖该 IR 区域的分析就可能失效。MLIR 通过 preserve 声明与依赖追踪来精确淘汰“变质”的分析缓存。不失效的缓存,就像用隔夜高汤做新菜——轻则串味,重则吃坏肚子。正确做法是每次 IR 变更后,按依赖图精准失效。

为何常见:把分析结果当作常量,忽略了它本质上是对“某时某刻 IR 快照”的描述,快照变了,结论就得重算。

误区看到日志里 Pass failed,第一反应是编译器挂了、代码写错了,赶紧翻堆栈找 Bug。

正解Pass 返回 failure 是 MLIR 的预期机制之一,表示“该 Pass 无法在当前 IR 上完成变换”。PassManager 会捕获 failure 并按策略回滚或生成崩溃复现文件(crash repro),真正的崩溃是未捕获异常或 IR 验证失败。就像厨师长说“这道菜今天做不了”,不等于厨房起火了。

为何常见:把 failure 与 error 混为一谈,没理解 failure 是编译器主动给出的“此路不通”信号,是流程控制的正常组成部分。

误区管道里写的 Pass 跑完一遍就算完事,干嘛要重复执行?重复跑不是浪费时间吗?

正解很多优化是“越跑越香”的:常量折叠一轮可能暴露出新的可折叠模式,需要再折叠;死代码消除一轮之后又出现了新的死代码。调度器通过循环反复执行这类 Pass 直到收敛,正如回锅肉要回锅才出味。但循环必须配收敛判定与最大迭代上限,防止无限空转。

为何常见:把 Pass 当作一次性操作,忽略了 IR 变换的级联效应——一个 Pass 的产物,往往能触发下一个 Pass 的新机会。

面试题

Q1. Pass 调度在 MLIR 中主要负责什么?

单选题 · easy

  • A.决定 Pass 执行顺序与时机← 正确答案
  • B.直接修改源代码文件
  • C.管理内存的分配释放
  • D.生成最终可执行文件

答案:A。Pass 调度的核心职责是决定 Pass 的执行顺序、时机与作用范围,就像后厨决定先切还是先炒。B 是前端词法分析的事,C 是运行时的事,D 是代码生成的结果,都不是调度本身。

Q2. 下列哪些属于 Pass 调度的优化粒度?

多选题 · mid

  • A.模块级 Pass 调度← 正确答案
  • B.函数级 Pass 调度← 正确答案
  • C.区域级 Pass 调度← 正确答案
  • D.指令级 Pass 调度

答案:ABC。MLIR 的 Pass 调度粒度是模块级、函数级和区域级,分别对应整道菜、单份菜、局部摆盘。指令级调度属于后端指令选择阶段,不在 Pass 调度的粒度体系内。

Q3. 管道表达式 func.func(convert-to-llvm) 的含义是什么?

单选题 · mid

  • A.将模块级 Pass 应用于整个模块
  • B.在函数级子管道中运行降级← 正确答案
  • C.在区域级子管道中运行转换
  • D.在模块级管道中并行调度

答案:B。func.func(...) 表示进入函数级子管道,对每个函数执行括号内的 Pass。这是嵌套管道最典型的写法——外层是模块管道,内层是函数级子管道,而不是模块级应用或并行调度。

Q4. Pass 执行完毕后,分析缓存何时需要失效?

单选题 · easy

  • A.当 IR 被该 Pass 修改后← 正确答案
  • B.当 IR 保持完全不变时
  • C.当分析结果首次计算后
  • D.当 Pass 名称包含优化时

答案:A。分析结果是 IR 快照的结论,IR 一旦被 Pass 修改,依赖它的分析就可能“变质”。就像高汤底料换了,汤的鲜味结论就得重估。IR 不变时缓存应复用;与 Pass 名称是否含“优化”无关。

Q5. 收敛循环中,判定停止的充分条件包括哪些?

多选题 · hard

  • A.IR 变化量低于预设阈值← 正确答案
  • B.重写次数不再增长← 正确答案
  • C.达到了最大迭代次数← 正确答案
  • D.源程序运行速度翻倍

答案:ABC。收敛判定看的是 IR 自身的变化,而不是程序运行速度——调度器拿不到运行时数据。A 是经典收敛条件,B 是重写驱动的停止信号,C 是安全阀。D 的“速度翻倍”需要实测,不能作为调度内判据。

Q6. 嵌套管道相较于顺序管道的核心优势是什么?

单选题 · extreme

  • A.内层 Pass 可局部作用于特定结构← 正确答案
  • B.外层 Pass 一定会重复执行多次
  • C.嵌套管道不需要验证合法性
  • D.嵌套管道必然减少编译时间

答案:A。嵌套管道的本质是“分区作业”——内层子管道只作用于匹配的结构单元,实现局部化优化。B 错在“一定会”;C 错在嵌套管道同样需要验证;D 错在嵌套可能增加开销,并非必然减少。

Q7. MLIR 中 PassManager 运行验证器的位置在哪里?

单选题 · mid

  • A.每个 Pass 执行的前后时刻← 正确答案
  • B.仅在整个管道收尾结束时
  • C.仅在 Pass 返回失败的时候
  • D.只在开启调试模式时运行

答案:A。PassManager 在每个 Pass 执行后、下个 Pass 执行前自动插入验证,确保 IR 始终合法。这与厨师每道工序后试味一样,是流程内建的质检。调试模式只是把验证结果打印出来,并非验证的前提。

Q8. 以下哪些因素会增大 Pass 调度总开销?

多选题 · mid

  • A.频繁的 IR 结构合法性验证← 正确答案
  • B.分析缓存命中率持续偏低← 正确答案
  • C.在管道中内联大量小函数← 正确答案
  • D.关闭全部调试信息输出口

答案:ABC。验证多了花时间(A),缓存不命中重复算(B),内联大量函数让后续 Pass 处理更多 IR(C),三者都推高开销。D 关闭调试输出只会减少开销,不会增大。

Q9. 动态调度与固定调度的核心区别是什么?

单选题 · easy

  • A.是否依据 IR 状态决定继续执行← 正确答案
  • B.是否使用同一个 Pass 管理器
  • C.是否输出到标准错误流通道
  • D.是否允许修改源代码文本内容

答案:A。固定调度按静态顺序每个 Pass 跑一轮;动态调度则看 IR 状态决定是否循环、是否重写管道。后者的“回锅”逻辑以 IR 状态为依据,与管理器对象、输出流、源码文本都无关。

Q10. 以下哪些手段能有效提升 Pass 调度效率?

多选题 · hard

  • A.对分析结果进行缓存复用← 正确答案
  • B.调度互不依赖的 Pass 并行← 正确答案
  • C.精简重复执行的 IR 验证环节← 正确答案
  • D.每个模块重新生成全部分析

答案:ABC。缓存复用减少重复计算,并行调度利用多核,精简验证降低固定开销,三者都是提速正道。D“重新生成全部分析”恰恰是在做重复劳动,等同每道菜都重新熬高汤,效率只会更低。