动态图与静态图

Ascend GPU CPU 进阶 模型运行

在线运行下载Notebook下载样例代码查看源文件

概述

MindSpore支持两种运行模式:

  • 静态图模式:又称Graph模式,将神经网络模型编译成一整张图,然后下发执行。该模式利用图优化、计算图整图下沉等技术提高运行性能,同时有助于规模部署和跨平台运行。

  • 动态图模式:又称PyNative模式,将神经网络中的各个算子逐一下发执行,方便用户编写和调试神经网络模型。

Graph和PyNative两种模式的区别主要有:

  • 使用场景:Graph模式需要一开始就构建好网络结构,然后框架做整图优化和执行,比较适合网络固定没有变化,且需要高性能的场景。而PyNative模式逐行执行算子,支持单独求梯度。

  • 网络执行:Graph模式和PyNative模式在执行相同的网络和算子时,精度效果是一致的。由于Graph模式运用了图优化、计算图整图下沉等技术,Graph模式执行网络的性能和效率更高。

  • 代码调试:在脚本开发和网络流程调试中,推荐使用PyNative模式进行调试。在PyNative模式下,可以方便地设置断点,获取网络执行的中间结果,也可以通过pdb的方式对网络进行调试。而Graph模式无法设置断点,只能先指定算子进行打印,然后在网络执行完成后查看输出结果。

默认情况下,MindSpore处于Graph模式,可以通过context.set_context(mode=context.PYNATIVE_MODE)切换为PyNative模式;同样地,MindSpore处于PyNative模式时,可以通过 context.set_context(mode=context.GRAPH_MODE)切换为Graph模式。

下面以Graph模式为例,演示MindSpore单算子、普通函数、模型的执行方式,并进一步说明如何在Graph模式和PyNative模式下进行性能优化及梯度求取。

执行方式

这里演示在Graph模式和PyNative模式下,单算子、普通函数、模型的执行方式。

在本案例的实际执行中,采取了MindSpore的默认方式GRAPH_MODE,用户也可以将其变更为PYNATIVE_MODE进行尝试。

[ ]:
import numpy as np
import mindspore.nn as nn
from mindspore import context, Tensor, ParameterTuple, ms_function
import mindspore.ops as ops
from mindspore.common.initializer import Normal
from mindspore.nn import WithLossCell, Momentum

# 设定为Graph模式,也可替换为PYNATIVE_MODE
context.set_context(mode=context.GRAPH_MODE, device_target="Ascend")

执行单算子

[1]:
# 打印Conv2d算子的输出
conv = nn.Conv2d(3, 4, 3, bias_init='zeros')
input_data = Tensor(np.ones([1, 3, 5, 5]).astype(np.float32))
output = conv(input_data)
print(output.asnumpy())
[[[[-0.02593261  0.01615404  0.01615404  0.01615404  0.01196378]
   [-0.01535788  0.05602208  0.05602208  0.05602208  0.04094065]
   [-0.01535788  0.05602208  0.05602208  0.05602208  0.04094065]
   [-0.01535788  0.05602208  0.05602208  0.05602208  0.04094065]
   [-0.01409336  0.04544117  0.04544117  0.04544117  0.0373004 ]]

  [[ 0.03874376  0.02201786  0.02201786  0.02201786  0.02687691]
   [ 0.05751193  0.02690699  0.02690699  0.02690699  0.03515062]
   [ 0.05751193  0.02690699  0.02690699  0.02690699  0.03515062]
   [ 0.05751193  0.02690699  0.02690699  0.02690699  0.03515062]
   [ 0.02599058  0.01130002  0.01130002  0.01130002  0.02304572]]

  [[-0.00022919  0.02640852  0.02640852  0.02640852  0.04932421]
   [ 0.01657246  0.0705748   0.0705748   0.0705748   0.0874946 ]
   [ 0.01657246  0.0705748   0.0705748   0.0705748   0.0874946 ]
   [ 0.01657246  0.0705748   0.0705748   0.0705748   0.0874946 ]
   [ 0.03821789  0.09614976  0.09614976  0.09614976  0.10491695]]

  [[ 0.0190958   0.02602289  0.02602289  0.02602289  0.01660084]
   [ 0.03556763  0.06862713  0.06862713  0.06862713  0.02653556]
   [ 0.03556763  0.06862713  0.06862713  0.06862713  0.02653556]
   [ 0.03556763  0.06862713  0.06862713  0.06862713  0.02653556]
   [ 0.00727296  0.04514674  0.04514674  0.04514674  0.01423099]]]]

执行普通函数

将若干算子组合成一个函数,然后直接通过函数调用的方式执行这些算子,并打印相关结果,如下例所示。

[2]:
def add_func(x, y):
    z = ops.add(x, y)
    z = ops.add(z, x)
    return z

