High Performance Computing

Chapter 6
Distributed-Memory Computing

Post Graduate Diploma in Data Science
High Performance Computing   •   Post Graduate

6. Distributed-Memory Computing

Introduction and Context

Distributed-memory computing is a central approach to high-performance computing in which a computation is executed by multiple processing elements, each with its own private memory. Unlike shared-memory systems, no process can directly read or write another process’s memory. Processes must therefore exchange data explicitly through a communication system, most commonly by using the Message Passing Interface (MPI).

This model is important because modern supercomputers are built primarily from interconnected nodes. Each node may contain several CPUs, accelerators, and a local memory subsystem, while the complete application operates across many nodes. Distributed-memory programming provides scalability, portability, and explicit control over data movement, but it also introduces responsibilities that do not arise, or are less visible, in shared-memory programming. Programmers must decide how data are divided, how processes communicate, when communication occurs, and how synchronization affects performance.

MPI is not a programming language. It is a standardized library specification with bindings for languages such as C, C++, and Fortran. It defines processes, communicators, message matching, collective operations, datatypes, and synchronization mechanisms. This chapter develops the principles required to design MPI applications using point-to-point and collective communication, communicators, nonblocking operations, domain decomposition, and communication–computation overlap.

Learning Objectives

After studying this chapter, the reader should be able to:

1. Explain the architecture and programming assumptions of distributed-memory systems.

2. Distinguish point-to-point, collective, blocking, and nonblocking communication.

3. Design MPI programs using ranks, communicators, tags, statuses, and derived datatypes.

4. Select appropriate collective operations for common data-distribution and reduction tasks.

5. Apply one- and multidimensional domain decomposition to numerical problems.

6. Analyze communication cost, synchronization, load imbalance, and scalability.

7. Use nonblocking operations to improve communication–computation overlap safely.

8. Identify common MPI errors and apply professional practices for reliable parallel software.

The Distributed-Memory Execution Model

An MPI application normally begins as a collection of processes launched together. Each process executes the same program, although its behavior may differ according to its rank. A rank is an integer identifier assigned within a communicator. The default communicator, `MPI_COMM_WORLD`, contains all processes participating in the launched application.

The Single Program, Multiple Data (SPMD) model is commonly used. Every process runs the same executable, but each process typically owns a different portion of the data. For example, in a vector operation, process 0 may hold elements 0–999, process 1 elements 1000–1999, and so forth. The program’s correctness depends on the consistency of these local views and on correctly coordinated communication.

A message contains data sent from one process to another. At minimum, a message is characterized by a source rank, destination rank, tag, communicator, datatype, and count. A receiver may specify an exact source and tag or use wildcards such as `MPI_ANY_SOURCE` and `MPI_ANY_TAG`. Wildcards provide flexibility but can make program behavior harder to reason about and debug.

MPI programs typically follow this lifecycle:

  • initialize MPI;
  • determine the process rank and communicator size;
  • allocate and initialize local data;
  • exchange data and perform local computation;
  • communicate results or perform reductions;
  • finalize MPI.

`MPI_Init` and `MPI_Finalize` delimit the MPI execution environment. Calling MPI routines before initialization or after finalization is invalid, except for a small set of permitted inquiry operations.

Communication Semantics: Messages, Matching, and Synchronization

Point-to-point communication

Point-to-point communication transfers data between two processes. The basic blocking operations are `MPI_Send` and `MPI_Recv`. A typical exchange is conceptually represented as:

```c

MPI_Send(buffer, count, datatype, destination, tag, comm);

MPI_Recv(buffer, count, datatype, source, tag, comm, &status);

```

The sender and receiver must agree on the communicator, matching tag, and compatible datatypes. The receiver’s buffer must be large enough for the incoming message. `MPI_Get_count` can be used with the returned status to determine how many elements were received.

Blocking does not necessarily mean that the sender waits until the receiver has copied the data into its final application buffer. MPI is permitted to buffer some messages internally. However, a blocking send returns only when the send buffer can safely be reused. The exact completion behavior depends on the MPI implementation and communication mode. A programmer must never modify or reuse a send buffer prematurely.

