Haskell-Accelerateでのループ
背景
多用するけどオリジナルのリファレンスには書かれていない用法だったので書いておきます。
ループというか指定した回数だけ関数を重ねがけするものです。GPU環境での実験ですが、CPU実行でも少量変更で良いはずです。
環境
名前 | バージョン |
---|---|
OS | Ubuntu 20.04 |
nvcc(cuda) | 11.8 |
ghc | 8.6.5 |
accelerate | 1.3.0.0 |
accelerate-llvm | 1.3.0.0 |
accelerate-llvm-native | 1.3.0.0 |
accelerate-llvm-ptx | 1.3.0.0 |
GPU | GeForce RTX3080 |
CPU | AMD Ryzen 9 3900XT |
(>->)によるループ
実装としては簡単なのですが、Accのネストが深くなり速度が出ません。
以下はn要素のベクトルの先頭5要素のみを倍にする操作をn回かけるものです。
fCalc = L.foldl1 (>->) $ L.replicate n fiveDouble
によりfiveDouble
をn個結合して1つのAcc関数を構成しています。
loop_compose.hs
{-# LANGUAGE CPP, BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall -fno-warn-name-shadowing #-}
import Data.Array.Accelerate as A
import Data.Array.Accelerate.LLVM.PTX
import Data.List as L
import Control.DeepSeq (deepseq)
fiveDouble :: Acc (Vector Int) -> Acc (Vector Int)
fiveDouble v = generate (shape v) f
where
f p@(I1 i) = ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.< 5) (orgVal * 2) orgVal
where orgVal = v ! p
nop :: Acc (Vector Int) -> Acc (Scalar Int)
nop _ = fill I0 0
main = do
let
n = 10
-- n = 10000
v = enumFromN (constant (Z :. n)) 0 :: Acc(Vector Int)
fCalc :: Acc (Vector Int) -> Acc (Vector Int)
fCalc = L.foldl1 (>->) $ L.replicate n fiveDouble
print $ run $ v
print $ run $ fiveDouble v
print $ run $ fCalc v
print $ fCalc v -- to show Acc tree.
-- (run $ (fCalc >-> nop) v) `deepseq` return ()
実行結果
Vector (Z :. 10) [0,1,2,3,4,5,6,7,8,9]
Vector (Z :. 10) [0,2,4,6,8,5,6,7,8,9]
Vector (Z :. 10) [0,1024,2048,3072,4096,5,6,7,8,9]
let
a0 = (\a0 ->
let
a1 = (\a1 ->
let
a2 = (\a2 ->
let
a3 = (\a3 ->
let
a4 = (\a4 ->
let
a5 = (\a5 ->
let
a6 = (\a6 ->
let
a7 = (\a7 ->
let
a8 = (\a8 ->
generate
(shape a8)
(\(T1 x0) ->
let
x1 = a8 ! x0
x2 = -1 == x0
in
if x2
then x1
省略
awhileによるループ
少々手間ですがAccのネストが浅く速度も出せます。
awhile
にて、メインで扱いたいVectorとセットでカウンタの役割をするScalarをペアにAcc (Scalar Int, Vector Int)
として扱います。カウンタに基づいたVector編集などにも使えるので便利でしょう。
注意: 普通のfor分と同じノリで使うと危ないです。カウンタが終了条件にかかる直前の処理でもメインの処理も実行してしまうので異常なメモリアクセスをする場合があり、扱うデータサイズが大きくなってからメモリアクセスエラーとなります。以下もやってるかも。後々修正します。
loop_awhile.hs
{-# LANGUAGE CPP, BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall -fno-warn-name-shadowing #-}
import Data.Array.Accelerate as A
import Data.Array.Accelerate.LLVM.PTX
import Data.List as L
import Control.DeepSeq (deepseq)
fiveDouble :: Acc (Vector Int) -> Acc (Vector Int)
fiveDouble v = generate (shape v) f
where
f p@(I1 i) = ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.< 5) (orgVal * 2) orgVal
where orgVal = v ! p
nop :: Acc (Vector Int) -> Acc (Scalar Int)
nop _ = fill I0 0
main = do
let
n = 10
-- n = 10000
v = enumFromN (constant (Z :. n)) 0 :: Acc(Vector Int)
fCalc :: Acc (Vector Int) -> Acc (Vector Int)
fCalc xs = asnd $ awhile cond f ini
where
ini = T2 (unit 0) xs :: Acc (Scalar Int, Vector Int)
f :: Acc (Scalar Int, Vector Int) -> Acc (Scalar Int, Vector Int)
f (T2 c xs) = T2 (unit $ (the c)+1) (fiveDouble xs)
cond :: Acc (Scalar Int, Vector Int) -> Acc (Scalar Bool)
cond (T2 c _) = unit $ (the c) A.< (lift n)
print $ run $ v
print $ run $ fiveDouble v
print $ run $ fCalc v
print $ fCalc v -- to show Acc tree.
-- (run $ (fCalc >-> nop) v) `deepseq` return ()
実行結果
Vector (Z :. 10) [0,1,2,3,4,5,6,7,8,9]
Vector (Z :. 10) [0,2,4,6,8,5,6,7,8,9]
Vector (Z :. 10) [0,1024,2048,3072,4096,5,6,7,8,9]
let
T2 a0 a1 = awhile
(\(T2 a0 a1) -> let a2 = unit (a0 ! () < 10, ()) in map (\(x0, ()) -> x0) a2)
(\(T2 a0 a1) ->
T2 (unit (1 + a0 ! ()))
(generate
(shape a1)
(\(T1 x0) -> let x1 = a1 ! x0 x2 = -1 == x0 in x2 ? x1 : x2 ? x1 : x0 < 5 ? 2 * x1 : x1)))
T2 (unit 0) (generate 10 (\(T1 x0) -> x0))
in
a1
比較
n=10000
として以下の様に変更しtime
コマンドで速度比較しました。nop
はGPUからのデータ転送の時間を除くためのものです。
loop_compose.hs
{-# LANGUAGE CPP, BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall -fno-warn-name-shadowing #-}
import Data.Array.Accelerate as A
import Data.Array.Accelerate.LLVM.PTX
import Data.List as L
import Control.DeepSeq (deepseq)
fiveDouble :: Acc (Vector Int) -> Acc (Vector Int)
fiveDouble v = generate (shape v) f
where
f p@(I1 i) = ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.< 5) (orgVal * 2) orgVal
where orgVal = v ! p
nop :: Acc (Vector Int) -> Acc (Scalar Int)
nop _ = fill I0 0
main = do
let
-- n = 10
n = 10000
v = enumFromN (constant (Z :. n)) 0 :: Acc(Vector Int)
fCalc :: Acc (Vector Int) -> Acc (Vector Int)
fCalc = L.foldl1 (>->) $ L.replicate n fiveDouble
--print $ run $ v
--print $ run $ fiveDouble v
--print $ run $ fCalc v
--print $ fCalc v -- to show Acc tree.
(run $ (fCalc >-> nop) v) `deepseq` return ()
loop_awhile.hs
{-# LANGUAGE CPP, BangPatterns #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TypeOperators #-}
{-# OPTIONS_GHC -Wall -fno-warn-name-shadowing #-}
import Data.Array.Accelerate as A
import Data.Array.Accelerate.LLVM.PTX
import Data.List as L
import Control.DeepSeq (deepseq)
fiveDouble :: Acc (Vector Int) -> Acc (Vector Int)
fiveDouble v = generate (shape v) f
where
f p@(I1 i) = ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.== -1) orgVal -- dummy
$ ifThenElse (i A.< 5) (orgVal * 2) orgVal
where orgVal = v ! p
nop :: Acc (Vector Int) -> Acc (Scalar Int)
nop _ = fill I0 0
main = do
let
-- n = 10
n = 10000
v = enumFromN (constant (Z :. n)) 0 :: Acc(Vector Int)
fCalc :: Acc (Vector Int) -> Acc (Vector Int)
fCalc xs = asnd $ awhile cond f ini
where
ini = T2 (unit 0) xs :: Acc (Scalar Int, Vector Int)
f :: Acc (Scalar Int, Vector Int) -> Acc (Scalar Int, Vector Int)
f (T2 c xs) = T2 (unit $ (the c)+1) (fiveDouble xs)
cond :: Acc (Scalar Int, Vector Int) -> Acc (Scalar Bool)
cond (T2 c _) = unit $ (the c) A.< (lift n)
-- print $ run $ v
-- print $ run $ fiveDouble v
-- print $ run $ fCalc v
-- print $ fCalc v -- to show Acc tree.
(run $ (fCalc >-> nop) v) `deepseq` return ()
結果:
$ghc -O2 loop_compose -threaded
$ghc -O2 loop_awhile -threaded
$ time ./loop_compose
real 0m8.689s
user 0m7.787s
sys 0m0.852s
$ time ./loop_awhile
real 0m1.889s
user 0m1.631s
sys 0m0.247s
4.6倍awhileが早い結果となりました。
GPUメモリ消費量はどちらも1Gbyte程度でした。すべてのケースで>->
が遅いわけではないです。後々早くなる例を掲載します。
参考文献
>->
の方法:https://www.oreilly.com/library/view/parallel-and-concurrent/9781449335939/ch06.html
awhile
の方法:https://github.com/AccelerateHS/accelerate-examples/blob/master/examples/quicksort/QuickSort.hs
Discussion