x = Tensor(np.ones([3, 3], dtype=np.float32))
y = Tensor(np.ones([3, 3], dtype=np.float32))
output = add_func(x, y)
print(output.asnumpy())
[[3. 3. 3.]
 [3. 3. 3.]
 [3. 3. 3.]]

执行网络

[3]:
import numpy as np
import mindspore.nn as nn
import mindspore.ops as ops
from mindspore import context, Tensor

class Net(nn.Cell):
    def __init__(self):
        super(Net, self).__init__()
        self.mul = ops.Mul()

    def construct(self, x, y):
        return self.mul(x, y)

x = Tensor(np.array([1.0, 2.0, 3.0]).astype(np.float32))
y = Tensor(np.array([4.0, 5.0, 6.0]).astype(np.float32))

net = Net()
print(net(x, y))
[ 4. 10. 18.]

Graph模式说明

性能优化

在Graph模式下,MindSpore通过源码转换的方式,将Python的源码转换成IR,再在此基础上进行相关的图优化,最终在硬件设备上执行优化后的图。MindSpore使用的是一种基于图表示的函数式IR,即MindIR,采用了接近于ANF函数式的语义。Graph模式是基于MindIR进行编译优化的,编译器可以利用图优化、计算图整图下沉等技术对执行图进行更大程度的优化,从而获得更好的执行性能。

使用Graph模式时,需要使用nn.Cell类并且在construct函数中编写执行代码, 或者调用@ms_function装饰器。

梯度求取

Graph模式中,定义了GradOperation计算神经网络的梯度,使用反向自动微分模式,即从正向网络的输出开始计算梯度。关于自动微分的详细信息,请参考自动微分

PyNative模式说明

性能优化

正如文章开头所说,Graph模式适合高性能的场景,但PyNative模式中也提供了性能优化的手段。MindSpore提供了Staging功能,该功能可以在PyNative模式下将Python函数或者Python类的方法编译成计算图,通过图优化等技术提高运行速度,是一种混合运行机制。Staging功能的使用通过ms_function装饰器达成,该装饰器会将模块编译成计算图,在给定输入之后,以图的形式下发执行。如下例所示:

[4]:
# 导入ms_function
from mindspore import ms_function

# 仍设定为PyNative模式
context.set_context(mode=context.PYNATIVE_MODE, device_target="Ascend")

add = ops.Add()

# 使用装饰器编译计算图
@ms_function
def add_fn(x, y):
    res = add(x, y)
    return res

x = Tensor(np.ones([4, 4]).astype(np.float32))
y = Tensor(np.ones([4, 4]).astype(np.float32))
z = add_fn(x, y)
print(z.asnumpy())
[[2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]
 [2. 2. 2. 2.]]

在加装了ms_function装饰器的函数中,如果包含不需要进行参数训练的算子(如poolingadd等算子),则这些算子可以在被装饰的函数中直接调用,如上例所示。如果被装饰的函数中包含了需要进行参数训练的算子(如ConvolutionBatchNorm等算子),则这些算子必须在被装饰的函数之外完成实例化操作。

[5]:
# Conv2d实例化操作
conv_obj = nn.Conv2d(in_channels=3, out_channels=4, kernel_size=3, stride=2, padding=0)
conv_obj.init_parameters_data()

@ms_function
def conv_fn(x):
    res = conv_obj(x)
    return res

input_data = np.random.randn(2, 3, 6, 6).astype(np.float32)
z = conv_fn(Tensor(input_data))
print(z.asnumpy())
[[[[ 0.00994964  0.01850731 -0.05146599]
   [ 0.02427048 -0.09082688 -0.00945184]
   [ 0.02710651 -0.07322617  0.02594434]]

  [[ 0.00056772 -0.05043615 -0.03873939]
   [-0.00445028  0.03694705 -0.03555503]
   [ 0.07329068 -0.02026664  0.01922888]]

  [[ 0.02257145 -0.04093865 -0.00493869]
   [ 0.01740007  0.02478302  0.02072578]
   [ 0.05831327 -0.03933404  0.01767443]]

  [[-0.03954437  0.02160874 -0.00700614]
   [ 0.03856367 -0.04015685  0.02508826]
   [-0.0229507  -0.03803677  0.02813173]]]


 [[[ 0.01678797 -0.02227589 -0.04470547]
   [-0.05720481 -0.15464461  0.00911596]
   [ 0.02566019 -0.04340314  0.03164666]]

  [[ 0.03300299 -0.05849815  0.05841954]
   [-0.11595733 -0.01524522  0.02947116]
   [ 0.05930116  0.00831041 -0.0466827 ]]

  [[-0.0797728   0.02910854  0.00766015]
   [-0.01380327 -0.03338642  0.02625138]
   [ 0.02279372 -0.00952736  0.02026749]]

  [[ 0.04039776 -0.05340278 -0.0083563 ]
   [ 0.04991922 -0.05205034 -0.0058607 ]
   [ 0.00686666  0.00064385  0.00301326]]]]

