一条大祥脚头像

AI 编译器入门:TVM 与 MLIR 的原理、差异与实战

1. 为什么需要 AI 编译器

深度学习模型从论文走向生产,中间隔着一道巨大的鸿沟:同一份模型,要跑在 NVIDIA GPU、AMD GPU、ARM CPU、手机 NPU、甚至各种自研加速器上。如果每个后端都手写一套算子实现,工程量会呈指数级爆炸。

传统做法是「框架 + 算子库」:PyTorch 定义计算图,后端调用 cuDNN、MKL、NNAPI 等库。这套方案在通用硬件上很成熟,但遇到新硬件、新算子、新量化格式时,往往要等厂商补齐库支持,迭代周期长。

AI 编译器要解决的核心问题就是:把高层计算图自动、可优化地映射到多样化的硬件上。它把「写算子」变成「写编译规则」,让一次模型定义可以生成多后端的机器码。

一个典型的 AI 编译流程可以抽象为:

模型定义
PyTorch / ONNX

图级 IR
计算图

图优化
算子融合 / 常量折叠

张量级 IR
循环与调度

调度优化
分块 / 向量化 / 并行

代码生成
LLVM / CUDA / 汇编

目标硬件

在这条链路上,TVM 和 MLIR 是当前最具代表性的两套基础设施,但它们的定位并不相同。

2. TVM:端到端的深度学习编译栈

2.1 TVM 的设计哲学

TVM 由陈天奇等人提出,最初的目标是「让深度学习框架的模型能高效跑在各种硬件上」。它的核心创新是计算与调度分离(compute / schedule separation)。

在 TVM 中,你只描述「算什么」,不描述「怎么算」:

import tvm
from tvm import te

# 计算:C[i, j] = sum_k A[i, k] * B[k, j]
n = 1024
A = te.placeholder((n, n), name="A")
B = te.placeholder((n, n), name="B")
k = te.reduce_axis((0, n), name="k")
C = te.compute(
    (n, n),
    lambda i, j: te.sum(A[i, k] * B[k, j], axis=k),
    name="C",
)

这段代码只声明了矩阵乘的数学语义,没有任何循环结构。真正的性能来自调度:

s = te.create_schedule(C.op)

# 分块,提升缓存命中率
bn = 32
i_outer, i_inner = s[C].split(C.op.axis[0], factor=bn)
j_outer, j_inner = s[C].split(C.op.axis[1], factor=bn)
k_outer, k_inner = s[C].split(k, factor=bn)

# 调整循环顺序,让内层访问连续
s[C].reorder(i_outer, j_outer, k_outer, i_inner, j_inner, k_inner)

# 向量化最内层
s[C].vectorize(j_inner)

# 多线程并行
s[C].parallel(i_outer)

同一份计算定义,换一套调度就能适配 CPU、GPU 或加速器。这种「一份计算、多份调度」的模式,是 TVM 能覆盖大量后端的关键。

2.2 TVM 的编译流水线

TVM 的完整链路大致分为四层 IR:

层级名称作用
高层Relay / Relax计算图表示,承载图级优化
张量级TIR(Tensor IR)显式循环与调度,承载算子级优化
底层TIR 降级后的低级 IR接近机器语义
目标LLVM IR / CUDA C / 汇编交给后端编译器

图级优化包括算子融合、常量折叠、死代码消除、布局变换等。算子级优化则依赖调度原语(split、reorder、tile、vectorize、parallel、bind)以及自动调优。

2.3 AutoTVM 与 Ansor:自动搜索调度

手写调度门槛高、调参耗时。TVM 提供了自动调优:

  • AutoTVM:基于模板的参数搜索,人工给出调度骨架,工具搜索分块大小、展开因子等参数。
  • Ansor(Auto-scheduler):无需模板,自动生成调度空间并搜索,适合复杂算子。
from tvm import autotvm

@autotvm.template("tutorial.matmul")
def matmul_template(n, bn):
    A = te.placeholder((n, n), name="A")
    B = te.placeholder((n, n), name="B")
    k = te.reduce_axis((0, n), name="k")
    C = te.compute((n, n), lambda i, j: te.sum(A[i, k] * B[k, j], axis=k))

    s = te.create_schedule(C.op)
    cfg = autotvm.get_config()
    cfg.define_split("tile_i", C.op.axis[0], num_outputs=2)
    cfg.define_split("tile_j", C.op.axis[1], num_outputs=2)
    # ... 应用配置并返回
    return s, [A, B, C]

调优结果会存成日志,编译时直接查表,避免线上重复搜索。

2.4 TVM 的适用场景

TVM 的优势在于端到端:从模型导入(ONNX、PyTorch)到多后端代码生成,一条链路打通。它特别适合:

  • 需要把模型部署到非主流硬件(自研 NPU、嵌入式芯片);
  • 需要针对特定 shape 做极致性能调优;
  • 需要统一管理多后端的推理服务。

