飞桨Paddle动转静@to_static技术设计
一、整体概要
在深度学习模型构建上,飞桨框架支持动态图编程和静态图编程两种方式,其代码编写和执行方式均存在差异:
- 动态图编程: 采用 Python 的编程风格,解析式地执行每一行网络代码,并同时返回计算结果。
- 静态图编程: 采用先编译后执行的方式。需先在代码中预定义完整的神经网络结构,飞桨框架会将神经网络描述为 Program 的数据结构,并对 Program 进行编译优化,再调用执行器获得计算结果。
动态图编程体验更佳、更易调试,但是因为采用 Python 实时执行的方式,开销较大,在性能方面与 C++ 有一定差距;静态图调试难度大,但是将前端 Python 编写的神经网络预定义为 Program 描述,转到 C++ 端重新解析执行,脱离了 Python 依赖,往往执行性能更佳,并且预先拥有完整网络结构也更利于全局优化。
从2.0 版本开始,Paddle默认开启了动态图执行模式,Paddle提供了动转静(@to_static)模块功能支持用户实现动态图编程,一键切换静态图训练和部署的编程体验。
二、转换原理
在飞桨框架内部,动转静模块在转换上主要包括对输入数据 InputSpec 的处理,对函数调用的递归转写,对 IfElse、For、While 控制语句的转写,以及 Layer 的 Parameters 和 Buffers 变量的转换。如下是动转静模块的转换技术大致流程:
2.1 AST 解析动态图代码
当某个函数被 @to_static 装饰、或用 paddle.jit.to_static() 包裹时,飞桨会隐式地解析动态图的 Python 代码(即解析:抽象语法树,简称 AST)。
2.2 AST 转写,得到静态图代码
- 函数转写:递归地对所有函数进行转写,实现用户仅需在最外层函数添加 @to_static 的体验效果。
- 控制流转写:用户的代码中可能包含依赖 Tensor 的控制流代码,飞桨框架会自动且有选择性地将 if、for、while 转换为静态图对应的控制流。
- 其他语法处理:包括 break、continue、assert、提前 return 等语法的处理。
2.3 生成静态图的 Program 和 Parameters
- 得到静态图代码后,根据用户指定的 InputSpec 信息(或训练时根据实际输入 Tensor 隐式创建的 InputSpec)作为输入,执行静态图代码生成 Program。每个被装饰的函数,都会被替换为一个 StaticFunction 对象,其持有此函数对应的计算图 Program,在执行 paddle.jit.save 时会被用到。
- 对于 trainable=True 的 Buffers 变量,动转静会自动识别并将其和 Parameters 一起保存到 .pdiparams 文件中。
2.4 执行动转静训练
- 使用执行引擎执行函数对应的 Program,返回输出 out。
- 执行时会根据用户指定的 build_strategy 策略应用图优化技术,提升执行效率。
2.5使用 paddle.jit.save 保存静态图模型
- 使用 paddle.jit.save 时会遍历模型 net 中所有的函数,将每个的 StaticFunction 中的计算图 Program 和涉及到的 Parameters 序列化为磁盘文件。
三、转静组网流程
3.1 样例解读
import numpy as np
import paddle
import paddle.nn as nn
class LinearNet(paddle.nn.Layer):
def __init__(self):
super(LinearNet, self).__init__()
self._linear = nn.Linear(10, 3)
@paddle.jit.to_static
def forward(self, x):
y = self._linear(x)
return y
# create network
layer = LinearNet()
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())
for batch_id, x in enumerate(data_loader()):
out = layer(image)
loss = paddle.mean(out)
loss.backward()
opt.step()
opt.clear_grad()
文档开始的样例中 forward 函数包含一行组网代码: Linear 。以 Linear 为例,在 Paddle 的框架底层,每个 Paddle 的组网 API 的实现包括两个分支:
class Linear(...):
def __init__(self, ...):
# ...(略)
def forward(self, input):
if in_dygraph_mode(): # 动态图分支
core.ops.matmul(input, self.weight, pre_bias, ...)
return out
else: # 静态图分支
self._helper.append_op(type="matmul", inputs=inputs, ...) # <----- 生成一个 Op
if self.bias is not None:
self._helper.append_op(type='elementwise_add', ...) # <----- 生成一个 Op
return out
动态图 layer 生成 Program ,其实是开启 paddle.enable_static()
时,在静态图下逐行执行用户定义的组网代码,依次添加(对应append_op
接口) 到默认的主 Program(即 main_program ) 中。当调用 loss.backward()
函数时,飞桨框架会根据loss的计算路径,进行反向自动链式求导生成对应的反向静态图子图。
上面提到,所有的组网代码都会在静态图模式下执行,以生成完整的 Program 。但静态图 append_op 有一个前置条件必须满足:
- 前置条件:append_op() 时,所有的 inputs,outputs 必须都是静态图的 Variable 类型,不能是动态图的 Tensor 类型。
- 原因:静态图下,操作的都是描述类单元:计算相关的 OpDesc ,数据相关的 VarDesc 。可以分别简单地理解为 Program 中的 Op 和 Variable 。
因此,在动转静时,我们在需要在某个统一的入口处,将动态图 Layers 中 Tensor 类型(包含具体数据)的 Weight 、Bias 等变量转换为同名的静态图 Variable。
- ParamBase → Parameters
- VarBase → Variable
技术实现上,我们选取了框架层面给飞桨静态图 Program 添加算子的 append_op
函数作为类型转换的统一入口:即 Block.append_op
函数中,生成 Op 之前
def append_op(self, *args, **kwargs):
if in_dygraph_mode():
# ... (动态图分支)
else:
inputs=kwargs.get("inputs", None)
outputs=kwargs.get("outputs", None)
# param_guard 会确保将 Tensor 类型的 inputs 和 outputs 转为静态图 Variable
with param_guard(inputs), param_guard(outputs):
op = Operator(
block=self,
desc=op_desc,
type=kwargs.get("type", None),
inputs=inputs,
outputs=outputs,
attrs=kwargs.get("attrs", None))
3.2 一键递归转写
Python语言的灵活性对动转静模块要求极高。相对于静态图编程,动态图下完全继承了Python语言的灵活性,因此对动转静的语法功能实现要求很高,既要兼顾对原生Python语法的支持,也要保证静态图接口的正确转换,在API使用上要尽量减少用户使用的成本。飞桨扩展优化了动转静核心API@to_static接口功能,除了支持装饰器模式之外,并实现用户实现仅需一行代码即可一键递归转成静态图,极大的减少了用户动转静时的代码改写量,提升了功能的易用性和用户的使用体验。
一键递归转写,得益于飞桨动转静的自动递归转写技术组件。通过借助对Python抽象语法树(下简称:AST)的解析,感知用户的函数调用栈信息,逐层对内部嵌套函数进行动态解析和转写,模拟实现“自动递归”的效果。为了减少同一函数的重复转写,飞桨新引入了两级缓存机制:即函数转写缓存和Program转写缓存。
函数转写缓存指对于同一个函数,在第一次转写时会缓存转写结果,在出现函数重复调用时直接命中缓存,减少相同code的AST抽象语法树解析和转写开销,达到复用的效果;Program转写缓存指对于同一个模型在每轮迭代执行时,会自动根据输入张量的shape、dtype信息,缓存已转写的Program,避免训练时每个step重复转写Program。
四、 动转静训练
在飞桨框架中,通常情况下使用动态图训练,即可满足大部分场景需求。 飞桨经过多个版本的持续优化,动态图模型训练的性能已经可以和静态图媲美。如果在某些场景下确实需要使用静态图模式训练,则可以使用动转静训练功能,即仍然采用更易用的动态图编程,添加少量代码,便可在底层转为静态图训练。
当用户在组网入口的forward函数处添加装饰器@to_static,会将此函数内的所 有subLayers 转化为一个静态子图,并分别执行。
在如下场景时可以考虑使用动转静进行模型训练,带来的性能提升效果较明显:
- 如果发现模型训练 CPU 向 GPU 调度不充分的情况下。如下是模型训练时执行单个 step 的 timeline 示意图,框架通过 CPU 调度底层 Kernel 计算,在某些情况下,如果 CPU 调度时间过长,会导致 GPU 利用率不高(可终端执行
watch -n 1 nvidia-smi
观察)。
动态图和静态图在 CPU 调度层面存在差异:
- 动态图训练时,CPU 调度时间涉及 Python 到 C++ 的交互(Python 前端代码调起底层 C++ OP)和 C++ 代码调度;
- 静态图训练时,是统一编译 C++ 后执行,CPU 调度时间没有 Python 到 C++ 的交互时间,只有 C++ 代码调度,因此比动态图调度时间短。
因此如果发现是 CPU 调度时间过长,导致的 GPU 利用率低的情况,便可以采用动转静训练提升性能。从应用层面看,如果模型任务本身的 Kernel 计算时间很长,相对来说调度到 Kernel 拉起造成的影响不大,这种情况一般用动态图训练即可,比如 Bert 等模型,反之如 HRNet 等模型则可以观察 GPU 利用率来决定是否使用动转静训练。
如果想要进一步对计算图优化,以提升模型训练性能的情况下。相对于动态图按一行行代码解释执行,动转静后飞桨能够获取模型的整张计算图,即拥有了全局视野,因此可以借助算子融合等技术对计算图进行局部改写,替换为更高效的计算单元,我们称之为“图优化”。如下是应用了算子融合策略后,模型训练时执行单个 step 的 timeline 示意图。相对于图 2,飞桨框架获取了整张计算图,按照一定规则匹配到 OP3 和 OP4 可以融合为 Fuse_OP,因此可以减少 GPU 的空闲时间,提升执行效率。
五、 动转静导出部署
动转静模块是架在动态图与静态图的一个桥梁,旨在打破动态图模型训练与静态部署的鸿沟,消除部署时对模型代码的依赖,打通与预测端的交互逻辑。下图展示了动态图模型训练——>动转静模型导出——>静态预测部署的流程。
在处理逻辑上,动转静主要包含两个主要模块:
- 代码层面:将模型中所有的 layers 接口在静态图模式下执行以转为 Op ,从而生成完整的静态 Program
- Tensor层面:将所有的 Parameters 和 Buffers 转为可导出的 Variable 参数( persistable=True )
通过 forward 导出预测模型导出一般包括三个步骤:
- 切换 eval() 模式:类似 Dropout 、LayerNorm 等接口在 train() 和 eval() 的行为存在较大的差异,在模型导出前,请务必确认模型已切换到正确的模式,否则导出的模型在预测阶段可能出现输出结果不符合预期的情况。
- 构造 InputSpec 信息:InputSpec 用于表示输入的shape、dtype、name信息,且支持用 None 表示动态shape(如输入的 batch_size 维度),是辅助动静转换的必要描述信息。
- 调用 save 接口:调用 paddle.jit.save接口,若传入的参数是类实例,则默认对 forward 函数进行 @to_static 装饰,并导出其对应的模型文件和参数文件。
如下是一个简单的示例:
import paddle
from paddle.jit import to_static
from paddle.static import InputSpec
class SimpleNet(paddle.nn.Layer):
def __init__(self):
super(SimpleNet, self).__init__()
self.linear = paddle.nn.Linear(10, 3)
def forward(self, x, y):
out = self.linear(x)
out = out + y
return out
def another_func(self, x):
out = self.linear(x)
out = out * 2
return out
net = SimpleNet()
# train(net) 模型训练 (略)
# step 1: 切换到 eval() 模式
net.eval()
# step 2: 定义 InputSpec 信息
x_spec = InputSpec(shape=[None, 3], dtype='float32', name='x')
y_spec = InputSpec(shape=[3], dtype='float32', name='y')
# step 3: 调用 jit.save 接口
net = paddle.jit.save(net, path='simple_net', input_spec=[x_spec, y_spec]) # 动静转换
执行上述代码样例后,在当前目录下会生成三个文件,即代表成功导出预测模型:
simple_net.pdiparams // 存放模型中所有的权重数据
simple_net.pdmodel // 存放模型的网络结构
simple_net.pdiparams.info // 存放额外的其他信息
飞桨Paddle动转静@to_static技术设计的更多相关文章
- 【百度飞桨】手写数字识别模型部署Paddle Inference
从完成一个简单的『手写数字识别任务』开始,快速了解飞桨框架 API 的使用方法. 模型开发 『手写数字识别』是深度学习里的 Hello World 任务,用于对 0 ~ 9 的十类数字进行分类,即输入 ...
- 百度飞桨数据处理 API 数据格式 HWC CHW 和 PIL 图像处理之间的关系
使用百度飞桨 API 例如:Resize Normalize,处理数据的时候. Resize:如果输入的图像是 PIL 读取的图像这个数据格式是 HWC ,Resize 就需要 HWC 格式的数据. ...
- Ubuntu 百度飞桨和 CUDA 的安装
Ubuntu 百度飞桨 和 CUDA 的安装 1.简介 本文主要是 Ubuntu 百度飞桨 和 CUDA 的安装 系统:Ubuntu 20.04 百度飞桨:2.2 为例 2.百度飞桨安装 访问百度飞桨 ...
- 我做的百度飞桨PaddleOCR .NET调用库
我做的百度飞桨PaddleOCR .NET调用库 .NET Conf 2021中国我做了一次<.NET玩转计算机视觉OpenCV>的分享,其中提到了一个效果特别好的OCR识别引擎--百度飞 ...
- Nginx+Tomcat构建动、静分离WEB架构
一.简介 二.环境介绍 三.后端服务器安装配置 四.安装论坛 五.安装配置前端Nginx服务器 六.验证服务 一.Tomcat简介 Tomcat是Apache 软件基金会(Apache Softwar ...
- 提速1000倍,预测延迟少于1ms,百度飞桨发布基于ERNIE的语义理解开发套件
提速1000倍,预测延迟少于1ms,百度飞桨发布基于ERNIE的语义理解开发套件 11月5日,在『WAVE Summit+』2019 深度学习开发者秋季峰会上,百度对外发布基于 ERNIE 的语义理解 ...
- 树莓派4B安装 百度飞桨paddlelite 做视频检测 (一、环境安装)
前言: 当前准备重新在树莓派4B8G 上面搭载训练模型进行识别检测,训练采用了百度飞桨的PaddleX再也不用为训练部署环境各种报错发愁了,推荐大家使用. 关于在树莓派4B上面paddlelite的文 ...
- 【一】ERNIE:飞桨开源开发套件,入门学习,看看行业顶尖持续学习语义理解框架,如何取得世界多个实战的SOTA效果?
参考文章: 深度剖析知识增强语义表示模型--ERNIE_财神Childe的博客-CSDN博客_ernie模型 ERNIE_ERNIE开源开发套件_飞桨 https://github.com/Pad ...
- 飞桨paddlespeech语音唤醒推理C实现
上篇(飞桨paddlespeech 语音唤醒初探)初探了paddlespeech下的语音唤醒方案,通过调试也搞清楚了里面的细节.因为是python 下的,不能直接部署,要想在嵌入式上部署需要有C下的推 ...
- 虚拟机迁移(QEMU动态迁移,Libvirt动(静)态迁移)
动静态迁移的原理 静态迁移是指在虚拟机关闭或暂停的情况下,将源宿主机上虚拟机的磁盘文件和配置文件拷贝到目标宿主机上.这种方式需要显式的停止虚拟机运行,对服务可用性要求高的需求不合适. *** 动态迁移 ...
随机推荐
- 关系类处理专题之创建关系类|RelationShipClass
/// <summary> /// 存在于数据库中的数据集中 /// </summary> /// <param name="mdbPath"> ...
- vue 作者在2022-2-7起宣布 vue3 正式作为默认版本
vue 作者在2022-2-7起宣布 vue3 正式作为默认版本 vue 作者尤雨溪在知乎上发布一篇文章,宣布 Vue3 将在 2022 年 2 月 7 日 成为新的默认版本! 并且还在文章中做出了一 ...
- vue 纵向滑动模块
代码 <template> <div> <!-- 左侧的滑动模块 --> <div class="scroll-box" :style=& ...
- strtok实现想到的...
1.实现容易,实现的方法很难想到 比如strtok函数 /* 获取第一个子字符串 */ token = strtok(str, s); /* 继续获取其他的子字符串 */ while( token ! ...
- 【建造者设计模式详解】Java/JS/Go/Python/TS不同语言实现
简介 建造者模式(Builder Pattern),也叫生成器模式,属于创建型模式.它使用多个简单的对象一步一步构建成一个复杂的对象.它允许你使用相同的创建代码生成不同类型和形式的对象. 当你希望使用 ...
- Java所用相关软件的大致安装流程
JAVA下载流程 一.相关环境的安装与配置 1.JDK的下载 去官网搜索相应的java版本,并进行下载 官网链接:www.xfdown.com/soft/125774.html在该链接下,可以下载ja ...
- SQL统计(一)
参考博客: https://blog.csdn.net/GuTiDong/article/details/81326787 按月份统计每个月的订单总金额 https://blog.csdn.net/h ...
- springboot--多环境启动
法一: 法二:
- springboot 连接不上 redis 的三种解决方案!
针对于这种情况,首先,我们最简单直接的方法就是需要确认Redis是否已经正常启动(验证方法:如果安装在Linux下的话可以使用ps-ef|grep redis来进行确认是否开启) 如果未开启,我们可以 ...
- 痞子衡嵌入式:恩智浦经典LPC系列MCU内部Flash IAP驱动入门
大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家介绍的是恩智浦经典LPC系列MCU内部Flash IAP驱动. LPC 系列 MCU 是恩智浦公司于 2003 年开始推出的非常具有代表性的产品 ...