static compiler 1

2020-05-20 17:27:58 浏览数 (1)

本文主要介绍静态图的主体设计思想和基本概念。

1. Program

Fluid将神经网络描述为Program这一数据结构。

Program由Block组成,即 Program = List[Block]

Block由Operator和Variable组成,即 Block = List[Operator] List[Variable]

与编程语言类比,我们可以将Program理解为程序,Block对应程序的控制流分支结构,如条件分支、循环分支等。Fluid的控制流Op(conditional_block,while,recurrent等)均通过Block表达。

不同Block里的变量可以重名。若父Block与子Block中存在同名变量,那么子Block的Operator运行时会优先找到子Block中的变量。

代码语言:txt复制
int func(int n) {
    // Main Block
    int a = 3;
    int b = 4;
    int c = a * b;
    int d = c / 10;
    
    ...
    
    int sum = 0;
    if (n > 0) { // sub-block
        for (int i = 0; i < 10; i  ) {
            sum  = i; // sub-sub-block
        }
    } else { // sub-block
        for (int i = 0; i < (-n); i  ) {
            sum -= i; // sub-sub-block
        }
    }
    
    std::cout << d << " " << sum << std::endl;
    
    return 0;
}

在组建神经网络的过程中会涉及两个Program,即startup program和main program。

startup program对应TensorFlow中的tf.global_initializer(),包含参数、learning rate、Optimizer Momentum等变量的初始化Op。

main program对应神经网络的主体结构。因此,在运行神经网络训练/预测时,我们需首先跑一次startup program进行初始化,然后跑多次main program进行训练/预测。

Fluid定义了全局默认的startup program和main program,即 fluid.default_startup_program()fluid.default_main_program() ,调用 fluid.layers.xxx API时,均会往全局默认的program中插入op。

若要切换全局的startup program和main program,可使用 fluid.program_guard() ,例如:

代码语言:txt复制
import paddle.fluid as fluid
startup_program = fluid.Program()
main_program = fluid.Program()

assert startup_program != fluid.default_startup_program()
assert main_program != fluid.default_main_program()

with fluid.program_guard(main_program, startup_program):
    assert startup_program == fluid.default_startup_program()
    assert main_program == fluid.default_main_program()
    ...
    
assert startup_program != fluid.default_startup_program()
assert main_program != fluid.default_main_program()

Python端的Program,Block,Operator,Variable分别对应于C 端的ProgramDesc,BlockDesc,OpDesc,VarDesc。

值得注意的是,Program是对神经网络的静态描述,其底层是Protobuf Description,因此在运行网络以前所有变量、Op均不存在。

2. C Place

Place表示设备,可以是GPU设备或CPU设备。

代码语言:txt复制
using Place = boost::variant<CUDAPlace, CPUPlace, CUDAPinnedPlace>;

boost::variant类似于C 的union,是一种类型安全的union,即multi-type, one-value。

同一设备的内存/显存的Place相同,即相同Place的Tensor的内存/显存空间在同一设备上。

3. C DeviceContext

DeviceContext表示设备,可以理解为是一个虚拟设备的概念,包含CPUDeviceContext和CUDADeviceContext等。理论上,一个Place可对应多个DeviceContext。

DeviceContext中包含一些额外的设备信息,例如cudaStream_t, cudnnHolder_t, cublasHandle_t等。

在目前Fluid的设计中,我们维护了一个全局的DeviceContextPool,记录了Place到DeviceContext的map(单映射,非multimap)。即在全局DeviceContextPool中,Place和DeviceContext是一一对应的。目前Fluid的所有单设备Op均运行在全局的DeviceContext中。

4. C Variable

C Variable是一个类似于std::any的结构,可以存储任意类型的变量。最常见的,Variable里存储LoDTensor,但Variable还可以存储SelectedRows,ReaderHolder等。

5. C Scope

Scope用于存储变量,Scope主要数据成员为:

代码语言:txt复制
class Scope {
 std::unordered_map<std::string, std::unique_ptr<Variable>> vars_; // Scope中存储的变量
 const Scope *parent_; // 父Scope
 std::list<Scope *> kids_; // 子Scope
};

