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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM