この質問が出されてから7年が経過しましたが、まだ誰もこの問題の適切な解決策を考え出していないようです。RepaにはmapM
/のtraverse
ような関数がありません。並列化せずに実行できるものも含まれます。さらに、ここ数年の進歩の量を考えると、それが起こる可能性は低いと思われます。
Haskellの多くの配列ライブラリは陳腐な状態であり、それらの機能セットに対する私の全体的な不満のために、massiv
Repaからいくつかの概念を借用した配列ライブラリに数年の作業を費やしましたが、それを完全に異なるレベルにしています。イントロで十分です。
今日に先立ち、機能のような3つの単項マップにあったmassiv
(関数のようにシノニムをカウントしません:imapM
、forM
ら。):
mapM
-任意の通常のマッピングMonad
。明らかな理由で並列化できず、少し遅い(mapM
リストの通常の行に沿って遅い)
traversePrim
-ここでは、に制限されていますPrimMonad
。これは、よりもはるかに高速ですmapM
が、その理由はこの説明では重要ではありません。
mapIO
-これは、名前が示すように、制限されていますIO
(またはに制限されてMonadUnliftIO
いますが、それは無関係です)。今いるのでIO
、コアと同じ数のチャンクに配列を自動的に分割し、個別のワーカースレッドを使用してIO
、それらのチャンクの各要素にアクションをマッピングできます。fmap
並列化も可能な純粋なとは異なりIO
、マッピングアクションの副作用と組み合わせたスケジューリングの非決定性のため、ここにいる必要があります。
そのため、この質問を読んだ後、問題はで実際に解決されると思いましたmassiv
が、それほど速くはありません。ランダムな番号などのように、発電機、mwc-random
で、他random-fu
の缶は多くのスレッド間で同じ発電機を使用しません。つまり、私が見逃していた唯一のパズルのピースは、「スポーンされたスレッドごとに新しいランダムシードを描画し、通常どおり続行する」ことでした。つまり、次の2つが必要でした。
- ワーカースレッドと同じ数のジェネレータを初期化する関数
- そして、アクションが実行されているスレッドに応じて、マッピング関数に正しいジェネレーターをシームレスに与える抽象化です。
それがまさに私がしたことです。
最初に、特別に作成されたrandomArrayWS
とinitWorkerStates
関数を使用した例を示します。これらは質問との関連性が高く、後でより一般的なモナドマップに移動します。それらの型シグネチャは次のとおりです。
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g -- ^ Use `initWorkerStates` to initialize you per thread generators
-> Sz ix -- ^ Resulting size of the array
-> (g -> m e) -- ^ Generate the value using the per thread generator.
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
に慣れていない人にとってmassiv
、Comp
引数は使用する計算戦略であり、注目すべきコンストラクタは次のとおりです。
Seq
-スレッドをフォークすることなく、計算を順次実行します
Par
-機能と同じ数のスレッドを起動し、それらを使用して作業を行います。
mwc-random
最初はパッケージを例として使用し、後でに移動しRVarT
ます。
λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)
上記では、システムのランダム性を使用してスレッドごとに別のジェネレーターを初期化しましたが、ワーカーからのWorkerId
単なるInt
インデックスである引数から派生させることで、スレッドごとに固有のシードを使用することもできました。そして、これらのジェネレータを使用して、ランダムな値を持つ配列を作成できます。
λ> randomArrayWS gens (Sz2 2 3) standard :: IO (Array P Ix2 Double)
Array P Par (Sz (2 :. 3))
[ [ -0.9066144845415213, 0.5264323240310042, -1.320943607597422 ]
, [ -0.6837929005619592, -0.3041255565826211, 6.53353089112833e-2 ]
]
Par
戦略を使用することにより、scheduler
ライブラリは利用可能なワーカー間で生成作業を均等に分割し、各ワーカーは独自のジェネレーターを使用するため、スレッドセーフになります。WorkerStates
それが同時に行われない限り、何も同じ任意の回数の再利用を妨げるものはありません。そうしないと例外が発生します。
λ> randomArrayWS gens (Sz1 10) (uniformR (0, 9)) :: IO (Array P Ix1 Int)
Array P Par (Sz1 10)
[ 3, 6, 1, 2, 1, 7, 6, 0, 8, 8 ]
次にmwc-random
、次のような関数を使用して、同じ概念を他の可能なユースケースに再利用できますgenerateArrayWS
。
generateArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> Sz ix -- ^ size of new array
-> (ix -> s -> m e) -- ^ element generating action
-> m (Array r ix e)
とmapWS
:
mapWS ::
(Source r' ix a, Mutable r ix b, MonadUnliftIO m, PrimMonad m)
=> WorkerStates s
-> (a -> s -> m b) -- ^ Mapping action
-> Array r' ix a -- ^ Source array
-> m (Array r ix b)
この機能をrvar
、random-fu
およびmersenne-random-pure64
ライブラリで使用する方法について約束された例を次に示します。私たちは、使用している可能性がrandomArrayWS
ここにも、しかし、例のための我々はすでに別の配列としましょうRVarT
、我々は必要がある場合には、Sを、mapWS
:
λ> import Data.Massiv.Array
λ> import Control.Scheduler (WorkerId(..), initWorkerStates)
λ> import Data.IORef
λ> import System.Random.Mersenne.Pure64 as MT
λ> import Data.RVar as RVar
λ> import Data.Random as Fu
λ> rvarArray = makeArrayR D Par (Sz2 3 9) (\ (i :. j) -> Fu.uniformT i j)
λ> mtState <- initWorkerStates Par (newIORef . MT.pureMT . fromIntegral . getWorkerId)
λ> mapWS mtState RVar.runRVarT rvarArray :: IO (Array P Ix2 Int)
Array P Par (Sz (3 :. 9))
[ [ 0, 1, 2, 2, 2, 4, 5, 0, 3 ]
, [ 1, 1, 1, 2, 3, 2, 6, 6, 2 ]
, [ 0, 1, 2, 3, 4, 4, 6, 7, 7 ]
]
上記の例ではMersenne Twisterの純粋な実装が使用されているという事実にもかかわらず、IOをエスケープできないことに注意することが重要です。これは、非決定的なスケジューリングによるものです。つまり、どのワーカーが配列のどのチャンクを処理するか、そしてその結果、どのジェネレーターが配列のどの部分に使用されるかは、決してわかりません。一方で、ジェネレーターがのように純粋で分割可能splitmix
な場合、純粋で確定的で並列化可能な生成関数を使用できますrandomArray
が、それはすでに別の話です。