이 질문을받은 지 7 년이 지났지 만 아직 아무도이 문제에 대한 좋은 해결책을 찾지 못한 것 같습니다. Repa에는 mapM
/ traverse
like 기능이 없으며 병렬화없이 실행할 수있는 기능도 있습니다. 더욱이 지난 몇 년 동안의 진전 정도를 고려할 때도 그렇게 될 것 같지 않습니다.
Haskell에있는 많은 어레이 라이브러리의 오래된 상태와 기능 세트에 대한 전반적인 불만족으로 인해 저는 massiv
Repa에서 일부 개념을 차용했지만 완전히 다른 수준으로 가져가는 어레이 라이브러리에 몇 년 동안 작업 했습니다. 인트로로 충분합니다.
오늘 이전에는 함수와 같은 세 개의 모나드 맵이있었습니다 massiv
(함수와 같은 동의어는 포함하지 않음 : imapM
, forM
et al.).
mapM
-임의의 Monad
. 명백한 이유 때문에 병렬화 할 수없고 약간 느립니다 ( mapM
목록에 비해 일반적인 줄을 따라 느림)
traversePrim
-여기서 우리는로 제한되어 있습니다 PrimMonad
. 이는보다 훨씬 빠르지 만이 mapM
논의에서는 그 이유가 중요하지 않습니다.
mapIO
-이것은 이름에서 알 수 있듯이로 제한됩니다 IO
(또는 오히려 MonadUnliftIO
,하지만 관련성이 없음). 우리는 IO
코어 수만큼 자동으로 배열을 분할하고 별도의 작업자 스레드를 사용 IO
하여 해당 청크의 각 요소에 대한 작업 을 매핑 할 수 있습니다. fmap
병렬화도 가능한 pure와 달리 , 우리는 IO
매핑 작업의 부작용과 결합 된 스케줄링의 비결 정성 때문에 여기에 있어야합니다 .
그래서이 질문을 읽었을 때 문제는 실제로에서 해결 massiv
되었지만 그렇게 빠르지 는 않다고 생각했습니다 . in mwc-random
및 in 과 random-fu
같은 난수 생성기는 여러 스레드에서 동일한 생성기를 사용할 수 없습니다. 즉, 내가 놓친 유일한 퍼즐 조각은 "생성 된 각 스레드에 대해 새로운 임의의 시드를 그리고 평소대로 진행하는 것"이었습니다. 즉, 두 가지가 필요했습니다.
- 작업자 스레드 수만큼 생성기를 초기화하는 함수
- 액션이 실행되는 스레드에 따라 매핑 함수에 올바른 생성기를 원활하게 제공하는 추상화.
그게 바로 제가 한 일입니다.
먼저 질문과 더 관련이 있는 특수 제작 randomArrayWS
및 initWorkerStates
기능을 사용하여 예제를 제공 하고 나중에보다 일반적인 모나드 맵으로 이동합니다. 다음은 유형 서명입니다.
randomArrayWS ::
(Mutable r ix e, MonadUnliftIO m, PrimMonad m)
=> WorkerStates g
-> Sz ix
-> (g -> m e)
-> 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
-> (ix -> s -> m e)
-> 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)
-> Array r' ix a
-> m (Array r ix b)
다음은 rvar
, random-fu
및 mersenne-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
있지만 이는 이미 별도의 이야기입니다.