使用样例

一、概述

1.1 动态图和静态图简介

在深度学习模型构建上,飞桨框架支持动态图编程和静态图编程两种方式,其代码编写和执行方式均存在差异。

  • 动态图编程: 采用 Python 的编程风格,解析式地执行每一行网络代码,并同时返回计算结果。在 模型开发 章节中,介绍的主要是动态图编程方式。

  • 静态图编程: 采用先编译后执行的方式。需先在代码中预定义完整的神经网络结构,飞桨框架会将神经网络描述为 Program 的数据结构,并对 Program 进行编译优化,再调用执行器获得计算结果。

动态图编程体验更佳、更易调试,但是因为采用 Python 实时执行的方式,开销较大,在性能方面与 C++ 有一定差距;静态图调试难度大,但是将前端 Python 编写的神经网络预定义为 Program 描述,转到 C++ 端重新解析执行,脱离了 Python 依赖,往往执行性能更佳,并且预先拥有完整网络结构也更利于全局优化。

想了解动态图和静态图的详细对比介绍,可参见 动态图和静态图的差异

1.2 动态图转静态图应用场景

飞桨框架在设计时,考虑同时兼顾动态图的高易用性和静态图的高性能优势,采用『动静统一』的方案:

  • 在模型开发和训练时,推荐采用动态图编程。 可获得更好的编程体验、更易用的接口、更友好的调试交互机制。

  • 在模型推理部署时,推荐采用动态图转静态图(以下简称:动转静)。平滑衔接将训好的动态图模型自动保存为静态图模型,可获得更好的模型运行性能。

以上是飞桨框架推荐的使用方法,即『训练调优用动态图,推理部署用动转静』。但是在某些对模型训练性能有更高要求的场景,也可以使用动转静训练,即在动态图组网代码中添加一行装饰器 @to_static ,便可在底层转为性能更优的静态图模式下训练。

本文即介绍动转静模块的相关用法:

  • 动转静训练:将动态图编程的模型,转换为静态图模型 (@to_static)进行训练。

  • 动转静模型保存和加载:既支持动转静训练的模型保存和加载,也支持将动态图训练好的模型,直接保存为静态图模型文件(paddle.jit.save),然后用于推理部署。

注:飞桨框架 2.0 及以上版本默认的编程模式是动态图模式,包括使用高层 API 编程和基础的 API 编程。如果想切换到静态图模式编程,可以在程序的开始执行 enable_static() 函数。如果程序已经使用动态图的模式编写了,想转成静态图模式训练或者保存模型用于部署,可以使用装饰器 @to_static

二、动转静训练

在飞桨框架中,通常情况下使用动态图训练,即可满足大部分场景需求。 飞桨经过多个版本的持续优化,动态图模型训练的性能已经可以和静态图媲美。如果在某些场景下确实需要使用静态图模式训练,则可以使用动转静训练功能,即仍然采用更易用的动态图编程,添加少量代码,便可在底层转为静态图训练。

实际操作只需在待转化的函数前添加一个装饰器 @paddle.jit.to_static,框架便通过解析 Python 代码(抽象语法树,简称:AST)等方法自动完成动静转换。具体原理可参见 转换原理 章节。

2.1 动转静训练应用场景

在如下场景时可以考虑使用动转静进行模型训练,带来的性能提升效果较明显:

  • 如果发现模型训练 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 的空闲时间,提升执行效率。

调用 @paddle.jit.to_static 进行动转静时可以使用 build_strategy 参数开启 fuse_elewise_add_act_openable_addto 等优化策略来对计算图进行优化,更多策略开关可以参考 BuildStrategy 接口文档。图优化策略的使用样例请参考:4.1 动转静训练计算图优化策略

2.2 动转静训练样例

结合一个简单的网络训练示例,介绍动转静训练的方法,主要有两种方式:

2.2.1 方式一:使用 @paddle.jit.to_static 装饰器

