3. 中间表示(IR)概述:IR的定义、分类与设计原则

聊到深度学习编译器,有个东西绕不开——中间表示,也就是IR。我刚开始接触编译器那会儿,觉得IR就是个过渡产物,没啥大不了的。后来踩了不少坑才明白,IR选得好不好,直接决定了编译器的上限。

说白了,IR就是编译器内部用来表示程序的一种数据结构。它既不是源代码,也不是目标机器码,而是夹在中间的一个抽象层。你想想看,编译器要处理那么多前端框架(PyTorch、TensorFlow、ONNX),又要生成不同硬件(CPU、GPU、NPU)的代码,没有IR做桥梁,那得多乱?

3.1 IR的定义

IR的全称是Intermediate Representation,翻译过来就是中间表示。它的核心作用就两个:承载语义方便变换

我个人的理解是,IR就像翻译过程中的"笔记"。你把中文翻译成英文,不可能直接一句一句硬翻,得先理解意思,记下要点,再组织成英文。编译器也是这个道理——先把深度学习模型"理解"成IR,再基于IR做各种优化,最后生成目标代码。

IR的三个核心特征:

  • 语义完整:能完整表达原始模型的计算逻辑
  • 结构规整:有固定的语法和结构,方便程序处理
  • 可变换:支持各种优化pass的改写和转换

嗯,这里要注意一点:IR不是唯一的。不同编译器、不同阶段,用的IR可能完全不同。这就引出了IR的分类问题。

3.2 IR的分类:HIR、MIR、LIR

根据抽象层级,IR通常分为三类:高层IR(HIR)、中层IR(MIR)和低层IR(LIR)。这个分类不是绝对的,但大部分编译器都遵循这个思路。

类型 抽象层级 典型代表 主要用途
HIR 接近源语言 Graph IR、TVM Relay 图优化、算子融合
MIR 中间层级 MLIR、XLA HLO 内存优化、数据布局变换
LIR 接近机器码 LLVM IR、TIR 寄存器分配、指令调度

3.2.1 高层IR(HIR)

HIR离原始模型最近。它保留了完整的计算图结构,包括张量形状、数据类型、算子语义等信息。我在做TVM Relay优化时,就经常在HIR层面做算子融合——把连续的conv+bias+relu合并成一个算子,减少内存访问。

HIR的特点是:语义丰富,但优化空间有限。因为它太"高"了,很多底层细节看不到,比如内存布局、寄存器分配这些,在HIR层面根本没法做。

3.2.2 中层IR(MIR)

MIR是HIR和LIR之间的桥梁。它把高层语义拆解成更细粒度的操作,同时保留足够的语义信息供优化使用。

举个例子,HIR里的一个"矩阵乘法"节点,到了MIR层面可能被拆成"循环+乘加操作"。这样,编译器就能做循环展开、向量化等优化了。

我曾经在MLIR里做过一个数据布局变换的pass,就是在MIR层面把NHWC格式转成NCHW格式。这种优化在HIR层面做太粗糙,在LIR层面做又太复杂,MIR刚刚好。

3.2.3 低层IR(LIR)

LIR离机器码只有一步之遥。它通常采用静态单赋值(SSA)形式,有明确的指令集语义。LLVM IR就是典型的LIR。

LIR层面的优化非常"硬核":寄存器分配、指令选择、指令调度……这些优化直接决定了最终代码的执行效率。我记得有一次调试一个性能问题,折腾了半天,最后发现是LIR层面的寄存器溢出导致的——嗯,这种问题在HIR和MIR层面根本看不出来。

3.3 IR的设计原则

设计一个好的IR,比想象中难得多。我见过不少编译器,因为IR设计得不好,后面优化根本推不动。这里分享几个我总结的设计原则。

3.3.1 原则一:语义清晰,无歧义

IR的语义必须明确。同一个操作,不能有多种解释。比如"加法",你得说清楚是整数加法还是浮点加法,有没有溢出处理。

我曾经接手过一个项目,它的IR里有个"通用计算节点",什么操作都能往里塞。结果呢?优化pass根本不敢动这个节点,因为不知道里面到底是什么。这就是语义不清晰的典型反面教材。

3.3.2 原则二:层次分明,职责单一

不同层级的IR各管各的事。HIR只管图结构和算子语义,别去操心寄存器分配;LIR只管指令选择和调度,别去管算子融合。

我建议在设计IR时,先想清楚"这个IR要服务哪些优化pass",然后只保留必要的语义信息。多余的语义就是负担。

3.3.3 原则三:可扩展,不僵化

深度学习领域发展太快,今天出来个新算子,明天出来个新硬件。IR如果设计得太死,后面加东西就痛苦了。

MLIR在这方面做得不错。它用"Dialect"机制来扩展IR——每个Dialect定义一组操作和类型,互不干扰。你想加一个新硬件支持?写个新Dialect就行,不用动已有的IR结构。

避坑指南: 我曾经在一个项目里把IR设计得过于"完美",把所有可能的操作都预定义好了。结果新算子一来,发现预定义的操作根本不够用,还得打补丁。后来我学乖了——IR设计要留有余地,别想着一次性搞定所有事情。

3.3.4 原则四:便于分析和变换

IR最终是要被分析和变换的。如果IR结构太复杂,分析pass写起来就痛苦;如果IR结构太简单,又丢失了太多语义信息。

这里有个平衡点。我个人习惯是:先保证分析pass能高效实现,再考虑语义完整性。因为分析pass是编译器里最常写的代码,如果每次写pass都像在解谜题,那开发效率就太低了。

3.4 知识体系总览

说了这么多,我画了一张图来总结IR的核心知识体系。你看一眼,应该就能对IR有个整体把握。

中间表示 (IR) HIR(高层IR) MIR(中层IR) LIR(低层IR) 图优化 · 算子融合 · 语义保留 循环变换 · 内存优化 · 数据布局 寄存器分配 · 指令选择 · 调度 设计原则 语义清晰无歧义 层次分明职责单一 可扩展不僵化 便于分析和变换

这张图把IR的分类和设计原则串在了一起。你看,HIR、MIR、LIR各有各的优化重点,而四个设计原则贯穿始终。说白了,IR设计就是一场"抽象与具体"的博弈——太抽象了优化做不了,太具体了又失去了通用性。

注意: 不要试图设计一个"万能IR"。我见过有人想把所有优化都在一个IR里做完,结果IR变得臃肿不堪,每个pass都要处理大量无关信息。记住:不同阶段用不同的IR,各司其职

好了,关于IR的概述就聊到这里。下一节我们会深入具体的IR表示形式,看看图IR、SSA IR这些到底是怎么工作的。


公众号:蓝海资料掘金营,微信deep3321