packages feed

dataframe-core-2.6.0.0: src-internal/DataFrame/Internal/Aggregation/Kernel/Dense.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}

-- | The low-cardinality dense reduction kernel (direct-indexed by group).
module DataFrame.Internal.Aggregation.Kernel.Dense (
    denseReduce,
    denseMaxMinusMin,
) where

import Data.Type.Equality (TestEquality (..), type (:~:) (Refl))
import qualified Data.Vector.Unboxed as VU
import qualified Data.Vector.Unboxed.Mutable as VUM
import System.IO.Unsafe (unsafePerformIO)
import Type.Reflection (typeRep)

import DataFrame.Internal.Aggregation.Reduction (Reduction (..))
import DataFrame.Internal.Column (
    Column (..),
    fromUnboxedVector,
    materializePacked,
 )
import DataFrame.Internal.Control.Concurrent (
    capabilities,
    parThreshold,
    parallelChunks,
    shouldParallelize,
 )

-- | Group count at or below which float reductions run one sequential pass.
seqFloatGroups :: Int
seqFloatGroups = 65536

-- | Run a recognised reduction through the direct-indexed path, if admitted.
denseReduce :: Reduction -> VU.Vector Int -> Int -> Column -> Maybe Column
denseReduce red g nGroups col = case col of
    UnboxedColumn Nothing (v :: VU.Vector a) ->
        case testEquality (typeRep @a) (typeRep @Int) of
            Just Refl -> denseInt red g nGroups v
            Nothing -> case testEquality (typeRep @a) (typeRep @Double) of
                Just Refl -> denseDouble red g nGroups v
                Nothing -> Nothing
    p@(PackedText _ _) -> denseReduce red g nGroups (materializePacked p)
    _ -> Nothing
{-# INLINEABLE denseReduce #-}

-- | The reductions admitted over an Int column.
denseInt :: Reduction -> VU.Vector Int -> Int -> VU.Vector Int -> Maybe Column
denseInt red g nGroups v = case red of
    RCount -> Just (fromUnboxedVector (countDense g nGroups (VU.length v)))
    RSum -> Just (fromUnboxedVector (sumIntDense g nGroups v))
    RMin -> Just (fromUnboxedVector (extremaIntDense True g nGroups v))
    RMax -> Just (fromUnboxedVector (extremaIntDense False g nGroups v))
    RMean -> Just (fromUnboxedVector (meanIntDense g nGroups v))
    _ -> Nothing

-- | The reductions admitted over a Double column.
denseDouble ::
    Reduction -> VU.Vector Int -> Int -> VU.Vector Double -> Maybe Column
denseDouble red g nGroups v = case red of
    RCount -> Just (fromUnboxedVector (countDense g nGroups (VU.length v)))
    RSum -> Just (fromUnboxedVector (sumDblDense g nGroups v))
    RMean -> Just (fromUnboxedVector (meanDblDense g nGroups v))
    RMin -> Just (fromUnboxedVector (extremaDblDense True g nGroups v))
    RMax -> Just (fromUnboxedVector (extremaDblDense False g nGroups v))
    _ -> Nothing

-- | The fused @max a - min b@ direct pass over both extrema at once.
denseMaxMinusMin :: VU.Vector Int -> Int -> Column -> Column -> Maybe Column
denseMaxMinusMin g nGroups ca cb = case (ca, cb) of
    ( UnboxedColumn Nothing (va :: VU.Vector x)
        , UnboxedColumn Nothing (vb :: VU.Vector y)
        )
            | Just Refl <- testEquality (typeRep @x) (typeRep @Int)
            , Just Refl <- testEquality (typeRep @y) (typeRep @Int) ->
                Just (fromUnboxedVector (maxMinusMinDenseInt g nGroups va vb))
            | Just Refl <- testEquality (typeRep @x) (typeRep @Double)
            , Just Refl <- testEquality (typeRep @y) (typeRep @Double) ->
                Just (fromUnboxedVector (maxMinusMinDenseDbl g nGroups va vb))
    _ -> Nothing
{-# INLINEABLE denseMaxMinusMin #-}

-- | Monomorphic entry points for the 'testEquality' dispatch above.
maxMinusMinDenseInt ::
    VU.Vector Int -> Int -> VU.Vector Int -> VU.Vector Int -> VU.Vector Int
maxMinusMinDenseInt = maxMinusMinDense minBound maxBound
{-# NOINLINE maxMinusMinDenseInt #-}

maxMinusMinDenseDbl ::
    VU.Vector Int -> Int -> VU.Vector Double -> VU.Vector Double -> VU.Vector Double
maxMinusMinDenseDbl = maxMinusMinDense (negate (1 / 0)) (1 / 0)
{-# NOINLINE maxMinusMinDenseDbl #-}

-- | Whether to fan out at this row count.
shouldPar :: Int -> Bool
shouldPar = shouldParallelize parThreshold

-- | Fork workers over disjoint row ranges, each with a private accumulator.
runPartialsOver ::
    Int -> Int -> (Int -> Int -> IO acc) -> IO [acc]
runPartialsOver n _caps = parallelChunks parThreshold n

countDense :: VU.Vector Int -> Int -> Int -> VU.Vector Int
countDense g nGroups n = unsafePerformIO $ do
    parts <- runPartialsOver n capabilities (countChunk g nGroups)
    mergeIntSum nGroups parts
{-# NOINLINE countDense #-}

countChunk :: VU.Vector Int -> Int -> Int -> Int -> IO (VUM.IOVector Int)
countChunk g nGroups lo hi = do
    acc <- VUM.replicate nGroups (0 :: Int)
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                c <- VUM.unsafeRead acc k
                VUM.unsafeWrite acc k (c + 1)
                go (i + 1)
    go lo
    pure acc

sumIntDense :: VU.Vector Int -> Int -> VU.Vector Int -> VU.Vector Int
sumIntDense g nGroups v = unsafePerformIO $ do
    parts <- runPartialsOver (VU.length v) capabilities (sumIntChunk g v nGroups)
    mergeIntSum nGroups parts
{-# NOINLINE sumIntDense #-}

sumIntChunk ::
    VU.Vector Int -> VU.Vector Int -> Int -> Int -> Int -> IO (VUM.IOVector Int)
sumIntChunk g v nGroups lo hi = do
    acc <- VUM.replicate nGroups (0 :: Int)
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                c <- VUM.unsafeRead acc k
                VUM.unsafeWrite acc k (c + VU.unsafeIndex v i)
                go (i + 1)
    go lo
    pure acc

-- | Double group sums, sequential at small group counts.
sumDblDense :: VU.Vector Int -> Int -> VU.Vector Double -> VU.Vector Double
sumDblDense g nGroups v
    | nGroups <= seqFloatGroups || not (shouldPar n) =
        unsafePerformIO (sumDblChunk g v nGroups 0 n >>= VU.unsafeFreeze)
    | otherwise = unsafePerformIO $ do
        parts <- runPartialsOver n capabilities (sumDblChunk g v nGroups)
        mergeDblSum nGroups parts
  where
    !n = VU.length v
{-# NOINLINE sumDblDense #-}

sumDblChunk ::
    VU.Vector Int ->
    VU.Vector Double ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector Double)
sumDblChunk g v nGroups lo hi = do
    acc <- VUM.replicate nGroups (0 :: Double)
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                c <- VUM.unsafeRead acc k
                VUM.unsafeWrite acc k (c + VU.unsafeIndex v i)
                go (i + 1)
    go lo
    pure acc

-- | Double mean: fused (sum, count) per group, divided once at finalize.
meanDblDense :: VU.Vector Int -> Int -> VU.Vector Double -> VU.Vector Double
meanDblDense g nGroups v
    | nGroups <= seqFloatGroups || not (shouldPar n) = unsafePerformIO $ do
        (s, c) <- meanDblChunk g v nGroups 0 n
        finalizeMeanDbl nGroups s c
    | otherwise = unsafePerformIO $ do
        parts <- runPartialsOver n capabilities (meanDblChunk g v nGroups)
        (s, c) <- mergeMeanDbl nGroups parts
        finalizeMeanDbl nGroups s c
  where
    !n = VU.length v
{-# NOINLINE meanDblDense #-}

meanDblChunk ::
    VU.Vector Int ->
    VU.Vector Double ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector Double, VUM.IOVector Int)
meanDblChunk g v nGroups lo hi = do
    s <- VUM.replicate nGroups (0 :: Double)
    c <- VUM.replicate nGroups (0 :: Int)
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                sv <- VUM.unsafeRead s k
                VUM.unsafeWrite s k (sv + VU.unsafeIndex v i)
                cv <- VUM.unsafeRead c k
                VUM.unsafeWrite c k (cv + 1)
                go (i + 1)
    go lo
    pure (s, c)

finalizeMeanDbl ::
    Int -> VUM.IOVector Double -> VUM.IOVector Int -> IO (VU.Vector Double)
finalizeMeanDbl nGroups s c = do
    out <- VUM.new nGroups
    let go !k
            | k >= nGroups = pure ()
            | otherwise = do
                sv <- VUM.unsafeRead s k
                cv <- VUM.unsafeRead c k
                VUM.unsafeWrite
                    out
                    k
                    (if cv == 0 then 0 / 0 else sv / fromIntegral cv)
                go (k + 1)
    go 0
    VU.unsafeFreeze out

extremaIntDense ::
    Bool -> VU.Vector Int -> Int -> VU.Vector Int -> VU.Vector Int
extremaIntDense isMin g nGroups v = unsafePerformIO $ do
    parts <-
        runPartialsOver (VU.length v) capabilities (extremaIntChunk isMin g v nGroups)
    mergeExtremaInt isMin nGroups parts
{-# NOINLINE extremaIntDense #-}

extremaIntChunk ::
    Bool ->
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector Int)
extremaIntChunk isMin g v nGroups lo hi = do
    let !seed = if isMin then maxBound else minBound
        combine a b = if isMin then min a b else max a b
    acc <- VUM.replicate nGroups seed
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                c <- VUM.unsafeRead acc k
                VUM.unsafeWrite acc k (combine c (VU.unsafeIndex v i))
                go (i + 1)
    go lo
    pure acc

extremaDblDense ::
    Bool -> VU.Vector Int -> Int -> VU.Vector Double -> VU.Vector Double
extremaDblDense isMin g nGroups v
    | not (shouldPar n) =
        unsafePerformIO (extremaDblChunk isMin g v nGroups 0 n >>= VU.unsafeFreeze)
    | otherwise = unsafePerformIO $ do
        parts <- runPartialsOver n capabilities (extremaDblChunk isMin g v nGroups)
        mergeExtremaDbl isMin nGroups parts
  where
    !n = VU.length v
{-# NOINLINE extremaDblDense #-}

extremaDblChunk ::
    Bool ->
    VU.Vector Int ->
    VU.Vector Double ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector Double)
extremaDblChunk isMin g v nGroups lo hi = do
    let !seed = if isMin then 1 / 0 else negate (1 / 0)
        combine a b = if isMin then min a b else max a b
    acc <- VUM.replicate nGroups seed
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                c <- VUM.unsafeRead acc k
                VUM.unsafeWrite acc k (combine c (VU.unsafeIndex v i))
                go (i + 1)
    go lo
    pure acc

-- | One streaming pass accumulating @max a@ and @min b@ together.
maxMinusMinDense ::
    (VU.Unbox a, Num a, Ord a) =>
    a ->
    a ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    VU.Vector a ->
    VU.Vector a
{-# SPECIALIZE maxMinusMinDense ::
    Int ->
    Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector Int ->
    VU.Vector Int ->
    VU.Vector Int
    #-}
{-# SPECIALIZE maxMinusMinDense ::
    Double ->
    Double ->
    VU.Vector Int ->
    Int ->
    VU.Vector Double ->
    VU.Vector Double ->
    VU.Vector Double
    #-}
maxMinusMinDense maxSeed minSeed g nGroups va vb
    | not (shouldPar n) = unsafePerformIO $ do
        (mx, mn) <- maxMinusMinChunk maxSeed minSeed g va vb nGroups 0 n
        finalizeMaxMinusMin nGroups mx mn
    | otherwise = unsafePerformIO $ do
        parts <-
            runPartialsOver
                n
                capabilities
                (maxMinusMinChunk maxSeed minSeed g va vb nGroups)
        (mx, mn) <- mergeMaxMin nGroups parts
        finalizeMaxMinusMin nGroups mx mn
  where
    !n = VU.length va
-- INLINEABLE (not NOINLINE) so the SPECIALIZE pragmas above take effect.
{-# INLINEABLE maxMinusMinDense #-}

maxMinusMinChunk ::
    (VU.Unbox a, Ord a) =>
    a ->
    a ->
    VU.Vector Int ->
    VU.Vector a ->
    VU.Vector a ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector a, VUM.IOVector a)
maxMinusMinChunk maxSeed minSeed g va vb nGroups lo hi = do
    mx <- VUM.replicate nGroups maxSeed
    mn <- VUM.replicate nGroups minSeed
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                cx <- VUM.unsafeRead mx k
                VUM.unsafeWrite mx k (max cx (VU.unsafeIndex va i))
                cn <- VUM.unsafeRead mn k
                VUM.unsafeWrite mn k (min cn (VU.unsafeIndex vb i))
                go (i + 1)
    go lo
    pure (mx, mn)

mergeMaxMin ::
    (VU.Unbox a, Ord a) =>
    Int ->
    [(VUM.IOVector a, VUM.IOVector a)] ->
    IO (VUM.IOVector a, VUM.IOVector a)
mergeMaxMin nGroups parts = case parts of
    [] -> error "mergeMaxMin: no partials"
    ((mx0, mn0) : rest) -> do
        let add (mx, mn) = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            xa <- VUM.unsafeRead mx0 k
                            xb <- VUM.unsafeRead mx k
                            VUM.unsafeWrite mx0 k (max xa xb)
                            na <- VUM.unsafeRead mn0 k
                            nb <- VUM.unsafeRead mn k
                            VUM.unsafeWrite mn0 k (min na nb)
                            go (k + 1)
                go 0
        mapM_ add rest
        pure (mx0, mn0)

finalizeMaxMinusMin ::
    (VU.Unbox a, Num a) =>
    Int ->
    VUM.IOVector a ->
    VUM.IOVector a ->
    IO (VU.Vector a)
finalizeMaxMinusMin nGroups mx mn = do
    out <- VUM.new nGroups
    let go !k
            | k >= nGroups = pure ()
            | otherwise = do
                a <- VUM.unsafeRead mx k
                b <- VUM.unsafeRead mn k
                VUM.unsafeWrite out k (a - b)
                go (k + 1)
    go 0
    VU.unsafeFreeze out

-- | Integer mean in one fused pass: running sum and count per group.
meanIntDense :: VU.Vector Int -> Int -> VU.Vector Int -> VU.Vector Double
meanIntDense g nGroups v
    | not (shouldPar n) = unsafePerformIO $ do
        (s, c) <- meanIntChunk g v nGroups 0 n
        finalizeMeanInt nGroups s c
    | otherwise = unsafePerformIO $ do
        parts <- runPartialsOver n capabilities (meanIntChunk g v nGroups)
        (s, c) <- mergePair nGroups parts
        finalizeMeanInt nGroups s c
  where
    !n = VU.length v
{-# NOINLINE meanIntDense #-}

meanIntChunk ::
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    Int ->
    Int ->
    IO (VUM.IOVector Int, VUM.IOVector Int)
meanIntChunk g v nGroups lo hi = do
    s <- VUM.replicate nGroups (0 :: Int)
    c <- VUM.replicate nGroups (0 :: Int)
    let go !i
            | i >= hi = pure ()
            | otherwise = do
                let !k = VU.unsafeIndex g i
                sv <- VUM.unsafeRead s k
                VUM.unsafeWrite s k (sv + VU.unsafeIndex v i)
                cv <- VUM.unsafeRead c k
                VUM.unsafeWrite c k (cv + 1)
                go (i + 1)
    go lo
    pure (s, c)

finalizeMeanInt ::
    Int -> VUM.IOVector Int -> VUM.IOVector Int -> IO (VU.Vector Double)
finalizeMeanInt nGroups s c = do
    out <- VUM.new nGroups
    let go !k
            | k >= nGroups = pure ()
            | otherwise = do
                sv <- VUM.unsafeRead s k
                cv <- VUM.unsafeRead c k
                VUM.unsafeWrite
                    out
                    k
                    (if cv == 0 then 0 / 0 else fromIntegral sv / fromIntegral cv)
                go (k + 1)
    go 0
    VU.unsafeFreeze out

mergeIntSum :: Int -> [VUM.IOVector Int] -> IO (VU.Vector Int)
mergeIntSum nGroups parts = case parts of
    [] -> VU.unsafeFreeze =<< VUM.replicate nGroups 0
    (p0 : rest) -> do
        let add !p = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            a <- VUM.unsafeRead p0 k
                            b <- VUM.unsafeRead p k
                            VUM.unsafeWrite p0 k (a + b)
                            go (k + 1)
                go 0
        mapM_ add rest
        VU.unsafeFreeze p0

-- | Sum per-worker Double partials into the first worker's, in worker order.
mergeDblSum :: Int -> [VUM.IOVector Double] -> IO (VU.Vector Double)
mergeDblSum nGroups parts = case parts of
    [] -> VU.unsafeFreeze =<< VUM.replicate nGroups 0
    (p0 : rest) -> do
        let add !p = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            a <- VUM.unsafeRead p0 k
                            b <- VUM.unsafeRead p k
                            VUM.unsafeWrite p0 k (a + b)
                            go (k + 1)
                go 0
        mapM_ add rest
        VU.unsafeFreeze p0

-- | Merge per-worker (sum, count) partials by exact integer addition.
mergePair ::
    Int ->
    [(VUM.IOVector Int, VUM.IOVector Int)] ->
    IO (VUM.IOVector Int, VUM.IOVector Int)
mergePair nGroups parts = case parts of
    [] -> (,) <$> VUM.replicate nGroups 0 <*> VUM.replicate nGroups 0
    ((s0, c0) : rest) -> do
        let add (s, c) = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            sa <- VUM.unsafeRead s0 k
                            sb <- VUM.unsafeRead s k
                            VUM.unsafeWrite s0 k (sa + sb)
                            ca <- VUM.unsafeRead c0 k
                            cb <- VUM.unsafeRead c k
                            VUM.unsafeWrite c0 k (ca + cb)
                            go (k + 1)
                go 0
        mapM_ add rest
        pure (s0, c0)

-- | As 'mergePair' but for the Double (sum, count) partials of the mean.
mergeMeanDbl ::
    Int ->
    [(VUM.IOVector Double, VUM.IOVector Int)] ->
    IO (VUM.IOVector Double, VUM.IOVector Int)
mergeMeanDbl nGroups parts = case parts of
    [] -> (,) <$> VUM.replicate nGroups 0 <*> VUM.replicate nGroups 0
    ((s0, c0) : rest) -> do
        let add (s, c) = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            sa <- VUM.unsafeRead s0 k
                            sb <- VUM.unsafeRead s k
                            VUM.unsafeWrite s0 k (sa + sb)
                            ca <- VUM.unsafeRead c0 k
                            cb <- VUM.unsafeRead c k
                            VUM.unsafeWrite c0 k (ca + cb)
                            go (k + 1)
                go 0
        mapM_ add rest
        pure (s0, c0)

mergeExtremaInt :: Bool -> Int -> [VUM.IOVector Int] -> IO (VU.Vector Int)
mergeExtremaInt isMin nGroups parts = case parts of
    [] ->
        VU.unsafeFreeze =<< VUM.replicate nGroups (if isMin then maxBound else minBound)
    (p0 : rest) -> do
        let combine a b = if isMin then min a b else max a b
            add !p = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            a <- VUM.unsafeRead p0 k
                            b <- VUM.unsafeRead p k
                            VUM.unsafeWrite p0 k (combine a b)
                            go (k + 1)
                go 0
        mapM_ add rest
        VU.unsafeFreeze p0

mergeExtremaDbl :: Bool -> Int -> [VUM.IOVector Double] -> IO (VU.Vector Double)
mergeExtremaDbl isMin nGroups parts = case parts of
    [] ->
        VU.unsafeFreeze
            =<< VUM.replicate nGroups (if isMin then 1 / 0 else negate (1 / 0))
    (p0 : rest) -> do
        let combine a b = if isMin then min a b else max a b
            add !p = do
                let go !k
                        | k >= nGroups = pure ()
                        | otherwise = do
                            a <- VUM.unsafeRead p0 k
                            b <- VUM.unsafeRead p k
                            VUM.unsafeWrite p0 k (combine a b)
                            go (k + 1)
                go 0
        mapM_ add rest
        VU.unsafeFreeze p0