A common error is deadlock. Consider two processes that both execute a blocking send before posting a receive. If the MPI implementation cannot buffer either message, both processes may wait indefinitely. Safer designs include posting receives first, using `MPI_Sendrecv`, or arranging an order in which one process sends while the other receives.

`MPI_Sendrecv` combines a send and a receive in one operation and is particularly useful for exchanging boundary data with neighboring processes. It reduces the risk of deadlock in symmetric exchanges.

Nonblocking communication

Nonblocking operations separate initiation from completion. `MPI_Isend` and `MPI_Irecv` return an `MPI_Request` immediately, allowing the process to perform other work. Completion is later verified using `MPI_Wait`, `MPI_Waitall`, `MPI_Test`, or related operations.

```c

MPI_Irecv(left_halo, n, MPI_DOUBLE, left, 10, comm, &req[0]);

MPI_Isend(right_halo, n, MPI_DOUBLE, right, 10, comm, &req[1]);

/ Perform independent computation here. /

MPI_Waitall(2, req, MPI_STATUSES_IGNORE);

```

The crucial rule is that a buffer involved in a nonblocking operation must not be altered or reused until the operation has completed. A nonblocking receive also cannot be read until completion has been confirmed. Nonblocking communication is therefore a correctness mechanism as well as a performance technique.

Nonblocking operations can improve performance in three ways. They may allow computation to proceed while data are in transit, permit multiple messages to be outstanding, and avoid waiting for a slow neighbor before beginning independent work. Nevertheless, overlap is not automatic. It depends on MPI progress, network hardware, message size, implementation behavior, and whether the application performs suitable independent work.

Collective communication

Collective operations involve all processes in a communicator, or a defined subgroup when a different communicator is used. Common operations include:

  • `MPI_Bcast`: one process distributes identical data to all processes;
  • `MPI_Scatter`: a root distributes different portions of data;
  • `MPI_Gather`: processes send data to a root;
  • `MPI_Allgather`: every process receives the gathered data;
  • `MPI_Reduce`: values are combined at a root using an operation such as sum or maximum;
  • `MPI_Allreduce`: the reduction result is distributed to every process;
  • `MPI_Barrier`: processes wait until all members reach the barrier.

Collective calls must be executed by all processes in the communicator in a compatible order. A process cannot omit a collective operation merely because it has no useful local data. Such mismatches can cause deadlock or undefined behavior.

Collectives are generally preferable to manually implementing common communication patterns because MPI implementations can select efficient algorithms, including tree-based broadcast, recursive-doubling exchange, or topology-aware reduction. However, collective calls are not automatically inexpensive. An `MPI_Allreduce` inside a frequently executed loop can become a scalability bottleneck, particularly when the number of processes is large.

Communicators and Process Organization

A communicator defines a communication context and a group of processes. The communication context is significant because messages sent in one communicator do not accidentally match messages sent in another. This isolation supports modularity: two libraries can use the same ranks and tags without interfering if they use separate communicators.

Applications can create subcommunicators with `MPI_Comm_split`, `MPI_Comm_create`, or related routines. For example, a climate model may create one communicator for atmospheric processes and another for ocean processes. A two-dimensional grid program may create row and column communicators to support efficient broadcasts and reductions along grid dimensions.

A communicator also provides a disciplined way to express algorithmic structure. Instead of repeatedly testing whether a process belongs to a particular group, the program can operate on a communicator whose membership already represents that group. The cost of creating communicators is normally paid during setup rather than inside the main computational loop.

MPI topology facilities, including Cartesian communicators, are useful for neighbor-based algorithms. A Cartesian communicator maps ranks onto a logical grid and can identify neighboring ranks with `MPI_Cart_shift`. This does not necessarily change the physical network mapping, but it expresses the communication structure clearly and may help an implementation optimize placement.

Derived Datatypes and Structured Data

MPI datatypes describe the memory layout of communicated data. Basic types such as `MPI_INT` and `MPI_DOUBLE` are sufficient for contiguous arrays. Derived datatypes are useful when the required data are noncontiguous, such as a column of a row-major matrix or a structure containing fields of different types.

