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
/ traverse
like, 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 mapM
atas 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, IO
kita dapat secara otomatis membagi array dalam banyak potongan karena ada inti dan menggunakan utas pekerja terpisah untuk memetakan IO
tindakan atas setiap elemen dalam potongan tersebut. Tidak seperti murni fmap
, yang juga dapat diparalelkan, kami harus berada di IO
sini 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-random
dan lainnya di random-fu
tidak 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 initWorkerStates
fungsinya, 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
-> Sz ix
-> (g -> m e)
-> m (Array r ix e)
initWorkerStates :: MonadIO m => Comp -> (WorkerId -> m s) -> m (WorkerStates s)
Bagi yang belum terbiasa massiv
, Comp
argumennya 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 Int
indeks 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 Par
strategi, scheduler
pustaka 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-random
kita 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
-> (ix -> s -> m e)
-> 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)
-> Array r' ix a
-> m (Array r ix b)
Berikut adalah contoh yang dijanjikan tentang cara menggunakan fungsionalitas ini dengan rvar
, random-fu
dan mersenne-random-pure64
pustaka. Kita juga bisa menggunakannya di randomArrayWS
sini, tetapi sebagai contoh katakanlah kita sudah memiliki array dengan RVarT
s 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 :, randomArray
tetapi itu sudah menjadi cerita yang terpisah.