2、AI软件栈全景:从应用层、框架层、编译器层、运行时层到硬件层的完整堆栈
聊AI编译器,不能只盯着编译器本身。
你得先看清楚整个AI软件栈长什么样。说白了,AI编译器只是中间的一环,它上下都有邻居。不了解这些邻居,你就很难理解编译器到底在解决什么问题。
我刚开始接触AI系统时,也犯过这个错误——一头扎进编译器代码里,结果发现很多设计决策,其实是被上层框架和下层硬件逼出来的。嗯,咱们先退一步,看看全景。
2.1 五层堆栈:一张图说清楚
我个人习惯把AI软件栈分成五层。从上到下依次是:
- 应用层:你写的业务代码,比如图像识别、自然语言处理。
- 框架层:PyTorch、TensorFlow、MindSpore 这些。
- 编译器层:这就是咱们课程的主角,比如 TVM、MLIR、XLA。
- 运行时层:负责调度、内存管理、设备驱动交互。
- 硬件层:GPU、NPU、CPU、FPGA 这些物理芯片。
你想想看,一个模型从你写完代码,到真正在芯片上跑起来,中间要经过多少道工序?每一层都有自己的职责,也都有自己的坑。
核心观点:AI编译器不是孤立存在的。它的输入来自框架层,输出要喂给运行时层和硬件层。理解这个上下游关系,是学好编译器的第一步。
下面这张SVG图,是我手绘的五层堆栈结构。你可以把它当作整个课程的地图。
2.2 每一层都在干什么?
2.2.1 应用层:离用户最近
这一层就是你写的Python脚本,或者调用的API。比如你用PyTorch写了个ResNet的训练脚本,或者用Transformers库加载了一个GPT模型。应用层只关心「我要做什么」,不关心「底层怎么实现」。
我记得有一次帮业务团队排查性能问题,他们抱怨模型推理太慢。结果发现是应用层每次推理都重新加载了一遍模型权重。嗯,这其实不是编译器的问题,是应用层没做好缓存。所以你看,有时候性能瓶颈不在底层,就在最上面。
2.2.2 框架层:承上启下
框架层是AI工程师最熟悉的。PyTorch的torch.nn.Module,TensorFlow的tf.keras.Model,都是这一层的东西。
框架层主要做三件事:
- 构建计算图:把用户写的网络结构转成一张有向无环图(DAG)。
- 自动微分:为反向传播计算梯度。
- 调用后端:把算子下发给编译器或运行时。
这里有个关键点:框架层产出的计算图,就是编译器层的输入。所以编译器工程师必须理解框架层产出的图结构是什么样的。我在项目中遇到过框架层产出的图里有大量reshape和transpose操作,这些操作在编译器层面是可以融合优化的。
小技巧:如果你在用PyTorch,可以用 torch.jit.trace 或 torch.fx 导出计算图,看看框架层到底输出了什么。这对理解编译器输入非常有帮助。
2.2.3 编译器层:核心战场
这就是咱们课程的主角。编译器层接收框架层产出的计算图,然后做一系列优化,最终生成硬件可执行的代码。
编译器层内部又可以细分为:
- 前端:解析计算图,做图级别的优化(算子融合、常量折叠、死代码消除)。
- 中端:做与硬件无关的优化,比如自动微分、循环优化、内存规划。
- 后端:针对特定硬件做代码生成,包括指令选择、寄存器分配、调度。
我曾经在一个项目里,发现同样的模型在GPU上跑得飞快,在NPU上却慢得离谱。后来定位到是编译器后端没有针对NPU的矩阵乘法指令做优化。说白了,编译器后端的质量,直接决定了硬件能不能发挥出真实性能。
2.2.4 运行时层:默默无闻的调度者
运行时层经常被忽视,但它其实非常重要。编译器生成的代码,最终要由运行时层来加载和执行。
运行时层负责:
- 设备管理:初始化GPU/NPU,分配显存。
- 算子调度:把编译好的算子按顺序提交到硬件上执行。
- 内存管理:为张量分配和回收内存,做内存复用。
- 异步执行:管理CUDA stream或NPU的硬件队列。
我建议你在调试性能问题时,不要只盯着编译器。有时候运行时层的内存分配策略不合理,会导致大量碎片,进而触发显存交换,性能直接腰斩。
避坑指南:我曾经遇到过一个问题——编译器优化后的代码在单次推理时很快,但并发推理时性能急剧下降。后来发现是运行时层的线程池配置不当,导致多个推理请求争抢计算资源。嗯,运行时层的并发模型,是个容易被忽略的坑。
2.2.5 硬件层:最终的执行者
硬件层是金字塔的底座。没有硬件,前面四层都是空中楼阁。
不同的硬件有不同的特性:
| 硬件类型 | 典型代表 | 核心特性 | 编译器关注点 |
|---|---|---|---|
| GPU | NVIDIA A100, H100 | 大量CUDA核心,高并行度 | 线程块划分、共享内存使用 |
| NPU | 华为昇腾, 寒武纪 | 专用矩阵计算单元 | 矩阵乘法指令映射、数据排布 |
| CPU | Intel Xeon, ARM | 通用计算,延迟敏感 | 向量化指令、缓存友好性 |
| FPGA | Xilinx, Intel FPGA | 可编程逻辑,低延迟 | 硬件描述语言生成、流水线设计 |
你想想看,编译器后端要为每种硬件生成不同的代码。这就是为什么AI编译器很难做——它要同时理解多种硬件的指令集架构和微架构特性。
2.3 层与层之间的接口
理解了每一层之后,咱们再看看层与层之间是怎么对接的。这些接口,其实就是编译器工程师每天打交道的「协议」。
- 框架层 → 编译器层:计算图(Graph IR)。常见格式有ONNX、TorchScript、TensorFlow SavedModel。
- 编译器层 → 运行时层:可执行文件或二进制算子库。比如TVM生成的.so文件,或者XLA生成的HLO模块。
- 运行时层 → 硬件层:硬件驱动API。比如CUDA Driver API、ROCm API、华为CANN API。
我在做TVM部署时,经常需要检查ONNX模型导出是否正确。有时候框架层和编译器层对某个算子的定义不一致,就会导致编译出来的结果不对。嗯,这种问题排查起来特别费时间。
2.4 为什么编译器工程师需要理解全栈?
你可能觉得,我只要把编译器这层搞懂就行了,其他层了解个大概就够了吧?
说实话,不够。
举个真实的例子。有一次我在优化一个BERT模型的推理性能,编译器层面做了算子融合、内存规划,看起来已经很好了。但实际跑起来,性能就是上不去。后来我一路排查到运行时层,发现是内存分配器每次都为输出张量重新分配显存,导致大量碎片。我改成了预分配策略,性能直接提升了30%。
你看,如果我只懂编译器,不懂运行时层的内存管理,这个问题根本找不到根因。
我的建议:作为AI编译器工程师,你的知识面至少要覆盖框架层到运行时层。硬件层可以不用精通,但至少要理解不同硬件的计算模型和内存层次结构。这样你在做编译器优化时,才能做出真正有效的决策。
好了,这一章咱们把AI软件栈的全景梳理了一遍。从应用层到硬件层,每一层都有自己的职责,也都有自己的坑。编译器层处在中间,承上启下,既要理解框架层的计算图,又要生成硬件能跑的代码。
下一章开始,咱们会深入编译器层的内部,看看它到底是怎么工作的。不过在那之前,我建议你先把自己手头的项目对号入座——看看你平时工作主要在哪一层,上下游是谁。这样学起来会更有感觉。
公众号:蓝海资料掘金营,微信deep3321