计算图与IR:计算图基础、中间表示(IR)概念、主流IR格式介绍
好,咱们今天聊聊计算图和IR。这两个概念,说白了就是模型编译器的“骨架”和“语言”。
我刚开始接触编译器时,也觉得这些概念挺虚的。直到有一次,我手写了一个简单的算子融合,结果跑出来结果不对。debug了整整两天,最后发现是计算图里一个节点的依赖关系搞错了。嗯,从那以后,我再也不敢小看计算图了。
计算图基础
计算图是什么?你想想看,一个深度学习模型,无非就是一堆算子的组合。卷积、ReLU、BatchNorm、全连接……这些算子按照一定的顺序连接起来,就形成了一张图。
这张图里,节点是算子,边是数据流向。就这么简单。
核心要点:计算图是一种有向无环图(DAG),它完整描述了模型的计算逻辑。
我个人习惯把计算图分成两种:
- 静态图:先定义好图结构,再执行。TensorFlow 1.x 就是典型代表。好处是编译优化空间大,坏处是调试麻烦。
- 动态图:边定义边执行,PyTorch 就是典型。灵活,好调试,但优化空间相对小一些。
我在项目中遇到过一个问题:一个用PyTorch写的模型,直接转成静态图后,性能反而下降了。为什么?因为动态图里的一些动态控制流,在静态图里被“拍平”了,导致计算图变得非常臃肿。后来我们手动做了一些图优化,才把性能提上来。
中间表示(IR)概念
IR,全称是Intermediate Representation,中间表示。说白了,就是编译器内部用来表示程序的一种“语言”。
为什么需要IR?你想想看,前端有各种框架(PyTorch、TensorFlow、ONNX),后端有各种硬件(GPU、NPU、CPU)。如果每个前端都要直接对接每个后端,那工作量就爆炸了。
IR就是中间的“翻译官”。前端把模型转成IR,后端从IR生成目标代码。这样,前端只需要对接IR,后端也只需要对接IR,解耦了。
我的经验:IR的设计决定了编译器的上限。一个好的IR,既要能表达丰富的语义,又要方便做各种优化。我曾经参与过一个项目,IR设计得太“高层”,导致很多底层优化做不了;后来改成多层IR,问题就解决了。
IR通常分为几个层次:
- 高层IR:接近原始模型,保留张量、算子等语义。比如ONNX。
- 中层IR:做一些初步优化,比如常量折叠、死代码消除。
- 低层IR:接近硬件,考虑内存布局、指令选择等。比如LLVM IR。
主流IR格式介绍
现在业界主流的IR格式,我挑几个重点说说。
| IR名称 | 所属项目 | 特点 | 适用场景 |
|---|---|---|---|
| ONNX | ONNX | 开放、跨框架、算子标准 | 模型交换、部署 |
| MLIR | LLVM | 多层IR、可扩展、模块化 | 编译器基础设施 |
| TorchScript | PyTorch | 动态图转静态图、Python子集 | PyTorch模型部署 |
| XLA HLO | TensorFlow | 高层优化、自动融合 | TensorFlow性能优化 |
ONNX:我最常用的IR。它定义了一套标准的算子集,不同框架的模型都可以转成ONNX。但要注意,ONNX的算子集更新比较慢,有些新算子可能不支持。我曾经踩过一个坑:一个用了F.grid_sample的模型,转ONNX时死活过不去,最后只能手写一个自定义算子。
MLIR:这是LLVM社区搞的“下一代编译器基础设施”。它的核心思想是“多层IR”,你可以定义自己的IR方言,然后在不同层次之间做转换。我个人觉得MLIR是未来的方向,但学习曲线比较陡。
TorchScript:PyTorch自家的IR。它把动态图“录制”成静态图,然后做优化。好处是和PyTorch无缝集成,坏处是只支持PyTorch。
XLA HLO:TensorFlow的IR,专门做高性能计算。它的自动融合做得很好,能把很多小算子融合成一个大的kernel,减少内存访问。
避坑指南:我曾经在一个项目里,把ONNX模型直接喂给一个只支持MLIR的编译器,结果各种报错。后来才发现,ONNX和MLIR的算子语义不完全一致,需要做一层“方言转换”。所以,选IR时一定要考虑清楚你的上下游工具链。
知识体系总览
下面这张图,是我自己总结的计算图与IR的知识体系。你可以把它当作一个“地图”,方便后续学习时定位。
嗯,这张图把今天讲的内容串起来了。计算图是基础,IR是桥梁,主流IR格式是工具。三者缺一不可。
最后说一句:别怕这些概念。你只要动手写一个简单的图优化pass,就全明白了。我当年也是这么过来的。