Posted at

Accelerateとglossで作るライフゲーム

やっぱり並列計算したいじゃないですか。でも「イチからディープラーニング組むぜ!!」というのも流石に無謀がすぎるのでライフゲームの盤面計算をAccelerateでやり、表示をglossでやってみました。glossについてはこちらの記事を参考にしました。

glossではじめるグラフィック描画 :: Haskell入門の次に読む記事 - Qiita - https://qiita.com/lotz/items/eb73e62a64bc208c2dd6

GPUじゃなしにIntel Core i7-8550U 8コア 4GHzでも5120x5120のライフゲームを秒間30回くらいまで計算できました。こちらのソースコードを元に解説していきたいと思います。

使うモジュールはこんな感じ。

import Data.Array.Accelerate              as A

import Data.Array.Accelerate.LLVM.Native as CPU
import Graphics.Gloss
import Graphics.Gloss.Accelerate.Data.Picture
import Data.Word
import Data.Array.Accelerate.System.Random.MWC

型定義はこちら。

type Model = Matrix Bool

type View = Matrix Word32

Modelがライフゲームの盤面、Viewはglossに転送するビットマップです。ともにMatrix aという型になっていますが、これはAccelerateのArray sh a型の特殊な場合(二次元行列)を表してます。次にModelViewに変換する関数を定義しましょう。

modelToBitmap :: Acc Model -> Acc View

modelToBitmap = A.map (\b -> b ? (0xff000000, 0xffffffff))

Array aAccで包んでやることでAccelerateで並列計算ができるようになります。実際に計算するには専用のmapを使います。また、mapを使うと値がExpという内部表現に包まれた型になるのでArray Boolを入れると関数にはExp Boolが渡されます。普通のifは使えないので専用の演算子(?)を使ってます。bTrueなら黒、Falseなら白に対応しています。お次はライフゲームの本体です。

step :: Acc Model -> Acc Model

step = stencil rule clamp
where
rule :: Stencil3x3 Bool -> Exp Bool
rule ((x11, x12, x13)
,(x21, x22, x23)
,(x31, x32, x33)
) =
let
n = (cond x11 (constant 1) (constant 0))
A.+ (cond x12 (constant 1) (constant 0))
A.+ (cond x13 (constant 1) (constant 0))
A.+ (cond x21 (constant 1) (constant 0))
A.+ (cond x23 (constant 1) (constant 0))
A.+ (cond x31 (constant 1) (constant 0))
A.+ (cond x32 (constant 1) (constant 0))
A.+ (cond x33 (constant 1) (constant 0))
in
cond x22
(n A.== (2 :: Exp Int) A.|| n A.== (3 :: Exp Int))
(n A.== (3 :: Exp Int))

ライフゲームではマス目の近傍が必要になりますが、stencilという関数がうまいこと関数の型を見てやってくれます。Stencil3x3という型がありますが、関数の仮引数を見ればわかるとおり単なるタプルになってます。あとはTrueを数え上げてライフゲームのルール通りにマス目の生死を決めれば終わりです……と言いたいところですが、まだある問題があります。盤面端の処理(境界条件)を考えてません。ですがこれもAccelerateにある便利関数を使うと解決です。clampがそれで、この場合盤面の外は今見ているマス目と同じ値になります。ドキュメントを見ていただければわかりやすいでしょう。他にもwrapmirrorが用意されていたり、functionで境界条件を自分で定義することもできます。ここまで来たらあとはmainだけです。

main :: IO ()

main = do
i <- randomArray uniform (Z:.1024:.1024)
simulate FullScreen white 30 i (flip bitmapOfArray False . CPU.run . modelToBitmap . use) $
\_ _ m ->
CPU.run $ step (use m)

randomArrayはランダムな値で埋められたArrayを返してくれるmwc-random-accelerateの便利関数です。また、bitmapOfArrayArrayからglossのPictureを作ってくれるgloss-accelerateの便利関数です。CPU.runで実際の並列計算が走り、ライフゲームが描写されます。

今回の例ではCPUをバックエンドに用いましたが、CUDAに対応したGPUに入れ替えるのも既存のコードをほぼいじらずできるようです。これで並列計算がはかどりますね。