FP is awesome...
FP is awesome...
... but in the "multicore era" languages live or die on
What is the difference between ...
parallelism
concurrency
... or are they the same thing?
Parallel Same answer but faster
Concurrent Communication and Synchronization
Parallel Same answer but faster
Concurrent Communication and Synchronization
Compiler automatically parallelize programs?
B | A if input of B depends on output of ATracking data dependencies and side effects
Tradeoff between parallelism benefits vs. overheads
Lets see how to parallelize a Sudoku Solver
solve :: String -> Maybe Grid
Lets see how to parallelize a Sudoku Solver
solve :: String -> Maybe Grid
String input problem descriptionlet puz = ".......2143.......6........2.15..........637...........68...4.....23........7...."
solve :: String -> Maybe Grid
Grid output descriptionghci> printGrid $ fromJust $ solve puz
8 5 7 | 3 4 9 | 6 2 1
4 3 2 | 8 6 1 | 5 9 7
6 1 9 | 7 5 2 | 8 4 3
---------+---------+---------
2 7 1 | 5 8 3 | 9 6 4
9 4 5 | 1 2 6 | 3 7 8
3 8 6 | 4 9 7 | 2 1 5
---------+---------+---------
7 6 8 | 9 1 5 | 4 3 2
1 9 4 | 2 3 8 | 7 5 6
5 2 3 | 6 7 4 | 1 8 9
import Sudoku
import Control.Exception
import System.Environment
main :: IO ()
main = do
[f] <- getArgs -- get filename containing puzzles
grids <- fmap lines $ readFile f -- parse file into puzzles
mapM_ (evaluate . solve) grids -- solve all puzzles
evaluate ? ...Lets look at an example ...
fib :: Integer -> Integer
fib 0 = 1
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
fib is a really slow function
ghci> fib 35
...
9227465
fib is a really slow function
ghci> let z = fib 35
ghci> -- comes back instantly!
How does it finish so fast?
ghci> :sprint z
_
fib is a really slow function
Can force it to be computed by demanding to show it
ghci> z
...
9227465
ghci> :sprint z
z = 9227465
Whats up with the evaluate ? ...
evaluate :: a -> IO a
: or Just or ...)evaluate forces Haskell to compute upto top-level constructor
ghci> let fib2 n = (fib n, fib (n+1))
evaluate forces Haskell to compute upto top-level constructor
ghci> let fib2 n = (fib n, fib (n+1))
ghci> let r = fib2 35
ghci> :sprint r
r = _
evaluate forces Haskell to compute upto top-level constructor
ghci> let fib2 n = (fib n, fib (n+1))
ghci> let r = fib2 35
ghci> r' <- evaluate r
ghci> :sprint r'
r' = (_,_)
ghci> r
...
ghci> r
(9227465,14930352)
ghci> :sprint r
r = (9227465,14930352)
ghci> :sprint r'
r' = (9227465,14930352)
import Sudoku
import Control.Exception
import System.Environment
main :: IO ()
main = do
[f] <- getArgs -- get filename containing puzzles
grids <- fmap lines $ readFile f -- parse file into puzzles
mapM_ (evaluate . solve) grids -- solve all puzzles
evaluate ? ...Just care if solution exists ...evaluate to force computation to top-level constructorLets compile with runtime stats ...
$ ghc -O2 sudoku1.hs -rtsopts
... and run!
$ ./sudoku1 sudoku17.1000.txt +RTS -s
Lets compile with runtime stats ...
$ ghc -O2 sudoku1.hs -rtsopts
... and run!
$ ./sudoku1 sudoku17.1000.txt +RTS -s
Not too shabby ... (elapsed is wall-clock time)
Total time 2.11s ( 2.11s elapsed)
import Sudoku
import Control.Exception
import System.Environment
main :: IO ()
main = do
[f] <- getArgs -- get filename containing puzzles
grids <- fmap lines $ readFile f -- parse file into puzzles
mapM_ (evaluate . solve) grids -- solve all puzzles
Parallelism anyone?
solve is embarrasingly parallel?a in parallel,bEval MonadThere's a library for everything ... Control.Parallel.Strategies
a Valuesdata Eval a
Eval MonadThere's a library for everything ... Control.Parallel.Strategies
a Valuesdata Eval a
instance Monad Eval
Eval MonadThere's a library for everything ... Control.Parallel.Strategies
data Eval a
instance Monad Eval
To execute a computation
runEval :: Eval a -> a
Eval MonadThere's a library for everything ... Control.Parallel.Strategies
data Eval a
instance Monad Eval
runEval :: Eval a -> a
rpar :: a -> Eval a
rseq :: a -> Eval a
rpar x : Evaluate x in parallelrseq x : Evaluate x in sequence (wait till done)See sudoku2.hs
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
let (as, bs) = splitAt (length grids `div` 2) grids
evaluate $ runEval $ do
a <- rpar (deep (map solve as)) -- spawn first half
b <- rpar (deep (map solve bs)) -- spawn second half
rseq a -- wait till done
rseq b -- wait till done
return ()
deep forces evaluation (since we're not printing out results...)Lets compile ...
$ ghc -O2 sudoku2.hs -rtsopts -threaded
... and run
$ ./sudoku2 sudoku17.1000.txt +RTS -N2 -s
.
.
SPARKS: 2 (1 converted, 0 overflowed, 0 dud, 0 GC'd, 1 fizzled)
.
.
Total time 2.66s ( 1.52s elapsed)
Speedup = 2.11/1.52 = 1.38
$ threadscope sudoku2.eventlog
Second core is idling towards the end...
Problem: Work is not divided evenly
2 blocks (list of puzzles splitAt middle)rpar e
rpar (e.g. e)rpar e
rpar (e.g. e)rpar to fire up bajillion sparks on input data!main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
let (as, bs) = splitAt (length grids `div` 2) grids
evaluate $ runEval $ do
a <- rpar (deep (map solve as)) -- spawn first half
b <- rpar (deep (map solve bs)) -- spawn second half
rseq a -- wait till done
rseq b -- wait till done
return ()
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
evaluate $ runEval $ parSolve grids
rpar each puzzleparSolve [] = return []
parSolve (x : xs) = do y <- rpar (deep (solve x))
ys <- parSolve xs
return (y : ys)
parMapCan bottle the parallel pattern in parMap
parMap :: (a -> b) -> [a] -> Eval [b]
parMap f [] = return []
parMap f (x:xs) = do y <- rpar (f x)
ys <- parMap xs
return (y:ys)
->
Can release the genie to solve sudoku in parallel sudoku3.hs
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
evaluate $ deep $ runEval $ parMap solve grids
Can release the genie to solve sudoku in parallel sudoku3.hs
Compile ...
$ ghc -O2 sudoku3.hs -rtsopts -threaded
... and run
$ ./sudoku3 sudoku17.1000.txt +RTS -N2 -s
.
SPARKS: 1000 (1000 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
.
Total time 3.10s ( 1.57s elapsed)
Can release the genie to solve sudoku in parallel sudoku3.hs
Not too bad, lets see if the cores are balanced...
$ threadscope sudoku3.eventlog
Can release the genie to solve sudoku in parallel sudoku3.hs
Can just throw more cores at it ... 5.4 x speedup
$ ./sudoku3 sudoku17.1000.txt +RTS -N2 -s
Total time 3.24s ( 1.64s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N3 -s
Total time 3.47s ( 1.17s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N4 -s
Total time 3.30s ( 0.84s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N6 -s
Total time 3.96s ( 0.67s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N8 -s
Total time 4.64s ( 0.61s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N10 -s
Total time 4.77s ( 0.50s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N12 -s
Total time 5.28s ( 0.47s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N14 -s
Total time 5.68s ( 0.44s elapsed)
Can release the genie to solve sudoku in parallel sudoku3.hs
Better still, can just throw more cores at it ...
... upto a point!
$ ./sudoku3 sudoku17.1000.txt +RTS -N12 -s
Total time 5.28s ( 0.47s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N14 -s
Total time 5.68s ( 0.44s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N16 -s
Total time 6.97s ( 0.48s elapsed)
$ ./sudoku3 sudoku17.1000.txt +RTS -N18 -s
Total time 8.42s ( 0.52s elapsed)
Co-ordinating that many cores not always worth it...
parMapStrategiesWe just saw how (parallel) computation pattern was bottled as parMap
Generalize the bottling using Strategies
type Strategy a = a -> Eval a
type Strategy a = a -> Eval a
Some Example Strategys ...
rpar :: a -> Eval a -- Evaluate a in PARALLEL with current thread
rseq :: a -> Eval a -- Evaluate a in SEQUENCE in current thread
... and so!
rpar :: Strategy a -- Evaluate a in PARALLEL with current thread
rseq :: Strategy a -- Evaluate a in SEQUENCE in current thread
type Strategy a = a -> Eval a
Some Example Strategys
rdeepseq :: NFData a => Strategy a
rdeepseq x = rseq (deep x)
deep) evaluation of x ...using :: a -> Strategy a -> a
x `using` s = runEval (s x)
s x :: Eval a, says how to evaluate xrunEval (s x), then executes the plan yielding the value xusing :: a -> Strategy a -> a
x `using` s = runEval (s x)
x `using` s == x
sparMap = algorithm + strategyparMap = map + strategyparMap = algorithm + strategy
parMap = map + strategy
parMap f xs = map f xs `using` strategy
strategy ?strategy = Evaluate List Elements in ParallelparMap = algorithm + strategy
parMap = map + strategy
parMap f xs = map f xs `using` strategy
strategy ?a[a]parList :: Strategy a -> Strategy [a]
parList xstrat [] = return []
parList strat (x:xs) = do x' <- rpar (x `using` xstrat) -- >
xs' <- parList xstrat xs
return (x' : xs')
parMap = algorithm + strategy
parMap = map + strategy
parMap f xs = map f xs `using` parList rseq
rseq but list in parallelstrategy ?a[a]parList :: Strategy a -> Strategy [a]
Lets use parList to solve sudoku in parallel sudoku4.hs
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
evaluate $ deep $ runEval $ map solve grids `using` parList rseq
Note: Entire parallelism secret sauce reduced to using parList rseq
$ ghc -O2 sudoku4.hs -rtsopts
$ ./sudoku4 sudoku17.1000.txt +RTS -N10 -s
SPARKS: 1000 (1000 converted, 0 overflowed, 0 dud, 0 GC'd, 0 fizzled)
Total time 5.16s ( 0.46s elapsed)
Problem: Given a set of points, group into K clusters.
Problem: Given a set of points, group into K clusters.
Centers chosen arbitrarily
Problem: Given a set of points, group into K clusters.
Problem: Given a set of points, group into K clusters.
data Vector = Vector Double Double
zeroVector :: Vector
zeroVector = Vector 0 0
addVector :: Vector -> Vector -> Vector
addVector (Vector a b) (Vector c d) = Vector (a+c) (b+d)
sqDistance :: Vector -> Vector -> Double
sqDistance (Vector x1 y1) (Vector x2 y2) = ((x1-x2)^2) + ((y1-y2)^2)
data Cluster = Cluster { clId :: Int -- ID of cluster 0..K
, clCount :: Int -- NUMBER of points in cluster
, clSum :: Vector -- SUM of points in cluster
, clCent :: Vector -- CENTER of cluster
}
makeCluster :: Int -> [Vector] -> Cluster
makeCluster clid vecs = Cluster { clId = clid
, clCount = count
, clSum = vecsum
, clCent = centre }
where
vecsum@(Vector a b) = foldl' addVector zeroVector vecs
centre = Vector (a / fromIntegral count) (b / fromIntegral count)
count = length vecs
combineClusters c1 c2 =
Cluster {clId = clId c1,
clCount = count,
clSum = vecsum,
clCent = Vector (a / fromIntegral count) (b / fromIntegral count)}
where count = clCount c1 + clCount c2
vecsum@(Vector a b) = addVector (clSum c1) (clSum c2)
kmeans_seq nclusters points initClusters = do
let
loop n clusters | n > tooMany = do printf "giving up."; return clusters
loop n clusters = do
let clusters' = step nclusters clusters points
if clusters' == clusters
then return clusters
else loop (n+1) clusters'
--
loop 0 initClusters
Each step is
step :: Int -> [Cluster] -> [Vector] -> [Cluster]
step nclusters clusters points = makeNewClusters (assign nclusters clusters points)
makeNewClusters :: Array Int [Vector] -> [Cluster]
makeNewClusters arr = [ makeCluster i ps | (i, ps) <- assocs arr, not (null ps) ]
Lets run it!
$ ghc -O2 -threaded -rtsopts -eventlog kmeans.hs
$ ./kmeans seq
...
Total time: 0.65
Note Program prints out its own time to ignore sequential IO
assign each point to cluster in parallelIdea assign each point to cluster in parallel
Problem: Not enough work for a spark ...
points in chunks ...chunks in parallelkmeans_strat :: Int -> Int -> [Vector] -> [Cluster] -> IO [Cluster]
kmeans_strat numChunks nclusters points clusters = do
let chunks = split numChunks points
let
loop n clusters | n > tooMany = do printf "giving up."; return clusters
loop n clusters = do
let new_clusterss = map (step nclusters clusters) chunks
`using` parList rdeepseq
clusters' = reduce nclusters new_clusterss
if clusters' == clusters
then return clusters
else loop (n+1) clusters'
--
loop 0 clusters
chunksmap the step over each chunkreduce the resulting new_clusterss from chunks into single clusters'Note Can just drop the
usingand get sequential code
reduce cluster-lists by combine-ing clusters
reduce :: Int -> [[Cluster]] -> [Cluster]
reduce nclusters css
= concatMap combine -- 4. combine all sub-clusters of each ID
$ elems -- 3. flatten into [[Cluster]], each inner list same ID
$ accumArray (flip (:)) [] (0,nclusters) -- 2. gather into Array ID [Cluster]
$ [ (clId c, c) | c <- concat css] -- 1. group all clusters by ID
where
combine [] = []
combine (c:cs) = [foldr combineClusters c cs]
Lets run it!
$ ./kmeans seq
...
Total time: 0.65
$ ./kmeans strat 800 +RTS -N2
Total time: 0.67
$ ./kmeans strat 800 +RTS -N3
Total time: 0.34
$ ./kmeans strat 800 +RTS -N4
Total time: 0.28
$ ./kmeans strat 800 +RTS -N6
Total time: 0.16
$ ./kmeans strat 800 +RTS -N8
Total time: 0.14
Eval has implicit data dependenciesStrategy hints for evaluationAlternative: Explicit dataflow parallelism
Strategy)The Par Monad
newtype Par a
instance Monad Par
Explicitly Force Task Creation
fork :: Par () -> Par ()
fork e forces e to be computed in a forked child taskTasks Communicate Via Shared Variables
data IVar a -- reference to `a` cell
a in future ...Tasks Communicate Via Shared Variables
data IVar a -- reference to `a` cell
a in future ...Operations on Shared Variables
new :: Par (IVar a) -- create
put :: NFData a => IVar a -> a -> Par () -- write-once
get :: IVar a -> Par a -- blocking read
NFData = fully-evaluated data stored in shared variablesTo Actually Execute the Action
runPar :: Par a -> a
See sudoku-par2.hs
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
let (as,bs) = splitAt (length grids `div` 2) grids
print $ length $ filter isJust $ runPar $ do
i1 <- new -- (1)
i2 <- new
fork $ put i1 (map solve as) -- (2)
fork $ put i2 (map solve bs)
as' <- get i1 -- (3)
bs' <- get i2
return (as' ++ bs')
IVar for each halfspawnspawn :: NFData a => Par a -> Par (IVar a)
spawn p = do i <- new -- > Create IVar
fork (do x <- p; put i x) -- > Fork process to write IVar
return i -- > Return IVar
spawn forks a computation in parallel...
Returns an IVar on which to wait for result
spawnmain = do
[f] <- getArgs
grids <- fmap lines $ readFile f
let (as,bs) = splitAt (length grids `div` 2) grids
print $ length $ filter isJust $ runPar $ do
i1 <- spawn (return (map solve as))
i2 <- spawn (return (map solve bs))
as' <- get i1
bs' <- get i2
return (as' ++ bs')
Run it! (Same as sudoku2 or sudoku-par2)
./sudoku-par2-spawn sudoku17.1000.txt +RTS -N2 -s
Total time 2.44s ( 1.46s elapsed)
parMapHow would we execute a list of tasks in parallel?
parMapHow would we execute a list of tasks in parallel?
Given:
f :: (a -> b)
[x1, ..., xn] :: [a]
Par [b]parMapHow would we execute a list of tasks in parallel?
parMap :: (NFData b) => (a -> b) -> [a] -> Par [b]
parMap f xs = do ivs <- mapM (spawn . return . f) -- >
mapM get ivs
spawn each f x in list in parallel ...parMapSee sudoku-par3.hs
main :: IO ()
main = do
[f] <- getArgs
grids <- fmap lines $ readFile f
print (length (filter isJust (runPar $ parMap solve grids)))
Run it!
./sudoku-par3 sudoku17.1000.txt +RTS -N10 -s
Total time 3.38s ( 0.38s elapsed)