Repa数组上的并行mapM


90

在我最近的工作Gibbs sampling,我已经作出了巨大的使用的RVar,在我看来,提供随机数生成近乎理想的接口。遗憾的是,由于无法在地图中使用单子动作,因此我无法使用Repa。

尽管显然单峰映射一般无法并行化,但在我看来,这RVar至少可以是一个可以安全地并行化效果的单子映射示例(至少在原理上;我对的内部运作并不十分熟悉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 2012年

1
RVar接口几乎肯定会需要一些补充,以适应使用给定的种子生成新的生成器。诚然,目前尚不清楚这项工作的机制,而且看起来确实很RandomSource具体。我天真地尝试绘制种子的尝试是做一些简单且可能很错误的事情,例如绘制元素向量(在的情况下mwc-random),并向每个元素添加1以为第一个工作人员生成种子,为第二个工作人员添加2。工人等。如果您需要加密质量的熵,那将是远远不够的;如果您只需要随机走动,希望很好。
bgamari 2012年

3
我在尝试解决类似问题时遇到了这个问题。我正在使用MonadRandom和System.Random并行进行Monadic随机计算。只有使用System.Random的split功能才有可能。它的缺点是产生不同的结果(由于的性质,split但它确实可以工作。但是,我试图将其扩展到Repa数组,但运气不佳。您在此方面取得了什么进展吗?还是死了?结束吗
汤姆·萨维奇

1
没有排序和计算之间的依赖关系的Monad听起来更像我。
约翰·泰瑞

1
我很犹豫。正如汤姆·萨维奇(Tom Savage)所指出的那样,split它提供了必要的基础,但是请注意源中如何split实现的注释:“-没有统计基础!”。我倾向于认为,任何拆分PRNG的方法都会在其分支之间留下可利用的相关性,但是没有统计背景来证明这一点。关于一般问题,我不确定
不稳定的

Answers:


7

提出此问题已有7年了,似乎仍然没有人提出这个问题的好的解决方案。Repa没有类似mapM/的traverse功能,即使没有并行也可以运行。此外,考虑到最近几年取得的进步,似乎也不大可能实现。

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

在此之前的今天,出现了像三种功能一元地图massiv(不包括类似功能的代名词:imapMforM。等):

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

因此,一旦我阅读了这个问题,我就以为自己可以在中解决该问题massiv,但是速度并没有那么快。in mwc-random和in 等随机数生成器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最初,我将使用package作为示例,然后转到RVarT

λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)

上面我们使用系统随机性为每个线程初始化了一个单独的生成器,但是我们也可以通过从WorkerId参数(仅Int是worker的索引)派生每个种子来使用唯一的种子。现在我们可以使用这些生成器来创建具有随机值的数组:

λ> 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这里使用,但是为了举例说明,我们已经有一个带有不同RVarTs 的数组,在这种情况下,我们需要一个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 19/12/24

4

由于PRNG的固有顺序性质,执行此操作可能不是一个好主意。相反,您可能需要按以下方式转换代码:

  1. 声明IO功能(main或您拥有的功能)。
  2. 根据需要读取任意数量的随机数。
  3. 将数字(现在是纯数字)传递到您的repa函数中。

是否可以在每个并行线程中预烧每个PRNG来创建统计独立性?
J. Abrahamson,

@ J.Abrahamson是的,有可能。看我的答案。
lehins
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.