packages feed

monad-bayes-0.1.0.0: benchmark/Speed.hs

import Criterion.Main
import Criterion.Types
import System.Random.MWC (createSystemRandom, GenIO)
import Control.Monad (replicateM)

import Debug.Trace

import GHC.IO.Handle
import System.Exit
import System.Process hiding (env)

import Control.Monad.Bayes.Class
import Control.Monad.Bayes.Sampler
import Control.Monad.Bayes.Weighted
import Control.Monad.Bayes.Inference.SMC
import Control.Monad.Bayes.Inference.RMSMC
import Control.Monad.Bayes.Population
import Control.Monad.Bayes.Traced

-- import NonlinearSSM
import qualified HMM
import qualified LogReg
import qualified LDA

-- | Path to the Anglican project with benchmarks.
anglicanPath :: String
anglicanPath = "/scratch/ams240/repos/anglican-white-paper/experiments"

-- | Running Leiningen repl process.
-- Leiningen takes a lot of time to start so we keep a repl
-- running to speed up benchmarking.
-- Contains in order input handle, output handle, and process handle.
data LeinProc = LeinProc Handle Handle ProcessHandle

anglicanModelName :: Model -> String
anglicanModelName (LR _) = "logisticRegression"
anglicanModelName (HMM _) = "hmm"
anglicanModelName (LDA _) = "lda"

clojureBool :: Bool -> String
clojureBool False = "false"
clojureBool True = "true"

-- | Format Haskell [Bool] as a Clojure Boolean vector.
clojureBoolVector :: [Bool] -> String
clojureBoolVector = clojureVector . map clojureBool

-- | Format a Haskell list as a Clojure vector.
clojureVector :: [String] -> String
clojureVector xs = "[" ++ unwords xs ++ "]"

-- | Format a Haskell list as a Clojure vector.
clojureShowVector :: Show a => [a] -> String
clojureShowVector xs = clojureVector $ map show xs

-- | Insert data into an Anglican model.
anglicanData :: LeinProc -> Model -> IO ()
anglicanData lein model = do
  anglican lein ["(use 'nstools.ns)\n"]
  anglican lein ["(ns+ " ++ anglicanModelName model ++ ")\n"]
  case model of
    LR dataset -> do
      let (xs, labels) = unzip dataset
      anglican lein ["(ns-unmap *ns* 'xs)\n"]
      anglican lein ["(def xs " ++ clojureShowVector xs ++ ")\n", "nil\n"]
      anglican lein ["(ns-unmap *ns* 'labels)\n"]
      anglican lein ["(def labels " ++ clojureBoolVector labels ++ ")\n", "nil\n"]
    HMM observations -> do
      anglican lein ["(ns-unmap *ns* 'observations)\n"]
      anglican lein ["(def observations " ++ clojureShowVector observations ++ ")\n", "nil\n"]
    LDA docs -> do
      anglican lein ["(ns-unmap *ns* 'docs)\n"]
      anglican lein ["(def docs " ++ clojureVector (map clojureShowVector docs) ++ ")\n", "nil\n"]
  anglican lein ["(use 'nstools.ns)\n"]
  anglican lein ["(ns+ anglican.core)\n"]

anglican :: LeinProc -> [String] -> IO ()
anglican (LeinProc input output _) cmds = do
  -- execute command
  mapM (hPutStr input) cmds
  hFlush input
  -- wait until all samples are produced
  waitForAnglican output

-- | Wait until an Anglican program finishes
waitForAnglican :: Handle -> IO ()
waitForAnglican handle = run where
  run = do
    l <- hGetLine handle
    -- traceIO l
    -- We recognize that Anglican is finished when the repl emits a line "nil"
    if l == "nil" then
      return ()
    else
      run