下面的示例代码中展示了如何使用动转静训练 LinearNet 网络,在前向计算 forward 函数前添加一个装饰器,即可以将动态图网络转为静态图网络。对于其他动态图下的训练代码无需进行改动即可进行动转静的训练。

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# define a random dataset
class RandomDataset(paddle.io.Dataset):
    def __init__(self, num_samples):
        self.num_samples = num_samples

    def __getitem__(self, idx):
        image = np.random.random([IMAGE_SIZE]).astype('float32')
        label = np.random.randint(0, CLASS_NUM, (1, )).astype('int64')
        return image, label

    def __len__(self):
        return self.num_samples

class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    @paddle.jit.to_static       # <----在前向计算 forward 函数前添加一个装饰器
    def forward(self, x):
        return self._linear(x)

def train(layer, loader, loss_fn, opt):
    for epoch_id in range(EPOCH_NUM):
        for batch_id, (image, label) in enumerate(loader()):
            out = layer(image)
            loss = loss_fn(out, label)
            loss.backward()
            opt.step()
            opt.clear_grad()
            print("Epoch {} batch {}: loss = {}".format(
                epoch_id, batch_id, np.mean(loss.numpy())))

# create network
layer = LinearNet()
loss_fn = nn.CrossEntropyLoss()
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())

# create data loader
dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
loader = paddle.io.DataLoader(dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    drop_last=True,
    num_workers=2)

# train
train(layer, loader, loss_fn, adam)

2.2.2 方式二:使用 @paddle.jit.to_static 函数

除了装饰器的方式外,也可以在组网后,增加一行 layer = paddle.jit.to_static(layer) 就可以将动态图网络转为静态图网络。

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# define a random dataset
class RandomDataset(paddle.io.Dataset):
    def __init__(self, num_samples):
        self.num_samples = num_samples

    def __getitem__(self, idx):
        image = np.random.random([IMAGE_SIZE]).astype('float32')
        label = np.random.randint(0, CLASS_NUM, (1, )).astype('int64')
        return image, label

    def __len__(self):
        return self.num_samples

class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    def forward(self, x):
        return self._linear(x)

def train(layer, loader, loss_fn, opt):
    for epoch_id in range(EPOCH_NUM):
        for batch_id, (image, label) in enumerate(loader()):
            out = layer(image)
            loss = loss_fn(out, label)
            loss.backward()
            opt.step()
            opt.clear_grad()
            print("Epoch {} batch {}: loss = {}".format(
                epoch_id, batch_id, np.mean(loss.numpy())))

# create network
layer = LinearNet()
layer = paddle.jit.to_static(layer) # <----通过函数式调用 paddle.jit.to_static(layer) 一键实现动转静
loss_fn = nn.CrossEntropyLoss()
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())

# create data loader
dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
loader = paddle.io.DataLoader(dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    drop_last=True,
    num_workers=2)

# train
train(layer, loader, loss_fn, adam)
  • 方式一在组网代码中 forward() 函数定义处装饰,意指将其转为静态图执行模式;

  • 方式二在组网代码后,对整个网络 Layer 对象调用了 paddle.jit.to_static() 方法,默认也是对 Layer.forward() 进行动静转换;

  • 两种方式均实现了动转静训练,最终实现的效果是一样。

值得注意的是:请确保被装饰的 Layer.forward 方法中仅实现预测功能,避免将训练所需的 loss 计算逻辑写入 forward 方法。

Layer 更准确的语义是描述一个具有预测功能的模型对象,接收输入的样本数据,输出预测的结果,而 loss 计算是仅属于模型训练中的概念。将 loss 计算的实现放到 Layer.forward 方法中,会使 Layer 在不同场景下概念有所差别,并且增大 Layer 使用的复杂性,这不是良好的编码行为,同时也会在最终保存预测模型时引入剪枝的复杂性,因此建议保持 Layer 实现的简洁性。

错误示例如下:

import paddle
import paddle.nn as nn

IMAGE_SIZE = 784
CLASS_NUM = 10

class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    @paddle.jit.to_static
    def forward(self, x, label=None):
        out = self._linear(x)
        # 不规范写法,forward 中包括对 loss 进行计算
        if label:
            loss = nn.functional.cross_entropy(out, label)
            avg_loss = nn.functional.mean(loss)
            return out, avg_loss
        else:
            return out

正确示例如下:

import paddle
import paddle.nn as nn

IMAGE_SIZE = 784
CLASS_NUM = 10

