在Repa数组上的并行mapM

时间:2012-06-27 16:27:02

标签: arrays haskell parallel-processing repa

在我最近的work Gibbs sampling中,我一直在充分利用RVar,在我看来,{{3}}为随机数生成提供了近乎理想的界面。可悲的是,由于无法在地图中使用monadic动作,我无法使用Repa。

虽然显然monadic地图一般不能并行化,但在我看来RVar可能至少是monad的一个例子,其中效果可以安全地并行化(至少在原则上;我不是非常熟悉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的实施,原则上人们会认为这将涉及为每个产生的线程绘制一个新的随机种子并继续通常

直观地说,似乎同样的想法可能会推广到其他一些monad。

所以,我的问题是:是否可以构建一个monad类ParallelMonad,其效果可以安全地并行化(可能至少有RVar居住)?

它看起来像什么?还有哪些monad可能会在这个课程中出现?还有其他人考虑过如何在维修中发挥作用吗?

最后,如果并行monadic动作的这个概念不能一概而论,那么在RVar的特定情况下,有没有人看到任何一种很好的方法来使这项工作(它会非常有用)?放弃RVar并行是一个非常困难的权衡。

2 个答案:

答案 0 :(得分:4)

由于PRNG具有固有的顺序性,这可能不是一个好主意。相反,您可能希望按如下方式转换代码:

  1. 声明一个IO函数(main,或者你有什么)。
  2. 根据需要阅读尽可能多的随机数。
  3. 将(现在纯粹的)数字传递给您的修复功能。

答案 1 :(得分:2)

问这个问题已有7年了,似乎仍然没有人提出这个问题的好的解决方案。 Repa没有类似mapM / traverse的函数,即使没有并行也可以运行。而且,考虑到过去几年中取得的进步,似乎也不大可能发生。

由于Haskell中许多数组库的状态过旧,以及我对其功能集的总体不满,我将几年的工作放在了一个数组库massiv中,该库借鉴了Repa的一些概念,但是将其带到一个完全不同的水平。介绍足够了。

在今天之前,massiv中有三个像函数一样的单子图(不包括类似函数imapMforM等的同义词)

  • mapM-任意Monad中的常规映射。由于明显的原因,无法并行化,并且速度也很慢(沿着列表上的常规mapM行很慢)
  • traversePrim-在这里,我们仅限于PrimMonad,它比mapM快得多,但是这样做的原因在本次讨论中并不重要。
  • mapIO-顾名思义,此限制为IO(或更确切地说为MonadUnliftIO,但这无关紧要)。因为我们处于IO中,所以我们可以将数组自动拆分为与内核一样多的块,并使用单独的工作线程在这些块中的每个元素上映射IO操作。与也可以并行化的纯fmap不同,由于调度的不确定性以及映射操作的副作用,我们必须进入IO

因此,一旦我阅读了这个问题,我就以为自己这个问题实际上可以在massiv中解决,但是没有那么快。随机数生成器(例如mwc-random中的随机数生成器和random-fu中的其他随机数生成器)不能跨多个线程使用同一生成器。这意味着,我唯一缺少的难题是:“为产生的每个线程绘制一个新的随机种子,并像往常一样进行”。换句话说,我需要两件事:

  • 一个函数,该函数将初始化将有尽可能多的工作线程的生成器
  • 以及一个抽象,它将根据动作在哪个线程中无缝地为映射函数提供正确的生成器。

那正是我所做的。

首先,我将使用特制的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)

对于那些不熟悉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)

以下是有关如何将rvarrandom-fumersenne-random-pure64库与该功能一起使用的示例。我们也可以在这里使用randomArrayWS,但是为了举例说明,我们已经有一个包含不同RVarT的数组,在这种情况下,我们需要一个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,但这已经是一个独立的故事了。