Common Lispで行列演算(2): MGL-MATを使う

前回LLAによるCPUを使った行列演算の話をしたので、今回はGPUを使った行列演算の話。

そもそものやりたいこととしては、機械学習ライブラリのMGLをインストールしたいのだが、Quicklispから入るMGLはバージョンが古く、cl-cudaに依存する最新版を入れるにはいくつかのパッケージをGithubからcloneしてくる必要がある。この依存パッケージのインストールなどでつまったので、とりあえずcl-cudaとMGL-MATをインストールして行列積を計算させるところまでのメモを残しておく。
2016/12/3追記: cl-cuda及びMGL-MATがQuicklispに登録されたのでGithubからダウンロードする必要はなくなった。

LLAのインストール

MGL-MATは前回紹介したLLAに依存しているので、前回記事の手順でインストールするBLASライブラリの場所を設定する必要があるので注意。

CUDA7.5のインストール

GPUを使った行列演算をさせる場合は、まずCUDAをインストールしなければならない。CPUのみで利用する場合はCUDAのインストールは不要。
cl-cudaのREADMEにはCUDA5.5までしか動作確認が載っていないので一抹の不安を覚えつつ、CUDAの現時点での最新版7.5をダウンロードしてきてインストールする。
ディストリビューションによってインストール方法が違うのでクイックスタートガイド(PDF)を参照。Utuntuの場合は上記ダウンロードページからdebファイルをダウンロードしてきてdpkgからインストールした後に、apt-getすればいいらしい。

$ sudo dpkg --install cuda-repo-<distro>-<version>.<architecture>.deb
$ sudo apt-get update
$ sudo apt-get install cuda

ここまででGPUのドライバが更新されたのでシステムを再起動する。

環境変数を設定する

自分の場合は~/.bashrcにこう書いておいた

export PATH=$PATH:/usr/local/cuda-7.5/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda-7.5/lib64
export C_INCLUDE_PATH=$C_INCLUDE_PATH:/usr/local/cuda-7.5/include
デモを動かす

ホームディレクトリにサンプルをインストールして、コンパイル、実行する。

$ cuda-install-samples-7.5.sh ~
$ cd ~/NVIDIA_CUDA-Samples_7.5/5_Simulations/nbody
$ make
$ ./nbody


こういう粒子がぐるぐる回るデモが出てくればインストールは成功している。

cl-cuda、MGL-MATのインストール

2016/12/3追記: cl-cuda及びMGL-MATがQuicklispに登録されたのでこのステップは不要になった
次に、さしあたって必要になるCommon Lispのパッケージを~/quicklisp/local-projects/以下にダウンロードしてくる。

$ cd ~/quicklisp/local-projects/
$ git clone https://github.com/arielnetworks/cl-pattern.git
$ git clone https://github.com/osicat/osicat.git
$ git clone https://github.com/takagi/cl-cuda.git
$ git clone https://github.com/melisgl/mgl-mat.git

先に設定した環境変数が有効になっているターミナルからSBCLを起動して、

(ql:quickload :mgl-mat)

で依存関係から全部コンパイルされる。CUDAを使わずにCPUでだけ使いたいという場合であっても同様の手順でいけるが、その場合環境変数の設定は不要。CUDAがインストールされていないことが自動的に判別されCPUモードになる。

次にテストが通るか確認してみる。

(ql:quickload :cl-cuda-test)
(ql:quickload :mgl-mat-test)
(in-package :mgl-mat)
(test)
SLIMEのLisp処理系を環境変数つきで起動する

このままSLIMEを起動しても実行時にエラーが出る。どうやら環境変数が反映されていないらしい。 .emacs等でLisp処理系を指定するところに:envキーワードを付けることで環境変数を指定できる。

(setq inferior-lisp-program "sbcl")

(add-to-list 'slime-lisp-implementations
             '(sbcl ("sbcl" "--core" "/home/wiz/lib/sbcl/sbcl.core" "--dynamic-space-size" "4096" "--noinform")
               :env ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/cuda-7.5/bin"
                     "C_INCLUDE_PATH=/usr/local/cuda-7.5/include"
                     "LD_LIBRARY_PATH=/usr/local/cuda-7.5/lib64")))

;; roswellを使う場合
(add-to-list 'slime-lisp-implementations
             '(sbcl-ros ("ros" "-L" "sbcl-bin" "-Q" "dynamic-space-size=4096" "-l" "~/.sbclrc" "run")
               :env ("PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/local/cuda-7.5/bin"
                     "C_INCLUDE_PATH=/usr/local/cuda-7.5/include"
                     "LD_LIBRARY_PATH=/usr/local/cuda-7.5/lib64")))

MGL-MATで行列積を試してみる。

ディープラーニングの計算で必要になるのは行列積なので、CPUに対してどれくらい速くなっているかを見る。
まずは行列を作って初期値をランダムに設定する。

(in-package :mgl-mat)

(defparameter *size* 3)

;; ctypeのデフォルトは倍精度 :double 。単精度の場合は :float を与える
(defparameter ma (make-mat (list *size* *size*) :ctype :float))
(defparameter mb (make-mat (list *size* *size*) :ctype :float))
(defparameter mc (make-mat (list *size* *size*) :ctype :float)) ; 結果が破壊的に代入される行列

