机器之心发布
机器之心编辑部
在近期举办的「WAVE SUMMIT 2020」深度学习开发者峰会上,百度飞桨总架构师于佃海提到:
「飞桨的迭代前进,离不开两个重要驱动轮:一个是产业实践的打磨,一个是用户体验的持续优化。两个驱动轮互相配合,给飞桨提供了持久、广泛的发展动力,驱动飞桨拥有一个最灵活易用的产业级深度学习框架。」
这两个驱动,对应到框架的设计,就是在确保高效的同时,做到灵活易用。基于这一指导思想,飞桨在编程界面上同时支持了命令式编程和声明式编程,即通常说的动态图和静态图。
两种编程方式各有优势,动态图类采用 “define-by-run” 的执行方式,写一行代码即可即时获得结果,在编程体验、调试便捷性等方面有绝佳的优势;而静态图采用 “define-and-run” 的方式,事先定义好整体网络结构再执行,能够对全局编译优化,更有利于性能的提升,也有利于模型的保存和部署,但在编程调试灵活性上有所欠缺。
如何兼顾这两种模式的优势,做到灵活编程、高效训练和部署,同时具备更统一的编程体验,是一个很大的挑战。
最新发布的飞桨核心框架 1.8 版本,带来了重磅更新,总结下来包括两点:
这次重大更新意味着什么呢?
这意味着开发者可以采用动态图模式编写和调试 AI 模型,享受便捷灵活的开发体验;并可实现和静态图相当的高性能训练,并无缝衔接模型存储和部署应用。
通俗地讲,就是以后开发 AI 模型再也不用做选择题啦!经过持续地融合统一,使用飞桨动态图、静态图的优势均可得到充分发挥,自然灵活兼顾卓越性能,开发者可以更容易地写出优雅的代码,实现更高效的开发和训练、部署。
飞桨是如何做到动静统一,性能卓越的呢?接下来将为你揭晓。
动静统一,带来极致用户体验
飞桨框架的设计思想,是期望将深度学习计算的编程和内在表示保持一致,所以从用户界面上没有引入 Graph 等概念,直接以程序化的 “Program” 形式描述神经网络模型的计算过程,对应的用户开发和通用的编程体验更加接近。
从更好全局编译优化以及上线部署的角度考虑,飞桨提供了一种完备的内在描述 ProgramDesc,可以表达任意复杂的模型,并实现编译期和运行时的分离——这个意义上可与声明式编程范式即 “静态图” 相对应。
从 Python 普及度、以及更方便用户灵活调试的角度出发,飞桨从 2018 年底开始扩展支持使用 Python 原生控制流、即时执行的命令式编程范式,也就是大家通常讲的“动态图”。
其实对飞桨而言,基于编程一致的计算描述,向动态图的扩展,以及动静转换都是非常自然的。
在之前的版本,飞桨已经提供了 TracedLayer,可以将不含数据相关的控制流的动态图模型转成静态图模型。在 1.8 版本中飞桨提供了功能更强大、更通用的 ProgramTranslator,可以完备地将 Python 语法下的计算定义转译为 Program,从而和全局优化延时执行模式打通,并可实现模型结构存储和上线部署。
操作上非常简单,只需要在定义神经网络时添加一个装饰器,就可以将对应函数内部的所有定义,包括依赖数据的控制流实现,递归地转换为静态 program 执行。并且在这种模式下,可以灵活控制,实现动静混合编程。
下面通过一个示例讲述如何在一个 Layer 中添加装饰器。
class MNIST(fluid.dygraph.Layer):
def __init__(self):
super(MNIST, self).__init__()
self.conv = Conv2D( 1, 20, 5)
self.pool2d = Pool2D( pool_size=2, pool_stride=2 )
self.pool_2_dim = 5 * 4 * 4
self.proj = Linear(self.pool_2_shape, 10,
act="softmax")
@declarative # 添加装饰器,将forward函数内Layers递归地转为静态图的Program执行
def forward(self, inputs):
x = self.conv(inputs)
x = self.pool2d( x )
x = fluid.layers.reshape( x, [-1, self.pool_2_dim])
x = self.proj(x)
return x
可以看到,仅仅需要在最外层的 Layer 加上一个 declarative 装饰器,即可把动态图转成静态图进行执行,不需要在其他任何地方做修改。
如果需要部署模型,通过 ProgramTranslator 将动态图的模型转成静态图的 program 即可,示例如下:
import paddle.fluid as fluid
from paddle.fluid.dygraph import to_variable
fluid.enable_imperative()
prog_trans = fluid.dygraph.ProgramTranslator()
model = MNIST()
in_np = np.random.random( [20, 3, 28, 28]).astype("float32")
in_var = to_vairbale( in_np )
out = model(in_var)
prog_trans.save_inference_model("./dy2stat_infer_model", fetch=[0])
借助 ProgramTranslator,通过执行一次 forward 运算得到一个静态图的 program。之后就可以复用之前静态图一键部署的逻辑进行部署上线。ProgramTranslator 功能新推,还将持续完善,欢迎持续关注飞桨更新动态。
总结一下,基于动静更加统一的飞桨框架,开发者可采用更灵活的动态图模式编程。
这将大大提升开发者的开发体验。
性能卓越,动态图媲美静态图的训练性能
其实,对于大部分任务而言,无需通过动静转换,飞桨的动态图训练已经具备非常高的性能。飞桨自 1.3 版本版增加动态图功能以来,持续数个版本,一直致力于提升训练的整体性能。目前在主流的任务上,飞桨动态图执行模式已经能够达到与静态图媲美的水平。
以上数据对比了几个主流模型在单张 NVIDIA V100 GPU 配置上的训练数据,测试环境如下:
可以看出同模型在动态图模式下的训练速度与静态图相当。
这主要得益于以下层面的优化。
1.降低框架开销(overhead)
框架的开销可以认为是任务训练的总时间减去 op kernel 计算的总时间,框架开销越小越好。对动态图的即时执行模式而言,由于频繁交互而又没有全局优化,框架开销对训练效率影响很大,特别是对于 RNN 这类任务。飞桨通过执行流程和数据结构优化来降低框架的 overhead:
2.引入多流异步的数据加载方案
数据加载的性能会严重影响整个训练的吞吐。动态图模式下每个 OP 执行,都会申请 Python 的全局锁(Global Interpreter Lock),这将导致异步 DataLoader 中数据处理的线程效率受到很大影响。如果训练时每个 batch 的数据量比较大,DataLoader 的性能就不如静态图下那么高效。为此,飞桨动态图引入了多线程数据处理流程,多线程不会受到全局锁的影响,进而提升执行效率。
3.更优的 cache 机制
在卷积(Convolution)运算中,可根据输入的 shape、dtype、data_format 等信息选择最优的 cudnn algorithm,由此来提升执行效率,但是搜索最优的 cudnn algorithm 会耗费比较多的时间。
通过引入 algorithm 的 cache 机制,可以降低搜索的耗时,提升任务的整体训练效率。用户仅需要设置 FLAGS_cudnn_exhaustive_search = 1 即可开启这种 cache 机制。通过开启该机制,语音的 waveflow 任务性能可提升 200%。
除了以上优化,最新的 1.8 版本在动态图功能上也有进一步增强,可以支持更多的任务,满足开发者多样化的需求,目前主流的模型都可以使用动态图进行实现。
1、支持 double grad 运算
在一些任务(如 GAN 相关的)中,强依赖梯度惩罚功能,在 1.8 版本提供了 double grad 的支持,方便任务的实现。double grad 简单的使用示例如下:
with fluid.enable_imperative() # 激活动态图模式
x = fluid.layers.ones(shape=[1], dtype='float32')
x.stop_gradient = False
y = x * x
# Since y = x * x, dx = 2 * x
# 调用grad方法
dx = fluid.dygraph.grad(
outputs=[y],
inputs=[x],
create_graph=create_graph,
retain_graph=True)[0]
z = y + dx
z.backward() # double grad计算
2、Layer 支持 hook 功能
1.8 版本提供了 forward pre-hook 和 forward post-hook 的支持,方便组网的时候对 input 进行修改。
forward pre-hook 的使用如下所示, 通过 forward_pre_hook,可以将输入整体扩大两倍,同时也可以支持更复杂的一些 pre_hook 运算( 比如参数的 normalization 运算 ):
import paddle.fluid as fluid
import numpy as np
# forward_pre_hook函数修改了layer的输入:input = input * 2
def forward_pre_hook(layer, input):
# 改变输入值
input_return = (input * 2)
return input_return
fluid.enable_imperative()
linear = fluid.Linear(13, 5, dtype="float32")
# 注册hook
forward_pre_hook_handle = linear.register_forward_pre_hook(forward_pre_hook)
value0 = np.arange(26).reshape(2, 13).astype("float32")
in0 = fluid.dygraph.to_variable(value0)
out0 = linear(in0)
# 移除hook
forward_pre_hook_handle.remove()
value1 = value0 * 2
in1 = fluid.dygraph.to_variable(value1)
out1 = linear(in1)
同时飞桨已经开源了主流模型的动态图实现,不要错过这个丰富的资源库:
https://github.com/PaddlePaddle/models/tree/develop/dygraph
Ernie 也开放了动态图的实现,赶快来试试吧:
https://github.com/PaddlePaddle/ERNIE