class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)
    # 规范写法,forward 中仅实现预测功能
    @paddle.jit.to_static
    def forward(self, x):
        return self._linear(x)

2.3 动转静训练执行流程

动转静训练的基本执行流程如下图:

三、动转静模型保存和加载

采用动态图模式将模型训练好后,如果需要将模型的结构和参数持久化保存到磁盘文件中,用于后续推理部署,则可以使用 paddle.jit.save 实现动转静模型保存。保存的模型可使用 paddle.jit.load 载入用于验证推理效果。

3.1 保存和加载机制介绍

如下图所示,动态图模式下,模型结构指的是 Python 前端组网代码;模型参数指的是 model.state_dict() 中存放的权重数据。

使用 paddle.jit.save 保存模型,通常是在后台执行了两个步骤:

  1. 先执行了动转静。当然如果前面已经执行了动转静训练,则跳过这一步。在处理逻辑上,主要包含两个主要模块:

    • 模型结构层面:将动态图模型中被 @paddle.jit.to_static 装饰的函数转化为完整的静态图 Program。

    • 模型参数层面:将动态图模型中的参数(Parameters 和 Buffers )转为 Persistable=True 的静态图模型参数 Variable。

  2. 再将静态图模型和参数导出为磁盘文件。Program 和 Variable 都可以直接序列化导出为磁盘文件,与前端代码完全解耦,导出的文件包括:

    • 后缀为 .pdmodel 的模型结构文件;

    • 后缀为 .pdiparams 的模型参数文件;

    • 后缀为 .pdiparams.info 的和参数状态有关的额外信息文件。

类似的,使用 paddle.jit.load 加载模型,即将上述三个文件加载为静态图模型的 Program 和 Variable,可用于执行静态图模式下训练调优或验证推理效果。

无论是动态图训练,还是动转静训练,都支持通过 paddle.jit.save 自动转为静态图模型,只是使用过程中有一些配置差异,下面通过示例介绍。

3.2 动转静训练后模型保存和加载样例

3.2.1 模型保存样例

接前文动转静训练的示例代码,训练完成后,使用 paddle.jit.save 对模型和参数进行存储:

# 如果保存模型用于推理部署,则需切换 eval()模式
# layer.eval()
# 使用 paddle.jit.save 保存训练好的静态图模型
path = "example.model/linear"
paddle.jit.save(layer, path)

注:由于类似 Dropout 、LayerNorm 等接口在 train() 和 eval() 状态的行为存在较大的差异,在模型导出前,请务必确认模型已切换到正确的模式,否则导出的模型在预测阶段可能出现输出结果不符合预期的情况。

  • 如果保存模型用于推理部署,需切换到 eval() 模式;

  • 如果保存模型用于后续训练调优,则切换到 train() 模式;

执行上述代码样例后,会在当前目录下生成三个文件,即代表成功导出静态图模型:

linear.pdiparams        // 存放模型中所有的权重数据
linear.pdmodel         // 存放模型的网络结构
linear.pdiparams.info   // 存放和参数状态有关的额外信息

导出的模型可用于在云、边、端不同的硬件环境中部署,可以支持不同语言环境部署,如 C++、Java、Python 等。飞桨提供了服务器端部署的 Paddle Inference、移动端/IoT 端部署的 Paddle Lite、服务化部署的 Paddle Serving 等,以实现模型的快速部署上线。具体介绍可参见 推理部署 章节。

3.2.2 模型加载样例

动转静训练保存模型后,如果需要再加载用于训练调优或验证推理效果,可以选择使用 paddle.jit.loadpaddle.load API。

  • 使用 paddle.jit.load 加载:该方式可以载入模型结构和参数,传入数据即可训练或推理。

  • 使用 paddle.load 加载:如果已有组网代码,则只传入模型参数也可再训练,因此也可以选择该方式加载。

3.2.2.1 使用 paddle.jit.load 加载

使用 paddle.jit.load 载入,载入后得到的是一个 Layer 的派生类对象 TranslatedLayer , TranslatedLayer 具有 Layer 的通用特征,可以进行模型调优。

注意:使用 paddle.jit.load 载入模型,如果要用于训练调优,在 paddle.jit.save 的时候不能切换成 eval() 模式进行保存。另外,为了规避变量名字冲突,载入之后会重命名变量。

