在近期举办的“WAVE SUMMIT 2020”深度学习开发者峰会上,百度飞桨总架构师于佃海提到:
“飞桨的迭代前进,离不开两个重要驱动轮:一个是产业实践的打磨,一个是用户体验的持续优化。两个驱动轮互相配合,给飞桨提供了持久、广泛的发展动力,驱动飞桨拥有一个最灵活易用的产业级深度学习框架。”
这两个驱动,对应到框架的设计,就是在确保高效的同时,做到灵活易用。基于这一指导思想,飞桨在编程界面上同时支持了命令式编程和声明式编程,即通常说的动态图和静态图。
两种编程方式各有优势,动态图类采用“define-by-run”的执行方式,写一行代码即可即时获得结果,在编程体验、调试便捷性等方面有绝佳的优势;而静态图采用“define-and-run”的方式,事先定义好整体网络结构再执行,能够对全局编译优化,更有利于性能的提升,也有利于模型的保存和部署,但在编程调试灵活性上有所欠缺。
如何兼顾这两种模式的优势,做到灵活编程、高效训练和部署,同时具备更统一的编程体验,是一个很大的挑战。
最新发布的飞桨核心框架1.8版本,带来了重磅更新,总结下来包括两点:
1、动态图性能更卓越,经过多个版本的持续深度优化,飞桨动态图的训练性能已经媲美静态图。
2、动静更加统一,完备实现了一键式动转静、动静混合编程,使动态图开发可以无缝衔接部署,并能通过静态图执行模式对部分模型实现进一步的训练加速。
这次重大更新意味着什么呢?
这意味着开发者可以采用动态图模式编写和调试AI模型,享受便捷灵活的开发体验;并可实现和静态图相当的高性能训练,并无缝衔接模型存储和部署应用。
通俗地讲,就是以后开发AI模型再也不用做选择题啦!经过持续地融合统一,使用飞桨动态图、静态图的优势均可得到充分发挥,自然灵活兼顾卓越性能,开发者可以更容易地写出优雅的代码,实现更高效的开发和训练、部署。
飞桨是如何做到动静统一,性能卓越的呢?接下来将为你揭晓。
动静统一,带来极致用户体验
从更好全局编译优化以及上线部署的角度考虑,飞桨提供了一种完备的内在描述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功能新推,还将持续完善,欢迎持续关注飞桨更新动态。
总结一下,基于动静更加统一的飞桨框架,开发者可采用更灵活的动态图模式编程。
这将大大提升开发者的开发体验。
性能卓越,
动态图媲美静态图的训练性能
以上数据对比了几个主流模型在单张NVIDIA V100 GPU配置上的训练数据,测试环境如下:
可以看出同模型在动态图模式下的训练速度与静态图相当。
这主要得益于以下层面的优化。
1. 降低框架开销(overhead)
框架的开销可以认为是任务训练的总时间减去op kernel计算的总时间,框架开销越小越好。对动态图的即时执行模式而言,由于频繁交互而又没有全局优化,框架开销对训练效率影响很大,特别是对于RNN这类任务。飞桨通过执行流程和数据结构优化来降低框架的overhead:
a) 减少Python与C++交互复杂数据结构开销。由于Python和C++端使用的数据结构不一致,会引入一次开销比较大的转换,通过优化流程,避免这类数据结构转换。
b) OP执行优化。每个OP执行时,均需要做一次数据构造,OP运行结束之后进行析构。通过优化C++端的执行流程,简化数据结构,降低整体的overhead。
c) 移除非必需的属性。在静态图时,每个OP需要引入一类属性对OP进行标记,这些属性的构造和析构耗费比较多的时间,但对于动态图不是必须的,通过移除属性的构造和析构,减低框架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
更多飞桨动态图的应用方法,欢迎访问:
https://www.paddlepaddle.org.cn/tutorials/projectdetail/386189
本文分享自 PaddlePaddle 微信公众号,前往查看
如有侵权,请联系 cloudcommunity@tencent.com 删除。
本文参与 腾讯云自媒体同步曝光计划 ,欢迎热爱写作的你一起参与!