MapM paralel pada array Repa

90

Dalam baru-baru ini saya bekerja dengan Gibbs sampling, saya telah membuat penggunaan besar dari RVaryang, dalam pandangan saya, menyediakan antarmuka yang ideal dekat ke generasi nomor acak. Sayangnya, saya tidak dapat menggunakan Repa karena ketidakmampuan untuk menggunakan tindakan monadik di peta.

Sementara peta monad yang jelas tidak dapat diparalelkan secara umum, menurut saya itu RVarmungkin setidaknya satu contoh monad di mana efek dapat diparalelkan dengan aman (setidaknya pada prinsipnya; Saya tidak terlalu akrab dengan cara kerja bagian dalam RVar) . Yakni, saya ingin menulis sesuatu seperti berikut ini,

drawClass :: Sample -> RVar Class
drawClass = ...

drawClasses :: Array U DIM1 Sample -> RVar (Array U DIM1 Class)
drawClasses samples = A.mapM drawClass samples

dimana A.mapMakan terlihat seperti,

mapM :: ParallelMonad m => (a -> m b) -> Array r sh a -> m (Array r sh b)

Sementara jelas bagaimana ini akan bekerja sangat tergantung pada implementasi RVardan yang mendasarinya RandomSource, pada prinsipnya orang akan berpikir bahwa ini akan melibatkan penarikan benih acak baru untuk setiap utas yang muncul dan dilanjutkan seperti biasa.

Secara intuitif, tampaknya gagasan yang sama ini mungkin digeneralisasikan ke beberapa monad lain.

Jadi, pertanyaan saya adalah: Bisakah seseorang membangun kelas ParallelMonadmonad yang efeknya dapat diparalelkan dengan aman (mungkin dihuni oleh, setidaknya, RVar)?

Akan terlihat seperti apa? Monad lain apa yang mungkin menghuni kelas ini? Apakah orang lain telah mempertimbangkan kemungkinan bagaimana hal ini dapat berhasil di Repa?

Akhirnya, jika gagasan aksi monadik paralel ini tidak dapat digeneralisasikan, apakah ada yang melihat cara yang bagus untuk membuat ini bekerja dalam kasus tertentu RVar(di mana akan sangat berguna)? Menyerah RVarkarena paralelisme adalah pertukaran yang sangat sulit.

bgamari
sumber
1
Saya kira poin penting adalah "menggambar benih acak baru untuk setiap utas yang muncul" - bagaimana langkah ini harus bekerja, dan bagaimana seharusnya benih digabungkan lagi setelah semua utas kembali?
Daniel Wagner
1
Antarmuka RVar hampir pasti membutuhkan beberapa tambahan untuk mengakomodasi pemijahan generator baru dengan benih yang diberikan. Memang, tidak jelas bagaimana mekanisme kerja ini dan tampaknya cukup RandomSourcespesifik. Upaya naif saya dalam menggambar benih adalah melakukan sesuatu yang sederhana dan kemungkinan besar sangat salah seperti menggambar vektor elemen (dalam kasus mwc-random) dan menambahkan 1 ke setiap elemen untuk menghasilkan benih untuk pekerja pertama, tambahkan 2 untuk yang kedua pekerja, dll. Sangat tidak memadai jika Anda membutuhkan entropi berkualitas kriptografi; semoga baik-baik saja jika Anda hanya perlu jalan-jalan acak.
bgamari
3
Saya menemukan pertanyaan ini saat mencoba memecahkan masalah serupa. Saya menggunakan MonadRandom dan System.Random untuk perhitungan acak monadik secara paralel. Ini hanya mungkin dengan splitfungsi System.Random . Ini memiliki kelemahan untuk menghasilkan hasil yang berbeda (karena sifat splittetapi berhasil. Namun, saya mencoba untuk memperluas ini ke array Repa dan tidak memiliki banyak keberuntungan. Apakah Anda membuat kemajuan dengan ini atau sudah mati- berakhir?
Tom Savage
1
Monad tanpa pengurutan dan ketergantungan antar komputasi terdengar lebih aplikatif bagi saya.
John Tyree
1
Saya ragu-ragu. Seperti yang splitdicatat oleh Tom Savage, memberikan dasar yang diperlukan, tetapi perhatikan komentar pada sumber bagaimana splitpenerapannya: "- tidak ada dasar statistik untuk ini!". Saya cenderung berpikir bahwa metode pemisahan PRNG akan meninggalkan korelasi yang dapat dieksploitasi antara cabang-cabangnya, tetapi tidak memiliki latar belakang statistik untuk membuktikannya. Mengenai pertanyaan umum, saya tidak yakin itu
isturdy

Jawaban:

7

Sudah 7 tahun sejak pertanyaan ini diajukan, dan sepertinya masih belum ada yang menemukan solusi yang baik untuk masalah ini. Repa tidak memiliki fungsi mapM/ traverselike, bahkan yang dapat berjalan tanpa paralelisasi. Terlebih lagi, mengingat jumlah kemajuan yang terjadi dalam beberapa tahun terakhir, tampaknya hal itu juga tidak mungkin terjadi.

Karena keadaan basi dari banyak perpustakaan array di Haskell dan ketidakpuasan saya secara keseluruhan dengan set fitur mereka, saya telah mengajukan beberapa tahun pekerjaan ke dalam perpustakaan array massiv, yang meminjam beberapa konsep dari Repa, tetapi membawanya ke tingkat yang sama sekali berbeda. Cukup dengan intro.

Sebelum hari ini, ada tiga peta monadik seperti fungsi dalam massiv(tidak termasuk sinonim seperti fungsi: imapM, forM. Et al):

  • mapM- pemetaan biasa secara sembarangan Monad. Tidak dapat diparalelkan karena alasan yang jelas dan juga agak lambat (seperti biasanya di mapMatas daftar lambat)
  • traversePrim- Di sini kami dibatasi PrimMonad, yang secara signifikan lebih cepat daripada mapM, tetapi alasan untuk ini tidak penting untuk diskusi ini.
  • mapIO- yang ini, seperti namanya, dibatasi untuk IO(atau lebih tepatnya MonadUnliftIO, tapi itu tidak relevan). Karena kita berada di dalam, IOkita dapat secara otomatis membagi array dalam banyak potongan karena ada inti dan menggunakan utas pekerja terpisah untuk memetakan IOtindakan atas setiap elemen dalam potongan tersebut. Tidak seperti murni fmap, yang juga dapat diparalelkan, kami harus berada di IOsini karena penjadwalan yang tidak ditentukan dikombinasikan dengan efek samping dari tindakan pemetaan kami.

Jadi, begitu saya membaca pertanyaan ini, saya berpikir bahwa masalahnya secara praktis sudah diselesaikan massiv, tetapi tidak secepat itu. Generator nomor acak, seperti in mwc-randomdan lainnya di random-futidak dapat menggunakan generator yang sama di banyak utas. Artinya, satu-satunya bagian dari teka-teki yang saya lewatkan adalah: "menggambar benih acak baru untuk setiap utas yang muncul dan melanjutkan seperti biasa". Dengan kata lain, saya membutuhkan dua hal:

  • Sebuah fungsi yang akan menginisialisasi sebanyak mungkin generator karena akan ada thread pekerja
  • dan abstraksi yang secara mulus akan memberikan generator yang benar ke fungsi pemetaan, bergantung pada utas tempat tindakan dijalankan.

Jadi itulah yang saya lakukan.

Pertama saya akan memberikan contoh penggunaan yang dibuat khusus randomArrayWS dan initWorkerStatesfungsinya, karena lebih relevan dengan pertanyaan dan kemudian pindah ke peta monadik yang lebih umum. Berikut adalah jenis tanda tangan mereka:

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)

