packages feed

futhark-0.26.4: src/Futhark/AD/Rev/Acc.hs

-- | Differentiation related to accumulators in the input program.
module Futhark.AD.Rev.Acc
  ( diffWithAcc,
    diffUpdateAcc,
  )
where

-- Note [Adjoints of accumulators]
--
-- The general case of taking adjoints of WithAcc is tricky.  We make
-- some assumptions and lay down a basic design.
--
-- First, we assume that any WithAccs that occur in the program are
-- come from one of these sources:
--
-- - A previous instance of VJP, which means we can rely on the operator having
--   a constant adjoint (it's addition as appropriate to the type).
--
-- - A scatter, meaning there is no operator.
--
-- (These can actually be distinguished by the presence of an operator, although
-- we do not currently bother.)
--
-- Second, the adjoint of an accumulator is an array of the same type
-- as the underlying array.  For example, the adjoint type of the
-- primal type 'acc(c, [n], {f64})' is '[n]f64'.  In principle the
-- adjoint of 'acc(c, [n], {f64,f32})' should be two arrays of type
-- '[]f64', '[]f32'.  Our current design assumes that adjoints are
-- single variables.  This is fixable.
--
-- In the return sweep, when inserting the with_acc, we still compute the
-- "original" accumulator result, but modified such that its initial value is
-- the adjoint of the result of the accumulator. We also modify the update_accs
-- of these accumulators to be with zero values. This means that the array that
-- is produced will be equal to the adjoint of the result, except for those
-- places that have been updated, where it will be zero. This is intuitively
-- sensible - values that have been overwritten (and so do not contribute to the
-- result) should obviously have zero sensitivity.
--
-- # Adjoint of UpdateAcc
--
-- Consider primal code
--
--     update_acc(acc, i, v)
--
-- Interpreted as an imperative statement, this means
--
--     acc[i] ⊕= v
--
-- for some '⊕'.  Normally all the compiler knows of '⊕' is that it
-- is associative and commutative, but because we assume that all
-- accumulators are the result of previous AD transformations, we
-- can assume that '⊕' actually behaves like addition - that is, has
-- unit partial derivatives.  So the return sweep is
--
--     v_adj += acc_adj[i]
--
-- Further, we modify the primal code so that it becomes
--
--     update_acc(acc, i, 0)
--
-- for some appropriate notion of zero.
--
-- # Adjoint of Map
--
-- Suppose we have primal code
--
--   let acc' =
--     map (...) acc
--
-- where "acc : acc(c, [n], {f64})" and the width of the Map is "w".
-- Our normal transformation for Map input arrays is to similarly map
-- their adjoint, but clearly this doesn't work here because the
-- semantics of mapping an adjoint is an "implicit replicate".  So
-- when generating the return sweep we actually perform that
-- replication:
--
--   map (...) (replicate w acc_adj)
--
-- But what about the contributions to "acc'"?  Those we also have to
-- take special care of.  The result of the map itself is actually a
-- multidimensional array:
--
--   let acc_contribs =
--     map (...) (replicate w acc'_adj)
--
-- which we must then sum to add to the contribution.
--
--   acc_adj += sum(acc_contribs)
--
-- I'm slightly worried about the asymptotics of this, since my
-- intuition of this is that the contributions might be rather sparse.
-- (Maybe completely zero?  If so it will be simplified away
-- entirely.)  Perhaps a better solution is to treat
-- accumulator-inputs in the primal code as we do free variables, and
-- create accumulators for them in the return sweep.
--
-- # Vectorised WithAcc
--
-- When WithAcc occurs in vectorised AD, the accumulator element types gain
-- extra leading "vectorised" dimensions corresponding to the enclosing vector
-- shape. For example, if the primal type inside a map of width @w@ is @acc(c,
-- [n], {f64})@, the adjoint type is @[w][n]f64@ -- but the internal accumulator
-- layout expects shape @[n][w]f64@ (the accumulator shape comes first, then the
-- vectorised dimensions, then element dimensions).
--
-- This means we must transpose accumulator adjoints when entering and
-- leaving the return-sweep WithAcc:
--
--  * On entry: transpose result adjoints from @[vec...][shape...]elem@ to
--    @[shape...][vec...]elem@ so they can serve as initial values for the
--    accumulators.
--
--  * On exit: transpose the produced arrays back from @[shape...][vec...]elem@
--    to @[vec...][shape...]elem@ to match the expected adjoint layout.
--
-- This is actually quite similar to how other SOACs must be handled.
--
-- Additionally, the accumulator parameter types in the lambda (and any
-- Acc-typed pattern elements or inner lambda parameters referring to the same
-- certs) must be updated to reflect the vectorised element types *before*
-- differentiation. This ensures that 'lookupAdj' on accumulator variables
-- inside the lambda produces adjoints with the correct vectorised type.
--
-- The UpdateAcc case is simpler under vectorisation: because the accumulator
-- adjoint already has the vectorised dimensions folded into its element type, a
-- plain index into the adjoint at the update indices directly yields the
-- correctly-shaped contribution.
--
-- # Consumption
--
-- A minor problem is that our usual way of handling consumption (Note
-- [Consumption]) is not viable, because accumulators are not
-- copyable.  Fortunately, while the accumulators that are consumed in
-- the forward sweep will also be present in the return sweep given
-- our current translation rules, they will be dead code.  As long as
-- we are careful to run dead code elimination after revVJP, we should
-- be good.

-- Note [Array Adjoints of Match]
--
-- Some unusual, but sadly not completely contrived, contain Match
-- expressions that return multiple arrays, and there the arrays
-- returned by one branch have overlapping aliases with another
-- branch, although in different places. As an example consider this:
--
--   let (X,Y) = if c
--               then (A, B)
--               else (B, A)
--
-- Because our aliasing representation cannot express mutually
-- exclusive aliases, we will consider X and Y to be aliased to each
-- other. In practice, this means it is unlikely for X or Y to be
-- consumed, because it would also consume the other (although it's
-- possible for carefully written code).
--
-- When producing adjoints for this, it will be something like
--
--   let (X_adj,Y_adj) = if c
--                       then (A_adj, B_adj)
--                       else (B_adj, A_adj)
--
-- which completely reflects the primal code. However, while it is
-- unlikely that any consumption takes place for the original primal
-- variables, it is almost guaranteed that X_adj and Y_adj will be
-- consumed (that is the main way we use adjoints after all), and due
-- to the conservative aliasing, when one is consumed, so is the
-- other! To avoid this tragic fate, we are forced to copy any
-- array-typed adjoints returned by a Match. This can be quite costly.
-- However:
--
-- 1) Futhark has pretty OK copy removal, so maybe it can get rid of
--    these by using information not available to the AD pass.
--
-- 2) In many cases, arrays will have accumulator adjoints, which are
--    not subject to this problem.
--
-- Issue #2228 was caused by neglecting to do this.

