AI框架精要:设计思想
本文主要介绍飞桨paddle平台的底层设计思想,可以帮助用户理解飞桨paddle框架的运作过程,以便于在实际业务需求中,更好的完成模型代码编写与调试及飞桨paddle框架的二次开发。
从编程范式上说,飞桨paddle兼容支持声明式编程和命令式编程,通俗地讲就是,静态图和动态图。其实飞桨paddle本没有图的概念,在飞桨paddle设计上,把一个神经网络定义成一段类似程序的描述,就是在用户写程序的过程中,就定义了模型表达及计算。在静态图的控制流实现方面,飞桨paddle借助自己实现的控制流OP而不是python原生的if else和for循环,这使得在飞桨paddle中的定义的program即一个网络模型,可以有一个内部的表达,是可以全局优化编译执行的。考虑对开发者来讲,更愿意使用python原生控制流,飞桨paddle也做了支持,并通过解释方式执行,这就是动态图。但整体上,两种编程范式是相对兼容统一的。2020年,飞桨paddle将发布更加完善的动态图功能,同时会保持更强劲的性能。
飞桨paddle平台中,将神经网络抽象为计算表示Operator(算子)和数据表示Variable(变量),如 图1 所示。神经网络的每层操作均由一个或若干Operator组成,每个Operator接受一系列的Variable作为输入,经计算后输出一系列的Variable。

图1 Operator和Variable关系示意图

根据Operator解析执行方式不同,飞桨paddle支持如下两种编程范式:
• 静态图模式(声明式编程范式):先编译后执行的方式。用户需预先定义完整的网络结构,再对网络结构进行编译优化后,才能执行获得计算结果。
• 动态图模式(命令式编程范式):解析式的执行方式。用户无需预先定义完整的网络结构,每写一行网络代码,即可同时获得计算结果。
举例来说,假设用户写了一行代码:y=x+1。在静态图模式下,运行此代码只会往计算图中插入一个Tensor加1的Operator,此时Operator并未真正执行,无法获得y的计算结果。但在动态图模式下,所有Operator均是即时执行的,运行完此代码后Operator已经执行完毕,用户可直接获得y的计算结果。
静态图模式和动态图模式的能力对比如下表所示:


说明:
由于本章节涉及飞桨paddle深度学习平台的架构设计,需要用户具备一定深度学习背景和C/C++编程能力。


静态图设计思想
静态图执行流程
在静态图模式下,飞桨paddle将神经网络描述为Program的数据结构,使用一种编程器式的执行流程,分为编译期和运行期两个阶段。
• 编译期:直接调用飞桨paddleAPI编写Python程序,向Program中添加变量Variable和算子Operator。用户只需描述前向计算,无需关心反向计算、分布式场景及异构设备场景的计算。
• 运行期:对Program进行编译优化,然后使用执行器Executor,创建Program中定义的变量,并执行Program中定义的算子。
下面以一个简单的飞桨paddle训练代码为例,体会下在静态图模式下,编译期和运行期代码的变化。
import paddle
import numpy as np

飞桨paddle2.0默认模式为动态图,需要开启静态图模式

paddle.enable_static()

编译期:调用飞桨paddle的API编写Python程序,如下述代码中定义了一个含conv2d的网络,并使用Adam优化器优化参数。

image = paddle.static.data(name=‘image’, shape=[None, 3, 224, 224], dtype=‘float32’)
conv_result = paddle.static.nn.conv2d(image, num_filters=64, filter_size=3)
loss = paddle.mean(conv_result)
adam = paddle.optimizer.Adam(learning_rate=1e-3)
adam.minimize(loss)

运行期:先运行一次startup program初始化网络参数,然后调用飞桨paddle的Executor和CompiledProgram API运行网络。

place = paddle.CPUPlace() # 使用何种设备运行网络,CPUPlace表示使用CPU运行,CUDAPlace表示使用GPU运行
executor = paddle.static.Executor(place) # 创建执行器
executor.run(paddle.static.default_startup_program()) # 运行startup program进行参数初始化

再使用CompiledProgram编译网络,准备执行。

compiled_program = paddle.static.CompiledProgram(paddle.static.default_main_program())

BATCH_NUM = 2
BATCH_SIZE = 32

for batch_id in range(BATCH_NUM):
input_image = np.random.random([BATCH_SIZE, 3, 224, 224]).astype(‘float32’)
loss_numpy, = executor.run(compiled_program, feed={‘image’: input_image}, fetch_list=[loss])
print(“Batch {}, loss = {}”.format(batch_id, loss_numpy))

关闭静态图模式

paddle.disable_static()
Batch 0, loss = [-0.09575158]
Batch 1, loss = [-0.11025753]
静态图核心架构
飞桨paddle静态图核心架构分为Python前端和C++后端两个部分,如 图2 所示:

图2 飞桨paddle静态图核心架构示意图

  • Python前端:
  1. Program由一系列的Block组成,每个Block包含各自的 Variable 和Operator。
  2. (可选操作)Transpiler将用户定义的Program转换为Transpiled Program(如:分布式训练时,将原来的Program拆分为Parameter Server Program 和Trainer Program)。
  • C++后端:
  1. (可选操作)C++后端将Python端的Program转换为统一的中间表达(Intermediate Representation,IR Graph),并进行相应的编译优化,最终得到优化后可执行的计算图。其中,编译优化包括但不限于:
    o Operator Fusion:将网络中的两个或多个细粒度的算子融合为一个粗粒度算子。例如,表达式z = relu(x + y)对应着2个算子,即执行x + y运算的elementwise_add算子和激活函数relu算子。若将这2个算子融合为一个粗粒度的算子,一次性完成elementwise_add和relu这2个运算,可节省中间计算结果的存储、读取等过程,以及框架底层算子调度的开销,从而提升执行性能和效率。
    o 存储优化:神经网络训练/预测过程会产生很多中间临时变量,占用大量的内存/显存空间。为节省网络的存储占用,飞桨paddle底层采用变量存储空间复用、内存/显存垃圾及时回收等策略,保证网络以极低的内存/显存资源运行。
  2. Executor创建优化后计算图或Program中的 Variable ,调度图中的Operator,从而完成模型训练/预测过程。
    静态图的核心概念
    飞桨paddle静态图的核心概念如下:
    • Variable:表示网络中的数据。
    • Operator:表示网络中的操作。
    • Block:表示编程语言中的控制流结构,如条件结构(if-else)、循环结构(while)等。
    • Program:基于Protobuf的序列化能力提供模型保存、加载功能。Protobuf是Google推出的一个结构化数据的序列化框架,可将结构化数据序列化为二进制流,或从二进制流中反序列化出结构化数据。飞桨paddle模型的保存、加载功能依托于Protobuf的序列化和反序列化能力。
    • Transpiler:可选的编译步骤,作用是将一个Program转换为另一个Program。
    • Intermediate Representation:在执行前期,用户定义的Program会转换为一个统一的中间表达。
    • Executor:用于快速调度 Operator ,完成网络训练/预测。
    Variable
    飞桨paddle的Variable 表示网络中的数据。 Variable 的C++底层数据结构为Protobuf表示的 VarDesc,包含如下信息:
    message VarDesc {
    // Variable的名称
    required string name = 1;

// Variable的类型,例如LOD_TENSOR、LOD_TENSOR_ARRAY等
required VarType type = 2;

// 是否为持久性变量,持久性变量在模型运行过程中不会销毁,持久性变量包括:模型参数、优化器参数等
// 非持久性变量可能在模型运行过程中销毁
optional bool persistable = 3;
}
Operator
飞桨paddle的 Operator 表示网络中的操作。 Operator 的C++底层数据结构为Protobuf表示的 OpDesc ,包含如下信息:
message OpDesc {

// Operator的类型
required string type = 3;

// Operator的输入变量列表
repeated Var inputs = 1;

// Operator的输出变量列表
repeated Var outputs = 2;

// Operator的属性列表
repeated Attr attrs = 4;
}
Operator 由如下4个域构成:
• type : std::string 类型,表示 Operator 的类型,如relu、conv2d、elementwise_add等。
• inputs : std::map<std::string, std::vectorstd::string> 类型,记录输入slot名称至实际输入变量 Variable 名称的映射。
例如,飞桨paddle sum 算子功能是将多个shape相同的输入Tensor(输入slot的名称为 X )累加为一个输出Tensor。若实际输入 Variable 的名称分别为 tmp_in_0 ,tmp_in_1 , tmp_in_2 ,则 sum 算子的 inputs 为 {“X”: [“tmp_in_0”, “tmp_in_1”, “tmp_in_2”]} 。 type 相同的算子拥有相同的输入slot名称(类似于函数的形参),但实际输入变量的名称(类似于函数的实参)可以不同。
• outputs : 与 inputs 类型相同,均为 std::map<std::string, std::vectorstd::string> 类型,记录输出slot名称至实际变量 Variable 名称的映射。
例如,飞桨paddle的 split 算子功能是将输入Tensor沿某个维度拆分为若干个Tensor(输出slot的名称为 Out )。若实际输出 Variable 的名称分别为 tmp_out_0 , tmp_out_1 , tmp_out_2 ,则 split 算子的 outputs为 {“Out”: [“tmp_out_0”, “tmp_out_1”, “tmp_out_2”]} 。
• attrs : std::map<std::string, Attribute> 类型,表示属性名称至实际属性值的映射,其中 Attribute 支持的类型包括:
o bool
o int32
o int64
o float32
o std::string
o std::vector
o std::vector
o std::vector
o std::vectorstd::string
o std::vector
Block
飞桨paddle的 Block 用于表示编程语言中的控制流结构,如条件结构(if-else)、循环结构(while)等,还描述了一组以顺序、选择或是循环执行的 Operator 以及 Operator 操作的对象:Tensor。Block 的C++底层数据结构为Protobuf表示的 BlockDesc ,包含如下信息:
message BlockDesc {
// 该Block的ID
required int32 idx = 1;

// 父Block的ID,类似于编程语言的父子Block关系
required int32 parent_idx = 2;

// 该Block中包含的Variable列表
repeated VarDesc vars = 3;

// 该Block中包含的Operator列表
repeated OpDesc ops = 4;
}
Block 的概念与编程语言中的类似,例如以下这段C++代码中包含三个Block:
#include

