并行计算具有快速随机性和纯度?

时间:2013-04-27 05:10:56

标签: haskell random parallel-processing

我的目标是使用parallel package中的parMap来并行化计算,但我还想为我的采样函数添加一些随机性。

如果没有随机性,我的计算只是一些数字运算,所以它是纯粹的,我可以使用parMap。为了获得良好的结果,我需要在每一步采取多个样本并平均结果。抽样需要随机化。

一种解决方案可能是使用random package,调用randoms然后在计算过程中使用该列表(通过将纯惰性列表传递给计算我会保持纯净)。不幸的是,这是一个非常慢的随机数生成器,我需要大量随机数,所以我更喜欢使用mwc-randommersenne-random(尽管我不认为mersenne-random仍然保持不变)

使用unsafePerformIO和mwc-random之类的东西来编写像randoms这样的函数是否安全?像这样:

randomsMWC :: Variate a => GenST s -> [a]
randomsMWC g = unsafePerformIO $ unsafeSTToIO $ randomsMWC' g
  where
  randomsMWC' g = do
    a  <- uniform g
    as <- unsafeInterleaveST $ randomsMWC' g
    return (a : as)

我是否需要转而使用parallel number generator?或者我是否需要咬紧牙关并承认我的算法在没有使用慢随机包的情况下根本不是纯粹的?

连连呢?谢谢!

4 个答案:

答案 0 :(得分:7)

如果有一个单线程的随机源不是性能问题,你可以得到一个纯粹的包装mwc-random

import Control.Monad.ST.Lazy
import Control.Monad
import System.Random.MWC

rList :: Variate a => Seed -> [a]
rList s = runST $ do
  g <- strictToLazyST $ restore s
  advance g

advance :: Variate a => Gen s -> ST s [a]
advance g = do
  x <- strictToLazyST $ uniform g
  xs <- x `seq` advance g
  return (x:xs)

这里rList取种子,然后懒洋洋地确定性地产生无限的懒数。我不确定strictToLazyST是否真的安全,但似乎没有人反对它。我没有做任何基准测试,但我怀疑这是非常快的。我假设mwc-random是线程安全的,因为使用生成器编码的explit数据流,并且它可以在ST monad中使用。邀请某人使用上面的黑客。我不认为seq是必要的,但它让我对strictToLazyST不再怀疑我知道我有确定性的评估顺序(并且它仍然懒得工作)。

你仍然需要随机性(即IO)生成一个真正的随机种子,但这应该让你保持大部分代码纯净,并让你将种子存储到文件或在必要时重用它。

GHCI:

λ: gen <- create
λ: x <- save gen
λ: drop 1 $ take 10 $ rList x :: [Int]
[4443157817804305558,-6283633474236987377,3602072957429409483,
 -5035101780705339502,-925260411398682800,423158235494603307,
 -6981326876987356147,490540715145304360,-8184987036354194323]

答案 1 :(得分:7)

我在Github上有一个不太准备发布的程序包hsrandom123,可能会对此有所帮助。我已经开始实现这个包,以便有一个合适的RNG用于并行计算。它从the random123 C library重新实现了Philox和Threefry RNG(也有一篇论文描述了那里的想法)。

我的库未发布是有原因的:虽然实际的RNG实现完成并且似乎产生与C版本相同的结果,但我还没有确定要使用的Haskell接口,并且很难记录库。如果您需要更多信息或帮助我使用它,请随时与我联系。

答案 2 :(得分:6)

我的猜测是 mersenne-random 不是线程安全的,因此使用任何unsafe...和并行化将导致从多个线程调用它时出现问题。 (另见GHC manual第8.2.4.1节。)

需要随机性的函数并不纯粹,它们需要一些随机源,它可以是外部的(hardware - 就像设备采样噪声),因此绑定到IO或伪随机,在计算过程中需要保持一些状态。无论哪种方式,它们都不能是纯粹的Haskell函数。

我首先将您的需求分离到特定的monad类型类,例如

class Monad m => MonadParRand m where
    random      :: MTRandom a => m a
    parSequence :: [m a] -> m [a]

这将允许您编写主代码而不受特定实现的约束。或者,如果你感觉大胆,可以使用monad-parallel并定义类似

的内容
class MonadParallel m => MonadParRand m where
    random      :: MTRandom a => m a

现在棘手的部分是如何定义randomparSequence(或MonadParallel的{​​{3}})以使其变速。由于您可以控制bindM2,因此您可以管理线程的生成方式以及它们保留的状态。因此,您可以将缓冲区绑定到从中绘制随机数的每个线程。如果缓冲区为空,它会对mersenne-random(或其他基于IO的数字生成器)进行同步调用,填充缓冲区并继续。

(如果你实现类似的东西,那么从中创建一个独立的库是非常好的。)


请注意,来自mersenne-random的randoms已经使用unsafeInterleaveIO来生成一个惰性列表,但我想这个列表只能从单个线程中使用。它也有改进的余地。它使用unsafeInterleaveIO并且有一些开销,如bindM2中所述:

  

这里有实际的开销。考虑急切地填充块并逐段提取元素。

答案 3 :(得分:0)

为了完整答案,让我解释一下我目前正在做什么来解决这个问题。

我没有尝试使计算变得纯粹,而是选择使用async包而不是parallel

如果我决定将当前的解决方案修改为纯粹,我将首先尝试Philip JF建议的解决方案,因此我将其答案标记为已接受的解决方案。

我的下一个挑战是弄清楚如何近似工作的最佳分块,以便穿线减少时间而不是花费更长的时间:)