imp-ppl-0.1.0.0: bench/Bench.hs
{-# OPTIONS_GHC -fno-full-laziness #-}
{-# LANGUAGE GADTs #-}
module Bench
( BenchCase(..)
, BenchConfig(..)
, defaultConfig
, runAll
) where
import Control.Monad (replicateM, when)
import Data.List (sort)
import Data.Time.Clock (getCurrentTime, diffUTCTime)
import System.IO (hFlush, stdout)
import System.Mem (performGC)
import Imp.DSL (Imp)
import Imp.Inference (intervalProbability, intervalProbabilityApprox)
import Imp.Inference.Optimize (optimizeProbability)
import Imp.Inference.Symbolic (intervalProbabilitySymbolic)
-- | A single benchmark case.
data BenchCase where
BenchCase :: Ord a => Int -> Imp g a -> (a -> Bool) -> BenchCase
-- | Median timing across the four inference methods, in seconds.
data BenchResult = BenchResult
{ brN :: Int
, brExact :: Maybe Double
, brSymbolic :: Maybe Double
, brApprox :: Double
, brGrad :: Double
}
data BenchConfig = BenchConfig
{ bcReps :: Int -- ^ median over this many reps
, bcGradSteps :: Int -- ^ gradient-descent step count
, bcGradLR :: Double -- ^ gradient-descent learning rate
, bcMaxExactN :: Maybe Int -- ^ skip exact enumeration when @N > this@
, bcMaxSymbolicN :: Maybe Int -- ^ skip symbolic when @N > this@
, bcGradBounds :: Bool -- ^ run gradient in both directions
, bcGcBetween :: Bool -- ^ 'performGC' before each timed rep
}
-- | Defaults tuned for the Ellsberg-style benches (fast individual reps,
-- high-dimensional gradient landscapes). Slower benches (e.g. Robot)
-- override 'bcGradSteps', 'bcGradLR', and 'bcGradBounds' to suit.
defaultConfig :: BenchConfig
defaultConfig = BenchConfig
{ bcReps = 10
, bcGradSteps = 30
, bcGradLR = 0.3
, bcMaxExactN = Just 20
, bcMaxSymbolicN = Just 20
, bcGradBounds = True
, bcGcBetween = True
}
-- ---------------------------------------------------------------------------
-- Pipeline
-- ---------------------------------------------------------------------------
-- | Run every case, emitting CSV (header + per-case rows) to stdout in real-time.
runAll :: BenchConfig -> [BenchCase] -> IO ()
runAll cfg cs = do
putStrLn csvHeader
hFlush stdout
mapM_ (\c -> runBench cfg c >>= emitCsvRow) cs
-- | Run a single benchmark case, timing each of the four inference
-- methods and returning their medians.
runBench :: BenchConfig -> BenchCase -> IO BenchResult
runBench cfg (BenchCase n prog predicate) = do
let skipExact = maybe False (n >) (bcMaxExactN cfg)
skipSymbolic = maybe False (n >) (bcMaxSymbolicN cfg)
steps = bcGradSteps cfg
lr = bcGradLR cfg
tExact <- if skipExact
then return Nothing
else Just <$> timeFresh cfg (\() -> intervalProbability prog predicate)
tSymbolic <- if skipSymbolic
then return Nothing
else Just <$> timeFresh cfg (\() -> intervalProbabilitySymbolic prog predicate)
tApprox <- timeFresh cfg (\() -> intervalProbabilityApprox prog predicate)
tGrad <- timeFresh cfg $ \() ->
if bcGradBounds cfg
then gradientBounds steps lr prog predicate
else let (_, v) = optimizeProbability prog predicate steps lr
in (v, v)
return (BenchResult n tExact tSymbolic tApprox tGrad)
-- | Gradient-descent bounds.
gradientBounds :: Ord a => Int -> Double -> Imp g a -> (a -> Bool) -> (Double, Double)
gradientBounds nSteps lr prog predicate =
let (_, pMax) = optimizeProbability prog predicate nSteps lr
(_, pMin) = optimizeProbability prog predicate nSteps (-lr)
in (pMin, pMax)
-- ---------------------------------------------------------------------------
-- Timing
-- ---------------------------------------------------------------------------
-- | Median wall-clock time (in seconds) of @bcReps@ fresh evaluations of
-- the supplied nullary function.
timeFresh :: BenchConfig -> (() -> (Double, Double)) -> IO Double
timeFresh cfg compute = do
ts <- replicateM (bcReps cfg) $ do
when (bcGcBetween cfg) performGC
(_, t) <- timeIt $
let (a, b) = compute () in a `seq` b `seq` return (a, b)
return t
return (median ts)
timeIt :: IO a -> IO (a, Double)
timeIt act = do
t0 <- getCurrentTime
x <- act
t1 <- getCurrentTime
return (x, realToFrac (diffUTCTime t1 t0))
median :: [Double] -> Double
median xs =
let s = sort xs
m = length s
in if odd m
then s !! (m `div` 2)
else 0.5 * (s !! (m `div` 2 - 1) + s !! (m `div` 2))
-- ---------------------------------------------------------------------------
-- CSV output
-- ---------------------------------------------------------------------------
-- | CSV column header: @N,method,time_s@.
csvHeader :: String
csvHeader = "N,method,time_s"
-- | Four CSV rows (one per method) for a single result.
emitCsvRow :: BenchResult -> IO ()
emitCsvRow r = do
let n = brN r
putStrLn (show n ++ ",exact," ++ maybe "NaN" show (brExact r))
putStrLn (show n ++ ",symbolic," ++ maybe "NaN" show (brSymbolic r))
putStrLn (show n ++ ",interval," ++ show (brApprox r))
putStrLn (show n ++ ",gradient," ++ show (brGrad r))
hFlush stdout