import Control.Monad
import Control.Monad.Identity
import Data.List ((\\))
import Futhark.AD.Rev.Monad
import Futhark.Builder
import Futhark.IR.SOACS
import Futhark.Tools
import Futhark.Transform.Rename
import Futhark.Util (chunks, takeLast)

-- | Transform updates on accumulators matching the given certificates into
-- updates that write provided zero values.
zeroOutUpdates :: [(VName, [SubExp])] -> Lambda SOACS -> Lambda SOACS
zeroOutUpdates certs_to_zeroes lam = lam {lambdaBody = onBody $ lambdaBody lam}
  where
    onExp = runIdentity . mapExpM mapper
      where
        mapper =
          (identityMapper :: (Monad m) => Mapper SOACS SOACS m)
            { mapOnOp = traverseSOACStms (\_ stms -> pure $ onStms stms),
              mapOnBody = \_ body -> pure $ onBody body
            }
    onStms = fmap onStm
    onStm (Let (Pat [pe]) aux (BasicOp (UpdateAcc safety acc is _)))
      | Acc c _ _ _ <- patElemType pe,
        Just zero <- lookup c certs_to_zeroes =
          Let (Pat [pe]) aux (BasicOp (UpdateAcc safety acc is zero))
    onStm (Let pat aux e) = Let pat aux $ onExp e

    onBody body = body {bodyStms = onStms $ bodyStms body}

