Минуло 7 років з того моменту, як було поставлено це питання, і все ще здається, що ніхто не придумав хорошого рішення цієї проблеми. Repa не має mapM
/ traverse
подібної функції, навіть такої, яка може працювати без розпаралелювання. Більше того, враховуючи обсяг прогресу, який був досягнутий за останні кілька років, здається малоймовірним, що він теж відбудеться.
Через застарілий стан багатьох бібліотек масивів у Хаскелі та моє загальне невдоволення їх наборами функцій, я висунув кілька років роботи над бібліотекою масивів massiv
, яка запозичує деякі поняття у Repa, але виводить це на зовсім інший рівень. Досить із вступом.
До сьогоднішнього дня, було три Монадический карта як функції в massiv
(не рахуючи синонімом типу функцій: imapM
, forM
. І ін):
mapM
- звичайне відображення в довільному порядку Monad
. Неможливо розпаралелювати зі зрозумілих причин, а також трохи повільно (за типом звичайного mapM
над списком повільно)
traversePrim
- тут ми обмежуємось цим PrimMonad
, що значно швидше, ніж mapM
, але причина цього не є важливою для цієї дискусії.
mapIO
- цим, як випливає з назви, обмежено IO
(вірніше MonadUnliftIO
, але це не має значення). Оскільки ми знаходимось, IO
ми можемо автоматично розділити масив на стільки фрагментів, скільки ядер, і використовувати окремі робочі потоки для відображення IO
дії над кожним елементом у цих фрагментах. На відміну від чистого fmap
, який також паралелізується, ми маємо бути IO
тут через недетермінованість планування у поєднанні з побічними ефектами нашої картографічної дії.
Тож, прочитавши це запитання, я подумав собі, що проблема практично вирішена massiv
, але не так швидко. Генератори випадкових чисел, такі як in 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 ]
]
Важливо зазначити, що, незважаючи на те, що в наведеному вище прикладі використовується чиста реалізація Mersenne Twister, ми не можемо уникнути введення. Це пов’язано з недетермінованим плануванням, а це означає, що ми ніколи не знаємо, хто з робітників буде обробляти який фрагмент масиву і, отже, який генератор буде використовуватися для якої частини масиву. З іншого боку, якщо генератор є чистим і розділеним, наприклад splitmix
, тоді ми можемо використовувати чисту, детерміновану та паралелізується функцію генерації:, randomArray
але це вже окрема історія.