hanalyze-bayes-0.2.0.1: src/Hanalyze/Model/HBM/VecAD.hs
{-# LANGUAGE BangPatterns #-}
-- |
-- Module : Hanalyze.Model.HBM.VecAD
-- Description : 自作の最小 reverse-mode AD (vector-op tape)
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: 自作・最小 reverse-mode AD (vector-op tape)。 「採用 = 案B
-- (自前 vector-op tape)」 と判断したエンジンを本実装用に移植したもの
-- (`bench/haskell/BenchHBMVecADSpike.hs` の gradHandroll 系)。
--
-- 設計: forward で「ベクトル演算ごとにノードを発番」 し、 各ノードの随伴更新
-- クロージャを逆順リストに積む (= 自前 Wengert tape)。 backward で出力に 1 を
-- seed し、 逆位相順 (= 発番の逆順 = prepend したリストの先頭) にクロージャを
-- replay して入力 (leaf) の随伴を得る。 tape は「ベクトル演算 1 個 = 1 ノード」
-- ゆえ @ad@ のスカラ tape (per-scalar-op で O(n) ノード) より桁で小さい。
--
-- スカラは長さ 1 の Storable Vector として随伴を持ち、 ノード随伴は単一の
-- mutable 配列に統一格納する。
--
-- 注意: 値依存制御フロー (分布の台チェック等) は tape に乗らない。 本エンジンは
-- 構造が値に依らず静的な部分 (Gaussian-恒等リンクの線形予測子 + 二乗和) 専用。
-- 非対応の構造は呼出側で scalar (@ad@) 経路に fallback する。
-- [English]: A hand-rolled minimal reverse-mode AD (vector-op tape). This is
-- the engine chosen ("adopt = plan B, a custom vector-op tape") and ported
-- into the production implementation from the spike
-- (`bench/haskell/BenchHBMVecADSpike.hs`'s gradHandroll family).
--
-- Design: on the forward pass, each vector operation is numbered as a node,
-- and each node's adjoint-update closure is pushed onto a reverse-order list
-- (= a custom Wengert tape). On the backward pass, the output is seeded with
-- 1, and closures are replayed in reverse topological order (= reverse
-- numbering order = the head of the prepended list) to obtain the adjoints
-- of the input leaves. Because the tape records "one vector operation = one
-- node," it is orders of magnitude smaller than @ad@'s scalar tape (which
-- creates O(n) nodes per scalar op).
--
-- Scalars carry their adjoint as a length-1 Storable Vector, and all node
-- adjoints are stored uniformly in a single mutable array.
--
-- Caveat: value-dependent control flow (e.g. distribution support checks)
-- does not survive on the tape. This engine only covers the part of the
-- structure that is static regardless of value (Gaussian-identity-link
-- linear predictor + sum of squares). Unsupported structures fall back to
-- the scalar (@ad@) path at the call site.
module Hanalyze.Model.HBM.VecAD
( -- * 値ハンドルと文脈
Rval (..)
, Ctx
, ridOf
-- * tape の実行
, runTape
-- * leaf
, inputVec
, inputScal
, constVec
-- * ベクトル演算 (随伴付き)
, idxHR
, sliceHR
, scaleHR
, vaddHR
, vsubHR
, dotHR
, gatherHR
, vexpHR
, bcastAddHR
, hadamardHR
, vmap1HR
-- * スカラ演算 (随伴付き)
, map1S
, cstS
, addS
, subS
, mulS
, divByS
, expS
, logS
, mulConstS
, addConstS
, foldVadd
) where
import Control.Monad (when)
import Control.Monad.ST
import Data.Array.ST (STArray, newArray, readArray, writeArray)
import Data.STRef
import qualified Data.Vector.Storable as VS
import qualified Data.Vector.Unboxed as VU
-- ===========================================================================
-- 値ハンドルと tape 文脈
-- ===========================================================================
-- | [日本語]: reverse-mode の値ハンドル: ノード id + primal (scalar / vector)。
-- [English]: A reverse-mode value handle: node id + primal (scalar / vector).
data Rval = RScal !Int !Double | RVec !Int !(VS.Vector Double)
ridOf :: Rval -> Int
ridOf (RScal i _) = i
ridOf (RVec i _) = i
type Adj s = STArray s Int (VS.Vector Double)
-- | [日本語]: 発番カウンタ + backward クロージャ列 (prepend = 発番の逆順)。
-- [English]: A numbering counter plus the list of backward closures
-- (prepend order = reverse numbering order).
data Ctx s = Ctx !(STRef s Int) !(STRef s [Adj s -> ST s ()])
fresh :: Ctx s -> ST s Int
fresh (Ctx cnt _) = do
n <- readSTRef cnt
writeSTRef cnt (n + 1)
pure n
record :: Ctx s -> (Adj s -> ST s ()) -> ST s ()
record (Ctx _ bw) f = modifySTRef' bw (f :)
-- | [日本語]: 随伴の加算 (空 = ゼロ扱い)。
-- [English]: Accumulate into an adjoint (an empty vector is treated as zero).
bumpA :: Adj s -> Int -> VS.Vector Double -> ST s ()
bumpA adj i contrib = do
cur <- readArray adj i
writeArray adj i (if VS.null cur then contrib else VS.zipWith (+) cur contrib)
readAdjS :: Adj s -> Int -> ST s Double
readAdjS adj i = do
v <- readArray adj i
pure (if VS.null v then 0 else v VS.! 0)
-- ===========================================================================
-- tape の実行 (forward build → seed → backward replay)
-- ===========================================================================
-- | [日本語]: tape を構築するアクション (出力ノード + 勾配を読みたい leaf 群を
-- 返す) を受け取り、 forward 評価 → 出力に 1 を seed → backward replay の
-- 上で、 各 leaf の随伴 (= 出力の各 leaf に対する勾配ベクトル) を返す。
--
-- @build@ は @(出力 Rval, [leaf Rval])@ を返す。 結果は leaf ごとの随伴
-- ベクトル (RScal leaf は長さ1、 RVec leaf は元の長さ)。
-- [English]: Takes an action that builds the tape (returning the output
-- node plus the leaves whose gradient we want to read), then performs a
-- forward evaluation, seeds the output with 1, replays backward, and
-- returns the adjoint of each leaf (= the gradient vector with respect to
-- that leaf).
--
-- @build@ returns @(output Rval, [leaf Rval])@. The result is one adjoint
-- vector per leaf (length 1 for an RScal leaf, original length for an
-- RVec leaf).
runTape :: (forall s. Ctx s -> ST s (Rval, [Rval])) -> [VS.Vector Double]
runTape build = runST $ do
cnt <- newSTRef 0
bw <- newSTRef []
let ctx = Ctx cnt bw
(out, leaves) <- build ctx
total <- readSTRef cnt
adj <- newArray (0, max 0 (total - 1)) VS.empty
writeArray adj (ridOf out) (VS.singleton 1)
closures <- readSTRef bw
mapM_ ($ adj) closures
mapM (\lf -> readArray adj (ridOf lf)) leaves
-- ===========================================================================
-- leaf
-- ===========================================================================
-- | [日本語]: ベクトル leaf (勾配を読む入力)。
-- [English]: A vector leaf (an input whose gradient we read).
inputVec :: Ctx s -> VS.Vector Double -> ST s Rval
inputVec ctx v = do
i <- fresh ctx
pure (RVec i v)
-- | [日本語]: スカラ leaf (勾配を読む入力)。
-- [English]: A scalar leaf (an input whose gradient we read).
inputScal :: Ctx s -> Double -> ST s Rval
inputScal ctx x = do
i <- fresh ctx
pure (RScal i x)
-- | [日本語]: 定数ベクトルノード (backward 無し)。
-- [English]: A constant vector node (no backward closure).
constVec :: Ctx s -> VS.Vector Double -> ST s Rval
constVec ctx v = do { i <- fresh ctx; pure (RVec i v) }
-- ===========================================================================
-- ベクトル演算 (随伴付き)
-- ===========================================================================
-- | [日本語]: 全長 @l@ の vec から要素 @i@ を取り出す (scalar 化)。 随伴 = e_i·dy。
-- [English]: Extracts element @i@ from a length-@l@ vec (turning it into a
-- scalar). Adjoint = e_i·dy.
idxHR :: Ctx s -> Int -> Int -> Rval -> ST s Rval
idxHR ctx l i (RVec vid v) = do
o <- fresh ctx
record ctx $ \adj -> do
g <- readAdjS adj o
when (g /= 0) $ bumpA adj vid (VS.generate l (\j -> if j == i then g else 0))
pure (RScal o (v VS.! i))
idxHR _ _ _ _ = error "idxHR: scalar input"
-- | [日本語]: 全長 @l@ の vec から @[off, off+len)@ を切り出す。 随伴は
-- zeros l に散布。
-- [English]: Slices out @[off, off+len)@ from a length-@l@ vec. The
-- adjoint is scattered into zeros of length l.
sliceHR :: Ctx s -> Int -> Int -> Int -> Rval -> ST s Rval
sliceHR ctx l off len (RVec vid v) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $
bumpA adj vid (VS.generate l (\j -> if j >= off && j < off + len then dy VS.! (j - off) else 0))
pure (RVec o (VS.slice off len v))
sliceHR _ _ _ _ _ = error "sliceHR: scalar input"
-- | [日本語]: scalar * vector。 ∂scalar = dy·v、 ∂v = scalar·dy。
-- [English]: scalar * vector. ∂scalar = dy·v, ∂v = scalar·dy.
scaleHR :: Ctx s -> Rval -> Rval -> ST s Rval
scaleHR ctx (RScal kid k) (RVec vid v) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $ do
bumpA adj kid (VS.singleton (VS.sum (VS.zipWith (*) dy v)))
bumpA adj vid (VS.map (* k) dy)
pure (RVec o (VS.map (* k) v))
scaleHR _ _ _ = error "scaleHR: shape"
-- | [日本語]: vector + vector。
-- [English]: vector + vector.
vaddHR :: Ctx s -> Rval -> Rval -> ST s Rval
vaddHR ctx (RVec aid a) (RVec bid b) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $ do
bumpA adj aid dy
bumpA adj bid dy
pure (RVec o (VS.zipWith (+) a b))
vaddHR _ _ _ = error "vaddHR: shape"
-- | [日本語]: vector - vector。
-- [English]: vector - vector.
vsubHR :: Ctx s -> Rval -> Rval -> ST s Rval
vsubHR ctx (RVec aid a) (RVec bid b) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $ do
bumpA adj aid dy
bumpA adj bid (VS.map negate dy)
pure (RVec o (VS.zipWith (-) a b))
vsubHR _ _ _ = error "vsubHR: shape"
-- | [日本語]: 内積。 ∂a = dy·b、 ∂b = dy·a。
-- [English]: Dot product. ∂a = dy·b, ∂b = dy·a.
dotHR :: Ctx s -> Rval -> Rval -> ST s Rval
dotHR ctx (RVec aid a) (RVec bid b) = do
o <- fresh ctx
record ctx $ \adj -> do
g <- readAdjS adj o
when (g /= 0) $ do
bumpA adj aid (VS.map (* g) b)
bumpA adj bid (VS.map (* g) a)
pure (RScal o (VS.sum (VS.zipWith (*) a b)))
dotHR _ _ _ = error "dotHR: shape"
-- | [日本語]: @u[gids]@ gather (gids/nG は定数)。 随伴は scatter-add で O(n)。
-- [English]: A @u[gids]@ gather (gids/nG are constants). The adjoint is
-- an O(n) scatter-add.
gatherHR :: Ctx s -> VU.Vector Int -> Int -> Rval -> ST s Rval
gatherHR ctx gids nG (RVec uid u) = do
let n = VU.length gids
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $
bumpA adj uid (VS.convert $
VU.accumulate (+) (VU.replicate nG 0) (VU.zip gids (VU.convert dy :: VU.Vector Double)))
pure (RVec o (VS.generate n (\i -> u VS.! (gids VU.! i))))
gatherHR _ _ _ _ = error "gatherHR: shape"
-- | [日本語]: elementwise exp (非線形 μ 用)。 ∂v = dy ⊙ exp(v)。
-- [English]: Elementwise exp (for a non-linear μ). ∂v = dy ⊙ exp(v).
vexpHR :: Ctx s -> Rval -> ST s Rval
vexpHR ctx (RVec vid v) = do
let ev = VS.map exp v
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $
bumpA adj vid (VS.zipWith (*) dy ev)
pure (RVec o ev)
vexpHR _ _ = error "vexpHR: scalar input"
-- | [日本語]: scalar + vector の broadcast 加算。 ∂scalar = Σ dy。
-- [English]: A broadcast addition of scalar + vector. ∂scalar = Σ dy.
bcastAddHR :: Ctx s -> Rval -> Rval -> ST s Rval
bcastAddHR ctx (RScal kid k) (RVec vid v) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $ do
bumpA adj kid (VS.singleton (VS.sum dy))
bumpA adj vid dy
pure (RVec o (VS.map (+ k) v))
bcastAddHR _ _ _ = error "bcastAddHR: shape"
-- | [日本語]: elementwise 積 v ⊙ w (gather(a)[i]·exp(-b·x_i) 用)。
-- ∂v = dy ⊙ w、 ∂w = dy ⊙ v。
-- [English]: An elementwise product v ⊙ w (for gather(a)[i]·exp(-b·x_i)).
-- ∂v = dy ⊙ w, ∂w = dy ⊙ v.
hadamardHR :: Ctx s -> Rval -> Rval -> ST s Rval
hadamardHR ctx (RVec aid a) (RVec bid b) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $ do
bumpA adj aid (VS.zipWith (*) dy b)
bumpA adj bid (VS.zipWith (*) dy a)
pure (RVec o (VS.zipWith (*) a b))
hadamardHR _ _ _ = error "hadamardHR: shape"
-- | [日本語]: 汎用 elementwise 単項 (ベクトル式 IR の log/recip/sqrt/tanh 等)。
-- @f@ とその導関数 @f'@ を受け、 ∂v = dy ⊙ f'(v) (v は入力 primal)。
-- [English]: A generic elementwise unary op (for the vector-expression IR's
-- log/recip/sqrt/tanh, etc.). Takes @f@ and its derivative @f'@;
-- ∂v = dy ⊙ f'(v) (v is the input primal).
vmap1HR :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
vmap1HR ctx f df (RVec vid v) = do
o <- fresh ctx
record ctx $ \adj -> do
dy <- readArray adj o
when (not (VS.null dy)) $
bumpA adj vid (VS.zipWith (\g x -> g * df x) dy v)
pure (RVec o (VS.map f v))
vmap1HR _ _ _ _ = error "vmap1HR: scalar input"
-- | [日本語]: 非空ベクトルノード列を vadd で畳む。
-- [English]: Folds a non-empty list of vector nodes with vadd.
foldVadd :: Ctx s -> [Rval] -> ST s Rval
foldVadd _ [] = error "foldVadd: empty"
foldVadd _ [x] = pure x
foldVadd ctx (x:y:xs) = vaddHR ctx x y >>= \z -> foldVadd ctx (z : xs)
-- ===========================================================================
-- スカラ演算 (随伴付き)
-- ===========================================================================
cstS :: Ctx s -> Double -> ST s Rval
cstS ctx x = do { i <- fresh ctx; pure (RScal i x) }
binS :: Ctx s -> (Double -> Double -> Double) -> (Double -> Double -> (Double, Double))
-> Rval -> Rval -> ST s Rval
binS ctx f df (RScal aid a) (RScal bid b) = do
o <- fresh ctx
record ctx $ \adj -> do
g <- readAdjS adj o
when (g /= 0) $ do
let (da, db) = df a b
bumpA adj aid (VS.singleton (g * da))
bumpA adj bid (VS.singleton (g * db))
pure (RScal o (f a b))
binS _ _ _ _ _ = error "binS: scalar expected"
addS, subS, mulS :: Ctx s -> Rval -> Rval -> ST s Rval
addS ctx = binS ctx (+) (\_ _ -> (1, 1))
subS ctx = binS ctx (-) (\_ _ -> (1, -1))
mulS ctx = binS ctx (*) (\a b -> (b, a))
-- | [日本語]: scalar 除算 (a/b)。
-- [English]: Scalar division (a/b).
divByS :: Ctx s -> Rval -> Rval -> ST s Rval
divByS ctx = binS ctx (/) (\a b -> (1 / b, negate a / (b * b)))
unS :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
unS ctx f df (RScal aid a) = do
o <- fresh ctx
record ctx $ \adj -> do
g <- readAdjS adj o
when (g /= 0) $ bumpA adj aid (VS.singleton (g * df a))
pure (RScal o (f a))
unS _ _ _ _ = error "unS: scalar expected"
expS, logS :: Ctx s -> Rval -> ST s Rval
expS ctx = unS ctx exp exp
logS ctx = unS ctx log (\a -> 1 / a)
-- | [日本語]: 汎用スカラ単項。 @f@ と導関数 @f'@ を受ける ('unS' の公開形)。
-- [English]: A generic scalar unary op. Takes @f@ and its derivative @f'@
-- (the public form of 'unS').
map1S :: Ctx s -> (Double -> Double) -> (Double -> Double) -> Rval -> ST s Rval
map1S = unS
mulConstS, addConstS :: Ctx s -> Double -> Rval -> ST s Rval
mulConstS ctx c = unS ctx (* c) (const c)
addConstS ctx c = unS ctx (+ c) (const 1)