HaskellでGPUプログラミングしてみた

repa触ってみたらGPUも触ってみたくなったので触りました。
ライフゲームを略。


次の時刻の盤面を計算する部分です。ドキュメントやサンプルを見ると、どうやらstencilを使うと良いようです。

step :: Board -> Acc Board
step arr = Acc.stencil stencil2D (Constant 0) . Acc.use $ arr

cellとその周囲cellから次の時刻の値を計算出来るという、流体シミュレーションのために有るような関数ですね。
その際、境界条件をどうするかが第二引数です。

data Boundary a = Clamp | Mirror | Wrap | Constant a

最外値がその外もそのまま続いているかのように扱うのがClamp、範囲外の値が反転しているものがMirror、境界外が反対側と繋がっているものがWrap、定数値が広がっているように扱うのがConstant aです。
ライフゲームでは範囲外の値は死んでいると判断されれば十分なのでConstant 0にしています。


そしてlifecheck部分。

stencil2D :: Stencil3x3 Int -> Exp Int
stencil2D ((a,b,c), (d,e,f), (g,h,i)) = (cnt ==* 2) ? (e, (cnt ==* 3) ? (1, 0))
    where
        cnt = a+b+c+d+f+g+h+i

初め普通にガードで書いたのですが、Eq.==は使えません的な実行時エラー出ました。どうやらGPU上の表現であるらしいAcc, Expなどといった値はいつもの関数をそのまま使う事は出来ないようです。

ここでは(?)演算子を使って分岐を行っています。

(?) :: Elt t => Exp Bool -> (Exp t, Exp t) -> Exp t

丁度三項演算子の様なものですね。結果はtupleでまとまっているので2項演算子ですが。

比較演算子も==, <, >といったものではなくて、==*, /=*, <*, >*, <=*, >=*といった*付きの演算子が用意されています。


GPU上でのAccな値を計算する関数がrunです。

repeatStep :: BoardSize -> Board -> IO ()
repeatStep siz board = do
    let board' = CUDA.run $ step board
    threadDelay 120000
    clearDisp
    printRect siz board'
    repeatStep siz board'

一ステップ毎にGPU上で計算しているとあまり効果がないような...
だけど多分Accな値は触りようが無い気がするので今回は仕方ないような。高fpsが要求される場面ではGPUとの連携はどうするのが良いのでしょうね。


ソースコードです。

そういえばcabal install accelerate-cudaで少しつまづいたのでした。

$ export DYLD_LIBRARY_PATH=/usr/local/cuda/lib

などとしたら通る様になりましたが。