For a two-dimensional stencil, a row is usually contiguous, whereas a column is separated by the matrix leading dimension. An MPI vector datatype can describe the column without copying it into a temporary buffer. Derived datatypes can reduce packing overhead and make the communication code reflect the data structure, although their construction and implementation costs should be measured. They must be committed with `MPI_Type_commit` and released with `MPI_Type_free` when no longer needed.

Datatype compatibility concerns the type signature, not necessarily identical datatype handles. The sender and receiver must describe compatible sequences of basic elements. Incorrect extents, displacements, or counts can produce corrupted results that are difficult to diagnose.

Domain Decomposition: Turning a Global Problem into Local Work

Domain decomposition divides a global data structure or computational domain among processes. Each process computes on its local subdomain and exchanges only the information required by neighboring subdomains. The method is especially effective for finite-difference methods, finite-volume methods, particle grids, image processing, and cellular simulations.

A one-dimensional decomposition divides an array or grid into contiguous blocks. If a grid has `N` rows and `P` processes, each process may receive approximately `N/P` rows. Because `N` may not be divisible by `P`, a balanced partition assigns either floor(`N/P`) or ceil(`N/P`) rows to each process. The first `N mod P` processes receive one additional row.

A multidimensional decomposition divides a grid into rectangles or blocks. A two-dimensional decomposition often reduces communication relative to a one-dimensional decomposition because each subdomain has a smaller boundary-to-volume ratio. Nevertheless, the best decomposition depends on the global dimensions, process count, memory layout, network topology, and computational workload.

A process’s local array commonly includes halo or ghost cells. These cells store copies of neighboring values needed by a stencil computation. For a five-point stencil, one layer of halo cells is sufficient; wider stencils require more layers. The standard iteration is:

1. initiate exchange of boundary values;

2. compute interior points that do not depend on incoming halos;

3. complete halo communication;

4. compute boundary points;

5. proceed to the next iteration.

This organization enables communication–computation overlap and avoids waiting unnecessarily before useful local work begins.

Communication Cost and Scalability

A simple communication model represents the time to send a message of size `m` as:

`Tcomm ≈ α + βm`

where `α` is the latency cost of initiating communication and `β` is the inverse bandwidth. A computation that exchanges many small messages is latency-sensitive; one that exchanges fewer large messages is more bandwidth-sensitive. Real systems also incur synchronization, software overhead, contention, and topology-dependent costs.

Strong scaling measures how execution time changes when more processes solve a fixed-size problem. Weak scaling measures how execution time changes when both the problem size and process count increase proportionally. A program may exhibit good weak scaling but poor strong scaling if the local subproblem becomes too small and communication or synchronization dominates.

Important performance indicators include speedup, parallel efficiency, communication volume, load imbalance, memory consumption, and time spent waiting. Amdahl’s law remains relevant: a serial fraction limits the maximum speedup. In distributed applications, global reductions and barriers can effectively increase the serial or synchronization fraction.

Performance analysis should distinguish computation time from communication time and from idle time caused by imbalance. Profilers and MPI-aware tools can reveal message counts, sizes, wait states, collective costs, and rank-to-rank imbalance. Optimization should target measured bottlenecks rather than presumed ones.

A Communication–Computation Overlap Pattern

The following pseudocode illustrates a robust halo-exchange pattern for a one-dimensional decomposition:

```c

/ Post receives before sends. /

MPI_Irecv(left_halo, halo_size, MPI_DOUBLE, left, 1, comm, &r[0]);

MPI_Irecv(right_halo, halo_size, MPI_DOUBLE, right, 2, comm, &r[1]);

MPI_Isend(first_edge, halo_size, MPI_DOUBLE, left, 2, comm, &r[2]);

MPI_Isend(last_edge, halo_size, MPI_DOUBLE, right, 1, comm, &r[3]);

compute_interior();

MPI_Waitall(4, r, MPI_STATUSES_IGNORE);

compute_boundary();

```