Bagi yang belum terbiasa massiv , Compargumennya adalah strategi komputasi yang akan digunakan, konstruktor terkenal adalah:

  • Seq - menjalankan komputasi secara berurutan, tanpa membagi utas apa pun
  • Par - putar utas sebanyak mungkin dan gunakan utas itu untuk melakukan pekerjaan.

Saya akan menggunakan mwc-random paket sebagai contoh pada awalnya dan kemudian pindah ke RVarT:

λ> import Data.Massiv.Array
λ> import System.Random.MWC (createSystemRandom, uniformR)
λ> import System.Random.MWC.Distributions (standard)
λ> gens <- initWorkerStates Par (\_ -> createSystemRandom)

Di atas kami menginisialisasi generator terpisah per utas menggunakan keacakan sistem, tetapi kami dapat juga menggunakan benih per utas unik dengan mengambilnya dari WorkerId argumen, yang hanya merupakan Intindeks pekerja. Dan sekarang kita dapat menggunakan generator tersebut untuk membuat array dengan nilai acak:

λ> 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 ]
  ]

Dengan menggunakan Parstrategi, schedulerpustaka akan membagi pekerjaan generasi secara merata di antara pekerja yang tersedia dan setiap pekerja akan menggunakan generatornya sendiri, sehingga membuatnya aman untuk thread. Tidak ada yang mencegah kami untuk menggunakan kembali hal yang samaWorkerStates jumlah acak yang selama itu tidak dilakukan secara bersamaan, yang jika tidak akan mengakibatkan pengecualian:

λ> 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 ]

Sekarang mengesampingkan mwc-randomkita dapat menggunakan kembali konsep yang sama untuk kasus penggunaan lain yang mungkin dengan menggunakan fungsi sepertigenerateArrayWS :

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)

dan 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)

Berikut adalah contoh yang dijanjikan tentang cara menggunakan fungsionalitas ini dengan rvar, random-fudan mersenne-random-pure64pustaka. Kita juga bisa menggunakannya di randomArrayWSsini, tetapi sebagai contoh katakanlah kita sudah memiliki array dengan RVarTs yang berbeda , dalam hal ini kita membutuhkan 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 ]
  ]

Penting untuk dicatat, bahwa meskipun implementasi murni dari Mersenne Twister digunakan dalam contoh di atas, kami tidak dapat lepas dari IO. Ini karena penjadwalan non-deterministik, yang berarti kita tidak pernah tahu pekerja mana yang akan menangani bagian mana dari larik dan akibatnya generator mana yang akan digunakan untuk bagian larik mana. Di sisi atas, jika generator itu murni dan dapat dipisahkan, seperti splitmix, maka kita dapat menggunakan fungsi pembangkitan yang murni, deterministik, dan dapat diparalelkan :, randomArraytetapi itu sudah menjadi cerita yang terpisah.

lehins
sumber
Jika Anda ingin melihat beberapa tolok ukur: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins
4

Mungkin bukan ide yang baik untuk melakukan ini karena sifat PRNG yang berurutan secara inheren. Sebaliknya, Anda mungkin ingin mentransisikan kode Anda sebagai berikut:

  1. Deklarasikan fungsi IO (main , atau apa pun yang Anda miliki).
  2. Baca nomor acak sebanyak yang Anda butuhkan.
  3. Berikan nomor (sekarang murni) ke fungsi repa Anda.
Mcandre
sumber
Apakah mungkin untuk membakar setiap PRNG di setiap utas paralel untuk menciptakan kebebasan statistik?
J. Abrahamson
@ J. Abrahamson ya, itu mungkin. Lihat jawabanku.
lehins