2. AI编译器核心概念:计算图、中间表示(IR)、算子、张量、数据流图
好,咱们今天聊聊AI编译器里最基础的那几个概念。说实话,我刚入行那会儿,也被这些术语绕得晕头转向。什么计算图、IR、算子、张量……听着都挺唬人。但说白了,它们就是编译器用来理解和管理深度学习模型的一套“语言”和“工具”。
你想想看,一个深度学习模型,本质上就是一堆数学运算的组合。但怎么让计算机高效地执行这堆运算?这就需要编译器来“翻译”和“优化”。而这些核心概念,就是翻译过程中必不可少的“单词”和“语法”。
2.1 计算图:模型的骨架
我个人习惯把计算图看作是模型的“骨架”或“蓝图”。它描述的是数据从输入到输出的整个流动过程,以及中间经历了哪些运算。
举个例子,一个简单的加法模型 z = x + y,它的计算图就是两个输入节点 x 和 y,指向一个加法节点 Add,然后输出 z。就这么简单。
但在实际项目中,模型动辄几百层,计算图就变得非常庞大。我记得有一次调试一个BERT模型,光是把计算图打印出来,就有几万行。那时候我才真正体会到,没有计算图这种抽象,你根本没法下手去分析和优化。
计算图的好处在于,它把“计算”和“控制流”分开了。编译器可以静态地分析整个图,找出哪些地方可以合并、哪些地方可以并行、哪些地方可以消除。这比一行一行地执行代码要高效得多。
2.2 中间表示(IR):编译器的“通用语言”
如果说计算图是骨架,那中间表示(IR)就是编译器内部的“通用语言”。不同的深度学习框架(比如PyTorch、TensorFlow)都有自己的前端表示,但编译器不能为每个框架都写一套优化逻辑。这时候,IR就派上用场了。
IR的作用,就是把各种前端模型统一成一种编译器能理解的形式。这样,所有的优化pass(优化过程)都只需要针对IR来写,一次编写,到处生效。
常见的IR有几种层次:
- 图级别IR: 比如TensorFlow的GraphDef、PyTorch的TorchScript。它们保留了计算图的整体结构,适合做算子融合、内存规划等宏观优化。
- 线性IR: 比如MLIR的Dialect、LLVM IR。它们更接近底层,把计算图展开成一系列指令序列,适合做循环优化、向量化等微观优化。
嗯,这里要注意,IR的设计直接决定了编译器的表达能力和优化上限。我见过一些团队自己造IR,结果因为表达能力不够,很多优化做不了,最后不得不推倒重来。所以,选IR或者设计IR时,一定要想清楚你的优化目标是什么。
2.3 算子:计算的基本单元
算子(Operator),也叫操作符或内核(Kernel),是计算图中最基本的执行单元。一个算子代表一个具体的数学运算,比如卷积(Conv2D)、矩阵乘法(MatMul)、激活函数(ReLU)等等。
每个算子都有其特定的计算逻辑和输入输出约束。比如,卷积算子要求输入是一个4D张量(NCHW或NHWC格式),卷积核是另一个4D张量,输出也是一个4D张量。
在AI编译器里,算子通常分为两类:
- 计算密集型算子: 比如卷积、矩阵乘法。这类算子计算量大,对硬件利用率要求高,通常需要手写汇编或使用库(如cuBLAS)来优化。
- 访存密集型算子: 比如ReLU、加法、Reshape。这类算子计算量小,但数据搬运多,优化重点在于减少内存访问次数和提升缓存命中率。
我曾经踩过一个坑:在优化一个模型时,我把所有精力都花在优化卷积上,结果发现性能瓶颈竟然是一个不起眼的Reshape算子。因为Reshape虽然计算量小,但它会导致内存的非连续访问,拖慢了整个流水线。所以,优化算子时,千万别只盯着计算量大的,访存密集型的同样不能忽视。
2.4 张量:数据的容器
张量(Tensor),你可以把它理解成一个多维数组。它是AI编译器里数据的基本载体。标量是0维张量,向量是1维张量,矩阵是2维张量,而图像数据通常是3维或4维张量(比如批次、通道、高度、宽度)。
张量有几个关键属性:
| 属性 | 说明 | 示例 |
|---|---|---|
| 形状(Shape) | 每个维度的大小 | [1, 3, 224, 224] |
| 数据类型(Dtype) | 每个元素的类型 | float32, int8, bfloat16 |
| 步长(Stride) | 每个维度相邻元素在内存中的偏移量 | [50176, 50176, 224, 1] |
| 布局(Layout) | 维度在内存中的排列顺序 | NCHW, NHWC |
在编译器里,张量的布局是个大问题。不同的硬件对布局的偏好不同。比如,NVIDIA GPU上卷积算子通常用NCHW布局,而CPU上很多算子偏好NHWC。编译器需要根据目标硬件,自动插入布局转换(Layout Transform)操作。我建议你在设计编译器时,把布局转换作为一个显式的IR操作,而不是隐式地处理,否则很容易出现bug。
2.5 数据流图:计算图的“运行时”视角
数据流图(Data Flow Graph)和计算图很像,但侧重点不同。计算图更关注“计算”本身,而数据流图更关注“数据”的流动和依赖关系。
在数据流图中,节点仍然是算子,但边代表的是数据的生产者-消费者关系。一个算子的输出,会成为另一个算子的输入。这种依赖关系决定了算子的执行顺序。
数据流图的一个关键特性是:只要一个算子的所有输入都准备好了,它就可以立即执行,而不需要等待前面的算子全部执行完。这为并行执行提供了可能。
举个例子,假设有A、B、C三个算子,A的输出给B和C。在数据流图中,B和C可以并行执行,因为它们都只依赖A。而在传统的控制流图中,你可能会顺序执行A、B、C,浪费了并行机会。
好了,这几个概念讲完了。它们之间是环环相扣的:计算图是骨架,IR是语言,算子是零件,张量是材料,数据流图是执行蓝图。理解它们,你就掌握了AI编译器的“内功心法”。