packages feed

futhark-0.25.32: src/Language/Futhark/Interpreter/AD.hs

module Language.Futhark.Interpreter.AD
  ( Op (..),
    ADVariable (..),
    ADValue (..),
    Tape (..),
    VJPValue (..),
    JVPValue (..),
    Counter (..),
    Depth (..),
    doOp,
    addFor,
    tapePrimal,
    primitive,
    varPrimal,
    deriveTape,
    unionWithM,
    unionsWithM,
  )
where

import Control.Monad (foldM, zipWithM)
import Control.Monad.Trans.Class (lift)
import Control.Monad.Trans.Except (ExceptT, catchE, runExceptT, throwE)
import Control.Monad.Trans.State (State, get, modify, runState)
import Data.Either (isRight)
import Data.Foldable (find, foldlM)
import Data.Functor ((<&>))
import Data.Map qualified as M
import Data.Maybe (fromJust, fromMaybe)
import Data.Text qualified as T
import Futhark.AD.Derivatives (pdBinOp, pdBuiltin, pdUnOp)
import Futhark.Analysis.PrimExp (PrimExp (..))
import Language.Futhark.Core (VName (..), nameFromString, nameFromText)
import Language.Futhark.Primitive
  ( BinOp (Add, FAdd, FMul, LogAnd, LogOr, Mul),
    CmpOp,
    ConvOp,
    Overflow (OverflowWrap),
    PrimType (Bool, FloatType, IntType),
    PrimValue (BoolValue),
    UnOp,
    binOpType,
    blankPrimValue,
    cmpOpType,
    convOpType,
    doBinOp,
    doCmpOp,
    doConvOp,
    doUnOp,
    flipConvOp,
    primFuns,
    primValueType,
    unOpType,
  )

-- | Used to uniquely identify values.
newtype Counter = Counter Int
  deriving (Eq, Ord, Num, Show)

type ADMonad = ExceptT String (State Counter)

incCounter :: ADMonad ()
incCounter = lift $ modify $ \i -> i + 1

-- Mathematical operations subject to AD.
data Op
  = OpBin BinOp
  | OpCmp CmpOp
  | OpUn UnOp
  | OpFn T.Text
  | OpConv ConvOp
  deriving (Show)

-- Checks if an operation matches the types of its operands
opTypeMatch :: Op -> [PrimType] -> Bool
opTypeMatch (OpBin op) p = all (\x -> binOpType op == x) p
opTypeMatch (OpCmp op) p = all (\x -> cmpOpType op == x) p
opTypeMatch (OpUn op) p = all (\x -> unOpType op == x) p
opTypeMatch (OpConv op) p = all (\x -> fst (convOpType op) == x) p
opTypeMatch (OpFn fn) p = case M.lookup fn primFuns of
  Just (t, _, _) -> and $ zipWith (==) t p
  Nothing -> error "opTypeMatch" -- It is assumed that the function exists

-- Gets the return type of an operation
opReturnType :: Op -> PrimType
opReturnType (OpBin op) = binOpType op
opReturnType (OpCmp op) = cmpOpType op
opReturnType (OpUn op) = unOpType op
opReturnType (OpConv op) = snd $ convOpType op
opReturnType (OpFn fn) = case M.lookup fn primFuns of
  Just (_, t, _) -> t
  Nothing -> error "opReturnType" -- It is assumed that the function exists

-- Returns the operation which performs addition (or an
-- equivalent operation) on the given type
addFor :: PrimType -> BinOp
addFor (IntType t) = Add t OverflowWrap
addFor (FloatType t) = FAdd t
addFor Bool = LogOr
addFor t = error $ "addFor: " ++ show t

-- Returns the function which performs multiplication
-- (or an equivalent operation) on the given type
mulFor :: PrimType -> BinOp
mulFor (IntType t) = Mul t OverflowWrap
mulFor (FloatType t) = FMul t
mulFor Bool = LogAnd
mulFor t = error $ "mulFor: " ++ show t

-- | An indication of the nesting depth of AD. This is used to avoid
-- pertubation confusion.
newtype Depth = Depth Int
  deriving (Ord, Eq, Show)

-- Types and utility functions--
-- When taking the partial derivative of a function, we
-- must differentiate between the values which are kept
-- constant, and those which are not
data ADValue
  = Variable Depth ADVariable
  | Constant PrimValue
  deriving (Show)

-- When performing automatic differentiation, each derived
-- variable must be augmented with additional data. This
-- value holds the primitive value of the variable, as well
-- as its data
data ADVariable
  = VJP VJPValue
  | JVP JVPValue
  deriving (Show)

