在我最近的工作与Gibbs sampling
,我已经作出了巨大的使用的RVar
,在我看来,提供随机数生成近乎理想的接口。 可悲的是,我一直无法利用惹巴的,因为不能在地图使用一元行动。
同时明确单子地图无法在一般的并行化,但在我看来, 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
)?
也许它是什么样的? 还有什么其他的单子可能会居住在这个班? 有没有人考虑的是如何在可能的工作惹巴的可能性?
最后,如果平行的一元行动这一概念不能一概而论,有没有人看到任何好的办法,使在特定的情况下,该工作RVar
(它是非常有用的)? 放弃RVar
的并行是一个非常艰难的权衡。
这可能不是一个好主意,这样做是由于的PRNG固有的有序性。 相反,你可能要转换您的代码如下:
- 声明一个IO功能(
main
,或者你有什么)。 - 因为你需要阅读尽可能多的随机数。
- 通过(现在纯)号到您repA的功能。
它已经7年,因为这个问题已经被问,它仍然好像没有人有很好的解决了这个问题上来。 惹巴没有mapM
/ traverse
像功能,即使是一个可以不并行运行。 此外,考虑到进步的量存在,在过去几年中,它似乎不太可能,它要么发生。
由于在Haskell许多阵列库和我与他们的功能集的整体不满的失效状态的我已经提出了几年的工作成阵列库的massiv
,它借鉴了惹巴一些概念,但它需要一个完全不同的水平。 足以与介绍。
在此之前的今天,出现了像三种功能一元地图massiv
(不包括像功能的代名词: imapM
, forM
等人):
-
mapM
-以任意常规映射Monad
。 不并行出于显而易见的原因,也是有点慢(通常沿的线条mapM
一个列表慢) -
traversePrim
-在这里,我们仅限于PrimMonad
,比显著快mapM
,但这样做的原因是不是这个讨论的重点。 -
mapIO
-这一个,如名称所暗示的,被限制为IO
(或更确切地说MonadUnliftIO
,但是这是不相关的)。 因为我们在IO
我们可以在尽可能多的块,因为有核和使用单独的工作线程映射的自动分割阵列IO
比在那些组块的每个元件的操作。 与单纯的fmap
,这也是并行的,我们必须在IO
这里,因为调度与我们的映射作用的副作用组合的非确定性的。
所以,一旦我看到这个问题,我心想,这个问题在实际解决了massiv
,但没有那么快。 随机数发生器,如在mwc-random
等人在random-fu
不能跨多个线程使用相同的发电机。 这意味着,我错过了这一难题的唯一的一块是:“引为每个线程一个新的随机种子产卵,并继续像往常一样”。 换句话说,我需要做两两件事:
- 因为要去工作线程将初始化许多发电机的函数
- 和抽象,将无缝地给出正确生成映射函数依赖于哪个线程操作已运行。
所以这正是我所做的。
首先,我会给使用特制的例子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 ]
]
需要注意的是,尽管纯执行梅森难题是在上面的例子中所使用的事实,我们无法逃避的IO是很重要的。 这是因为非确定性调度,这意味着我们永远不知道哪一个工人将处理数组的这一块,因此该发电机将被用于该阵列的哪个部分。 在向上的一面,如果发电机是纯粹的分裂型,如splitmix
,那么我们就可以使用纯,确定性和并行生成功能: randomArray
,但是这已经是一个独立的故事。