At physical boundaries, a process may have no neighbor. Such cases should be represented explicitly, often by using `MPI_PROC_NULL`, which allows communication calls to complete harmlessly. This is cleaner than duplicating the entire exchange logic for edge processes.

Overlap must be designed with care. If `compute_interior` accidentally reads a halo that has not arrived, the program has a race-like correctness error. If it modifies a send buffer before the corresponding request completes, the message may be corrupted. The division between interior and boundary computation must therefore be documented and tested.

Applied Case: Distributed Two-Dimensional Heat Simulation

Background

Consider an illustrative engineering application that predicts temperature evolution on a metal plate. The plate is represented by a two-dimensional grid. At each time step, the temperature at an interior point is computed from the values of its four neighboring points. The global grid is too large for one node’s memory and must be distributed across a cluster.

Situation and problem

A straightforward parallel implementation assigns rectangular blocks to MPI processes. Each process requires one layer of neighboring values to update its local cells. If the implementation exchanges all four boundaries and then waits before doing any computation, the network delay is exposed at every time step. The objective is to preserve correctness while reducing idle time and maintaining balanced work.

Analysis

The stencil has nearest-neighbor communication. The interior points of each block depend only on values already stored locally, whereas points along the four edges depend on halo values from neighboring processes. Communication volume is proportional to the block perimeter, while computation is proportional to its area. Consequently, larger blocks generally improve the computation-to-communication ratio, but memory capacity and the total process count constrain block size.

A Cartesian communicator is appropriate because it represents the two-dimensional process grid. Neighbor ranks can be identified systematically, including periodic boundaries if the physical model requires them. Each process stores its local block with an additional halo layer.

Approach and solution

At initialization, the root or a parallel input routine distributes the initial temperature field. Processes create a Cartesian communicator and determine their north, south, east, and west neighbors. At each iteration, they post nonblocking receives for the four halo regions, post nonblocking sends for their own boundaries, and compute the interior. After `MPI_Waitall`, they update the boundary cells. Every fixed number of iterations, an `MPI_Allreduce` computes the global maximum change, allowing the program to test convergence consistently across all processes.

The convergence reduction should not be performed more often than necessary. If the numerical method permits, several local iterations may be completed before a global convergence check. This reduces synchronization, although the acceptable frequency depends on the algorithm’s numerical requirements.

Outcome

The design scales more effectively than a blocking exchange followed by a full-domain computation because useful interior work can proceed while messages are transferred. It also remains modular: the communicator describes the process topology, halo management is separated from numerical update logic, and the reduction expresses the global convergence condition directly.

Learning points

The case demonstrates that distributed performance depends on algorithmic structure, not merely on adding processes. Domain decomposition, boundary-to-volume ratio, nonblocking operations, collective reductions, and convergence semantics must be considered together. It also shows why correctness conditions—especially buffer ownership and request completion—must be established before optimization.

Common Errors, Risks, and Limitations

MPI programs can fail in ways that are intermittent and scale-dependent. Frequent mistakes include mismatched tags, reversed source and destination ranks, incorrect counts, invalid datatypes, missing collective calls, and failure to wait for nonblocking requests. A program that works with two processes may deadlock with four because a particular buffering behavior changes.

Global barriers are often inserted to make output appear orderly, but unnecessary barriers reduce scalability. Similarly, frequent `MPI_Allreduce` operations may serialize progress. Excessive use of wildcards can make message matching nondeterministic. Debug output from every rank can overwhelm the filesystem and obscure the real problem.

Load imbalance is another major limitation. Equal numbers of grid cells do not guarantee equal execution time when the computation depends on data, adaptive refinement, irregular geometry, or variable-cost functions. Dynamic redistribution may be necessary, but migration itself has a communication cost.

Failure handling is more difficult in traditional MPI because the default model often assumes that a process failure terminates the job. Large-scale production systems therefore require checkpointing, restart strategies, scheduler integration, and, where available, fault-tolerant MPI extensions. Reproducibility can also be affected by the order of floating-point reductions, since floating-point addition is not mathematically associative.

Professional and Governance Considerations

