Theano与其他深度学习框架的比较,主要强调一系列低水平的设计选择,没有特别顺序。
概述
微分
计算图形/IR
优化
调度
GPU/CPU透明化
外部函数接口
动态编译/元编程
Theano 心愿列表
微分
符号微分:Theano,CGT;自动微分:Torch, MXNet
虽然符号微分和自动微分实现的方式完全不同,但在实际使用中符号微分和自动微分总是被混淆或者交替出现
反向自动微分是反向传播的一般化 [1].当需要计算梯度时,输入是向前传播通过计算图时,各个节点记住的值。然后进行图的反向遍历,该方法称为每个节点的“backward(反向)”方法,它使用梯度(相对于节点的输出)作为参数。
另一方面,符号微分取决于每个运算符有其自己的梯度定义,即构建图的相同符号运算符。当某个参数的梯度需要计算时,就会反向遍历节点,每个节点都返回一系列扩展计算图的符号运算符。最后的结果是一个扩展的计算图,它定义了一个从输入到梯度的路径。
图形尺寸
如果采用自动微分法,同一个图需遍历两次,同时需要优化和编译一半的图。
调试
自动微分法更容易调试,因为在用户调用每个操作符的某一个点上都会产生一个错误。使用符号微分,新的操作符可能作为梯度计算的一部分出现,这对用户来说定位错误抛出点比较困难。
优化
符号微分需要通过图的彻底优化达到诸如删除公共子表达式的目的,而自动微分不需要。然而,符号微分法可能要考虑更多的优化,如操作符在向前传播时不能融合,而在反向传播时可以融合。
记忆
自动微分需要每个操作符来记住它的输入(至少,原始的实现是这么做的[2])。记忆优化后的符号微分的记忆效率可能更高。
高阶导数
符号微分法的优点在于只有前向R-op需要实现。而使用自动微分法,我们需要实现前向和后向R-op[3].
灵活性
当我们有多个损失函数且需要有一个符号表示梯度以进行进一步处理时,符号微分似乎更加灵活。自动微分法支持多种情况,但为了能够进一步给出梯度的符号表达式,需要对框架进行扩展。
[1]Automatic differentiation in machine learning: a survey
[2]The Data-Flow Equations of Checkpointing in Reverse Automatic Differentiation
[3]torch/nn#399
计算图形/IR
许多深度学习的库都依赖于计算图能力,它可以作为我们程序的中间表示( intermediate representation, IR)。图的惰性构造可以优化(Theano, CGT),调度(MXNet),或者自动微分(Torch, MXNet) 。问题在于此图应该包含什么信息。
形状
在构造图时定义一个张量的的形状,这么做会显著的降低灵活性(例如,小批量有不同的形状)。另外,形状信息可以完全地从图中排除,这是theano目前的情况(除了一些诸如卷积的特殊运算)。 [*] 中间区域使得规范化的形状信息可选。这可以用于优化目的:如果一些张量确定是固定大小的,那么他们可以在编译前就分配(从内存效率性能考虑)[4].
内存分配
内存分配是IR组成的一部分,这意味着我们可以用y = x + 1代表Var(x), Constant(1) -> [Add] -> y, 或者代表const_storage = Allocate((1,)); y_storage = Allocate(x.shape); const_storage, x_storagey_storage -> [Add] ->y_storage。无论是哪种方法,优点都是不明确的。
循环和分枝
循环和分枝可以用不同的方式来实现。Theano的方法看起来是通过把环当做单结点来实现计算图的无环性(scan运算符),其本身包含另一个计算图。分支由任务调度器处理(在Theano中常常也称作虚拟机)。其他的大多数框架不支持循环和分枝。一种更加有说服力的处理循环的方法是在计算图中允许循环。为了保持严格的静态单一赋值(SSA,[ 5 ])的形式,需要实现Phi函数(或是类似的东西[ 6 ])。保持图形平面使循环更具扩展性,例如允许嵌套循环。
设备独立性
Theano尝试支持各种后端,在Theano中,他们分享相同的IR,这在优化阶段转化为一个指定设备的表示。与设备独立的优化通过设置优化的“等级”把指定设备分离。这种编程方式可以使分离更充分。通过这种编译器,我们将有一个IR优化器和一个完全独立的设备特定的优化器和(或者)任务调度器。
[5]Wikipedia: Static single assignment
[6]WebKit code
优化
Theano特点的之一是它的优化引擎,这既保证了计算效率,也保证了数值稳定性[7]。只有CGT模仿这种行为;其他框架不包含优化框架。一个明显的并行可以从优化文献中得出:
公共子表达式消除(Common subexpression eliminiation,CSE,在Theano中使用merge 函数)
常量折叠(Constant folding)
常量传播(Constant propagation)
代数简化(Algebraic simplification,加,乘等等)
代数重新关联(Dead store elimination,称为“规范化”)
死存储消除(Dead store elimination,Theano将丢弃不需要计算指定输出的部分计算图)
操作强度降低/局部优化(Operator strength reduction/local optimizations,例如合并多个操作符成一个单一的GEMM调用)
Theano应用更加独特的是数字稳定性优化(这违反一个编译器产生语义等价输出的概念)。
非逐元素融合(Non-elementwise fusing)
一个有趣的想法是评估操作符non-elementwise融合的可能性,例如矩阵与矩阵乘法有一个共同的结合面,例如Y = Wx和Y’= W’x可以计算单一调用[Y| Y’] =[W | W’]x(LSTM在模块中的实现技巧)。这样做涉及内存分配优化。
调试
一个优化框架使得调试复杂化,因为结果计算图和用户构造图的不同使得它很难确定哪个确切指令造成了错误。
性能
优化的主要目的是提高性能。对于Theano,有和没有优化的差异显著。然而,其他的框架可以实现和没有优化的Theano框架相似的速度。这可能是由于Theano细粒度(finer-grained)运算符(相对于其他框架更独立的“层”),或是它们对于Theano来说是一种内在固有的低效率。
可用性
一个优化的框架需要用户较少的专业知识;低效操作符的自动优化,允许用户集中于构建模型,而不是优化其代码的效率。
调度
任务调度(或指令调度,并行与优化文献)是在计算图节点(操作符)已被优化之后(必选,可选,编译)执行的调度。显著增益可以在这里通过使用并行获得(CUDA中的streams[8],CPU中的threads)。许多新的框架都集中这个方法上,例如MXNet [9], Purine [10], Minerva [11]等等。
[8]CUDA streams
[9]MXNet’s “dependency engine”
[10]Purine: A bi-graph based deep learning framework
[11]Minvera’s “dataflow engine”
多GPU
调度程序还可以编写支持多GPU的情况。自动模式的并行性是远远不足以解决问题的,而数据的并行可以采用一种简单的方式来调度。
异步调用
理想情况下,调度器应该异步调用。例如,数据传输和计算可以并行执行的,所以我们在原来的数据仍在计算时,在GPU加载数据。在某些情况下,可以取得显著的结果。明智地编码及接口,比当前的方法更复杂。
CUDA 流与事件
CUDA内核可以同时发出多个单独的CUDA流,从而并行执行。它们还可以做出同步让它们等待事件。非常想知道通过这些方法可以受益多少(如通过调用诸如cudaEventQuery来比较采取0流[0 stream]还是同步流[ synchronizing streams])。
GPU/CPU 透明化
CPU和GPU之间可以通过多种方法进行交互。
完全透明化(Fully transparent)
Theano不仅尝试在需要时通过交互来完全地透明化卸载GPU,而且可以完全透明地把数据转入和转出主机。例如,当一个特定的操作符仅在CPU上实现时。好处是即使不是在GPU上实现的运算,仍然可以利用GPU来加速。缺点是Theano在用户未意识到的情况下给主机传递数据表现欠佳(例如,他们使用了高级索引)。这意味着用户仍需编写”GPU-runnable”Theano代码。
显式传输(Explicit transfers)
Torch 使用了一种更明显的方法。GPU-enabled操作符只接受GPU数组,反之也是如此。在GPU移动张量仅仅需要调用张量的cuda()或者double()函数[†]。 优点是用户能够完全意识到正在执行计算任务的设备。缺点是脚本需要通过把张量转移给GPU来占用GPU。
互斥模式(Exclusive mode)
最后一个选项是在单个计算图中不允许CPU与GPU之间的传输。缺点是,如果用户真的需要在CPU上执行一部分计算,他们需要需要构建和编译独立的计算图。优点是用户能够完全意识到传输的过程。在内部,这种约束可以简化代码。这也是我相信Caffe的一面。
需要注意的是Torch并没有一个图形编译器或者任务调度器,这意味着用户任何时候都可以以用一条命令来调用 cuda()或者 double()终止计算。很难想象这种在GPU-CPU之间的显性传输在完全计算图方法中是怎样的。
外部函数接口
Theano:C扩展 , CGT: Cython, scikit-cuda: ctypes, cuda4py: CFFI
Python和C语言的之间的接口有各种各样的方法。
C 扩展 (Python C API)
Theano的当前做法,资源开销较少,支持较好,但引入了一定的复杂性,如:用户可以负责传递Python对象的引用计数。
Ctypes
部分标准库,可以使用诸如scikit-cuda形式的方法。特别简单,支持度较好并且仅要求Python代码。然而,会出现很大的开销(与C-API相比,开销会从50%提升至250%)。
CFFI
PyPy的首选方式是与C做接口,这个灵感是来源于LuaJIT的 FFI(这是Torch使用Lua语言编写的主要原因)。这种方式比较冗长,但是速度很快而且简单,cuda4py库采用的就是这个。
Cython
当前流行的框架将Python版本的扩展(注释)编译成C扩展。Cython在库CGT中被采用。[15]
Numba
尽管Numba不直接提供C语言的直接接口,但是它的JIT编译器支持把Python函数和ctypes [16] 与CFFI[17]一起调用编译,以此来加快速度。
[12]http://www.dalkescientific.com/writings/NBN/c_extensions.html
[13]http://stackoverflow.com/a/8069179
[14]cuda4py
[15]cycgt.pyx
[16]Numba ctypes
[17]Numba cffi
动态编译/元编程
上述库可以动态编译操作符(如Theano),也可以简单地调用预编译的操作符(如Torch)。
性能
内核和C函数的动态编译使他们能够完全专业化,跳过不必要的输入检查,少用一般的实现,而是用更有效的实现。像cuDNN和cuBLAS框架,使用手动调整内核减少动态编译的需要,因为它们往往比自定义编写那些更加有效。
编译时间
具有动态编译的操作引入了在评估之前等待函数编译的需要。这可以通过使操作的非编译版本来减轻(例如,一些Theano操作符有numpy的实现),但要注意,这会引起维持每个操作符多个实现的负担,并需要操作符的行为相同。
核融合
有了动态编译的内核允许,例如element-wise操作可以被融合到单个内核中。对于GPU编程,这可以是有益的,因为启动内核和从全局将数据传输到共享存储器开销可能非常大。在Theano,这被称为“elemwise fusion”。
外部语言接口
如果计算图形的多个节点可以一起编译,那么它可以限制来自主机语言的函数调用次数(例如Python中的Theano,Lua中的Torch)。对于像Python这样的语言,具有相对缓慢的FFI和显著函数调用的开销,这么做会显著提高效率。
鲁棒性
动态编译操作在管道中引入了一个编译步骤,它可以是错误的源,例如(库)路径一定要设置正确,在编译过程中会出现的内存问题等等。动态编译的操作符难以调试,因为重现这个错误可能需要重现在该操作符被编译下的确切条件。
共享库
CGT编译在运行时被动态链接的分享库,而Theano编译的是一个Python C-API扩展(需要python和numpy头文件)。后者显著更慢。
Theano 心愿列表
坚持使用符号微分。
没有更多的动态编译(除了逐元素内核可能融合);像cuDNN手动调整的库已经减少了这种方法的优点,它明显地简化了代码,等待模块进行编译。
支持计算图形循环,来自phi函数的全部SSA。
GPU任务调度支持CUDA流和异步调用。
CFFI或者Cython接口;C-API更复杂,如果一些更人性化的替代品表能够以类似的速度执行,那么就会带来一些优势。
独占CPU/GPU模式;简化代码并移除意外减慢的源。
从后端优化严格分离IR。
蓝天思维:Non-elementwise的内核融合,需要智能的内存分配。
原文链接:A comparison of deep learning frameworks
译者:刘帝伟 审校:刘翔宇
责编:周建丁(投稿请联系zhoujd@csdn.net)
2016年3月18日-19日,由CSDN重磅打造的数据库核心技术与实战应用峰会、互联网应用架构实战峰会将在上海举行。这两场峰会将邀请业内顶尖的架构师和技术专家,共同探讨高可用/高并发系统架构设计、新技术应用、移动应用架构、微服务、智能硬件架构、云数据库实战、新一代数据库平台、产品选型、性能调优、大数据应用实战等领域的热点话题与技术。(报名参会)