Scope与编程语言的作用域类似,当调用Scope::FindVar时,会首先在当前Scope中查找变量是否存在,若存在则直接返回,否则递归地从父Scope里寻找该变量。

6. Operator

Op的成员

Op主要包含4个信息:

  • type: std::string类型,表示Op的名称,如"matmul","conv2d","reshape"等。
  • inputs: std::map<std::string, std::vector<std::string>>类型,表示Op的输入变量,map的key为slot名称,对应于OpProtoAndCheckerMaker中定义的名称;map的value为实际的变量名,对应于Scope中的变量名。
  • outputs: std::map<std::string, std::vector<std::string>>类型,表示Op的输出变量。key和value的含义与inputs类似。
  • attributes: std::map<std::string, boost::variant<...>>类型,表示Op的属性,例如transpose选择哪些维度进行转置等。

OperatorBase

OperatorBase是所有Op的基类,其Run方法的声明为:

代码语言:txt复制
void OperatorBase::Run(const Scope &scope, const platform::Place& place) {...}

运行Op时,需指明Scope和Place。Op运行过程中,会首先从Scope中获取输入输出变量,然后从Place中获取设备信息,进行计算。

OpKernel

OperatorWithKernel继承自OperatorBase,我们称继承自OperatorWithKernel的Op为有Kernel的Op。

Kernel的目的是为了区分不同的运行设备(CPU/GPU)、数据类型(float/double/int)、库(MKLDNN/CUDNN)、layout(NCHW/NHWC)等。

一个Op可以有多个Kernel实现,Kernel实现应继承自OpKernel<T>。

OperatorWithKernel重写了OperatorBase的RunImpl方法,进行了以下操作:

  • 根据inputs和outputs,从Scope中找出所有输入输出变量,形成map<string, vector<Variable *>>,构造出ExecutionContext。
  • 根据inputs Tensor的设备、layout等信息,判断是否需要对Tensor进行设备转换、Layout转换等。例如,若前一个Op的输出Tensor的CPU上,当前Op需要运行在GPU上,需要将当前Op的输入Tensor copy到GPU上。在转换过程中,会从当前Scope中new一个新的Scope,并在新Scope中创建同名变量进行Transfer。
  • 调用OperatorWithKernel::InferShape方法推导输出变量的shape信息。
  • 根据inputs Tensor的设备、layout、数据类型等信息,从所有的Kernel中选择合适的Kernel,将ExecutionContext传入OpKernel<T>::Compute方法进行计算。

7. 编译期过程简介

在Python端组网过程中,即调用fluid.layers.xxx API组网时,亦称为编译期,会往Program中插入Op,具体为:

  • 若Op包含参数,在default_startup_program的block 0中插入参数的初始化Op,然后将参数copy到default_main_program中。
  • 在default_main_program中插入相应的Op。

每次往Python端Program插入Op时,均会走以下步骤:

  • 若Op没有Kernel,则不进行任何操作。
  • 若Op有Kernel,则:
    • 若Op有注册InferVarTypeInference,则调用InferVarTypeInference推导输出变量的类型(LoDTensor还是SelectedRows等)。
    • 调用OperatorWithKernel::InferShape方法,根据输入变量推导输出变量的shape。

在调用optimizer.minimize()的过程中,会发生以下几个动作:

  • 调用每个前向Op的GradOpDescMaker创建反向Op,并插入到default_main_program中。
  • 在Program中为每个参数插入各自的optimizer op。

8. 运行期过程简介

当Python端的Program构建完毕后,Executor::Run会取出Program的Block 0中的所有OpDesc,调用OpRegistry::CreateOp方法根据OpDesc创建OperatorBase,然后调用OperatorBase::Run()方法运行所有Op,具体方式为:

代码语言:txt复制
void Executor::Run(const ProgramDesc &program, const Scope &scope, const platform::Place &place) {
    std::vector<std::unique_ptr<OperatorBase>> ops;
    
    for (auto &op_desc : program.Block(0).AllOps()) {
        auto op = OpRegistry::CreateOp(op_desc);
        ops.emplace_back(std::move(op));
    }
    
    for (auto &op : ops) {
        op->Run(scope, place);
    }
}

0 人点赞