เป็นเวลา 7 ปีแล้วที่มีการถามคำถามนี้และดูเหมือนว่าไม่มีใครคิดวิธีแก้ปัญหาที่ดีสำหรับปัญหานี้ Repa ไม่มีฟังก์ชันa mapM
/ traverse
like แม้แต่ฟังก์ชันเดียวที่สามารถทำงานได้โดยไม่ต้องขนานกัน ยิ่งไปกว่านั้นเมื่อพิจารณาถึงจำนวนความคืบหน้าในช่วงไม่กี่ปีที่ผ่านมาดูเหมือนว่าไม่น่าจะเกิดขึ้นเช่นกัน
เนื่องจากสถานะเก่าของไลบรารีอาร์เรย์จำนวนมากใน Haskell และความไม่พอใจโดยรวมของฉันกับชุดคุณลักษณะของพวกเขาฉันจึงได้ทำงานสองสามปีในไลบรารีอาร์เรย์massiv
ซึ่งยืมแนวคิดบางอย่างจาก Repa แต่นำไปสู่ระดับที่แตกต่างไปจากเดิมอย่างสิ้นเชิง พอแล้วกับบทนำ
ก่อนที่จะมีในวันนี้มีสามแผนที่เอกเช่นฟังก์ชั่นในmassiv
(ไม่นับคำพ้องเช่นฟังก์ชั่น: imapM
, forM
., et al):
mapM
- Monad
การทำแผนที่ตามปกติโดยพลการ ไม่สามารถขนานกันได้ด้วยเหตุผลที่ชัดเจนและยังค่อนข้างช้า (ตามแนวปกติmapM
ในรายการช้า)
traversePrim
- เราถูก จำกัด ไว้ที่นี่PrimMonad
ซึ่งเร็วกว่าอย่างเห็นได้ชัดmapM
แต่เหตุผลนี้ไม่สำคัญสำหรับการสนทนานี้
mapIO
- ชื่อนี้ถูก จำกัด ไว้ที่IO
(หรือมากกว่าMonadUnliftIO
แต่ไม่เกี่ยวข้อง) เนื่องจากเราอยู่ในIO
เราสามารถแบ่งอาร์เรย์โดยอัตโนมัติเป็นชิ้น ๆ ได้มากที่สุดเท่าที่มีคอร์และใช้เธรดผู้ปฏิบัติงานแยกกันเพื่อแมปการIO
กระทำกับแต่ละองค์ประกอบในส่วนเหล่านั้น ซึ่งแตกต่างจากเพียวfmap
ซึ่งสามารถขนานกันได้เช่นกันเราต้องอยู่IO
ที่นี่เนื่องจากการไม่กำหนดตารางเวลารวมกับผลข้างเคียงของการทำแผนที่ของเรา
ดังนั้นเมื่อฉันอ่านคำถามนี้ฉันคิดกับตัวเองว่าปัญหาได้รับการแก้ไขในทางปฏิบัติmassiv
แต่ไม่เร็วขนาดนั้น ตัวสร้างตัวเลขสุ่มเช่นในmwc-random
และอื่น ๆ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
s ต่างกันอยู่แล้วซึ่งในกรณีนี้เราต้องการ a 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
แต่นั่นเป็นเรื่องราวที่แยกจากกัน