packages feed

futhark-0.27.1: src/Futhark/Pass/Flatten/Monad.hs

{-# LANGUAGE TypeFamilies #-}

-- | General definitions for the flattening transformation.
--
-- Defines not just the core monads that are involved, but also the various
-- representations, except perhaps the ones that are completely local to another
-- module.
module Futhark.Pass.Flatten.Monad
  ( IrregularKind (..),
    IrregularRep (..),
    ResRep (..),
    DistEnv (..),
    FlattenOps (..),

    -- * Flattening monad
    FlattenM,
    FlattenState (..),
    runFlattenM,

    -- * Demands
    BuiltinFn (..),
    LiftMode (..),
    DemandFn (..),
    demandLifted,
    demandBuiltin,

    -- * Insertions
    insertRepM,
    insertRepsM,
    insertIrregularM,
    insertRegulars,

    -- * Various
    resVar,
    inputReps,
    segsAndElems,
  )
where

import Control.Monad
import Control.Monad.State
import Data.Bifunctor (bimap, second)
import Data.Foldable
import Data.Map qualified as M
import Data.Maybe (fromMaybe)
import Data.Set qualified as S
import Futhark.IR.GPU
import Futhark.IR.SOACS (SOACS)
import Futhark.Pass.Flatten.Distribute
import Futhark.Tools
import Prelude hiding (div, rem)

-- | If true, 'sanityCheck' blocks are evaluated.
doSanityCheck :: Bool
doSanityCheck = True

-- | Run a sanity-check that may verify invariants. The idea is that these can
-- be disabled without affecting the correctness of the pass, although there is
-- no constructive guarantee that no important effects take place in here.
sanityCheck :: (Monad m) => m () -> m ()
sanityCheck = when doSanityCheck

-- Note [Representation of Flat Arrays]
--
-- This flattening implementation uses largely the nomenclature and
-- structure described by Cosmin Oancea. In particular, consider an
-- irregular array 'A' where
--
--   - A has 'n' segments (outermost dimension).
--
--   - A has element type 't'.
--
--   - A has a total of 'm' elements (where 'm' is divisible by 'n',
--     and may indeed be 'm').
--
-- Then A is represented by the following arrays:
--
--   - A_D : [m]t; the "data array".
--
--   - A_S : [n]i64; the "shape array" giving the number of scalar elements of each segment.
--
--   - A_F : [m]bool; the "flag array", indicating when an element begins a
--     new segment.
--
--   - A_O : [n]i64; the offset array, indicating for each segment
--     where it starts in the data (and flag) array.
--
--   - A_II1 : [m]i64; the "segment indices"; a mapping from element
--     index to index of the segment it belongs to.
--
--   - A_II2 : [m]i64; the "inner indices"; a mapping from element index
--     to index within its corresponding segment.
--
-- The arrays that are not the data array are collectively called the
-- "structure arrays". All of the structure arrays can be computed
-- from each other, but conceptually they all coexist.
--
-- Note that we only consider the *outer* dimension to be the
-- "segments". Also, 't' may actually be an array itself (although in
-- this case, the shape of 't' must be invariant to all parallel
-- dimensions). The inner structure is preserved through code, not
-- data. (Or in practice, ad-hoc auxiliary arrays produced by code.)
-- In Cosmin's notation, we maintain only the information for the
-- outermost dimension.
--
-- As an example, consider an irregular array
--
--   A = [ [], [ [1,2,3], [4], [], [5,6] ], [ [7], [], [8,9,10] ] ]
--
-- then
--
--   n = 3
--
--   m = 10
--
--   A_D = [1,2,3,4,5,6,7,8,9,10]
--
--   A_S = [0, 6, 4]
--
--   A_F = [T,F,F,F,F,F,T,F,F,F]
--
--   A_O = [0, 0, 6]
--
--   A_II1 = [1,1,1,1,1,1,2,2,2,2]
--
--   A_II2 = [0,0,0,1,3,3,0,2,2,2]

data IrregularKind
  = Dense
  | Replicated
  deriving (Show, Eq)

data IrregularRep = IrregularRep
  { -- | Array of size of each segment, type @[]i64@.
    irregularS :: VName,
    irregularF :: VName,
    irregularO :: VName,
    irregularD :: VName,
    irregularK :: IrregularKind
  }
  deriving (Show)

data ResRep
  = -- | This variable is represented completely straightforwardly- if it is an
    -- array, it is a regular array.
    Regular VName
  | -- | The representation of an irregular array.
    Irregular IrregularRep
  deriving (Show)

newtype DistEnv = DistEnv {distResMap :: M.Map ResTag ResRep}

insertRep :: ResTag -> ResRep -> DistEnv -> DistEnv
insertRep rt rep env = env {distResMap = M.insert rt rep $ distResMap env}

insertRepM :: ResTag -> ResRep -> DistEnv -> FlattenM DistEnv
insertRepM rt rep env = do
  sanityCheck $ do
    case rep of
      Regular _ -> pure ()
      Irregular (IrregularRep shape flags offsets data_ _kind) -> do
        shape_t <- lookupType shape
        flags_t <- lookupType flags
        data_t <- lookupType data_
        offsets_t <- lookupType offsets

        unless (arrayRank flags_t == 1 && elemType flags_t == Bool) $
          error $
            "Invalid flag array type: " <> prettyString flags_t
        unless (arrayRank offsets_t == 1 && elemType offsets_t == int64) $
          error $
            "Invalid offsets array type: " <> prettyString offsets_t
        unless (arrayRank shape_t == 1 && elemType shape_t == int64) $
          error $
            "Invalid shape array type: " <> prettyString shape_t
        when (arrayRank data_t /= 1) $
          error $
            "Invalid data array array: " <> prettyString data_t
  pure $ insertRep rt rep env

insertRepsM :: [(ResTag, ResRep)] -> DistEnv -> FlattenM DistEnv
insertRepsM =
  flip $ foldM (flip $ uncurry insertRepM)

insertReps :: [(ResTag, ResRep)] -> DistEnv -> DistEnv
insertReps = flip $ foldl (flip $ uncurry insertRep)

insertIrregularM :: VName -> VName -> VName -> ResTag -> VName -> IrregularKind -> DistEnv -> FlattenM DistEnv
insertIrregularM shape flags offsets rt data_ kind env = do
  let rep = Irregular $ IrregularRep shape flags offsets data_ kind
  insertRepM rt rep env

insertRegulars :: [ResTag] -> [VName] -> DistEnv -> DistEnv
insertRegulars rts xs =
  insertReps (zip rts $ map Regular xs)

instance Monoid DistEnv where
  mempty = DistEnv mempty

instance Semigroup DistEnv where
  DistEnv x <> DistEnv y = DistEnv (x <> y)

resVar :: ResTag -> DistEnv -> ResRep
resVar rt env = fromMaybe bad $ M.lookup rt $ distResMap env
  where
    bad = error $ "resVar: unknown tag: " ++ show rt

segsAndElems :: DistEnv -> [DistInput] -> (Maybe (VName, VName, VName), [VName])
segsAndElems _ [] = (Nothing, [])
segsAndElems env (DistInputFree v _ : vs) =
  second (v :) $ segsAndElems env vs
segsAndElems env (DistInput rt _ : vs) =
  case resVar rt env of
    Regular v' ->
      second (v' :) $ segsAndElems env vs
    Irregular (IrregularRep segments flags offsets elems k) -> do
      case k of
        Dense -> do
          bimap (mplus $ Just (segments, flags, offsets)) (elems :) $ segsAndElems env vs
        Replicated ->
          second (flags :) $ segsAndElems env vs

-- | Mapping from original variable names to their distributed resreps.
inputReps :: DistInputs -> DistEnv -> M.Map VName (Type, ResRep)
inputReps inputs env = M.fromList $ map (second getRep) inputs
  where
    getRep di = case di of
      DistInput rt t -> (t, resVar rt env)
      DistInputFree v' t -> (t, Regular v')

-- | A representation of the different kinds of builtin functions we can
-- generate. This is used to only generate the ones we actually need for a given
-- program.
data BuiltinFn
  = BuiltinSegIota
  | BuiltinRepIota
  | BuiltinPrefixSum
  | BuiltinPartition
  deriving (Eq, Ord, Show)

data LiftMode
  = UniformLift
  | NonUniformLift
  deriving (Eq, Ord, Show)

-- | Indicate the need for a function to be generated. Instead of immediately
-- generating them ourselves, we collect requirements from multiple flattening
-- operations and satisfy them in their entirety.
data DemandFn
  = -- | We need this function to be lifted.
    DemandLifted Name LiftMode
  | DemandBuiltin BuiltinFn
  deriving (Eq, Ord, Show)

data FlattenState = FlattenState
  { -- In order to generate more stable threshold names, we keep track of the
    -- numbers used for thresholds separately from the ordinary name source.
    stateThresholdCounter :: Int,
    stateNameSource :: VNameSource,
    -- A set of those functions that we have emitted calls to, and which will
    -- need to be generated.
    stateDemandFns :: S.Set DemandFn
  }

newtype FlattenM a = FlattenM (BuilderT GPU (State FlattenState) a)
  deriving
    ( Functor,
      Applicative,
      Monad,
      LocalScope GPU,
      HasScope GPU,
      MonadState FlattenState,
      MonadFreshNames
    )

instance MonadBuilder FlattenM where
  type Rep FlattenM = GPU
  mkExpDecM pat e = FlattenM $ mkExpDecM pat e
  mkBodyM stms res = FlattenM $ mkBodyM stms res
  mkLetNamesM pat e = FlattenM $ mkLetNamesM pat e

  addStms = FlattenM . addStms
  collectStms (FlattenM m) = FlattenM $ collectStms m

instance MonadFreshNames (State FlattenState) where
  getNameSource = gets stateNameSource
  putNameSource src = modify $ \s -> s {stateNameSource = src}

-- | Do not nest these - the counter for thresholds will be wrong.
runFlattenM :: (MonadFreshNames m) => Scope GPU -> FlattenM a -> m (a, S.Set DemandFn)
runFlattenM scope (FlattenM m) = modifyNameSource $ \src ->
  let initial_state =
        FlattenState
          { stateThresholdCounter = 0,
            stateNameSource = src,
            stateDemandFns = mempty
          }
      (x, s) = runState (fst <$> runBuilderT m scope) initial_state
   in ((x, stateDemandFns s), stateNameSource s)

-- | Indicate that we rather need a lifted version of this function.
demandLifted :: Name -> LiftMode -> FlattenM ()
demandLifted fname mode = modify $ \s ->
  s {stateDemandFns = S.insert (DemandLifted fname mode) $ stateDemandFns s}

-- | Demand the presence of this builtin function.
demandBuiltin :: BuiltinFn -> FlattenM ()
demandBuiltin b = modify $ \s ->
  s {stateDemandFns = S.insert (DemandBuiltin b) $ stateDemandFns s}

-- | Functions for tying together disparate modules - this is to avoid mutually
-- recursive modules.
data FlattenOps = FlattenOps
  { flattenSegLevel :: SegLevel,
    -- | How to treat irregularity when distributing the bodies we encounter.
    flattenIrregularity :: DistIrregularity,
    flattenFunHasParallelism :: FunHasParallelism,
    -- | Flatten a 'DistStm' using the given ops, which need not be the ones
    -- this record belongs to - see 'atSegLevel'. Use 'flattenDistStm' to
    -- continue with the current ops.
    flattenDistStmWith :: FlattenOps -> Segments -> DistEnv -> DistStm -> FlattenM DistEnv,
    -- | Flatten a scalar statement at the given seg level, which need not be
    -- the one this record carries. Use 'flattenScalarStm' for the latter.
    flattenScalarStmAt :: SegLevel -> Segments -> DistEnv -> DistInputs -> [DistResult] -> Stm SOACS -> FlattenM DistEnv,
    -- | Transform a statement as if it occurred at the top level, including
    -- multi-versioning of SOACs. Used when a transformation (e.g. loop
    -- interchange) produces a statement that should be treated as if the
    -- program had looked like that all along.
    flattenTopLevelStm :: Stm SOACS -> FlattenM ()
  }