Who Am I
Talking To?
Every call so far said MPI_COMM_WORLD. Today: rows, columns, a hand-picked few, and a grid MPI lays out for you. Two of the four ideas come with a bug that passes at small p.
Concept map
Every call since Week 3 has involved every rank. Real programs need a collective over some of them: a row of a matrix, the ranks on one node, the ranks a library owns. Click a node to jump to it.
Split: color picks the group, key orders it
12 ranks as a 3 × 4 grid. Every row needs its own sum. MPI_Allreduce takes one communicator; on WORLD it sums all 12.
The grid. Click a rank.
world rank shown; color = row
Each rank has to say which group it wants (a label) and where it wants to sit in it (an order). Those are exactly MPI_Comm_split's color and key. Rank 6 on a 4-wide grid: row 6 / 4 = 1, place 6 % 4 = 2.
Rank 6: row 1, rowRank 2 of 4.
Same process, new number. Ask the new communicator; never reuse a WORLD rank.
int color = rank / COLS; // my row: 0, 1, 2 int key = rank; // order inside the row MPI_Comm row; // collective on WORLD: every rank calls it MPI_Comm_split(MPI_COMM_WORLD, color, key, &row); MPI_Comm_rank(row, &rowRank); // rank 6 -> 2 MPI_Comm_size(row, &rowSize); // -> 4 MPI_Allreduce(&v, &rowSum, 1, MPI_INT, MPI_SUM, row); MPI_Comm_free(&row); // create, use, free // columns: color = rank % COLS rank 6 -> col 2, colRank 1 // key = -rank: each row numbered right to left
Split explorer
Equal colors land in the same new communicator. Key sorts the ranks inside it; ties break by old rank. The table is what split_Rows, split_Cols and split_KeyReversed print.
Same ranks, same tag, never crossed
context_Dup(). What a library does first: duplicate the communicator it is handed.
MPI_Comm lib; MPI_Comm_dup(MPI_COMM_WORLD, &lib); // what a library does first if (rank == 0) { // Isend: Send could deadlock MPI_Isend(&x111, 1, MPI_INT, 1, 0, lib, &req); // sent FIRST MPI_Send (&x222, 1, MPI_INT, 1, 0, MPI_COMM_WORLD); MPI_Wait(&req, &st); } if (rank == 1) { // wildcards on source AND tag MPI_Recv(&got, 1, MPI_INT, MPI_ANY_SOURCE, MPI_ANY_TAG, MPI_COMM_WORLD, &st); }
lib), then 222 (on WORLD). Both go to rank 1 with tag 0. Rank 1 receives on WORLD with MPI_ANY_SOURCE and MPI_ANY_TAG. What is got?got = 222
context_Dup CHECK ok (got == 222, and the lib receive then gets 111)
111 was sent first, from the same rank, with the same tag. The WORLD receive cannot see it. A wildcard matches source and tag, never communicator.
Why MPI_Isend for the first message: rank 1 receives on WORLD first. A blocking MPI_Send on lib may wait until its receive is posted, and that receive comes second: a Send-then-Recv deadlock. Eager vs rendezvous is Mon Oct 12.
The ragged two-stage sum
sum_RowThenCol(). v = rank + 1. Row Allreduce, then a column Allreduce of rowSum. At p = 12 (3 × 4) every rank prints 78 = 12·13/2.
The idea is sound: a two-stage reduction over rows then columns is how 2-D codes reduce. Same log p steps in total, each on a smaller communicator, and you get the row sums along the way. At p = 12: row sums 10, 26, 42; each column holds one member of each row, so each column sums 10 + 26 + 42 = 78.
Now p = 6, still 4 wide. Draw the grid first.
p = 6, COLS = 4: row 1 is short. The answer is in the picture.
world 0 total 21 world 1 total 21 world 2 total 10 world 3 total 10 world 4 total 21 world 5 total 21 sum_RowThenCol expect 21 (ragged) CHECK 2 of 6 WRONG sum_RowLeaders total 21 CHECK ok
Column 2 is {2}. Column 3 is {3}. Nobody below them. Their column sum sees row 0 only: 1 + 2 + 3 + 4 = 10. Ranks 0, 1, 4, 5 are in full columns and get the right answer, which is why the program looks mostly right.
Explore the shape
The same two stages, computed here. Full columns are drawn solid, short columns hatched; a rank whose total is wrong is outlined in red.
Leaders only; everyone else opts out
sum_RowLeaders(). Three moves: reduce to the row leader, leaders-only allreduce, broadcast back down the row.
MPI_Comm_split(MPI_COMM_WORLD, rank / COLS, rank, &row); int color = (rowRank == 0) ? 0 : MPI_UNDEFINED; MPI_Comm_split(MPI_COMM_WORLD, color, rank, &leaders); MPI_Reduce(&v, &rowSum, 1, MPI_INT, MPI_SUM, 0, row); if (leaders != MPI_COMM_NULL) { MPI_Allreduce(&rowSum, &total, 1, MPI_INT, MPI_SUM, leaders); } MPI_Bcast(&total, 1, MPI_INT, 0, row); // back down the row if (leaders != MPI_COMM_NULL) { MPI_Comm_free(&leaders); } MPI_Comm_free(&row);
Step through the three moves
Leaders (rowRank 0) are outlined. Every other rank passed MPI_UNDEFINED and holds MPI_COMM_NULL for leaders.
MPI_UNDEFINED and get MPI_COMM_NULL. Never use or free MPI_COMM_NULL. The two guards are the discipline. The next screen shows what happens without the second one.Boost: bmpi::communicator row = world.split(world.rank() / COLS); returns a communicator; its destructor frees it. The key defaults to the old rank. boost_Split in the file.
Groups: the hand-picked few
group_Hardcoded(), from the old Communicators code. When the ranks you want are not a function of your own rank (so no color formula), build a group and make a communicator from it.
MPI_Group worldGroup, primeGroup; MPI_Comm_group(MPI_COMM_WORLD, &worldGroup); const int ranks[4] = {2, 3, 5, 7}; MPI_Group_incl(worldGroup, 4, ranks, &primeGroup); MPI_Comm primeComm; MPI_Comm_create_group(MPI_COMM_WORLD, primeGroup, 0, &primeComm); MPI_Comm_free(&primeComm); // every rank MPI_Group_free(&primeGroup); MPI_Group_free(&worldGroup);
A group is a set of ranks, no context. You cannot send on it. MPI_Comm_create_group is collective over the group, not over WORLD: only the members have to call it. A non-member may call it too; it returns at once with MPI_COMM_NULL.
$ mpiexec -n 4 ./MPI 4 --hardcoded *** An error occurred in MPI_Group_incl *** MPI_ERR_RANK: invalid rank $ mpiexec -n 8 ./MPI 4 --hardcoded *** An error occurred in MPI_Comm_free *** MPI_ERR_COMM: invalid communicator
Aborts at 4. Aborts at 8, too. p = 4: ranks 5 and 7 don't exist. p = 8: ranks 0, 1, 4, 6 got MPI_COMM_NULL (legally) and then freed it (illegally). Both are caught because the default handler is MPI_ERRORS_ARE_FATAL. Switch it off and the same code goes quiet. Output is abridged Open MPI 4.1.6 text; MVAPICH words it differently but aborts in the same calls.
// the fix, group_Primes():
for (int i = 0; i < p; i++) {
if (isPrime(i)) primes.push_back(i);
}
if (primeComm != MPI_COMM_NULL) MPI_Comm_free(&primeComm);
Compute the list from p. Free only what you got.
Who is a member, who holds COMM_NULL
hardcoded {2, 3, 5, 7}
group_Primes: isPrime(i) for i < p
Aside: lecture07 calls MPI_Init, not boost::mpi::environment. The Boost environment sets MPI_ERRORS_RETURN on WORLD so it can throw C++ exceptions. Under it these unchecked C calls did not abort: p = 4 segfaulted later with no message, and p = 8 printed "success". An error handler that returns is only as good as the code that reads the return value.
Grids: let MPI hand out the coordinates
MPI_Dims_create(p, ndims, dims) picks the shape. MPI_Cart_create builds the communicator. MPI_Cart_coords(cart, rank, maxdims, coords) fills in (x, y, z).
Dims_create: a balanced factoring
Factors p into ndims factors as balanced as it can, filling only the zero entries. The deck's three cases: 8 → 2×2×2, 12 → 3×2×2, 6 → 3×2×1. Then MPI_Cart_create numbers ranks row-major over that shape (last coordinate fastest).
cart_Coords2of3(), from the old Communicators code, builds a 3-D grid of 8 ranks (2 × 2 × 2 torus), then asks MPI_Cart_coords(cart, rank, 2, coords) for 2 coordinates into a 2-element array. No error, no warning at compile time. What could an MPI library do with that call?Same source, same p. Two real MPIs picked different answers.
Open MPI 4.1.6 (a laptop, the course's test machine)
cart 0 (0,0) cart 4 (1,0) cart 1 (0,0) cart 5 (1,0) cart 2 (0,1) cart 6 (1,1) cart 3 (0,1) cart 7 (1,1) 4 distinct (x,y) for 8 ranks fine at p <= 7: dims[2] was 1
MPICH 4.2 (MVAPICH 4.1 on Cardinal is built on MPICH)
MPI_Cart_coords(..., maxdims=2, ...)
failed
Number of dimensions 3 is too
large (maximum is 2)
refused at every p, even p = 1
Your laptop says (0,0) twice; Cardinal says no. "It works on my laptop" is a statement about one implementation. Fix, cart_Coords(): ndims = 3 in every call, coords[3], and ask the Cartesian communicator (not WORLD) for your rank, because reorder = 1 lets MPI renumber ranks to fit the machine. MPI_Cart_rank(coords) round-trips; CHECK ok at every p.
What "fill two and stop" gives at other p
Model of the Open MPI behaviour: the shape from Dims_create above (ndims = 3), row-major numbering, the first two coordinates kept. Pairs that collide are red.
Ask for neighbours, not rank ± 1
cart_Shift(). A 1-D line. Every rank runs the same line; no if at the edge.
int dims[1] = {p}, periods[1] = {0}; // open line
MPI_Cart_create(MPI_COMM_WORLD, 1, dims, periods, 0, &line);
MPI_Cart_shift(line, 0, +1, &left, &right); // source, dest
int got = -1;
MPI_Sendrecv(&me, 1, MPI_INT, right, 0,
&got, 1, MPI_INT, left, 0, line, &st);
// open: rank 0: left = MPI_PROC_NULL, got stays -1
// periodic: rank 0: left = p - 1, got = p - 1
The shift, step by step
MPI_PROC_NULL does nothing. A receive from it completes at once and writes nothing. That deletes the if (rank == 0) ... else if (rank == p-1) edge code from Week 4-style programs. Sendrecv, not Send then Recv: everyone sends right and receives from the left in one call; the library orders it. This is a halo exchange: every stencil code does it every step.2-D: cart_Sub in the file does Dims_create(p, 2), Cart_create, then MPI_Cart_sub(grid, remain = {0, 1}) keeps dimension 1, so each rank lands in its row's communicator. No color math. When COLS divides p the file builds the grid as p/COLS × COLS, so these are exactly split_Rows' communicators, derived from the grid instead of from arithmetic. A Cartesian grid cannot be ragged, so for other p it falls back to Dims_create's shape.
Strips or blocks? Count the α's
Week 5's model, t(m) = α + mβ, applied to the grid's shape. n × n doubles over p ranks, one halo exchange per step, a rank's messages one after another (the conservative case). α ≈ 2 µs, β ≈ 0.05 ns/B.
Strips (1-D)
2 · (α + 8n·β)
2 neighbours (up, down), a full row of n doubles = 8n bytes each.
Blocks (√p × √p)
4 · (α + 8n/√p·β)
4 neighbours, an edge of n/√p doubles each. At p = 16: 4 · (α + 2n·β).
Calculator
Model only (α = 2 µs, β = 1/(20 GB/s)). At p = 16 the two are equal when 2α + 16nβ = 4α + 8nβ, so 8nβ = 2α and n = α / 4β = 2000 ns / 0.2 ns = 10⁴. Below it strips win: fewer α's. n = 10³: 4.8 vs 8.4 µs. n = 10⁵: 84 vs 48 µs.
What the model leaves out: with reorder = 1, MPI may place grid neighbours on the same node, where α is smaller. The standard permits that; it does not promise it. Whether Cardinal does is a measurement, not a slide: the question is left open here.
Run it on Cardinal
Branch lecture07. Every method ends in CHECK; the program prints a rank/row table. About 20 minutes.
-
Clone once, check out the lecture branch
ssh <your-username>@cardinal.osc.edu git clone -o class git@gitlab.com:cs4540-spring-2019/cs-4170-5170-fall-2026/mpi.git mpi cd mpi git fetch class git checkout -b lecture07 class/lecture07
-
Load the modules, build
Modules do not persist between logins. Load them every time.
module load mvapich/4.1 boost/1.83.0 cd Default && make OSC && cd ..
-
Get nodes of your own
The login node is shared; run nothing there. Two nodes, so a message between ranks crosses the network. Load the modules again inside the allocation.
salloc --nodes=2 --ntasks-per-node=6 -c 2 --time=00:15:00 -A PCS0313 module load mvapich/4.1 boost/1.83.0
Every run:
srun -c 2 -n p ./Default/MPI [COLS] [--hardcoded]. Default COLS = 4.group_Hardcodedruns only with--hardcoded.
Experiments
A · Three rows of four
srun -c 2 -n 12 ./Default/MPI
What you should see
The rank/row table: three rows of 4. Find rank 6: row 1, rowRank 2 of 4; column 2, colRank 1. split_KeyReversed numbers each row right to left. context_Dup: got = 222. sum_RowThenCol at p = 12: every rank 78, CHECK ok, because 4 divides 12. cart_Coords2of3: Cardinal's MVAPICH is MPICH-based and should refuse the maxdims = 2 call (the file sets MPI_ERRORS_RETURN on the cart communicator so the demo can print it and carry on); cart_Coords CHECK ok.
B · The ragged grid
srun -c 2 -n 6 ./Default/MPI srun -c 2 -n 8 ./Default/MPI
What you should see
p = 6: sum_RowThenCol expect 21 (ragged) CHECK 2 of 6 WRONG, with ranks 2 and 3 printing total 10 and the others 21; sum_RowLeaders total 21 CHECK ok. p = 8: both ok, since 4 divides 8. That is the point: the test at 8 never finds it. Try -n 6 ./Default/MPI 3 (COLS = 3, which divides 6) and -n 8 ./Default/MPI 3 (which does not) and predict each before you run it.
C · The hardcoded group, twice
srun -c 2 -n 4 ./Default/MPI 4 --hardcoded srun -c 2 -n 8 ./Default/MPI 4 --hardcoded
What you should see
Both abort. At p = 4 the failing call is MPI_Group_incl (ranks 5 and 7 do not exist); at p = 8 it is MPI_Comm_free on ranks 0, 1, 4, 6, which free MPI_COMM_NULL. The deck's transcript is Open MPI text; MVAPICH words it differently but aborts in the same calls. Without --hardcoded the run uses group_Primes, which computes the list from p and frees only what it got: CHECK ok.
D · The open line
srun -c 2 -n 5 ./Default/MPI
What you should see
cart_Shift: rank 0 receives from MPI_PROC_NULL and its got stays −1; every other rank k gets k − 1. cart_Sub at p = 5: 4 does not divide 5, so the file falls back to Dims_create's 2-D shape for the row communicators. Then exit to release the allocation.
Check yourself
Six questions, instant feedback, nothing recorded.
MPI_Comm_split(WORLD, rank / COLS, rank, &row). Rank 9's color and its rank in row?MPI_Comm_split?lib (a dup of WORLD) and then 222 on WORLD, both to rank 1, tag 0. Rank 1 receives on WORLD with ANY_SOURCE and ANY_TAG. What does it get, and why?MPI_Dims_create(12, 3, dims) with dims = {0, 0, 0} givesMPI_Cart_shift(line, 0, +1, &left, &right) and then MPI_Sendrecv sending to right, receiving from left. What happens?