在图像、语音识别、自然语言处理、强化学习等诸多技术领域,深度学习已被证明非常有效,在某些问题上已经达到甚至超越了人类水平。然而,深度学习对计算能力有很大的依赖性。除了改变模型和算法,深度学习计算能否从系统层面进行优化,提高计算资源的使用效率?在这篇文章中,微软亚洲研究院异构计算高级研究员吴明与大家分享了他对深度学习计算优化的一些看法。深度学习近年来取得了长足的进步,已经或有望在我们日常生活的诸多场景中得到成功应用,例如自动驾驶、安防、翻译、医疗等。可以说,计算机计算和通信能力的大幅提升是深度学习成功的重要因素。深度学习为什么要依赖超大算力?首先,深度学习本质上是一门以统计学为基础的科学,因此大规模的样本数据对于深度学习的效果至关重要。其次,更大、更复杂的神经网络模型被证明是非常有效的,并被广泛应用于产品中,这也对计算能力产生了更大的要求和消耗。例如,具有8层神经元的AlexNet网络在2012年的ImageNet数据集上实现了16%的错误率,网络的一次迭代需要大约1.4GFLOPs的计算。微软提出的使用152层神经元的残差网络(ResNet)在2015年在此数据集上实现了3.5%的错误率,一次迭代的计算量约为22.6GFLOP,是AlexNet的16倍。在当今的生产环境中,与图像、语音和自然语言处理相关的模型,如人脸识别、语音转文本、机器翻译等,即使给予相当大的计算资源,许多模型仍然需要数周才能完成训练.同样,深度学习模型是快速迭代的。在人工智能领域,每年学术界和工业界都会提出大量新模型。对于每一个实际问题,开发者都需要不断尝试不同的模型和算法,即使是同一个模型算法,也需要反复调试超参数以获得最佳的预测效果。可以想象,如果模型的每次训练都需要几周的时间,那么寻找最佳模型的过程将是非常漫长和痛苦的。此外,模型的在线推理对性能要求更为严格。在线服务具有严格的服务水平协议(SLA),因此在部署大规模模型时,需要手动重新优化已经在深度学习框架(如TensorFlow)上训练过的模型,从而导致大量额外工程开销。可见,深度学习计算的进一步优化对深度学习的快速发展和成功应用起着至关重要的作用。深度学习计算优化的挑战与机遇目前,深度学习计算优化存在几大挑战:1)单个计算单元(如GPU)的资源限制往往不能满足对大规模数据和模型的处理需求,所以需要使用多台机器和多个计算单元来横向扩展计算规模。我们如何才能最小化通信开销并最大化多台机器的并行性?2)如何优化神经网络的计算,使其能够最大限度地发挥单个硬件计算单元的效率?3)很多硬件计算单元(GPU、FPGA等)虽然具有强大的计算能力,但是它们的内存资源(即设备内存)却非常稀缺。当它们无法提供模型运行所需的内存资源时,要么无法继续计算,要么计算所需的数据需要在主存和设备内存之间传输,导致运行开销较大。我们如何才能更好地利用有限的设备内存资源,以免对计算效率产生负面影响?4)深度学习开发者和研究者通常只想专注于神经网络模型和算法本身,不想被复杂的优化问题分散精力。这意味着深度学习框架等系统软件可以实现自动优化,对模型开发者是透明的。那么,如何对具体的优化进行合理的抽象,使其更灵活、更容易集成到系统框架中,是一个需要认真思考的问题。 其实任何优化问题都可以从两个角度来看:模型算法和系统。一方面,我们可以通过改变模型和算法来提高其运行速度,优化其计算资源的使用效率。这种优化往往对特定的算法非常有效,但不容易扩展和应用到其他算法。另一方面,也就是微软亚洲研究院异构计算组正在进行的研究是在系统中实现模型算法无关的优化。这样的优化通常可以为更多的应用带来性能上的好处,同时也满足了我们前面提到的透明度的要求。用系统优化助力深度学习计算为了更好的理解系统这一层的优化,我们先简单介绍一下深度学习框架系统的背景知识。目前业界流行的深度学习系统(包括TensorFlow、PyTorch、CNTK、MxNet、Caffe等)大多采用分层架构设计。在前端提供一种高级语言(如Python)的接口抽象,让用户可以方便地描述神经网络结构,即深度学习的模型。描述良好的模型在被系统运行之前,首先会被转换成数据流图(Data-flowGraph)。在这个数据流图中,节点是具体的矩阵运算(即Operators,如Sigmoid、MatrixMultiplication等),连接不同节点的边是运算节点的输入输出矩阵。这个数据流图也可以看作是深度学习计算的中间表达。然后,深度学习系统的后端将这个数据流图映射到实际的硬件上进行高效执行,大部分的系统级优化都是在这个阶段完成的。加速分布式深度学习训练分布式训练的主要瓶颈是多台机器之间的通信开销。如今,计算机网络的硬件技术得到了很大的发展。InfiniBand的RDMA网卡(RemoteDirectMemoryAccess,是一种硬件网络技术,无需远程机器上的CPU干预,就可以让计算机访问远程内存)已经可以提供50~100Gbps的网络带宽和微秒级的传输延迟。当前许多针对深度学习应用程序的GPU队列都部署了此类网络。然而,深度学习系统如何充分利用硬件提供的通信能力,在分布式训练中实现更大的性能提升呢?另外,使用RDMA软件接口进行通信,可以绕过TCP/IP协议栈,减少操作系统内核。状态在头顶运行。在这种网络通信技术的支持下,任何与通信相关的计算处理开销都将变得非常显着,而这正是许多原本基于TCP/IP设计的网络通信机制所存在的问题。RPC(RemoteProcedureCall,远程过程调用)是一种广泛使用的多机间通信的抽象原语,其主要设计目标是通用性。在不考虑RDMA的情况下,很多深度学习框架都会使用RPC机制(比如gRPC)来实现多机之间的通信。但是RPC需要维护一个内部私有缓存,所以不得不在用户数据存储空间和内部缓存之间引入数据拷贝。使用RDMA网络时,这种内存复制开销变得非常明显。我们通过微基准测试观察到,与使用基于TCP/IP的gRPC相比,直接通过RDMA接口传输消息(针对不同的消息大小)可以有2到10倍的性能提升。那么对于深度学习的应用负载,如何更好的利用RDMA硬件的能力呢?首先分析一下深度学习应用的几个特点:1)Tensor是深度学习计算中最重要的数据结构,大量的计算开销都花在了处理Tensor上。Tensor是一个比较简单的数据结构,主要由两部分组成:meta-data和payload。Payload是一个基本元素的数组,meta-data是Tensor的形状信息,即维度和每个维度的大小。这种简单的数据结构在传输过程中其实不需要复杂的序列化和反序列化函数。2)在相当多的情况下,Tensor是dense的,它的size比较大,也就是说在传输这样的Tensor的时候不需要额外的batch处理。3)深度学习的训练过程是迭代的。每次迭代处理一个小批量。在不同的迭代之间,很多Tensor的数据流图和形状信息是不变的,很多形状信息可以在运行前静态确定。基于以上特点,我们可以分析数据流图,找到那些可以静态确定形状信息的Tensor,这样在运行前,可以在接收端预分配RDMA可访问的内存空间,并对应可用的远程访问地址被传递给发送者。这样,在运行时,发送端可以通过单向RDMA请求直接将Tensor数据传输给接收端,从而完全避免了不必要的额外内存拷贝,实现了零拷贝通信过程。我们在TensorFlow上对该机制进行了实验,与基于TCP/IP的gRPC相比,该方法在一系列典型模型上取得了多项性能提升。即使与针对RDMA优化的gRPC相比,我们的方法仍然可以实现超过50%的性能提升。另外,我们在分布式深度学习方向关注的另一个问题是如何自动优化资源无关数据流图的分布式执行,即自动划分数据流图中的计算任务,并为其分配相应的任务.计算资源最大化计算效率。Google的JeffDean团队在这方面做了很好的开创性工作。但仅限于模型并行、单机多卡的运行环境。目前,这仍然是一个非常重要和有前途的方向。需要结合数据并行、分布式、异构环境综合考虑。提高单个计算单元的计算效率前面提到,使用深度学习框架实现的模型算法在运行前会转化为数据流图。很多有实际应用价值的模型都非常复杂,由它们转换出来的数据流图通常由上千个运算节点组成,其中包括很多计算量非常小的节点,也就是说它们的输入矩阵的大小是小,或者其计算逻辑的复杂性相对于访问输入数据的复杂性较低。大量此类操作节点会引入一些运行时开销,如下所示,并且此类开销可能很大。1)深度学习系统在运行时,需要根据数据流图中节点的依赖关系来调度节点的执行。调度每个节点的系统开销与运算节点的计算量没有直接关系,所以对于由很多小运算节点组成的计算流图,系统调度带来的额外开销会比较大;2)对于运行在GPU上的计算,每一个运算节点的实现都对应一个GPU核函数,而这个核函数的每次执行都需要CPU调用显卡驱动启动,所以也带来了恒定的数量级额外费用。与计算量小的核函数的执行相比,这个开销是非常明显的;3)计算量小的运算节点往往难以挖掘出足够的数据并行性,从而无法充分利用处理器硬件中的计算资源。解决这个问题的主要思路是KernelFusion。一些手动优化方法使用了这种思想,例如NVIDIA基于CuDNN的RNN库函数。它将整个递归神经网络实现为一个GPU核函数,从而获得非常好的性能。但是,它的缺点也很明显,即不够灵活和通用,无法应用于其他网络或循环神经网络的一些变体。我们更关注的是如何自动优化深度学习系统中的任何网络模型。目前,学术界和工业界已经有一些系统采用编译的方式生成融合内核代码,如TVM、Halide、Taco等。这些系统使用TensorAlgebra作为前端表示方法,然后可以将每个TensorAlgebra表达式编译成相应的内核代码。TensorAlgebra可以作为底层的中间表达式集成到深度学习系统中,也就是说可以将高层的数据流图转化为由TensorAlgebra表达式组成的代码块,然后编译成可执行代码。但是,这些系统对可以融合的运算节点有很多限制,不能很好地融合多个非逐点运算,例如多个矩阵乘法运算。然而,我们发现打破这个限制并融合更多的操作节点可以带来更显着的性能提升。在GPU环境中融合多个非pointwise操作具有挑战性,因为在一个non-pointwise操作中输入矩阵的每个元素可能依赖于前一个操作的输出矩阵中许多不同位置的元素的值,所以一个Barriersynchronization需要在这两个操作之间插入原语。Barrier在GPU中的实现需要保证内核的所有线程块在运行时都保持活跃,这意味着我们必须要求融合内核使用有限数量的线程块,但同时能够处理远不仅仅是线程块。块中数据块的数量。为了解决这个问题,我们尝试采用persistent-thread线程块模型,即启动固定数量的线程块,并在fusedcore的整个生命周期内保持活跃。我们的优化系统在生成融合内核代码的过程中类似于解决一个bin-pack问题,即将待融合的子数据流图中每个操作节点要处理的数据块分配给合适的Active线程块,使每个线程块的负载尽可能均衡,保持原数据流图中各操作节点操作的并行性。为了生成优化的GPU核函数,一个重要的考虑因素是线程块和数据块的合理划分。然而,这取决于一些非常复杂的因素,例如操作节点操作中的计算和内存访问复杂度的比例,GPU的共享内存大小,寄存器文件的大小以及分配方法。因此,很难静态地确定最佳选择。幸运的是,深度学习的迭代特性以及需要相当多的迭代才能收敛,使我们能够使用早期的迭代过程来收集运行时动态信息,以帮助优化系统以做出更明智的决策。克服设备内存资源限制设备内存的大小通常会限制可以处理的模型的大小。解决这个问题的一种方法是压缩和量化模型。如今,学术界和工业界的大量研究工作提出了不同的压缩和量化方法。然而,在实际应用场景中使用压缩和量化仍然是一个繁琐的迭代过程。在这个过程中,用户可以尝试以下几个方面。1)压缩方式不同。比如是根据模型的参数值是否趋于零,还是转化为一定的贡献值后是否趋近于零?压缩是否考虑了某种结构(如果是面向GPU的,可能需要压缩成块稀疏矩阵来提高运行效率)?量化后的值点是按照均值区间划分的还是基于某种聚类的?2)不同程度的压缩。需要考虑压缩了哪些层的神经元参数,因为并不是所有的层都对压缩模型的效果同样敏感;选择不同的压缩比或量化位。3)为了在大压缩比下保持良好的模型效果,压缩过程可能需要循序渐进,比如一次压缩10%,然后重新训练,重复这个过程,直到达到目标压缩比。那么每个渐进过程的压缩率就是一个需要调整的参数。显然,如此繁琐的过程需要一个好的工具来方便。这也是我们组正在关注的一个问题。我们正在尝试扩展TensorFlow的API,让用户可以在模型脚本中直接控制量化和压缩的方法、对象、程度和过程。压缩和量化通常用于解决模型部署时内存资源不足的问题,而解决模型训练时内存不足问题的思路之一就是用计算代替内存。比如数据流图中某个运算节点的计算量很小,但是输出的中间结果量很大,那么更好的处理方式是不把这个中间结果存储在内存中,而是留到以后使用的时候重新执行本次运算节点的计算。当然,重新计算仍然会引入一些额外的开销。其实还有另外一种方法可以解决这个问题,就是将大的输入数据存储在CPU的主存中,将运算节点实现为流处理,将大的输入数据拷贝到GPU段的设备内存中,以及通过异步拷贝,每段的计算时间和下一段的拷贝时间可以重叠,从而掩盖数据拷贝的开销。对于矩阵乘法等操作,由于计算复杂度高于内存访问复杂度,当段很大时,计算时间和复制时间可以达到最大重叠。但是,如果要执行的运算不是矩阵乘法,而是一些简单的逐点运算,那么计算复杂度就无法用内存复制开销来抵消。所以这个方法还需要和kernelfusion结合起来。例如,将矩阵乘法和后面的逐点运算集成在一起,每一段的计算都会完成该段的矩阵乘法和逐点运算,然后再处理下一段。
