设计与架构
本文档适用于想要了解 TVM 架构和/或积极开发项目的开发者。本文档组织结构如下:
- 编译流程示例 概述了 TVM 将模型的高级描述转换为可部署模块所采取的步骤。
- 逻辑架构组件 部分描述了逻辑组件。后面的部分是针对每个逻辑组件的具体指南,按组件的名称编排。
- 设备/ Target 交互 文档描述了 TVM 如何与所有受支持的物理设备,以及代码生成的 target 进行交互。
- 查看 开发者操作指南 获取实用开发技巧。
本指南提供了架构的一些补充视图。首先研究端到端的编译流程,并讨论关键的数据结构和转换。这种基于 runtime 的视图侧重于运行编译器时每个组件的交互。接下来研究代码库的逻辑模块及其关系。这部分提供了设计的静态总体视图。
编译流程示例
本指南研究编译器中的编译流程示例,下图显示了流程。在高层次,它包含以下步骤:
- 导入:前端组件将模型引入到 IRModule 中,它包含了内部表示模型的函数集合。
- 转换:编译器将 IRModule 转换为功能与之等效或近似等效(例如在量化的情况下)的 IRModule。许多转换与 target(后端)无关,并且允许 target 配置转换 pipeline。
- Target 转换:编译器将 IRModule 转换(codegen)为指定 target 的可执行格式。target 的转换结果被封装为 runtime.Module,可以在 runtime 环境中导出、加载和执行。
- Runtime 执行:用户加载 runtime.Module,并在支持的 runtime 环境中运行编译好的函数。
关键数据结构
设计和理解复杂系统的最佳方法之一,就是识别关键数据结构和操作(转换)这些数据结构的 API。识别了关键数据结构后,就可以将系统分解为逻辑组件,这些逻辑组件定义了关键数据结构的集合,或是数据结构之间的转换。
IRModule 是整个堆栈中使用的主要数据结构。一个 IRModule(intermediate representation module)包含一组函数。目前支持两种主要的功能变体(variant):
- relay::Function 是一种高级功能程序表示。一个 relay.Function 通常对应一个端到端的模型。可将 relay.Function 视为额外支持控制流、递归和复杂数据结构的计算图。
- tir::PrimFunc 是一种底层程序表示,包含循环嵌套选择、多维加载/存储、线程和向量/张量指令的元素。通常用于表示算子程序,这个程序在模型中执行一个(可融合的)层。 在编译期间,Relay 函数可降级为多个 tir::PrimFunc 函数和一个调用这些 tir::PrimFunc 函数的顶层函数。
转换
前面介绍了关键数据结构,接下来讲转换。转换的目的有:
- 优化:将程序转换为等效,甚至更优的版本。
- 降级:将程序转换为更接近 target 的较低级别表示。 relay/transform 包含一组优化模型的 pass。优化包括常见的程序优化(例如常量折叠和死码消除),以 及特定于张量计算的 pass(例如布局转换和 scale 因子折叠)。
在 Relay 优化流程的后期,运行 pass(FuseOps),将端到端函数(例如 MobileNet)分解为子功能(例如 conv2d-relu)段。这个过程帮助将原始问题分为两个子问题:
- 所有子函数的编译和优化。
- 整体执行结构:对生成的子函数进行一系列调用,执行整个模型。 使用下层 tir 阶段来编译和优化每个子函数。对于特定的 targets,也可以直接进入 target 转换阶段,使用外部代码生成器。
有几种不同的方法(在 relay/backend 目录)来处理对整体执行问题的调用。对于具有已知 shape 且没有控制流的简单模型,可以降级为图执行器,这个图执行器存储计算图中的执行结构。我们还支持用于动态执行的虚拟机后端。
最后,我们计划支持 ahead-of-time 编译,它将高级执行结构编译成可执行和生成的原始函数。所有这些执行模式都被统一的 runtime.Module 接口封装,指南的后半部分将进行讨论。
tir/transform 包含 TIR 级函数的转换过程。许多 tir passes 的目的是降级。例如,有些 pass 将多维访问展平为一维指针访问,将内联函数扩展至特定于 target 的函数,以及将函数入口修饰为满足 runtime 调用约定。当然,也有一些 pass 的目的是为了优化,例如访问索引简化和死码消除。
LLVM、CUDA C 和其他 target 编译器都可以在 target 阶段处理许多底层优化。因此,我们将底层优化(如寄存器分配)留给下游编译器,只关注它们未涵盖的优化。