Theano2.1.14-基础知识之理解为了速度和正确性的内存别名

来自:http://deeplearning.net/software/theano/tutorial/aliasing.html

Understanding Memory Aliasing for Speed and Correctness

    内存的重用是theano提升代码运行速度的一种方法,而且理解theano如何别名(alias)缓冲区对编写的程序速度的提升和正确性的保证很重要。

    这部分是基于theano处理内存的基础上来说明原则的,而且解释了为了获得更快的性能,在什么时候该改变某些函数默认的行为和方法。

一、内存模型:两个空间

     可以通过一些简单的原则来指导theano的函数处理。首先,theano会管理一个内存池,并且在这个池中theano会追踪值的变化。

  • Theano 会管理它自己的内存空间,并且通常不会与非theano代码的python变量的内存相重叠。
  • Theano 函数只修改在theano内存空间中的缓冲区。
  • Theano的内存空间包括分配给存储shared变量和临时用来执行函数的缓冲区。
  • 物理意义上来说,theano的内存空间会跨越主机,GPU设备,而且在未来可能会涉及到远程机器上的对象。
  • 分配给shared变量的内存缓冲区是唯一的:它不会被另一个shared变量所别名(aliased)。
  • Theano管理的内存在theano函数未运行和theano的库代码未运行的时候是保持不变的。
  • 一个函数的默认行为是返回用户空间的变量作为输出,然后期待用户空间的变量作为输入。

    介于theano管理的内存和用户管理的内存的区别可以通过一些theano函数(例如,sharedget_value 和针对于In和Out的构造函数) borrow=True flag来细分。这可以让一些方法在冒着微妙的bug的风险下在整体的程序 (通过别名内存)上更快 (避免了复制操作) 。

    该章节剩下的部分意在帮助你理解什么时候使用 borrow=True 参数是安全的,并且可以让代码更快的运行。

二、Borrowing when 创建共享变量

     borrow 可以作为共享变量构造函数的参数:

import numpy, theano
np_array = numpy.ones(2, dtype='float32')

s_default = theano.shared(np_array)
s_false   = theano.shared(np_array, borrow=False)
s_true    = theano.shared(np_array, borrow=True)
    默认情况(s_default) 和显式的设置borrow=False 这两种情况下,构造的共享变量是对np_array进行深度复制的。所以我们后续对np_array的改变不会影响到这两个共享变量。

np_array += 1 # now it is an array of 2.0 s改变的操作

s_default.get_value()  # -> array([1.0, 1.0])
s_false.get_value()    # -> array([1.0, 1.0])
s_true.get_value()     # -> array([2.0, 2.0])

    如果我们在cpu上运行这段代码,那么我们对np_array的改变可以通过 s_true.get_value()看到,因为Numpy arrays是可变的,而且s_ture使用了np_array对象作为它的内部缓冲。

    然而,对np_array的别名和 s_true 没法保证这种情况一定发生,也许会临时性的发生,也许根本不发生。 没法保证是因为当theano使用的是gpu的时候,那么borrow 这个flag是没有任何影响的。只能临时性的发生是因为如果我们调用一个theano函数来更新s_ture的值,那么这个别名关系也许会,也许不会被打破 (该函数允许通过修改它的缓冲区来更新这个shared变量,这会保留这个别名,或者改变这个变量指向的缓冲区,而这会终止这个别名)。

要点(Take home message:):

   当shared变量代表一个大的对象的时候,在shared变量的构造函数中使用borrow=True是安全的操作(也是好主意) (在内存占用方面) ,而且你不需要在内存中对它进行复制。

    想要利用副作用,从而通过使用 borrow=True 来修改 shared 变量的方法不是一个别名技术,因为在一些设备上 (例如GPU 设备)该技术不会生效的。

三、Borrowing when 访问共享变量的值

检索

     borrow 参数可以同样用来控制如何检索 shared 变量的值。

s = theano.shared(np_array)

v_false = s.get_value(borrow=False) # N.B. borrow default is False
v_true = s.get_value(borrow=True)

    当 borrow=False 传递给 get_value, 的时候,意味着返回的值不会是对theano的内部的内存进行别名。当borrow=True 传递给 get_value的时候,意味着返回的值可能是对theano的内部的内存的别名。不过这两种调用都会对内部的内存进行复制,产生副本。

   使用 borrow=True 还仍然会进行复制的原因是因为shared变量的内部的表达并不是和你想的一样。当你通过传递一个NumPy 数组来创建一个shared变量的时候,例如,然后 get_value() 也必须返回一个NumPy 数组。这就是为什么theano可以让gpu的使用看起来透明的原因。不过当你使用gpu的时候(或在未来可能是一个远程机器),那么 numpy.ndarray 就不是你数据的中间表示了。如果你真的想要theano返回它的中间表示且不想对它进行复制,那么你应该对get_value函数使用return_internal_type=True 参数。它将不会 cast内部对象 (总是在常量时间内返回的),不过也许会由环境因素(例如:计算设备,Numpy数组的dtype)而返回各种不同的数据类型。

v_internal = s.get_value(borrow=True, return_internal_type=True)

   可以将 borrow=False 和 return_internal_type=True结合起来使用 ,这会返回内部对象的一个深度复制。这对内部的调试来说是很重要的,而不是对通常的使用而言的。 

    我们可以透明使用theano对不同类型的优化,有个原则就是当shared变量创建之后, get_value() 在默认情况下总是会返回和它接受的同一个对象类型。所以如果你在gpu上手动创建了数据,然后用这个数据在gpu上创建一个共享变量。 当return_internal_type=False的时候get_value总是会返回gpu上的数据。

