AI学习者 · 2021年03月05日

MIT高性能自动微分框架Enzyme论文分析

文章转载于:知乎
作者:金雪锋

麻省理工学院(MIT)提出的自动微分框架 Enzyme 在 NeurIPS 2020 大会上引起过不少人的兴趣,最近也简单分析一下,供参考。

概述

当前对于将新领域,例如物理模拟、游戏引擎、气候模型等,引入到机器学习中来,存在一个普遍问题--求梯度时,需要将外来代码通过源码重写或者操作符重载,以融入现有AD(automatic differentiation自动微分)工具(Adept、Autograd等)或深度学习框架(TensorFlow、PyTorch、MindSpore等),这增加了在机器学习工作流中引入外来代码的工作量。而MIT的这篇论文提出了Enzyme,一个基于LLVM IR的自动微分编译器插件,来缓解该问题。Enzyme可以生成用LLVM IR表达的静态可分析程序的梯度。

Enzyme的关键点:

  1. 可以自动生成基于静态可分析的LLVM IR的梯度。
  2. 在优化后的IR上做自动微分,以得到一个高性能的梯度计算。

设计

1、类型分析

LLVM IR没必要表示出所有数据的潜在类型,所以Enzyme做了一些自己的抽象解析。如引入类型树,实现了一个类型传播规则。

2、活动分析

分析出哪些指令对梯度计算有影响以及哪些指令是不可微分的,从而减少不必要的计算。

3、影子内存

对于正向部分需要计算的数据,Enzyme会复制一份影子内存,用于保存梯度计算结果,一直到梯度计算不需要时才会释放。

4、合成梯度

根据类型和活动分析的结果,创建梯度函数,梯度函数包括正向部分和反向部分。

5、缓存

Enzyme在计算反向部分时,如果需要正向部分的结果,默认情况下会重新计算正向。但是对于不可能重新计算的操作(如Read)或效率低的操作,Enzyme也提供了缓存机制保存正向结果用于计算反向部分。

6、生成高性能的梯度计算

Enzyme是在优化过的LLVM IR上做AD的。论文给出了一个在LICM(loop-invariant code motion)优化后做AD的如下例子,相比于在AD之后做LICM,计算复杂度有一个O(N^2)到O(N)的提升。

Top:一个O(N^2)的norm函数,如果使用loop-invariant-codemotion (LICM)把mag函数提出来,能够将复杂度优化到O(N)。Left:执行LICM之后做AD,得到的一个O(N)的 ∇norm函数。Right:在AD之后执行LICM,得到的一个O(N^2)的 ∇norm函数,∇mag仍然在循环内部,因为它使用了循环内部的一个值,LICM并不生效。

下图为论文给出的在不同的benchmark上,Enzyme与其他自动微分工具的性能对比。其中,Ref是Enzyme在优化前做AD的一个对比实验组,与Ref相比,每个benchmark平均下来,在优化后做AD,有4.5倍的性能提升。

注:这个性能对比主要还是和同等LLVM AD工具比

Left:不同的AD系统在benchmark上的相对加速比,柱状越高的越好。值1.0表示当前benchmark上最快的AD系统,值0.5表示该AD系统生成的梯度计算,花费的时间是最快的2倍。Right:以秒为单位的几何平均运行时间。

Enzyme和Ref两条流水线,在AD前后执行优化的情况

Enzyme的使用

要生成基于LLVM语言的代码梯度,需要调用一个外部函数\_\_enzyme\_autodiff。

以C程序为例,求一个函数的梯度:

  1. 准备一个C程序
// test.c
#include <stdio.h>
extern double __enzyme_autodiff(void*, double);//extern funciton
double square(double x) {
    return x * x;
}
double dsquare(double x) {
    // This returns the derivative of square or 2 * x
    return __enzyme_autodiff(square, x);
}
int main() {
    for(double i=1; i<5; i++)
        printf("square(%f)=%f, dsquare(%f)=%f", i, square(i), i, dsquare(i));
}

2. 使用clang生成llvm ir

clang test.c -S -emit-llvm -o input.ll -O2 -fno-vectorize -fno-slp-vectorize -fno-unroll-loops

3. 使用Enzyme生成梯度

opt input.ll -load=/path/to/Enzyme/enzyme/build/Enzyme/LLVMEnzyme-<VERSION>.so -enzyme -o output.ll -S

4. 进行AD后的优化并生成可执行文件

clang output.ll -O3 -o a.exe

另外,在做AD时,Enzyme需要获取所有需要微分函数的IR。如果只是一种源代码的IR很容易获得,对于多种源代码或者使用外部库的代码,Enzyme使用Link-Time Optimization (LTO)--一种用于整个程序优化的编译器技术,保留所有源文件的IR一直到链接阶段。为了在多种源代码的代码库中使用,使能LTO,在合并的IR上运行Enzyme,对于静态库可以在编译时加上-fembed-bitcode命令,使得在静态库里面包含中间代码。

目前看Enzyme还支持CPU。

Enzyme在ML框架的应用

根据上述使用Enzyme的方法,借助AI框架的自定义算子能力,可以把Enzyme内嵌到各种框架中,以MindSpore为例:

借助MindSpore自定义算子的能力把Enzyme嵌入到MindSpore中去使用:

自定义算子(CPU) - MindSpore r1.1 documentation​www.mindspore.cn

方法如下:

  1. 自定义正向算子Enzyme,其中算子实现为根据外来源代码路径,调用clang去生成外来源代码的.so,并通过dlfcn库加载该.so,根据函数名去获取并调用正向函数。
  2. 自定义反向算子EnzymeGrad,其中算子实现为根据外来源代码路径,调用clang和enzyme去生成外来源代码经过AD的.so,并通过dlfcn库加载该.so,根据函数名去获取并调用梯度函数。
  3. 定义反向传播函数(bprop),函数的计算逻辑为调用自定义好的反向算子EnzymeGrad。
  4. 定义正向网络,网络中调用自定义好的Enzyme算子。最后根据正向网络调用GradOperation函数求梯度,就可以使用Enzyme生成梯度了。

当然现在Enzyme也已经可以应用到Pytorch或TF中。

主要结论:

  1. Enzyme提供了一种梯度计算的性能提升方法:在某些优化完成之后才进行AD。
  2. Enzyme能在低级别的LLVM IR上进行AD,非常方便嵌入现有的框架。

论文地址:https://arxiv.org/pdf/2010.01709.pdf

github地址:https://github.com/wsmoses/Enzyme

推荐阅读

更多嵌入式AI技术干货请关注嵌入式AI专栏。
推荐阅读
关注数
16756
内容数
1233
嵌入式端AI,包括AI算法在推理框架Tengine,MNN,NCNN,PaddlePaddle及相关芯片上的实现。欢迎加入微信交流群,微信号:aijishu20(备注:嵌入式)
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息