示例代码如下:

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# 载入 paddle.jit.save 保存的模型
path = "example.model/linear"
loaded_layer = paddle.jit.load(path)

载入模型及参数后进行预测,示例如下(接上文示例):

# 执行预测
loaded_layer.eval()
x = paddle.randn([1, IMAGE_SIZE], 'float32')
pred = loaded_layer(x)

载入模型及参数后进行调优(fine-tune),示例如下(接上文示例):

# 定义一个随机数据集
class RandomDataset(paddle.io.Dataset):
    def __init__(self, num_samples):
        self.num_samples = num_samples

    def __getitem__(self, idx):
        image = np.random.random([IMAGE_SIZE]).astype('float32')
        label = np.random.randint(0, CLASS_NUM, (1, )).astype('int64')
        return image, label

    def __len__(self):
        return self.num_samples

def train(layer, loader, loss_fn, opt):
    for epoch_id in range(EPOCH_NUM):
        for batch_id, (image, label) in enumerate(loader()):
            out = layer(image)
            loss = loss_fn(out, label)
            loss.backward()
            opt.step()
            opt.clear_grad()
            print("Epoch {} batch {}: loss = {}".format(
                epoch_id, batch_id, np.mean(loss.numpy())))

# 对载入后的模型进行训练调优
loaded_layer.train()
dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
loader = paddle.io.DataLoader(dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    drop_last=True,
    num_workers=2)
loss_fn = nn.CrossEntropyLoss()
adam = opt.Adam(learning_rate=0.001, parameters=loaded_layer.parameters())
train(loaded_layer, loader, loss_fn, adam)
# 训练调优后再次保存
paddle.jit.save(loaded_layer, "fine-tune.model/linear", input_spec=[x])
3.2.2.2 使用 paddle.load 加载

paddle.jit.save 同时保存了模型和参数,如果已有组网代码,只需要从存储结果中载入模型的参数,则可以使用 paddle.load 接口载入,返回所存储模型的 state_dict ,并使用 set_state_dict 方法将模型参数与 Layer 关联。示例如下:

import paddle
import paddle.nn as nn

IMAGE_SIZE = 784
CLASS_NUM = 10

# 网络定义
class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    @paddle.jit.to_static
    def forward(self, x):
        return self._linear(x)

# 创建一个网络
layer = LinearNet()

# 载入 paddle.jit.save 保存好的参数
path = "example.model/linear"
state_dict = paddle.load(path)

# 将加载后的参数赋给 layer 并进行预测
layer.set_state_dict(state_dict, use_structured_name=False)
layer.eval()
x = paddle.randn([1, IMAGE_SIZE], 'float32')
pred = layer(x)

3.3 动态图训练后模型保存和加载样例

结合一个简单的网络训练示例,介绍动态图训练后,转为静态图模型保存的方法。先用动态图模式训练一个模型:

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# 定义一个随机数数据集
class RandomDataset(paddle.io.Dataset):
    def __init__(self, num_samples):
        self.num_samples = num_samples

    def __getitem__(self, idx):
        image = np.random.random([IMAGE_SIZE]).astype('float32')
        label = np.random.randint(0, CLASS_NUM, (1, )).astype('int64')
        return image, label

    def __len__(self):
        return self.num_samples
# 定义神经网络
class LinearNet(nn.Layer):
    def __init__(self):
        super().__init__()
        self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)

    def forward(self, x):
        return self._linear(x)
# 定义训练过程
def train(layer, loader, loss_fn, opt):
    for epoch_id in range(EPOCH_NUM):
        for batch_id, (image, label) in enumerate(loader()):
            out = layer(image)
            loss = loss_fn(out, label)
            loss.backward()
            opt.step()
            opt.clear_grad()
            print("Epoch {} batch {}: loss = {}".format(
                epoch_id, batch_id, np.mean(loss.numpy())))

# 构建神经网络
layer = LinearNet()
# 设置损失函数
loss_fn = nn.CrossEntropyLoss()
# 设置优化器
adam = opt.Adam(learning_rate=0.001, parameters=layer.parameters())

# 构建 DataLoader 数据读取器
dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
loader = paddle.io.DataLoader(dataset,
    batch_size=BATCH_SIZE,
    shuffle=True,
    drop_last=True,
    num_workers=2)