要点(Take home message:):

      当你的代码没有修改返回的值的时候使用get_value(borrow=True) 是安全的 (而且有时候也更快)。不要使用副作用(side-effect)来修改一个“shared”变量,因为它会让你的代码具有设备依赖。通过副作用的方法来修改gpu变量是不可能的。

分配

    Shared 变量同样有一个 set_value 方法可以接受一个可选的 borrow=True 参数。该语义相似于那些创建新shared变量的语义, borrow=False 是默认的,而且 borrow=True 意思是theano可能会重用作为变量内部存储的缓冲区。

    手动更新shared变量的值的一个标准的模式是:

s.set_value(
    some_inplace_fn(s.get_value(borrow=True)),
    borrow=True)

      该模式工作的时候是不管计算设备是什么的,当后者(应该说的是设备)可能会在没有复制的情况下暴露theano的内部变量,那么它就会和in-place一样快的速度进行更新。

    当在gpu上分配 shared 变量的时候,gpu与主机之间的内存迁移是很耗时的。这里是一些提示,用于却跑如何快速和高效的使用gpu的内存和带宽:

  • 在Theano 0.3.1之前, set_value 在gpu上不是以in-place方式工作的。也就是说,有时候gpu对于旧的内存没有释放之前就对新变量进行内存的分配了。如果你在gpu内存上运行的已经接近于上限了,这可能会导致你得到gpu内存已经耗尽的提示。

    解决方法:更新到最新的theano。

  • 如果你打算反复的对一个共享变量的进出数据块进行交换,你可能会想要重用第一次分配的内存I,这可以更快而且更好的使用内存

         解决方法: 更新到最新版本的theano (>0.3.0)并考虑 padding源数据来确保每个块都有着相同的size。
  • 同样值得提到的就是,当前gpu的复制只支持连续的内存。所以theano必须先将你提供的值转换成c-连续形式,然后在对它进行复制。这需要额外的在主机上对数据进行复制。

    解决方法:确保你想要赋值给 CudaNdarraySharedVariable 的数据已经是C-连续了。

    可以在sandbox.cuda.var – The Variables for Cuda-allocated arrays上面找到当前gpu版本的set_value()的实现过程。

四、Borrowing when 构造函数对象

     borrow 参数同样可以提供给 In 和 Out 对象来控制theano.function 是如何处理它的参数 argument[s] 和返回值value[s]的:

import theano, theano.tensor

x = theano.tensor.matrix()
y = 2 * x
f = theano.function([theano.In(x, borrow=True)], theano.Out(y, borrow=True))

    Borrowing 一个输入意味着theano会将你提供的参数暂时的视为是theano池的一部分。因此,在执行函数(例如,f)的时候,在对其他变量进行计算的过程中,你的输入可能会作为一个缓冲区而重用(和重写)。

    Borrowing 一个输出意味着theano不会在每次调用函数的时候坚持分配一个新的输出缓冲区。它可能会在之前的调用的基础上重用同一个,并重写旧的内容。因而,它会通过副作用来重写旧的返回值。这些返回值也会在执行另一个编译好的函数上被重写 (例如,输出会被别名成一个shared变量)。所以在调用更多的theano函数之前,小心使用一个 borrowed 返回值。默认情况下当然是不borrow 内部结果的。

    同样可以传递一个 return_internal_type=True flag 给 Out 变量,该变量有着和对shared变量的get_value函数设置return_internal_type flag时一样的解释。不同于 get_value(), return_internal_type=True 和 borrow=True 参数的结合,然后传给 Out() 不能保证说避开了对一个输出值的复制。他们只是隐式的对graph的编译和优化提供了更多的灵活性。

对于 GPU的graphs来说,borrowing是影响速度的主要因素:

from theano import function, config, shared, sandbox, tensor, Out
import numpy
import time

vlen = 10 * 30 * 768  # 10 x # cores x # threads per core
iters = 1000

rng = numpy.random.RandomState(22)
x = shared(numpy.asarray(rng.rand(vlen), config.floatX))
f1 = function([], sandbox.cuda.basic_ops.gpu_from_host(tensor.exp(x)))
f2 = function([],
              Out(sandbox.cuda.basic_ops.gpu_from_host(tensor.exp(x)),
                  borrow=True))
t0 = time.time()
for i in xrange(iters):
    r = f1()
t1 = time.time()
no_borrow = t1 - t0
t0 = time.time()
for i in xrange(iters):
    r = f2()
t1 = time.time()
print 'Looping', iters, 'times took', no_borrow, 'seconds without borrow',
print 'and', t1 - t0, 'seconds with borrow.'
if numpy.any([isinstance(x.op, tensor.Elemwise) and
              ('Gpu' not in type(x.op).__name__)
              for x in f1.maker.fgraph.toposort()]):
    print 'Used the cpu'
else:
    print 'Used the gpu'
结果:

$ THEANO_FLAGS=device=gpu0,floatX=float32 python test1.py
Using gpu device 0: GeForce GTX 275
Looping 1000 times took 0.368273973465 seconds without borrow and 0.0240728855133 seconds with borrow.
Used the gpu

要点(Take home message:):

    当在函数返回值后,输入x 对于函数来说是不需要的,你可以将它做为额外的工作空间,然后考虑使用 In(x, borrow=True)来对它进行标记 。它可以让函数变得更快,而且减少对内存的需求。当一个返回值y 很大(内存占用方面),你只需要当它返回的时候,读取它一次然后考虑对它进行标记 Out(y, borrow=True)。

参考资料:

[1] 官网:http://deeplearning.net/software/theano/tutorial/aliasing.html


posted @ 2015-06-18 15:46  仙守  阅读(2576)  评论(0编辑  收藏  举报