From 920fa1af4e55367c4538a4cd8e560a80510190d5 Mon Sep 17 00:00:00 2001 From: 0x45f Date: Wed, 17 Nov 2021 03:38:59 +0000 Subject: [PATCH 1/5] refine dy2stat docs --- .../04_dygraph_to_static/basic_usage_cn.md | 615 +++++++++--------- .../04_dygraph_to_static/case_analysis_cn.md | 48 +- .../04_dygraph_to_static/debugging_cn.md | 2 +- .../04_dygraph_to_static/export_model_cn.md | 472 -------------- .../04_dygraph_to_static/grammar_list_cn.md | 2 +- .../04_dygraph_to_static/principle_cn.md | 440 +++++++++++++ 6 files changed, 802 insertions(+), 777 deletions(-) delete mode 100644 docs/guides/04_dygraph_to_static/export_model_cn.md create mode 100644 docs/guides/04_dygraph_to_static/principle_cn.md diff --git a/docs/guides/04_dygraph_to_static/basic_usage_cn.md b/docs/guides/04_dygraph_to_static/basic_usage_cn.md index b3159c24a2a..7e5ce07052d 100644 --- a/docs/guides/04_dygraph_to_static/basic_usage_cn.md +++ b/docs/guides/04_dygraph_to_static/basic_usage_cn.md @@ -1,32 +1,59 @@ -# 基础接口用法 +# 使用样例 ## 一、 @to_static 概览 动静转换(@to_static)通过解析 Python 代码(抽象语法树,下简称:AST) 实现一行代码即可转为静态图功能,即只需在待转化的函数前添加一个装饰器 ``@paddle.jit.to_static`` 。 -如下是一个使用 @to_static 装饰器的 ``Model`` 示例: +如下是使用 @to_static 进行动静转换的两种方式: -```python -import paddle -from paddle.jit import to_static +- 方式一:使用 @to_static 装饰器装饰 ``Model`` 的 ``forward`` 函数 -class SimpleNet(paddle.nn.Layer): - def __init__(self): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) + ```python + import paddle + from paddle.jit import to_static + + class SimpleNet(paddle.nn.Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + # 方式一:装饰 forward 函数(支持训练) + @to_static + def forward(self, x, y): + out = self.linear(x) + out = out + y + return out - # 方式一:装饰 forward 函数(支持训练) - @to_static - def forward(self, x, y): - out = self.linear(x) - out = out + y - return out + net = SimpleNet() + x = paddle.rand([2, 10]) + y = paddle.rand([2, 3]) + out = net(x, y) + ``` -net = SimpleNet() -# 方式二:(推荐)仅做预测模型导出时,推荐此种用法 -net = paddle.jit.to_static(net) # 动静转换 -``` +- 方式二:调用 ``paddle.jit.to_static()`` 函数 + + ```python + import paddle + from paddle.jit import to_static + + 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 + + net = SimpleNet() + # 方式二:(推荐)仅做预测模型导出时,推荐此种用法 + net = paddle.jit.to_static(net) # 动静转换 + x = paddle.rand([2, 10]) + y = paddle.rand([2, 3]) + out = net(x, y) + ``` 动转静 @to_static 除了支持预测模型导出,还兼容转为静态图子图训练,仅需要在 ``forward`` 函数上添加此装饰器即可,不需要修改任何其他的代码。 @@ -36,440 +63,432 @@ net = paddle.jit.to_static(net) # 动静转换 -## 二、 输入层 InputSpec +## 二、动转静模型导出 +动转静模块**是架在动态图与静态图的一个桥梁**,旨在打破动态图与静态部署的鸿沟,消除部署时对模型代码的依赖,打通与预测端的交互逻辑。 -静态图下,模型起始的 Placeholder 信息是通过 ``paddle.static.data`` 来指定的,并以此作为编译期的 ``InferShape`` 推导起点。 + -```python -import paddle -# 开启静态图模式 -paddle.enable_static() -# placeholder 信息 -x = paddle.static.data(shape=[None, 10], dtype='float32', name='x') -y = paddle.static.data(shape=[None, 3], dtype='float32', name='y') -out = paddle.static.nn.fc(x, 3) -out = paddle.add(out, y) -``` +在处理逻辑上,动转静主要包含两个主要模块: ++ **代码层面**:将所有的 Paddle ``layers`` 接口在静态图模式下执行以转为 ``Op`` ,从而生成完整的静态 ``Program`` ++ **Tensor层面**:将所有的 ``Parameters`` 和 ``Buffers`` 转为**可导出的 ``Variable`` 参数**( ``persistable=True`` ) -动转静代码示例,通过 ``InputSpec`` 设置 ``Placeholder`` 信息: + +### 2.1 forward 函数导出 + +如下是一个简单的 ``Model`` 的代码: ```python 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) - # 方式一:在函数定义处装饰 - @to_static 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) 模型训练 (略) -# 方式二:(推荐)仅做预测模型导出时,推荐此种用法 -x_spec = InputSpec(shape=[None, 10], name='x') -y_spec = InputSpec(shape=[3], name='y') +# step 1: 切换到 eval() 模式 +net.eval() -net = paddle.jit.to_static(net, input_spec=[x_spec, y_spec]) # 动静转换 -``` +# 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]) # 动静转换 +``` -在导出模型时,需要显式地指定输入 ``Tensor`` 的**签名信息**,优势是: +执行上述代码样例后,在当前目录下会生成三个文件: +``` +simple_net.pdiparams // 存放模型中所有的权重数据 +simple_net.pdimodel // 存放模型的网络结构 +simple_net.pdiparams.info // 存放额外的其他信息 +``` -+ 可以指定某些维度为 ``None`` , 如 ``batch_size`` ,``seq_len`` 维度 -+ 可以指定 Placeholder 的 ``name`` ,方面预测时根据 ``name`` 输入数据 +预测模型导出一般包括三个步骤: -> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./export_model_cn.html#inputspec) ++ **切换 `eval()` 模式**:类似 `Dropout` 、`LayerNorm` 等接口在 `train()` 和 `eval()` 的行为存在较大的差异,在模型导出前,**请务必确认模型已切换到正确的模式,否则导出的模型在预测阶段可能出现输出结果不符合预期的情况。** ++ **构造 `InputSpec` 信息**:InputSpec 用于表示输入的shape、dtype、name信息,且支持用 `None` 表示动态shape(如输入的 batch_size 维度),是辅助动静转换的必要描述信息。 ++ **调用 `save` 接口**:调用 `paddle.jit.save`接口,若传入的参数是类实例,则默认对 `forward` 函数进行 `@to_static` 装饰,并导出其对应的模型文件和参数文件。 -## 三、函数转写 +### 2.2 InputSpec 功能介绍 -在 NLP、CV 领域中,一个模型常包含层层复杂的子函数调用,动转静中是如何实现**只需装饰最外层的 ``forward`` 函数**,就能递归处理所有的函数。 +动静转换在生成静态图 Program 时,依赖输入 Tensor 的 shape、dtype 和 name 信息。因此,Paddle 提供了 InputSpec 接口,用于指定输入 Tensor 的描述信息,并支持动态 shape 特性。 -如下是一个模型样例: -```python -import paddle -from paddle.jit import to_static +#### 2.2.1 InputSpec 构造 -class SimpleNet(paddle.nn.Layer): - def __init__(self): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) - @to_static - def forward(self, x, y): - out = self.my_fc(x) # <---- self.other_func - out = add_two(out, y) # <---- other plain func - return out +**方式一:直接构造** - def my_fc(self, x): - out = self.linear(x) - return out -# 此函数可以在任意文件 -def add_two(x, y): - out = x + y - return out +InputSpec 接口在 ``paddle.static`` 目录下, 只有 ``shape`` 是必须参数, ``dtype`` 和 ``name`` 可以缺省,默认取值分别为 ``float32`` 和 ``None`` 。使用样例如下: -net = SimpleNet() -# 查看转写的代码内容 -paddle.jit.set_code_level(100) +```python +from paddle.static import InputSpec -x = paddle.zeros([2,10], 'float32') -y = paddle.zeros([3], 'float32') +x = InputSpec([None, 784], 'float32', 'x') +label = InputSpec([None, 1], 'int64', 'label') -out = net(x, y) +print(x) # InputSpec(shape=(-1, 784), dtype=VarType.FP32, name=x) +print(label) # InputSpec(shape=(-1, 1), dtype=VarType.INT64, name=label) ``` -可以通过 ``paddle.jit.set_code_level(100)`` 在执行时打印代码转写的结果到终端,转写代码如下: - -```python -def forward(self, x, y): - out = paddle.jit.dy2static.convert_call(self.my_fc)(x) - out = paddle.jit.dy2static.convert_call(add_two)(out, y) - return out - -def my_fc(self, x): - out = paddle.jit.dy2static.convert_call(self.linear)(x) - return out - -def add_two(x, y): - out = x + y - return out -``` +**方式二:由 Tensor 构造** -如上所示,所有的函数调用都会被转写如下形式: +可以借助 ``InputSpec.from_tensor`` 方法,从一个 Tensor 直接创建 InputSpec 对象,其拥有与源 Tensor 相同的 ``shape`` 和 ``dtype`` 。 使用样例如下: ```python - out = paddle.jit.dy2static.convert_call( self.my_fc )( x ) - ^ ^ ^ ^ - | | | | -返回列表 convert_call 原始函数 参数列表 -``` - -即使函数定义分布在不同的文件中, ``convert_call`` 函数也会递归地处理和转写所有嵌套的子函数。 +import numpy as np +import paddle +from paddle.static import InputSpec -## 四、控制流转写 +x = paddle.to_tensor(np.ones([2, 2], np.float32)) +x_spec = InputSpec.from_tensor(x, name='x') +print(x_spec) # InputSpec(shape=(2, 2), dtype=VarType.FP32, name=x) +``` -控制流 ``if/for/while`` 的转写和处理是动转静中比较重要的模块,也是动态图模型和静态图模型实现上差别最大的一部分。 +> 注:若未在 ``from_tensor`` 中指定新的name,则默认使用与源Tensor相同的name。 -**转写上有两个基本原则:** -+ **并非**所有动态图中的 ``if/for/while`` 都会转写为 ``cond_op/while_op`` -+ **只有**控制流的判断条件 **依赖了``Tensor``**(如 ``shape`` 或 ``value`` ),才会转写为对应 Op +**方式三:由 numpy.ndarray** +也可以借助 ``InputSpec.from_numpy`` 方法,从一个 `Numpy.ndarray` 直接创建 InputSpec 对象,其拥有与源 ndarray 相同的 ``shape`` 和 ``dtype`` 。使用样例如下: - +```python +import numpy as np +from paddle.static import InputSpec +x = np.ones([2, 2], np.float32) +x_spec = InputSpec.from_numpy(x, name='x') +print(x_spec) # InputSpec(shape=(2, 2), dtype=VarType.FP32, name=x) +``` +> 注:若未在 ``from_numpy`` 中指定新的 name,则默认使用 None 。 -### 4.1 IfElse +#### 2.2.2 基本用法 -无论是否会转写为 ``cond_op`` ,动转静都会首先对代码进行处理,**转写为 ``cond`` 接口可以接受的写法** +**方式一: @to_static 装饰器模式** -**示例一:不依赖 Tensor 的控制流** +如下是一个简单的使用样例: ```python -def not_depend_tensor_if(x, label=None): - out = x + 1 - if label is not None: # <----- python bool 类型 - out = paddle.nn.functional.cross_entropy(out, label) - return out - -print(to_static(not_depend_tensor_ifw).code) -# 转写后的代码: -""" -def not_depend_tensor_if(x, label=None): - out = x + 1 - - def true_fn_1(label, out): # true 分支 - out = paddle.nn.functional.cross_entropy(out, label) - return out +import paddle +from paddle.jit import to_static +from paddle.static import InputSpec +from paddle.fluid.dygraph import Layer + +class SimpleNet(Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) - def false_fn_1(out): # false 分支 + @to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]) + def forward(self, x, y): + out = self.linear(x) + out = out + y return out - out = paddle.jit.dy2static.convert_ifelse(label is not None, true_fn_1, - false_fn_1, (label, out), (out,), (out,)) +net = SimpleNet() - return out -""" +# save static model for inference directly +paddle.jit.save(net, './simple_net') ``` +在上述的样例中, ``@to_static`` 装饰器中的 ``input_spec`` 为一个 InputSpec 对象组成的列表,用于依次指定参数 x 和 y 对应的 Tensor 签名信息。在实例化 SimpleNet 后,可以直接调用 ``paddle.jit.save`` 保存静态图模型,不需要执行任何其他的代码。 + +> 注: +> 1. input_spec 参数中不仅支持 InputSpec 对象,也支持 int 、 float 等常见 Python 原生类型。 +> 2. 若指定 input_spec 参数,则需为被装饰函数的所有必选参数都添加对应的 InputSpec 对象,如上述样例中,不支持仅指定 x 的签名信息。 +> 3. 若被装饰函数中包括非 Tensor 参数,推荐函数的非 Tensor 参数设置默认值,如 ``forward(self, x, use_bn=False)`` -**示例二:依赖 Tensor 的控制流** + +**方式二:to_static函数调用** + +若期望在动态图下训练模型,在训练完成后保存预测模型,并指定预测时需要的签名信息,则可以选择在保存模型时,直接调用 ``to_static`` 函数。使用样例如下: ```python -def depend_tensor_if(x): - if paddle.mean(x) > 5.: # <---- Bool Tensor 类型 - out = x - 1 - else: - out = x + 1 - return out - -print(to_static(depend_tensor_if).code) -# 转写后的代码: -""" -def depend_tensor_if(x): - out = paddle.jit.dy2static.data_layer_not_check(name='out', shape=[-1], - dtype='float32') - - def true_fn_0(x): # true 分支 - out = x - 1 - return out +class SimpleNet(Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) - def false_fn_0(x): # false 分支 - out = x + 1 + def forward(self, x, y): + out = self.linear(x) + out = out + y return out - out = paddle.jit.dy2static.convert_ifelse(paddle.mean(x) > 5.0, - true_fn_0, false_fn_0, (x,), (x,), (out,)) +net = SimpleNet() - return out -""" +# train process (Pseudo code) +for epoch_id in range(10): + train_step(net, train_reader) + +net = to_static(net, input_spec=[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]) + +# save static model for inference directly +paddle.jit.save(net, './simple_net') ``` +如上述样例代码中,在完成训练后,可以借助 ``to_static(net, input_spec=...)`` 形式对模型实例进行处理。Paddle 会根据 input_spec 信息对 forward 函数进行递归的动转静,得到完整的静态图,且包括当前训练好的参数数据。 + -规范化代码之后,所有的 ``IfElse`` 均转为了如下形式: +**方式三:支持 list 和 dict 推导** + +上述两个样例中,被装饰的 forward 函数的参数均为 Tensor 。这种情况下,参数个数必须与 InputSpec 个数相同。但当被装饰的函数参数为 list 或 dict 类型时,``input_spec`` 需要与函数参数保持相同的嵌套结构。 + +当函数的参数为 list 类型时,input_spec 列表中对应元素的位置,也必须是包含相同元素的 InputSpec 列表。使用样例如下: ```python - out = convert_ifelse(paddle.mean(x) > 5.0, true_fn_0, false_fn_0, (x,), (x,), (out,)) - ^ ^ ^ ^ ^ ^ ^ ^ - | | | | | | | | - 输出 convert_ifelse 判断条件 true分支 false分支 分支输入 分支输入 输出 +class SimpleNet(Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + @to_static(input_spec=[[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]]) + def forward(self, inputs): + x, y = inputs[0], inputs[1] + out = self.linear(x) + out = out + y + return out ``` +其中 ``input_spec`` 参数是长度为 1 的 list ,对应 forward 函数的 inputs 参数。 ``input_spec[0]`` 包含了两个 InputSpec 对象,对应于参数 inputs 的两个 Tensor 签名信息。 -``convert_ifelse`` 是框架底层的函数,在逐行执行用户代码生成 ``Program`` 时,执行到此处时,会根据**判断条件**的类型( ``bool`` 还是 ``Bool Tensor`` ),自适应决定是否转为 ``cond_op`` 。 +当函数的参数为dict时, ``input_spec`` 列表中对应元素的位置,也必须是包含相同键(key)的 InputSpec 列表。使用样例如下: ```python -def convert_ifelse(pred, true_fn, false_fn, true_args, false_args, return_vars): +class SimpleNet(Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) - if isinstance(pred, Variable): # 触发 cond_op 的转换 - return _run_paddle_cond(pred, true_fn, false_fn, true_args, false_args, - return_vars) - else: # 正常的 python if - return _run_py_ifelse(pred, true_fn, false_fn, true_args, false_args) + @to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), {'x': InputSpec(shape=[3], name='bias')}]) + def forward(self, x, bias_info): + x_bias = bias_info['x'] + out = self.linear(x) + out = out + x_bias + return out ``` +其中 ``input_spec`` 参数是长度为 2 的 list ,对应 forward 函数的 x 和 bias_info 两个参数。 ``input_spec`` 的最后一个元素是包含键名为 x 的 InputSpec 对象的 dict ,对应参数 bias_info 的 Tensor 签名信息。 -### 4.2 For/While -``For/While`` 也会先进行代码层面的规范化,在逐行执行用户代码时,才会决定是否转为 ``while_op``。 +**方式四:指定非Tensor参数类型** -**示例一:不依赖 Tensor 的控制流** +目前,``to_static`` 装饰器中的 ``input_spec`` 参数仅接收 ``InputSpec`` 类型对象。若被装饰函数的参数列表除了 Tensor 类型,还包含其他如 Int、 String 等非 Tensor 类型时,推荐在函数中使用 kwargs 形式定义非 Tensor 参数,如下述样例中的 use_act 参数。 ```python -def not_depend_tensor_while(x): - a = 1 - while a < 10: # <---- a is python scalar - x = x + 1 - a += 1 +class SimpleNet(Layer): + def __init__(self, ): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + self.relu = paddle.nn.ReLU() - return x + def forward(self, x, use_act=False): + out = self.linear(x) + if use_act: + out = self.relu(out) + return out -print(to_static(not_depend_tensor_while).code) -""" -def not_depend_tensor_while(x): - a = 1 +net = SimpleNet() +# 方式一:save inference model with use_act=False +net = to_static(input_spec=[InputSpec(shape=[None, 10], name='x')]) +paddle.jit.save(net, path='./simple_net') - def while_condition_0(a, x): - return a < 10 - def while_body_0(a, x): - x = x + 1 - a += 1 - return a, x +# 方式二:save inference model with use_act=True +net = to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), True]) +paddle.jit.save(net, path='./simple_net') +``` - [a, x] = paddle.jit.dy2static.convert_while_loop(while_condition_0, - while_body_0, [a, x]) - return x -""" -``` +在上述样例中,假设 step 为奇数时,use_act 取值为 False ; step 为偶数时, use_act 取值为 True 。动转静支持非 Tensor 参数在训练时取不同的值,且保证了取值不同的训练过程都可以更新模型的网络参数,行为与动态图一致。 +在借助 ``paddle.jit.save`` 保存预测模型时,动转静会根据 input_spec 和 kwargs 的默认值保存推理模型和网络参数。**建议将 kwargs 参数默认值设置为预测时的取值。** -**示例二:依赖 Tensor 的控制流** -```python -def depend_tensor_while(x): - bs = paddle.shape(x)[0] +更多关于动转静 ``to_static`` 搭配 ``paddle.jit.save/load`` 的使用方式,可以参考 [【模型的存储与载入】](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/02_paddle2.0_develop/08_model_save_load_cn.html)。 - for i in range(bs): # <---- bas is a Tensor - x = x + 1 - return x +## 三、动、静态图部署区别 -print(to_static(depend_tensor_while).code) -""" -def depend_tensor_while(x): - bs = paddle.shape(x)[0] - i = 0 +当训练完一个模型后,下一阶段就是保存导出,实现**模型**和**参数**的分发,进行多端部署。如下两小节,将介绍动态图和静态图的概念和差异性,以帮助理解动转静如何起到**桥梁作用**的。 +### 3.1 动态图预测部署 - def for_loop_condition_0(x, i, bs): - return i < bs +动态图下,**模型**指的是 Python 前端代码;**参数**指的是 ``model.state_dict()`` 中存放的权重数据。 + +```python +net = SimpleNet() - def for_loop_body_0(x, i, bs): - x = x + 1 - i += 1 - return x, i, bs +# .... 训练过程(略) - [x, i, bs] = paddle.jit.dy2static.convert_while_loop(for_loop_condition_0, - for_loop_body_0, [x, i, bs]) - return x -""" +layer_state_dict = net.state_dict() +paddle.save(layer_state_dict, "net.pdiparams") # 导出模型 ``` + -``convert_while_loop`` 的底层的逻辑同样会根据 **判断条件是否为``Tensor``** 来决定是否转为 ``while_op`` +即意味着,动态图预测部署时,除了已经序列化的参数文件,还须提供**最初的模型组网代码**。 -## 五、 Parameters 与 Buffers +在动态图下,模型代码是 **逐行被解释执行** 的。如: -### 5.1 动态图 layer 生成 Program +```python +import paddle -文档开始的样例中 ``forward`` 函数包含两行组网代码: ``Linear`` 和 ``add`` 操作。以 ``Linear`` 为例,在 Paddle 的框架底层,每个 Paddle 的组网 API 的实现包括两个分支: +zeros = paddle.zeros(shape=[1,2], dtype='float32') +print(zeros) -```python +#Tensor(shape=[1, 2], dtype=float32, place=CPUPlace, stop_gradient=True, +# [[0., 0.]]) +``` -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 +> 前端 zeros 接口 → core.ops.fill_constant (Pybind11) → 后端 Kernel → 前端 Tensor 输出 - return out -``` +如下是一个简单的 Model 示例: -动态图 ``layer`` 生成 ``Program`` ,其实是开启 ``paddle.enable_static()`` 时,在静态图下逐行执行用户定义的组网代码,依次添加(对应 ``append_op`` 接口) 到默认的主 Program(即 ``main_program`` ) 中。 +```python -### 5.2 动态图 Tensor 转为静态图 Variable +import paddle -上面提到,所有的组网代码都会在静态图模式下执行,以生成完整的 ``Program`` 。**但静态图 ``append_op`` 有一个前置条件必须满足:** +class SimpleNet(paddle.nn.Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) -> **前置条件**:append_op() 时,所有的 inputs,outputs 必须都是静态图的 Variable 类型,不能是动态图的 Tensor 类型。 + def forward(self, x, y): + out = self.linear(x) + out = out + y + return out +net = SimpleNet() +``` -**原因**:静态图下,操作的都是**描述类单元**:计算相关的 ``OpDesc`` ,数据相关的 ``VarDesc`` 。可以分别简单地理解为 ``Program`` 中的 ``Op`` 和 ``Variable`` 。 +动态图下,当实例化一个 ``SimpleNet()`` 对象时,隐式地执行了如下几个步骤: -因此,在动转静时,我们在需要在**某个统一的入口处**,将动态图 ``Layers`` 中 ``Tensor`` 类型(包含具体数据)的 ``Weight`` 、``Bias`` 等变量转换为**同名的静态图 ``Variable``**。 ++ 创建一个 ``Linear`` 对象,记录到 ``self._sub_layer`` 中(dict 类型) -+ ParamBase → Parameters -+ VarBase → Variable + + 创建一个 ``ParamBase`` 类型的 ``weight`` ,记录到 ``self._parameters`` 中(dict类型) + + 创建一个 ``ParamBase`` 类型的 ``bias`` ,记录到 ``self._parameters`` 中 -技术实现上,我们选取了框架层面两个地方作为类型**转换的入口**: +一个复杂模型可能包含很多子类,框架层就是通过 ``self._sub_layer`` 和 ``self._parameters`` 两个核心数据结构关联起来的,这也是后续动转静原理上操作的两个核心属性。 -+ ``Paddle.nn.Layer`` 基类的 ``__call__`` 函数 - ```python - def __call__(self, *inputs, **kwargs): - # param_guard 会对将 Tensor 类型的 Param 和 buffer 转为静态图 Variable - with param_guard(self._parameters), param_guard(self._buffers): - # ... forward_pre_hook 逻辑 +```python +sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=net.parameters()) + ^ + | + 所有待更新参数 +``` - outputs = self.forward(*inputs, **kwargs) # 此处为forward函数 +### 3.2 静态图预测部署 - # ... forward_post_hook 逻辑 +静态图部署时,**模型**指的是 ``Program`` ;参数指的是所有的 ``Persistable=True`` 的 ``Variable`` 。二者都可以序列化导出为磁盘文件,**与前端代码完全解耦**。 - return outputs - ``` +```python +main_program = paddle.static.default_main_program() -+ ``Block.append_op`` 函数中,生成 ``Op`` 之前 - ```python - 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)) - ``` +# ...... 训练过程(略) +prog_path='main_program.pdimodel' +paddle.save(main_program, prog_path) # 导出为 .pdimodel -以上,是动态图转为静态图的两个核心逻辑,总结如下: +para_path='main_program.pdiparams' +paddle.save(main_program.state_dict(), para_path) # 导出为 .pdiparams +``` -+ 动态图 ``layer`` 调用在动转静时会走底层 ``append_op`` 的分支,以生成 ``Program`` -+ 动态图 ``Tensor`` 转为静态图 ``Variable`` ,并确保编译期的 ``InferShape`` 正确执行 + -### 5.3 Buffer 变量 +即意味着, ``Program`` 中包含了模型所有的计算描述( ``OpDesc`` ),不存在计算逻辑有遗漏的地方。 -**什么是 ``Buffers`` 变量?** -+ **Parameters**:``persistable`` 为 ``True`` ,且每个 batch 都被 Optimizer 更新的变量 -+ **Buffers**:``persistable`` 为 ``True`` ,``is_trainable = False`` ,不参与更新,但与预测相关;如 ``BatchNorm`` 层中的均值和方差 +**静态图编程,总体上包含两个部分:** -在动态图模型代码中,若一个 ``paddle.to_tensor`` 接口生成的 ``Tensor`` 参与了最终预测结果的的计算,则此 ``Tensor`` 需要在转换为静态图预测模型时,也需要作为一个 ``persistable`` 的变量保存到 ``.pdiparam`` 文件中。 ++ **编译期**:组合各个 ``Layer`` 接口,搭建网络结构,执行每个 Op 的 ``InferShape`` 逻辑,最终生成 ``Program`` ++ **执行期**:构建执行器,输入数据,依次执行每个 ``OpKernel`` ,进行训练和评估 -**举一个例子(错误写法):** +在静态图编译期,变量 ``Variable`` 只是**一个符号化表示**,并不像动态图 ``Tensor`` 那样持有实际数据。 ```python import paddle -from paddle.jit import to_static +# 开启静态图模式 +paddle.enable_static() -class SimpleNet(paddle.nn.Layer): - def __init__(self, mask): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) +zeros = paddle.zeros(shape=[1,2], dtype='float32') +print(zeros) +# var fill_constant_1.tmp_0 : LOD_TENSOR.shape(1, 2).dtype(float32).stop_gradient(True) +``` - # mask value,此处不会保存到预测模型文件中 - self.mask = mask # 假设为 [0, 1, 1] +**从框架层面上,静态图的调用链:** - def forward(self, x, y): - out = self.linear(x) - out = out + y - mask = paddle.to_tensor(self.mask) # <----- 每次执行都转为一个 Tensor - out = out * mask - return out -``` +> layer 组网(前端) → InferShape 检查(编译期) → Executor(执行期) → 逐个执行 OP -**推荐的写法是:** +如下是 ``SimpleNet`` 的静态图模式下的组网代码: ```python -class SimpleNet(paddle.nn.Layer): - def __init__(self, mask): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) +import paddle +# 开启静态图模式 +paddle.enable_static() - # 此处的 mask 会当做一个 buffer Tensor,保存到 .pdiparam 文件 - self.mask = paddle.to_tensor(mask) # 假设为 [0, 1, 1] +# placeholder 信息 +x = paddle.static.data(shape=[None, 10], dtype='float32', name='x') +y = paddle.static.data(shape=[None, 3], dtype='float32', name='y') - def forward(self, x, y): - out = self.linear(x) - out = out + y - out = out * self.mask # <---- 直接使用 self.mask - return out +out = paddle.static.nn.fc(x, 3) +out = paddle.add(out, y) +# 打印查看 Program 信息 +print(paddle.static.default_main_program()) + +# { // block 0 +# var x : LOD_TENSOR.shape(-1, 10).dtype(float32).stop_gradient(True) +# var y : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(True) +# persist trainable param fc_0.w_0 : LOD_TENSOR.shape(10, 3).dtype(float32).stop_gradient(False) +# var fc_0.tmp_0 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) +# persist trainable param fc_0.b_0 : LOD_TENSOR.shape(3,).dtype(float32).stop_gradient(False) +# var fc_0.tmp_1 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) +# var elementwise_add_0 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) + +# {Out=['fc_0.tmp_0']} = mul(inputs={X=['x'], Y=['fc_0.w_0']}, force_fp32_output = False, op_device = , op_namescope = /, op_role = 0, op_role_var = [], scale_out = 1.0, scale_x = 1.0, scale_y = [1.0], use_mkldnn = False, x_num_col_dims = 1, y_num_col_dims = 1) +# {Out=['fc_0.tmp_1']} = elementwise_add(inputs={X=['fc_0.tmp_0'], Y=['fc_0.b_0']}, Scale_out = 1.0, Scale_x = 1.0, Scale_y = 1.0, axis = 1, mkldnn_data_type = float32, op_device = , op_namescope = /, op_role = 0, op_role_var = [], use_mkldnn = False, use_quantizer = False, x_data_format = , y_data_format = ) +# {Out=['elementwise_add_0']} = elementwise_add(inputs={X=['fc_0.tmp_1'], Y=['y']}, Scale_out = 1.0, Scale_x = 1.0, Scale_y = 1.0, axis = -1, mkldnn_data_type = float32, op_device = , op_namescope = /, op_role = 0, op_role_var = [], use_mkldnn = False, use_quantizer = False, x_data_format = , y_data_format = ) +} ``` -总结一下 ``buffers`` 的用法: +静态图中的一些概念: + ++ **Program**:与 ``Model`` 对应,描述网络的整体结构,内含一个或多个 ``Block`` ++ **Block** + + **global_block**:全局 ``Block`` ,包含所有 ``Parameters`` 、全部 ``Ops`` 和 ``Variables`` + + **sub_block**:控制流,包含控制流分支内的所有 ``Ops`` 和必要的 ``Variables`` ++ **OpDesc**:对应每个前端 API 的计算逻辑描述 ++ **Variable**:对应所有的数据变量,如 ``Parameter`` ,临时中间变量等,全局唯一 ``name`` 。 + + -+ 若某个非 ``Tensor`` 数据需要当做 ``Persistable`` 的变量序列化到磁盘,则最好在 ``__init__`` 中调用 ``self.XX= paddle.to_tensor(xx)`` 接口转为 ``buffer`` 变量 +> 注:更多细节,请参考 [【官方文档】模型的存储与载入](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/02_paddle2.0_develop/08_model_save_load_cn.html)。 diff --git a/docs/guides/04_dygraph_to_static/case_analysis_cn.md b/docs/guides/04_dygraph_to_static/case_analysis_cn.md index 4f1d09cf0fc..9a56f7de66f 100644 --- a/docs/guides/04_dygraph_to_static/case_analysis_cn.md +++ b/docs/guides/04_dygraph_to_static/case_analysis_cn.md @@ -1,4 +1,4 @@ -# 常见案例解析 +# 案例解析 在[【基础接口用法】](./basic_usage_cn.html)章节我们介绍了动转静的用法和机制,下面会结合一些具体的模型代码,解答动转静中比较常见的问题。 @@ -273,7 +273,45 @@ jit.save(mode, model_path) 此 flag 继承自 ``nn.Layer`` ,因此可通过 ``model.train()`` 和 ``model.eval()`` 来全局切换所有 sublayers 的分支状态。 -## 七、再谈控制流 +## 七、非forward函数导出 + +`@to_static` 与 `jit.save` 接口搭配也支持导出非forward 的其他函数,具体使用方式如下: + +```python +# SimpleNet 类的定义见 1.1 + +net = SimpleNet() +# train(net) # 模型训练 + +# step 1: 切换到 eval() 模式 (同上) +net.eval() + +# step 2: 定义 InputSpec 信息 (同上) +x_spec = InputSpec(shape=[None, 3], dtype='float32', name='x') + +# step 3: @to_static 装饰 +static_func = to_static(net.another_func, input_spec=[x_spec]) + +# step 4: 调用 jit.save 接口 +net = paddle.jit.save(static_func, path='another_func') +``` + +使用上的区别主要在于: + ++ **`@to_static` 装饰**:导出其他函数时需要显式地用 `@to_static` 装饰,以告知动静转换模块将其识别、并转为静态图 Program; ++ **`save`接口参数**:调用`jit.save`接口时,需将上述被`@to_static` 装饰后的函数作为**参数**; + +执行上述代码样例后,在当前目录下会生成三个文件: +``` +another_func.pdiparams // 存放模型中所有的权重数据 +another_func.pdimodel // 存放模型的网络结构 +another_func.pdiparams.info // 存放额外的其他信息 +``` + + +> 关于动转静 @to_static 的用法,可以参考 [基本用法](./basic_usage_cn.html);搭配 `paddle.jit.save` 接口导出预测模型的用法案例,可以参考 [案例解析](./case_analysis_cn.html) 。 + +## 八、再谈控制流 前面[【控制流转写】(./basic_usage_cn.html#sikongzhiliuzhuanxie)]提到,不论控制流 ``if/for/while`` 语句是否需要转为静态图中的 ``cond_op/while_op`` ,都会先进行代码规范化,如 ``IfElse`` 语句会规范为如下范式: @@ -293,7 +331,7 @@ out = convert_ifelse(paddle.mean(x) > 5.0, true_fn_0, false_fn_0, (x,), (x,), (o ``` -### 7.1 list 与 LoDTensorArray +### 8.1 list 与 LoDTensorArray 当控制流中,出现了 ``list.append`` 类似语法时,情况会有一点点特殊。 @@ -359,7 +397,7 @@ def forward(x): > 因为框架底层的 ``LoDTensorArray = std::vector< LoDTensor >`` ,不支持两层以上 ``vector`` 嵌套 -### 7.2 x.shape 与 paddle.shape(x) +### 8.2 x.shape 与 paddle.shape(x) 模型中比较常见的控制流转写大多数与 ``batch_size`` 或者 ``x.shape`` 相关。 @@ -381,7 +419,7 @@ def forward(self, x): > 动态 shape 推荐使用 ``paddle.shape(x)[i]`` ,动转静也对 ``x.shape[i]`` 做了很多兼容处理。前者写法出错率可能更低些。 -## 八、jit.save 与默认参数 +## 九、jit.save 与默认参数 最后一步是预测模型的导出,Paddle 提供了 ``paddle.jit.save`` 接口,搭配 ``@to_static`` 可以导出预测模型。 diff --git a/docs/guides/04_dygraph_to_static/debugging_cn.md b/docs/guides/04_dygraph_to_static/debugging_cn.md index 6b53dd82235..2fc3cf77d86 100644 --- a/docs/guides/04_dygraph_to_static/debugging_cn.md +++ b/docs/guides/04_dygraph_to_static/debugging_cn.md @@ -1,4 +1,4 @@ -# 报错调试经验 +# 报错调试 ## 一、动转静报错日志 ### 1.1 错误日志怎么看 diff --git a/docs/guides/04_dygraph_to_static/export_model_cn.md b/docs/guides/04_dygraph_to_static/export_model_cn.md deleted file mode 100644 index 44e5dea3c73..00000000000 --- a/docs/guides/04_dygraph_to_static/export_model_cn.md +++ /dev/null @@ -1,472 +0,0 @@ -# 预测模型导出 - - -## 一、动转静模型导出 - -动转静模块**是架在动态图与静态图的一个桥梁**,旨在打破动态图与静态部署的鸿沟,消除部署时对模型代码的依赖,打通与预测端的交互逻辑。 - - - - - -在处理逻辑上,动转静主要包含两个主要模块: - -+ **代码层面**:将所有的 Paddle ``layers`` 接口在静态图模式下执行以转为 ``Op`` ,从而生成完整的静态 ``Program`` -+ **Tensor层面**:将所有的 ``Parameters`` 和 ``Buffers`` 转为**可导出的 ``Variable`` 参数**( ``persistable=True`` ) - - -### 1.1 forward 函数导出 - -如下是一个简单的 ``Model`` 的代码: - -```python -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.pdimodel // 存放模型的网络结构 -simple_net.pdiparams.info // 存放额外的其他信息 -``` - - -预测模型导出一般包括三个步骤: - -+ **切换 `eval()` 模式**:类似 `Dropout` 、`LayerNorm` 等接口在 `train()` 和 `eval()` 的行为存在较大的差异,在模型导出前,**请务必确认模型已切换到正确的模式,否则导出的模型在预测阶段可能出现输出结果不符合预期的情况。** -+ **构造 `InputSpec` 信息**:InputSpec 用于表示输入的shape、dtype、name信息,且支持用 `None` 表示动态shape(如输入的 batch_size 维度),是辅助动静转换的必要描述信息。 -+ **调用 `save` 接口**:调用 `paddle.jit.save`接口,若传入的参数是类实例,则默认对 `forward` 函数进行 `@to_static` 装饰,并导出其对应的模型文件和参数文件。 - - -### 1.2 其他函数导出 - -`@to_static` 与 `jit.save` 接口搭配也支持导出非forward 的其他函数,具体使用方式如下: - -```python -# SimpleNet 类的定义见 1.1 - -net = SimpleNet() -# train(net) # 模型训练 - -# step 1: 切换到 eval() 模式 (同上) -net.eval() - -# step 2: 定义 InputSpec 信息 (同上) -x_spec = InputSpec(shape=[None, 3], dtype='float32', name='x') - -# step 3: @to_static 装饰 -static_func = to_static(net.another_func, input_spec=[x_spec]) - -# step 4: 调用 jit.save 接口 -net = paddle.jit.save(static_func, path='another_func') -``` - -使用上的区别主要在于: - -+ **`@to_static` 装饰**:导出其他函数时需要显式地用 `@to_static` 装饰,以告知动静转换模块将其识别、并转为静态图 Program; -+ **`save`接口参数**:调用`jit.save`接口时,需将上述被`@to_static` 装饰后的函数作为**参数**; - -执行上述代码样例后,在当前目录下会生成三个文件: -``` -another_func.pdiparams // 存放模型中所有的权重数据 -another_func.pdimodel // 存放模型的网络结构 -another_func.pdiparams.info // 存放额外的其他信息 -``` - - -> 关于动转静 @to_static 的用法,可以参考 [基本用法](./basic_usage_cn.html);搭配 `paddle.jit.save` 接口导出预测模型的用法案例,可以参考 [案例解析](./case_analysis_cn.html) 。 - - -### 1.3 InputSpec 功能介绍 - -动静转换在生成静态图 Program 时,依赖输入 Tensor 的 shape、dtype 和 name 信息。因此,Paddle 提供了 InputSpec 接口,用于指定输入 Tensor 的描述信息,并支持动态 shape 特性。 - - -#### 1.3.1 InputSpec 构造 - - -**方式一:直接构造** - - -InputSpec 接口在 ``paddle.static`` 目录下, 只有 ``shape`` 是必须参数, ``dtype`` 和 ``name`` 可以缺省,默认取值分别为 ``float32`` 和 ``None`` 。使用样例如下: - -```python -from paddle.static import InputSpec - -x = InputSpec([None, 784], 'float32', 'x') -label = InputSpec([None, 1], 'int64', 'label') - -print(x) # InputSpec(shape=(-1, 784), dtype=VarType.FP32, name=x) -print(label) # InputSpec(shape=(-1, 1), dtype=VarType.INT64, name=label) -``` - - -**方式二:由 Tensor 构造** - -可以借助 ``InputSpec.from_tensor`` 方法,从一个 Tensor 直接创建 InputSpec 对象,其拥有与源 Tensor 相同的 ``shape`` 和 ``dtype`` 。 使用样例如下: - -```python -import numpy as np -import paddle -from paddle.static import InputSpec - -x = paddle.to_tensor(np.ones([2, 2], np.float32)) -x_spec = InputSpec.from_tensor(x, name='x') -print(x_spec) # InputSpec(shape=(2, 2), dtype=VarType.FP32, name=x) -``` - -> 注:若未在 ``from_tensor`` 中指定新的name,则默认使用与源Tensor相同的name。 - - -**方式三:由 numpy.ndarray** - -也可以借助 ``InputSpec.from_numpy`` 方法,从一个 `Numpy.ndarray` 直接创建 InputSpec 对象,其拥有与源 ndarray 相同的 ``shape`` 和 ``dtype`` 。使用样例如下: - -```python -import numpy as np -from paddle.static import InputSpec - -x = np.ones([2, 2], np.float32) -x_spec = InputSpec.from_numpy(x, name='x') -print(x_spec) # InputSpec(shape=(2, 2), dtype=VarType.FP32, name=x) -``` - -> 注:若未在 ``from_numpy`` 中指定新的 name,则默认使用 None 。 - - -#### 1.3.2 基本用法 - -**方式一: @to_static 装饰器模式** - -如下是一个简单的使用样例: - -```python -import paddle -from paddle.jit import to_static -from paddle.static import InputSpec -from paddle.fluid.dygraph import Layer - -class SimpleNet(Layer): - def __init__(self): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) - - @to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]) - def forward(self, x, y): - out = self.linear(x) - out = out + y - return out - -net = SimpleNet() - -# save static model for inference directly -paddle.jit.save(net, './simple_net') -``` - -在上述的样例中, ``@to_static`` 装饰器中的 ``input_spec`` 为一个 InputSpec 对象组成的列表,用于依次指定参数 x 和 y 对应的 Tensor 签名信息。在实例化 SimpleNet 后,可以直接调用 ``paddle.jit.save`` 保存静态图模型,不需要执行任何其他的代码。 - -> 注: -> 1. input_spec 参数中不仅支持 InputSpec 对象,也支持 int 、 float 等常见 Python 原生类型。 -> 2. 若指定 input_spec 参数,则需为被装饰函数的所有必选参数都添加对应的 InputSpec 对象,如上述样例中,不支持仅指定 x 的签名信息。 -> 3. 若被装饰函数中包括非 Tensor 参数,推荐函数的非 Tensor 参数设置默认值,如 ``forward(self, x, use_bn=False)`` - - -**方式二:to_static函数调用** - -若期望在动态图下训练模型,在训练完成后保存预测模型,并指定预测时需要的签名信息,则可以选择在保存模型时,直接调用 ``to_static`` 函数。使用样例如下: - -```python -class SimpleNet(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 - -net = SimpleNet() - -# train process (Pseudo code) -for epoch_id in range(10): - train_step(net, train_reader) - -net = to_static(net, input_spec=[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]) - -# save static model for inference directly -paddle.jit.save(net, './simple_net') -``` - -如上述样例代码中,在完成训练后,可以借助 ``to_static(net, input_spec=...)`` 形式对模型实例进行处理。Paddle 会根据 input_spec 信息对 forward 函数进行递归的动转静,得到完整的静态图,且包括当前训练好的参数数据。 - - -**方式三:支持 list 和 dict 推导** - -上述两个样例中,被装饰的 forward 函数的参数均为 Tensor 。这种情况下,参数个数必须与 InputSpec 个数相同。但当被装饰的函数参数为 list 或 dict 类型时,``input_spec`` 需要与函数参数保持相同的嵌套结构。 - -当函数的参数为 list 类型时,input_spec 列表中对应元素的位置,也必须是包含相同元素的 InputSpec 列表。使用样例如下: - -```python -class SimpleNet(Layer): - def __init__(self): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) - - @to_static(input_spec=[[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')]]) - def forward(self, inputs): - x, y = inputs[0], inputs[1] - out = self.linear(x) - out = out + y - return out -``` - -其中 ``input_spec`` 参数是长度为 1 的 list ,对应 forward 函数的 inputs 参数。 ``input_spec[0]`` 包含了两个 InputSpec 对象,对应于参数 inputs 的两个 Tensor 签名信息。 - -当函数的参数为dict时, ``input_spec`` 列表中对应元素的位置,也必须是包含相同键(key)的 InputSpec 列表。使用样例如下: - -```python -class SimpleNet(Layer): - def __init__(self): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) - - @to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), {'x': InputSpec(shape=[3], name='bias')}]) - def forward(self, x, bias_info): - x_bias = bias_info['x'] - out = self.linear(x) - out = out + x_bias - return out -``` - -其中 ``input_spec`` 参数是长度为 2 的 list ,对应 forward 函数的 x 和 bias_info 两个参数。 ``input_spec`` 的最后一个元素是包含键名为 x 的 InputSpec 对象的 dict ,对应参数 bias_info 的 Tensor 签名信息。 - - -**方式四:指定非Tensor参数类型** - -目前,``to_static`` 装饰器中的 ``input_spec`` 参数仅接收 ``InputSpec`` 类型对象。若被装饰函数的参数列表除了 Tensor 类型,还包含其他如 Int、 String 等非 Tensor 类型时,推荐在函数中使用 kwargs 形式定义非 Tensor 参数,如下述样例中的 use_act 参数。 - -```python - -class SimpleNet(Layer): - def __init__(self, ): - super(SimpleNet, self).__init__() - self.linear = paddle.nn.Linear(10, 3) - self.relu = paddle.nn.ReLU() - - def forward(self, x, use_act=False): - out = self.linear(x) - if use_act: - out = self.relu(out) - return out - -net = SimpleNet() -# 方式一:save inference model with use_act=False -net = to_static(input_spec=[InputSpec(shape=[None, 10], name='x')]) -paddle.jit.save(net, path='./simple_net') - - -# 方式二:save inference model with use_act=True -net = to_static(input_spec=[InputSpec(shape=[None, 10], name='x'), True]) -paddle.jit.save(net, path='./simple_net') -``` - - -在上述样例中,假设 step 为奇数时,use_act 取值为 False ; step 为偶数时, use_act 取值为 True 。动转静支持非 Tensor 参数在训练时取不同的值,且保证了取值不同的训练过程都可以更新模型的网络参数,行为与动态图一致。 - -在借助 ``paddle.jit.save`` 保存预测模型时,动转静会根据 input_spec 和 kwargs 的默认值保存推理模型和网络参数。**建议将 kwargs 参数默认值设置为预测时的取值。** - - -更多关于动转静 ``to_static`` 搭配 ``paddle.jit.save/load`` 的使用方式,可以参考 [【模型的存储与载入】](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/02_paddle2.0_develop/08_model_save_load_cn.html)。 - - -## 二、动、静态图部署区别 - -当训练完一个模型后,下一阶段就是保存导出,实现**模型**和**参数**的分发,进行多端部署。如下两小节,将介绍动态图和静态图的概念和差异性,以帮助理解动转静如何起到**桥梁作用**的。 -### 2.1 动态图预测部署 - -动态图下,**模型**指的是 Python 前端代码;**参数**指的是 ``model.state_dict()`` 中存放的权重数据。 - -```python -net = SimpleNet() - -# .... 训练过程(略) - -layer_state_dict = net.state_dict() -paddle.save(layer_state_dict, "net.pdiparams") # 导出模型 -``` - - - -即意味着,动态图预测部署时,除了已经序列化的参数文件,还须提供**最初的模型组网代码**。 - -在动态图下,模型代码是 **逐行被解释执行** 的。如: - -```python -import paddle - -zeros = paddle.zeros(shape=[1,2], dtype='float32') -print(zeros) - -#Tensor(shape=[1, 2], dtype=float32, place=CPUPlace, stop_gradient=True, -# [[0., 0.]]) -``` - - -**从框架层面上,上述的调用链是:** - -> 前端 zeros 接口 → core.ops.fill_constant (Pybind11) → 后端 Kernel → 前端 Tensor 输出 - -如下是一个简单的 Model 示例: - -```python - -import paddle - -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 - -net = SimpleNet() -``` - -动态图下,当实例化一个 ``SimpleNet()`` 对象时,隐式地执行了如下几个步骤: - -+ 创建一个 ``Linear`` 对象,记录到 ``self._sub_layer`` 中(dict 类型) - - + 创建一个 ``ParamBase`` 类型的 ``weight`` ,记录到 ``self._parameters`` 中(dict类型) - + 创建一个 ``ParamBase`` 类型的 ``bias`` ,记录到 ``self._parameters`` 中 - -一个复杂模型可能包含很多子类,框架层就是通过 ``self._sub_layer`` 和 ``self._parameters`` 两个核心数据结构关联起来的,这也是后续动转静原理上操作的两个核心属性。 - -```python -sgd = paddle.optimizer.SGD(learning_rate=0.1, parameters=net.parameters()) - ^ - | - 所有待更新参数 -``` - -### 2.2 静态图预测部署 - -静态图部署时,**模型**指的是 ``Program`` ;参数指的是所有的 ``Persistable=True`` 的 ``Variable`` 。二者都可以序列化导出为磁盘文件,**与前端代码完全解耦**。 - -```python -main_program = paddle.static.default_main_program() - -# ...... 训练过程(略) - -prog_path='main_program.pdimodel' -paddle.save(main_program, prog_path) # 导出为 .pdimodel - -para_path='main_program.pdiparams' -paddle.save(main_program.state_dict(), para_path) # 导出为 .pdiparams -``` - - - - -即意味着, ``Program`` 中包含了模型所有的计算描述( ``OpDesc`` ),不存在计算逻辑有遗漏的地方。 - - -**静态图编程,总体上包含两个部分:** - -+ **编译期**:组合各个 ``Layer`` 接口,搭建网络结构,执行每个 Op 的 ``InferShape`` 逻辑,最终生成 ``Program`` -+ **执行期**:构建执行器,输入数据,依次执行每个 ``OpKernel`` ,进行训练和评估 - -在静态图编译期,变量 ``Variable`` 只是**一个符号化表示**,并不像动态图 ``Tensor`` 那样持有实际数据。 - -```python -import paddle -# 开启静态图模式 -paddle.enable_static() - -zeros = paddle.zeros(shape=[1,2], dtype='float32') -print(zeros) -# var fill_constant_1.tmp_0 : LOD_TENSOR.shape(1, 2).dtype(float32).stop_gradient(True) -``` - -**从框架层面上,静态图的调用链:** - -> layer 组网(前端) → InferShape 检查(编译期) → Executor(执行期) → 逐个执行 OP - - -如下是 ``SimpleNet`` 的静态图模式下的组网代码: - -```python -import paddle -# 开启静态图模式 -paddle.enable_static() - -# placeholder 信息 -x = paddle.static.data(shape=[None, 10], dtype='float32', name='x') -y = paddle.static.data(shape=[None, 3], dtype='float32', name='y') - -out = paddle.static.nn.fc(x, 3) -out = paddle.add(out, y) -# 打印查看 Program 信息 -print(paddle.static.default_main_program()) - -# { // block 0 -# var x : LOD_TENSOR.shape(-1, 10).dtype(float32).stop_gradient(True) -# var y : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(True) -# persist trainable param fc_0.w_0 : LOD_TENSOR.shape(10, 3).dtype(float32).stop_gradient(False) -# var fc_0.tmp_0 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) -# persist trainable param fc_0.b_0 : LOD_TENSOR.shape(3,).dtype(float32).stop_gradient(False) -# var fc_0.tmp_1 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) -# var elementwise_add_0 : LOD_TENSOR.shape(-1, 3).dtype(float32).stop_gradient(False) - -# {Out=['fc_0.tmp_0']} = mul(inputs={X=['x'], Y=['fc_0.w_0']}, force_fp32_output = False, op_device = , op_namescope = /, op_role = 0, op_role_var = [], scale_out = 1.0, scale_x = 1.0, scale_y = [1.0], use_mkldnn = False, x_num_col_dims = 1, y_num_col_dims = 1) -# {Out=['fc_0.tmp_1']} = elementwise_add(inputs={X=['fc_0.tmp_0'], Y=['fc_0.b_0']}, Scale_out = 1.0, Scale_x = 1.0, Scale_y = 1.0, axis = 1, mkldnn_data_type = float32, op_device = , op_namescope = /, op_role = 0, op_role_var = [], use_mkldnn = False, use_quantizer = False, x_data_format = , y_data_format = ) -# {Out=['elementwise_add_0']} = elementwise_add(inputs={X=['fc_0.tmp_1'], Y=['y']}, Scale_out = 1.0, Scale_x = 1.0, Scale_y = 1.0, axis = -1, mkldnn_data_type = float32, op_device = , op_namescope = /, op_role = 0, op_role_var = [], use_mkldnn = False, use_quantizer = False, x_data_format = , y_data_format = ) -} -``` - - -静态图中的一些概念: - -+ **Program**:与 ``Model`` 对应,描述网络的整体结构,内含一个或多个 ``Block`` -+ **Block** - + **global_block**:全局 ``Block`` ,包含所有 ``Parameters`` 、全部 ``Ops`` 和 ``Variables`` - + **sub_block**:控制流,包含控制流分支内的所有 ``Ops`` 和必要的 ``Variables`` -+ **OpDesc**:对应每个前端 API 的计算逻辑描述 -+ **Variable**:对应所有的数据变量,如 ``Parameter`` ,临时中间变量等,全局唯一 ``name`` 。 - - - -> 注:更多细节,请参考 [【官方文档】模型的存储与载入](https://www.paddlepaddle.org.cn/documentation/docs/zh/develop/guides/02_paddle2.0_develop/08_model_save_load_cn.html)。 diff --git a/docs/guides/04_dygraph_to_static/grammar_list_cn.md b/docs/guides/04_dygraph_to_static/grammar_list_cn.md index a82c4a47163..54a5d559ea2 100644 --- a/docs/guides/04_dygraph_to_static/grammar_list_cn.md +++ b/docs/guides/04_dygraph_to_static/grammar_list_cn.md @@ -1,4 +1,4 @@ -# 语法支持列表 +# 支持语法 ## 一、主要针对场景 diff --git a/docs/guides/04_dygraph_to_static/principle_cn.md b/docs/guides/04_dygraph_to_static/principle_cn.md new file mode 100644 index 00000000000..9c0541c7367 --- /dev/null +++ b/docs/guides/04_dygraph_to_static/principle_cn.md @@ -0,0 +1,440 @@ +# 转换原理 + + +## 一、 输入层 InputSpec + + +静态图下,模型起始的 Placeholder 信息是通过 ``paddle.static.data`` 来指定的,并以此作为编译期的 ``InferShape`` 推导起点。 + +```python +import paddle +# 开启静态图模式 +paddle.enable_static() + +# placeholder 信息 +x = paddle.static.data(shape=[None, 10], dtype='float32', name='x') +y = paddle.static.data(shape=[None, 3], dtype='float32', name='y') + +out = paddle.static.nn.fc(x, 3) +out = paddle.add(out, y) +``` + + +动转静代码示例,通过 ``InputSpec`` 设置 ``Placeholder`` 信息: + +```python +import paddle +from paddle.jit import to_static + +class SimpleNet(paddle.nn.Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + # 方式一:在函数定义处装饰 + @to_static + def forward(self, x, y): + out = self.linear(x) + out = out + y + return out + +net = SimpleNet() + +# 方式二:(推荐)仅做预测模型导出时,推荐此种用法 +x_spec = InputSpec(shape=[None, 10], name='x') +y_spec = InputSpec(shape=[3], name='y') + +net = paddle.jit.to_static(net, input_spec=[x_spec, y_spec]) # 动静转换 +``` + + +在导出模型时,需要显式地指定输入 ``Tensor`` 的**签名信息**,优势是: + + ++ 可以指定某些维度为 ``None`` , 如 ``batch_size`` ,``seq_len`` 维度 ++ 可以指定 Placeholder 的 ``name`` ,方面预测时根据 ``name`` 输入数据 + +> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./export_model_cn.html#inputspec) + + +## 二、函数转写 + +在 NLP、CV 领域中,一个模型常包含层层复杂的子函数调用,动转静中是如何实现**只需装饰最外层的 ``forward`` 函数**,就能递归处理所有的函数。 + +如下是一个模型样例: + +```python +import paddle +from paddle.jit import to_static + +class SimpleNet(paddle.nn.Layer): + def __init__(self): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + @to_static + def forward(self, x, y): + out = self.my_fc(x) # <---- self.other_func + out = add_two(out, y) # <---- other plain func + return out + + def my_fc(self, x): + out = self.linear(x) + return out + +# 此函数可以在任意文件 +def add_two(x, y): + out = x + y + return out + +net = SimpleNet() +# 查看转写的代码内容 +paddle.jit.set_code_level(100) + +x = paddle.zeros([2,10], 'float32') +y = paddle.zeros([3], 'float32') + +out = net(x, y) +``` + +可以通过 ``paddle.jit.set_code_level(100)`` 在执行时打印代码转写的结果到终端,转写代码如下: + +```python +def forward(self, x, y): + out = paddle.jit.dy2static.convert_call(self.my_fc)(x) + out = paddle.jit.dy2static.convert_call(add_two)(out, y) + return out + +def my_fc(self, x): + out = paddle.jit.dy2static.convert_call(self.linear)(x) + return out + +def add_two(x, y): + out = x + y + return out +``` + + +如上所示,所有的函数调用都会被转写如下形式: + +```python + out = paddle.jit.dy2static.convert_call( self.my_fc )( x ) + ^ ^ ^ ^ + | | | | +返回列表 convert_call 原始函数 参数列表 +``` + +即使函数定义分布在不同的文件中, ``convert_call`` 函数也会递归地处理和转写所有嵌套的子函数。 + +## 三、控制流转写 + +控制流 ``if/for/while`` 的转写和处理是动转静中比较重要的模块,也是动态图模型和静态图模型实现上差别最大的一部分。 + +**转写上有两个基本原则:** + ++ **并非**所有动态图中的 ``if/for/while`` 都会转写为 ``cond_op/while_op`` ++ **只有**控制流的判断条件 **依赖了``Tensor``**(如 ``shape`` 或 ``value`` ),才会转写为对应 Op + + + + + + +### 3.1 IfElse + +无论是否会转写为 ``cond_op`` ,动转静都会首先对代码进行处理,**转写为 ``cond`` 接口可以接受的写法** + +**示例一:不依赖 Tensor 的控制流** + +```python +def not_depend_tensor_if(x, label=None): + out = x + 1 + if label is not None: # <----- python bool 类型 + out = paddle.nn.functional.cross_entropy(out, label) + return out + +print(to_static(not_depend_tensor_ifw).code) +# 转写后的代码: +""" +def not_depend_tensor_if(x, label=None): + out = x + 1 + + def true_fn_1(label, out): # true 分支 + out = paddle.nn.functional.cross_entropy(out, label) + return out + + def false_fn_1(out): # false 分支 + return out + + out = paddle.jit.dy2static.convert_ifelse(label is not None, true_fn_1, + false_fn_1, (label, out), (out,), (out,)) + + return out +""" +``` + + +**示例二:依赖 Tensor 的控制流** + +```python +def depend_tensor_if(x): + if paddle.mean(x) > 5.: # <---- Bool Tensor 类型 + out = x - 1 + else: + out = x + 1 + return out + +print(to_static(depend_tensor_if).code) +# 转写后的代码: +""" +def depend_tensor_if(x): + out = paddle.jit.dy2static.data_layer_not_check(name='out', shape=[-1], + dtype='float32') + + def true_fn_0(x): # true 分支 + out = x - 1 + return out + + def false_fn_0(x): # false 分支 + out = x + 1 + return out + + out = paddle.jit.dy2static.convert_ifelse(paddle.mean(x) > 5.0, + true_fn_0, false_fn_0, (x,), (x,), (out,)) + + return out +""" +``` + + +规范化代码之后,所有的 ``IfElse`` 均转为了如下形式: + +```python + out = convert_ifelse(paddle.mean(x) > 5.0, true_fn_0, false_fn_0, (x,), (x,), (out,)) + ^ ^ ^ ^ ^ ^ ^ ^ + | | | | | | | | + 输出 convert_ifelse 判断条件 true分支 false分支 分支输入 分支输入 输出 +``` + + +``convert_ifelse`` 是框架底层的函数,在逐行执行用户代码生成 ``Program`` 时,执行到此处时,会根据**判断条件**的类型( ``bool`` 还是 ``Bool Tensor`` ),自适应决定是否转为 ``cond_op`` 。 + +```python +def convert_ifelse(pred, true_fn, false_fn, true_args, false_args, return_vars): + + if isinstance(pred, Variable): # 触发 cond_op 的转换 + return _run_paddle_cond(pred, true_fn, false_fn, true_args, false_args, + return_vars) + else: # 正常的 python if + return _run_py_ifelse(pred, true_fn, false_fn, true_args, false_args) +``` + + +### 3.2 For/While + +``For/While`` 也会先进行代码层面的规范化,在逐行执行用户代码时,才会决定是否转为 ``while_op``。 + +**示例一:不依赖 Tensor 的控制流** + +```python +def not_depend_tensor_while(x): + a = 1 + + while a < 10: # <---- a is python scalar + x = x + 1 + a += 1 + + return x + +print(to_static(not_depend_tensor_while).code) +""" +def not_depend_tensor_while(x): + a = 1 + + def while_condition_0(a, x): + return a < 10 + + def while_body_0(a, x): + x = x + 1 + a += 1 + return a, x + + [a, x] = paddle.jit.dy2static.convert_while_loop(while_condition_0, + while_body_0, [a, x]) + + return x +""" +``` + + +**示例二:依赖 Tensor 的控制流** + +```python +def depend_tensor_while(x): + bs = paddle.shape(x)[0] + + for i in range(bs): # <---- bas is a Tensor + x = x + 1 + + return x + +print(to_static(depend_tensor_while).code) +""" +def depend_tensor_while(x): + bs = paddle.shape(x)[0] + i = 0 + + def for_loop_condition_0(x, i, bs): + return i < bs + + def for_loop_body_0(x, i, bs): + x = x + 1 + i += 1 + return x, i, bs + + [x, i, bs] = paddle.jit.dy2static.convert_while_loop(for_loop_condition_0, + for_loop_body_0, [x, i, bs]) + return x +""" +``` + + +``convert_while_loop`` 的底层的逻辑同样会根据 **判断条件是否为``Tensor``** 来决定是否转为 ``while_op`` + +## 四、 Parameters 与 Buffers + +### 4.1 动态图 layer 生成 Program + +文档开始的样例中 ``forward`` 函数包含两行组网代码: ``Linear`` 和 ``add`` 操作。以 ``Linear`` 为例,在 Paddle 的框架底层,每个 Paddle 的组网 API 的实现包括两个分支: + +```python + +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`` ) 中。 + +### 4.2 动态图 Tensor 转为静态图 Variable + +上面提到,所有的组网代码都会在静态图模式下执行,以生成完整的 ``Program`` 。**但静态图 ``append_op`` 有一个前置条件必须满足:** + +> **前置条件**:append_op() 时,所有的 inputs,outputs 必须都是静态图的 Variable 类型,不能是动态图的 Tensor 类型。 + + +**原因**:静态图下,操作的都是**描述类单元**:计算相关的 ``OpDesc`` ,数据相关的 ``VarDesc`` 。可以分别简单地理解为 ``Program`` 中的 ``Op`` 和 ``Variable`` 。 + +因此,在动转静时,我们在需要在**某个统一的入口处**,将动态图 ``Layers`` 中 ``Tensor`` 类型(包含具体数据)的 ``Weight`` 、``Bias`` 等变量转换为**同名的静态图 ``Variable``**。 + ++ ParamBase → Parameters ++ VarBase → Variable + +技术实现上,我们选取了框架层面两个地方作为类型**转换的入口**: + ++ ``Paddle.nn.Layer`` 基类的 ``__call__`` 函数 + ```python + def __call__(self, *inputs, **kwargs): + # param_guard 会对将 Tensor 类型的 Param 和 buffer 转为静态图 Variable + with param_guard(self._parameters), param_guard(self._buffers): + # ... forward_pre_hook 逻辑 + + outputs = self.forward(*inputs, **kwargs) # 此处为forward函数 + + # ... forward_post_hook 逻辑 + + return outputs + ``` + ++ ``Block.append_op`` 函数中,生成 ``Op`` 之前 + ```python + 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)) + ``` + + +以上,是动态图转为静态图的两个核心逻辑,总结如下: + ++ 动态图 ``layer`` 调用在动转静时会走底层 ``append_op`` 的分支,以生成 ``Program`` ++ 动态图 ``Tensor`` 转为静态图 ``Variable`` ,并确保编译期的 ``InferShape`` 正确执行 + + +### 4.3 Buffer 变量 + +**什么是 ``Buffers`` 变量?** + ++ **Parameters**:``persistable`` 为 ``True`` ,且每个 batch 都被 Optimizer 更新的变量 ++ **Buffers**:``persistable`` 为 ``True`` ,``is_trainable = False`` ,不参与更新,但与预测相关;如 ``BatchNorm`` 层中的均值和方差 + +在动态图模型代码中,若一个 ``paddle.to_tensor`` 接口生成的 ``Tensor`` 参与了最终预测结果的的计算,则此 ``Tensor`` 需要在转换为静态图预测模型时,也需要作为一个 ``persistable`` 的变量保存到 ``.pdiparam`` 文件中。 + +**举一个例子(错误写法):** + +```python +import paddle +from paddle.jit import to_static + +class SimpleNet(paddle.nn.Layer): + def __init__(self, mask): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + # mask value,此处不会保存到预测模型文件中 + self.mask = mask # 假设为 [0, 1, 1] + + def forward(self, x, y): + out = self.linear(x) + out = out + y + mask = paddle.to_tensor(self.mask) # <----- 每次执行都转为一个 Tensor + out = out * mask + return out +``` + + +**推荐的写法是:** + +```python +class SimpleNet(paddle.nn.Layer): + def __init__(self, mask): + super(SimpleNet, self).__init__() + self.linear = paddle.nn.Linear(10, 3) + + # 此处的 mask 会当做一个 buffer Tensor,保存到 .pdiparam 文件 + self.mask = paddle.to_tensor(mask) # 假设为 [0, 1, 1] + + def forward(self, x, y): + out = self.linear(x) + out = out + y + out = out * self.mask # <---- 直接使用 self.mask + return out +``` + + +总结一下 ``buffers`` 的用法: + ++ 若某个非 ``Tensor`` 数据需要当做 ``Persistable`` 的变量序列化到磁盘,则最好在 ``__init__`` 中调用 ``self.XX= paddle.to_tensor(xx)`` 接口转为 ``buffer`` 变量 From 95ef376fcf5f099327b365a53dfef8987b5baed4 Mon Sep 17 00:00:00 2001 From: 0x45f Date: Wed, 17 Nov 2021 05:49:20 +0000 Subject: [PATCH 2/5] fix index_cn.rst --- docs/guides/04_dygraph_to_static/index_cn.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/guides/04_dygraph_to_static/index_cn.rst b/docs/guides/04_dygraph_to_static/index_cn.rst index b54141fa883..e3d976d0c52 100644 --- a/docs/guides/04_dygraph_to_static/index_cn.rst +++ b/docs/guides/04_dygraph_to_static/index_cn.rst @@ -8,15 +8,15 @@ PaddlePaddle 在2.0版本之后,正式支持动态图转静态图(@to_static 如下将详细地介绍动静转换的各个模块内容: -- `基础接口用法 `_ : 介绍了动静转换 @to_static 的基本用法 +- `使用样例 `_ : 介绍了动静转换 @to_static 的基本用法 -- `语法支持列表 `_ :介绍了动静转换功能已支持的语法概况 +- `转换原理 `_ :介绍了动静转换的内部原理 -- `预测模型导出 <./export_model/index_cn.html>`_ :介绍了导出动态图预测模型的详细教程 +- `支持语法 `_ :介绍了动静转换功能已支持的语法概况 -- `常见案例解析 <./case_analysis_cn.html>`_ : 介绍使用 @to_static 时常见的问题和案例解析 +- `案例解析 <./case_analysis_cn.html>`_ : 介绍使用 @to_static 时常见的问题和案例解析 -- `报错调试经验 `_ :介绍了动静转换 @to_static 的调试方法和经验 +- `报错调试 `_ :介绍了动静转换 @to_static 的调试方法和经验 @@ -24,8 +24,8 @@ PaddlePaddle 在2.0版本之后,正式支持动态图转静态图(@to_static :hidden: basic_usage_cn.rst + principle_cn.md grammar_list_cn.md - export_model_cn.md case_analysis_cn.md debugging_cn.md From e453616ffa1c7e01588879197319f7239ec1a364 Mon Sep 17 00:00:00 2001 From: 0x45f Date: Wed, 17 Nov 2021 07:04:16 +0000 Subject: [PATCH 3/5] fix code review --- .../04_dygraph_to_static/basic_usage_cn.md | 4 ++++ .../04_dygraph_to_static/case_analysis_cn.md | 21 +++++++++++++++---- .../04_dygraph_to_static/grammar_list_cn.md | 6 ++---- .../04_dygraph_to_static/principle_cn.md | 3 ++- 4 files changed, 25 insertions(+), 9 deletions(-) diff --git a/docs/guides/04_dygraph_to_static/basic_usage_cn.md b/docs/guides/04_dygraph_to_static/basic_usage_cn.md index 7e5ce07052d..287bb9016c3 100644 --- a/docs/guides/04_dygraph_to_static/basic_usage_cn.md +++ b/docs/guides/04_dygraph_to_static/basic_usage_cn.md @@ -26,9 +26,11 @@ return out net = SimpleNet() + net.eval() x = paddle.rand([2, 10]) y = paddle.rand([2, 3]) out = net(x, y) + paddle.jit.save(net, './net') ``` - 方式二:调用 ``paddle.jit.to_static()`` 函数 @@ -48,11 +50,13 @@ return out net = SimpleNet() + net.eval() # 方式二:(推荐)仅做预测模型导出时,推荐此种用法 net = paddle.jit.to_static(net) # 动静转换 x = paddle.rand([2, 10]) y = paddle.rand([2, 3]) out = net(x, y) + paddle.jit.save(net, './net') ``` 动转静 @to_static 除了支持预测模型导出,还兼容转为静态图子图训练,仅需要在 ``forward`` 函数上添加此装饰器即可,不需要修改任何其他的代码。 diff --git a/docs/guides/04_dygraph_to_static/case_analysis_cn.md b/docs/guides/04_dygraph_to_static/case_analysis_cn.md index 9a56f7de66f..a8f0988a214 100644 --- a/docs/guides/04_dygraph_to_static/case_analysis_cn.md +++ b/docs/guides/04_dygraph_to_static/case_analysis_cn.md @@ -1,7 +1,7 @@ # 案例解析 -在[【基础接口用法】](./basic_usage_cn.html)章节我们介绍了动转静的用法和机制,下面会结合一些具体的模型代码,解答动转静中比较常见的问题。 +在[【使用样例】](./basic_usage_cn.html)章节我们介绍了动转静的用法和机制,下面会结合一些具体的模型代码,解答动转静中比较常见的问题。 ## 一、 @to_static 放在哪里? @@ -77,7 +77,7 @@ -> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./input_spec_cn.html#inputspec) +> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./basic_usage_cn.html#inputspec) ## 三、内嵌 Numpy 操作? @@ -278,7 +278,20 @@ jit.save(mode, model_path) `@to_static` 与 `jit.save` 接口搭配也支持导出非forward 的其他函数,具体使用方式如下: ```python -# SimpleNet 类的定义见 1.1 +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) # 模型训练 @@ -309,7 +322,7 @@ another_func.pdiparams.info // 存放额外的其他信息 ``` -> 关于动转静 @to_static 的用法,可以参考 [基本用法](./basic_usage_cn.html);搭配 `paddle.jit.save` 接口导出预测模型的用法案例,可以参考 [案例解析](./case_analysis_cn.html) 。 +> 关于动转静 @to_static 的用法,以及搭配 `paddle.jit.save` 接口导出预测模型的用法案例,可以参考 [使用样例](./basic_usage_cn.html) 。 ## 八、再谈控制流 diff --git a/docs/guides/04_dygraph_to_static/grammar_list_cn.md b/docs/guides/04_dygraph_to_static/grammar_list_cn.md index 54a5d559ea2..ced8b5df3a3 100644 --- a/docs/guides/04_dygraph_to_static/grammar_list_cn.md +++ b/docs/guides/04_dygraph_to_static/grammar_list_cn.md @@ -11,11 +11,9 @@ 3. 当出现不支持的语法时,如何修改源码适配动转静语法 -若您初次接触动转静功能,或对此功能尚不熟悉,推荐您阅读:[基础接口用法](./basic_usage_cn.html); +若您初次接触动转静功能,或对此功能尚不熟悉,推荐您阅读:[使用样例](./basic_usage_cn.html); -若您想进行预测模型导出,或想深入了解此模块,推荐您阅读:[预测模型导出](./export_model_cn.html); - -若您动静转换遇到了问题,或想学习调试的技巧,推荐您阅读:[报错调试经验](./debugging_cn.html)。 +若您动静转换遇到了问题,或想学习调试的技巧,推荐您阅读:[报错调试](./debugging_cn.html)。 ## 二、语法支持速查列表 diff --git a/docs/guides/04_dygraph_to_static/principle_cn.md b/docs/guides/04_dygraph_to_static/principle_cn.md index 9c0541c7367..a773b801f43 100644 --- a/docs/guides/04_dygraph_to_static/principle_cn.md +++ b/docs/guides/04_dygraph_to_static/principle_cn.md @@ -1,5 +1,6 @@ # 转换原理 +在框架内部,动转静模块在转换上主要包括对InputSpec的处理,对函数调用的递归转写,对IfElse、For、While控制语句的转写,以及Layer的Parameters和Buffers变量的转换。下面将从这四个方面介绍动转静模块的转换原理。 ## 一、 输入层 InputSpec @@ -54,7 +55,7 @@ net = paddle.jit.to_static(net, input_spec=[x_spec, y_spec]) # 动静转换 + 可以指定某些维度为 ``None`` , 如 ``batch_size`` ,``seq_len`` 维度 + 可以指定 Placeholder 的 ``name`` ,方面预测时根据 ``name`` 输入数据 -> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./export_model_cn.html#inputspec) +> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./basic_usage_cn.html#inputspec) ## 二、函数转写 From 18d93ee91dd9f7fc394df498d21401d0362806c0 Mon Sep 17 00:00:00 2001 From: 0x45f Date: Mon, 22 Nov 2021 08:42:20 +0000 Subject: [PATCH 4/5] modify the explanation for imgs --- .../04_dygraph_to_static/basic_usage_cn.md | 21 +++++++------------ .../04_dygraph_to_static/case_analysis_cn.md | 2 +- .../04_dygraph_to_static/principle_cn.md | 8 +++---- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/docs/guides/04_dygraph_to_static/basic_usage_cn.md b/docs/guides/04_dygraph_to_static/basic_usage_cn.md index 287bb9016c3..07fb0f8b864 100644 --- a/docs/guides/04_dygraph_to_static/basic_usage_cn.md +++ b/docs/guides/04_dygraph_to_static/basic_usage_cn.md @@ -1,7 +1,7 @@ # 使用样例 -## 一、 @to_static 概览 +## 一、 使用 @to_static 进行动静转换 动静转换(@to_static)通过解析 Python 代码(抽象语法树,下简称:AST) 实现一行代码即可转为静态图功能,即只需在待转化的函数前添加一个装饰器 ``@paddle.jit.to_static`` 。 @@ -18,8 +18,7 @@ super(SimpleNet, self).__init__() self.linear = paddle.nn.Linear(10, 3) - # 方式一:装饰 forward 函数(支持训练) - @to_static + @to_static # 动静转换 def forward(self, x, y): out = self.linear(x) out = out + y @@ -33,7 +32,7 @@ paddle.jit.save(net, './net') ``` -- 方式二:调用 ``paddle.jit.to_static()`` 函数 +- 方式二:调用 ``paddle.jit.to_static()`` 函数,仅做预测模型导出时推荐此种用法。 ```python import paddle @@ -51,7 +50,6 @@ net = SimpleNet() net.eval() - # 方式二:(推荐)仅做预测模型导出时,推荐此种用法 net = paddle.jit.to_static(net) # 动静转换 x = paddle.rand([2, 10]) y = paddle.rand([2, 3]) @@ -59,9 +57,7 @@ paddle.jit.save(net, './net') ``` -动转静 @to_static 除了支持预测模型导出,还兼容转为静态图子图训练,仅需要在 ``forward`` 函数上添加此装饰器即可,不需要修改任何其他的代码。 - -基本执行流程如下: +动转静 @to_static 除了支持预测模型导出,还兼容转为静态图子图训练,仅需要在 ``forward`` 函数上添加此装饰器即可,不需要修改任何其他的代码。基本执行流程如下: @@ -69,7 +65,7 @@ ## 二、动转静模型导出 -动转静模块**是架在动态图与静态图的一个桥梁**,旨在打破动态图与静态部署的鸿沟,消除部署时对模型代码的依赖,打通与预测端的交互逻辑。 +动转静模块**是架在动态图与静态图的一个桥梁**,旨在打破动态图与静态部署的鸿沟,消除部署时对模型代码的依赖,打通与预测端的交互逻辑。下图展示了**动态图模型训练——>动转静模型导出——>静态预测部署**的流程。 @@ -134,7 +130,7 @@ simple_net.pdiparams.info // 存放额外的其他信息 + **调用 `save` 接口**:调用 `paddle.jit.save`接口,若传入的参数是类实例,则默认对 `forward` 函数进行 `@to_static` 装饰,并导出其对应的模型文件和参数文件。 -### 2.2 InputSpec 功能介绍 +### 2.2 使用 InputSpec 指定模型输入 Tensor 信息 动静转换在生成静态图 Program 时,依赖输入 Tensor 的 shape、dtype 和 name 信息。因此,Paddle 提供了 InputSpec 接口,用于指定输入 Tensor 的描述信息,并支持动态 shape 特性。 @@ -354,7 +350,7 @@ paddle.save(layer_state_dict, "net.pdiparams") # 导出模型 -即意味着,动态图预测部署时,除了已经序列化的参数文件,还须提供**最初的模型组网代码**。 +上图展示了动态图下**模型训练——>参数导出——>预测部署**的流程。如图中所示,动态图预测部署时,除了已经序列化的参数文件,还须提供**最初的模型组网代码**。 在动态图下,模型代码是 **逐行被解释执行** 的。如: @@ -426,8 +422,7 @@ paddle.save(main_program.state_dict(), para_path) # 导出为 .pdiparams - -即意味着, ``Program`` 中包含了模型所有的计算描述( ``OpDesc`` ),不存在计算逻辑有遗漏的地方。 +上图展示了静态图下**模型训练——>模型导出——>预测部署**的流程。如图所示,静态图模型导出时将``Program``和模型参数都导出为磁盘文件,``Program`` 中包含了模型所有的计算描述( ``OpDesc`` ),不存在计算逻辑有遗漏的地方。 **静态图编程,总体上包含两个部分:** diff --git a/docs/guides/04_dygraph_to_static/case_analysis_cn.md b/docs/guides/04_dygraph_to_static/case_analysis_cn.md index a8f0988a214..4afbf9ab093 100644 --- a/docs/guides/04_dygraph_to_static/case_analysis_cn.md +++ b/docs/guides/04_dygraph_to_static/case_analysis_cn.md @@ -77,7 +77,7 @@ -> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./basic_usage_cn.html#inputspec) +> 注:InputSpec 接口的高阶用法,请参看 [【使用InputSpec指定模型输入Tensor信息】](./basic_usage_cn.html#inputspec) ## 三、内嵌 Numpy 操作? diff --git a/docs/guides/04_dygraph_to_static/principle_cn.md b/docs/guides/04_dygraph_to_static/principle_cn.md index a773b801f43..a9e464c21b2 100644 --- a/docs/guides/04_dygraph_to_static/principle_cn.md +++ b/docs/guides/04_dygraph_to_static/principle_cn.md @@ -55,7 +55,7 @@ net = paddle.jit.to_static(net, input_spec=[x_spec, y_spec]) # 动静转换 + 可以指定某些维度为 ``None`` , 如 ``batch_size`` ,``seq_len`` 维度 + 可以指定 Placeholder 的 ``name`` ,方面预测时根据 ``name`` 输入数据 -> 注:InputSpec 接口的高阶用法,请参看 [【InputSpec 功能介绍】](./basic_usage_cn.html#inputspec) +> 注:InputSpec 接口的高阶用法,请参看 [【使用InputSpec指定模型输入Tensor信息】](./basic_usage_cn.html#inputspec) ## 二、函数转写 @@ -129,7 +129,9 @@ def add_two(x, y): ## 三、控制流转写 -控制流 ``if/for/while`` 的转写和处理是动转静中比较重要的模块,也是动态图模型和静态图模型实现上差别最大的一部分。 +控制流 ``if/for/while`` 的转写和处理是动转静中比较重要的模块,也是动态图模型和静态图模型实现上差别最大的一部分。如下图所示,对于控制流的转写分为两个阶段:转写期和执行期。在转写期,动转静模块将控制流语句转写为统一的形式;在执行期,根据控制流是否依赖 ``Tensor`` 来决定是否将控制流转写为相应的 ``cond_op/while_op`` 。 + + **转写上有两个基本原则:** @@ -137,8 +139,6 @@ def add_two(x, y): + **只有**控制流的判断条件 **依赖了``Tensor``**(如 ``shape`` 或 ``value`` ),才会转写为对应 Op - - ### 3.1 IfElse From b5466dba99cab5b93a146b9cea284180ffd192cd Mon Sep 17 00:00:00 2001 From: 0x45f Date: Tue, 23 Nov 2021 11:46:58 +0000 Subject: [PATCH 5/5] change title --- docs/guides/04_dygraph_to_static/principle_cn.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/guides/04_dygraph_to_static/principle_cn.md b/docs/guides/04_dygraph_to_static/principle_cn.md index a9e464c21b2..3fb9f769ccb 100644 --- a/docs/guides/04_dygraph_to_static/principle_cn.md +++ b/docs/guides/04_dygraph_to_static/principle_cn.md @@ -2,7 +2,7 @@ 在框架内部,动转静模块在转换上主要包括对InputSpec的处理,对函数调用的递归转写,对IfElse、For、While控制语句的转写,以及Layer的Parameters和Buffers变量的转换。下面将从这四个方面介绍动转静模块的转换原理。 -## 一、 输入层 InputSpec +## 一、 设置 Placeholder 信息 静态图下,模型起始的 Placeholder 信息是通过 ``paddle.static.data`` 来指定的,并以此作为编译期的 ``InferShape`` 推导起点。 @@ -32,7 +32,6 @@ class SimpleNet(paddle.nn.Layer): super(SimpleNet, self).__init__() self.linear = paddle.nn.Linear(10, 3) - # 方式一:在函数定义处装饰 @to_static def forward(self, x, y): out = self.linear(x) @@ -41,7 +40,6 @@ class SimpleNet(paddle.nn.Layer): net = SimpleNet() -# 方式二:(推荐)仅做预测模型导出时,推荐此种用法 x_spec = InputSpec(shape=[None, 10], name='x') y_spec = InputSpec(shape=[3], name='y')