Presentation is loading. Please wait.

Presentation is loading. Please wait.

Futures, Scheduling, and Work Distribution

Similar presentations


Presentation on theme: "Futures, Scheduling, and Work Distribution"— Presentation transcript:

1 Futures, Scheduling, and Work Distribution
The Art of Multiprocessor Programming – Chapter 16 Dor Katzelnick 14/12/2017 Based on Companion slides for The Art of Multiprocessor Programming by Maurice Herlihy & Nir Shavit

2 How to write Parallel Apps?
We will talk about How to split a program into parallel parts Execute them Efficiently In this chapter we how to decompose certain kinds of problems into components that can be executed in parallel. Some applications break down naturally into parallel threads. For example, when a request arrives at a web server, the server can just create a thread (or assign an existing thread) to handle the request. Applications that can be structured as producers and consumers also tend to be easily parallelizable. In this chapter, however, we look at applications that have inherent parallelism, but where it is not obvious how to take advantage of it. Art of Multiprocessor Programming© Herlihy –Shavit 2007

3 Matrix Multiplication
To take another specific example, let’s look at matrix multiplication, one of the most common and important applications in scientific computing. Art of Multiprocessor Programming© Herlihy –Shavit 2007

4 Matrix Multiplication
cij = k=0N-1 aki * bjk Let us start by by thinking about how to multiply two matrices in parallel. Recall that if $a_{ij}$ is the value at position $(i,j)$ of matrix $A$, then the product $C$ of two $n \times n$ matrices $A$ and $B$ is given by this formula. Art of Multiprocessor Programming© Herlihy –Shavit 2007

5 Matrix Multiplication
class Worker extends Thread { int row, col; Worker(int row, int col) { this.row = row; this.col = col; } public void run() { double dotProduct = 0.0; for (int i = 0; i < n; i++) dotProduct += a[row][i] * b[i][col]; c[row][col] = dotProduct; }}} Here is a worker thread. The worker in position $(i,j)$ computes $c_{ij}$. Art of Multiprocessor Programming© Herlihy –Shavit 2007

6 Matrix Multiplication
class Worker extends Thread { int row, col; Worker(int row, int col) { this.row = row; this.col = col; } public void run() { double dotProduct = 0.0; for (int i = 0; i < n; i++) dotProduct += a[row][i] * b[i][col]; c[row][col] = dotProduct; }}} a thread A worker is a thread, meaning that it runs concurrently with other threads. Art of Multiprocessor Programming© Herlihy –Shavit 2007

7 Matrix Multiplication
class Worker extends Thread { int row, col; Worker(int row, int col) { this.row = row; this.col = col; } public void run() { double dotProduct = 0.0; for (int i = 0; i < n; i++) dotProduct += a[row][i] * b[i][col]; c[row][col] = dotProduct; }}} Which matrix entry to compute Each worker thread is given the coordinates of the term it computes. Art of Multiprocessor Programming© Herlihy –Shavit 2007

8 Matrix Multiplication
class Worker extends Thread { int row, col; Worker(int row, int col) { this.row = row; this.col = col; } public void run() { double dotProduct = 0.0; for (int i = 0; i < n; i++) dotProduct += a[row][i] * b[i][col]; c[row][col] = dotProduct; }}} Actual computation Here is the actual computation. Art of Multiprocessor Programming© Herlihy –Shavit 2007

9 Matrix Multiplication
void multiply() { Worker[][] worker = new Worker[n][n]; for (int row …) for (int col …) worker[row][col] = new Worker(row,col); worker[row][col].start(); worker[row][col].join(); } Here is the driver code Art of Multiprocessor Programming© Herlihy –Shavit 2007

10 Matrix Multiplication
void multiply() { Worker[][] worker = new Worker[n][n]; for (int row …) for (int col …) worker[row][col] = new Worker(row,col); worker[row][col].start(); worker[row][col].join(); } First we create an n-by-n array of threads … Create nxn threads Art of Multiprocessor Programming© Herlihy –Shavit 2007

11 Matrix Multiplication
void multiply() { Worker[][] worker = new Worker[n][n]; for (int row …) for (int col …) worker[row][col] = new Worker(row,col); worker[row][col].start(); worker[row][col].join(); } Start them Then we start each of the threads … Art of Multiprocessor Programming© Herlihy –Shavit 2007

12 Matrix Multiplication
void multiply() { Worker[][] worker = new Worker[n][n]; for (int row …) for (int col …) worker[row][col] = new Worker(row,col); worker[row][col].start(); worker[row][col].join(); } Start them Then we wait for them to finish. When they’re done, so are we. Wait for them to finish Art of Multiprocessor Programming© Herlihy –Shavit 2007

13 Matrix Multiplication
void multiply() { Worker[][] worker = new Worker[n][n]; for (int row …) for (int col …) worker[row][col] = new Worker(row,col); worker[row][col].start(); worker[row][col].join(); } Start them What’s wrong with this picture? In principle, this might seem like an ideal design. The program is highly parallel, and the threads do not even have to synchronize. Nevertheless, this is actually a very bad design. Why? Wait for them to finish Art of Multiprocessor Programming© Herlihy –Shavit 2007

