1. 课程导入

编译器技术在过去二十年经历了从“手工优化”到“分层抽象”的深刻变革。LLVM的出现统一了中后端优化与代码生成的接口,但面对AI加速器、FPGA、专用DSP以及异构计算架构的爆发式增长,传统编译器的“前端-中端-后端”三层模型逐渐暴露出抽象层次单一、扩展成本高昂的问题。MLIR(Multi-Level Intermediate Representation,多级中间表示)正是在这一背景下由Google于2019年发起并贡献给LLVM基金会。它并非要取代LLVM IR,而是要在LLVM IR之上构建一套可复用的、支持多抽象级别的中间表示基础设施。本章作为课程的第一讲,将系统梳理MLIR的起源动机、核心设计目标、与LLVM的协作关系,并深入剖析Dialect与Pass这两个贯穿MLIR开发始终的核心概念。

MLIR 核心概念关系图:起源、分层与Pass驱动 起源与动机 • 硬件多样性(AI ASIC/FPGA/DSP) • 前端语言多样化(Python/C++/DSL) • LLVM IR 抽象层次单一 • 重复建设轮子:TVM/XLA/GLOW 设计目标 • 多抽象级别表示(Multi-Level) • 可扩展、可组合的基础设施 • 渐进式 lowering 与优化 • 复用 LLVM 中后端能力 与LLVM的关系 • MLIR 是 LLVM 项目的一部分 • MLIR 可 lowering 到 LLVM IR • 复用 LLVM 优化与代码生成 • 不替代 LLVM,而是补充上层 MLIR 核心抽象:Dialect 与 Pass Dialect(方言) • 定义操作(Operation)、类型(Type)、 属性(Attribute)的命名空间 • 示例: tensor, linalg, affine, scf, arith, func, llvm, gpu, spirv • 每个 Dialect 自带 verifier、parser、 printer 和 canonicalization 规则 Pass(优化与转换) • 对 IR 进行遍历、匹配、重写 • 两种形式: 1. 分析 Pass(Analysis) 2. 转换 Pass(Transformation) • 驱动 Dialect 间 lowering • 支持 pipeline 编排与复用 • 示例:--convert-linalg-to-loops lowering 高层 Dialect 中层级 Dialect LLVM Dialect LLVM IR / 机器码 Pass Pipeline 驱动逐级 lowering,每级保留语义等价性

1.1 MLIR的起源:从硬件碎片化到编译器基础设施重构

核心原理:为什么LLVM IR不够用了?

LLVM IR的设计初衷是作为编译器中端的“通用语言”——它假设所有前端(C/C++/Rust等)经过一系列降级后,都能统一到一组相对底层的操作上(load/store、算术运算、控制流、函数调用等)。这一假设在CPU领域基本成立,因为x86、ARM、RISC-V等架构的指令集抽象层次相近。然而,当编译器需要面向AI加速器(如TPU、NPU)、GPU、FPGA、DSP等异构硬件时,LLVM IR暴露出三个根本性缺陷:

  • 抽象层次错位:AI加速器上的“矩阵乘”是一个原子操作,但在LLVM IR中会被拆解为成千上万条标量指令,优化器无法恢复其语义,更无法针对硬件特性(如脉动阵列、片上缓存)做优化。
  • 类型系统不匹配:LLVM IR只支持整数、浮点、向量、指针等基础类型,无法表达张量的形状、布局(NCHW vs NHWC)、量化参数(int8/uint8/对称/非对称)等高层信息。
  • 扩展成本极高:若要在LLVM IR中新增一个“矩阵乘”指令,需要修改LLVM核心的指令定义、Verifier、文档、所有后端,这对任何团队都是不可接受的。

在MLIR出现之前,业界已经出现了大量“各自为政”的编译器栈:Google的XLA(面向TPU)、TVM(面向GPU/FPGA)、Intel的nGraph、Facebook的Glow等。它们都在LLVM之上或之外构建了私有的IR和优化管道,导致代码无法复用、社区分裂。MLIR的提出正是为了终结这种“重复造轮子”的局面。

判断方法:何时应该使用MLIR?

判断一个编译器项目是否适合引入MLIR,可以从以下三个维度评估:

评估维度适合使用MLIR不适合使用MLIR
目标硬件异构加速器、自定义指令集、FPGA、多后端支持仅面向x86/ARM等成熟CPU,且无扩展计划
输入语言DSL、Python框架、多前端融合、需要高层语义保留单一传统语言(如仅C),且无需自定义优化
优化需求需要硬件特定优化(布局转换、算子融合、量化)仅需标准优化(内联、循环展开、常量传播)

一个典型的反例是:如果只是为某个C语言子集编写一个简单的静态编译器,直接使用LLVM前端(Clang)+ LLVM IR即可,引入MLIR反而会增加不必要的抽象层和开发成本。

案例拆解:TVM与MLIR的对比

TVM是一个面向深度学习模型的编译器栈,它设计了Relay IR(高层图IR)和TIR(张量IR)。TVM团队在开发过程中发现,Relay和TIR之间的边界模糊,且无法复用LLVM社区的优化Pass。当MLIR发布后,TVM社区开始探索将TIR迁移到MLIR的affine dialect和linalg dialect上。这一迁移的核心收益在于:

  • MLIR提供了现成的循环变换基础设施(affine dialect),无需TVM自己维护一套循环优化框架。
  • MLIR的Dialect间转换机制(如linalg→affine→scf→llvm)使得TVM可以聚焦于高层图优化,而将底层代码生成交给MLIR+LLVM。
  • 代价是TVM需要学习MLIR的API和Pass基础设施,迁移成本约3-6人月。

这个案例说明,MLIR并非银弹,但它确实为“高层IR与底层IR之间需要多级过渡”的场景提供了标准化的解决方案。

常见误区与总结

误区1:认为MLIR会取代LLVM IR。实际上,MLIR的llvm dialect最终会lowering到LLVM IR,两者是协作关系而非竞争关系。
误区2:认为MLIR只能用于AI编译器。MLIR的Dialect机制是通用的,它同样适用于HPC、信号处理、FPGA综合等场景。
误区3:认为MLIR开箱即用。MLIR是一个“编译器开发工具包”,你需要为自己的领域定义Dialect和Pass,这需要一定的学习曲线。

1.2 MLIR的设计目标:多抽象级别与渐进式Lowering

核心原理:从“单一IR”到“IR光谱”

传统编译器的IR是“一维”的——从AST到LLVM IR再到机器码,中间只有一次大的抽象跳跃。MLIR的核心设计哲学是:编译器应该由多个抽象级别的IR组成,每个IR(即Dialect)服务于特定的优化目标,且相邻IR之间的转换(Lowering)应该是渐进、可验证的。例如,一个深度学习模型的编译过程可以经历以下Dialect层次:

  1. Tensor Dialect:表示张量运算,保留形状、数据类型、布局等高层信息,适合做算子融合和布局优化。
  2. Linalg Dialect:将张量运算结构化地映射到循环结构,支持循环分块、向量化等变换。
  3. Affine Dialect:显式表达循环嵌套和内存访问模式,适合做循环重排、并行化。
  4. SCF Dialect(Structured Control Flow):将Affine循环降级为通用的结构化控制流(for/while/if)。
  5. LLVM Dialect:将SCF和算术操作映射到LLVM IR的语义。

这种“IR光谱”设计使得每一层优化都只关注本层的信息,不需要关心底层细节。例如,Tensor Dialect的算子融合Pass不需要知道最终是x86还是GPU,它只负责将两个tensor操作合并为一个。

操作流程:如何设计一个自定义Dialect

在实际项目中,开发者经常需要定义自己的Dialect。以下是标准流程:

// 1. 定义Dialect类(继承mlir::Dialect)
class MyDialect : public mlir::Dialect {
public:
  explicit MyDialect(mlir::MLIRContext *ctx);
  // 注册类型、属性、操作
};

// 2. 定义Operation(继承mlir::Op)
class MyOp : public mlir::Op<MyOp, ...> {
public:
  using Op::Op;
  // 定义操作数、结果、属性、Verifier
  static void build(mlir::OpBuilder &builder, ...);
  mlir::LogicalResult verify();
};

// 3. 在Dialect构造函数中注册
MyDialect::MyDialect(mlir::MLIRContext *ctx)
    : mlir::Dialect(getDialectNamespace(), ctx) {
  addOperations<MyOp>();
  addTypes<MyType>();
}

定义完成后,还需要编写Canonicalization Patterns(简化模式)和Fold Hooks(常量折叠),这些是MLIR优化Pass的基础。

实战场景:Tensor Dialect的算子融合