# 开始训练
train(layer, loader, loss_fn, adam)

3.3.1 模型保存样例

动态图模型训练完成后,保存为静态图模型用于推理部署,主要包括三个步骤:

  1. 切换 eval() 模式: 类似 Dropout 、LayerNorm 等接口在 train() 和 eval() 的行为存在较大的差异,在模型导出前,请务必确认模型已切换到正确的模式,否则导出的模型在预测阶段可能出现输出结果不符合预期的情况。用于推理部署切换到 eval()模式,用于后续训练调优则切换到 train()模式。

  2. 构造 InputSpec 信息: InputSpec 用于表示模型输入数据的 shape、dtype、name 信息,是辅助动静转换的必要描述信息。这是由于静态图模型在调用执行器前并不执行实际操作,因此也并不读入实际数据,需要设置 “占位符” 表示输入数据。详细请参见 InputSpec 的用法介绍

  3. 调用 save 接口: 调用 paddle.jit.save 接口,若传入的参数是类实例(如示例中的 Layer 类),则框架会默认对神经网络中定义的前向计算 forward 函数进行 @paddle.jit.to_static 装饰(执行动转静),并导出其对应的模型文件和参数文件。

    from paddle.static import InputSpec
    # 1.切换 eval()模式
    layer.eval()
    # 2. 构造 InputSpec 信息
    input_spec = InputSpec([None, 784], 'float32', 'x')
    # 3.调用 paddle.jit.save 接口转为静态图模型
    path = "example.dy_model/linear"
    paddle.jit.save(
        layer=layer,
        path=path,
        input_spec=[input_spec])
    

执行上述代码样例后,会在当前目录下生成三个文件,即代表成功导出可用于推理部署的静态图模型:

linear.pdiparams        // 存放模型中所有的权重数据
linear.pdmodel         // 存放模型的网络结构
linear.pdiparams.info   // 存放和参数状态有关的额外信息

3.3.2 模型加载样例

动态图训练保存模型后,模型加载通常就是用于验证推理效果,使用 paddle.jit.load 载入。

示例代码如下:

import numpy as np
import paddle
import paddle.nn as nn
import paddle.optimizer as opt

BATCH_SIZE = 16
BATCH_NUM = 4
EPOCH_NUM = 4

IMAGE_SIZE = 784
CLASS_NUM = 10

# 载入 paddle.jit.save 保存的模型
path = "example.dy_model/linear"
loaded_layer = paddle.jit.load(path)

载入模型及参数后进行预测,示例如下:

# 执行预测
loaded_layer.eval()
x = paddle.randn([1, IMAGE_SIZE], 'float32')
pred = loaded_layer(x)