14 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Thread Overhead Threads Require resources Memory for stacks Setup, teardown Scheduler overhead Worse for short-lived threads In practice, however, while this design might perform for small matrices, it would perform very poorly for matrices large enough to be interesting. Here is why: threads require memory for stacks and other bookkeeping information Creating, scheduling, and destroying threads takes a substantial amount of computation. Creating lots of short-lived threads is an inefficient way to organize a multi-threaded computation. Art of Multiprocessor Programming© Herlihy –Shavit 2007

15 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Thread Pools More sensible to keep a pool of long-lived threads Threads assigned short-lived tasks Runs the task Rejoins pool Waits for next assignment A more effective way to organize such a program is to create a pool of long-lived threads. Each thread in the pool repeatedly waits until it is assigned a task, a short-lived unit of computation. When a thread is assigned a task, it executes that task, and then rejoins the pool to await its next assignment. Thread pools can be platform-dependent: it makes sense for large-scale multiprocessors to provide large pools, and vice-versa. Thread pools avoid the cost of creating and destroying threads in response to short-lived fluctuations in demand. Art of Multiprocessor Programming© Herlihy –Shavit 2007

16 Thread Pool = Abstraction
Insulate programmer from platform Big machine, big pool And vice-versa Portable code Runs well on any platform No need to mix algorithm/platform concerns In addition to performance benefits, thread pools have another equally important, but less obvious advantage: they insulate the application programmer from platform-specific details such as the number of concurrent threads that can be scheduled efficiently. Thread pools make it possible to write a single program that runs equally well on a uniprocessor, a small-scale multiprocessor, and a large-scale multiprocessor. Thread pools provide a simple interface that hides complex, platform-dependent engineering trade-offs. Art of Multiprocessor Programming© Herlihy –Shavit 2007

17 ExecutorService Interface
In java.util.concurrent Task = Runnable object If no result value expected Calls run() method. Task = Callable<T> object If result value of type T expected Calls T call() method. In Java, a thread pool is called an executor service. It provides the ability to submit a task, to wait for a set of submitted tasks to complete, and to cancel uncompleted tasks. A task that does not return a result is usually represented as a Runnable object, where the work is performed by a run{} method that takes no arguments and returns no results. A task that returns a value of type T is usually represented as a callable<T> object, where the result is returned by a T call() method that takes no arguments. Art of Multiprocessor Programming© Herlihy –Shavit 2007

18 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Future<T> Callable<T> task = …; Future<T> future = executor.submit(task); T value = future.get(); Here is how to use an executor service to compute a value asynchronously. Art of Multiprocessor Programming© Herlihy –Shavit 2007

19 Future<T> Callable<T> task = …; Future<T> future = executor.submit(task); T value = future.get(); When a Callable<T> object is submitted to an executor service, the service returns an object implementing the Future<T> interface. Submitting a Callable<T> task returns a Future<T> object Art of Multiprocessor Programming© Herlihy –Shavit 2007

20 Future<T> Callable<T> task = …; Future<T> future = executor.submit(task); T value = future.get(); The Future provides a get() method that returns the result, blocking if necessary until the result is ready. (It also provides methods for canceling uncompleted computations, and for testing whether the computation is complete.) The Future’s get() method blocks until the value is available Art of Multiprocessor Programming© Herlihy –Shavit 2007

21 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Future<?> Runnable task = …; Future<?> future = executor.submit(task); future.get(); What if we want to run an asynchronous computation that does not return a value? Art of Multiprocessor Programming© Herlihy –Shavit 2007

22 Future<?> Runnable task = …; Future<?> future = executor.submit(task); future.get(); Submitting a Runnable task also returns a future. Unlike the future returned for a Callable<T> object, this future does not return a value. A future that does not return an interesting value is declared to have class Future<?>. Submitting a Runnable task returns a Future<?> object Art of Multiprocessor Programming© Herlihy –Shavit 2007

23 Future<?> Runnable task = …; Future<?> future = executor.submit(task); future.get(); What if we want to run an asynchronous computation that does not return a value? The Future’s get() method blocks until the computation is complete Art of Multiprocessor Programming© Herlihy –Shavit 2007

24 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Note Executor Service submissions Are only suggestions, could run in parallel But the executor May execute tasks sequentially … The important thing to understand is that spawn and sync are suggestions to the OS, not commands. In fact, we do not have direct control over how the OS does scheduling. What we are saying is “if there are n processors, then here is work they could do in parallel” Art of Multiprocessor Programming© Herlihy –Shavit 2007

25 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition For now, let’s look at the details of how to do matrix addition in parallel. Matrix multiplication, which is far more interesting, is covered in the text. Art of Multiprocessor Programming© Herlihy –Shavit 2007

26 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition For now, let’s look at the details of how to do matrix addition in parallel. Matrix multiplication, which is far more interesting, is covered in the text. 4 parallel additions Art of Multiprocessor Programming© Herlihy –Shavit 2007

