4. Fusion Compiler IR设计理念

好,咱们今天聊聊Fusion IR的设计。说实话,我在LLVM上折腾了七八年,第一次看到Fusion IR的设计文档时,心里就一个感觉——这帮人真敢想。但仔细琢磨下来,你会发现它的设计哲学其实非常务实。

4.1 设计哲学:面向融合,减少冗余

Fusion IR的核心目标就两个词:融合精简

什么叫面向融合?传统编译器里,IR是IR,调度是调度,代码生成是代码生成,各玩各的。但Fusion IR不一样,它从一开始就考虑到了算子融合、内存融合、控制流融合这些场景。说白了,它不想让你在IR层面做完优化后,还得跑到调度器里再折腾一遍。

减少冗余这块,我印象特别深。LLVM IR里有很多“历史遗留”的指令,比如allocagetelementptr这些,它们在Fusion IR里被大幅简化了。为什么?因为Fusion IR假设你是在一个更高级的抽象层次上工作,不需要关心栈帧布局这种底层细节。

核心设计原则:

  • 融合优先:IR结构天然支持算子、循环、内存的融合
  • 最小指令集:能合并的指令绝不拆分,能省略的元数据绝不保留
  • 显式数据流:数据依赖关系一目了然,不像LLVM那样需要跑一遍def-use链
  • 层次化结构:支持从高层语义到底层指令的渐进式lowering

我记得有一次,我在做GPU算子融合时,LLVM IR里需要手动插入一堆phi节点和select指令来维护控制流。但在Fusion IR里,一个fused_region就能搞定,代码量直接砍掉一半。

4.2 与LLVM IR的映射关系

很多人问我:“Fusion IR和LLVM IR到底啥关系?”我的回答是:不是替代,是互补

Fusion IR可以看作是LLVM IR的一个“上层建筑”。它把LLVM IR里那些琐碎的细节封装起来,提供更高级的语义。但最终,Fusion IR还是要lowering到LLVM IR才能生成机器码。

我画了一张映射关系图,你一看就明白了:

Fusion IR(高层语义) fused_region tensor_ops memory_views ... lowering / 转换 LLVM IR(底层表示) alloca / load / store add / mul / fma phi / select / br ... 代码生成 目标机器码(x86 / ARM / GPU)

你看,Fusion IR在上层,LLVM IR在中间,机器码在最下面。Fusion IR的每个高层指令,最终都会展开成一组LLVM IR指令。但反过来,LLVM IR的优化结果也可以反馈到Fusion IR层面,形成双向的优化循环。

我的经验:在做Fusion IR到LLVM IR的lowering时,最容易踩的坑是元数据丢失。Fusion IR里很多语义信息(比如张量的形状、内存的对齐要求)在LLVM IR里没有直接对应的表示。我建议你写一个专门的FusionMetadataPass,把这些信息编码到LLVM的!annotation元数据里。

4.3 Fusion IR的扩展指令集

Fusion IR的指令集设计很有意思。它不像LLVM那样追求“通用”,而是针对AI编译器和HPC场景做了大量定制。

我挑几个有代表性的指令说说:

指令名称 功能描述 对应LLVM IR 我的评价
fused_region 定义一个可融合的计算区域,包含多个算子 无直接对应,需展开为多个基本块 这是Fusion IR的灵魂指令
tensor_contract 张量缩并操作,支持任意维度的规约 需展开为嵌套循环+fadd/fmul 比LLVM的reduce灵活得多
memory_view 创建张量的子视图,不拷贝数据 getelementptr + bitcast 省去了大量指针运算
shuffle_tensor 张量维度重排,类似numpy的transpose 需手动生成索引计算 GPU上性能优化利器
fused_loop 融合循环,支持多级循环嵌套的合并 需手动做loop fusion优化 编译期自动完成,省心

举个例子,假设你要写一个矩阵乘加ReLU的操作。在LLVM IR里,你得先写矩阵乘的循环,再写ReLU的循环,然后手动做loop fusion。但在Fusion IR里,一行搞定:

// Fusion IR 示例
%result = fused_region {
  %mm = tensor_contract(%A, %B) : (tensor<64x128xf32>, tensor<128x64xf32>) -> tensor<64x64xf32>
  %relu = elementwise_max(%mm, %zero) : tensor<64x64xf32>
  return %relu : tensor<64x64xf32>
}

编译器会自动把tensor_contractelementwise_max融合成一个kernel,中间不需要额外的内存读写。

注意:扩展指令集虽然方便,但不要滥用。我见过有人把整个模型塞进一个fused_region里,结果编译时间暴涨到几个小时。合理的做法是:按计算边界拆分,比如每个layer一个fused_region

4.4 元数据与属性系统

元数据这块,Fusion IR和LLVM IR的思路完全不同。LLVM的元数据是“附加”在指令上的,可有可无。但Fusion IR的元数据是“内建”的,很多优化必须依赖它。

我举个例子你就明白了。在LLVM里,你要给一个load指令加对齐信息,得写:

%val = load i32, i32* %ptr, align 4

但在Fusion IR里,对齐信息是类型系统的一部分:

%val = load : tensor<64xf32, alignment=128>

你看,对齐信息直接写在类型里,而不是作为指令的属性。这样做的好处是:类型推导时就能传播对齐信息,不需要额外跑一遍对齐分析pass。

Fusion IR的属性系统也很有意思。它把属性分为三类:

  • 类型属性:张量形状、数据类型、内存布局、对齐要求
  • 计算属性:精度要求(fp32/fp16/int8)、快速数学标志、近似计算开关
  • 调度属性:线程块大小、共享内存分配、流水线深度

我记得有一次,我在调一个Transformer模型的推理性能。LLVM IR里,我得手动给每个fadd指令加fast标志,漏一个性能就上不去。但在Fusion IR里,我只需要在fused_region上加一个fast_math=true的属性,所有内部指令自动继承这个属性。

避坑指南:我曾经在迁移一个老项目时,发现Fusion IR的元数据传播机制有个“坑”——属性覆盖规则。内层fused_region的属性会覆盖外层,但如果你在内层显式指定了fast_math=false,外层的fast_math=true就不会生效。这个行为跟C++的作用域规则很像,但一开始很容易忽略。

最后说一句,Fusion IR的元数据系统还在快速演进中。我建议你关注一下MLIR社区的transform dialect,它正在尝试把元数据操作也变成一等公民。嗯,这个方向很有意思,但那是另一个故事了。


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