-- | Start a Leiningen process in a directory that contains benchmarks.
startLein :: IO LeinProc
startLein = do
  let setup = (shell "lein repl"){cwd = Just anglicanPath, std_in = CreatePipe, std_out = CreatePipe}
  (Just input, Just output, _, process) <- createProcess setup
  -- wait until Leiningen starts producing output to make sure it's ready
  _ <- hWaitForInput output (-1) -- wait for output indefinitely
  let lein = LeinProc input output process
  anglican lein ["(use 'nstools.ns)\n"]
  return lein


-- | Path to the WebPPL project with benchmarks.
webpplPath :: String
webpplPath = "/scratch/ams240/repos/anglican-white-paper/experiments/WebPPL"

-- | Format Haskell list as a Javascript list.
javascriptList :: Show a => [a] -> String
javascriptList xs = unwords (map show xs)

webpplModelName :: Model -> String
webpplModelName (LR _) = "logisticRegression"
webpplModelName (HMM _) = "hmm"
webpplModelName (LDA _) = "lda"

-- | Environment to execute benchmarks in.
data Env = Env {rng :: GenIO, lein :: LeinProc}

data ProbProgSys = MonadBayes | Anglican | WebPPL
  deriving(Show)

data Model = LR [(Double,Bool)] | HMM [Double] | LDA [[String]]
instance Show Model where
  show (LR xs) = "LR" ++ show (length xs)
  show (HMM xs) = "HMM" ++ show (length xs)
  show (LDA xs) = "LDA" ++ show (length $ head xs)
buildModel :: MonadInfer m => Model -> m String
buildModel (LR dataset) = show <$> LogReg.logisticRegression dataset
buildModel (HMM dataset) = show <$> HMM.hmm dataset
buildModel (LDA dataset) = show <$> LDA.lda dataset
modelLength :: Model -> Int
modelLength (LR xs) = length xs
modelLength (HMM xs) = length xs
modelLength (LDA xs) = sum (map length xs)

data Alg = MH Int | SMC Int | RMSMC Int Int
instance Show Alg where
  show (MH n) = "MH" ++ show n
  show (SMC n) = "SMC" ++ show n
  show (RMSMC n t) = "RMSMC" ++ show n ++ "-" ++ show t
runAlg :: Model -> Alg -> SamplerIO String
runAlg model (MH n) = show <$> prior (mh n (buildModel model))
runAlg model (SMC n) = show <$> runPopulation (smcSystematic (modelLength model) n (buildModel model))
runAlg model (RMSMC n t) = show <$> runPopulation (rmsmcLocal (modelLength model) n t (buildModel model))

prepareBenchmarkable :: GenIO -> ProbProgSys -> Model -> Alg -> Benchmarkable
prepareBenchmarkable g MonadBayes model alg = nfIO $ sampleIOwith (runAlg model alg) g
prepareBenchmarkable _ Anglican _ _  = error "Anglican benchmarks not available"
prepareBenchmarkable _ WebPPL _ _ = error "WebPPL benchmarks not available"

prepareBenchmark :: Env -> ProbProgSys -> Model -> Alg -> Benchmark
prepareBenchmark e MonadBayes model alg =
  bench (show MonadBayes ++ sep ++ show model ++ sep ++ show alg) $
  prepareBenchmarkable (rng e) MonadBayes model alg where
    sep = "_"
prepareBenchmark e Anglican model alg = env prepareData (const $ bench name $ whnfIO collect) where
  name = show Anglican ++ sep ++ show model ++ sep ++ show alg
  sep = "_"
  algString (MH n) = "-a lmh -n " ++ show n
  algString (SMC n) = "-a smc -n " ++ show n
  algString (RMSMC _ _) = error "Anglican does not support resample-move SMC"
  prepareData = anglicanData (lein e) model
  collect = do
    anglican (lein e) $ ["(time (m! " ++ anglicanModelName model ++ " " ++ algString alg ++ "))\n"]
