packages feed

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

-- | Flattening of 'Match'.
module Futhark.Pass.Flatten.Match
  ( flattenMatch,
  )
where

import Control.Monad
import Data.Containers.ListUtils (nubOrd)
import Data.List qualified as L
import Data.Map qualified as M
import Data.Maybe
import Data.Set qualified as S
import Data.Tuple.Solo
import Futhark.IR.GPU
import Futhark.IR.SOACS
import Futhark.Pass.Flatten.Distribute
import Futhark.Pass.Flatten.General
import Futhark.Tools

-- Given the indices for which a branch is taken and its body,
-- distribute the statements of the body of that branch.
distributeBranch ::
  FlattenOps ->
  Segments ->
  DistEnv ->
  DistInputs ->
  VName ->
  Body SOACS ->
  M.Map VName ResRep ->
  FlattenM (DistInputs, DistEnv, DistStms)
distributeBranch ops segments env inps is body acc_reps = do
  let lvl = flattenSegLevel ops
      free_in_body = filter (isVariant inps . Var) (namesToList $ freeIn body)
  scope <- askScope
  free_sizes <-
    foldMap freeIn <$> mapM (lookupInputType inps) free_in_body
  let free_variant_sizes = filter (isVariant inps . Var) (namesToList free_sizes)
      free_size_vars = nubOrd (free_variant_sizes <> free_in_body)
  (ts, vs, reps) <-
    unzip3 <$> mapM (splitInput lvl segments env inps is acc_reps) free_size_vars
  let inputs = do
        (v, t, i) <- zip3 vs ts [0 ..]
        pure (v, DistInput (ResTag i) t)
  let env' = DistEnv $ M.fromList $ zip (map ResTag [0 ..]) reps
  let (inputs', dstms) = distributeBodyWith ops scope segments inputs body
  pure (inputs', env', dstms)

-- Given a single result from each branch as well the *unlifted*
-- result type, merge the results of all branches into a single result.
mergeResult ::
  SegLevel ->
  Segments ->
  SubExp ->
  [VName] ->
  [ResRep] ->
  DistResult ->
  FlattenM ResRep
mergeResult lvl segments w iss branchesRep dist_res
  -- Regular case
  | isRegularDistResult dist_res = do
      let (DistType _ _ resType) = distResType dist_res
      if isAcc resType
        then do
          xs <- mapM asRegular branchesRep
          pure $ Regular $ last xs
        else do
          let resultType = Array (elemType resType) (Shape [w] <> arrayShape resType) NoUniqueness
          xs <- mapM asRegular branchesRep
          -- Create the blank space for the result
          resultSpace <- letExp "blank_res" =<< eBlank resultType
          -- Write back the values of each branch to the blank space
          result <- foldM (scatterRegular lvl) resultSpace $ zip iss xs
          result_t <- arrayShape <$> lookupType result
          result' <-
            letExp "match_res_reg" . BasicOp $
              Reshape result (reshapeAll result_t (segmentsShape segments <> arrayShape resType))
          pure $ Regular result'
  -- Irregular case
  | DistType _ _ (Array pt _ _) <- distResType dist_res = do
      branchesIrregRep <- mapM asIrregular branchesRep
      let segsType = Array (IntType Int64) (Shape [w]) NoUniqueness
      -- Create a blank space for the 'segs'
      segsSpace <- letExp "blank_segs" =<< eBlank segsType
      -- Write back the segs of each branch to the blank space
      segs <- foldM (scatterRegular lvl) segsSpace $ zip iss (irregularS <$> branchesIrregRep)
      (_, offsets, num_data) <- exScanAndSum lvl segs
      let resultType = Array pt (Shape [num_data]) NoUniqueness
      -- Create the blank space for the result
      resultSpace <- letExp "blank_res" =<< eBlank resultType
      -- Write back the values of each branch to the blank space
      elems <- foldM (scatterIrregular lvl offsets) resultSpace $ zip iss branchesIrregRep
      flags <- genFlags lvl num_data offsets
      pure $
        Irregular $
          IrregularRep
            { irregularS = segs,
              irregularF = flags,
              irregularO = offsets,
              irregularD = elems,
              irregularK = Dense
            }
  | otherwise = error "mergeResult: non-array irregular result"
  where
    asRegular (Regular v) = pure v
    asRegular _ = error "mergeResult: mismatched reps"

    asIrregular (Irregular irreg) = pure irreg
    asIrregular _ = error "mergeResult: mismatched reps"

-- | Flatten a single branch body of a variant 'Match', but guard its execution
-- on the branch actually being taken by some segment. When a branch receives no
-- segments (its partition is empty) we must not run its flattened code: it may
-- call lifted recursive functions, which would recurse forever on an empty
-- batch. An untaken branch's results are never read ('mergeResult' scatters
-- them back through the branch's empty index array), so we just yield blanks.
--
-- Like 'flattenUniformMatch', the branch is lifted to a 'Result' of flat rep
-- components and the reps recovered with 'distResultsToResReps'; here we
-- additionally wrap it in a @branch_size > 0@ 'Match'.
guardBranch ::
  FlattenOps ->
  SubExp ->
  DistEnv ->
  DistInputs ->
  DistStms ->
  [DistResult] ->
  Result ->
  FlattenM [ResRep]
guardBranch ops branch_size env inputs dstms res result = do
  let branch_segments = [branch_size]
  (taken_body, taken_types) <-
    buildBody $ do
      body_res <- liftBodyWithDistResults ops branch_segments inputs env dstms res result
      ts <- mapM (subExpType . resSubExp) body_res
      pure (body_res, ts)
  -- Blanks for the untaken branch have the same types as the taken branch.
  -- Sizes bound inside the branch (the length of irregular data) are not in
  -- scope here, so we zero them; the 'Match' then makes them existential.
  untaken_body <- buildBody_ $ do
    let blank t = t `setArrayShape` Shape (map (const (intConst Int64 0)) (arrayDims t))
    subExpsRes <$> mapM (letSubExp "blank" <=< eBlank . blank) taken_types
  match_e <-
    eIf
      (eCmpOp (CmpSlt Int64) (eSubExp (intConst Int64 0)) (eSubExp branch_size))
      (pure taken_body)
      (pure untaken_body)
  match_res <- letTupExp "guarded_branch" match_e
  rets <- expExtType match_e
  pure $ distResultsToResReps res $ drop (S.size (shapeContext rets)) match_res

flattenVariantMatch ::
  FlattenOps ->
  Segments ->
  DistEnv ->
  DistInputs ->
  [DistResult] ->
  StmAux () ->
  [SubExp] ->
  [Case (Body SOACS)] ->
  Body SOACS ->
  MatchDec ExtType ->
  FlattenM DistEnv
flattenVariantMatch ops segments env inps res _aux scrutinees cases defaultCase _rt = do
  let lvl = flattenSegLevel ops
  w <- letSubExp "w" <=< toExp $ product $ segmentDims segments
  -- We need to partition the indices of the scrutinees by which case they match.
  -- Lift the scrutinees.
  -- If it's a variable, we know it's a scalar and the lifted version will therefore be a regular array.
  lifted_scrutinees <- forM scrutinees $ \scrut -> do
    liftSubExpRegular lvl segments inps env (segmentsShape segments) scrut
  -- Cases for tagging values that match the same branch.
  -- The default case is the 0'th equvalence class.
  let equiv_cases =
        zipWith
          (\(Case pat _) n -> Case pat $ eBody [toExp $ intConst Int64 n])
          cases
          [1 ..]
  let equiv_case_default = eBody [toExp $ intConst Int64 0]
  -- Match the scrutinees againts the branch cases
  equiv_classes <- letExp "equiv_classes" <=< segMap lvl (MkSolo w) $ \(MkSolo i) -> do
    -- unflatten index
    let seg_is = unflattenIndex (segmentDims segments) (pe64 i)
    scruts <- mapM (letSubExp "scruts" <=< flip eIndex (map toExp seg_is)) lifted_scrutinees
    cls <- letSubExp "cls" =<< eMatch scruts equiv_cases equiv_case_default
    pure [subExpRes cls]
  let num_cases = fromIntegral $ length cases + 1
  n_cases <- letExp "n_cases" <=< toExp $ intConst Int64 num_cases
  -- Parition the indices of the scrutinees by their equvalence class such
  -- that (the indices) of the scrutinees belonging to class 0 come first,
  -- then those belonging to class 1 and so on.
  (partition_sizes, partition_offs, partition_inds) <- doPartition lvl n_cases equiv_classes
  inds_t <- lookupType partition_inds
  -- Get the indices of each scrutinee by equivalence class
  branch_info <- forM [0 .. num_cases - 1] $ \i -> do
    num_data <-
      letSubExp ("size" <> nameFromString (show i))
        =<< eIndex partition_sizes [toExp $ intConst Int64 i]
    begin <-
      letSubExp ("idx_begin" <> nameFromString (show i))
        =<< eIndex partition_offs [toExp $ intConst Int64 i]
    inds <-
      letExp ("inds_branch" <> nameFromString (show i)) $
        BasicOp . Index partition_inds $
          fullSlice inds_t [DimSlice begin num_data (intConst Int64 1)]
    pure (num_data, inds)
  let (branch_sizes, inds) = unzip branch_info

  -- Distribute and lift the branch bodies.
  -- We put the default case at the start as it's the 0'th equivalence class
  -- and is therefore the first segment after the partition.
  let branch_bodies = defaultCase : map (\(Case _ body) -> body) cases
  let branch_results = map bodyResult branch_bodies
  -- Accumulator results are threaded from one branch to the next and cannot be
  -- blanked, so we only guard branch execution when no accumulators are
  -- involved. XXX: can we be sure this will never be a problem?
  let hasAcc = any (\dr -> case distResType dr of DistType _ _ t -> isAcc t) res
  -- acc inputs are handled differently, each branch use the result of the previous branch
  (branch_reps, _) <-
    foldM
      ( \(branch_reps_acc, acc_reps) (branch_size, branch_inds, body, result) -> do
          let branch_segments = [branch_size]
          (inputs, env', dstms) <-
            distributeBranch ops segments env inps branch_inds body acc_reps
          reps <-
            if hasAcc
              then do
                env'' <- foldM (flattenDistStm ops branch_segments) env' dstms
                zipWithM (liftDistResultRep lvl branch_segments inputs env'') res result
              else guardBranch ops branch_size env' inputs dstms res result
          let acc_reps' = replaceAccReps acc_reps reps
          pure (branch_reps_acc <> [reps], acc_reps')
      )
      ([], M.empty)
      (L.zip4 branch_sizes inds branch_bodies branch_results)
  -- Merging acc results is done by using the last branch result
  reps <- zipWithM (mergeResult lvl segments w inds) (L.transpose branch_reps) res
  insertRepsM (zip (map distResTag res) reps) env
  where
    findAccCert :: VName -> (VName, DistInput) -> Maybe VName
    findAccCert cert v_inp =
      let (v, inp) = v_inp
       in if isAcc (distInputType inp)
            then case distInputType inp of
              Acc cert' _ _ _ | cert == cert' -> Just v
              _ -> Nothing
            else Nothing

    -- Idealy this should be a singleton
    findAccCerts :: VName -> [VName]
    findAccCerts cert = mapMaybe (findAccCert cert) inps

    replaceAccRep acc_reps (dist_res, rep) =
      let (DistType _ _ t) = distResType dist_res
       in if not $ isAcc t
            then
              acc_reps
            else
              let (Acc cert _ _ _) = t
                  accVars = findAccCerts cert
               in foldl (\m v -> M.insert v rep m) acc_reps accVars
    replaceAccReps acc_reps reps = foldl replaceAccRep acc_reps $ zip res reps

flattenUniformMatch ::
  FlattenOps ->
  Segments ->
  DistEnv ->
  DistInputs ->
  [DistResult] ->
  StmAux () ->
  [SubExp] ->
  [Case (Body SOACS)] ->
  Body SOACS ->
  MatchDec ExtType ->
  FlattenM DistEnv
flattenUniformMatch ops segments env inps res aux scrutinees cases defaultCase rt = do
  scope <- askScope
  new_cases <- forM cases $ \(Case c body) -> do
    let (case_body_inputs, case_dstms) =
          distributeBodyWith ops scope segments inps body

    fmap (Case c) . buildBody_ $
      liftBodyWithDistResults ops segments case_body_inputs env case_dstms res (bodyResult body)
  new_default_body <- do
    let (new_default_body_inputs, new_default_dstms) =
          distributeBodyWith ops scope segments inps defaultCase
    buildBody_ $
      liftBodyWithDistResults ops segments new_default_body_inputs env new_default_dstms res (bodyResult defaultCase)

  -- Maybe it is better to build MatchDec ourselves
  match_e <-
    eMatch'
      scrutinees
      [Case c (pure body) | Case c body <- new_cases]
      (pure new_default_body)
      (matchSort rt)

  match_res <-
    certifying (distCerts inps aux env) $
      letTupExp "match_res" match_e

  rets <- expExtType match_e
  -- get rid of the existential context
  let payload_res = drop (S.size (shapeContext rets)) match_res
  let reps = distResultsToResReps res payload_res
  insertRepsM (zip (map distResTag res) reps) env

-- | Flatten a 'Match'
flattenMatch ::
  FlattenOps ->
  Segments ->
  DistEnv ->
  DistInputs ->
  [DistResult] ->
  StmAux () ->
  [SubExp] ->
  [Case (Body SOACS)] ->
  Body SOACS ->
  MatchDec ExtType ->
  FlattenM DistEnv
flattenMatch ops segments env inps res aux scrutinees cases defaultCase rt =
  -- 'flattenUniformMatch' keeps the scrutinees in a plain GPU 'Match', which is
  -- only well-scoped when they are invariant to the nest. Whenever a scrutinee is
  -- variant we must partition the segments by branch, even if no branch contains
  -- parallelism (this happens e.g. for a variant conditional with an irregular
  -- result, which cannot be sequentialised into a scalar group).
  if any (isVariant inps) scrutinees
    then flattenVariantMatch ops segments env inps res aux scrutinees cases defaultCase rt
    else flattenUniformMatch ops segments env inps res aux scrutinees cases defaultCase rt