MPP Exercises
MPP Exercises
U S
IT
T H
Y
O F
H
G
E
R
D I U
N B
David Henty
1 Hello World
Since both Cirrus and ARCHER are clusters of shared-memory nodes (36 and 24 cores per node respec-
tively), it can be interesting to know if two MPI processes are on the same or different nodes. This is not
specified by MPI – it is a function of the job launcher program (i.e. mpiexec_mpt or aprun).
1. Use the function MPI_Get_processor_name() to get each rank to print out where it is run-
ning. What is the default distribution of processes across multiple nodes?
2. You have some control over how the processes are allocated to nodes via additional options, e.g.
-ppn for mpiexec_mpt or -N for aprun. Modify the arguments to the job launcher to check
that these behave as you expect.
2 Parallel calculation of π
1 N
π dx 1 X 1
Z
= ≈ 2
4 1 + x2 N i=1 i− 12
0 1+ N
where the answer becomes more accurate with increasing N . Iterations over i are independent so the
calculation can be parallelised.
For the following exercises you should set N = 840. This number is divisible by 2, 3, 4, 5, 6, 7 and 8
which is convenient when you parallelise the calculation!
1. Modify your Hello World program so that each process independently computes the value of π and
prints it to the screen. Check that the values are correct (each process should print the same value).
1
2. Now arrange for different processes to do the computation for different ranges of i. For example,
on two processes: rank 0 would do i = 1, 2, . . . , N2 ; rank 1 would do i = N2 + 1, N2 + 2, . . . , N .
Print the partial sums to the screen and check the values are correct by adding them up by hand.
3. Now we want to accumulate these partial sums by sending them to the master (rank 0) to add up:
• all processes (except the master) send their partial sum to the master
• the master receives the values from all the other processes, adding them to its own partial sum
You should use the MPI routines MPI_Ssend and MPI_Recv.
4. Use the function MPI_Wtime (see below) to record the time it takes to perform the calculation.
For a given value of N , does the time decrease as you increase the number of processes? Note that
to ensure that the calculation takes a sensible amount of time (e.g. more than a second) you will
probably have to perform the calculation of π several thousands of times.
5. Ensure your program works correctly if N is not an exact multiple of the number of processes P .
The MPI_Wtime() routine returns a double-precision floating-point number which represents elapsed
wall-clock time in seconds. The timer has no defined starting-point, so in order to time a piece of code,
two calls are needed and the difference should be taken between them.
There are a number of important considerations when timing a parallel program:
1. Due to system variability, it is not possible to accurately time any program that runs for a very
short time. A rule of thumb is that you cannot trust any measurement much less than one second.
2. To ensure a reasonable runtime, you will probably have to repeat the calculation many times within
a do/for loop. Make sure that you remove any print statements from within the loop, otherwise there
will be far too much output and you will simply be measuring the time taken to print to screen.
3. Due to the SPMD nature of MPI, each process will report a different time as they are all running
independently. A simple way to avoid confusion is to synchronise all processes when timing, e.g.
MPI_Barrier(MPI_COMM_WORLD); // Line up at the start line
tstart = MPI_Wtime(); // Fire the gun and start the clock
... // Code to be timed in here ...
MPI_Barrier(MPI_COMM_WORLD); // Wait for everyone to finish
tstop = MPI_WTime(); // Stop the clock
Note that the barrier is only needed to get consistent timings – it should not affect code correctness.
With synchronisation in place, all processes will record roughly the same time (the time of the
slowest process) and you only need to print it out on a single process (e.g. rank 0).
4. To get meaningful timings for more than a few processes you must run on the backend of morar
using qsub. If you run interactively then you will have more MPI processes than physical cores
and you will not see any speedup.
1. Write two versions of the code to sum the partial values: one where the master does explicit
receives from each of the P − 1 other processes in turn, the other where it issues P − 1 receives
each from any source (using wildcarding).
2. Print out the final value of π to its full precision (e.g.. 10 decimal places for single precision, or 20
for double). Do your two versions give exactly the same result as each other? Does each version
give exactly the same value every time you run it?
2
3. To fix any problems for the wildcard version, you can receive the values from all the processors
first, then add them up in a specific order afterwards. The master should declare a small array and
place the result from process i in position i in the array (or i + 1 for Fortran!). Once all the slots
are filled, the final value can be calculated. Does this fix the problem?
4. You have to repeat the entire calculation many times if you want to time the code. When you do
this, print out the value of π after the final repetition. Do both versions get a reasonable answer?
Can you spot what the problem might be for the wildcard version? Can you think of a way to fix
this using tags?
Size (bytes) # Iterations Total time (secs) Time per message Bandwidth (MB/s)
3 Ping Pong
1. Write a program in which two processes (say rank 0 and rank 1) repeatedly pass a message back
and forth. Use the synchronous mode MPI_Ssend to send the data. You should write your
program so that it operates correctly even when run on more than two processes, i.e. processes
with rank greater than one should simply do nothing. For simplicity, use a message that is an array
of integers. Remember that this is like a game of table-tennis:
2. Insert timing calls to measure the time taken by all the communications. You will need to time
many ping-pong iterations to get a reasonable elapsed time, especially for small message lengths.
3. Investigate how the time taken varies with the size of the message. You should fill in your results
in Table 1. What is the asymptotic bandwidth for large messages?
4. Plot a graph of time against message size to determine the latency (i.e. the time taken for a message
of zero length); plot a graph of the bandwidth to see how this varies with message size.
The bandwidth and latency are key characteristics of any parallel machine, so it is always instructive to
run this ping-pong code on any new computers you may get access to.
1. How do the ping-pong bandwidth and latency figures vary when you use buffered or standard
modes (MPI_Bsend and MPI_Send)?
Note: to send large messages with buffered sends you will have to supply MPI with additional
buffer space using MPI_Buffer_attach().
2. Write a program in which the master process sends the same message to all the other processes
in MPI_COMM_WORLD and then receives the message back from all of them. How does the time
taken vary with the size of the messages and with the number of processes?
3
Step 1 C Step 2 C+B
2 2
C B B A
D 3 1 B D+C 3 1 B+A
D A C D
0 0
A A+D
2 2
A D
B C
0 0
A+D+C A+D+C+B
1. Measure the time taken for a global sum and investigate how it varies with increasing P . Plot a
graph of time against P — does the ring method scale as you would expect?
2. Using these timings, estimate how long it takes to send each individual message between processes.
How does this result compare with the latency figures from the ping-pong exercise?
4
3. The MPI_Sendrecv call is designed to avoid deadlock by combining the separate send and
receive operations into a single routine. Write a new version of the global sum using this routine
and compare the time taken with the previous implementation. Which is faster?
4. Investigate the time taken when you use standard and buffered sends rather than synchronous mode
(using MPI_Bsend you do not even need to use the non-blocking form as it is guaranteed to be
asynchronous). Which is the fastest? By comparing to the time taken by the combined send and
receive operation, can you guess how MPI_Sendrecv is actually being implemented?
5 Collective communications
1. Re-write the ring example using an MPI reduction operation to perform the global sum.
2. How does the execution time vary with P and how does this compare to your own implementation
which used the ring method?
1. Compare the performance of a single reduction of an array of N values compared with N separate
calls to reductions of a single value. Can you explain the results (the latency and bandwidth values
from the pingpong code are useful to know)?
2. You can ensure all processes get the answer of a reduction by doing MPI_Reduce followed by
MPI_Bcast, or using MPI_Allreduce. Compare the performance of these two methods.
3. Imagine you want all the processes to write their output, in order, to a single file. The important
point is that only one process can have the file open at any given time. Using the MPI_Barrier
routine, modify your code so each process in turn opens, appends to, then closes the output file.
For a 1D arrangement of processes it may seem a lot of effort to use a Cartesian topology rather than
managing the processes by hand, e.g. calculating the ranks of the nearest neighbours. It is, however,
worth learning how to use topologies as these book-keeping calculations become tedious to do by hand
in two and three dimensions. For simplicity we will only use the routines in one dimension. Even for this
simple case the exercise shows how easy it is to change the boundary conditions when using topologies.
1. Measure the time taken for the global sum in both periodic and non-periodic topologies, and inves-
tigate how it varies with P .
5
2. Extend the one-dimensional ring topology to a two-dimensional cylinder (periodic in one direction,
non-periodic in the other). Perform two separate reduction operations, one in each of the two
dimensions of the cylinder.
1111
0000
0000
1111
0000
1111
0000
1111 1111111111111
0000000000000
0000
1111 0000000000000
1111111111111
0000
1111 0000000000000
1111111111111
0000
1111 0000000000000
1111111111111
0000
1111
1,6 M
0000000000000
1111111111111
N
1,5 2,5
0000
1111
1,4 2,4 3,4
0000
1111
0000
1111
0000
1111
1,3 2,3 3,3 4,3
0000
1111
1,2 2,2 3,2 4,2 5,2
1,1 2,1 3,1 4,1 5,1 6,1
0000
1111
N M
7 Derived Datatypes
We will extend exercise 4 to perform a global sum of a non-basic datatype. A simple example of this
is a compound type containing both an integer and a double-precision floating-point number. Such a
compound type can be declared as a structure in C, or a derived type in Fortran, as follows:
x.ival = 1; x%ival = 1
y.dval = 9.0; y%dval = 9.0
If you are unfamiliar with using derived types in Fortran then I recommend that you go straight to exercise
number 2 which deals with defining MPI datatypes to map onto subsections of arrays. This is, in fact,
the most common use of derived types in scientific applications of MPI.
1. Modify the ring exercise so that it uses an MPI_Type_struct derived datatype to pass round
the above compound type, and computes separate integer and floating-point totals. You will need
to use MPI_Address to obtain the displacements of ival and dval. Initialise the integer part
to rank and the floating-point part to (rank + 1)2 and check that you obtain the correct results.
2. Modify your existing ping-pong code to exchange N × N square matrices between the processes
(int msg[N][N] in C or INTEGER MSG(N,N) in Fortran). Initialise the matrix elements to
be equal to rank so that you can keep track of the messages. Define MPI_Type_contiguous
and MPI_Type_vector derived types to represent N × M (type mcols) and M × N (type
mrows) subsections of the matrix, where M ≤ N . Which datatype to use for each subsection
depends on whether you are using C or Fortran. You may find it helpful to refer to Figure 2 to
clarify this, where I draw the arrays in the standard fashion for matrices (indices start from one
rather than zero, first index goes down, second index goes across).
6
Set N = 10 and M = 3 and exchange columns 4, 5 and 6 of the matrix using the mcols type. Print
the entire matrix on each process after every stage of the ping-pong to check that for correctness.
Now modify your program to exchange rows 4, 5 and 6 using the mrows type.
1. For the compound type, print out the values of the displacements. Do you understand the results?
2. Modify the program that performed a global sum on a compound datatype so that it uses an MPI
collective routine. You will have to register your own reduction operation so that the MPI library
knows what calculation you want to be performed. Remember that addition is not a pre-defined
operation on your compound type; it still has to be defined even in the native language.
3. Modify your ping-pong code for matrix subsections so that you send type mcols and receive type
mrows. Do things function as you would expect?
7
8 Global Summation Using a Hypercube Algorithm
Although you should always perform global summations by using MPI_Reduce or MPI_Allreduce
with MPI_Op=MPI_SUM, it is an interesting exercise to program your own version using a more efficient
algorithm than the previous naive “message-round-a-ring” approach.
A more efficient method, at least for a number of processes that is a power of two, is to imagine that the
processes are arranged in a cube. The coordinates of the processes in the cube are taken from the binary
representation of the rank, therefore ensuring that exactly one process sits at each vertex of the cube.
Processes operate in pairs, swapping partial sums between neighbouring processes in each dimension in
turn. Figure 3 illustrates how this works in three dimensions (i.e. 23 = 8 processes).
3 7 011 111
1 5 001 101
2 6 010 110
0 4 000 100
011 111
001 101
010 110
000 100
3 7 H H+D
3 7
1 5 1 5
2 6 2 6
G G+C
0 4 0 4
A E A+E E+A
3 7 (H+D)+(F+B)
1 5
2 6 z
(G+C)+(E+A)
y
0 4 x
(A+E)+(C+G) (E+A)+(G+C)
An elegant way to program this is to construct a periodic cartesian topology of the appropriate dimension
and compute neighbours using MPI_Cart_shift. When each process swaps data with its neighbour
you must ensure that your program does not deadlock. This can be done by a variety of methods, includ-
ing a ping-pong approach where each pair of processes agrees in advance who will do a send followed
by a receive, and who will do a receive followed by a send. How do you expect the time to scale with the
number of processes, and how does this compare to the measured time taken by MPI_Allreduce?