depth :: ADValue -> Depth
depth (Variable d _) = d
depth (Constant _) = Depth 0

primal :: ADValue -> ADValue
primal (Variable _ (VJP (VJPValue t))) = tapePrimal t
primal (Variable _ (JVP (JVPValue v _))) = primal v
primal (Constant v) = Constant v

primalFor :: Depth -> ADValue -> ADValue
primalFor cur v@(Variable tag _) | cur /= tag = v
primalFor _ (Variable _ (VJP (VJPValue t))) = tapePrimal t
primalFor cur (Variable _ (JVP (JVPValue v _))) = primalFor cur v
primalFor _ (Constant v) = Constant v

primitive :: ADValue -> PrimValue
primitive (Variable _ v) = varPrimal v
primitive (Constant v) = v

varPrimal :: ADVariable -> PrimValue
varPrimal (VJP (VJPValue t)) = primitive $ tapePrimal t
varPrimal (JVP (JVPValue v _)) = primitive $ primal v

-- Evaluates a PrimExp using doOp'
evalPrimExp :: M.Map VName ADValue -> PrimExp VName -> ADMonad ADValue
evalPrimExp m (LeafExp n _) =
  maybe (throwE $ "Unknown variable " <> show n) pure $ M.lookup n m
evalPrimExp _ (ValueExp pv) =
  pure $ Constant pv
evalPrimExp m (BinOpExp op x y) = do
  x' <- evalPrimExp m x
  y' <- evalPrimExp m y
  doOp' (OpBin op) [x', y']
evalPrimExp m (CmpOpExp op x y) = do
  x' <- evalPrimExp m x
  y' <- evalPrimExp m y
  doOp' (OpCmp op) [x', y']
evalPrimExp m (UnOpExp op x) = do
  x' <- evalPrimExp m x
  doOp' (OpUn op) [x']
evalPrimExp m (ConvOpExp op x) = do
  x' <- evalPrimExp m x
  doOp' (OpConv op) [x']
evalPrimExp m (FunExp fn p _) = do
  p' <- mapM (evalPrimExp m) p
  doOp' (OpFn fn) p'

-- Returns a list of PrimExps calculating the partial
-- derivative of each operands of a given operation
lookupPDs :: Op -> [PrimExp VName] -> Maybe [PrimExp VName]
lookupPDs (OpBin op) [x, y] = Just $ do
  let (a, b) = pdBinOp op x y
  [a, b]
lookupPDs (OpUn op) [x] = Just [pdUnOp op x]
lookupPDs (OpFn fn) p = pdBuiltin (nameFromText fn) p
lookupPDs _ _ = Nothing

