mapM แบบขนานบนอาร์เรย์ Repa


90

ในงานล่าสุดของฉันกับGibbs samplingฉันได้ใช้ประโยชน์อย่างRVarเต็มที่ซึ่งในมุมมองของฉันเป็นอินเทอร์เฟซที่เหมาะสำหรับการสร้างตัวเลขแบบสุ่ม น่าเศร้าที่ฉันไม่สามารถใช้ Repa ได้เนื่องจากไม่สามารถใช้การกระทำแบบ monadic ในแผนที่ได้

ในขณะที่แผนที่ monadic โดยทั่วไปไม่สามารถขนานกันได้อย่างชัดเจน แต่สำหรับฉันแล้วRVarอาจเป็นตัวอย่างของ monad อย่างน้อยหนึ่งตัวอย่างที่เอฟเฟกต์สามารถขนานกันได้อย่างปลอดภัย (อย่างน้อยตามหลักการฉันไม่คุ้นเคยกับการทำงานภายในของ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
ฉันเดาว่าจุดยึดคือ "การวาดเมล็ดพันธุ์แบบสุ่มใหม่สำหรับแต่ละเธรดที่เกิด" - ขั้นตอนนี้ควรทำงานอย่างไรและควรรวมเมล็ดอีกครั้งเมื่อเธรดทั้งหมดกลับมาอีกครั้งอย่างไร
Daniel Wagner

1
อินเทอร์เฟซ RVar แทบจะต้องมีการเพิ่มเติมเพื่อรองรับการวางไข่เครื่องกำเนิดไฟฟ้าใหม่ด้วยเมล็ดพันธุ์ที่กำหนด เป็นที่ยอมรับว่ามันไม่ชัดเจนว่ากลไกของงานนี้เป็นอย่างไรและดูเหมือนจะค่อนข้างRandomSourceเฉพาะเจาะจง ความพยายามที่ไร้เดียงสาของฉันในการวาดเมล็ดพันธุ์คือการทำบางสิ่งที่เรียบง่ายและมีแนวโน้มที่จะผิดพลาดมากเช่นวาดเวกเตอร์ขององค์ประกอบ (ในกรณีของmwc-random) และเพิ่ม 1 ในแต่ละองค์ประกอบเพื่อสร้างเมล็ดพันธุ์สำหรับคนงานคนแรกเพิ่ม 2 สำหรับคนที่สอง คนงาน ฯลฯ ไม่เพียงพออย่างยิ่งหากคุณต้องการเอนโทรปีคุณภาพการเข้ารหัส หวังว่าจะดีถ้าคุณต้องการเดินแบบสุ่ม
bgamari

3
ฉันเจอคำถามนี้ขณะพยายามแก้ปัญหาที่คล้ายกัน ฉันใช้ MonadRandom และ System.Random สำหรับการคำนวณแบบสุ่ม monadic ควบคู่กันไป สิ่งนี้ทำได้เฉพาะกับsplitฟังก์ชันSystem.Random มันมีข้อเสียในการสร้างผลลัพธ์ที่แตกต่างกัน (เนื่องจากลักษณะของsplitแต่มันใช้งานได้อย่างไรก็ตามฉันกำลังพยายามขยายสิ่งนี้ไปยังอาร์เรย์ Repa และไม่มีโชคมากนักคุณมีความคืบหน้าเกี่ยวกับเรื่องนี้หรือไม่หรือมันตายไปแล้ว - จบ?
Tom Savage

1
Monad ที่ไม่มีการจัดลำดับและการอ้างอิงระหว่างการคำนวณฟังดูคล้ายกับฉันมากกว่า
John Tyree

1
ฉันลังเล ตามที่ Tom Savage กล่าวไว้splitให้พื้นฐานที่จำเป็น แต่โปรดสังเกตความคิดเห็นเกี่ยวกับแหล่งที่มาสำหรับวิธีsplitการใช้งาน: "- ไม่มีพื้นฐานทางสถิติสำหรับสิ่งนี้!" ฉันมีแนวโน้มที่จะคิดว่าวิธีการใด ๆ ในการแยก PRNG จะทำให้ความสัมพันธ์ที่หาประโยชน์ได้ระหว่างสาขาของมัน แต่ไม่มีพื้นฐานทางสถิติที่จะพิสูจน์ได้ เกี่ยวกับคำถามทั่วไปฉันไม่แน่ใจว่า
isturdy

คำตอบ:


7

เป็นเวลา 7 ปีแล้วที่มีการถามคำถามนี้และดูเหมือนว่าไม่มีใครคิดวิธีแก้ปัญหาที่ดีสำหรับปัญหานี้ Repa ไม่มีฟังก์ชันa mapM/ traverselike แม้แต่ฟังก์ชันเดียวที่สามารถทำงานได้โดยไม่ต้องขนานกัน ยิ่งไปกว่านั้นเมื่อพิจารณาถึงจำนวนความคืบหน้าในช่วงไม่กี่ปีที่ผ่านมาดูเหมือนว่าไม่น่าจะเกิดขึ้นเช่นกัน

เนื่องจากสถานะเก่าของไลบรารีอาร์เรย์จำนวนมากใน 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 -- ^ 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 ต่างกันอยู่แล้วซึ่งในกรณีนี้เราต้องการ 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แต่นั่นเป็นเรื่องราวที่แยกจากกัน


ในกรณีที่คุณต้องการดูเกณฑ์มาตรฐาน: alexey.kuleshevi.ch/blog/2019/12/21/random-benchmarks
lehins

4

อาจไม่ใช่ความคิดที่ดีที่จะทำเช่นนี้เนื่องจากลักษณะของ PRNG ตามลำดับโดยเนื้อแท้ แต่คุณอาจต้องการเปลี่ยนรหัสของคุณดังนี้:

  1. ประกาศฟังก์ชัน IO ( mainหรือคุณมีอะไร)
  2. อ่านตัวเลขสุ่มให้มากที่สุดเท่าที่คุณต้องการ
  3. ส่งตัวเลข (ตอนนี้บริสุทธิ์) ไปยังฟังก์ชัน repa ของคุณ

เป็นไปได้ไหมที่จะเบิร์นในแต่ละ PRNG ในแต่ละเธรดคู่ขนานเพื่อสร้างความเป็นอิสระทางสถิติ
J. Abrahamson

@ J. Abrahamson ใช่มันจะเป็นไปได้ ดูคำตอบของฉัน
lehins
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.