3.4 模型保存和加载注意事项

  1. 由于调用 paddle.jit.save 接口实际隐含了动转静的动作,因此动态图训练后可以不需要通过加装饰器 @paddle.jit.to_staticpaddle.jit.to_static() 函数的方式将动态图转为静态图,直接调用 paddle.jit.save 接口即可。

  2. 动态图训练场景下,使用 paddle.jit.save 保存模型时,必须指定 Layer 的 InputSpec ,Layer 对象 forward 方法的每一个参数均需要对应的 InputSpec 描述,不能省略。

  3. 动转静训练场景下,通常不用指定 InputSpec ,这是因为飞桨框架会自动根据训练时实际输入 Tensor 的 shape、dtype 等信息创建对应的 InputSpec ,不需要再指定。但是若模型的输入数据是动态输入(如输入数据的 shape 有一维度是可变的,该维度需设置为 None 或 -1),这种情况下则需要在 @paddle.jit.to_static 接口中指明模型的 InputSpec ,示例如下:

    import paddle
    import paddle.nn as nn
    from paddle.static import InputSpec
    
    IMAGE_SIZE = 784
    CLASS_NUM = 10
    
    # 网络定义
    class LinearNet(nn.Layer):
        def __init__(self):
            super().__init__()
            self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)
        # 输入数据是动态输入(shape 中有一个维度是可变的),因此需要添加 InputSpec
        @paddle.jit.to_static(input_spec=[InputSpec(shape=[None, 784], dtype='float32')])
        def forward(self, x):
            return self._linear(x)
    
  4. 前文中大多都是用 paddle.jit.save 保存的 Layer.forward 类实例,保存内容包括模型结构和参数。当保存单独的一个函数时, paddle.jit.save 只会保存这个函数对应的静态图模型结构 Program ,不会保存和这个函数相关的参数。如果必须保存参数,请使用 Layer 类封装这个函数。 示例代码如下:

    # 定义一个函数
    def fun(inputs):
        return paddle.tanh(inputs)
    
    path = 'func/model'
    inps = paddle.rand([3, 6])
    origin = fun(inps)
    
    # 将函数对应的 Program 结构进行保存
    paddle.jit.save(
        fun,
        path,
        input_spec=[
            InputSpec(
                shape=[None, 6], dtype='float32', name='x'),
        ])
    
    # 载入保存后的 fun 并执行
    load_func = paddle.jit.load(path)
    load_result = load_func(inps)
    
  5. 动转静训练时,如果想保存多个函数,则需要用 @paddle.jit.to_static 装饰每一个待保存的函数。示例代码如下:

    import paddle
    import paddle.nn as nn
    from paddle.static import InputSpec
    
    IMAGE_SIZE = 784
    CLASS_NUM = 10
    
    class LinearNet(nn.Layer):
        def __init__(self):
            super().__init__()
            self._linear = nn.Linear(IMAGE_SIZE, CLASS_NUM)
            self._linear_2 = nn.Linear(IMAGE_SIZE, CLASS_NUM)
    
        # 装饰 forward 方法,InputSpec 指定为 None
        @paddle.jit.to_static(input_spec=[InputSpec(shape=[None, IMAGE_SIZE], dtype='float32')])
        def forward(self, x):
            return self._linear(x)
    
        # 装饰需要保存的非 forward 方法,InputSpec 指定为 None
        @paddle.jit.to_static(input_spec=[InputSpec(shape=[None, IMAGE_SIZE], dtype='float32')])
        def another_forward(self, x):
            return self._linear_2(x)
    
    inps = paddle.randn([1, IMAGE_SIZE])
    layer = LinearNet()
    before_0 = layer.another_forward(inps)
    before_1 = layer(inps)
    # 模型保存
    path = "example.model/linear"
    paddle.jit.save(layer, path)
    

    注:只有在 forward 之外还需要保存其他函数时才用这个特性,如果仅装饰非 forward 函数,而 forward 本身函数没有被装饰,是不符合规范的。当保存多个函数时, InputSpec 信息需要在各个函数的 @paddle.jit.to_static 里分别指定,并且 input_spec 参数必须为 None,因为此时 save 接口 input_spec 参数无法知道它应该配置给哪个函数。

  • 该场景下保存的模型命名规则如下:

    • forward 的模型名字为:模型名+后缀 ,其他函数的模型名字为:模型名+函数名+后缀 。每个函数有各自的 pdmodel 和 pdiparams 的文件,所有函数共用 pdiparams.info 。上述示例代码将在 example.model 文件夹下产生 5 个文件: linear.another_forward.pdiparamslinear.pdiparamslinear.pdmodellinear.another_forward.pdmodellinear.pdiparams.info

3.5 InputSpec 的用法介绍

InputSpec 用于表示模型输入数据的 shape、dtype、name 信息,是辅助动静转换的必要描述信息。

在静态图模式下,飞桨框架会将神经网络描述为 Program 的数据结构,并对 Program 进行编译优化,再调用执行器获得计算结果。可以看到静态图模式下运行,在调用执行器前并不执行实际操作(这个阶段一般称为“组网阶段”或者“编译阶段”),因此也并不读入实际数据,所以在静态图中还需要一种特殊的变量来表示输入数据,一般称为“占位符”,动转静提供了 InputSpec 接口配置该“占位符”,用于表示输入数据的描述信息。

另外在无法确定输入数据维度时,可以通过 InputSpec 将相应维度指定为 None 来表示动态 shape(如输入的 batch_size 维度)。

3.5.1 构造 InputSpec

(1)方式一:直接构造

