【笔记】深度学习框架与静态/动态计算图

深度学习框架是用于构建和训练 DNN 模型的工具,常见的框架包括TensorFlow、PyTorch等。这些框架的核心区别之一在于计算图的管理方式。本文将初步梳理流行的深度学习框架官网对静态计算图(Static Computation Graph)和动态计算图(Dynamic Computation Graph)的介绍。

MindSpore:动静态图结合

引用自:MindSpore

静态图和动态图的概念

目前主流的深度学习框架的执行模式有两种,分别为静态图模式和动态图模式。

  • 静态图模式下,
    • 程序在编译执行时先生成神经网络的图结构,然后再执行图中涉及的计算操作
    • 因此,在静态图模式下,编译器利用图优化等技术对执行图进行更大程度的优化,从而获得更好的执行性能,有助于规模部署和跨平台运行
  • 动态图模式下,
    • 程序按照代码的编写顺序执行,在执行正向过程中根据反向传播的原理,动态生成反向执行图
    • 这种模式下,编译器将神经网络中的各个算子逐一下发执行,方便用户编写和调试神经网络模型

OneFlow:动态图与静态图

引用自:静态图模块 nn.Graph - OneFlow

目前,深度学习框架中模型的运行方式主要有两种,即 动态图 与 静态图,在 OneFlow 中,也被习惯称为 Eager 模式 和 Graph 模式 。

这两种方式各有优缺点,OneFlow 对两种方式均提供了支持,默认情况下是 Eager 模式

一般而言,动态图更易用,静态图性能更具优势

扩展阅读:动态图与静态图

用户定义的神经网络,都会被深度学习框架转为计算图,如 自动求梯度 中的例子:

def loss(y_pred, y):
    return flow.sum(1/2*(y_pred-y)**2)

x = flow.ones(1, 5)  # 输入
w = flow.randn(5, 3, requires_grad=True)
b = flow.randn(1, 3, requires_grad=True)
z = flow.matmul(x, w) + b

y = flow.zeros(1, 3)  # label
l = loss(z,y)

对应的计算图为:
在这里插入图片描述

动态图(Dynamic Graph)

动态图的特点在于,它是一边执行代码,一边完成计算图的构建的。 以上代码和构图关系可看下图(注意:下图对简单的语句做了合并)

在这里插入图片描述

因为动态图是一边执行一边构图,所以很灵活,可以随时修改图的结构,运行一行代码就能得到一行的结果,易于调试。但是因为深度学习框架无法获取完整的图信息(随时可以改变、永远不能认为构图已经完成),因此无法进行充分的全局优化,在性能上会相对欠缺。

静态图(Static Graph)

与动态图不同,静态图先定义完整的计算图。即需要用户先声明所有计算节点后,框架才开始进行计算。这可以理解为在用户代码与最终运行的计算图之间,框架起到了编译器的作用

在这里插入图片描述

以 OneFlow 框架为例,用户的代码会被先转换为完整的计算图,然后再由 OneFlow Runtime 模块运行。

静态图这种先获取完整网络,再编译运行的方式,使得它可以做很多动态图做不到的优化,因此性能上更有优势。并且编译完成后的计算图,也更容易跨平台部署。

不过,在静态图中真正的计算发生时,已经与用户的代码没有直接关系了,因此静态图的调试较不方便

PaddlePaddle:声明式编程(静态图)与命令式编程(动态图)

引用自:飞桨PaddlePaddle-源于产业实践的开源深度学习平台
零基础实践深度学习 → 第八章:精通深度学习的高级内容(3)→ 设计思想和静动态图

从深度学习模型构建方式上看,飞桨支持声明式编程(静态图/Declarative programming)和命令式编程(动态图/Imperative programming)两种方式。二者的区别是:

  • 静态图采用先编译后执行的方式。用户需预先定义完整的网络结构,再对网络结构进行编译优化后,才能执行获得计算结果。
  • 动态图采用解析式的执行方式。用户无需预先定义完整的网络结构,每执行一行代码就可以获得代码的输出结果。

在飞桨设计上,把一个神经网络定义成一段类似程序的描述,就是在用户写程序的过程中,就定义了模型表达及计算。

  • 在静态图的控制流实现方面,飞桨借助自己实现的控制流OP而不是python原生的if else和for循环,这使得在飞桨中的定义的program即一个网络模型,可以有一个内部的表达,是可以全局优化编译执行的。
  • 考虑对开发者来讲,更愿意使用python原生控制流,飞桨也做了支持,并通过解释方式执行,这就是动态图。但整体上,我们两种编程范式是相对兼容统一的。

举例来说,假设用户写了一行代码:y=x+1

  • 在静态图模式下,运行此代码只会往计算图中插入一个Tensor加1的Operator,此时Operator并未真正执行,无法获得y的计算结果。
  • 但在动态图模式下,所有Operator均是即时执行的,运行完代码后Operator已经执行完毕,用户可直接获得y的计算结果。

静态图模式和动态图模式的能力对比如下表所示:

对比项静态图模式动态图模式
是否可即时获得每层计算结果否,必须构建完整网络后才能运行
调试难易性欠佳,不易调试结果即时,调试方便
性能由于计算图完全确定,可优化的空间更多,性能更佳计算图动态生成,图优化的灵活性受限,部分场景性能不如静态图
预测部署能力可直接预测部署不可直接预测部署,需要转换为静态图模型后再能部署

飞桨静态图

静态图核心架构

飞桨静态图核心架构分为Python前端C++后端两个部分,如 图4 所示:

图4 飞桨静态图核心架构示意图
图4 飞桨静态图核心架构示意图

用户通过Python语言使用飞桨,但训练和预测的执行后端均为C++程序,这使得飞桨兼具用户轻松的编程体验和极高的执行效率。

