本文主要介绍静态图的主体设计思想和基本概念。
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()
,例如:
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);
}
}