prepareBenchmark _ WebPPL model alg = bench name $ whnfIO run where
  name = show WebPPL ++ sep ++ show model ++ sep ++ show alg
  sep = "_"
  algString (MH n) = "--alg MCMC --samples " ++ show n ++ " --rejuv 0 "
  algString (SMC n) = "--alg SMC --samples " ++ show n ++ " --rejuv 0 "
  algString (RMSMC n t) = "--alg SMC --samples " ++ show n ++ " --rejuv " ++ show t ++ " "
  dataString (LR dataset) = let (xs, labels) = unzip dataset in "--xs='" ++ javascriptList xs ++ "' --labels='" ++ javascriptList (map (\b -> if b then 1 else 0) labels) ++ "'"
  dataString (HMM obs) = "--obs='" ++ javascriptList obs ++ "'"
  dataString (LDA docs) = unwords $ map (\(i,doc) -> "--doc" ++ show i ++ "='" ++ unwords doc ++ "'") (zip [1..5] docs)
  run = do
    let command = "node " ++ webpplModelName model ++ ".js " ++ algString alg ++ dataString model
    (_, _, _, process) <- createProcess $ (shell command){cwd = Just webpplPath, std_out = NoStream, std_err = NoStream}
    exitCode <- waitForProcess process
    case exitCode of
      ExitSuccess -> return ()
      ExitFailure i -> error $ "WebPPL terminated with exit code " ++ show i


-- | Checks if the requested benchmark is implemented.
supported :: (ProbProgSys, Model, Alg) -> Bool
supported (Anglican, _, RMSMC _ _) = False
supported _ = True

systems = [
            MonadBayes
            -- Anglican,
            -- WebPPL
          ]

lengthBenchmarks e lrData hmmData ldaData = benchmarks where
  lrLengths = [10] ++ map (*100) [1..10]
  hmmLengths = [10] ++ map (*100) [1..10]
  ldaLengths = [5] ++ map (*50) [1..10]
  models =
    map (LR . (`take` lrData)) lrLengths ++
    map (HMM . (`take` hmmData)) hmmLengths ++
    map (\n -> LDA $ map (take n) ldaData) ldaLengths
  algs = [
    MH 100,
    SMC 100,
    RMSMC 10 1
    ]
  benchmarks = map (uncurry3 (prepareBenchmark e)) $ filter supported xs where
        uncurry3 f (x,y,z) = f x y z
        xs = do
          m <- models
          s <- systems
          a <- algs
          return (s,m,a)

samplesBenchmarks e lrData hmmData ldaData = benchmarks where
  lrLengths = [50]
  hmmLengths = [20]
  ldaLengths = [10]
  models = map (LR . (`take` lrData)) lrLengths ++
           map (HMM . (`take` hmmData)) hmmLengths ++
           map (\n -> LDA $ map (take n) ldaData) ldaLengths
  algs =  map (\x -> MH (100*x)) [1..10] ++ map (\x -> SMC (100*x)) [1..10]
          ++ map (\x -> RMSMC 10 (10*x)) [1..10]
  benchmarks = map (uncurry3 (prepareBenchmark e)) $ filter supported xs where
        uncurry3 f (x,y,z) = f x y z
        xs = do
          a <- algs
          s <- systems
          m <- models
          return (s,m,a)

main :: IO ()
main = do

  g <- createSystemRandom
  l <- startLein
  let e = Env g l

  lrData <- sampleIOwith (LogReg.syntheticData 1000) g
  hmmData <- sampleIOwith (HMM.syntheticData 1000) g
  ldaData <- sampleIOwith (LDA.syntheticData 5 1000) g

  let configLength = defaultConfig{csvFile = Just "speed-length.csv", rawDataFile = Just "raw.dat"}
  defaultMainWith configLength (lengthBenchmarks e lrData hmmData ldaData)

  let configSamples = defaultConfig{csvFile = Just "speed-samples.csv", rawDataFile = Just "raw.dat"}
  defaultMainWith configSamples (samplesBenchmarks e lrData hmmData ldaData)