-- Update accumulator parameter types in the lambda to include vectorised
-- element types. Also updates all Acc-typed pattern elements and inner
-- lambda parameters that reference the same accumulator certs.
updateAccParamTypes :: Int -> Shape -> Lambda SOACS -> Lambda SOACS
updateAccParamTypes n_inputs adj_sh lam
  | adj_sh == mempty = lam
  | otherwise =
      let (cert_ps, rest_ps) = splitAt n_inputs (lambdaParams lam)
          (acc_ps, other_ps) = splitAt n_inputs rest_ps
          acc_ps' = map (updateParam cert_names) acc_ps
          cert_names = map paramName cert_ps
          body' = updateBody cert_names (lambdaBody lam)
          ret' = map (updateAccType cert_names) (lambdaReturnType lam)
       in lam
            { lambdaParams = cert_ps ++ acc_ps' ++ other_ps,
              lambdaReturnType = ret',
              lambdaBody = body'
            }
  where
    updateParam :: [VName] -> Param Type -> Param Type
    updateParam certs p =
      p {paramDec = updateAccType certs (paramDec p)}

    updateAccType :: [VName] -> Type -> Type
    updateAccType certs (Acc cert acc_shape ts u)
      | cert `elem` certs =
          Acc cert acc_shape (map (`arrayOfShape` adj_sh) ts) u
    updateAccType _ t = t

    updateBody :: [VName] -> Body SOACS -> Body SOACS
    updateBody certs body =
      body {bodyStms = fmap (updateStm certs) (bodyStms body)}

    updateStm :: [VName] -> Stm SOACS -> Stm SOACS
    updateStm certs (Let pat aux e) =
      Let (updatePat certs pat) aux (updateExp certs e)

    updatePat :: [VName] -> Pat Type -> Pat Type
    updatePat certs (Pat pes) =
      Pat $ map (\pe -> pe {patElemDec = updateAccType certs (patElemDec pe)}) pes

    updateExp :: [VName] -> Exp SOACS -> Exp SOACS
    updateExp certs = runIdentity . mapExpM mapper
      where
        mapper =
          (identityMapper :: (Monad m) => Mapper SOACS SOACS m)
            { mapOnBody = \_ b -> pure $ updateBody certs b,
              mapOnOp = pure . updateSOAC certs
            }

    updateSOAC :: [VName] -> SOAC SOACS -> SOAC SOACS
    updateSOAC certs = runIdentity . mapSOACM mapper
      where
        mapper =
          identitySOACMapper
            { mapOnSOACLambda = pure . updateLambda certs
            }

    updateLambda :: [VName] -> Lambda SOACS -> Lambda SOACS
    updateLambda certs l =
      l
        { lambdaParams = map (updateParam certs) (lambdaParams l),
          lambdaReturnType = map (updateAccType certs) (lambdaReturnType l),
          lambdaBody = updateBody certs (lambdaBody l)
        }

diffWithAcc ::
  VjpOps ->
  Pat Type ->
  StmAux () ->
  [(Shape, [VName], Maybe (Lambda SOACS, [SubExp]))] ->
  Lambda SOACS ->
  ADM () ->
  ADM ()
