MPI: Message Passing
Session 11 • 2311CSC501J — Parallel Processing • Lab 2: Parallel Sort
What You'll Learn
- Processes, ranks & the communicator (SPMD)
- Point-to-point:
Send/Recv - Collectives: Bcast, Scatter, Gather, Reduce
- Lab 2: a distributed parallel sort
"Shared memory stops at the edge of one machine. Message passing is how thousands of machines cooperate."
— the whole point of MPI
Shared Memory Hits a Wall
OpenMP (Session 10) was easy — but it can't leave the box
- OpenMP threads all share one memory on one machine.
- A single server maxes out at ~64–128 cores and a few TB of RAM. Then you're stuck.
- Weather models, AI training, and physics simulations need thousands of machines.
MPI = Message Passing Interface. The standard for distributed-memory parallelism: many machines, each with its own private memory, cooperating by explicitly sending messages to each other.
Shared memory (OpenMP)
Threads read/write the same RAM. No copying — but bounded by one machine.
Distributed memory (MPI)
No shared RAM. Data moves only by messages — but scales to a whole cluster.
The MPI Mental Model
MPI runs processes, not threads. mpirun -np 4 launches 4 full copies of your program — each with its own private memory. They cannot see each other's variables. Ever.
| Term | Meaning |
|---|---|
| Process | One running copy of your program, with its own memory |
| Rank | A process's unique id: 0, 1, ... size−1 (its "jersey number") |
| Size | How many processes there are in total |
| Communicator | MPI_COMM_WORLD — the group of all processes |
SPMD = Single Program, Multiple Data (callback to Flynn, Session 02). One source file, run by every process; each behaves differently based on its rank. if (rank == 0) { ... } is how one process plays "master."
The Four Lines Every MPI Program Has
#include <mpi.h>
int main(int argc, char **argv) {
MPI_Init(&argc, &argv); // 1. start MPI (first call)
int rank, size;
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 2. which process am I?
MPI_Comm_size(MPI_COMM_WORLD, &size); // 3. how many of us?
printf("Hello from rank %d of %d\n", rank, size);
MPI_Finalize(); // 4. shut MPI down (last call)
return 0;
}
Compile & run
mpicc hello.c -o hello
mpirun -np 4 ./hello
Output (order varies!)
Hello from rank 2 of 4
Hello from rank 0 of 4
Hello from rank 3 of 4
Hello from rank 1 of 4
The print order changes between runs — the processes run truly at once. That non-determinism is your first sign of real parallelism.
Point-to-Point: Send & Recv
One process hands a value directly to another. The two most fundamental MPI calls:
MPI_Send(&value, count, MPI_INT, dest, tag, MPI_COMM_WORLD);
MPI_Recv(&value, count, MPI_INT, source, tag, MPI_COMM_WORLD, &status);
// buffer how-many type who label group
⚠ The deadlock trap
Send/Recv are blocking. If both processes call Recv first, each waits for a message the other hasn't sent — forever. Rule: order them so somebody sends before everybody receives.
Non-blocking MPI_Isend / MPI_Irecv return immediately (you MPI_Wait later). They let you overlap communication with computation and sidestep the deadlock.
Worked Example: The Deadlock Everyone Writes Once
Two ranks swap data. Every rank runs the same three lines — SPMD, as always.
int partner = 1 - rank; // 0 <-> 1
MPI_Send(sendbuf, n, MPI_DOUBLE, partner, 0, MPI_COMM_WORLD);
MPI_Recv(recvbuf, n, MPI_DOUBLE, partner, 0, MPI_COMM_WORLD, &status);
| n | Rank 0 | Rank 1 | Result |
|---|---|---|---|
| 1 | Send — MPI copies it into a small internal buffer and returns immediately | same | Works perfectly |
| 1,000,000 | Send blocks — too big to buffer, waits for a matching Recv | Send blocks — same reason | DEADLOCK — forever |
Neither rank ever reaches its MPI_Recv, because each is stuck inside its MPI_Send waiting for the other to reach the MPI_Recv it will never reach. Both processes sit at 100% CPU, forever, with no error message.
And this is the part that ends careers: the code is correct for small messages. It passes every test you write on your laptop with 4 elements. It hangs the first time it runs on real data — usually at 3 a.m., on a cluster you're billed for by the hour. The threshold where MPI_Send stops buffering is not in the standard; it varies by implementation, by interconnect, by message size.
Three fixes, worst to best
1. Order them by rank
if (rank % 2 == 0) {
Send(); Recv();
} else {
Recv(); Send();
}
Works. Ugly, easy to get wrong with odd process counts, and doesn't generalise to a ring.
2. MPI_Sendrecv
MPI_Sendrecv(
sendbuf,n,...,partner,0,
recvbuf,n,...,partner,0,
comm, &status);
One call. MPI is required to make it deadlock-free. This is the right answer for a simple exchange.
3. Non-blocking
MPI_Isend(..., &req[0]);
MPI_Irecv(..., &req[1]);
/* do useful work! */
MPI_Waitall(2, req, ...);
Deadlock-free and lets you overlap communication with computation. What real HPC code does.
The habit to build now: never assume MPI_Send returns before the matching receive is posted. Write as if it always blocks. Then your program is correct at every message size — which is the only kind of correct that counts.
Collectives: The Workhorses
You could build everything from Send/Recv — but you shouldn't. Collectives move data among all processes at once, and the library does it far more efficiently (often log(P) steps).
| Collective | What it does |
|---|---|
MPI_Bcast | Root sends one value to every process |
MPI_Scatter | Root splits an array — one chunk to each process |
MPI_Gather | Inverse of Scatter: collect chunks back onto root |
MPI_Reduce | Combine values (sum, max, ...) onto root |
MPI_Allreduce | Like Reduce, but every process gets the result |
Scatter = fan-out, Reduce = fan-in. Together they are the map-reduce pattern — the same shape as the data jobs behind every large-scale analytics system.
Seeing the Collectives
BROADCAST SCATTER
root: [7] root: [10 20 30 40]
| | | | |
+----+----+----+ +------+ | | +------+
v v v v v v v v
[7] [7] [7] [7] [10] [20] [30] [40]
r0 r1 r2 r3 r0 r1 r2 r3
GATHER REDUCE (MPI_SUM)
[10] [20] [30] [40] [10] [20] [30] [40]
| | | | \ \ / /
+----+----+----+ +---(+)---+
v v
root: [10 20 30 40] root: [100]
Open 05-collectives.html from the Examples page to watch these animate. Every distributed algorithm is built by composing these four moves.
Worked Example: Trace Four Collectives
Rank 0 starts with [10, 20, 30, 40, 50, 60, 70, 80]. Four ranks. Sum it. Watch exactly what each rank holds after each call.
| After this call… | Rank 0 | Rank 1 | Rank 2 | Rank 3 |
|---|---|---|---|---|
| at the start | [10..80] | — | — | — |
MPI_Scattercut it up, one piece each | [10,20] | [30,40] | [50,60] | [70,80] |
| local work — no MPI | 30 | 70 | 110 | 150 |
MPI_Reduce(SUM, root=0)combine into the root | 360 | nothing | nothing | nothing |
MPI_Allreduce(SUM)combine, everyone gets it | 360 | 360 | 360 | 360 |
MPI_Gather(root=0)collect the pieces, uncombined | [30,70,110,150] | — | — | — |
Two distinctions worth getting exactly right — both are exam questions:
- Reduce vs Allreduce: who ends up with the answer. Use
Reduceif only the root prints it; useAllreduceif every rank needs it to continue (a convergence check, a global maximum, a normalisation factor). - Gather vs Reduce:
Gathercollects the pieces side by side;Reducecombines them with an operator. Same traffic, different result.
Why you should never hand-roll these. The obvious implementation of Reduce is "every rank sends to rank 0" — p−1 messages arriving at one node, taking O(p) time and saturating one link. A real MPI library uses a tree: ranks pair up, then the winners pair up, and so on — O(log p).
At 1,024 ranks that's 1,023 steps versus 10. Your MPI library also knows the machine's topology and picks a tree that matches the actual network. Four lines of your code, decades of somebody else's tuning.
Recognise this trace? It is exactly the four-ranks example from Session 02 — the same array, the same split, the same combine — except there you wrote the sends and receives yourself. Collectives are that pattern, named and optimised.
Lab 2: Parallel Sort — The Four Moves
Sorting a huge array on one machine is slow. Split the work across processes:
1. Scatter
Rank 0 splits the array into equal chunks, one per process.
2. Local sort
Each process sorts its own chunk (qsort) — all at the same time.
3. Gather
Sorted chunks return to rank 0 — now size sorted runs side by side.
4. Merge
Rank 0 does a cheap k-way merge of the sorted runs into one sorted array.
Do the expensive sorting in parallel, then pay a small, linear merge at the end. This is the sort phase of MapReduce and of real distributed sorts.
Lab 2: The Code, In One Picture
rank 0 builds: [42 7 91 15 | 3 88 60 25] (unsorted, 8 elements, 2 procs)
MPI_Scatter -> rank 0: [42 7 91 15] rank 1: [3 88 60 25]
qsort (local)-> rank 0: [7 15 42 91] rank 1: [3 25 60 88] // both at once
MPI_Gather -> rank 0: [7 15 42 91 | 3 25 60 88] (two sorted runs)
k-way merge -> rank 0: [3 7 15 25 42 60 88 91] // fully sorted!
MPI_Scatter(data, per, MPI_INT, chunk, per, MPI_INT, 0, MPI_COMM_WORLD);
qsort(chunk, per, sizeof(int), cmp_int); // each process, in parallel
MPI_Gather(chunk, per, MPI_INT, gathered, per, MPI_INT, 0, MPI_COMM_WORLD);
if (rank == 0) k_way_merge(gathered, size, per, sorted);
Full commented source: examples/04-parallel-sort.c. Scatter/Gather need equal chunks, so the total size divides evenly by the process count.
OpenMP vs MPI
| OpenMP (S10) | MPI (this session) | |
|---|---|---|
| Unit of work | Threads | Processes |
| Memory | Shared | Private (per process) |
| Scale | One machine | Thousands of machines |
| Data sharing | Automatic (same RAM) | Explicit messages |
| Effort | Add a pragma | Rewrite around messages |
Hybrid MPI + OpenMP is the real supercomputer recipe: MPI between nodes, OpenMP within each node. Messages across the cluster, shared memory inside each box.
Worked Example: When Is MPI Actually Worth It?
Sum 1 billion doubles (8 GB). One second serially. Three ways to do it in parallel — and the winner is not the one with the most machines.
| Approach | Compute | Communication | Total |
|---|---|---|---|
| Serial, one core | 1,000 ms | 0 | 1,000 ms |
| OpenMP, 8 cores, one box | 125 ms | ~0 — shared memory | 125 ms |
| MPI, 8 nodes, data starts on node 0 | 125 ms | 640 ms to scatter 8 GB + 5 µs to reduce | 765 ms |
| MPI, 8 nodes, data already distributed | 125 ms | 5 µs — just the reduce | 125 ms |
| MPI, 800 nodes, data already distributed | 1.25 ms | 15 µs | 1.3 ms |
Rows 2 and 3 do identical arithmetic on identical hardware counts, and MPI is 6× slower. Every one of those 640 milliseconds is spent shipping data that was already sitting in the right place.
Rows 4 and 5 are the same MPI code, with one thing changed: where the data lives at the start.
So MPI is not "the faster tool." It is the tool for two situations:
- The data doesn't fit in one machine. 8 GB fits; 80 TB does not. Then distribution isn't a choice you're optimising — it's a constraint you're obeying.
- The data is already distributed. It was generated there, logged there, or lives on a distributed filesystem. Moving it to one box would cost more than the computation.
If neither is true — if it fits on one machine and it's already there — use OpenMP. Simpler code, no deadlocks, and it wins on the clock too.
This is Session 04's lesson in a new setting: computation is cheap, communication is expensive. It's also why the next unit's big idea — "move the code to the data, not the data to the code" — is worth 640 milliseconds a time. Hadoop and Spark were built entirely around row 3 being unacceptable.
You Already Know This Pattern
MPI is distributed systems with a different vocabulary. Every idea maps to something you'll build in production:
| In MPI | In the systems you build |
|---|---|
MPI_Send / MPI_Recv | A message on a queue / an RPC between services |
MPI_Scatter + MPI_Reduce | Fan-out to workers, fan-in the results (map-reduce) |
MPI_Bcast | Push config/state to every worker at once |
| Send/Recv deadlock | Two services each waiting on the other — a distributed deadlock |
The parallel-sort you'll write in Lab 2 is exactly how a data warehouse sorts a table too big for one machine: shard it, sort each shard, merge. Same idea, thread → core → node → cluster.
Recap & What's Next
Key Takeaways
- MPI = processes with private memory, cooperating by messages — it scales past one machine.
- Every program:
Init→Comm_rank/Comm_size→ work →Finalize. Send/Recvare point-to-point and blocking — mind the deadlock order.- Prefer collectives (Bcast/Scatter/Gather/Reduce) — the map-reduce workhorses.
- Lab 2: scatter → local sort → gather → merge = a distributed sort.
Homework
- Run
01-hello-mpi.cwith-np 2,4,8; note how the print order changes. - Modify
03-collectives.cto compute the maximum instead of the sum (MPI_MAX). - Trace Lab 2 by hand for 6 numbers on 2 processes: scatter, sort, gather, merge.
Next session: CUDA & the Modern GPU Stack
From a cluster of CPUs to the thousands of cores inside one GPU.