假设我们有一个模型包含“卷积→ReLU→池化”三个操作。在Tensor Dialect层面,这三个操作是独立的节点。通过一个融合Pass,我们可以将它们合并为一个“ConvReLUPool”操作。这个Pass的输入是Tensor Dialect的IR,输出仍然是Tensor Dialect的IR,但操作数量从3个减少到1个。融合后的IR在后续lowering到Linalg时,可以直接生成一个高效的循环嵌套,避免中间结果的写回内存。

关键参数:融合Pass需要检查每个操作的输入输出形状是否匹配、数据类型是否一致、是否有其他用户引用中间结果。如果ReLU的输出被另一个分支使用,则不能融合。

常见误区与总结

经验提醒:不要试图在一个Dialect中表达所有信息。Dialect的设计原则是“单一职责”——每个Dialect只关注一个抽象层次。如果发现你的Dialect需要同时处理张量形状和循环结构,考虑拆分为两个Dialect,并通过转换Pass连接。

1.3 与LLVM的关系:协作而非替代

核心原理:MLIR在LLVM生态中的定位

MLIR是LLVM项目的一个子项目,它位于LLVM IR的上层。MLIR的llvm dialect提供了对LLVM IR指令、类型和属性的直接映射,因此任何MLIR IR最终都可以通过--convert-to-llvm Pass转换为LLVM IR。这意味着:

  • MLIR可以复用LLVM的所有中后端优化(如指令选择、寄存器分配、指令调度)。
  • MLIR的Pass可以调用LLVM的分析和转换接口(如LoopInfo、ScalarEvolution)。
  • MLIR的代码生成可以无缝对接LLVM的Target后端(x86、ARM、NVPTX等)。

从工程角度看,MLIR与LLVM的关系类似于“前端框架”与“后端引擎”——MLIR负责处理高层语义和硬件特定优化,LLVM负责底层的通用代码生成。

判断方法:何时lowering到LLVM IR?

并非所有MLIR IR都需要lowering到LLVM IR。以下决策树可以帮助判断:

场景推荐路径原因
目标为CPU(x86/ARM)MLIR → LLVM IR → 机器码LLVM的CPU后端成熟稳定
目标为GPU(CUDA/ROCm)MLIR → GPU Dialect → LLVM NVPTX/AMDGPU需要GPU特定的内存模型和并行语义
目标为FPGAMLIR → 自定义硬件Dialect → HLS/VerilogLLVM IR无法表达硬件时序和资源约束
目标为自研AI加速器MLIR → 自定义加速器Dialect → 专用代码生成需要完全控制指令级并行和数据流

案例拆解:一个端到端的MLIR编译流程

以Google的TPU编译器为例,其编译流程如下:

  1. 前端将TensorFlow计算图转换为MLIR的mhlo dialect(HLO:High-Level Operations)。
  2. 执行mhlo层面的优化:算子融合、常量折叠、形状推断。
  3. 通过--convert-mhlo-to-linalg将mhlo转换为linalg dialect。
  4. 在linalg层面执行循环变换(分块、向量化、并行化)。
  5. 通过--convert-linalg-to-tpu将linalg转换为TPU自定义dialect。
  6. TPU dialect的Pass生成TPU微码(类似汇编)。

整个流程中,LLVM IR并未出现——因为TPU不需要LLVM的指令选择。但如果是面向CPU的推理引擎,则第5步会替换为--convert-linalg-to-loops和--convert-scf-to-cf,最终lowering到LLVM IR。

常见误区与总结

风险提示:MLIR的灵活性也带来了“过度设计”的风险。有些团队在不需要多级IR的情况下强行引入MLIR,导致编译流程复杂化。建议从最小可行Dialect开始,逐步扩展,而不是一开始就设计一个庞大的IR体系。

1.4 Dialect与Pass:MLIR的两大支柱

核心原理:Dialect是词汇表,Pass是语法规则

如果把MLIR比作一门语言,那么Dialect就是词汇表——它定义了有哪些“单词”(操作)和“词性”(类型、属性);Pass就是语法规则——它定义了如何组织这些单词形成有意义的“句子”(优化后的IR)。Dialect和Pass是正交的:一个Dialect可以被多个Pass操作,一个Pass也可以跨多个Dialect工作。