(defun randomize (m)
  "行列の要素を[0, 1]の範囲の乱数で初期化する"
  (destructuring-bind (rows cols)
      (mat-dimensions m) ; array-dimensionsじゃなくてmat-dimensions
    (dotimes (row rows)
      (dotimes (col cols)
        (setf (mref m row col) (random 1.0))))) ; arefじゃなくてmref
  m)

(randomize ma)
;; #<MAT 3x3 ABF #2A((0.64851093 0.82050586 0.2198838)
;;                   (0.6513314 0.78702486 0.7623421)
;;                   (0.19958317 0.3317876 0.087501764))>
(randomize mb)
;; #<MAT 3x3 ABF #2A((0.49600554 0.36851585 0.93187964)
;;                   (0.665609 0.85561895 0.8448553)
;;                   (0.031312823 0.5805893 0.41502512))>

行列積をやる関数はgemm!で、その計算式は C = alpha * AB + beta * C なので、単純にABの結果をCに代入したいときはalpha=1.0、beta=0.0にすればいい。

(gemm! 1.0 ma mb 0.0 mc)
;; #<MAT 3x3 AF #2A((0.87468624 1.0686891 1.3888001)
;;                  (0.87078595 1.356027 1.5882758)
;;                  (0.3225751 0.4082359 0.50261545))>

実は上の行列積はLLAを使った計算で、CPUしか使っていない。cudaを使った計算をするにはwith-cuda*マクロを使う。

(with-cuda* ()
  (gemm! 1.0 ma mb 0.0 mc))

(print
 (with-cuda* ()
   (print (gemm! 1.0 ma mb 0.0 mc))))

;; #<MAT 3x3 aCf #2A((0.8746863 1.0686891 1.3888001)
;;                   (0.8707859 1.356027 1.5882758)
;;                   (0.3225751 0.4082359 0.50261545))> 
;; #<MAT 3x3 AF #2A((0.8746863 1.0686891 1.3888001)
;;                  (0.8707859 1.356027 1.5882758)
;;                  (0.3225751 0.4082359 0.50261545))>   

MAT構造体のサイズの後ろにAとかCとかついているが、with-cuda*の中の世界ではCUDAの配列として扱われているようで、with-cuda*から出るときにCommon Lispの配列としてアクセスするようになるらしい。
時間測定用の関数は

(defun run (size &optional (n 100))
  "gemmをn回計算して実行時間を測定する。行列の初期化は計測しない。"
  (let ((ma (randomize (make-mat (list size size) :ctype :float)))
        (mb (randomize (make-mat (list size size) :ctype :float)))
        (mc (make-mat (list size size) :ctype :float)))
    (time
     (with-cuda* ()
       (loop repeat n
             do (gemm! 1.0 ma mb 0.0 mc)
             finally (return nil))))))
実行結果

計測した実行時間を、前回LLAでやってみたときの図にプロットしてみるとこうなる。横軸が行列のサイズで、縦軸が行列積を100回やったときの時間である。環境はUbuntu 14.04、CPUはCore i5 6440、GPUGeforce GTX 750 Ti。

さすがにGPUを使っているMGL-MATはCPUよりも速い。

OpenBLASとcuBLAS(MGL-MAT)を抜き出して4000次元まで比較してみると約7倍の開きがある。

実行してみると、行列積そのものは速いのだが、行列の初期化にえらい時間がかかっている。arefと違ってmrefは非常に重い計算らしく、randomizeに時間を取られているっぽい。自分で定義しなくても行列を一様分布や正規分布でランダマイズする関数がついていて、CUDAによる高速化が効くのでwith-cuda*の中で初期化する。

(defun run2 (size &optional (n 10000))
  "gemmをn回計算して実行時間を測定する。行列の初期化は計測しない。"
  (let ((ma (make-mat (list size size) :ctype :float))
        (mb (make-mat (list size size) :ctype :float))
        (mc (make-mat (list size size) :ctype :float)))
    (time
     (with-cuda* ()
       (uniform-random! ma :limit 1.0)
       (uniform-random! mb :limit 1.0)
       (loop repeat n
             do (gemm! 1.0 ma mb 0.0 mc)
             finally (return nil))))))

runとrun2の実行時間は、

MGL-MAT> (time (run 1000 100))
Evaluation took:
  0.557 seconds of real time
  0.548603 seconds of total run time (0.453418 user, 0.095185 system)
  98.56% CPU
  1,886,762,588 processor cycles
  98,240 bytes consed
  
Evaluation took:
  4.812 seconds of real time
  4.779511 seconds of total run time (4.659018 user, 0.120493 system)
  [ Run times consist of 0.009 seconds GC time, and 4.771 seconds non-GC time. ]
  99.33% CPU
  16,322,222,280 processor cycles
  588,154,320 bytes consed
  
NIL
MGL-MAT> (time (run2 1000 100))
Evaluation took:
  0.532 seconds of real time
  0.520530 seconds of total run time (0.425539 user, 0.094991 system)
  [ Run times consist of 0.005 seconds GC time, and 0.516 seconds non-GC time. ]
  97.93% CPU
  1,805,827,497 processor cycles
  12,098,320 bytes consed
  
Evaluation took:
  0.533 seconds of real time
  0.520857 seconds of total run time (0.425852 user, 0.095005 system)
  [ Run times consist of 0.005 seconds GC time, and 0.516 seconds non-GC time. ]
  97.75% CPU
  1,806,942,422 processor cycles
  12,098,320 bytes consed

次はMGLでDeep Belief NetworkでMNISTをやる。