packages feed

dataframe-core-1.1.0.0: src/DataFrame/Internal/AggKernelPar.hs

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

{- | Parallel scatter-accumulate aggregation kernel.

This builds on the sequential kernel ('DataFrame.Internal.AggKernel') and the
Round-5 grouping layout: 'groupBy' hands us @valueIndices@ (rows ordered by
group) and @offsets@ (per-group boundaries), so each group's rows are a
contiguous run @valueIndices[offsets[g] .. offsets[g+1])@ in their original row
order.

The parallel driver splits the dense group-id range @[0, nGroups)@ into one
contiguous chunk per capability, balanced by row count. Because group ranges are
disjoint and their @valueIndices@ runs are disjoint, every worker reads and
writes its own slice of the shared output array(s) — there is NO cross-worker
overlap and NO merge. And because each group's rows are visited in the same
original-row order as the sequential @rowToGroup@ scan, the per-group fold order
is unchanged, so the result is /byte-identical/ to the sequential kernel at any
@-N@ (no fold-order drift, even for the float sums).

Forks plain 'forkIO' workers (no sparks), one per capability; falls back to the
sequential 'DataFrame.Internal.AggKernel' path when @caps == 1@ or the row count
is below 'parThreshold'.
-}
module DataFrame.Internal.AggKernelPar (
    scatterReducePar,
    momentScatterPar,
) where

import Control.Concurrent (forkIO, getNumCapabilities)
import Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)
import Control.Exception (SomeException, throwIO, try)
import Control.Monad (when)
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.AggKernel (
    Reduction (..),
    scatterColumnToDouble,
    scatterReduce,
 )
import DataFrame.Internal.AggPlan (Moments (..), momentScatter)
import DataFrame.Internal.Column (
    Column (..),
    Columnable,
    fromUnboxedVector,
    materializePacked,
 )

-------------------------------------------------------------------------------
-- Parallelisation policy
-------------------------------------------------------------------------------

{- | Below this many rows the fork overhead is not worth it; the sequential
kernel runs instead. Mirrors 'DataFrame.Internal.GroupingPar.parThreshold'.
-}
parThreshold :: Int
parThreshold = 200000

capabilities :: Int
capabilities = unsafePerformIO getNumCapabilities
{-# NOINLINE capabilities #-}

-- | Whether to take the parallel path at this row count.
shouldPar :: Int -> Bool
shouldPar n = n >= parThreshold && capabilities > 1

-------------------------------------------------------------------------------
-- Group-range partitioning by row count
-------------------------------------------------------------------------------

{- | Split @[0, nGroups)@ into @caps@ contiguous group ranges, each holding a
near-equal share of rows. Returns @caps + 1@ boundaries @b@ with @b[0] == 0@ and
@b[caps] == nGroups@; worker @w@ owns groups @[b[w], b[w+1])@. A row-balanced
split keeps skew low even when group sizes vary wildly.
-}
groupRangeBounds :: VU.Vector Int -> Int -> Int -> VU.Vector Int
groupRangeBounds offs nGroups caps = VU.create $ do
    b <- VUM.new (caps + 1)
    let !nRows = VU.unsafeIndex offs nGroups
        !per = max 1 ((nRows + caps - 1) `div` caps)
        -- First group whose start offset reaches @target@ (>= prev), or nGroups.
        adv !target !gg
            | gg >= nGroups = nGroups
            | VU.unsafeIndex offs gg >= target = gg
            | otherwise = adv target (gg + 1)
        go !w !prev
            | w >= caps = VUM.unsafeWrite b caps nGroups
            | otherwise = do
                let !target = min nRows (w * per)
                    !g = adv target prev
                VUM.unsafeWrite b w g
                go (w + 1) g
    VUM.unsafeWrite b 0 0
    go 1 0
    pure b

{- | Fork @caps@ workers, worker @w@ running @act (b[w]) (b[w+1])@ over its
disjoint group range; join and rethrow the first failure. Sequential when
@caps == 1@.
-}
forEachRange :: VU.Vector Int -> Int -> (Int -> Int -> IO ()) -> IO ()
forEachRange bounds caps act
    | caps <= 1 = act (VU.unsafeIndex bounds 0) (VU.unsafeIndex bounds caps)
    | otherwise = do
        vars <- mapM spawn [0 .. caps - 1]
        results <- mapM takeMVar vars
        mapM_ (either (throwIO :: SomeException -> IO ()) pure) results
  where
    spawn w = do
        var <- newEmptyMVar
        let !s = VU.unsafeIndex bounds w
            !e = VU.unsafeIndex bounds (w + 1)
        _ <- forkIO (try (act s e) >>= putMVar var)
        pure var

-------------------------------------------------------------------------------
-- Parallel single-column reductions
-------------------------------------------------------------------------------

{- | Parallel counterpart of 'scatterReduce'. Returns 'Nothing' on the same
columns the sequential kernel rejects (boxed/nullable/non-Int-Double); on the
sequential path or tiny inputs it delegates to 'scatterReduce'. The result is
byte-identical to 'scatterReduce' (same per-group fold order).
-}
scatterReducePar ::
    Reduction -> VU.Vector Int -> VU.Vector Int -> Int -> Column -> Maybe Column
scatterReducePar red vis offs nGroups col
    | not (shouldPar (VU.length vis)) || nGroups <= 1 =
        scatterReduce red (rtgFromVis vis offs nGroups) nGroups col
    | otherwise = case col of
        UnboxedColumn Nothing (v :: VU.Vector a) ->
            case testEquality (typeRep @a) (typeRep @Int) of
                Just Refl -> Just (reduceParTyped red vis offs nGroups v intIdent)
                Nothing -> case testEquality (typeRep @a) (typeRep @Double) of
                    Just Refl -> Just (reduceParTyped red vis offs nGroups v dblIdent)
                    Nothing -> Nothing
        p@(PackedText _ _) -> scatterReducePar red vis offs nGroups (materializePacked p)
        _ -> Nothing
{-# NOINLINE scatterReducePar #-}

{- | Reconstruct @rowToGroup@ from the group layout for the sequential delegate.
Only used on the small/-N1 path, so the extra pass is negligible.
-}
rtgFromVis :: VU.Vector Int -> VU.Vector Int -> Int -> VU.Vector Int
rtgFromVis vis offs nGroups = VU.create $ do
    let n = VU.length vis
    rtg <- VUM.new (max 1 n)
    let go !g
            | g >= nGroups = pure ()
            | otherwise = do
                let !e = VU.unsafeIndex offs (g + 1)
                    inner !pos
                        | pos >= e = pure ()
                        | otherwise = do
                            VUM.unsafeWrite rtg (VU.unsafeIndex vis pos) g
                            inner (pos + 1)
                inner (VU.unsafeIndex offs g)
                go (g + 1)
    go 0
    pure rtg

data Idents a = Idents {minSeed :: !a, maxSeed :: !a}

intIdent :: Idents Int
intIdent = Idents maxBound minBound

dblIdent :: Idents Double
dblIdent = Idents (1 / 0) (negate (1 / 0))

{- | The monomorphic parallel reduction body. Each scatter allocates its full
@nGroups@ output array(s) once, then workers fill disjoint group ranges in
parallel. The result type follows the sequential kernel exactly.
-}
reduceParTyped ::
    forall a.
    (Columnable a, VU.Unbox a, Num a, Ord a, Real a) =>
    Reduction ->
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Idents a ->
    Column
reduceParTyped red vis offs nGroups v idents =
    let !caps = capabilities
        !bounds = groupRangeBounds offs nGroups caps
     in case red of
            RCount -> fromUnboxedVector (unsafePerformIO (countPar vis offs nGroups caps bounds))
            RSum -> fromUnboxedVector (unsafePerformIO (sumPar vis offs nGroups v caps bounds))
            RMin ->
                fromUnboxedVector
                    (unsafePerformIO (extremaPar min (minSeed idents) vis offs nGroups v caps bounds))
            RMax ->
                fromUnboxedVector
                    (unsafePerformIO (extremaPar max (maxSeed idents) vis offs nGroups v caps bounds))
            RMean -> fromUnboxedVector (unsafePerformIO (meanPar vis offs nGroups v caps bounds))
            RVar ->
                fromUnboxedVector
                    (unsafePerformIO (varPar False vis offs nGroups v caps bounds))
            RStd ->
                fromUnboxedVector (unsafePerformIO (varPar True vis offs nGroups v caps bounds))
            RTop2Sum -> fromUnboxedVector (unsafePerformIO (top2Par vis offs nGroups v caps bounds))
{-# INLINE reduceParTyped #-}

-- | Iterate the rows of groups @[gs, ge)@ in @valueIndices@/group order.
overGroups ::
    VU.Vector Int -> VU.Vector Int -> Int -> Int -> (Int -> Int -> IO ()) -> IO ()
overGroups vis offs gs ge step = grp gs
  where
    grp !g
        | g >= ge = pure ()
        | otherwise = do
            let !e = VU.unsafeIndex offs (g + 1)
                inner !pos
                    | pos >= e = pure ()
                    | otherwise = step g (VU.unsafeIndex vis pos) >> inner (pos + 1)
            inner (VU.unsafeIndex offs g)
            grp (g + 1)
{-# INLINE overGroups #-}

countPar ::
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector Int)
countPar _vis offs nGroups caps bounds = do
    out <- VUM.replicate nGroups (0 :: Int)
    forEachRange bounds caps $ \gs ge ->
        let grp !g
                | g >= ge = pure ()
                | otherwise = do
                    let !c = VU.unsafeIndex offs (g + 1) - VU.unsafeIndex offs g
                    VUM.unsafeWrite out g c
                    grp (g + 1)
         in grp gs
    VU.unsafeFreeze out

sumPar ::
    (VU.Unbox a, Num a) =>
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector a)
sumPar vis offs nGroups v caps bounds = do
    out <- VUM.replicate nGroups 0
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            cur <- VUM.unsafeRead out g
            VUM.unsafeWrite out g (cur + VU.unsafeIndex v row)
    VU.unsafeFreeze out
{-# INLINE sumPar #-}

extremaPar ::
    (VU.Unbox a) =>
    (a -> a -> a) ->
    a ->
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector a)
extremaPar combine seed vis offs nGroups v caps bounds = do
    out <- VUM.replicate nGroups seed
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            cur <- VUM.unsafeRead out g
            VUM.unsafeWrite out g (combine cur (VU.unsafeIndex v row))
    VU.unsafeFreeze out
{-# INLINE extremaPar #-}

meanPar ::
    (VU.Unbox a, Real a) =>
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector Double)
meanPar vis offs nGroups v caps bounds = do
    s <- VUM.replicate nGroups (0 :: Double)
    cnt <- VUM.replicate nGroups (0 :: Int)
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            let !x = realToFrac (VU.unsafeIndex v row)
            cs <- VUM.unsafeRead s g
            VUM.unsafeWrite s g (cs + x)
            cc <- VUM.unsafeRead cnt g
            VUM.unsafeWrite cnt g (cc + 1)
    out <- VUM.new nGroups
    let fin !k
            | k >= nGroups = pure ()
            | otherwise = do
                sv <- VUM.unsafeRead s k
                c <- VUM.unsafeRead cnt k
                VUM.unsafeWrite out k (if c == 0 then 0 / 0 else sv / fromIntegral c)
                fin (k + 1)
    fin 0
    VU.unsafeFreeze out
{-# INLINE meanPar #-}

{- | Per-group Welford variance/sd, parallel by group range. The recurrence is
applied in original-row order within each group, identical to the sequential
@varScatter@, so the result is byte-identical (no parallel-combine needed: each
group lives wholly inside one worker's range).
-}
varPar ::
    (VU.Unbox a, Real a) =>
    Bool ->
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector Double)
varPar takeSqrt vis offs nGroups v caps bounds = do
    cnt <- VUM.replicate nGroups (0 :: Int)
    meanV <- VUM.replicate nGroups (0 :: Double)
    m2 <- VUM.replicate nGroups (0 :: Double)
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            let !x = realToFrac (VU.unsafeIndex v row)
            c <- VUM.unsafeRead cnt g
            mu <- VUM.unsafeRead meanV g
            mm <- VUM.unsafeRead m2 g
            let !c' = c + 1
                !delta = x - mu
                !mu' = mu + delta / fromIntegral c'
                !mm' = mm + delta * (x - mu')
            VUM.unsafeWrite cnt g c'
            VUM.unsafeWrite meanV g mu'
            VUM.unsafeWrite m2 g mm'
    out <- VUM.new nGroups
    let fin !k
            | k >= nGroups = pure ()
            | otherwise = do
                c <- VUM.unsafeRead cnt k
                mm <- VUM.unsafeRead m2 k
                let var = if c < 2 then 0 else mm / fromIntegral (c - 1)
                VUM.unsafeWrite out k (if takeSqrt then sqrt var else var)
                fin (k + 1)
    fin 0
    VU.unsafeFreeze out
{-# INLINE varPar #-}

top2Par ::
    (VU.Unbox a, Real a) =>
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector a ->
    Int ->
    VU.Vector Int ->
    IO (VU.Vector Double)
top2Par vis offs nGroups v caps bounds = do
    let ninf = negate (1 / 0) :: Double
    m1 <- VUM.replicate nGroups ninf
    m2 <- VUM.replicate nGroups ninf
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            let !x = realToFrac (VU.unsafeIndex v row)
            a1 <- VUM.unsafeRead m1 g
            if x > a1
                then do
                    VUM.unsafeWrite m1 g x
                    VUM.unsafeWrite m2 g a1
                else do
                    a2 <- VUM.unsafeRead m2 g
                    when (x > a2) (VUM.unsafeWrite m2 g x)
    out <- VUM.new nGroups
    let fin !k
            | k >= nGroups = pure ()
            | otherwise = do
                a1 <- VUM.unsafeRead m1 k
                a2 <- VUM.unsafeRead m2 k
                let sm = (if isInfinite a1 then 0 else a1) + (if isInfinite a2 then 0 else a2)
                VUM.unsafeWrite out k sm
                fin (k + 1)
    fin 0
    VU.unsafeFreeze out
{-# INLINE top2Par #-}

-------------------------------------------------------------------------------
-- Parallel fused two-column moments (Q9)
-------------------------------------------------------------------------------

{- | Parallel counterpart of 'momentScatter': one fused pass over both columns,
each group's six sums accumulated in original-row order inside one worker's
range. Byte-identical to 'momentScatter'; delegates to it on the sequential
path. Returns 'Nothing' unless both columns are non-null unboxed Int/Double.
-}
momentScatterPar ::
    VU.Vector Int -> VU.Vector Int -> Int -> Column -> Column -> Maybe Moments
momentScatterPar vis offs nGroups colX colY
    | not (shouldPar (VU.length vis)) || nGroups <= 1 =
        momentScatter (rtgFromVis vis offs nGroups) nGroups colX colY
    | otherwise = do
        xs <- scatterColumnToDouble colX
        ys <- scatterColumnToDouble colY
        let !caps = capabilities
            !bounds = groupRangeBounds offs nGroups caps
        pure (unsafePerformIO (momentPar vis offs nGroups xs ys caps bounds))
{-# NOINLINE momentScatterPar #-}

momentPar ::
    VU.Vector Int ->
    VU.Vector Int ->
    Int ->
    VU.Vector Double ->
    VU.Vector Double ->
    Int ->
    VU.Vector Int ->
    IO Moments
momentPar vis offs nGroups xs ys caps bounds = do
    cnt <- VUM.replicate nGroups (0 :: Int)
    sx <- VUM.replicate nGroups (0 :: Double)
    sy <- VUM.replicate nGroups (0 :: Double)
    sxx <- VUM.replicate nGroups (0 :: Double)
    syy <- VUM.replicate nGroups (0 :: Double)
    sxy <- VUM.replicate nGroups (0 :: Double)
    let bump arr g d = VUM.unsafeRead arr g >>= \c -> VUM.unsafeWrite arr g (c + d)
    forEachRange bounds caps $ \gs ge ->
        overGroups vis offs gs ge $ \g row -> do
            let !x = VU.unsafeIndex xs row
                !y = VU.unsafeIndex ys row
            VUM.unsafeRead cnt g >>= \c -> VUM.unsafeWrite cnt g (c + 1)
            bump sx g x
            bump sy g y
            bump sxx g (x * x)
            bump syy g (y * y)
            bump sxy g (x * y)
    Moments . fromUnboxedVector
        <$> VU.unsafeFreeze cnt
        <*> (fromUnboxedVector <$> VU.unsafeFreeze sx)
        <*> (fromUnboxedVector <$> VU.unsafeFreeze sy)
        <*> (fromUnboxedVector <$> VU.unsafeFreeze sxx)
        <*> (fromUnboxedVector <$> VU.unsafeFreeze syy)
        <*> (fromUnboxedVector <$> VU.unsafeFreeze sxy)