Numpy / Theano で行列の積を高速に計算するには
オープンキャンパス,研究会出張,学科会議+教授会,企業訪問,と続いた日々が昨日で一段落して,今日はだらだらと後始末(諸々の報告書作成)とか某誌編集委員の仕事とか.それもある程度片付いたので,ちょっとだけ研究.Pylearn2 の tutorial でお勉強 - 4時間目 - まんぼう日記 のつづきです.夏休みの自由研究としてはじめたのに,もう休み終わりや〜ん.
さて.Pylearn2 の tutorial にある convolutional net のサンプルを動かしたら遅くてCPUも効率的に使えてない様子だったので,その原因を追求しようと試みました.Pylearn2 は Theano を使い,Theano は Numpy を使い,Numpy は ATLAS,BLAS, LAPACK 等の数値計算ライブラリを使い,という関係になっているので,原因は
- Numpy が効率のよい数値計算ライブラリを呼び出せてない
- 1. は問題ないけど,Theano がおかしなことになっている
- 1. と 2. は問題なく,Pylearn2 がおかしなことになっている
- Pylearn2 も悪くなくて,マシンが遅いだけ
という4つのいずれかだと考えて,切り分けてみましょう.Pylearn2 のプログラムを解析して直接原因を探るのではないので,間接的に推測するだけですが.その過程で,「MacPorts で入れた Numpy で行列の積を高速に計算するにはどの variant がよい?」というのをちょこっと調べてみることにもなりました.
環境
以下の実験共通の条件というか計算機環境は次の通り.
- iMac 27inch late 2012, Core i7 3.4GHz (Quad Core), メモリ 32GB
- OS X 10.8 Mountain Lion
- MacPorts を使用
- Python は MacPorts で入れた 2.7 を使用
Numpy が効率のよい数値計算ライブラリを呼び出せてない?
MacPorts で入れた Numpy を使って行列の積を計算するプログラムを実行して,計算時間や,ちゃんとマルチスレッドで動いてるか,といったことを調べる実験を行います.
実験方法
以下のプログラムを動かして,行列の積に要するCPU時間を測定するとともに,「アクティビティモニタ」の表示を観察して,実行時のスレッド数やCPU使用率を目視で確認します.
import numpy as np import time N = 200 t_start = time.clock() A = np.random.rand( N*N ).reshape( ( N, N ) ) B = np.random.rand( N*N ).reshape( ( N, N ) ) t_elapsed = time.clock() - t_start print 'CPU time for initialization [sec] = ', t_elapsed T = 10 t_start = time.clock() for i in range( T ): C = np.dot( A, B ) t_elapsed = time.clock() - t_start print 'N = ', N, 'T = ', T, 'average CPU time [sec] = ', t_elapsed / T
ただ,実際に動かしてみたら time.clock() の値は使えなさそう(マルチスレッドやから?)だと分かったので,次のように実行してみました.以下,上記のプログラムを hoge.py という名前のファイルに保存したとします.
$ time python -m cProfile hoge.py
timeコマンドで hoge.py 全体の実行時間を測るとともに,Python のプロファイラで関数呼び出し毎の時間も取得してみたということ.後者については
26.4. Python プロファイラ — Python 2.7ja1 documentation
に説明があります.
実験N1: MacPortsで入れた ATLAS を使う場合
使ったマシンの現状が
$ port installed atlas py27-numpy py27-scipy atlas @3.10.2_1+clang (active) py27-numpy @1.8.2_0+atlas+gcc48 (active) py27-scipy @0.14.0_0+atlas+gcc48 (active)
だったので,そのままで実験.Numpy がどんなライブラリを呼び出すように設定されているかは,次のようにして調べることができます.
$ python >>> import numpy as np >>> np.__config__.show() atlas_threads_info: libraries = ['lapack', 'ptf77blas', 'ptcblas', 'tatlas'] library_dirs = ['/opt/local/lib'] define_macros = [('ATLAS_INFO', '"\\"3.10.2\\""')] language = f77 include_dirs = ['/opt/local/include'] blas_opt_info: libraries = ['ptf77blas', 'ptcblas', 'tatlas'] library_dirs = ['/opt/local/lib'] define_macros = [('ATLAS_INFO', '"\\"3.10.2\\""')] language = c include_dirs = ['/opt/local/include'] atlas_blas_threads_info: libraries = ['ptf77blas', 'ptcblas', 'tatlas'] library_dirs = ['/opt/local/lib'] define_macros = [('ATLAS_INFO', '"\\"3.10.2\\""')] language = c include_dirs = ['/opt/local/include'] openblas_info: NOT AVAILABLE lapack_opt_info: libraries = ['lapack', 'ptf77blas', 'ptcblas', 'tatlas'] library_dirs = ['/opt/local/lib'] define_macros = [('ATLAS_INFO', '"\\"3.10.2\\""')] language = f77 include_dirs = ['/opt/local/include'] lapack_mkl_info: NOT AVAILABLE blas_mkl_info: NOT AVAILABLE mkl_info: NOT AVAILABLE
ちゃんとわかってませんが,上記を見るとMacPortsで入れたATLAS使うようになってそうです. 実行してみると…
$ time python -m cProfile hoge.py N = 5000 CPU time for initialization [sec] = 0.490314 N = 5000 T = 10 average CPU time [sec] = 30.6721502 <== この値は参考になりません 10697 function calls (10598 primitive calls) in 41.132 seconds : 1 0.092 0.092 41.133 41.133 hoge.py:2() 2 0.490 0.245 0.490 0.245 {method 'rand' of 'mtrand.RandomState' objects} 10 40.490 4.049 40.490 4.049 {numpy.core._dotblas.dot}
というわけで,5000 x 5000 の行列同士の積一回あたり4秒でした.また,アクティビティモニタで観察すると,スレッド数は9で,CPU使用率は800%近くになっていました.Quad Core の i7 だから論理コア数は8なので,800%ってのが全論理コアフル稼働状態のはず.
実験N2: Xcode付属の vecLib Framework を使う場合
上記の実験では特におかしな所は見当たりませんが,「MacPorts で入れた Atlas を使わへんかったらどうなんやろ,これより速いんか遅いんか」って気になったので,そっちも実験.Mac OS X では,Xcode の vecLib Framework の中に BLAS と LAPACK が含まれている( vecLib Reference )んですが,MacPorts で +atlas の variant つけずに Numpy & Scipy をインストールすると,そっちを使うみたいなので.
py27-numpy と py27-scipy を sudo port -f deactivate してから,+atlas なしで install.
$ port installed py27-numpy py27-scipy py27-numpy @1.8.2_0 (active) py27-numpy @1.8.2_0+atlas+gcc48 py27-scipy @0.14.0_0+atlas+gcc48 py27-scipy @0.14.0_0+gcc48 (active)
で,
$ python >>> import numpy as np >>> np.__config__.show() atlas_threads_info: NOT AVAILABLE blas_opt_info: extra_link_args = ['-Wl,-framework', '-Wl,Accelerate'] extra_compile_args = ['-msse3', '-I/System/Library/Frameworks/vecLib.framework/Headers'] define_macros = [('NO_ATLAS_INFO', 3)] atlas_blas_threads_info: NOT AVAILABLE openblas_info: NOT AVAILABLE lapack_opt_info: extra_link_args = ['-Wl,-framework', '-Wl,Accelerate'] extra_compile_args = ['-msse3'] define_macros = [('NO_ATLAS_INFO', 3)] atlas_info: NOT AVAILABLE lapack_mkl_info: NOT AVAILABLE blas_mkl_info: NOT AVAILABLE atlas_blas_info: NOT AVAILABLE mkl_info: NOT AVAILABLE
となりました.この状態で実験N1と同じプログラムを実行してみると,
$ time python -m cProfile hoge.py N = 5000 CPU time for initialization [sec] = 0.631823 N = 5000 T = 10 average CPU time [sec] = 11.2395173 <== この値は参考になりません 10697 function calls (10598 primitive calls) in 30.880 seconds 1 0.099 0.099 30.881 30.881 hoge.py:2() 2 0.628 0.314 0.628 0.314 {method 'rand' of 'mtrand.RandomState' objects} 10 30.095 3.010 30.095 3.010 {numpy.core._dotblas.dot} real 0m30.960s user 1m52.414s sys 0m0.745s
実験N1で積一回あたり4秒やったのが3秒に.また,アクティビティモニタで観察すると,スレッド数は4で,CPU使用率は400%弱でした.
実験N1,N2の考察
というわけで,少なくともnp.dot()に関しては,
- MacPorts で入れた Numpy は,ATLASやBLAS等を使ってちゃんとマルチスレッドで効率よく計算してくれる
- MacPortsで入れた ATLAS を使うより,vecLib を使う方が速い
ようです.したがって,最初に書いた 1. から 4. までの可能性のうち,1. は否定できそうです.
Theano がおかしなことになっている?
次に,Theano がおかしなことをしている可能性を検討します.
実験方法
import numpy as np import theano import theano.tensor as T A = T.dmatrix( 'A' ) B = T.dmatrix( 'B' ) AB = T.dot( A, B ) f = theano.function( [A, B], AB ) N = 10000 a = np.random.rand( N*N ).reshape( ( N, N ) ) b = np.random.rand( N*N ).reshape( ( N, N ) ) t_start = time.clock() c = np.dot( a, b ) t_elapsed = time.clock() - t_start print 'N = ', N, 'CPU time for np.dot() [sec] = ', t_elapsed t_start = time.clock() d = f( a, b ) t_elapsed = time.clock() - t_start print 'N = ', N, 'CPU time for theano [sec] = ', t_elapsed
上記のプログラムを theano0905.py という名前のファイルに保存したとして,
$ time python theano0905.py
として結果を眺めてみます.上で述べたように time.clock() の値は信用できませんが,np.dot() と theano の f の場合の時間を比較できればよいので.
実験T1: MacPortsで入れた ATLAS を使う場合
実験N1と同じ条件での実験です.
$ time python theano0905.py N = 10000 CPU time for np.dot() [sec] = 245.931655 N = 10000 CPU time for theano [sec] = 246.231735 real 1m8.628s user 8m15.898s sys 0m2.111s
Theano 経由でも np.dot() とほぼ同じ時間.アクティビティモニタで見ても,どっちの計算もスレッド数9, CPU使用率は800%弱でした.
実験T2: Xcode付属の vecLib Framework を使う場合
こちらは実験N2と同じ.
$ time python theano0905.py N = 10000 CPU time for np.dot() [sec] = 92.035379 N = 10000 CPU time for theano [sec] = 92.663039 real 0m54.150s user 3m9.805s sys 0m1.343s
やはり np.dot() とTheano経由でほぼ同じ時間で,timeコマンドの出力(経過時間(real))ではこちらの方が速いという結果.アクティビティモニタではスレッド数4, CPU使用率 400%弱.
実験T1,T2の考察
Theano は素直に Numpy を呼び出して効率良く計算できていると言えそうです.したがって,2. も否定できるようです.
Pylearn2 が悪い?
ここまでの実験の結果から,Numpy や Theano は悪くなくて,Pylearn2 がおかしなことになってるか,Pylearn2 も悪くなくて単純にマシンが遅いだけ,のいずれかと言えそうだとわかりました.ということで,Pylearn2 の tutorial でお勉強 - 3時間目 - まんぼう日記 の convolutional net のサンプルを上記2種類の条件で実行して,最初の1epochにかかる時間を見てみると…
- 実験N1,T1と同様の条件: 23分16秒,スレッド数1,CPU使用率100%弱
- 実験N2,T2と同様の条件: 23分15秒,スレッド数1,CPU使用率100%弱
という結果になりました…Pylearn2が悪いのか?
むむー.どうしたもんかなあ.