【infra之路】AI 编译器学习笔记(四):PatternRewriter 与贪心 driver

MLIR 里大部分优化 pass(包括 Triton 的很多变换)都基于这套机制,面试几乎必问。这一篇以"BiasAdd + ReLU 融合"为例,讲清楚 pattern、rewriter、driver 三者的分工。API 以 LLVM 20 为准,示例中的 op 名是示意。


一、一句话定位

MLIR 中大多数变换都是"找到一小片符合条件的 op,换成另一片"。PatternRewriter 这套设施把它拆成两件事:

  • pattern 只管:匹配什么、改成什么;
  • driver 负责:怎么遍历、何时应用、何时停。

写 pattern 的人不需要关心遍历顺序,这是它最核心的设计。

二、三个角色

1. Pattern

继承 OpRewritePattern<根Op>,实现 matchAndRewrite:

cpp 复制代码
struct FuseBiasRelu : public OpRewritePattern<ReluOp> {
  using OpRewritePattern<ReluOp>::OpRewritePattern;   // 继承构造函数

  LogicalResult matchAndRewrite(ReluOp relu,
                                PatternRewriter &rewriter) const override {
    // 匹配:沿 use-def 链往上找
    auto biasAdd = relu.getInput().getDefiningOp<BiasAddOp>();
    if (!biasAdd)
      return rewriter.notifyMatchFailure(relu, "input is not bias_add");
    // 合法性:中间结果不能有别的使用者
    if (!biasAdd.getResult().hasOneUse())
      return rewriter.notifyMatchFailure(relu, "bias_add has other users");
    // 改写:一切修改都经过 rewriter
    rewriter.replaceOpWithNewOp<BiasReluOp>(
        relu, relu.getType(), biasAdd.getInput(), biasAdd.getBias());
    return success();
  }
};

2. PatternRewriter

继承自 OpBuilder,所以能建 op;另外提供 replaceOp、eraseOp、replaceOpWithNewOp、modifyOpInPlace。所有对 IR 的修改都必须经过它,因为 driver 靠它的 listener 得知哪些 op 发生了变化。

3. Driver

最常用的是贪心 driver:

cpp 复制代码
RewritePatternSet patterns(&getContext());
patterns.add<FuseBiasRelu>(&getContext());
if (failed(applyPatternsGreedily(getOperation(), std::move(patterns))))
  signalPassFailure();

旧名字 applyPatternsAndFoldGreedily 在老代码和教程中仍很常见。

三、贪心 driver 做了什么

它维护一个 worklist,初始放入范围内所有 op,然后循环:

  1. 取出一个 op;若它无使用者且无副作用,直接删除;
  2. 尝试 fold;
  3. 依次尝试以它为根的 pattern;
  4. IR 一旦改变,listener 把受影响的 op 重新加入 worklist:新建的 op、被替换值的使用者、被删 op 的操作数的定义者。

直到 worklist 为空,即达到不动点。迭代次数有上限,超过仍未收敛则返回 failure。

这解释了上面代码的一个细节:我没有手动删除 bias_add 。它失去唯一使用者后成为死代码,driver 会自动清除------前提是这个 op 声明了无副作用(Pure 等 trait)。没标的话它会一直留在 IR 里。

四、容易被追问的点

为什么根选 relu 而不是 bias_add? 匹配沿 use-def 链往上走:一个操作数只有一个定义者,往上是确定的;一个结果可以有很多使用者,往下要枚举。所以根通常选 DAG 最下游的 op。

返回值契约。 success() 表示 IR 已修改,failure() 表示完全没动过。改了却返回 failure,driver 不会重新处理;没改却返回 success,会空转到迭代上限。编译 LLVM 时打开 MLIR_ENABLE_EXPENSIVE_PATTERN_API_CHECKS 可以检出这类错误。

不能绕过 rewriter。 直接调 op->erase() 或 value.replaceAllUsesWith(),worklist 里会留下悬空指针或漏掉应重新访问的 op。

不收敛。 两个 pattern 互相撤销(A→B、B→A),或一个 pattern 生成的 IR 能再次匹配自己,都会跑到上限。

pattern 必须无状态。 matchAndRewrite 是 const 成员函数,不要在 pattern 对象里存跨调用的信息。

五、和相邻概念的区别

做什么 关键限制
fold 把 op 化简为已有的值或常量 不能新建 op,因此可在任何地方安全调用
RewritePattern + 贪心 driver 同一抽象层内的优化、规范化 跑到不动点,没有"合法性"概念
ConversionPattern + applyPartialConversion 跨 dialect 的 lowering 由 ConversionTarget 定义合法 op,目标是消灭非法 op,还要处理类型转换

面试常问"为什么融合 pass 和 lowering pass 用不同的 driver",答案就在最后一列。下一篇详细讲第三行。

此外,简单的一对一替换也可以用 TableGen 的 DRR(Pat<>)或 PDL 声明式地写,带复杂条件的仍用 C++。Triton 仓库中 lib/Dialect/Triton/Transforms/ 下的 Combine.td 与 Combine.cpp 就是两种写法混用的真实例子。

六、调试

使用带断言的 LLVM 构建,加 -debug-only=greedy-rewriter,可以看到每个 op 尝试了哪些 pattern、为什么失败。notifyMatchFailure 里的字符串就显示在这里,这是推荐用它而不是直接 return failure() 的原因。


思考题与参考答案

1. 去掉 hasOneUse 检查会怎样?

若 bias_add 的结果还有其他使用者,融合后 bias_add 不会被删除(仍有人用),而新的 bias_relu 又重新计算了一遍加偏置。语义仍然正确,但产生了重复计算和额外访存,性能反而变差。

2. pattern 返回 success 后,哪些 op 会被重新放入 worklist?

新建的 bias_relu;原 relu 结果的使用者(如 return);被删除的 relu 的操作数的定义者,即 bias_add。bias_add 此时已无使用者,若无副作用就会被 driver 删除。

3. 什么时候写 fold,什么时候写 canonicalization pattern?

结果能直接是一个已有的值或常量(如 x + 0 → x),写 fold;需要创建新 op 或做更复杂的结构调整,写 canonicalization pattern。

相关推荐
智享 AI 科技1 小时前
2026年AI私域系统怎么选?从功能到服务的全维度对比
大数据·人工智能·经验分享
镜象科技1 小时前
AI心理健康解决方案服务商行业趋势:镜象科技如何引领校园心理健康数字化升级
人工智能·科技
威嵌神州1 小时前
10.1 寸集成加密专网加固平板|外场加密通信硬件基线选型评估
人工智能·嵌入式硬件·平板
在夜里告别9891 小时前
视频掉帧怎么用AI补帧
人工智能
寻道码路1 小时前
大模型工程化实战(十八):全息地图——十七篇判据拼成一张「拿处境一查,就知道看哪篇、抓哪个决策、抄哪段代码」的检索图
人工智能·大模型·ai工程化·大模型工程化·agent工程化·rag工程化
JuiceFS1 小时前
地平线 EB 级存储工程实践
人工智能·后端·自动驾驶
JAVA_AI人工智能项目实战1 小时前
Agent核心架构(四层架构、ReAct、OTAC、自主决策、Multi-Agent)
人工智能·语言模型·自然语言处理·架构
坐吃山猪2 小时前
FDE前沿部署工程师
人工智能·开发
十铭忘2 小时前
四大坐标系的转换6——Plücker 坐标
人工智能