但 TVM 也有短板:自定义算子接入需要理解 TIR 与调度体系,学习曲线陡峭;图级 IR 与算子级 IR 之间的边界相对固定,跨层优化不够灵活。

3. MLIR:可复用的编译器基础设施

3.1 MLIR 要解决什么问题

MLIR(Multi-Level IR)由 LLVM 社区发起,出发点不是「做一个 AI 编译器」,而是「做一套可扩展的编译器基础设施」。

它观察到:不同领域(机器学习、量子计算、硬件描述、数据库查询)都需要 IR,但各自需要的抽象层级不同。如果每个领域都从零造 IR,会重复实现大量通用能力(类型系统、Pass 管理、模式匹配、诊断)。

MLIR 的答案是:提供一套 IR 元框架,让各领域定义自己的方言(Dialect)。

3.2 核心概念:Dialect、Operation、Pass

MLIR 中一切皆 Operation。一个 Operation 由名字、操作数、结果、属性、区域(Region)组成。方言是一组相关 Operation 的集合。

// 一个简单的 MLIR 片段
func.func @matmul(%A: tensor<128x256xf32>, %B: tensor<256x64xf32>) -> tensor<128x64xf32> {
  %C = linalg.matmul ins(%A, %B : tensor<128x256xf32>, tensor<256x64xf32>)
                     outs(%init : tensor<128x64xf32>) -> tensor<128x64xf32>
  return %C : tensor<128x64xf32>
}

这里出现了 func、linalg 等方言。MLIR 生态中常见的方言包括:

方言抽象层级典型用途
func函数函数定义与调用
arith标量运算加减乘除、比较
tensor / memref张量 / 内存值语义与缓冲语义
linalg结构化线性代数矩阵乘、卷积等
scf结构化控制流for / while / if
affine仿射循环多面体优化
gpuGPU 抽象线程块、共享内存
llvmLLVM IR对接 LLVM 后端

Pass 是 MLIR 的优化单元,可以按需组合:

void buildPipeline(mlir::PassManager &pm) {
  pm.addPass(mlir::createCanonicalizerPass());
  pm.addPass(mlir::createCSEPass());
  pm.addNestedPass<mlir::func::FuncOp>(mlir::createLinalgElementwiseOpFusionPass());
  pm.addPass(mlir::createConvertLinalgToLoopsPass());
  pm.addPass(mlir::createConvertSCFToCFPass());
}

3.3 渐进式降级

MLIR 最核心的思想是渐进式降级(progressive lowering):从高层方言逐步转换到低层方言,每一层只做该层该做的事。

高层方言
torch / tf / stablehlo

结构化方言
linalg / tensor

循环方言
scf / affine

底层方言
memref / vector / gpu

LLVM 方言

LLVM IR / 机器码

这种分层让优化可以「在合适的抽象层级做合适的事」:算子融合在 linalg 层做,循环变换在 affine 层做,寄存器分配交给 LLVM。相比一次性从高层 IR 直接降到机器码,可维护性和可扩展性都更好。

3.4 MLIR 的适用场景

MLIR 适合作为编译器框架使用:

  • 构建领域专用编译器(AI、DSL、硬件描述);
  • 在已有编译器中引入多层 IR,做跨层优化;
  • 复用社区成熟的方言与 Pass,减少重复造轮子。

代价是:MLIR 本身不提供端到端的模型导入与部署能力,你需要自己搭建从框架到 MLIR、再从 MLIR 到目标硬件的完整链路。

4. TVM 与 MLIR 的对比

两者经常被拿来比较,但严格说它们不在同一层面:TVM 是「AI 编译器产品」,MLIR 是「编译器基础设施」。

维度TVMMLIR
定位端到端深度学习编译栈可扩展编译器基础设施
核心抽象Relay/Relax + TIRDialect + Operation
计算与调度显式分离,调度原语丰富通过方言与 Pass 表达
自动调优AutoTVM / Ansor 成熟需自行集成或借助外部工具
模型导入内置 ONNX / PyTorch 前端需自行实现或借助 torch-mlir
多后端内置 CPU/GPU/加速器后端依赖方言与下游后端
学习曲线调度体系较陡概念多但分层清晰
扩展方式新增调度与后端新增方言与 Pass

一个常见的误解是「MLIR 会取代 TVM」。实际上两者可以互补:TVM 的 Relax 已经引入 MLIR 风格的架构,部分后端也借助 MLIR 生态;而基于 MLIR 的 torch-mlir、IREE 等项目,则在 MLIR 之上重建了端到端能力。

