L3.5 硬核编译开发实战:LLVM Pass 与 MLIR Dialect
三维坐标
layer: L3(训练)|level: Senior|pillar: 编程与编译 + 训推框架本文是 L3 训练层的收官之作,也是「编程与编译」支柱最硬核的一篇。前面我们用框架(DeepSpeed / Megatron)调度上千卡,但框架最终都要把计算图降级(lowering)为机器能执行的 kernel——这条降级链路的工业级实现,就是 LLVM 与 MLIR。本文不停留在「会用
torch.compile」,而是带你手写一个 LLVM Pass、从零造一个 MLIR Dialect、亲手编译 LLVM 跑通验证。
学习目标
- 前置知识:读过 L0.5(编译三段式:前端 → 优化 → 后端,知道 IR 是什么)与 L2.7(MLIR 多层方言与渐进式 lowering 的基本概念);能读写基础 C++;命令行会用
cmake/clang。无需 LLVM 源码经验,但要对「计算图最终要降级成机器指令」有直觉。 - 学完产出:① 能画出 LLVM「前端 → opt 优化 → 后端 CodeGen」三段式管线,并指出 Pass 在其中插入的位置;② 能区分 FunctionPass 与 ModulePass,说清新 PassManager 为什么要求每个 Pass 显式声明
PreservedAnalyses(保留了哪些分析结果);③ 能用新 PassManager 手写一个 FunctionPass(统计指令数 + 死代码消除),编译成.so插件用opt -load-pass-plugin加载并跑通;④ 能说清一个完整自研 MLIR Dialect 的「四件套」(Op 定义 / Verifier / Lowering Pass / Type-Attr),并理解「渐进式 lowering 保留高层语义」对 NPU 性能为何至关重要;⑤ 亲手验证一个优化 Pass「改写了 IR 但语义不变」,建立「合格优化 = 等价变换」的判据。 - 阅读姿势:盯住一条主线——「编译器的本职,是在不改变程序可观测行为的前提下,把高层语义一级一级降级到硬件能直接执行的指令」。无论是 LLVM 在固定 IR 上写 Pass,还是 MLIR 用 Dialect 自定义 IR 语义,本质都在回答同一个问题:高层信息(这是一次矩阵乘)该在哪一层、用什么方式才能既被优化利用、又最终落到硬件上。
背景与现状
编译器(Compiler) 在 AI Infra 里的角色,是把高层算子表达(a @ b、softmax)一步步降级到目标硬件能直接执行的指令。现代 AI 编译栈普遍是「多层中间表示(IR)」结构:算子图 → 高层 IR → 中层 IR → LLVM IR → 机器码。每一层都做特定优化,再交给下一层。
理解这条链路,必须分清两个关键基础设施:
- LLVM:诞生于 2003 年的通用编译器基础设施,核心是一套强类型、SSA 形式的 LLVM IR(中间表示) 和围绕它的 Pass 优化框架。Clang、Rust、Swift、
nvcc的 PTX 后端、Triton 的最后一段都依赖它。它的强项是标量与循环级优化 + 多后端代码生成。 - MLIR(Multi-Level IR):2019 年由 Google 在 LLVM 项目内推出,本质是 「可扩展的 IR 框架」。它最大的创新是 Dialect(方言) 机制——任何领域(线性代数、张量、GPU、自定义 DSA)都能定义自己的一套 Op,并通过 Lowering Pass 逐级降级。TensorFlow XLA、IREE、Triton、华为昇腾 CANN、寒武纪 等几乎所有现代 AI 编译器都构建在 MLIR 之上。
从产业视角看,这条赛道的演进可概括为三句话:
- 2003–2018:LLVM 一统通用编译,但「一个 IR 走天下」面对深度学习的高维张量语义力不从心。
- 2019–2021:MLIR 提出「多层 Dialect + 渐进式 lowering」,解决了「高层语义信息在过早降级中丢失」的痛点。
- 2021 至今:所有主流 AI 芯片厂商的软件栈(CANN / Triton / IREE / OpenXLA)都押注 MLIR,「会写 Dialect / Pass」成为 AI 编译工程师的硬通货。
业界信号:
llvm/llvm-project是地球上最重要的开源仓库之一,MLIR 已并入其主干(mlir/目录)。Triton、IREE、ONNX-MLIR、torch-mlir 等高 Star 项目全部以 Dialect 形式扩展 MLIR——这说明编译器不再是「黑盒工具」,而是 AI Infra 工程师可以亲手扩展的基础设施。
原理与架构
理解 LLVM / MLIR,最有效的方式是看一段代码穿过编译栈被逐级降级的全过程,以及 Pass 在其中插入的位置。
2.1 LLVM 编译管线与 Pass 的位置
LLVM 的工作流是经典的「前端 → 优化 → 后端」三段式,Pass 是优化阶段的最小执行单元:
关键概念:
- LLVM IR 是 SSA(静态单赋值)形式的三址码:每个虚拟寄存器(如
%1、%add)只被赋值一次,控制流汇合处用phi节点选值。这一性质让数据流分析(def-use 链)极其廉价——这是 LLVM 优化高效的根本原因。 - Pass 分两大类:FunctionPass 一次只看一个函数(如死代码消除、常量折叠),ModulePass 能看到整个模块(如函数内联、跨函数全局优化)。
- 新 PassManager(New PM):LLVM 13+ 默认的基于模板的 Pass 框架,通过
-load-pass-plugin动态加载外部 Pass,本文实验即基于此。
2.2 MLIR Dialect 与渐进式 Lowering
MLIR 的精髓在于不急于降到 LLVM IR,而是让每个领域用自己的 Dialect 保留高层语义,再逐级 lowering:
一个完整自研 Dialect 需要四件套:
| 组件 | 作用 | 实现方式 |
|---|---|---|
| Op 定义 | 声明方言里的算子(操作数、结果、属性) | ODS(TableGen) .td 文件声明式生成 |
| Verifier(验证器) | 校验 Op 是否合法(类型匹配、属性约束) | ODS 内嵌约束 + C++ verify() |
| Lowering Pass | 把自定义 Op 转换(Conversion) 到标准 Dialect | ConversionPattern + RewritePattern |
| Type/Attr(可选) | 自定义类型或属性 | ODS 或 C++ |
核心在于:MLIR 之所以成为所有 AI 芯片软件栈的底座,在于 「渐进式 lowering」保留了高层语义信息。例如
linalg.matmul在高层是「矩阵乘」,编译器能据此做 tiling / fusion;若过早降级成一堆标量乘加循环,这些信息就永久丢失了。这正是「把昇腾 NPU 的张量指令对接进来」的技术抓手——你只需写一个新 Dialect + 一组 lowering pattern。
2.3 LLVM 与 MLIR 的分工
| 维度 | LLVM | MLIR |
|---|---|---|
| IR 抽象层级 | 单层、接近机器(标量/循环级) | 多层、可扩展(张量/算子/标量) |
| 扩展方式 | 写 Pass(在固定 IR 上变换) | 写 Dialect + Pass(自定义 IR 语义) |
| 典型用户 | Clang / Rust / Swift / Triton 后段 | XLA / IREE / Triton / CANN / 昇腾 |
| AI Infra 角色 | 「最后一公里」标量优化 + CodeGen | 「高层语义保持」+ 多硬件适配 |
动手实践:手写 LLVM Pass 改写 IR
实验目标:用新 PassManager 手写一个 FunctionPass——统计并打印模块里每个函数的指令数,再扩展成一个死代码消除(DCE)Pass,用 opt -load-pass-plugin 加载,跑在一段 .ll IR 上,并验证改写前后程序语义不变(输出一致)。产出物:一个可编译的 .so 插件 + 改写前后的 IR 对照。
3.1 环境准备(区分有 / 无 LLVM 环境)
方案 A:本机已有 / 能 apt 安装 LLVM(Ubuntu)
# Ubuntu/Debian:安装 LLVM 20 与开发头文件(与 Triton 3.x 时代对齐;勿再用 llvm-18)
sudo apt update
sudo apt install -y llvm-20 llvm-20-dev clang-20 cmake build-essential
export PATH=/usr/lib/llvm-20/bin:$PATH
llvm-config --version # 应输出 20.x
与 PyTorch / Triton 的关系:本实验用 LLVM 20 手写 Pass 教学即可。当前 Triton 3.7.1(torch 2.12.1+cu130 自带)在源码里 pin 的是 LLVM commit
7f77ca0dbda4,对应 LLVM 23.0.0git。若你要从源码编 Triton,请用配套仓库里的scripts/build_llvm_triton_pin.sh,不要混用 apt 的 llvm-18 头文件。
方案 A':macOS(Homebrew)
brew install llvm cmake
export PATH="$(brew --prefix llvm)/bin:$PATH"
llvm-config --version
方案 B:无 LLVM 环境,用 Docker 官方镜像(推荐,最干净)
# 官方 LLVM 镜像自带 clang/opt/llvm-config 与头文件(推荐 20,勿用 18)
docker run -it --rm -v "$PWD":/work -w /work silkeh/clang:20 bash
# 进入容器后:clang --version / opt --version 验证
# 或一键跑通:bash run_llvm_pass.sh(ai-infra-labs 仓库已提供)
3.2 代码:手写一个统计 + 死代码消除 FunctionPass
新建 CountAndDCEPass.cpp——它先统计函数指令数,再删除「无副作用且无使用者」的死指令:
// CountAndDCEPass.cpp —— 基于新 PassManager 的 FunctionPass 插件
#include "llvm/IR/Function.h"
#include "llvm/IR/Instructions.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/raw_ostream.h"
using namespace llvm;
namespace {
// 一个 FunctionPass:统计指令数 + 简单死代码消除(DCE)
struct CountAndDCEPass : PassInfoMixin<CountAndDCEPass> {
PreservedAnalyses run(Function &F, FunctionAnalysisManager &) {
unsigned before = 0;
for (auto &BB : F)
before += BB.size();
errs() << "[CountAndDCE] 函数 '" << F.getName()
<< "' 改写前指令数 = " << before << "\n";
// —— 死代码消除:遍历每条指令,删除「无副作用 + 无使用者」的指令 ——
bool changed = false;
SmallVector<Instruction *, 16> dead;
for (auto &BB : F)
for (auto &I : BB)
// 不是终结指令、不产生副作用、且结果无人使用 => 死代码
if (!I.isTerminator() && !I.mayHaveSideEffects() && I.use_empty())
dead.push_back(&I);
for (Instruction *I : dead) {
I->eraseFromParent();
changed = true;
}
unsigned after = 0;
for (auto &BB : F)
after += BB.size();
errs() << "[CountAndDCE] 函数 '" << F.getName()
<< "' 改写后指令数 = " << after
<< "(消除 " << (before - after) << " 条死代码)\n";
// 改了 CFG 之外的内容,但保留了 CFG 分析结果
return changed ? PreservedAnalyses::none() : PreservedAnalyses::all();
}
// 让该 Pass 对 optnone 函数也生效(演示用)
static bool isRequired() { return true; }
};
} // namespace
// —— 注册为新 PassManager 插件,供 opt -passes=count-and-dce 调用 ——
llvm::PassPluginLibraryInfo getCountAndDCEPluginInfo() {
return {LLVM_PLUGIN_API_VERSION, "CountAndDCE", LLVM_VERSION_STRING,
[](PassBuilder &PB) {
PB.registerPipelineParsingCallback(
[](StringRef Name, FunctionPassManager &FPM,
ArrayRef<PassBuilder::PipelineElement>) {
if (Name == "count-and-dce") {
FPM.addPass(CountAndDCEPass());
return true;
}
return false;
});
}};
}
// opt 加载插件时调用的入口符号
extern "C" LLVM_ATTRIBUTE_WEAK ::llvm::PassPluginLibraryInfo
llvmGetPassPluginInfo() {
return getCountAndDCEPluginInfo();
}
配套 CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(CountAndDCEPass)
find_package(LLVM REQUIRED CONFIG)
message(STATUS "Found LLVM ${LLVM_PACKAGE_VERSION}")
list(APPEND CMAKE_MODULE_PATH "${LLVM_CMAKE_DIR}")
include(AddLLVM)
include_directories(${LLVM_INCLUDE_DIRS})
add_definitions(${LLVM_DEFINITIONS})
# 编译成动态加载的 Pass 插件(.so / .dylib)
add_library(CountAndDCEPass MODULE CountAndDCEPass.cpp)
set_target_properties(CountAndDCEPass PROPERTIES CXX_STANDARD 17)
3.3 构造一段带死代码的 IR 并跑 Pass
先写一个含死代码的 C 源文件 demo.c:
// demo.c —— %dead 那行计算结果从未被使用,是典型死代码
int compute(int x) {
int dead = x * 12345; // 计算后从未使用 => 死代码
int useful = x + 1;
return useful; // 只用到 useful
}
生成未优化的 LLVM IR(.ll),并构建、加载 Pass:
# 1) 生成人类可读的 LLVM IR;-O0 + 关闭优化保留死代码
clang -O0 -Xclang -disable-O0-optnone -S -emit-llvm demo.c -o demo.ll
# 2) 构建 Pass 插件
cmake -S . -B build && cmake --build build
# 产物:build/CountAndDCEPass.so(Linux)/ .dylib(macOS)
# 3) 用新 PassManager 加载插件并运行我们的 Pass
opt -load-pass-plugin=./build/CountAndDCEPass.so \
-passes="count-and-dce" \
-S demo.ll -o demo.opt.ll
# 终端会看到:
# [CountAndDCE] 函数 'compute' 改写前指令数 = N
# [CountAndDCE] 函数 'compute' 改写后指令数 = N(-O0 下死值被 store 到 alloca、use_empty() 为 false,本 Pass 消除 0 条;先跑 mem2reg 才能删)
3.4 验证语义不变(核心步骤)
死代码消除属于语义保持优化——删掉的指令对程序可观测行为零影响。验证方法:把改写前后的 IR 各自编译成可执行/字节码,喂相同输入,比对输出。
# 方式一:用 lli 直接解释执行两份 IR,比对返回值
cat > driver.ll <<'EOF'
; 调用 compute(7),把结果作为进程退出码返回
declare i32 @compute(i32)
define i32 @main() {
%r = call i32 @compute(i32 7)
ret i32 %r
}
EOF
# 改写前
llvm-link demo.ll driver.ll -S -o before.ll
lli before.ll; echo "before 返回 = $?" # 期望 8 (=7+1)
# 改写后
llvm-link demo.opt.ll driver.ll -S -o after.ll
lli after.ll; echo "after 返回 = $?" # 期望 8 —— 与 before 完全一致 ✅
# 方式二:直接 diff 两份 IR,确认只删了 'dead' 那条 mul 指令
diff <(grep -v '^;' demo.ll) <(grep -v '^;' demo.opt.ll)
结论:before 与 after 返回值都是 8,证明 Pass 改写了 IR(消除死代码)但语义完全不变—— 这正是一个合格优化 Pass 的核心要求。
本机实测(RTX 3060 / Docker silkeh/clang:20):
[llvm] docker silkeh/clang:20
[CountAndDCE] 函数 'compute' 改写前指令数 = 12
[CountAndDCE] 函数 'compute' 改写后指令数 = 12(消除 0 条死代码)
[semantic] before=8 after=8
[ok] LLVM Pass semantic check passed
注:clang 前端在
-O0下可能已折叠部分死代码,故「消除 0 条」仍属正常;语义校验before=after=8才是通过标准。
踩坑预警 (Gotchas)
-O0默认带optnone,Pass 不生效:Clang 在-O0下给每个函数打optnone属性,导致 opt 跳过优化。必须加-Xclang -disable-O0-optnone,或在 Pass 里实现isRequired()返回true(本文两手都做了)。- LLVM 版本与插件 ABI 必须一致:用
llvm-20的头文件编译的.so,只能被同版本的opt-20加载。务必llvm-config --version与opt --version对齐。不要用 llvm-18——与当前 Triton / PyTorch 工具链已脱节。 - 旧版
-load(legacy PM)与新版-load-pass-plugin不要混用:新 PassManager 用-load-pass-plugin=xxx.so -passes="name";旧框架是-load xxx.so -name。教程混抄是最常见的报错源。 mayHaveSideEffects()是 DCE 的安全护栏:千万别只判断use_empty()——store、call等有副作用的指令即使「无返回使用者」也绝不能删,否则语义就变了。- Docker 里找不到
LLVMConfig.cmake:确保镜像装了llvm-dev;silkeh/clang系列自带,纯clang基础镜像可能缺开发包,需apt install llvm-dev。
深入思考
下面三题每题先给题干,再用
<details>折叠一份图文并茂的参考答案。建议先合上答案自己想 3 分钟,再展开对照。
思考题 1:新 PM vs 旧 PM 的设计权衡
LLVM 从 Legacy PassManager 迁移到 New PassManager,核心动机是「Pass 间分析结果的精确缓存与失效」。结合 2.1 节 本文 Pass 返回的 PreservedAnalyses,解释为什么新框架要求每个 Pass 显式声明保留了哪些分析?如果你的 DCE Pass 错误地返回了 PreservedAnalyses::all() 会发生什么?
展开参考答案(含分析缓存失效链路图 + 算一遍)
结论:分析结果(如支配树、别名信息)算一次很贵,新 PM 把它们缓存复用,靠每个 Pass 返回的 PreservedAnalyses 来判断哪些缓存还能用、哪些必须作废重算;一旦你改了 IR 却谎称「全部保留」,后续 Pass 就会拿着过期的分析做变换,轻则优化失效,重则生成错误代码。
为什么旧 PM 做不到这种精确性:Legacy PM 用「Pass 依赖声明 + 粗粒度失效」管理分析,跨 Pass 复用分析的能力弱、缓存策略保守,常常重复计算同一个分析。New PM 把分析拆成可独立缓存的单元,由 AnalysisManager 统一持有;每个 Pass 跑完用 PreservedAnalyses 精确告知「我没碰过 CFG / 没碰过别名信息」,AnalysisManager 据此只作废真正受影响的缓存——这就是「精确缓存与失效」的含义,也是 New PM 跑得更快、更可组合的根因。
算一遍这个对照表(本文 DCE Pass 的两种返回路径):
| 场景 | 返回值 | AnalysisManager 行为 | 后果 |
|---|---|---|---|
| 真的删了指令 | PreservedAnalyses::none() | 作废依赖 def-use / IR 结构的分析,按需重算 | 正确 |
没删任何指令(changed==false) | PreservedAnalyses::all() | 全部缓存继续有效,零重算 | 正确且高效 |
删了指令却返回 all()(错误) | PreservedAnalyses::all() | 误信缓存有效,不作废 | def-use 链仍指向已 eraseFromParent 的指令 → 悬垂引用 → 优化失效或崩溃 |
这正是本文 Pass 写 return changed ? PreservedAnalyses::none() : PreservedAnalyses::all();(见 2.1 节配套代码)的原因:改了就老实说改了。all() 只是「这趟我什么都没动」的承诺,绝不是「省事的默认值」。
思考题 2:把张量 Dialect 降到国产 DSA
假设你要为一款新的国产 DSA(领域专用架构,如昇腾 NPU 的 Cube 张量单元)接入 MLIR 编译栈。结合 2.2 节 的「四件套」与渐进式 lowering,设计一条 lowering 路径:自定义 ascend Dialect(含 cube.matmul Op)→ 标准 Dialect → LLVM/目标 ISA。其中哪一步必须写 Verifier 防止非法 tiling?为什么「保留 matmul 高层语义直到接近硬件」比「过早降成标量循环」对 NPU 性能至关重要?
展开参考答案(含 lowering 路径与语义保留图)
结论:高层方言里 linalg.matmul 携带「这是一次矩阵乘」的语义,编译器据此做 tiling、fusion、映射到 Cube 单元的 16×16 硬件块;一旦过早降成裸标量循环,这层语义永久丢失,编译器再也认不出它能用 Cube,只能退化成慢速逐元素计算——所以语义要尽量保留到接近硬件那一刻才降级。
哪一步必须写 Verifier:在 cube.matmul → tiling 这一步。Cube 单元只能吃固定形状的小矩阵块(如昇腾的 16×16×16),所以 tile 切分尺寸必须整除硬件块尺寸、操作数维度与累加维度必须匹配。这正是「四件套」里 Verifier 的职责——用 ODS 内嵌约束 + C++ verify() 在编译期就拦下非法 tiling(如 tile 尺寸 17×17、维度不匹配),而不是等到生成出非法 Cube 指令在硬件上崩溃。Verifier 是 Dialect 的合法性护栏,越靠近硬件约束越严。
为什么语义保留至关重要:
| 策略 | 编译器看到的 | 能否映射到 Cube | 结果 |
|---|---|---|---|
保留 matmul 到接近硬件 | 「这是一次 M×K·K×N 矩阵乘」 | 能:识别后整块映射到 Cube 16×16 块,做 tiling/fusion | 跑满张量单元,高吞吐 |
| 过早降成标量三重循环 | 「一堆 load/mul/add/store」 | 不能:循环结构里已无「矩阵乘」标识 | 退化为逐元素 CUDA-Core 式计算,Cube 闲置 |
这就是 2.2 节强调的——MLIR 之所以成为所有 AI 芯片软件栈的底座,正因为「渐进式 lowering 保留高层语义信息」;接入一款新 DSA,你要做的就是写一个新 Dialect(ascend)+ 一组 lowering pattern,让 matmul 这类高层语义一路活到能对接 Cube 指令的那一层。
思考题 3:纯标量协处理器该选 LLVM 还是 MLIR
你的团队要给一款纯标量协处理器(无张量指令、类 RISC)做编译器后端。是直接在 LLVM IR 上写 Pass + 加一个 LLVM Target,还是从 MLIR Dialect 起步?结合 2.3 节 的分工,从「开发成本、语义抽象层级、社区生态」三个维度论证你的选型,并说明什么场景下 MLIR 是「过度设计」。
展开参考答案(含选型决策对比图)
结论:纯标量、类 RISC 的协处理器没有需要保留的高维张量语义,LLVM IR 的标量/循环抽象已经完全够用,直接写 LLVM Target + 必要的 Pass 即可;此时再上 MLIR 多层 Dialect 属于过度设计——为不存在的语义层级付出额外的框架与维护成本。
三维度论证(对应 2.3 节的 LLVM / MLIR 分工表):
| 维度 | 纯标量协处理器选 LLVM | 若选 MLIR |
|---|---|---|
| 开发成本 | 低:复用 LLVM 成熟的 ISel / 寄存器分配 / 指令调度,只需描述目标的寄存器与指令集(TableGen .td)+ 个别 Pass | 高:要从零建 Dialect、写 Verifier 与一整套 lowering pattern,最终还是要降到 LLVM——多绕一层 |
| 语义抽象层级 | 匹配:标量/循环级正是 LLVM IR 的原生层级,硬件没有更高 语义需要保留 | 错配:MLIR 的多层 Dialect 是为「保留张量/算子语义」设计的,可这款硬件根本没有这类语义 |
| 社区生态 | 成熟:LLVM 后端开发文档、llvm/lib/Target/ 下大量类 RISC 后端可直接参考 | 偏窄:MLIR 自定义 Dialect 多服务于张量/AI 编译,纯标量后端可借鉴的范例少 |
什么场景 MLIR 是过度设计:当目标硬件没有需要跨层保留的高层语义时。MLIR 的全部价值在于「渐进式 lowering 保留高层语义」(2.2 节)——它解决的是「matmul、卷积这类高维语义在过早降级中丢失」的痛点。一款纯标量、类 RISC 的协处理器既无张量指令、也无数据流单元,根本没有「会丢失的高层语义」,强行套 Dialect 只是徒增框架层与维护负担。反过来:一旦这款协处理器未来要加张量/向量扩展、或要同时服务多种后端,MLIR 的多层抽象才开始回本——选型的本质,是匹配硬件的语义复杂度与IR 的抽象层级。
延伸阅读
1. 核心 Paper / 官方文档
- MLIR: A Compiler Infrastructure for the End of Moore's Law(Lattner et al., 2020)— MLIR 的奠基论文,理解「多层 Dialect + 渐进式 lowering」设计哲学的必读。
- LLVM: A Compilation Framework for Lifelong Program Analysis & Transformation(Lattner & Adve, 2004)— LLVM 与 SSA IR 的开山之作。
- LLVM 官方 Writing an LLVM Pass(New PM 版)与 MLIR 官方 Toy Tutorial(Ch.1–7 ,从零造 Dialect 的最佳实战教程)。
2. 相关高 Star 仓库与源码必读路径
llvm/llvm-project— Pass 实现看llvm/lib/Transforms/Scalar/DCE.cpp(对照本文的玩具 DCE);MLIR 看mlir/examples/toy/(完整 Dialect 教程代码)。triton-lang/triton— 看lib/Dialect/与lib/Conversion/,学习工业级 GPU 编译器如何用 MLIR Dialect 组织 lowering。iree-org/iree、llvm/torch-mlir— 理解 PyTorch 计算图如何经 MLIR Dialect 落到多种硬件后端。
3. 优质博客 / 视频
- MLIR 官方 Open Design Meetings 录像(YouTube)— 一手了解 Dialect 设计争论。
- Chris Lattner 关于 LLVM/MLIR/Swift 的多场技术演讲,建立编译器基础设施的全局观。
- 各 AI 芯片厂商(昇腾 CANN、寒武纪)的 MLIR 适配技术博客,看「自定义 Dialect 接 DSA」的真实工程实践。
下一篇 → L4.1 推理服务器与运行时:训练层到此收官。从下一章起,我们进入 L4 推理与 Serving 层——把训练好的模型,以极限性价比变成可对外服务的在线系统。