-- Shared AD logic--
-- This function performs a mathematical operation on a
-- list of operands, performing automatic differentiation
-- if one or more operands is a Variable (of depth > 0)
doOp :: Op -> [ADValue] -> Counter -> Either String (ADValue, Counter)
doOp op o uid = case runState (runExceptT $ doOp' op o) uid of
  (Left s, _) -> Left s
  (Right v, uid') -> Right (v, uid')

doOp' :: Op -> [ADValue] -> ADMonad ADValue
doOp' op o
  | not $ opTypeMatch op (map primValueType pv) =
      -- This function may be called with arguments of invalid types,
      -- because it is used as part of an overloaded operator.
      throwE $ unwords ["invalid types for op", show op, "and operands", show o]
  | otherwise = do
      let dep = case op of
            OpCmp _ -> Depth 0 -- AD is not well-defined for comparason operations
            -- There are no derivatives for those written in
            -- PrimExp (check lookupPDs)
            _ -> maximum (map depth o)
      if dep == Depth 0
        then maybe (throwE "failed to evaluate const") pure constCase <* incCounter
        else nonconstCase dep
  where
    pv = map primitive o

    divideDepths :: Depth -> ADValue -> Either ADValue ADVariable
    divideDepths _ v@(Constant {}) = Left v
    divideDepths d v@(Variable d' v') = if d' < d then Left v else Right v'

    -- TODO: There may be a more graceful way of
    -- doing this
    extractVJP :: Either ADValue ADVariable -> Either ADValue VJPValue
    extractVJP (Right (VJP v)) = Right v
    extractVJP (Left v) = Left v
    extractVJP _ =
      -- This will never be called when the maximum depth layer is JVP
      error "extractVJP"

    -- TODO: There may be a more graceful way of
    -- doing this
    extractJVP :: Either ADValue ADVariable -> Either ADValue JVPValue
    extractJVP (Right (JVP v)) = Right v
    extractJVP (Left v) = Left v
    extractJVP _ =
      -- This will never be called when the maximum depth layer is VJP
      error "extractJVP"

    -- In this case, every operand is a constant, and the
    -- mathematical operation can be applied as it would be
    -- otherwise
    constCase =
      Constant <$> case (op, pv) of
        (OpBin op', [x, y]) -> doBinOp op' x y
        (OpCmp op', [x, y]) -> BoolValue <$> doCmpOp op' x y
        (OpUn op', [x]) -> doUnOp op' x
        (OpConv op', [x]) -> doConvOp op' x
        (OpFn fn, _) -> do
          (_, _, f) <- M.lookup fn primFuns
          f pv
        _ -> error "doOp': opTypeMatch"

    nonconstCase dep = do
      -- In this case, some values are variables. We therefore
      -- have to perform the necessary steps for AD

      -- First, we calculate the value for the previous depth
      let oprev = map (primalFor dep) o
      vprev <- doOp' op oprev

      -- Then we separate the values of the maximum depth from
      -- those of a lower depth
      let o' = map (divideDepths dep) o
      -- Then we find out what type of AD is being performed
      case find isRight o' of
        -- Finally, we perform the necessary steps for the given
        -- type of AD
        Just (Right (VJP {})) ->
          Variable dep . VJP . VJPValue
            <$> vjpHandleOp op (map extractVJP o') vprev
        Just (Right (JVP {})) ->
          Variable dep . JVP . JVPValue vprev
            <$> jvpHandleOp op (map extractJVP o')
        _ ->
          -- Since the maximum depth is non-zero, there must be at
          -- least one variable of depth > 0
          error "find isRight"

calculatePDs :: Op -> [ADValue] -> ADMonad [ADValue]
calculatePDs op args =
  -- Create a unique VName for each operand
  let n = map (\i -> VName (nameFromString $ "x" ++ show i) i) [1 .. length args]
      -- Put the operands in the environment
      m = M.fromList $ zip n args

      -- Look up, and calculate the partial derivative
      -- of the operation with respect to each operand
      pde =
        fromMaybe (error "lookupPDs failed") $
          lookupPDs op $
            zipWith (\v val -> LeafExp v $ primValueType $ primitive val) n args
      res = mapM (\x -> catchE (evalPrimExp m x) $ error . ("evalPrimExp failed: " <>)) pde
   in res

-- VJP / Reverse mode automatic differentiation--
-- In reverse mode AD, the entire computation
-- leading up to a variable must be saved
-- This is represented as a Tape
newtype VJPValue = VJPValue Tape
  deriving (Show)

-- | Represents a computation tree, as well as every intermediate
-- value in its evaluation.
data Tape
  = -- | This represents a variable. Each variable is given a unique ID,
    -- and has an initial value
    TapeID Counter ADValue
  | -- | This represents a constant.
    TapeConst ADValue
  | -- | This represents the application of a mathematical operation.
    -- Each parameter is given by its Tape, and the return value of
    -- the operation is saved
    TapeOp Op [Tape] Counter ADValue
  deriving (Show)

-- | Returns the primal value of a Tape.
tapePrimal :: Tape -> ADValue
tapePrimal (TapeID _ v) = v
tapePrimal (TapeConst v) = v
tapePrimal (TapeOp _ _ _ v) = v

-- This updates Tape of a VJPValue with a new operation,
-- treating all operands of a lower depth as constants
vjpHandleOp :: Op -> [Either ADValue VJPValue] -> ADValue -> ADMonad Tape
vjpHandleOp op p v = do
  i <- lift get
  pure $ TapeOp op (map toTape p) i v
  where
    toTape (Left v') = TapeConst v'
    toTape (Right (VJPValue t)) = t

unionWithM :: (Monad m, Ord k) => (a -> a -> m a) -> M.Map k a -> M.Map k a -> m (M.Map k a)
unionWithM f m1 m2 = do
  let m = M.union (M.difference m1 m2) (M.difference m2 m1)
  let k = M.keys $ M.intersection m1 m2
  v <- mapM (\k' -> f (fromJust $ M.lookup k' m1) (fromJust $ M.lookup k' m2)) k
  pure $ foldl (\m' (k', v') -> M.insert k' v' m') m (zip k v)

unionsWithM :: (Foldable f, Monad m, Ord k) => (a -> a -> m a) -> f (M.Map k a) -> m (M.Map k a)
unionsWithM f = foldM (unionWithM f) M.empty

-- | This calculates every partial derivative of a 'Tape'. The result
-- is a map of the partial derivatives, each key corresponding to the
-- ID of a free variable (see TapeID).
deriveTape :: Tape -> ADValue -> Counter -> Either String (M.Map Counter ADValue, Counter)
deriveTape tp s uid = case runState (runExceptT $ deriveTape' tp s) uid of
  (Left e, _) -> Left e
  (Right v, uid') -> Right (v, uid')

deriveTape' :: Tape -> ADValue -> ADMonad (M.Map Counter ADValue)
deriveTape' (TapeID i _) s = pure $ M.singleton i s
deriveTape' (TapeConst _) _ = pure M.empty
deriveTape' tp@(TapeOp op p uid _) s =
  fst <$> derive tp s M.empty (countReferences p $ M.singleton (-uid - 1) 1)
  where
    add x y = doOp' (OpBin $ addFor $ opReturnType op) [x, y]
    mul x y = doOp' (OpBin $ mulFor $ opReturnType op) [x, y]
    madd :: Counter -> ADValue -> M.Map Counter ADValue -> ADMonad (M.Map Counter ADValue)
    madd i a m = case M.lookup i m of
      Just b -> add a b <&> (\x -> M.insert i x m)
      Nothing -> pure $ M.insert i a m
    derive ::
      Tape ->
      ADValue ->
      M.Map Counter ADValue ->
      M.Map Counter Int ->
      ADMonad (M.Map Counter ADValue, M.Map Counter Int)
    derive (TapeID i _) s' ss rs = madd i s' ss <&> (,rs)
    derive (TapeConst _) _ ss rs = pure (ss, rs)
    derive (TapeOp op' p' uid' _) s' ss rs = do
      -- Decrease the reference counter
      let r = fromJust (M.lookup (-uid' - 1) rs) - 1
          rs' = M.insert (-uid' - 1) r rs
      -- Add the sensitivity
      ss' <- madd (-uid' - 1) s' ss
      -- If there are still more references left, do nothing
      if r > 0
        then pure (ss', rs')
        else -- Otherwise, derive the tape
          if r == 0
            then do
              let s'' = fromJust (M.lookup (-uid' - 1) ss')

              -- Calculate the new sensitivities
              s''' <- case op' of
                OpConv op'' ->
                  -- In case of type conversion, simply convert the sensitivity
                  sequence [doOp' (OpConv $ flipConvOp op'') [s'']]
                _ -> calculatePDs op' (map tapePrimal p') >>= mapM (mul s'')

              -- Propagate the new sensitivities
              foldlM (\(ss'', rs'') (p'', s'''') -> derive p'' s'''' ss'' rs'') (ss', rs') $ zip p' s'''
            else error "TODO: This branch is unreachable unless `countReferences` undercounts"
    countReferences :: [Tape] -> M.Map Counter Int -> M.Map Counter Int
    countReferences p' d' = foldl f d' p'
    f d'' x =
      case x of
        (TapeOp _ p'' uid'' _) -> case M.lookup (-uid'' - 1) d'' of
          Just v -> M.insert (-uid'' - 1) (v + 1) d''
          Nothing -> countReferences p'' $ M.insert (-uid'' - 1) 1 d''
        _ -> d''

-- JVP / Forward mode automatic differentiation--

-- | In JVP, the derivative of the variable must be saved. This is
-- represented as a second value.
data JVPValue = JVPValue ADValue ADValue
  deriving (Show)

-- | This calculates the tangent part of the JVPValue resulting
-- from the application of a mathematical operation on one or more
-- JVPValues.
jvpHandleOp :: Op -> [Either ADValue JVPValue] -> ADMonad ADValue
jvpHandleOp op p = do
  case op of
    OpConv _ ->
      -- In case of type conversion, simply convert
      -- the old tangent
      doOp' op [tangent $ head p]
    _ -> do
      -- Calculate the new tangent using the chain rule
      pds <- calculatePDs op $ map primal' p
      vs <- zipWithM mul pds $ map tangent p
      foldM add (Constant $ blankPrimValue op_t) vs
  where
    op_t = opReturnType op
    primal' (Left v) = v
    primal' (Right (JVPValue v _)) = v
    tangent (Left _) = Constant $ blankPrimValue $ opReturnType op
    tangent (Right (JVPValue _ d)) = d
    add x y = doOp' (OpBin $ addFor $ opReturnType op) [x, y]
    mul x y = doOp' (OpBin $ mulFor $ opReturnType op) [x, y]