InputSpec 接口在 paddle.static 目录下, 只有 shape 是必须参数, dtype 和 name 可以缺省,默认取值分别为 float32 和 None 。使用样例如下:

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)

(2)方式二:由 Tensor 构造

可以借助 InputSpec.from_tensor 方法,从一个 Tensor 直接创建 InputSpec 对象,其拥有与源 Tensor 相同的 shape 和 dtype 。 使用样例如下:

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。

(3)方式三:由 Example Tensor 列表构造

可以通过训练时的输入数据构造,使用 forward 训练时的示例输入得到输入数据的 shape、dtype 等信息,比如使用数据加载器 DataLoader 得到的输入 image 来构造,示例如下:

import paddle
from paddle.static import InputSpec
# 省略动态图训练代码
# 保存时将输入数据传入 input_spec 参数
paddle.jit.save(
    layer=layer,
    path=path,
    input_spec=[image])

(4)方式四:由 numpy.ndarray 构造

也可以借助 InputSpec.from_numpy 方法,从一个 Numpy.ndarray 直接创建 InputSpec 对象,其拥有与源 ndarray 相同的 shape 和 dtype 。使用样例如下:

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 。

3.5.2 基本用法

(1)方式一: 在 @to_static 装饰器中调用

如下是一个简单的使用样例:

import paddle
from paddle.nn import Layer
from paddle.jit import to_static
from paddle.static import InputSpec

class SimpleNet(Layer):
    def __init__(self):
        super().__init__()
        self.linear = paddle.nn.Linear(10, 3)
    # 在装饰器中调用 InputSpec
    @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 graph mode 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)

(2)方式二:在 to_static 函数中调用

若期望在动态图下训练模型,在训练完成后保存预测模型,并指定预测时需要的签名信息,则可以选择在保存模型时,直接调用 to_static 函数。使用样例如下:

class SimpleNet(Layer):
    def __init__(self):
        super().__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)
# 在 paddle.jit.to_static 函数中调用 InputSpec
net = to_static(net, input_spec=[InputSpec(shape=[None, 10], name='x'), InputSpec(shape=[3], name='y')])

# save static graph model for inference directly
paddle.jit.save(net, './simple_net')

如上述样例代码中,在完成训练后,可以借助 to_static(net, input_spec=...) 形式对模型实例进行处理。飞桨框架会根据 input_spec 信息对 forward 函数进行递归的动转静处理,得到完整的静态图,且包括当前训练好的参数数据。

(3)方式三:通过 list 和 dict 推导

上述两个样例中,被装饰的 forward 函数的参数均为 Tensor 。这种情况下,参数个数必须与 InputSpec 个数相同。但当被装饰的函数参数为 list 或 dict 类型时,input_spec 需要与函数参数保持相同的嵌套结构。

当函数的参数为 list 类型时,input_spec 列表中对应元素的位置,也必须是包含相同元素的 InputSpec 列表。使用样例如下:


class SimpleNet(Layer):
    def __init__(self):
        super().__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 列表。使用样例如下:

class SimpleNet(Layer):
    def __init__(self):
        super().__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 签名信息。

(4)方式四:指定非 Tensor 参数类型

若被装饰函数的参数列表除了 Tensor 类型,还包含其他如 Int、 String 等非 Tensor 类型时,推荐在函数中使用 kwargs 形式定义非 Tensor 参数,如下述样例中的 use_act 参数。

class SimpleNet(Layer):
    def __init__(self, ):
        super().__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 参数默认值设置为预测时的取值。

四、动转静更多用法

4.1 动转静训练计算图优化策略

