Pass Infrastructure
Relay 和 TVM IR 都 包含了一系列优化 pass,它们可提高模型的性能指标,如平均推理、内存占用或特定设备的功耗。有一套标准优化以及机器学习特定的优化,包括常量折叠、死代码消除、算子布局更改、算子融合、buffer 处理和循环转换等。每个 pass 都被构造为 ir-to-ir 转换,它们用遍历期间和/或遍历之前收集的分析结果进行转换。
TVM 的快速发展催生更加系统化和有效的方式,来管理这些 pass。此外,管理 TVM 堆栈不同层(例如 Relay 和 tir)pass 的通用框架极大便利了开发者,使他们可以快速原型化并将实现的 pass 插入至系统。
该文档描述了一个 infra 的设计——利用生产编译器来管理优化 pass 的方式,以及用于构建层的现代深度学习框架的风格。
例如,许多现有的生产编译器,如 GCC 和 LLVM,都使用 pass 管理器来有效地管理 pass 的执行。最初管理 pass 很简单,因为 pass 的数量很少,但成熟的编译器包含数百个单独的 pass。外部用户通常希望正确调度自定义 pass,而无需修改单个手工制作的 pass 顺序。
类似地,现代深度学习框架,如 Pytorch 和 MXNet Gluon,也倾向于分别通过 Sequential 和 Block 实现 pass 模式的层构建方案。得到了这样的结构后,这些现代框架能够方便地将模块/层添加到容器中,并轻松地构建神经网络。
Relay pass infra 的设计,很大程度上是受到 LLVM 中使用的分层 pass 管理器,以及流行的深度学习框架中使用的块式容器的启发。Pass infra 的主要目标:
- 实现更好的优化程序编排。用户可以灵活地定制和构建自己的优化 pipeline。
- 提供一种对用户更友好的方式来调试优化 pass。
- 减轻开发者手动解决 pass 之间的依赖关系的工作量。
- 为开发者简化新 pass 的实现。允许用户在 Python 中实现 pass,并让 pass 基础架构操纵其执行。
设计
聚焦用户扩展的简易性,使用户可以快速添加新的 pass,且向后兼容。设计包含后端和前端。前者(即后端)实现了 pass infra 的主要逻辑。后者(即前端)为用户提供简单的 API 进行交互,允许用户快速创建自己的优化 pipeline。
C++ 后端
PassInfo
对象包含一个 pass 所需的基本信息,其中 name
是 pass 名称,opt_level
表示将在哪个优化级别启用 pass,required
表示执行某个 pass 所需的 pass(有关详细信息,参阅 include/tvm/ir/transform.h)。例如,在注册 pass 期间(稍后介绍),pass 开发者可以指定 pass 的名称、执行的优化级别和/或所需的 pass。opt_level
可帮助 pass infra 识别在用户提供的优化级别下运行时,是否需要执行某个 pass。pass infra 可以用 required
字段来解决 pass 依赖。
class PassInfoNode : public Object {
String name;
int opt_level;
Array<String> required;
};
PassContext
PassContext
具备优化 pass 的有用信息。例如,它包含错误报告系统,可以提供优化失败原因的诊断。 PassContext
用来替换旧的 BuildConfig
,BuildConfig
用于帮助用户配置编译选项,包括优化级别和必需/禁用的 pass 等。例如,有一个配置,在 opt_level=3
执行所有 pass ,同时用 PassContext
提供的 disabled_pass=xx
来禁用一些 pass 。在 opt_level=3
处全局化所有 pass ,并排除禁用 pass 列表中的所有 pass。PassContext
还提供了一种检测所有 pass 的方法。参阅 Pass Instrument 这一节。
用户可以用这个类编写 Python with
语法,从而在一定的配置下进行优化。此外,用户可以通过 PassContext::Current()
以线程安全的方式获取某个程序范围内可用的上下文,因为线程本地存储的 PassContextThreadLocalStore
用于保存创建的 pass 上下文对象。稍后用示例来展示如何用 C++ 和 Python API 来通过 pass 上下文创建编译 pipeline。
class PassContextNode : public Object {
public:
int opt_level{2};
tvm::Array<tvm::Expr> required_pass;
tvm::Array<tvm::Expr> disabled_pass;
mutable Optional<DiagnosticContext> diag_ctx;
Map<String, ObjectRef> config;
Array<instrument::PassInstrument> instruments;
};
class PassContext : public NodeRef {
public:
TVM_DLL static PassContext Create();
TVM_DLL static PassContext Current();
TVM_DLL void InstrumentEnterPassContext();
TVM_DLL void InstrumentExitPassContext();
TVM_DLL bool InstrumentBeforePass(const IRModule& mod, const PassInfo& info) const;
TVM_DLL void InstrumentAfterPass(const IRModule& mod, const PassInfo& info) const;
/* 省略其他字段。 */
private:
// pass 上下文范围的入口。
TVM_DLL void EnterWithScope();
// pass 上下文范围的退 出。
TVM_DLL void ExitWithScope();
// 获取 Python `with` 语法的类。
friend class tvm::With<PassContext>;
};
struct PassContextThreadLocalEntry {
/*! \摘要:默认 pass 上下文。 */
PassContext default_context;
/*! \摘要:当前 pass 上下文 */
std::stack<PassContext> context_stack;
PassContextThreadLocalEntry() {
default_context = PassContext(make_node<PassContextNode>());
}
};
/*! 保存 pass 上下文的线程本地存储 */
typedef dmlc::ThreadLocalStore<PassContextThreadLocalEntry>
PassContextThreadLocalStore;
Pass 构造
pass infra 以分层的方式设计,可以在不同粒度的 Relay/tir 程序下工作。我们引入一个纯虚类 PassNode
,作为不同优化 pass 的基础。这个类包含几个必须由子类在模块、函数或 pass 序列级别实现的虚拟方法。
class PassNode : Object {
virtual PassInfo Info() const = 0;
virtual Module operator()(const IRModule& mod
const PassContext& pass_ctx) const = 0;
};
仿函数展示了 pass 是如何实现的,即它始终在特定上下文对 IRModule
起作用。所有 pass 都以 Module
到 Module
的方式设计。因此,由 pass infra 管理的优化将始终更新整个模块。
已经创建了几个子类来实现不同类型的优化 pass,例如,函数级 pass、模块级 pass 和顺序 pass。每个子类本身都可以充当 pass 管理器。例如,它们可以收集所需的 pass 并执行,或是基于给定的元数据构建依赖关系图。访问 src/relay/ir/transform.cc 和 src/ir/transform.cc,查看完整定义。