diffWithAcc ops pat aux inputs lam m = do
  addStm $ Let pat aux $ WithAcc inputs lam
  m
  returnSweepCode $ do
    adj_shape <- askShape
    adjs <- mapM lookupAdj $ patNames pat
    -- Transpose the accumulator result adjoints from [vec...][shape...]elem
    -- to [shape...][vec...]elem, matching the internal accumulator layout.
    adjs' <- transposeAdjs adj_shape adjs
    lam' <- renameLambda lam
    -- Update the lambda's accumulator parameter types to reflect vectorised
    -- element types BEFORE differentiation, so that lookupAdj on Acc variables
    -- inside the lambda gives the correct vectorised adjoint type.
    let lam'_vec = updateAccParamTypes n_inputs adj_shape lam'
    free_vars <- filterM isActive $ namesToList $ freeIn lam'_vec
    free_accs <- filterM (fmap isAcc . lookupType) free_vars
    let free_vars' = free_vars \\ free_accs
    lam'' <- diffLambda' adjs' free_vars' lam'_vec
    (inputs_zeroes, inputs') <-
      unzip <$> zipWithM (renameInputLambda adj_shape) (chunks lengths adjs) inputs
    let certs = map paramName $ take n_inputs $ lambdaParams lam''
    raw_adjs <-
      letTupExp "with_acc_contrib" . WithAcc inputs' $
        zeroOutUpdates (zip certs inputs_zeroes) lam''
    -- The accumulator results have shape [shape...][vec...]elem. Transpose
    -- back to [vec...][shape...]elem for the adjoint.
    let n_arrs = sum lengths
        (arr_adjs, free_adjs) = splitAt n_arrs raw_adjs
    arr_adjs' <- zipWithM (transposeAccResult adj_shape) (map (\(s, _, _) -> s) inputs) arr_adjs
    zipWithM_ insAdj arrs arr_adjs'
    zipWithM_ insAdj free_vars' free_adjs
  where
    n_inputs = length inputs
    lengths = map (\(_, as, _) -> length as) inputs
    arrs = concatMap (\(_, as, _) -> as) inputs

    -- Transpose the accumulator-related adjoints from [vec...][shape...]elem
    -- to [shape...][vec...]elem. Non-accumulator adjs are left unchanged.
    transposeAdjs :: Shape -> [Adj] -> ADM [Adj]
    transposeAdjs adj_sh adjs
      | adj_sh == mempty = pure adjs
      | otherwise = do
          let n_arrs = sum lengths
              (acc_adjs, other_adjs) = splitAt n_arrs adjs
          acc_adjs' <- mapM transposeAdj acc_adjs
          pure $ acc_adjs' ++ other_adjs

    transposeAdj :: Adj -> ADM Adj
    transposeAdj adj = do
      v <- adjVal adj
      v' <- vecToInner v
      pure $ AdjVal $ Var v'

    -- Transpose [shape...][vec...][elem...] to [vec...][shape...][elem...]
    transposeAccResult :: Shape -> Shape -> VName -> ADM VName
    transposeAccResult adj_sh shape v
      | adj_sh == mempty = pure v
      | otherwise = do
          v_t <- lookupType v
          let r = shapeRank adj_sh
              s = shapeRank shape
              total = arrayRank v_t
              perm = [s .. s + r - 1] ++ [0 .. s - 1] ++ [s + r .. total - 1]
          letExp (baseName v <> "_tr") $ BasicOp $ Rearrange v perm

    renameInputLambda adj_sh as_adj (shape, as, _) = do
      -- Compute element types with vectorised dimensions included.
      orig_nes_ts <- mapM (fmap (stripArray (shapeRank shape)) . lookupType) as
      let vec_nes_ts = map (`arrayOfShape` adj_sh) orig_nes_ts
      zeroes <- mapM (zeroArray mempty) vec_nes_ts
      -- Transpose adjoints from [vec...][shape...]elem to [shape...][vec...]elem
      -- so they match the accumulator layout.
      as' <- mapM adjVal as_adj
      as'' <- mapM vecToInner as'
      pure (map Var zeroes, (shape, as'', Nothing))

    diffLambda' res_adjs get_adjs_for (Lambda params ts body) = do
      localScope (scopeOfLParams params) $ do
        Body () stms res <- vjpBody ops res_adjs get_adjs_for body
        let body' = Body () stms $ take n_inputs res <> takeLast (length get_adjs_for) res
        ts' <- mapM lookupType get_adjs_for
        pure $ Lambda params (take n_inputs ts <> ts') body'

diffUpdateAcc ::
  Pat Type ->
  StmAux () ->
  Safety ->
  VName ->
  [SubExp] ->
  [SubExp] ->
  ADM () ->
  ADM ()
diffUpdateAcc pat aux safety acc is vs m = do
  addStm $ Let pat aux $ BasicOp $ UpdateAcc safety acc is vs
  m
  pat_adjs <- mapM lookupAdjVal (patNames pat)
  returnSweepCode $ do
    forM_ (zip pat_adjs vs) $ \(adj, v) -> do
      adj_t <- lookupType adj
      let index_adj = pure $ BasicOp $ Index adj $ fullSlice adj_t $ map DimFix is
      adj_i <-
        letExp "updateacc_val_adj" =<< case safety of
          Unsafe ->
            index_adj
          Safe ->
            -- The primal UpdateAcc may be out-of-bounds, in which case
            -- indexing the adjoint is dangerous.
            eIf
              (eShapeInBounds (arrayShape adj_t) (map eSubExp is))
              (eBody [index_adj])
              (eBody [pure $ zeroExp $ stripArray (length is) adj_t])
      updateSubExpAdj v adj_i