Dialect的核心组件包括:

  • Operation:IR的基本单元,类似于LLVM的Instruction,但更灵活(可以包含任意数量的操作数和区域)。
  • Type:定义操作数的类型系统,如TensorType、MemRefType、VectorType。
  • Attribute:编译期常量信息,如张量形状、量化参数、循环步长。
  • Interface:为操作提供统一的行为抽象,如LoopLikeInterface、ViewLikeInterface。

Pass的核心机制包括:

  • Pattern Rewriter:基于模式匹配的IR重写引擎,是大多数优化Pass的基础。
  • Analysis:对IR进行只读分析(如依赖分析、活跃变量分析),结果可被多个Pass复用。
  • Pass Pipeline:将多个Pass按顺序编排,支持嵌套和复用。

操作流程:编写一个简单的Pass

// 定义一个转换Pass:将MyOpA替换为MyOpB
struct ConvertMyOpPass : public mlir::PassWrapper<ConvertMyOpPass, 
                                                    mlir::OperationPass<mlir::func::FuncOp>> {
  void runOnOperation() override {
    mlir::func::FuncOp func = getOperation();
    func.walk([&](MyOpA op) {
      mlir::OpBuilder builder(op);
      builder.setInsertionPoint(op);
      auto newOp = builder.create<MyOpB>(op.getLoc(), op.getOperand());
      op.replaceAllUsesWith(newOp.getResult());
      op.erase();
    });
  }
};

// 注册Pass
void registerMyPasses() {
  mlir::PassRegistration<ConvertMyOpPass>("convert-myop-a-to-b", 
                                           "Convert MyOpA to MyOpB");
}

这段代码展示了Pass的基本骨架:遍历IR、匹配目标操作、创建新操作、替换使用点、删除旧操作。实际项目中,更推荐使用mlir::RewritePattern来编写声明式的转换规则,因为Pattern Rewriter会自动处理操作数替换和IR合法性维护。

实战场景:Dialect间Lowering的典型模式

假设我们需要将自定义的matmul操作lowering到linalg dialect。标准做法是编写一个ConversionPattern:

class MatmulLowering : public mlir::OpConversionPattern<MatmulOp> {
public:
  using OpConversionPattern::OpConversionPattern;
  
  mlir::LogicalResult matchAndRewrite(
      MatmulOp op, OpAdaptor adaptor,
      mlir::ConversionPatternRewriter &rewriter) const override {
    // 将MatmulOp转换为linalg.matmul
    rewriter.replaceOpWithNewOp<mlir::linalg::MatmulOp>(
        op, op.getResult().getType(), 
        adaptor.getOperands());
    return mlir::success();
  }
};

这个Pattern在--convert-my-dialect-to-linalg Pass中被注册。当Pass运行时,它会遍历所有MatmulOp,并尝试将其替换为linalg.matmul。如果替换成功,原操作被删除;如果失败(例如类型不匹配),则保留原操作并报告错误。

常见误区与总结

经验提醒:编写Pass时,务必使用mlir::OpBuilder和mlir::ConversionPatternRewriter来修改IR,而不是直接操作操作数的内部指针。MLIR的IR是SSA形式的,直接修改可能导致use-def链不一致。

1.5 本章总结与学习路径建议

本章从MLIR的起源出发,梳理了其设计目标、与LLVM的关系,并深入剖析了Dialect和Pass这两个核心概念。需要强调的是,MLIR不是一门“语言”,而是一个编译器开发框架——它的价值在于让你能够以标准化的方式构建自定义的IR和优化管道。在后续章节中,我们将逐一深入:第2章讲解MLIR的IR结构(Operation、Value、Region、Block);第3章讲解Dialect开发实战;第4章讲解Pass基础设施与Pipeline编排;第5章开始进入具体的Dialect(affine、linalg、scf等)和转换模式。建议读者在学习过程中,始终带着“我的编译器需要哪些抽象层次?每个层次需要什么信息?”这两个问题去理解MLIR的设计选择。

本章核心要点回顾:
  • MLIR的诞生源于硬件多样性和LLVM IR抽象层次单一之间的矛盾。
  • MLIR的核心设计目标是“多抽象级别表示”和“渐进式Lowering”。
  • MLIR与LLVM是协作关系:MLIR负责高层优化,LLVM负责底层代码生成。
  • Dialect定义IR的“词汇表”,Pass定义IR的“语法规则”,两者正交组合。
  • 开发MLIR项目时,应从最小可行Dialect开始,避免过度设计。