int64_t func(int64_t x, int64_t y)
{
bool condition = (x < y); // block 0
int64_t output;

if (condition)             // block 0
{int64_t true_out = 1;  // block 1output = true_out;     // block 1
}
else
{int64_t false_out = 0; // block 2output = false_out;    // block 2
}return output;

}
类似的,飞桨paddle代码的 Program 包含如下三段Block:
import paddle

paddle.enable_static()

x = paddle.static.data(name=‘x’, dtype=‘int64’, shape=[1]) # block 0
y = paddle.static.data(name=‘y’, dtype=‘int64’, shape=[1]) # block 0

condition = paddle.less_than(x, y) # block 0

def true_block():
true_out = paddle.ones(shape=[1], dtype=‘int64’) # block 1
return true_out

def false_block():
false_out = paddle.zeros(shape=[1], dtype=‘int64’) # block 2
return false_out

根据条件condition判断执行true_block还是false_block

output = paddle.static.nn.cond(condition, true_block, false_block)

paddle.disable_static()
/opt/conda/envs/python35-paddle120-env/lib/python3.7/site-packages/paddle/fluid/layers/utils.py:77: DeprecationWarning: Using or importing the ABCs from ‘collections’ instead of from ‘collections.abc’ is deprecated, and in 3.8 it will stop working
return (isinstance(seq, collections.Sequence) and
每个Block 拥有自己的 Operator 和 Variable ,不同 Block 中的同名 Variable 是不同的变量。
Program
Program 的C++底层数据结构为Protobuf表示的 ProgramDesc,基于Protobuf的序列化能力提供模型保存、加载功能。ProgramDesc由若干 BlockDesc构成,其中最外层的Block称为 global block(对应Block ID为0),其余Block称为 sub block。
Program、Block 的关系如 图3 所示。

图3ProgramBlock关系示意图

在模型训练/预测过程中,往往需要对参数进行一次初始化,随后多次执行训练/预测代码,以达到参数最优。因此,一段飞桨paddle程序通常包含两个 Program :
• Startup Program:初始化 Operator 所在的 Program ,包括模型参数初始化、优化器参数初始化、reader初始化等 Operator 。框架定义了一个全局默认的Startup Program,即 paddle.static.default_startup_program() 。若用户没有显式指定Startup Program,则框架会使用默认的 paddle.static.default_startup_program() 。
• Main Program:模型主体结构所在的 Program ,包括前向计算、反向计算、模型参数更新、优化器参数更新等 Operator 。框架定义了一个全局默认的Main Program,即 paddle.static.default_main_program() 。若用户没有显式指定Main Program,则框架会使用默认的 paddle.static.default_main_program() 。
Startup Program用于模型初始化,Main Program负责描述网络主体结构。因此在模型训练过程中,往往只需要运行一次Startup Program(初始化一次),然后多次运行Main Program训练模型。
下面以五个典型语句为例,体会一下 Program 在编译期的变化及其内部执行机制。
import paddle
import numpy as np

飞桨paddle2.0默认模式为动态图,需要开启静态图模式

paddle.enable_static()

语句1 :在 paddle.static.default_main_program() 中定义变量 image

image = paddle.static.data(name=‘image’, shape=[None, 3, 224, 224], dtype=‘float32’)

语句2 :在 Program 中插入conv2d算子。由于conv2d算子包含参数

因此语句中还隐含包括参数创建、参数初始化、算子插入等流程。

本语句具体执行事物如下:

在 paddle.static.default_startup_program()和paddle.static.default_main_program()

中创建conv2d算子的权重参数weight和bias。

在 paddle.static.default_startup_program()中插入权重参数weight和bias的初始化算子。

在 paddle.static.default_main_program()中插入conv2d算子,以及conv2d的输出变量conv_result 。

conv_result = paddle.static.nn.conv2d(image, num_filters=64, filter_size=3)

语句3 :在Program中插入mean算子。由于mean算子不包含参数,因此语句不涉及

paddle.static.default_startup_program()修改,只会在paddle.static.default_main_program()

中插入reduce_mean算子和对应的输出变量loss。

loss = paddle.mean(conv_result)

语句4 :定义Adam优化器,准备做参数优化。

adam = paddle.optimizer.Adam(learning_rate=1e-3)

语句5 :调用优化器的miminize。

具体执行事物如下:

在 paddle.static.default_startup_program() 中插入学习率、优化器参数

(即Adam的Moment1、Moment2、Beta1Pow和Beta2Pow)变量及对应的初始化算子。

在 paddle.static.default_main_program() 中插入反向算子,并创建对应的前向变量的梯度变量。

在 paddle.static.default_main_program() 中插入优化器算子,用于根据参数梯度值更新参数。

adam.minimize(loss)

说明:

由于以上代码中未指定Startup Program和Main Program,此处使用 paddle.static.default_startup_program()

和 paddle.static.default_main_program()

关闭静态图模式

paddle.disable_static()
Transpiler
Transpiler 是一个 Program 层面的编译器,其作用是将一个 Program 转换为另一个 Program ,设计的目的是实现 Program 的自动转换,使得用户只需关系核心的模型训练/预测逻辑,无需关心底层实现细节。 Transpiler 不是必需的编译步骤。
如 图4 所示,在Parameter Server + Trainer的分布式训练模式下,完成一个批次训练的流程如下:
• Trainer:负责执行网络的前向和反向算子,计算参数的梯度后发送给Parameter Server。
• Parameter Server:接收Trainer计算得到的参数梯度,执行网络优化器算子,更新网络的参数,并将更新后的参数发送给Trainer。

图4 分布式训练转换Program示意图

由此可见,Parameter Server和Trainer执行的算子是不同的,需要一个自动的转化机制将用户定义的原始 Program 转换为Parameter Server端和Trainer端的不同 Program ,并插入Parameter Server和Trainer间的通信算子,分布式训练的 DistributedTranspiler 用于完成上述转换。
Intermediate Representation
在执行前期,用户定义的 Program 会转换为一个统一的中间表达,即Intermediate Representation,简称IR。
IR Graph代码示意如下:
import paddle

paddle.enable_static()

image = paddle.static.data(shape=[None, 3, 224, 224], name=‘image’, dtype=‘float32’)
label = paddle.static.data(shape=[None, 1], name=‘label’, dtype=‘int64’)

y = paddle.static.nn.fc(image, size=1000)

loss = paddle.nn.functional.softmax_with_cross_entropy(y, label)

mean_loss = paddle.mean(loss)

paddle.disable_static()
飞桨paddle底层使用 SSA Graph有向无环图的形式表示IR,如 图5 所示。

图5 IR Graph示意图

• fc_w 和 fc_b 分别是网络中全连接层的权重参数和偏置参数,全连接层底层由 mul 和 elementwise_add 两个算子组成。
• Variable 和 Operator 是Graph的结点:
o Variable 的输入结点为产生该 Variable 的 Operator , 输出结点为以该 Variable 为输入的 Operator 。
o Operator 的输入结点为该 Operator 的输入 Variable 结点,输出结点为该 Operator 的输出 Variable 结点。
基于统一的IR Graph表达,飞桨paddle底层会进行Graph层面的优化,包括Operator Fusion,存储占用优化等,以提升执行效率。
在接口层面,用户调用 paddle.static.CompiledProgram 后即可获得一张经过IR Graph优化后的计算图。
import paddle

train_program = paddle.static.default_main_program() # 训练网络

CompiledProgram内部会将Program转换为IR Graph,并进行一系列的图优化操作

compiled_prog = paddle.static.CompiledProgram(train_program)
说明
IR的概念起源于编译器,是介于程序源代码与目标代码之间的中间表达形式。飞桨paddle的IR与编译器的IR类似,具有如下优势:
• 便于编译优化算法的开发:所有的编译优化算法均以优化前的IR作为输入,并输出优化后的IR,因此不同的编译优化算法可以方便地串联起来使用,相互解耦,便于编译优化算法的开发。
• 便于适配不同的后端硬件:不同后端硬件(Nvidia GPU、Intel CPU、ARM、FPGA等)的架构差异很大,若框架缺少统一的IR表达,则需要针对每一种不同的IR表达适配每一种不同的硬件平台,工作量巨大。若框架有统一的IR表达,则针对每一种不同的硬件平台做一次适配即可,且可把不同硬件平台的公共、通用的部分剥离出来抽象到IR层面,减少代码冗余度,提高可维护性。
• 便于实现不同框架模型间的相互转换:每个深度学习框架往往均有自己的统一IR表达,实现不同框架模型间的转换时,只需要实现不同框架间IR的相互转换即可,开发成本低。


Executor
Executor 用于快速调度 Operator ,完成网络训练/预测。无论是 Program 还是 IR Graph,在执行网络前均只有网络的静态描述,此时网络还未运行,未有真正创建的占有存储空间的运行期变量。飞桨paddle的 Executor 内部使用 Scope 管理运行期的 Variable 。Scope 的主要数据成员为:
class Scope {
// 变量名称到变量的映射
std::unordered_map<std::string, std::unique_ptr> vars_;

// 父Scope
Scope *parent_;

// 子Scope列表
std::list<Scope *> kids_;
};
Scope 与编程语言中的变量作用域类似,在查找变量时,会先在当前 Scope 中查找,若有则返回; 若没有则递归地从父 Scope 中查到,直到父 Scope 为空,说明变量不存在。
Executor 的创建方式如以下代码所示,其中 place 参数指明在何种设备上运行,目前飞桨paddle支持 CUDAPlace 和 CPUPlace 两种设备运行网络。
import paddle

USE_CUDA = False

place = paddle.CUDAPlace(0) if USE_CUDA else paddle.CPUPlace()

executor = paddle.static.Executor(place)


执行器 Executor.run 方法用于运行网络,具体调用方式为:
train_program = … # 训练网络,可以是Program或CompiledProgram

loss_numpy_value = executor.run(train_program, feed={‘x’: x_data, ‘y’: y_data}, fetch_list=[loss])
Executor 的执行对象可以为 Program 或 CompiledProgram (即IR Graph),其运行的基本步骤为:
• 在 Scope 中创建 Program 或 CompiledProgram 中的 Variable 。 持久性变量(模型参数、优化器参数等,即persistable属性为True的变量)创建于顶层的 Scope ,非持久性变量(临时变量)创建于顶层 Scope 的子 Scope 中。
• 若执行对象为 Program ,则按照 Program 中 Operator 的排列次序顺序依次执行 Operator 。 若执行对象为 CompiledProgram ,则按照IR Graph中 Operator 的图依赖关系多线程地调度 Operator 。 每个 Operator 执行过程中,会首先从 Scope 中取出输入输出变量,然后根据输入变量进行一系列的运行后,将结果写入输出变量中。
• 所有 Operator 执行完毕后,销毁顶层 Scope 的子 Scope ,即将网络中所有非持久性变量删除,保留持久性变量。
动态图设计思想
动态图模式是一种命令式的编程方式,无需构建完整的计算图,即可实时获得执行结果。
动态图的执行流程
在动态图模式下,Operator 是即时执行的,即用户每调用一个飞桨paddleAPI,API均会马上执行返回结果。在模型训练过程中,在运行前向 Operator 的同时,框架底层会自动记录对应的反向 Operator 所需的信息,即一边执行前向网络,另一边同时构建反向计算图。
举例来说,在只有relu和sum两个算子的网络中,动态图执行流程如下代码注释。
import numpy as np
import paddle

x_np = np.random.random([4, 5]).astype(‘float32’)
x = paddle.to_tensor(x_np)

运行前向relu算子,记录反向relu信息

y = paddle.nn.functional.relu(x)

运行前向sum算子,记录反向sum信息

z = paddle.sum(y)

根据反向计算图执行反向

z.backward()
• 当用户调用 y = paddle.nn.functional.relu(x) 时,框架底层会执行如下两个操作:
o 调用relu算子,根据输入x计算输出y。
o 记录relu反向算子需要的信息。relu算子的反向计算公式为 x_grad = y_grad * (y > 0) ,因此反向计算需要前向输出变量y,在构建反向计算图时会将y的信息记录下来。
• 当用户调用 z = paddle.sum(y) 时,框架底层会执行如下两个操作:
o 因为这里是将y的所有元素求和,是reduce_sum,调用reduce_sum算子,根据输入y计算出z。
o 记录reduce_sum反向算子需要的信息。reduce_sum算子的反向计算公式为 y_grad = z_grad.broadcast(y.shape) ,因此反向计算需要前向输入变量y,在构建反向计算图时会将y的信息记录下来。
由于前向计算的同时,反向算子所需的信息已经记录下来,即反向计算图已构建完毕,因此后续用户调用 z.backward() 的时候即可根据反向计算图执行反向算子,完成网络反向计算,即依次执行:
z_grad = [1] # 反向执行的起点z_grad为[1]
y_grad = z_grad.broadcast(y.shape) # 执行reduce_sum的反向算子:y_grad为与y维度相同的Tensor,每个元素值均为1
x_grad = y_grad * (y > 0) # 执行relu的反向算子:x_grad为与y维度相同的Tensor,每个元素值为1(当y > 0时)或0(当y <= 0时)


说明:

  1. 在使用GPU计算时,为了保证更高的执行效率,框架本身不会等待前向 Operator 的CUDA Kernel 执行完毕后才返回。即在Python端用户构建网络的同时,C++后端可能仍在异步地执行CUDA Kernel。只有在用户需要获得 Tensor 的值时(例如调用 y.numpy() ),框架才会等待CUDA Kernel执行完毕。这样既保证了运算的高效性,又保证了用户能获取到正确的 Tensor 值。
  2. 在模型预测过程中,用户调用了 layer.eval() 切换到预测模式时,框架在运行前向 Operator 后将不再记录反向信息。此时会更加节省存储资源,这是因为反向 Operator 往往需要前向 Tensor 参与反向计算,若用户切换到预测模式,则不会记录反向 Operator ,同时反向 Operator 所需的前向Tensor 亦能得到及时释放。

动态图变量和算子的底层表示
由于动态图模式下算子是即时执行,可即时获得变量的计算结果,因此动态图的变量和算子必须存储有运行时的信息。动态图的变量和算子在C++端分别以 VarBase 和 OpBase 的数据结构表示。
动态图的变量表示
VarBase 的主要成员为:
class OpBase;

class VarBase {
Variable var_;
std::shared_ptr grad_var_;
std::vector<std::shared_ptr> grad_ops_;
};
• var_: 用于存储运行时的Tensor信息。例如,当用户在Python端调用 tensor.numpy() 接口时会返回 var_ 中存储的Tensor数值。
• grad_var_: 用于存储该变量对应的反向梯度变量。 VarBase 存储 grad_var_ 的目的是便于根据前向变量找到一次反向梯度变量,根据一次反向梯度变量找到二次反向梯度变量,依此类推。
例如,当用户在Python端调用 tensor.gradient() 接口时会返回 grad_var_ ;若变量不需要计算梯度,则 grad_var_ 为空。若某个变量存在二次反向梯度,则用户可在Python端调用 tensor.gradient().gradient() 获得之(即返回C++端的grad_var_->grad_var_)。
• grad_ops_: 用于存储以变量为输入的反向算子列表,仅对反向梯度变量有效,对于前向变量此域为空。grad_ops_ 的目的是在计算前向算子的同时,辅助构建反向计算图。
动态图的算子表示
OpBase 的主要成员为:
class OpBase {
GradVarMap grad_ins_;
GradVarMap grad_outs_;
std::vector<std::shared_ptr> grad_pending_ops_;
};
• grad_ins_: 反向算子所有输入构成的映射表,其key为反向算子的输入slot,value为输入的 VarBase 。
• grad_outs_: 反向算子所有输出构成的映射表,其key为反向算子的输出slot,value为输出的 VarBase 。
• grad_pending_ops_: 反向计算图中该反向算子的后继算子列表。
动态图底层执行逻辑的实现
当用户在Python端调用飞桨paddle的前向算子API时,动态图框架底层将执行以下操作:

  1. 根据输入inputs,运行前向算子,得到输出outputs。
  2. 若前向算子不需要计算梯度,则直接返回。
  3. 若前向算子需要计算梯度,则创建对应的反向算子列表grad_ops( std::vector<std::shared_ptr> 类型)。
  4. 对于grad_ops中每个反向算子grad_op,执行下述操作:
    o 设置grad_op的输入变量 grad_ins_ 和输出变量 grad_outs_ 。其中,grad_ins_ 可能包含:前向输入变量forward_inputs、前向输出变量forward_outputs以及前向输出变量的梯度forward_outputs_grads; grad_outs_ 包含前向输入变量的梯度forward_inputs_grads。
    o 将grad_op添加到每个前向输出变量的梯度forward_outputs_grads的 grad_ops_ 域中,表示此变量为grad_op的输入。
    o 设置grad_op的grad_pending_ops_ 域等于 grad_outs_ 的 grad_ops_ 域的总和,表示grad_op的后继反向算子为以 grad_outs_ 为输入的所有反向算子。
    下面以一段动态图代码示意动态图前向运行和反向图的构建过程:
    import paddle

class ExampleLayer(paddle.nn.Layer):
def init(self):
super(ExampleLayer, self).init()
self._embedding1 = paddle.nn.Embedding(size=[128, 10])
self._embedding2 = paddle.nn.Embedding(size=[128, 10])

def forward(self, x):emb1 = self._embedding1(x) # 语句1emb2 = self._embedding2(x) # 语句2mul_out = emb1 * emb2 # 语句3relu_out = paddle.nn.functional.relu(mul_out) # 语句4return relu_out

代码对应的前向计算图和反向计算图如 图6 所示。

图6 动态图代码示例的前向计算图和反向计算图

图中W1和W2分别是代码中两个Embedding层的词表参数,@GRAD表示梯度变量,飞桨paddleEmbedding底层的算子为lookup_table。上述代码每个语句执行完毕后,反向计算图的变化如下所述:
• 语句1:构建第一个反向算子lookup_table_grad,其输入为emb1@GRAD,输出为W1@GRAD,后继的反向算子为空。因为Embedding层的输入x不需要梯度,因此反向计算图中不含x@GRAD。
• 语句2:构建第二个反向算子lookup_table_grad,其输入为emb2@GRAD,输出为W2@GRAD,后继的反向算子为空。因为Embedding层的输入x不需要梯度,因此反向计算图中不含x@GRAD。
• 语句3:构建第三个反向算子elementwise_mul_grad,其输入为mul_out@GRAD,输出为emb1@GRAD和emb2@GRAD,后继的反向算子为前述构建的2个lookup_table_grad算子。
• 语句4:构建第四个反向算子relu_grad,其输入为relu_out@GRAD,输出为mul_out@GRAD,后继的反向算子为elementwise_mul_grad。
梯度自动计算Autograd
由于前向组网过程中,框架已自动记录了反向计算图。当用户调用 tensor.backward() 的时候,框架会从调用该接口的 VarBase 节点开始,根据图依赖关系遍历执行反向计算图的每个 OpBase ,并进行相应的梯度累加,完成梯度自动计算Autograd的过程。
以 图7(反向计算图) 为例,假设调用 backward() 接口的变量为relu_out@GRAD,则Autograd的具体流程为:

  1. 计算每个反向算子的依赖数dependency_num,即其前继算子的数量。
    对于 图7(反向计算图) ,所有算子均只有1个前继算子,因此每个算子的依赖数均为1。
  2. 声明一个空的算子队列queue,并将调用 backward() 接口的变量的 grad_ops_ 进入算子队列queue。
    对于 图7(反向计算图) ,将relu_out@GRAD的 grad_ops_ 即relu_grad进入算子队列queue。
  3. 若算子队列queue未空,则取出队列头部的算子op,执行下述操作:
    o 执行反向算子op。
    o 遍历反向算子op的 grad_pending_ops_ 域,将其每个后继算子的依赖数dependency_num减1。若某个后继算子的依赖数减至0,说明此算子的所有前继算子均以执行完毕,可以开始执行此算子,将此算子加入算子队列queue。
    对于 图7(反向计算图) ,具体的执行流程为:
    o relu_grad算子出队列queue并执行,然后将elementwise_mul_grad算子加入队列queue,此时队列queue剩余1个算子。
    o elementwise_mul_grad算子出队列queue并执行,然后将2个lookup_table_grad算子加入队列queue,此时队列queue剩余2个算子。
    o 第一个lookup_table_grad算子出队列queue并执行,无算子需要加入队列queue,此时队列queue剩余1个算子。
    o 第二个lookup_table_grad算子出队列queue并执行,无算子需要加入队列queue,此时队列queue剩余0个算子,为空。
  4. 若算子队列queue为空,则说明反向计算图中的所有算子均已执行完毕,Autograd计算完成。
    变量生命周期管理
    动态图的变量可能同时被飞桨paddlePython前端和C++后端持有,只有在Python前端和C++后端均不需要该变量时,变量才能被释放,否则可能出现内存泄漏或重复释放。 对此,飞桨paddle采用自动引用计数的方式,管理每个变量的生命周期,保证无论变量的最后一次引用出现在Python前端还是C++后端,均能被正确、自动地释放,实现了变量生命周期管理的自动管理。
    动态图和静态图的异同
    由上述动态图和静态图的底层实现可知,动态图模式和静态图模式底层算子实现的方法是相同的,最大的不同点在于:
    • 在静态图模式下,完整的网络结构在执行前是已知的,因此图优化分析的灵活性比较大,往往执行性能更佳,但调试难度大。
    以算子融合Operator Fusion为例,假设网络中有3个变量x,y,z和2个算子tanh和relu。在静态图模式下,可以分析出变量y在后续的网络中是否还会被使用,如果不再使用y,则可以将算子tanh和relu融合为一个粗粒度的算子,消除中间变量y,以提高执行效率。
    y = tanh(x)
    z = relu(y)
    • 在动态图模式下,完整的网络结构在执行前是未知的,因此图优化分析的灵活性比较低,执行性能往往不如静态图,但调试方便。
    仍以Operator Fusion为例,因为后续网络结构未知,无法得知变量y在后续的网络中是否还会被使用,因此难以执行算子融合操作。但因为算子即时执行,随时均可输出网络的计算结果,更易于调试。

AI框架精要:设计思想相关推荐

  1. Spring 框架蕴含的设计思想

    在 Google Guava 源码讲解中,我们讲到开发通用功能模块的一些比较普适的开发思想,比如产品意识.服务意识.代码质量意识.不要重复早轮子等.今天,我们剖析一下 Spring 框架背后的一些经典 ...

  2. 自己做量化交易软件(11)通通量化AI框架的核心--框架结构

    自己做量化交易软件(11)通通量化AI框架的核心–框架结构 既然我说了要开源通通量化AI框架,就算大家得到了代码,也不清楚怎么去改进和修改.因此我在最后完善框架的空闲,逐步介绍框架的核心设计思想,大家 ...

  3. 五分钟带你了解Django框架设计思想!

    Python编程语言的持续火爆,在最新Tiobe编程语言排行榜中位列第五,热度持续增加,尤其是在 AI 和 大数据时代,Python是 AI 和大数据时代的第一开发语言.这已经是一个不争的事实了,唯一 ...

  4. 揭秘支撑百度搜索、Feed、小程序三大业务的MVVM框架设计思想,San 核心人员倾力打造...

    如果你是一名前端工程师,那么一定接触过组件化框架,比如 Angular.React.Vue-- 对于前端开发者而言,无论是初入职场的新兵还是久经沙场的老将,在面对纷繁复杂的业务需求和层出不穷的技术选型 ...

  5. 【设计思想解读开源框架】java监听模式和观察者模式

    深耕技术,啃下22个技术点 互联网行业更新换代非常快,行业常态便是不断学习,因此这些主流技术你一个都不能落下! ①并发编程 Java并发编程是整个Java开发体系中最难以理解,但也是最重要的知识点之一 ...

  6. 【设计思想解读开源框架】java如何发送post请求

    在这里分享一份 [mybatis从入门到精通] 的强力教程,定能够助你一臂之力. Mybatis基本介绍 ORM和MyBatis 对象/关系数据库映射(ORM) 基本映射方式 流行的ORM框架简介 目 ...

  7. 【嵌入式Linux】嵌入式Linux驱动开发基础知识之LED驱动框架--面向对象、分层设计思想

    文章目录 前言 1.LED驱动程序框架 1.1.对于LED驱动,我们想要什么样的接口? 1.2.LED驱动要怎么写,才能支持多个板子?分层写 1.3.程序分析 驱动程序 应用程序 Makefile 1 ...

  8. OSGI框架的功能和设计思想

    摘录自InfoQ电子书:<OSGi原理与最佳实践(精选版).pdf> 支持模块化的动态部署 基于 OSGi 而构建的系统可以以模块化的方式(例如 jar 文件等)动态地部署至框架中,从而增 ...

  9. 框架设计:浅谈ECS设计思想(一)

    从一开始的面向过程编程,再到后来面向对象编程.随着硬件性能的不断改进,用户对软件应用的要求也水涨船高.愈发庞大的应用不再是一个人或几个人的小团队能够完成的呢,分工愈来愈明显,逼迫着编程思想不断进步. ...

最新文章

  1. mysql查询赋值、修改拼接字符串
  2. what???现在的研究生和导师普遍都没有真正理解科研的本质
  3. 工信部支持的项目接单平台,团队、公司请进
  4. JVM 调优 —— GC 长时间停顿问题及解决方法
  5. XSS 前端防火墙 —— 天衣无缝的防护
  6. 30秒实现Vue吸顶效果
  7. 封装cookie.js、EventUtil.js、
  8. 哎,最近心情非常烦乱!
  9. 动态规划编程面试_面试的前25大动态编程问题
  10. 那些牛逼的数据分析师,SQL用的到底有多溜
  11. 职称计算机ppt考试试题,职称计算机考试PPT试题
  12. 第9章 推箱子(《C和C++游戏趣味编程》配套教学视频)
  13. 多媒体台式计算机安装方法,台式机如何组装 台式机组装注意事项【详解】
  14. LNMP详解(十二)——Nginx URL重写实战
  15. android poi导出excel,解决java poi导出excel2003不能超过65536行的问题
  16. libiconv android编译,(OK) 编译libiconv-1.14(静态库)—CentOS 7— android-ndk
  17. HexCompare比较任意Bin/Hex/Srec/s19/elf文件
  18. 驻马店计算机招聘信息网,2017河南职称计算机考试报名:驻马店职称计算机报名入口...
  19. 程序员的圣诞节是怎么样的?
  20. 做实验好比开车,危险一直都在,为啥出事的就是你?

热门文章

  1. 2022-2028年中国铝工业投资分析及前景预测报告(全卷)
  2. Redis 笔记(16)— info 指令和命令行工具(查看内存、状态、客户端连接数、监控服务器、扫描大key、采样服务器、执行批量命令等)
  3. 【J2SE】语言基础
  4. pytorch的backward
  5. 一站式智能芯片定制技术
  6. OFRecord 数据集加载
  7. 转置卷积Transposed Convolution
  8. ARM Cortex-M嵌入式C基础编程(下)
  9. 深度学习模型训练过程
  10. Android ListView 删除 item