跳到主要内容

L3.5 硬核编译开发实战:LLVM Pass 与 MLIR Dialect

三维坐标 layer: L3(训练)level: Seniorpillar: 编程与编译 + 训推框架

本文是 L3 训练层的收官之作,也是「编程与编译」支柱最硬核的一篇。前面我们用框架(DeepSpeed / Megatron)调度上千卡,但框架最终都要把计算图降级(lowering)为机器能执行的 kernel——这条降级链路的工业级实现,就是 LLVMMLIR。本文不停留在「会用 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 @ bsoftmax)一步步降级到目标硬件能直接执行的指令。现代 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) 到标准 DialectConversionPattern + RewritePattern
Type/Attr(可选)自定义类型或属性ODS 或 C++

核心在于:MLIR 之所以成为所有 AI 芯片软件栈的底座,在于 「渐进式 lowering」保留了高层语义信息。例如 linalg.matmul 在高层是「矩阵乘」,编译器能据此做 tiling / fusion;若过早降级成一堆标量乘加循环,这些信息就永久丢失了。这正是「把昇腾 NPU 的张量指令对接进来」的技术抓手——你只需写一个新 Dialect + 一组 lowering pattern。

2.3 LLVM 与 MLIR 的分工

维度LLVMMLIR
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)

结论beforeafter 返回值都是 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 --versionopt --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()——storecall 等有副作用的指令即使「无返回使用者」也绝不能删,否则语义就变了。
  • Docker 里找不到 LLVMConfig.cmake:确保镜像装了 llvm-devsilkeh/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==falsePreservedAnalyses::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/ireellvm/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 层——把训练好的模型,以极限性价比变成可对外服务的在线系统。