Unit IV — Programming Models & Tools

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

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
ProcessOne running copy of your program, with its own memory
RankA process's unique id: 0, 1, ... size−1 (its "jersey number")
SizeHow many processes there are in total
CommunicatorMPI_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
1Send — MPI copies it into a small internal buffer and returns immediatelysameWorks perfectly
1,000,000Send blocks — too big to buffer, waits for a matching RecvSend blocks — same reasonDEADLOCK — 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_BcastRoot sends one value to every process
MPI_ScatterRoot splits an array — one chunk to each process
MPI_GatherInverse of Scatter: collect chunks back onto root
MPI_ReduceCombine values (sum, max, ...) onto root
MPI_AllreduceLike 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_Scatter
cut it up, one piece each
[10,20][30,40][50,60][70,80]
local work — no MPI3070110150
MPI_Reduce(SUM, root=0)
combine into the root
360nothingnothingnothing
MPI_Allreduce(SUM)
combine, everyone gets it
360360360360
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 Reduce if only the root prints it; use Allreduce if every rank needs it to continue (a convergence check, a global maximum, a normalisation factor).
  • Gather vs Reduce: Gather collects the pieces side by side; Reduce combines 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 workThreadsProcesses
MemorySharedPrivate (per process)
ScaleOne machineThousands of machines
Data sharingAutomatic (same RAM)Explicit messages
EffortAdd a pragmaRewrite 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 core1,000 ms01,000 ms
OpenMP, 8 cores, one box125 ms~0 — shared memory125 ms
MPI, 8 nodes, data starts on node 0125 ms640 ms to scatter 8 GB
+ 5 µs to reduce
765 ms
MPI, 8 nodes, data already distributed125 ms5 µs — just the reduce125 ms
MPI, 800 nodes, data already distributed1.25 ms15 µs1.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_RecvA message on a queue / an RPC between services
MPI_Scatter + MPI_ReduceFan-out to workers, fan-in the results (map-reduce)
MPI_BcastPush config/state to every worker at once
Send/Recv deadlockTwo 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

Homework

Next session: CUDA & the Modern GPU Stack

From a cluster of CPUs to the thousands of cores inside one GPU.