Distributed applications should be developed with reproducibility, traceability, and responsible resource use in mind. Scientific results should record software versions, compiler options, MPI implementation, process placement, input data, and relevant runtime settings. Numerical validation must compare parallel results with trusted serial or high-precision references, while recognizing that different reduction orders may produce small round-off differences.

Cluster resources are shared organizational assets. Efficient process placement, appropriate job sizes, and avoidance of unnecessary communication reduce energy consumption and queue pressure. Sensitive data transmitted between nodes should be protected according to institutional policy. MPI itself does not automatically provide application-level confidentiality or authorization; those concerns belong to the system, network, and software security design.

Current Developments and Best Practices

Modern applications increasingly combine MPI with intra-node threading or accelerator programming. MPI may distribute subdomains among nodes, while OpenMP, CUDA, HIP, or another model handles computation within a node. This hybrid approach can reduce the number of MPI processes and improve use of shared resources, but it introduces additional concerns involving thread safety, memory placement, and host–device transfers.

MPI implementations continue to develop topology-aware collectives, persistent communication, neighborhood collectives, partitioned communication, and improved support for accelerators. These features can reduce repeated setup costs or express regular communication patterns more directly. They should be adopted based on measured need and verified implementation support.

Reliable MPI development follows several practices:

  • define data ownership and communication contracts explicitly;
  • use communicators to isolate application modules;
  • prefer established collectives or `MPI_Sendrecv` for regular patterns;
  • post receives before sends in symmetric exchanges;
  • use nonblocking operations only with clear buffer-lifetime rules;
  • eliminate unnecessary barriers and global reductions;
  • balance computation and communication through suitable decomposition;
  • validate results at small process counts before scaling;
  • measure with representative problem sizes and realistic placement;
  • retain a clear, correct version before applying performance optimizations.

Chapter Summary

Distributed-memory computing assigns each process private memory and requires explicit communication. MPI provides the principal abstractions: ranks, communicators, messages, datatypes, point-to-point operations, collectives, and requests. Point-to-point communication supports direct data exchange, while collective operations express common group-wide patterns such as broadcast, scatter, gathering, and reduction. Communicators provide process-group isolation and support logical topologies.

Domain decomposition transforms a global problem into local subproblems connected by halo exchanges. Its quality depends on load balance, memory capacity, communication volume, and boundary-to-volume ratio. Nonblocking operations can overlap communication with independent computation, but only when buffer ownership, request completion, and data dependencies are respected. Scalability is shaped by latency, bandwidth, synchronization, imbalance, and serial work. Effective MPI design therefore combines algorithmic correctness, communication awareness, measurement, and disciplined software engineering.

Key Terms

Rank: The integer identifier of a process within a communicator.

Communicator: An MPI object defining a process group and an isolated communication context.

Message: Data transferred between MPI processes together with matching information.

Tag: An integer label used to distinguish messages.

Point-to-point communication: Direct communication between a sender and a receiver.

Collective communication: An operation involving all processes in a communicator or subgroup.

Blocking operation: An operation that does not return until its defined completion condition is satisfied.

Nonblocking operation: An operation that initiates communication and returns a request for later completion testing.

MPI request: An object representing an outstanding nonblocking operation.

Halo or ghost cell: A local copy of neighboring data required by a subdomain computation.

Domain decomposition: Division of a global computational problem into process-owned subdomains.

Derived datatype: An MPI description of a possibly noncontiguous memory layout.

Cartesian communicator: A communicator that represents processes arranged in a logical grid.

Deadlock: A state in which processes wait indefinitely for events that cannot occur.

Latency: The fixed cost associated with initiating communication.

Bandwidth: The rate at which data can be transferred after communication begins.

Strong scaling: Performance behavior for a fixed problem size as process count increases.

Weak scaling: Performance behavior when problem size increases with process count.

Communication–computation overlap: Execution of independent computation while communication is in progress.

Load imbalance: Unequal computational or communication work among processes.

Review Questions

Short Answer Questions

1. What information is used to match an MPI message with a receive operation?

2. Why must a nonblocking send buffer not be modified before completion?

