Паралельна картаM на масивах Repa


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
Я думаю, що точкою стримування є "вимальовування нового випадкового насіння для кожної породженої нитки" - як повинен діяти цей крок і як насіння слід об'єднати знову, як тільки всі потоки повернуться?
Даніель Вагнер,

1
Інтерфейс RVar майже напевно потребував би деяких доповнень, щоб забезпечити появу нового генератора із заданим насінням. Слід визнати, незрозуміло, як працює ця механіка, і це видається цілком RandomSourceконкретним. Моєю наївною спробою намалювати насіння було б зробити щось просте і, мабуть, дуже неправильне, наприклад, намалювати вектор елементів (у випадку mwc-random) і додати 1 до кожного елемента, щоб отримати насіння для першого працівника, додати 2 для другого працівник тощо. Дивно неадекватно, якщо вам потрібна ентропія криптографічної якості; сподіваємось, прекрасно, якщо вам просто потрібна випадкова прогулянка.
bgamari

3
Я зіткнувся з цим запитанням, намагаючись вирішити подібну проблему. Я використовую MonadRandom та System.Random для паралельних монадичних обчислень. Це можливо лише за допомогою splitфункції System.Random . Він має недолік у отриманні різних результатів (через природу, splitале це працює. Однак, я намагаюся поширити це на масиви Repa і не маю великої удачі. Чи досягли ви з цим будь-якого прогресу чи це мертве кінець?
Tom Savage

1
Монада без послідовності та залежностей між обчисленнями звучить для мене більше схожою на програму.
John Tyree

1
Я вагаюся. Як зазначає Том Сетвейж, це splitзабезпечує необхідну основу, але зверніть увагу на коментар до джерела щодо того, як splitце реалізовано: "- для цього немає статистичної основи!". Я схиляюся до думки, що будь-який метод розподілу PRNG залишить корисну взаємозв'язок між його гілками, але не має статистичної довідки про це. Щодо загального питання, я не впевнений, що
відбулось

Відповіді:


7

Минуло 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тут, але для прикладу, скажімо, ми вже маємо масив з різними 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, ми не можемо уникнути введення. Це пов’язано з недетермінованим плануванням, а це означає, що ми ніколи не знаємо, хто з робітників буде обробляти який фрагмент масиву і, отже, який генератор буде використовуватися для якої частини масиву. З іншого боку, якщо генератор є чистим і розділеним, наприклад splitmix, тоді ми можемо використовувати чисту, детерміновану та паралелізується функцію генерації:, randomArrayале це вже окрема історія.


На випадок, якщо ви хочете побачити деякі орієнтири: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins

4

Це, мабуть, не гарна ідея робити це через послідовно суттєвий характер PRNG. Натомість вам може знадобитися перенести ваш код наступним чином:

  1. Заявіть функцію вводу-виводу ( mainабо що у вас є).
  2. Прочитайте скільки завгодно випадкових чисел.
  3. Передайте (тепер чисті) числа на свої функції репарації.

Чи можна було б вживати кожен PRNG у кожному паралельному потоці, щоб створити статистичну незалежність?
Дж. Абрахамсон,

@ J.Abrahamson так, це було б можливо. Дивіться мою відповідь.
lehins
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.