Python前端

  1. 在Python端,静态图模式是在形成Program的完整表达后,编译优化并交于执行器执行。Program由一系列的Block组成,每个Block包含各自的 VariableOperator
  2. (可选操作)Transpiler将用户定义的Program转换为Transpiled Program,如:分布式训练时,将原来的Program拆分为Parameter Server Program 和Trainer Program。在两者中均插入了通信的算子,用于不同训练服务器之间通信参数梯度的情况。

图5 原始Program经过特定的Transpiler形成特定功能的Program
图5 原始Program经过特定的Transpiler形成特定功能的Program

C++后端

  1. (可选操作)C++后端将Python端的Program转换为统一的中间表达(Intermediate Representation,IR Graph),并进行相应的编译优化,最终得到优化后可执行的计算图。其中,编译优化包括但不限于:
    1. Operator Fusion:将网络中的两个或多个细粒度的算子融合为一个粗粒度算子。例如,表达式z = relu(x + y)对应着2个算子,即执行x + y运算的elementwise_add算子和激活函数relu算子。若将这2个算子融合为一个粗粒度的算子,一次性完成elementwise_add和relu这2个运算,可节省中间计算结果的存储、读取等过程,以及框架底层算子调度的开销,从而提升执行性能和效率。通俗点理解,读者在中小学做过的数学公式简化是类似的道理,本来一个十分复杂和冗长的计算过程,合并成一个公式后进行简化,有时候会得到一个非常简单的结果。
    2. 存储优化:神经网络训练/预测过程会产生很多中间临时变量,占用大量的内存/显存空间。为节省网络的存储占用,飞桨底层采用变量存储空间复用内存/显存垃圾及时回收等策略,保证网络以极低的内存/显存资源运行。
  2. Executor创建优化后计算图或Program中的 Variable ,调度图中的Operator,从而完成模型训练/预测过程。

IR graph:IR全称是 Intermediate Representation,表示统一的中间表达。 IR的概念起源于编译器,是介于程序源代码与目标代码之间的中间表达形式。有如下好处:

  • 便于编译优化(非必须);
  • 便于部署适配不同硬件(Nvidia GPU、Intel CPU、ARM、FPGA等),减少适配成本。

图6 IR代码图示

图6 IR代码图示

飞桨动态图

在动态图模式下,Operator 是即时执行的,即用户每调用一个飞桨API,API均会马上执行返回结果。在模型训练过程中,在运行前向 Operator 的同时,框架底层会自动记录对应的反向 Operator 所需的信息,即一边执行前向网络,另一边同时构建反向计算图

举例来说,在只有relu和reduce_sum两个算子的网络中,动态图执行流程如 图7 所示。

图7 动态图代码执行流程
图7 动态图代码执行流程

  • 当用户调用 y = paddle.nn.functional.relu(x_pd) 时,框架底层会执行如下两个操作:
    • 调用relu算子,根据输入x计算输出y。
    • 记录relu反向算子需要的信息。relu算子的反向计算公式为 x_grad = y_grad * (y > 0) ,因此反向计算需要前向输出变量y,在构建反向计算图时会将y的信息记录下来。
  • 当用户调用 z = paddle.sum(y) 时,框架底层会执行如下两个操作:
    • 调用reduce_sum算子,根据输入y计算出z。
    • 记录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时)

图8 动态图根据“前向计算图”自动构建“反向计算图”

图8 动态图根据“前向计算图”自动构建“反向计算图”

由此可见,在动态图模式下,执行器并不是先掌握完整的网络全图,再按照固定模式批量执行。而是根据Python的原生控制流代码,逐条执行前向计算过程和后向计算过程,其中后向计算的逻辑飞桨框架会在前向计算的时候自动化构建,不需要用户再操心。

动态图和静态图的差异

动态图模式和静态图模式底层算子实现的方法是相同的,不同点在于:

  1. 代码组织方式不同
  2. 代码执行方式不同

代码组织方式不同

  • 在使用静态图实现算法训练时,需要使用很多代码完成预定义的过程,包括program声明,执行器Executor执行program等等。
  • 但是在动态图中,动态图的代码是实时解释执行的,训练过程也更加容易调试。

图9 动态图和静态图编码对比

图9 动态图和静态图编码对比

  • 如 图9 右侧所示,是我们相对熟悉的动态图编写模式:使用的方式声明网络后,开启两层的训练循环,每层循环中完整的完成四个训练步骤(前向计算、计算损失,计算梯度和后向传播)。
  • 但 图9 左侧则是静态图的编写模式:使用函数方式声明网络,然后要编写大量预定义的配置项,如选择的损失函数,训练所在的机器环境等等。在这些训练配置定义好后,声明一个执行器exe(运行Program,调度Operator完成网络训练/预测),将数据和模型传入exe.run()函数,一次性的完成整个训练过程。

代码执行方式不同

  • 在静态图模式下,完整的网络结构在执行前是已知的,因此图优化分析的灵活性比较大,往往执行性能更佳,但调试难度大。

以算子融合Operator Fusion为例,假设网络中有3个变量x,y,z和2个算子tanh和relu。在静态图模式下,我们可以分析出变量y在后续的网络中是否还会被使用,如果不再使用y,则可以将算子tanh和relu融合为一个粗粒度的算子,消除中间变量y,以提高执行效率。

y = tanh(x)
z = relu(y)
  • 在动态图模式下,完整的网络结构在执行前是未知的,因此图优化分析的灵活性比较低,执行性能往往不如静态图,但调试方便。

仍以Operator Fusion为例,因为后续网络结构未知,我们无法得知变量y在后续的网络中是否还会被使用,因此难以执行算子融合操作。但因为算子即时执行,随时均可输出网络的计算结果,更易于调试。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值