如下是一个 ResNet50 模型动转静训练时,通过在 to_static 函数中配置 build_strategy 参数来开启算子融合 fuse_elewise_add_act_opsenable_addto 图优化策略的使用样例。不同的模型可应用的优化策略不同,比如算子融合策略一般与模型中用到的 API 有关系:

  • 若存在 elementwise_add 后跟 relu 等激活函数,则可以尝试开启 fuse_elewise_add_act_ops

  • 若存在 relu 后跟 depthwise_conv2 函数,则可以尝试开启 fuse_relu_depthwise_conv

  • 若存在较多的 conv2dAPI 的调用,则可以尝试开启 enable_addto ,更多策略开关可以参考 BuildStrategy 接口文档。

    import os
    import paddle
    import numpy as np
    import paddle.optimizer as opt
    from paddle import nn
    from paddle.vision.models import resnet50
    
    BATCH_SIZE = 16
    BATCH_NUM = 4
    EPOCH_NUM = 4
    
    IMAGE_SIZE = 224
    CLASS_NUM = 1000
    
    # 定义一个随机数数据集
    class RandomDataset(paddle.io.Dataset):
        def __init__(self, num_samples):
            self.num_samples = num_samples
    
        def __getitem__(self, idx):
            image = np.random.random([3, IMAGE_SIZE, IMAGE_SIZE]).astype('float32')
            label = np.random.randint(0, CLASS_NUM, (1, )).astype('int64')
            return image, label
    
        def __len__(self):
            return self.num_samples
    
    # 定义训练过程
    def train(layer, loader, loss_fn, opt):
        for epoch_id in range(EPOCH_NUM):
            for batch_id, (images, labels) in enumerate(loader()):
                out = layer(images)
                loss = loss_fn(out, labels)
                loss.backward()
                opt.step()
                opt.clear_grad()
                print("Epoch {} batch {}: loss = {}".format(
                    epoch_id, batch_id, np.mean(loss.numpy())))
    
    def get_build_strategy():
        build_strategy = paddle.static.BuildStrategy()
        # addto 策略常搭配 FLAGS_max_inplace_grad_add 变量使用
        build_strategy.enable_addto = True
        os.environ['FLAGS_max_inplace_grad_add'] = "8"
        build_strategy.fuse_elewise_add_act_ops = True
        return build_strategy
    
    # 构建神经网络
    model = resnet50()
    # 动转静,并设置计算图优化策略
    model = paddle.jit.to_static(model, build_strategy=get_build_strategy())
    # 设置损失函数
    loss_fn = nn.CrossEntropyLoss()
    # 设置优化器
    adam = opt.Adam(learning_rate=0.001, parameters=model.parameters())
    
    # 构建 DataLoader 数据读取器
    dataset = RandomDataset(BATCH_NUM * BATCH_SIZE)
    loader = paddle.io.DataLoader(dataset,
        batch_size=BATCH_SIZE,
        shuffle=True,
        drop_last=True,
        num_workers=2)
    
    # 开始训练
    train(model, loader, loss_fn, adam)
    

4.2 动转静训练开启 AMP

自动混合精度(Automatic Mixed Precision,AMP) 训练的方法,可在模型训练时,自动为算子选择合适的数据计算精度(float32 或 float16 / bfloat16),在保持训练精度(accuracy)不损失的条件下,能够加速训练。

如下是动态图开启 AMP 训练后,执行单个 step 的 timeline 示意图。相对于 FP32 训练,开启 AMP 后,每个 GPU 的 Kernel 计算效率进一步提升,耗时更短(图中蓝框更窄了),但对 CPU 端的调度性能要求也更高了。

由于动转静后,转为静态图训练可以有效地减少 Python 与 C++ 端交互的开销,减少框架调度时间,提升训练性能。经验证,在 Bert Base 和 Transformer Base 模型 AMP 训练任务上测试,动转静训练后性能有 20% 左右的提升。因此在 AMP 训练任务上,推荐尝试开启动转静实现训练加速。开启 AMP 的具体用法请参见 自动混合精度训练 章节。

五、总结

飞桨框架动转静模块的设计,主要是为了兼顾动态图的易用性和静态图的高性能。有了动转静模块,开发者并不需要同时深入掌握动态图和静态图的编程方法,只需要掌握动态图的使用,只需添加一行装饰器 @paddle.jit.to_static ,即可转为静态图训练,飞桨框架在后台帮助完成了动转静,前台仍然保持动态图的使用习惯即可。总结动转静的主要用法:

  • 动转静训练:即动态图编码,转静态图训练。只需通过 @paddle.jit.to_static 装饰模型 Layer 类实例的 forward 函数即可实现。

  • 动转静模型保存和加载:既支持动转静训练后模型保存和加载,也支持动态图编码和训练,直接转为静态图模型文件,用于模型推理部署。直接使用 paddle.jit.save 保存即可,飞桨框架自动完成了动转静和保存操作。