packages feed

lazyppl-1.1.0: test/Concurrency.hs

{-# OPTIONS_GHC -fno-cse -fno-full-laziness #-}
module Main where

import Control.Concurrent (forkFinally, newEmptyMVar, putMVar, takeMVar)
import Control.Concurrent.MVar (MVar)
import Control.Exception (evaluate, throwIO)
import Control.Monad (forM, replicateM, unless, when)
import Data.IORef (IORef, atomicModifyIORef', newIORef, writeIORef)
import qualified Data.Set as Set
import LazyPPL (Prob, mh, randomTree, runProb, sample, uniform, weightedsamples)
import LazyPPL.Distributions.Counter (newCounter, readAndIncrement)
import LazyPPL.Distributions.GP (gp)
import LazyPPL.Distributions.Memoization (generalmemoize)
import System.Exit (exitFailure)
import System.IO.Unsafe (unsafePerformIO)
import System.Random (mkStdGen)
import System.Timeout (timeout)

-- Runs `draw` `runs` times in each of two threads at once.
concurrentDraws :: Int -> IO a -> IO [a]
concurrentDraws runs draw = do
  slots <- forM [1, 2 :: Int] $ \_ -> do
    slot <- newEmptyMVar
    _ <- forkFinally (replicateM runs (draw >>= evaluate)) (putMVar slot)
    pure slot
  concat <$> mapM (\slot -> takeMVar slot >>= either throwIO pure) slots

report :: String -> Bool -> String -> IO Bool
report name ok detail = do
  putStrLn $ (if ok then "PASS " else "FAIL ") ++ name ++ ": " ++ detail
  pure ok

-- Each inference run takes its own generator from the global one. Two runs
-- that start at the same moment must still get different generators, so no
-- two runs share their first draw.
distinctFirstDraws :: String -> IO Double -> IO Bool
distinctFirstDraws name draw = do
  draws <- concurrentDraws 20000 draw
  let distinct = Set.size (Set.fromList draws)
  report name (distinct == length draws)
    (show distinct ++ " distinct first draws in " ++ show (length draws) ++ " concurrent runs")

-- The pause point of the interleaving tests below: the thread that makes the
-- `pauseAt`-th marked call signals `inside`, then waits for `release`. The
-- wait has a time limit, because a correct implementation can make the other
-- thread wait for this one.
{-# NOINLINE pauseHook #-}
pauseHook :: IORef (IO ())
pauseHook = unsafePerformIO (newIORef (pure ()))

markedCall :: IO ()
markedCall = do
  action <- atomicModifyIORef' pauseHook (\a -> (a, a))
  action

pauseAt :: Int -> IO (MVar (), MVar ())
pauseAt target = do
  inside <- newEmptyMVar
  release <- newEmptyMVar
  calls <- newIORef (0 :: Int)
  writeIORef pauseHook $ do
    n <- atomicModifyIORef' calls (\c -> (c + 1, c + 1))
    when (n == target) $ do
      putMVar inside ()
      _ <- timeout 2000000 (takeMVar release)
      pure ()
  pure (inside, release)

-- Keys whose comparison makes a marked call when the left key is 1. The
-- comparison results are the ordinary ones.
newtype Key = Key Int deriving (Eq, Show)

instance Ord Key where
  compare (Key a) (Key b) = unsafePerformIO (when (a == 1) markedCall) `seq` compare a b

-- Two threads query a memoized stochastic function at two new keys at once.
-- The first thread pauses at its second comparison, after it has read the
-- table and before it has stored its entry, and the second thread runs in
-- between. Each key must still get its own draw, and a repeated query must
-- return the draw made for its key.
memoInterleaving :: IO Bool
memoInterleaving = do
  let f = runProb (generalmemoize (const uniform :: Key -> Prob Double)) (randomTree (mkStdGen 7))
  _ <- evaluate (f (Key 0))
  (inside, release) <- pauseAt 2
  slot <- newEmptyMVar
  _ <- forkFinally (evaluate (f (Key 1))) (putMVar slot)
  takeMVar inside
  second <- evaluate (f (Key 2))
  putMVar release ()
  first <- takeMVar slot >>= either throwIO pure
  writeIORef pauseHook (pure ())
  repeated1 <- evaluate (f (Key 1))
  repeated2 <- evaluate (f (Key 2))
  report "memoize under an interleaving" (first /= second && repeated1 == first && repeated2 == second)
    ("(first1,first2,repeat1,repeat2) = " ++ show (first, second, repeated1, repeated2))

-- The same interleaving for a white-noise Gaussian process, whose values at
-- distinct points are independent. The covariance function makes the marked
-- call at (1,1), inside the first thread's query.
gpInterleaving :: IO Bool
gpInterleaving = do
  let cov x y = unsafePerformIO (when (x == 1 && y == 1) markedCall) `seq` (if x == y then 1 else 0)
      f = runProb (gp cov) (randomTree (mkStdGen 8))
  _ <- evaluate (f 0)
  (inside, release) <- pauseAt 1
  slot <- newEmptyMVar
  _ <- forkFinally (evaluate (f 1)) (putMVar slot)
  takeMVar inside
  second <- evaluate (f 2)
  putMVar release ()
  first <- takeMVar slot >>= either throwIO pure
  writeIORef pauseHook (pure ())
  repeated1 <- evaluate (f 1)
  repeated2 <- evaluate (f 2)
  report "white-noise gp under an interleaving" (first /= second && repeated1 == first && repeated2 == second)
    ("(f1,f2,repeat f1,repeat f2) = " ++ show (first, second, repeated1, repeated2))

-- A counter hands out each number once, even to two threads at once. The
-- counter is bound by evaluate, as a Prob bind would bind it: a let-bound
-- counter can be inlined into the lambda below, with one IORef per call.
counterUnique :: IO Bool
counterUnique = do
  counter <- evaluate (runProb newCounter (randomTree (mkStdGen 9)))
  let next k = runProb (readAndIncrement counter) (randomTree (mkStdGen k))
  seeds <- newIORef (0 :: Int)
  numbers <- concurrentDraws 20000 $ do
    k <- atomicModifyIORef' seeds (\s -> (s + 1, s))
    evaluate (next k)
  let distinct = Set.size (Set.fromList numbers)
  report "counter shared by two threads" (distinct == length numbers)
    (show distinct ++ " distinct numbers from " ++ show (length numbers) ++ " increments")

main :: IO ()
main = do
  results <- sequence
    [ distinctFirstDraws "weightedsamples" (fst . head <$> weightedsamples (sample uniform))
    , distinctFirstDraws "mh" (fst . head <$> mh 1 (sample uniform))
    , memoInterleaving
    , gpInterleaving
    , counterUnique
    ]
  unless (and results) exitFailure