梯度求取

PyNative模式中支持单独的梯度求取操作,下面演示如何利用这一特性调试网络模型。具体操作可通过GradOperation求该函数或者网络所有的输入梯度。需要注意,输入类型仅支持Tensor。

构建网络如下。

[6]:
class LeNet5(nn.Cell):
    """
    Lenet网络结构
    """
    def __init__(self, num_class=10, num_channel=1):
        super(LeNet5, self).__init__()
        # 定义所需要的运算
        self.conv1 = nn.Conv2d(num_channel, 6, 5, pad_mode='valid')
        self.conv2 = nn.Conv2d(6, 16, 5, pad_mode='valid')
        self.fc1 = nn.Dense(16 * 5 * 5, 120, weight_init=Normal(0.02))
        self.fc2 = nn.Dense(120, 84, weight_init=Normal(0.02))
        self.fc3 = nn.Dense(84, num_class, weight_init=Normal(0.02))
        self.relu = nn.ReLU()
        self.max_pool2d = nn.MaxPool2d(kernel_size=2, stride=2)
        self.flatten = nn.Flatten()

    def construct(self, x):
        # 使用定义好的运算构建前向网络
        x = self.conv1(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        x = self.conv2(x)
        x = self.relu(x)
        x = self.max_pool2d(x)
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        x = self.relu(x)
        x = self.fc3(x)
        return x

# 实例化网络
net = LeNet5()

如上文所说,利用GradOperation求函数输入的梯度。

[7]:
class GradWrap(nn.Cell):
    """求函数输入梯度"""
    def __init__(self, network):
        super(GradWrap, self).__init__(auto_prefix=False)
        self.network = network
        # 用Tuple的形式包装weight
        self.weights = ParameterTuple(filter(lambda x: x.requires_grad, network.get_parameters()))

    def construct(self, x, label):
        weights = self.weights
        # 返回值为梯度
        return ops.GradOperation(get_by_list=True)(self.network, weights)(x, label)

在PyNative模式中进行网络训练。

[8]:
# 设定优化器、损失函数
optimizer = Momentum(filter(lambda x: x.requires_grad, net.get_parameters()), 0.1, 0.9)
criterion = nn.SoftmaxCrossEntropyWithLogits(sparse=True, reduction='mean')

# 通过WithLossCell获取Loss值
net_with_criterion = WithLossCell(net, criterion)

# 调用GradWrap
train_network = GradWrap(net_with_criterion)
train_network.set_train()

# 产生输入数据
input_data = Tensor(np.ones([32, 1, 32, 32]).astype(np.float32) * 0.01)
label = Tensor(np.ones([32]).astype(np.int32))
output = net(Tensor(input_data))

# 利用前向网络计算loss
loss_output = criterion(output, label)

# 求得梯度
grads = train_network(input_data, label)

# 输出loss
success = optimizer(grads)
loss = loss_output.asnumpy()
print(loss)
2.3025854

动静统一

MindSpore支持动态图和静态图两种模式,动态图通过解释执行,具有动态语法亲和性,表达灵活;静态图使用JIT编译优化执行,偏静态语法,在语法上有较多限制。由于动态图和静态图的编译流程不一致,两者的语法约束是不一致的。

动态图和静态图互相转换

在MindSpore中,我们可以通过控制模式输入参数来切换执行使用动态图还是静态图,通过context.set_context(mode=context.GRAPH_MODE)可以设置静态图模式,通过context.set_context(mode=context.PYNATIVE_MODE)可以设置成动态图模式。由于在静态图下,对于Python语法有所限制,因此从动态图切换成静态图时,需要符合静态图的语法限制,才能正确使用静态图来进行执行。MindSpore静态图的语法限制可以参考静态图语法限制

动静结合

MindSpore支持在动态图下使用静态编译的方式来进行混合执行,通过使用ms_function修饰需要用静态图来执行的函数对象,即可实现动态图和静态图的混合执行,更多ms_function的使用可参考ms_function文档

JIT Fallback

JIT Fallback是从静态图的角度出发考虑静态图和动态图的统一,希望静态图模式能够尽量多地支持动态图模式的语法,其借鉴了传统JIT编译的Fallback的思路。MindSpore默认使用静态图模式即Graph模式,不是所有的Python语法都能支持,用户在编写程序时容易遇到语法约束限制。通过JIT Fallback,用户可以灵活地进行静态图和动态图的切换。

当前JIT Fallback有条件地支持Graph模式的部分常量场景,包括调用第三方库、创建及使用Tensor、调用Python的print打印等。

代码用例如下:

[9]:
import numpy as np
import mindspore.nn as nn
from mindspore import context, Tensor

context.set_context(mode=context.GRAPH_MODE, device_target="GPU")

class Net(nn.Cell):
    def construct(self):
        x = np.array([1, 2, 3])
        y = Tensor(x)
        return y

net = Net()
print(net())

[1 2 3]