packages feed

hanalyze-0.2.0.0: bench/haskell/BenchM2Iso.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# OPTIONS_GHC -fno-full-laziness -fno-cse #-}
-- | Phase 85.5: M2 (random intercept) 単独 NUTS の A/B 計測ドライバ。
--
-- Phase 85.3 の回帰ガードで M2 の wall が Phase 84 基準比 ~1.5× 悪化して
-- 3 run 再現した (M3-M8 は非回帰・per-eval A/B は +10-18% のみ・iter 増で
-- 悪化率上昇 = GC/heap の疑い)。 本ドライバは BenchHBMScaling の M2 を
-- データ・init・config 込みで単独再現し、 `-rtsopts` 付きでビルドして
-- `+RTS -s` の alloc / GC / MUT 時間を旧 lib (worktree) と直接比較する。
--
--   cabal run bench-m2-iso -f benches -- 1600 +RTS -s
module Main where

import           Control.Monad                    (forM_)
import qualified Data.Map.Strict                  as Map
import qualified Data.Text                        as T
import qualified Data.Vector                      as V
import qualified System.Random.MWC                as MWC
import           System.Random.MWC.Distributions  (standard)
import           System.Environment               (getArgs)
import           Text.Printf                      (printf)

import           Hanalyze.Model.HBM               (ModelP, glmmRandomIntercept,
                                                   GlmmFamily (..))
import           Hanalyze.MCMC.NUTS               (NUTSConfig (..),
                                                   defaultNUTSConfig, nuts)
import           Hanalyze.MCMC.Core               (Chain, posteriorMean)

import           BenchUtil                        (timeitIO)

-- BenchHBMScaling と同一の M2 DGP (8 群 × 12 = 96 obs・決定的)
nGroups, perGroup :: Int
nGroups  = 8
perGroup = 12

normals :: Int -> Int -> IO [Double]
normals seed k = do
  g <- MWC.initialize (V.singleton (fromIntegral seed))
  mapM (const (standard g)) [1 .. k]

genM2Data :: IO ([[Double]], [Int], [Double])
genM2Data = do
  let (b0, b1, tauU, s) = (1.0, 0.8, 1.5, 1.0)
      n = nGroups * perGroup
  xz <- normals 21 n
  ez <- normals 22 n
  uz <- normals 23 nGroups
  let us   = map (* tauU) uz
      gids = [ i `div` perGroup | i <- [0 .. n - 1] ]
      xs   = map (* 2.0) xz
      ys   = [ b0 + b1 * x + (us !! g) + s * e
             | (x, g, e) <- zip3 xs gids ez ]
      xRows = [ [1.0, x] | x <- xs ]
  return (xRows, gids, ys)

mkConfig :: Int -> NUTSConfig
mkConfig iters = defaultNUTSConfig
  { nutsIterations    = iters
  , nutsBurnIn        = 500
  , nutsStepSize      = 0.1
  , nutsMaxDepth      = 10
  , nutsAdaptStepSize = True
  , nutsTargetAccept  = 0.8
  , nutsAdaptMass     = True
  }

main :: IO ()
main = do
  args <- getArgs
  let iters = case args of
        (a : _) -> read a
        _       -> 1600
  (xr, gids, ys) <- genM2Data
  let m2 :: ModelP ()
      m2 = glmmRandomIntercept GlmmGaussian xr gids ys
      names = ["beta_0", "beta_1", "tau_u", "sigma"]
              ++ [ T.pack ("u_" ++ show j) | j <- [0 .. nGroups - 1] ]
      initP = Map.fromList $
        [ ("beta_0", 1.0), ("beta_1", 0.8), ("tau_u", 1.5), ("sigma", 1.0) ]
        ++ [ (T.pack ("u_" ++ show j), 0.0) | j <- [0 .. nGroups - 1] ]
      probe ch = sum [ maybe 0 id (posteriorMean p ch) | p <- names ]
      run :: Int -> IO Chain
      run i = do
        g <- MWC.initialize (V.singleton (fromIntegral (42 + i)))
        nuts m2 (mkConfig iters) initP g
  (ms, ch) <- timeitIO 5 probe run
  printf "M2_iso iter=%d warmup=500 reps=5 median=%.1f ms  beta_1=%.4f\n"
    iters ms (maybe 0 id (posteriorMean "beta_1" ch))