lvish-1.1.1.1: Control/LVish/BulkRetry.hs
{-# LANGUAGE ScopedTypeVariables, BangPatterns #-}
{-# LANGUAGE DataKinds #-}
-- | EXPERIMENTAL version which eventually should be made generic across Par monads
-- (i.e. a BulkRetryT transformer), and should thus be extended to transparently
-- catch any attempts by a thread to block, not just the special non-blocking calls
-- provided by *this* library.
module Control.LVish.BulkRetry
where
import qualified Data.Bits.Atomic as B
import Foreign.Storable (sizeOf, Storable)
import Control.Monad (unless)
import Control.LVish
import Control.LVish.Internal (unsafeDet)
import Control.Par.Class (LVarSched(returnToSched))
-- import Data.LVar.NatArray
import Data.LVar.NatArray.Unsafe (NatArray, unsafePeek)
import Data.Par.Splittable (pforEach)
import Data.Par.Range (range)
import Data.Par.Set () -- Instances only.
import qualified Data.Foldable as F
import qualified Data.Set as S
-- import Data.LVar.PureSet as IS
import Data.LVar.SLSet as IS
import Data.LVar.Generic (freeze)
-- import Data.Par.Range
--------------------------------------------------------------------------------
-- | The point where users send abort messages.
data RetryHub s = RetryHub (ISet s Int) -- ^ This stores the iterations that fail.
Int -- ^ This is the current iteration
-- -- | Non-blocking get on a `NatArray`.
-- getNB :: forall s d elt . (Storable elt, B.AtomicBits elt, Num elt) =>
-- RetryHub s -> NatArray s elt -> Int -> Par d s elt
-- -- LVarSched (Par d s)
-- getNB (RetryHub fails) arr ind = do
-- x <- unsafePeek arr ind
-- -- if empty, don't block, do this:
-- case x of
-- Nothing -> do logDbgLn 4 $ " [dbg-lvish] getNB: iteration failed, enqueue for retry: "++show ind
-- insert ind fails
-- returnToSched
-- Just res -> return res
-- | Non-blocking get on a `NatArray`. In this prototype we require that the user
-- manually CPS the computation, so that the delimited continuation between this get
-- and the end of the loop iteration is passed explicitly as an argument.
--
-- The current reason for this compromise is that the HandlerPool mechanism is not
-- robust to us dropping the current continuation with `returnToSched`. We would
-- need a version of HandlerPool's that interoperates with a user-level callCC, that is
-- we would need something like bracket/dynamic-wind for our continuation monad.
getNB_cps :: forall s d elt . (Storable elt, B.AtomicBits elt, Num elt) =>
RetryHub s
-> NatArray s elt -- ^ Array to dereference
-> Int -- ^ Which index to get
-> (elt -> Par d s ()) -- ^ Delimited continuation.
-> Par d s ()
-- LVarSched (Par d s)
getNB_cps (RetryHub fails thisiter) arr ind cont = do
x <- unsafePeek arr ind
-- if empty, don't block, do this:
case x of
Nothing -> do logDbgLn 4 $ " [dbg-lvish] getNB: iteration "++ show thisiter
++" failed, due to get on index "++show ind
insert thisiter fails
return ()
Just res -> do logDbgLn 4 $ " [dbg-lvish] getNB: result available, calling continuation (iter "++show thisiter++")"
cont res
{-# INLINE getNB_cps #-}
desired_tasks :: Int
desired_tasks = 16 -- FIXME: num procs * overpartition
-- | A parallel for-loop which aborts and retries failed iterations in bulk, rather
-- than allowing them to "block" and suffering the overhead of capturing and storing
-- their continuations.
--
-- `forSpeculative` continues retrying until ALL iterations have completed. It is
-- thus a *synchronous* parallel for loop.
forSpeculative :: (Int, Int) -- ^ Inclusive/Exclusive range to run.
-> (RetryHub s -> Int -> Par d s ()) -- ^ Body of the loop
-> Par d s ()
-- forSpeculative :: (Int, Int) -> (RetryHub s -> Int -> Par QuasiDet s ()) -> Par QuasiDet s ()
-- TODO: Requires idempotency!!
forSpeculative (st,end) bodyfn = do
logDbgLn 2 $ " [dbg-lvish] Begin forSpeculative, bounds "++show (st,end)
let sz = end - st
-- Even in a trivial loop, 2000 iters per task should be enough:
prefix = min sz (2000 * desired_tasks)
-- TODO: automatic strategies for tuning the input prefix size would be helpful.
-- One approach that might make sense would be to auto-tune based on the
-- time/iteration observed. That is, gradually increase to try to approximate a
-- minimum reasonable task size and no bigger.
body' = bodyfn
-- body' retry ix = bodyfn retry ix
let flush leftover fails =
-- unless (S.null leftover) $ do
-- TODO: need parallel fold, this is sequential...
F.foldlM (\ () ix -> do
logDbgLn 3 $ " [dbg-lvish] forSpeculative: flushing iter "++show ix
body' (RetryHub fails ix) ix)
() leftover
let flushLoop leftover = do
fails <- newEmptySet
-- FIXME: Add parallelism
flush leftover fails -- Sequential...
snap <- unsafeDet $ freeze fails
logDbgLn 3 $ " [dbg-lvish] forSpeculative: did one sequential flush, remaining: "++show snap
unless (S.null snap) $
-- error$ "forSpeculative: failures not flushed with a sequential run!:\n "++show snap
flushLoop snap
-- Outer loop of "rounds", in which we try a prefix of the iteration space.
let loop !round leftover offset 0 = do
logDbgLn 3 $ " [dbg-lvish] forSpeculative: got to the end, only failures left."
flushLoop leftover
loop !round leftover offset remain = do
logDbgLn 3 $ " [dbg-lvish] forSpeculative starting round "++
show round++": offset "++show offset++", remaining "++show remain
-- Set of iterations that failed in THIS upcoming round:
fails <- newEmptySet
let chunkend = offset + (min prefix remain)
hp <- newPool
-- Here we keep the failed iterations "to the left" of the new batch, i.e. we
-- fork them first.
-- FINISHME: need Split instance.
logDbgLn 4 $ " [dbg-lvish] forSpeculative RElaunching failures: "++show leftover
-- This version is poor because it forks on a per-iteration basis upon retry:
-- F.foldrM (\ ix () -> forkHP (Just hp) (body' (RetryHub fails ix) ix)) () leftover
-- F.foldrM (\ ix () -> body' (RetryHub fails ix) ix) () leftover
-- pforEach leftover $ bodyfn (RetryHub fails)
asyncForEachHP (Just hp) leftover $ \ ix -> bodyfn (RetryHub fails ix) ix
-- TODO: if we keep failing it's better to expand the prefix. That way we
-- end up with a logarithmic number of retries for each iterate in the worst
-- case, rather than linear (making the whole loop unnecessarily quadratic).
logDbgLn 4 $ " [dbg-lvish] forSpeculative launching new batch: "++show (offset,chunkend)
asyncForEachHP (Just hp) (range offset chunkend) $ \ ix ->
body' (RetryHub fails ix) ix
logDbgLn 4 $ " [dbg-lvish] forSpeculative: return from par for-loop; now quiesce."
quiesce hp
logDbgLn 4 $ " [dbg-lvish] forSpeculative: quiesce finished, next freeze failed set."
snap <- unsafeDet $ freeze fails
logDbgLn 4 $ " [dbg-lvish] forSpeculative finish round; failed iterates: "++show snap
loop (round+1) snap chunkend (remain - (chunkend - offset))
loop 0 S.empty 0 sz
-- After the last quiesce, we're done.