3. Distinguish `MPI_Bcast` from `MPI_Scatter`.

4. What is the purpose of a halo region in a stencil computation?

5. Why can frequent global reductions limit scalability?

Long Answer Questions

1. Explain the MPI execution model, including ranks, communicators, initialization, and finalization.

2. Compare blocking and nonblocking point-to-point communication, including their correctness requirements and performance implications.

3. Describe how a Cartesian domain decomposition can be designed for a two-dimensional numerical grid.

4. Explain the role of collective communication in distributed scientific applications and discuss its limitations.

5. Analyze the factors that determine the scalability of a distributed-memory application.

Analytical / Application Questions

1. Design a halo-exchange sequence for a two-dimensional five-point stencil using nonblocking operations. Identify which cells can be computed before communication completes.

2. A program performs one `MPI_Allreduce` at every iteration and shows declining efficiency as process count increases. Analyze the likely causes and propose alternatives.

3. A blocking MPI program deadlocks when every process sends its boundary before receiving. Explain the mechanism and redesign the exchange using either `MPI_Sendrecv` or nonblocking operations.

22. Multiple Choice Questions with Answers

Question 1

Which MPI object prevents messages from one communication module from accidentally matching messages in another module?

A. Datatype

B. Communicator

C. Request

D. Status

Correct Answer: B. Communicator

Question 2

A process posts MPI_Irecv and MPI_Isend, then computes interior grid points. What must occur before it computes boundary points that depend on incoming halos?

A. MPI_Finalize

B. MPI_Bcast

C. Completion of the communication requests

D. Creation of a new communicator

Correct Answer: C. Completion of the communication requests

Question 3

Which operation combines values from all processes and makes the result available to every process?

A. MPI_Reduce

B. MPI_Gather

C. MPI_Allreduce

D. MPI_Scatter

Correct Answer: C. MPI_Allreduce

Question 4

Two neighboring processes both issue blocking sends before receiving, and the messages cannot be buffered. What is the most likely result?

A. Automatic load balancing

B. Deadlock

C. Data compression

D. Successful asynchronous execution

Correct Answer: B. Deadlock

Question 5

In a regular stencil computation, why are halo cells used?

A. To store MPI ranks permanently

B. To duplicate neighboring values needed by local updates

C. To eliminate all communication

D. To increase the serial fraction

Correct Answer: B. To duplicate neighboring values needed by local updates

Question 6

For a message of size m, the model Tcomm ≈ α + βm indicates that α primarily represents what?

A. Computation per grid point

B. Message initiation latency

C. Floating-point precision

D. Load imbalance

Correct Answer: B. Message initiation latency

Question 7

Which design best supports communication–computation overlap in a stencil iteration?

A. Wait for all messages before computing any cell

B. Compute independent interior cells while halo transfers are outstanding

C. Use a barrier after every local arithmetic operation

D. Modify all send buffers immediately after posting sends

Correct Answer: B. Compute independent interior cells while halo transfers are outstanding

Question 8

Why might a two-dimensional block decomposition outperform a one-dimensional decomposition?

A. It always eliminates halo exchanges

B. It can reduce the boundary-to-volume ratio for each subdomain

C. It requires no process coordination

D. It guarantees identical execution time for all processes

Correct Answer: B. It can reduce the boundary-to-volume ratio for each subdomain

Question 9

Which statement about collective operations is correct?

A. Only the root must call a collective operation

B. Processes may call collectives in arbitrary incompatible orders

C. Collective calls must be executed compatibly by the participating communicator members

D. Collectives never involve synchronization costs

Correct Answer: C. Collective calls must be executed compatibly by the participating communicator members

Question 10

A nonblocking receive has been posted, but the application reads its receive buffer before MPI_Wait or MPI_Test confirms completion. What is the principal risk?

A. The communicator is automatically duplicated

B. The application may use incomplete or invalid data

C. The message is guaranteed to be retransmitted

D. The process becomes serial automatically

Correct Answer: B. The application may use incomplete or invalid data

High Performance Computing
Chapter 6 of 15