Repaアレイの並列mapM


90

との最近の作業ではGibbs samplingRVar乱数生成にほぼ理想的なインターフェースを提供するものを活用しています。残念ながら、マップでモナディックアクションを使用できないため、Repaを使用できませんでした。

明らかにモナディックマップは一般的に並列化できませんがRVar、効果を安全に並列化できるモナドの少なくとも1つの例である可能性があります(少なくとも原則として、私はの内部動作にあまり精通していませんRVar)。 。つまり、次のようなものを書きたいのですが、

drawClass :: Sample -> RVar Class
drawClass = ...

drawClasses :: Array U DIM1 Sample -> RVar (Array U DIM1 Class)
drawClasses samples = A.mapM drawClass samples

どこにA.mapM似ているでしょう、

mapM :: ParallelMonad m => (a -> m b) -> Array r sh a -> m (Array r sh b)

これがどのように機能するかはRVar、とその基礎となるの実装に大きく依存しますがRandomSource、原則として、生成されたスレッドごとに新しいランダムシードを描画し、通常どおり続行することを含むと考えられます。

直感的には、この同じ考えが他のいくつかのモナドに一般化されるかもしれないようです。

だから、私の質問です:ParallelMonad効果を安全に並列化できるモナドのクラスを構築できますか(おそらく、少なくともによって居住されRVarます)?

それはどのように見えるでしょうか?このクラスには他にどのようなモナドが生息しているのでしょうか?他の人は、これがRepaでどのように機能する可能性を検討しましたか?

最後に、この並列モナディックアクションの概念を一般化できない場合、RVar(非常に便利な)特定のケースでこれを機能させる良い方法を誰かが見ているでしょうか?RVar並列処理をあきらめることは、非常に難しいトレードオフです。


1
付着点は「生成されたスレッドごとに新しいランダムシードを描画すること」だと思います。この手順はどのように機能し、すべてのスレッドが戻ったらシードを再びマージする必要がありますか?
Daniel Wagner

1
RVarインターフェースは、ほぼ確実に、指定されたシードで新しいジェネレーターを生成することに対応するためにいくつかの追加を必要とします。確かに、この仕組みがどのように機能するかは不明であり、非常にRandomSource具体的であるように見えます。シードを描画する私の素朴な試みは、要素のベクトル(の場合mwc-random)を描画し、各要素に1を追加して最初のワーカーのシードを生成し、2番目のワーカーに2を追加するなど、単純で非常に間違っている可能性があることを行うことです。ワーカーなど。暗号品質のエントロピーが必要な場合は、まったく不十分です。ランダムウォークが必要な場合は、うまくいけばうまくいきます。
bgamari 2012年

3
私は同様の問題を解決しようとしているときにこの質問に出くわしました。モナドランダム計算にMonadRandomとSystem.Randomを並行して使用しています。これは、System.Randomのsplit関数でのみ可能です。これにはさまざまな結果が生成されるという欠点があります(その性質上、split機能します。ただし、これをRepa配列に拡張しようとしていますが、あまり運がありません。これで何か進歩したか、それとも死んでいますか?終わり?
トム・サベージ

1
シーケンシングと計算間の依存関係のないモナドは、私にとってより適用可能に聞こえます。
John Tyree 2013年

1
私はためらっています。Tom Savageが指摘splitするように、必要な基礎を提供しますsplitが、実装方法のソースに関するコメントに注意してください:「-これには統計的基礎がない!」。私は、PRNGを分割するどのような方法でも、ブランチ間に悪用可能な相関関係が残ると考えがちですが、それを証明する統計的背景はありません。一般的な質問に関して、私は確信がありません
isturdy

回答:


7

この質問が出されてから7年が経過しましたが、まだ誰もこの問題の適切な解決策を考え出していないようです。RepaにはmapM/のtraverseような関数がありません。並列化せずに実行できるものも含まれます。さらに、ここ数年の進歩の量を考えると、それが起こる可能性は低いと思われます。

Haskellの多くの配列ライブラリは陳腐な状態であり、それらの機能セットに対する私の全体的な不満のために、massivRepaからいくつかの概念を借用した配列ライブラリに数年の作業を費やしましたが、それを完全に異なるレベルにしています。イントロで十分です。

今日に先立ち、機能のような3つの単項マップにあったmassiv(関数のようにシノニムをカウントしません:imapMforMら。):

  • mapM-任意の通常のマッピングMonad。明らかな理由で並列化できず、少し遅い(mapMリストの通常の行に沿って遅い)
  • traversePrim-ここでは、に制限されていますPrimMonad。これは、よりもはるかに高速ですmapMが、その理由はこの説明では重要ではありません。
  • mapIO-これは、名前が示すように、制限されていますIO(またはに制限されてMonadUnliftIOいますが、それは無関係です)。今いるのでIO、コアと同じ数のチャンクに配列を自動的に分割し、個別のワーカースレッドを使用してIO、それらのチャンクの各要素にアクションをマッピングできます。fmap並列化も可能な純粋なとは異なりIO、マッピングアクションの副作用と組み合わせたスケジューリングの非決定性のため、ここにいる必要があります。

そのため、この質問を読んだ後、問題はで実際に解決されると思いましたmassivが、それほど速くはありません。ランダムな番号などのように、発電機、mwc-randomで、他random-fuの缶は多くのスレッド間で同じ発電機を使用しません。つまり、私が見逃していた唯一のパズルのピースは、「スポーンされたスレッドごとに新しいランダムシードを描画し、通常どおり続行する」ことでした。つまり、次の2つが必要でした。

  • ワーカースレッドと同じ数のジェネレータを初期化する関数
  • そして、アクションが実行されているスレッドに応じて、マッピング関数に正しいジェネレーターをシームレスに与える抽象化です。

それがまさに私がしたことです。

最初に、特別に作成されたrandomArrayWSinitWorkerStates関数を使用した例を示します。これらは質問との関連性が高く、後でより一般的なモナドマップに移動します。それらの型シグネチャは次のとおりです。

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)

に慣れていない人にとってmassivComp引数は使用する計算戦略であり、注目すべきコンストラクタは次のとおりです。

  • 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)

この機能をrvarrandom-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が、それはすでに別の話です。


ベンチマークを確認したい場合:alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins

4

PRNGには本質的に順次的な性質があるため、これを行うことはおそらくお勧めできません。代わりに、次のようにコードを移行することができます。

  1. IO関数(main、または何を持っているか)を宣言します。
  2. 必要な数の乱数を読み取ります。
  3. (今は純粋な)数値をrepa関数に渡します。

各並列スレッドで各PRNGをバーンインして統計的独立性を作成することは可能ですか?
J.アブラハムソン2013年

@ J.Abrahamsonはい、それは可能でしょう。私の答えを見てください。
lehins
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.