27 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition Task class AddTask implements Runnable { Matrix a, b; // multiply this! public void run() { if (a.dim == 1) { c[0][0] = a[0][0] + b[0][0]; // base case } else { (partition a, b into half-size matrices aij and bij) Future<?> f00 = exec.submit(add(a00,b00)); Future<?> f11 = exec.submit(add(a11,b11)); f00.get(); …; f11.get(); }} Let’s look at how one would do matrix addition using a thread pool. The above is not actual working Java code, and the reader should look at the textbook for the detailed working code. Not real Java code Art of Multiprocessor Programming© Herlihy –Shavit 2007

28 Matrix Addition Task Base case: add directly
class AddTask implements Runnable { Matrix a, b; // multiply this! public void run() { if (a.dim == 1) { c[0][0] = a[0][0] + b[0][0]; // base case } else { (partition a, b into half-size matrices aij and bij) Future<?> f00 = exec.submit(add(a00,b00)); Future<?> f11 = exec.submit(add(a11,b11)); f00.get(); …; f11.get(); }} The matrix addition program is recursive. When we get to the bottom, do the additions directly. Here we show the recursion bottoming out at dimension 1. In practice, it makes sense to bottom out at a larger number. The code here is a simplified version of the code in the textbook that shows the details of how the split is performed. Base case: add directly Art of Multiprocessor Programming© Herlihy –Shavit 2007

29 Matrix Addition Task Constant-time operation
class AddTask implements Runnable { Matrix a, b; // multiply this! public void run() { if (a.dim == 1) { c[0][0] = a[0][0] + b[0][0]; // base case } else { (partition a, b into half-size matrices aij and bij) Future<?> f00 = exec.submit(add(a00,b00)); Future<?> f11 = exec.submit(add(a11,b11)); f00.get(); …; f11.get(); }} We can split a matrix into half-size matrices in constant time by manipulating indexes and offsets. Constant-time operation Art of Multiprocessor Programming© Herlihy –Shavit 2007

30 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition Task class AddTask implements Runnable { Matrix a, b; // multiply this! public void run() { if (a.dim == 1) { c[0][0] = a[0][0] + b[0][0]; // base case } else { (partition a, b into half-size matrices aij and bij) Future<?> f00 = exec.submit(add(a00,b00)); Future<?> f11 = exec.submit(add(a11,b11)); f00.get(); …; f11.get(); }} Each recursive call returns a Future<?>, meaning that we call it for its side-effects, not its return value. Submit 4 tasks Art of Multiprocessor Programming© Herlihy –Shavit 2007

31 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition Task class AddTask implements Runnable { Matrix a, b; // multiply this! public void run() { if (a.dim == 1) { c[0][0] = a[0][0] + b[0][0]; // base case } else { (partition a, b into half-size matrices aij and bij) Future<?> f00 = exec.submit(add(a00,b00)); Future<?> f11 = exec.submit(add(a11,b11)); f00.get(); …; f11.get(); }} After launching all of these parallel tasks, we now evaluate each of the futures, meaning that we wait until the tasks are complete. Let them finish Art of Multiprocessor Programming© Herlihy –Shavit 2007

32 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Dependencies Matrix example is not typical Tasks are independent Don’t need results of one task … To complete another Often tasks are not independent The matrix example uses futures only to signal when a task is complete. Futures can also be used to pass values from completed tasks. To illustrate this use of futures, we consider how to decompose the well-known Fibonacci function into a multithreaded program. Art of Multiprocessor Programming© Herlihy –Shavit 2007

33 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Fibonacci 1 if n = 0 or 1 F(n) F(n-1) + F(n-2) otherwise Note potential parallelism Dependencies To illustrate another use of futures, we consider how to decompose the well-known Fibonacci function into a multithreaded program. Art of Multiprocessor Programming© Herlihy –Shavit 2007

34 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Disclaimer This Fibonacci implementation is Egregiously inefficient So don’t deploy it! But illustrates our point How to deal with dependencies Art of Multiprocessor Programming© Herlihy –Shavit 2007

35 Multithreaded Fibonacci
class FibTask implements Callable<Integer> { static ExecutorService exec = Executors.newCachedThreadPool(); int arg; public FibTask(int n) { arg = n; } public Integer call() { if (arg > 2) { Future<Integer> left = exec.submit(new FibTask(arg-1)); Future<Integer> right = exec.submit(new FibTask(arg-2)); return left.get() + right.get(); } else { return 1; }}} Here is the code for a simple Fibonacci program. Of course, this is not actually an effective way to compute Fibonacci numbers, but it is a nice simple example. Art of Multiprocessor Programming© Herlihy –Shavit 2007

36 Multithreaded Fibonacci
class FibTask implements Callable<Integer> { static ExecutorService exec = Executors.newCachedThreadPool(); int arg; public FibTask(int n) { arg = n; } public Integer call() { if (arg > 2) { Future<Integer> left = exec.submit(new FibTask(arg-1)); Future<Integer> right = exec.submit(new FibTask(arg-2)); return left.get() + right.get(); } else { return 1; }}} Parallel calls Here is the code for a simple Fibonacci program. Of course, this is not actually an effective way to compute Fibonacci numbers, but it is a nice simple example. Art of Multiprocessor Programming© Herlihy –Shavit 2007

37 Multithreaded Fibonacci
class FibTask implements Callable<Integer> { static ExecutorService exec = Executors.newCachedThreadPool(); int arg; public FibTask(int n) { arg = n; } public Integer call() { if (arg > 2) { Future<Integer> left = exec.submit(new FibTask(arg-1)); Future<Integer> right = exec.submit(new FibTask(arg-2)); return left.get() + right.get(); } else { return 1; }}} Pick up & combine results Here is the code for a simple Fibonacci program. Of course, this is not actually an effective way to compute Fibonacci numbers, but it is a nice simple example. Art of Multiprocessor Programming© Herlihy –Shavit 2007

38 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Dynamic Behavior Multithreaded program is A directed acyclic graph (DAG) That unfolds dynamically Each node is A single unit of work Formally, we can model a multithreaded program as a directed acyclic graph, where each node in the graph is a step of the program. A thread in this model is just a maximal sequence of instructions that doesn’t include spawn, synch, or return statements. Art of Multiprocessor Programming© Herlihy –Shavit 2007

39 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Fib DAG fib(4) fib(3) fib(2) submit get fib(1) Here is the DAG for the Fib program we saw earlier. Each circle is a thread. Each black horizontal arrow is a sequential dependency (the code says to do this before that). The red downward arrows represent spawn calls, and the green upward arrows indicate where the child thread rejoins the parent Art of Multiprocessor Programming© Herlihy –Shavit 2007

40 Arrows Reflect Dependencies
fib(4) get submit fib(3) fib(2) fib(2) fib(1) Here we can see a little bit of how the DAG unfolds in time. Art of Multiprocessor Programming© Herlihy –Shavit 2007

41 Art of Multiprocessor Programming© Herlihy –Shavit 2007
How Parallel is That? Define work: Total time on one processor Define critical-path length: Longest dependency path Can’t beat that! Art of Multiprocessor Programming© Herlihy –Shavit 2007

42 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Fib Work fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) Art of Multiprocessor Programming© Herlihy –Shavit 2007

43 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Fib Work 1 2 3 4 5 6 7 8 9 How much work does Fib(4) take? This is just a matter of counting the steps. 10 11 12 13 14 15 work is 17 16 17 Art of Multiprocessor Programming© Herlihy –Shavit 2007

44 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Fib Critical Path fib(4) What is the critical path for Fib(4)? We just have to find a path of maximal length. Art of Multiprocessor Programming© Herlihy –Shavit 2007

45 Fib Critical Path Critical path length is 8 fib(4) 1 8 2 7 3 4 6 5
It is not hard to see that the longest path length in this DAG has length 8. It is not unique. 3 4 6 Critical path length is 8 5 Art of Multiprocessor Programming© Herlihy –Shavit 2007

46 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Simple Notations TP = time on P processors T1 = work (time on 1 processor) T∞ = critical path length (time on ∞ processors) It is convenient to define the following notion. Let TP denote the time needed to execute a particular program on P processors (which program will be clear from context). Two special values are of particular interest. The work, T1, is the time it would take on a single processor, with no parallelism. The critical path length, T∞ , is the time it would take on an unbounded number of processors. No matter how many resources we have or how rich we are, the critical path length limits how quickly we can execute this program. Art of Multiprocessor Programming© Herlihy –Shavit 2007

47 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Simple Bounds TP ≥ T1/P In one step, can’t do more than P work TP ≥ T∞ Can’t beat infinite resources Here are a few simple observations. First, buying P processors isn’t going to speed up your application by more than P. Another way to think about this is that P processors can do at most P work in one time step. Second, the time to execute a program on P processors cannot be less than the time to execute the program on an unbounded number of processors. Art of Multiprocessor Programming© Herlihy –Shavit 2007

48 Art of Multiprocessor Programming© Herlihy –Shavit 2007
More Notations Speedup on P processors Ratio T1/TP How much faster with P processors Linear speedup T1/TP = Θ(P) Max speedup (average parallelism) T1/T∞ The speedup on P processors is just the ratio between how long it takes with one processor and how long it takes with P. Programs with long critical paths will have poorer speedups than programs with shorter critical paths. We say a program has linear speedup if the speedup is proportional to the number of processors. This is the best we can do. Sometimes you hear people talk of “superlinear speedup”, where using P processes speeds up a computation by a factor of more than P. This phenomenon happens some times with parallel searches where the first processor to find a match halts the computation. The maximum possible speedup is the ratio between the work and the critical path length. This quantity is often called the average parallelism, or just the parallelism. Art of Multiprocessor Programming© Herlihy –Shavit 2007

49 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition For now, let’s look at the details of how to do matrix addition in parallel. Matrix multiplication, which is far more interesting, is covered in the text. Art of Multiprocessor Programming© Herlihy –Shavit 2007

50 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Matrix Addition For now, let’s look at the details of how to do matrix addition in parallel. Matrix multiplication, which is far more interesting, is covered in the text. 4 parallel additions Art of Multiprocessor Programming© Herlihy –Shavit 2007

51 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Addition Let AP(n) be running time For n x n matrix on P processors For example A1(n) is work A∞(n) is critical path length Let’s see if we can analyze the running time for adding two matrices in this model. Define Ap(n) to be the time needed to add two n X n matrices on P processes. Two special cases of interest are A1(n), the overall work needed to do the addition, and A∞(n), the critical path length. Art of Multiprocessor Programming© Herlihy –Shavit 2007

52 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Addition Work is A1(n) = 4 A1(n/2) + Θ(1) Partition, synch, etc 4 spawned additions The work is easy to compute. To add two n X n matrices, you need to add 4 half-size matrices, plus some change. The change is just the cost of partitioning the matrix, synchronizing, etc., which is constant. Art of Multiprocessor Programming© Herlihy –Shavit 2007

53 Addition Same as double-loop summation Work is
A1(n) = 4 A1(n/2) + Θ(1) = Θ(n2) You can solve this recurrence using standard methods A1(n) = 4 A1(n/2) + Θ(1) = 4(4A1(n/4)+O(1)) + O(1) = 22logn+log n = n2+log n = O(n2) and it turns out that the overall work for adding two n X n matrices is about n2, same as the usual double-loop summation, which is reassuring, because anything much better or worse would be alarming. (Why?) Same as double-loop summation Art of Multiprocessor Programming© Herlihy –Shavit 2007

54 Addition Critical Path length is A∞(n) = A∞(n/2) + Θ(1)
Partition, synch, etc Computing the critical path length is different from computing the work in one important respect: we can use all the parallelism we want. In this case, all the half-size additions are done in parallel, so we only count one half-size addition, not four. We still have the constant overhead because partition and so on are not done in parallel with the addition. spawned additions in parallel Art of Multiprocessor Programming© Herlihy –Shavit 2007

55 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Addition Critical Path length is A∞(n) = A∞(n/2) + Θ(1) = Θ(log n) Here too, if we solve the recurrence, we discover that the critical path length is quite short: logarithmic in n. This suggests that matrix addition has a high degree of inherent parallelism. Art of Multiprocessor Programming© Herlihy –Shavit 2007

56 Matrix Multiplication Redux
Let’s return to the problem of matrix multiplication. Art of Multiprocessor Programming© Herlihy –Shavit 2007

57 Matrix Multiplication Redux
We split each matrix into four sub-matrices, and express the problem in terms of the submatrices. Art of Multiprocessor Programming© Herlihy –Shavit 2007

58 Art of Multiprocessor Programming© Herlihy –Shavit 2007
First Phase … First, there are 8 multiplications, which can be done in parallel. 8 multiplications Art of Multiprocessor Programming© Herlihy –Shavit 2007

59 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Second Phase … Second, there are four additions, which can also be done in parallel, but not until the multiplications are complete. 4 additions Art of Multiprocessor Programming© Herlihy –Shavit 2007

60 Multiplication Work is M1(n) = 8 M1(n/2) + A1(n) Final addition
8 parallel multiplications Now let us turn our attention to matrix multiplication. The work required is straightforward: we need to do 8 half-size multiplications followed by one full-size addition. Because we are measuring work here, we do not care what is done in parallel. Art of Multiprocessor Programming© Herlihy –Shavit 2007

61 Multiplication Same as serial triple-nested loop Work is
M1(n) = 8 M1(n/2) + Θ(n2) = Θ(n3) When we solve the recurrence, it turns out that the total work is order of n cubed, which is the same work we would do in a straightforward, non-concurrent triply-nested loop. Same as serial triple-nested loop Art of Multiprocessor Programming© Herlihy –Shavit 2007

62 Multiplication Critical path length is M∞(n) = M∞(n/2) + A∞(n)
Final addition Half-size parallel multiplications The critical path length is also easy to compute. The 8 half-size multiplications are done in parallel, so we charge for the once. The final addition takes place after the multiplications are all complete, so we have to add that in as well. Art of Multiprocessor Programming© Herlihy –Shavit 2007

63 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Multiplication Critical path length is M∞(n) = M∞(n/2) + A∞(n) = M∞(n/2) + Θ(log n) = Θ(log2 n) Solving the recurrence shows that the critical path length is about log-squared n, which is pretty small. Art of Multiprocessor Programming© Herlihy –Shavit 2007

64 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Parallelism M1(n)/ M∞(n) = Θ(n3/log2 n) To multiply two 1000 x 1000 matrices 10003/102=107 Much more than number of processors on any real machine Now here is something interesting. Remember that the parallelism is the ratio between the problem’s work and its critical path. It is a rough measure of how may processors we can keep busy on this kind of problem. For matrix multiplication, the numerator is n-cubed, and the denominator is log-squared n, which is pretty large. For example, to multiply two 1000 X 1000 matrices we get a parallelism of about ten million. (Notice that the log is base-2, and log base-two of 1000 is about 10). This calculation suggests that we can keep roughly ten million processors busy multiplying matrices of this size. Of course, this vastly exceeds the number of processors you are likely to find on any real machine any time soon. This calculation also gives us an idea why computers for numeric computations like the earth simulator in Japan, that does meteorological calculations, have thousands of processors. There is a lot of parallelism to be used in the applications and the more processors the better. Art of Multiprocessor Programming© Herlihy –Shavit 2007

65 Shared-Memory Multiprocessors
Parallel applications Do not have direct access to HW processors Mix of other jobs All run together Come & go dynamically למעשה, הבעיה של זמינות של מעבדים היא אפילו יותר מורכבת. על מכונה אמיתית, צריך לחלוק את המעבדים עם עוד מספר של תוכניות שבאות והולכות בצורה דינמית, באופן לא צפוי. Art of Multiprocessor Programming© Herlihy –Shavit 2007

66 Ideal Scheduling Hierarchy
Tasks User-level scheduler Processors In an ideal world, we would map our application-level threads onto dedicated physical processors. In real life, however, we do not have much control over how the OS kernel schedules processors. Instead, a user-level scheduler maps threads to processes, and a kernel-level schedule (not under our control) maps processes to physical processors. We control the first level of the mapping, but not the second. Note that threads are typically short-lived while processes are typically long-lived. Art of Multiprocessor Programming© Herlihy –Shavit 2007

67 Realistic Scheduling Hierarchy
Tasks User-level scheduler Threads In an ideal world, we would map our application-level threads onto dedicated physical processors. In real life, however, we do not have much control over how the OS kernel schedules processors. Instead, a user-level scheduler maps threads to processes, and a kernel-level schedule (not under our control) maps processes to physical processors. We control the first level of the mapping, but not the second. Note that threads are typically short-lived while processes are typically long-lived. Kernel-level scheduler Processors Art of Multiprocessor Programming© Herlihy –Shavit 2007

68 Art of Multiprocessor Programming© Herlihy –Shavit 2007
For Example Initially, All P processors available for application Serial computation Takes over one processor Leaving P-1 for us Waits for I/O We get that processor back …. אנחנו תלויים בחסדיו של הקרנל For example, you might start out with all P processors working on your application, and then a serial computation takes over one processor, leaving you with P-1, and then the serial job suspends itself waiting for I/O and we get that processor back, and so on. The point is that we have no control over the number of processors we get, and indeed we can’t even predict what will happen. Art of Multiprocessor Programming© Herlihy –Shavit 2007

69 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Speedup Map threads onto P processes Cannot get P-fold speedup What if the kernel doesn’t cooperate? Can try for speedup proportional to time-averaged number of processors the kernel gives us Ideally, if we run our application onto P processes, we would like to get a P-fold speedup, but since we don’t control the kernel-level scheduler, we can’t make any absolute guarantees. Instead, the best we can do is to exploit as well as we can the processor time we actually get. Instead of trying for a P-fold speedup, can we try for a speedup proportional to the average number of processors we are assigned? Art of Multiprocessor Programming© Herlihy –Shavit 2007

70 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Scheduling Hierarchy User-level scheduler Tells kernel which threads are ready Kernel-level scheduler Synchronous (for analysis, not correctness!) Picks pi threads to schedule at step i Processor average over T steps is: So we end up with the following scheduling hierarchy. The user-level scheduler tells kernel which processes are ready. We model the kernel-level scheduler as a synchronous system, for ease of analysis. At time step i, the kernel makes pi processors available for processes. Of particular interest is the time-weighted average PA of the number of processes we get, which we call the processor average. Art of Multiprocessor Programming© Herlihy –Shavit 2007

71 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Greed is Good Greedy scheduler Schedules as much as it can At each time step Optimal schedule is greedy (why?) But not every greedy schedule is optimal Define a greedy scheduler to be one that schedules as many processes as it can at each time step. It is not hard to see that some greedy schedule is optimal (because any non-greedy schedule can be transformed into a greedy schedule that performs at least as well). You can also work out a simple example showing that not every greedy schedule is optimal. Art of Multiprocessor Programming© Herlihy –Shavit 2007

72 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Theorem Greedy scheduler ensures that T ≤ T1/PA + T∞(P-1)/PA Nevertheless, greedy schedules do pretty well, both in practice (easy to implement) and in analysis. We will show that any greedy schedule does at least as well as this formula. What does this formula say? Art of Multiprocessor Programming© Herlihy –Shavit 2007

73 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Deconstructing T ≤ T1/PA + T∞(P-1)/PA Let’s examine the components of this expression. Art of Multiprocessor Programming© Herlihy –Shavit 2007

74 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Deconstructing T ≤ T1/PA + T∞(P-1)/PA We are deriving a bound on the time for any greedy schedule. The bound may be way off, but we know that even if we are unlucky or inept enough to pick the worst possible greedy schedule, we won’t do worse than this. Actual time Art of Multiprocessor Programming© Herlihy –Shavit 2007

75 Deconstructing T ≤ T1/PA + T∞(P-1)/PA
One limit to how well we can do is the total work distributed among the processors we get. It turns out that we can express this part of the bound in terms of the average number of processors we get. Work divided by processor average Art of Multiprocessor Programming© Herlihy –Shavit 2007

76 Deconstructing T ≤ T1/PA + T∞(P-1)/PA
The critical path length is another limit on how well we can do. Remember that no amount of parallelism is going to speed up your critical path. Here the critical path length is multiplied by a “fudge factor” that reflects the ratio between the number of processes P and the actual processor average. Cannot do better than critical path length Art of Multiprocessor Programming© Herlihy –Shavit 2007

77 Deconstructing T ≤ T1/PA + T∞(P-1)/PA
The higher the average the better it is … Art of Multiprocessor Programming© Herlihy –Shavit 2007

78 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Proof Strategy Here is our proof strategy. The expression at the top is the definition of the processor average, the time-weighted average of the number of processors that the kernel assigned us. We can rewrite this equation to extract T, the time quantity we want to bound. Now the natural step is to bound the summation highlighted here, bearing in mind that the scheduler is greedy. Bound this! Art of Multiprocessor Programming© Herlihy –Shavit 2007

79 Put Tokens in Buckets Processor available but couldn’t find work
Processor found work A convenient way to think about this problem is to imagine that we have two buckets, a work bucket and an idle bucket. At each time step, we get one token for each processor the kernel gives us. If we have work for that processor, we but that token in the work bucket. If we do not have any work for that processor, we put a token in the idle bucket. Now, let’s count the tokens. work idle Art of Multiprocessor Programming© Herlihy –Shavit 2007

80 Art of Multiprocessor Programming© Herlihy –Shavit 2007
At the end …. Total #tokens = The total number of tokens is just the total number of processor steps, given by the summation at the top. work idle Art of Multiprocessor Programming© Herlihy –Shavit 2007

81 Art of Multiprocessor Programming© Herlihy –Shavit 2007
At the end …. T1 tokens The number of tokens in the work bucket is T1, the application’s total work. How many tokens are in the idle bucket?: work idle Art of Multiprocessor Programming© Herlihy –Shavit 2007

82 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Must Show ≤ T∞(P-1) tokens We claim that the number of tokens in the idle bucket cannot exceed this expression. work idle Art of Multiprocessor Programming© Herlihy –Shavit 2007

83 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Idle Steps An idle step is one where there is at least one idle processor Only time idle tokens are generated Focus on idle steps Art of Multiprocessor Programming© Herlihy –Shavit 2007

84 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Every Move You Make … Scheduler is greedy At least one node ready Number of idle threads in one idle step At most pi-1 ≤ P-1 How many idle steps? Because the application is still running, at least one node is ready for execution. Because the scheduler is greedy, at least one node is actually executed, so at least one processor is not idle, and the number of idle threads is at most pi-1 which is bounded by P-1. Art of Multiprocessor Programming© Herlihy –Shavit 2007

85 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Unexecuted sub-DAG fib(4) get submit fib(3) fib(2) fib(2) fib(1) fib(1) fib(1) At any step, the unexecuted part of the program is still a DAG (why: you can’t execute a step if it depends on something else). fib(1) fib(1) Art of Multiprocessor Programming© Herlihy –Shavit 2007

86 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Unexecuted sub-DAG fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(1) Consider the longest path in that DAG (the critical path of the unexecuted part). This critical path is clearly going to limit how quickly we can finish the program. fib(1) fib(1) Longest path Art of Multiprocessor Programming© Herlihy –Shavit 2007

87 Last node ready to execute
Unexecuted sub-DAG fib(4) fib(3) fib(2) fib(2) fib(1) fib(1) fib(1) We know that the last node in the path is ready to execute (why: it doesn’t depend on anything by definition). Moreover, we know this is an idle step, so there’s not enough work for all the processors. But the scheduler is greedy, so we must have scheduled that last node in this step. As a result, every idle round shortens this critical path by one node (other steps may shorten it too, but we don’t count that). Last node ready to execute fib(1) fib(1) Art of Multiprocessor Programming© Herlihy –Shavit 2007

88 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Every Step You Take … Consider longest path in unexecuted sub-DAG at step i At least one node in path ready Length of path shrinks by at least one at each step Initially, path is T∞ So there are at most T∞ idle steps Putting these arguments together, the critical path shrinks by one in each idle step, the critical path starts out with length T∞, so there are at most that many idle steps. Art of Multiprocessor Programming© Herlihy –Shavit 2007

89 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Counting Tokens At most P-1 idle threads per step At most T∞ steps So idle bucket contains at most T∞(P-1) tokens Both buckets contain T1 + T∞(P-1) tokens Each idle step contributes at most P-1 tokens to the idle bucket, and there are at most T∞ idle steps, so the idle bucket contains at most T∞(P-1) tokens. Together both buckets contain T1 + T∞(P-1) tokens. Art of Multiprocessor Programming© Herlihy –Shavit 2007

90 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Recapitulating Let’s recapitulate our argument. We started out with this expression for the application’s execution time. We then bounded the sum of the pi using the tokens argument to get the nextbound. Putting them back together, we get the final expression, which is the one we wanted. Art of Multiprocessor Programming© Herlihy –Shavit 2007

91 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Turns Out This bound is within a factor of 2 of optimal Actual optimal is NP-complete It turns out that greedy scheduling works pretty well, because it is within a factor of 2 of optimal. We don’t bother searching for the optimal schedule because the problem is NP-complete. Art of Multiprocessor Programming© Herlihy –Shavit 2007

92 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Work Distribution zzz… We now understand that the key to achieving a good speed-up is to keep user-level threads supplied with tasks, so that the resulting schedule is as greedy as possible. Multithreaded computations, however, create and destroy tasks dynamically, sometimes in unpredictable ways. A work distribution algorithm (sometimes called a load-sharing) algorithm is needed to assign ready tasks to idle threads as efficiently as possible. Art of Multiprocessor Programming© Herlihy –Shavit 2007

93 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Work Dealing In work dealing, an overloaded thread tries to deal out surplus tasks on others. What’s wrong with this approach? Yes! Art of Multiprocessor Programming© Herlihy –Shavit 2007

94 The Problem with Work Dealing
D’oh! D’oh! The problem with work-dealing is that if all threads have work, then they will spend too much time trying to offload tasks on one another. D’oh! Art of Multiprocessor Programming© Herlihy –Shavit 2007

95 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Work Stealing No work… zzz Yes! In work stealing, a thread that runs out of work will try to ``steal'' work from others. This approach has the advantage that no coordination is necessary if all tasks are overloaded. Art of Multiprocessor Programming© Herlihy –Shavit 2007

96 Lock-Free Work Stealing
Each thread has a pool of ready work Remove work without synchronizing If you run out of work, steal someone else’s Choose victim at random Here we are going to look at a simple lock-free work stealing algorithm. Each process keeps a pool of threads to work on. It can remove a thread from its own pool without any expensive synchronization. If it runs out of threads, it picks a victim at random and tries to steal a thread. Stealing does require synchronization, not surprisingly. Art of Multiprocessor Programming© Herlihy –Shavit 2007

97 Local Work Pools Each work pool is a Double-Ended Queue
Our basic data structure is a collection of work pools implemented using double-ended queues, so-called because threads can be removed from either end of the queue. Art of Multiprocessor Programming© Herlihy –Shavit 2007

98 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Work DEQueue1 work Our basic data structure is a double-ended queue, so-called because threads can be removed from either end of the queue. The version we present here is the BDEQueue presented in the book due to Arora, Blumofe, and Plaxton. pushBottom popBottom 1. Double-Ended Queue Art of Multiprocessor Programming© Herlihy –Shavit 2007

99 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Obtain Work Obtain work Run thread until Blocks or terminates The queue’s owner removes a thread from the queue by calling the popBottom method. It then works on that thread for as long as it can, that is, until the thread blocks or finishes. popBottom Art of Multiprocessor Programming© Herlihy –Shavit 2007

100 Art of Multiprocessor Programming© Herlihy –Shavit 2007
New Work Unblock node Spawn node If the process creates a new thread, say by calling spawn, then it pushes one of the two threads (parent or child) back onto the queue using the pushBottom method. pushBottom Art of Multiprocessor Programming© Herlihy –Shavit 2007

101 Whatcha Gonna do When the Well Runs Dry?
@&%$!! Eventually, the process may run out of work in its double-ended queue. empty Art of Multiprocessor Programming© Herlihy –Shavit 2007

102 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Steal Work from Others Pick random guy’s DEQeueue Pick some other thread’s pool uniformly at random. Art of Multiprocessor Programming© Herlihy –Shavit 2007

103 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Steal this Thread! popTop The process then selects another process at random, and tries to steal a thread from it by calling the popTop() method. Art of Multiprocessor Programming© Herlihy –Shavit 2007

104 Thread DEQueue Methods pushBottom popBottom Never happen concurrently
popTop Never happen concurrently To summarize, the double-ended queue provides three methods. The pushBottom and popBottom methods work on the bottom end of the queue, and are never called concurrently (why: only called by owner). The popTop method removes a thread from the top of the queue, and it can be called concurrently by thieves. Thieves never push anything onto the queue. Art of Multiprocessor Programming© Herlihy –Shavit 2007

105 Thread DEQueue Methods pushBottom popBottom popTop
These most common – make them fast (minimize use of CAS) To summarize, the double-ended queue provides three methods. The pushBottom and popBottom methods work on the bottom end of the queue, and are never called concurrently (why: only called by owner). The popTop method removes a thread from the top of the queue, and it can be called concurrently by thieves. Thieves never push anything onto the queue. Art of Multiprocessor Programming© Herlihy –Shavit 2007

106 Thread DEQueue Methods pushBottom popBottom popTop
May happen concurrently ! To summarize, the double-ended queue provides three methods. The pushBottom and popBottom methods work on the bottom end of the queue, and are never called concurrently (why: only called by owner). The popTop method removes a thread from the top of the queue, and it can be called concurrently by thieves. Thieves never push anything onto the queue. Art of Multiprocessor Programming© Herlihy –Shavit 2007

107 Art of Multiprocessor Programming© Herlihy –Shavit 2007
Work Balancing 2 5 – 3 =2 In work shedding, an overloaded thread tries to offload surplus tasks on others. What’s wrong with this approach? 5 5 > 3 Art of Multiprocessor Programming© Herlihy –Shavit 2007

108 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} Art of Multiprocessor Programming© Herlihy –Shavit 2007

109 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} Keep running tasks Art of Multiprocessor Programming© Herlihy –Shavit 2007

110 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} With probability 1/|queue| Art of Multiprocessor Programming© Herlihy –Shavit 2007

111 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} Choose random victim Art of Multiprocessor Programming© Herlihy –Shavit 2007

112 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} Lock queues in canonical order Art of Multiprocessor Programming© Herlihy –Shavit 2007

113 Work-Balancing Thread
public void run() { int me = ThreadID.get(); while (true) { Runnable task = queue[me].deq(); if (task != null) task.run(); int size = queue[me].size(); if (random.nextInt(size+1) == size) { int victim = random.nextInt(queue.length); int min = …, max = …; synchronized (queue[min]) { synchronized (queue[max]) { balance(queue[min], queue[max]); }}}}} Rebalance queues Art of Multiprocessor Programming© Herlihy –Shavit 2007

114 Balancing Vs Stealing Can offload more than 1 task at a time
When one thread is overloaded, and the rest are out of tasks: Load will be balanced, relatively fast. In Work stealing, we will have contentions.

115 Summary Futures Realistic Multiprocessor Scheduling Work Distribution
Matrix Addition and Multiplication Fibonacci Series Realistic Multiprocessor Scheduling Greedy Scheduler is good enough Work Distribution Work dealing, Work Stealing and Work Balancing


Download ppt "Futures, Scheduling, and Work Distribution"

Similar presentations


Ads by Google