zoukankan      html  css  js  c++  java
  • 第8章 计算性能

    深度学习简介

    预备知识

    深度学习基础

    深度学习计算

    卷积神经网络

    循环神经网络

    优化算法

    计算性能

    命令式和符号式混合编程

    本书到目前为止一直都在使用命令式编程,它使用编程语句改变程序状态。考虑下面这段简单的命令式程序。

    def add(a, b):
        return a + b
    
    def fancy_func(a, b, c, d):
        e = add(a, b)
        f = add(c, d)
        g = add(e, f)
        return g
    
    fancy_func(1, 2, 3, 4) # 10
    

    和我们预期的一样,在运行语句e = add(a, b)时,Python会做加法运算并将结果存储在变量e中,从而令程序的状态发生改变。类似地,后面的两条语句f = add(c, d)g = add(e, f)会依次做加法运算并存储变量。

    虽然使用命令式编程很方便,但它的运行可能很慢。一方面,即使fancy_func函数中的add是被重复调用的函数,Python也会逐一执行这3条函数调用语句。另一方面,我们需要保存变量ef的值直到fancy_func中所有语句执行结束。这是因为在执行e = add(a, b)f = add(c, d)这2条语句之后我们并不知道变量ef是否会被程序的其他部分使用。

    与命令式编程不同,符号式编程通常在计算流程完全定义好后才被执行。多个深度学习框架,如Theano和TensorFlow,都使用了符号式编程。通常,符号式编程的程序需要下面3个步骤:

    1. 定义计算流程;
    2. 把计算流程编译成可执行的程序;
    3. 给定输入,调用编译好的程序执行。

    下面我们用符号式编程重新实现本节开头给出的命令式编程代码。

    def add_str():
        return '''
    def add(a, b):
        return a + b
    '''
    
    def fancy_func_str():
        return '''
    def fancy_func(a, b, c, d):
        e = add(a, b)
        f = add(c, d)
        g = add(e, f)
        return g
    '''
    
    def evoke_str():
        return add_str() + fancy_func_str() + '''
    print(fancy_func(1, 2, 3, 4))
    '''
    
    prog = evoke_str()
    print(prog)
    y = compile(prog, '', 'exec')
    exec(y)
    

    输出:

    def add(a, b):
        return a + b
    
    def fancy_func(a, b, c, d):
        e = add(a, b)
        f = add(c, d)
        g = add(e, f)
        return g
    
    print(fancy_func(1, 2, 3, 4))
    
    10
    

    以上定义的3个函数都仅以字符串的形式返回计算流程。最后,我们通过compile函数编译完整的计算流程并运行。由于在编译时系统能够完整地获取整个程序,因此有更多空间优化计算。例如,编译的时候可以将程序改写成print((1 + 2) + (3 + 4)),甚至直接改写成print(10)。这样不仅减少了函数调用,还节省了内存。

    对比这两种编程方式,我们可以看到以下两点。

    • 命令式编程更方便。当我们在Python里使用命令式编程时,大部分代码编写起来都很直观。同时,命令式编程更容易调试。这是因为我们可以很方便地获取并打印所有的中间变量值,或者使用Python的调试工具。

    • 符号式编程更高效并更容易移植。一方面,在编译的时候系统容易做更多优化;另一方面,符号式编程可以将程序变成一个与Python无关的格式,从而可以使程序在非Python环境下运行,以避开Python解释器的性能问题。

    混合式编程取两者之长

    大部分深度学习框架在命令式编程和符号式编程之间二选一。例如,Theano和受其启发的后来者TensorFlow使用了符号式编程,Chainer和它的追随者PyTorch使用了命令式编程,而Gluon则采用了混合式编程的方式

    ......

    由于PyTorch仅仅采用了命令式编程,所以跳过本节剩余部分,感兴趣的可以去看原文

    异步计算

    此节内容对应PyTorch的版本本人没怎么用过,网上参考资料也比较少,所以略:),有兴趣的可以去看看原文

    关于PyTorch的异步执行我只在官方文档找到了一段:

    By default, GPU operations are asynchronous. When you call a function that uses the GPU, the operations are enqueued to the particular device, but not necessarily executed until later. This allows us to execute more computations in parallel, including operations on CPU or other GPUs.
    In general, the effect of asynchronous computation is invisible to the caller, because (1) each device executes operations in the order they are queued, and (2) PyTorch automatically performs necessary synchronization when copying data between CPU and GPU or between two GPUs. Hence, computation will proceed as if every operation was executed synchronously.
    You can force synchronous computation by setting environment variable CUDA_LAUNCH_BLOCKING=1. This can be handy when an error occurs on the GPU. (With asynchronous execution, such an error isn’t reported until after the operation is actually executed, so the stack trace does not show where it was requested.)

    大致翻译一下就是:

    默认情况下,PyTorch中的 GPU 操作是异步的。当调用一个使用 GPU 的函数时,这些操作会在特定的设备上排队但不一定会在稍后立即执行。这就使我们可以并行更多的计算,包括 CPU 或其他 GPU 上的操作。

    一般情况下,异步计算的效果对调用者是不可见的,因为(1)每个设备按照它们排队的顺序执行操作,(2)在 CPU 和 GPU 之间或两个 GPU 之间复制数据时,PyTorch会自动执行必要的同步操作。因此,计算将按每个操作同步执行的方式进行。

    可以通过设置环境变量CUDA_LAUNCH_BLOCKING = 1来强制进行同步计算。当 GPU 产生error时,这可能非常有用。(异步执行时,只有在实际执行操作之后才会报告此类错误,因此堆栈跟踪不会显示请求的位置。)

    自动并行计算

    上一节提到,默认情况下,GPU 操作是异步的。当调用一个使用 GPU 的函数时,这些操作会在特定的设备上排队,但不一定会在稍后执行。这允许我们并行更多的计算,包括 CPU 或其他 GPU 上的操作。
    下面看一个简单的例子。

    首先导入本节中实验所需的包或模块。注意,需要至少2块GPU才能运行本节实验。

    import torch
    import time
    
    assert torch.cuda.device_count() >= 2
    

    我们先实现一个简单的计时类。

    class Benchmark():  # 本类已保存在d2lzh_pytorch包中方便以后使用
        def __init__(self, prefix=None):
            self.prefix = prefix + ' ' if prefix else ''
    
        def __enter__(self):
            self.start = time.time()
    
        def __exit__(self, *args):
            print('%stime: %.4f sec' % (self.prefix, time.time() - self.start))
    

    再定义run函数,令它做20000次矩阵乘法。

    def run(x):
        for _ in range(20000):
            y = torch.mm(x, x)
    

    接下来,分别在两块GPU上创建Tensor

    x_gpu1 = torch.rand(size=(100, 100), device='cuda:0')
    x_gpu2 = torch.rand(size=(100, 100), device='cuda:1')
    

    然后,分别使用它们运行run函数并打印运行所需时间。

    with Benchmark('Run on GPU1.'):
        run(x_gpu1)
        torch.cuda.synchronize()
    
    with Benchmark('Then run on GPU2.'):
        run(x_gpu2)
        torch.cuda.synchronize()
    

    输出:

    Run on GPU1. time: 0.2989 sec
    Then run on GPU2. time: 0.3518 sec
    

    尝试系统能自动并行这两个任务:

    with Benchmark('Run on both GPU1 and GPU2 in parallel.'):
        run(x_gpu1)
        run(x_gpu2)
        torch.cuda.synchronize()
    

    输出:

    Run on both GPU1 and GPU2 in parallel. time: 0.5076 sec
    

    可以看到,当两个计算任务一起执行时,执行总时间小于它们分开执行的总和。这表明,PyTorch能有效地实现在不同设备上自动并行计算。


    注:本节与原书有很多不同,原书传送门

    多GPU计算

    注:相对于本章的前面几节,我们实际中更可能遇到本节所讨论的情况:多GPU计算。原书将MXNet的多GPU计算分成了8.4和8.5两节,但我们将关于PyTorch的多GPU计算统一放在本节讨论。
    需要注意的是,这里我们谈论的是单主机多GPU计算而不是分布式计算。如果对分布式计算感兴趣可以参考PyTorch官方文档

    本节中我们将展示如何使用多块GPU计算,例如,使用多块GPU训练同一个模型。正如所期望的那样,运行本节中的程序需要至少2块GPU。事实上,一台机器上安装多块GPU很常见,这是因为主板上通常会有多个PCIe插槽。如果正确安装了NVIDIA驱动,我们可以通过在命令行输入nvidia-smi命令来查看当前计算机上的全部GPU(或者在jupyter notebook中运行!nvidia-smi)。

    nvidia-smi
    

    输出:

    Wed May 15 23:12:38 2019
    +-----------------------------------------------------------------------------+
    | NVIDIA-SMI 390.48                 Driver Version: 390.48                    |
    |-------------------------------+----------------------+----------------------+
    | GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
    | Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
    |===============================+======================+======================|
    |   0  TITAN X (Pascal)    Off  | 00000000:02:00.0 Off |                  N/A |
    | 46%   76C    P2    87W / 250W |  10995MiB / 12196MiB |      0%      Default |
    +-------------------------------+----------------------+----------------------+
    |   1  TITAN X (Pascal)    Off  | 00000000:04:00.0 Off |                  N/A |
    | 53%   84C    P2   143W / 250W |  11671MiB / 12196MiB |      4%      Default |
    +-------------------------------+----------------------+----------------------+
    |   2  TITAN X (Pascal)    Off  | 00000000:83:00.0 Off |                  N/A |
    | 62%   87C    P2   190W / 250W |  12096MiB / 12196MiB |    100%      Default |
    +-------------------------------+----------------------+----------------------+
    |   3  TITAN X (Pascal)    Off  | 00000000:84:00.0 Off |                  N/A |
    | 51%   83C    P2   255W / 250W |   8144MiB / 12196MiB |     58%      Default |
    +-------------------------------+----------------------+----------------------+
                                                                                   
    +-----------------------------------------------------------------------------+
    | Processes:                                                       GPU Memory |
    |  GPU       PID   Type   Process name                             Usage      |
    |=============================================================================|
    |    0     44683      C   python                                      3289MiB |
    |    0    155760      C   python                                      4345MiB |
    |    0    158310      C   python                                      2297MiB |
    |    0    172338      C   /home/yzs/anaconda3/bin/python              1031MiB |
    |    1    139985      C   python                                     11653MiB |
    |    2     38630      C   python                                      5547MiB |
    |    2     43127      C   python                                      5791MiB |
    |    2    156710      C   python3                                      725MiB |
    |    3     14444      C   python3                                     1891MiB |
    |    3     43407      C   python                                      5841MiB |
    |    3     88478      C   /home/tangss/.conda/envs/py36/bin/python     379MiB |
    +-----------------------------------------------------------------------------+
    

    从上面的输出可以看到一共有四块TITAN X GPU,每一块总共有约12个G的显存,此时每块的显存都占得差不多了......此外还可以看到GPU利用率、运行的所有程序等信息。

    Pytorch在0.4.0及以后的版本中已经提供了多GPU训练的方式,本文用一个简单的例子讲解下使用Pytorch多GPU训练的方式以及一些注意的地方。

    多GPU计算

    先定义一个模型:

    import torch
    net = torch.nn.Linear(10, 1).cuda()
    net
    

    输出:

    Linear(in_features=10, out_features=1, bias=True)
    

    要想使用PyTorch进行多GPU计算,最简单的方法是直接用torch.nn.DataParallel将模型wrap一下即可:

    net = torch.nn.DataParallel(net)
    net
    

    输出:

    DataParallel(
      (module): Linear(in_features=10, out_features=1, bias=True)
    )
    

    这时,默认所有存在的GPU都会被使用。

    如果我们机子中有很多GPU(例如上面显示我们有4张显卡,但是只有第0、3块还剩下一点点显存),但我们只想使用0、3号显卡,那么我们可以用参数device_ids指定即可:torch.nn.DataParallel(net, device_ids=[0, 3])

    多GPU模型的保存与加载

    我们现在来尝试一下按照4.5节(读取和存储)推荐的方式进行一下模型的保存与加载。
    保存模型:

    torch.save(net.state_dict(), "./8.4_model.pt")
    

    加载模型前我们一般要先进行一下模型定义,此时的new_net并没有使用多GPU:

    new_net = torch.nn.Linear(10, 1)
    new_net.load_state_dict(torch.load("./8.4_model.pt"))
    

    然后我们发现报错了:

    RuntimeError: Error(s) in loading state_dict for Linear:
    	Missing key(s) in state_dict: "weight", "bias". 
    	Unexpected key(s) in state_dict: "module.weight", "module.bias". 
    

    事实上DataParallel也是一个nn.Module,只是这个类其中有一个module就是传入的实际模型。因此当我们调用DataParallel后,模型结构变了(在外面加了一层而已,从8.4.1节两个输出可以对比看出来)。所以直接加载肯定会报错的,因为模型结构对不上。

    所以正确的方法是保存的时候只保存net.module:

    torch.save(net.module.state_dict(), "./8.4_model.pt")
    new_net.load_state_dict(torch.load("./8.4_model.pt")) # 加载成功
    

    或者先将new_netDataParallel包括以下再用上面报错的方法进行模型加载:

    torch.save(net.state_dict(), "./8.4_model.pt")
    new_net = torch.nn.Linear(10, 1)
    new_net = torch.nn.DataParallel(new_net)
    new_net.load_state_dict(torch.load("./8.4_model.pt")) # 加载成功
    

    注意这两种方法的区别,推荐用第一种方法,因为可以按照普通的加载方法进行正确加载。


    注:本节与原书基本不同,原书传送门

    作者:鄂河
    声明:本博客所有文章均来源于网络或本人原创,仅用于学习用途,欢迎评论区讨论,会逐一完善内容。
  • 相关阅读:
    蛙蛙推荐:JS里声明事件处理的几种方式
    蛙蛙推荐:asp错误处理
    蛙蛙推荐:偶心目中的编程高手,大家也推荐一下
    access能否用vbs来写存储过程,是否支持参数名称 【星期一 2005年7月4日】
    MySQL索引相关
    ubuntu 禁用触摸板
    ubuntuFQ(转)
    bash编程笔记
    Hive环境搭建与入门(转)
    Linux SSH远程文件/目录传输命令scp
  • 原文地址:https://www.cnblogs.com/panghuhu/p/14269152.html
Copyright © 2011-2022 走看看