选择建议:

  • 想快速把模型部署到多种硬件,优先考虑 TVM 或 IREE;
  • 想自研编译器、需要深度定制 IR 与优化流程,优先考虑 MLIR;
  • 团队已有 LLVM 积累,MLIR 的迁移成本更低。

5. 实战:一个最小可运行的 TVM 示例

下面用 TVM 编译一个向量加法,并验证结果。示例基于 TVM 0.15 及以上版本。

import numpy as np
import tvm
from tvm import te

n = 1024
A = te.placeholder((n,), name="A", dtype="float32")
B = te.placeholder((n,), name="B", dtype="float32")
C = te.compute((n,), lambda i: A[i] + B[i], name="C")

s = te.create_schedule(C.op)
# 按 64 分块并向量化
i_outer, i_inner = s[C].split(C.op.axis[0], factor=64)
s[C].vectorize(i_inner)

# 编译为 LLVM 后端
target = tvm.target.Target("llvm")
fadd = tvm.build(s, [A, B, C], target=target, name="vec_add")

# 在 CPU 上执行
ctx = tvm.cpu(0)
a_np = np.random.rand(n).astype("float32")
b_np = np.random.rand(n).astype("float32")
a_tvm = tvm.nd.array(a_np, ctx)
b_tvm = tvm.nd.array(b_np, ctx)
c_tvm = tvm.nd.array(np.zeros(n, dtype="float32"), ctx)

fadd(a_tvm, b_tvm, c_tvm)

np.testing.assert_allclose(c_tvm.numpy(), a_np + b_np, rtol=1e-5)
print("TVM 向量加法结果正确")

这段代码展示了 TVM 的完整闭环:声明计算 → 编写调度 → 指定目标 → 编译执行 → 数值验证。把 target 换成 cuda,同一份计算就能生成 GPU 代码(需相应调整调度)。

6. 实战:一个最小可运行的 MLIR 示例

MLIR 的入门通常从 mlir-opt 开始。安装 LLVM/MLIR 后,可以写一个 .mlir 文件并用 Pass 管道处理。

// add.mlir
func.func @add(%a: tensor<4xf32>, %b: tensor<4xf32>) -> tensor<4xf32> {
  %c = arith.addf %a, %b : tensor<4xf32>
  return %c : tensor<4xf32>
}

用 mlir-opt 做规范化与 CSE:

mlir-opt add.mlir \
  --canonicalize \
  --cse \
  -o add_opt.mlir

如果要观察降级过程,可以把 linalg 逐步降到循环:

mlir-opt matmul.mlir \
  --convert-linalg-to-loops \
  --convert-scf-to-cf \
  --convert-func-to-llvm \
  --reconcile-unrealized-casts \
  -o matmul_lowered.mlir

每一步都会输出不同抽象层级的 IR,这正是 MLIR 渐进式降级思想的可视化体现。

如果需要自己写 Pass,可以用 C++ 或 Python 绑定:

#include "mlir/Pass/Pass.h"
#include "mlir/IR/PatternMatch.h"

struct MyPass : public mlir::PassWrapper<MyPass, mlir::OperationPass<mlir::func::FuncOp>> {
  void runOnOperation() override {
    auto func = getOperation();
    // 遍历 Operation,做你的变换
    func.walk([](mlir::Operation *op) {
      // ...
    });
  }
};

7. 选型与学习路径

给不同背景的读者一些具体建议:

如果你是算法工程师,想把模型部署到新硬件:从 TVM 的 Relax 或 IREE 入手,先跑通官方教程里的端到端示例,再逐步深入调度与后端。

如果你是编译器工程师,想自研 AI 编译器:从 MLIR 的 Toy Tutorial 开始,理解 Dialect 定义、Pass 管理与降级流程,再研究 torch-mlir、IREE 等真实项目。

如果你在做推理框架:两者都值得了解。TVM 的图优化与自动调优经验可以直接借鉴;MLIR 的分层 IR 设计则适合作为长期架构参考。

学习资源方面,TVM 官方文档与「TVM 深度学习编译器」系列文章质量较高;MLIR 官方文档的 Toy Tutorial 是最友好的入门材料,配合 LLVM 社区的演讲视频效果更好。

8. 总结

AI 编译器的本质,是在「模型语义」与「硬件执行」之间建立可自动化的映射关系。TVM 用计算与调度分离、自动调优和端到端链路,解决了「一份模型跑多硬件」的工程问题;MLIR 用方言体系和渐进式降级,解决了「编译器如何可扩展、可复用」的架构问题。

两者不是替代关系,而是不同层次的答案。理解 TVM,你会知道一个可用的 AI 编译器长什么样;理解 MLIR,你会知道如何把它拆解、重组、扩展到新的领域。把这两条线都走一遍,再回头看任何一个推理框架或芯片工具链,都会清晰很多。

转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/Maxwell_Newton/article/details/169466172

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--