Introduction
Hello! Welcome back to our journey into distributed-memory parallelism with MPI.
In our last lesson, you mastered MPI_Scatter and MPI_Gather, the fundamental tools for distributing unique chunks of data to processes and collecting them back. This "distribute-compute-collect" pattern is at the heart of many parallel algorithms.
Today, we will apply these skills to a classic and highly practical problem: parallel matrix-vector multiplication (). This operation is a cornerstone of scientific and engineering computing, appearing in fields from finite element analysis in mechanical engineering to solving large systems of linear equations. By the end of this lesson, you will be able to implement a parallel version of this algorithm using the MPI collectives you've learned.
Our strategy will be based on a straightforward row-wise data decomposition, where we split the matrix A by rows and assign a block of rows to each process.
1. Decomposing the Problem
Let's first recall the sequential algorithm for matrix-vector multiplication, where A is an matrix and x is an vector:
The key insight for parallelization is that the calculation of each element of the result vector is independent of all other elements (where ). This is a perfect example of data parallelism.
Our approach will be to give each process a "stripe" of rows from the matrix A. This is known as row-wise block-striped decomposition.

This leads to a critical question: what data does each process need to perform its work?
- Unique Data: Each process needs its own unique block of rows from matrix
A. - Shared Data: To compute the inner product for any given row, a process needs access to the entire vector
x. Therefore, the vectorxmust be available to all processes.
With this understanding, we can outline a simple and effective parallel algorithm.
The following presentation slides clearly outline the steps for a parallel matrix-vector multiplication using this row-wise strategy. It's a great high-level overview.
Please review the slide titled 'Putting gather, scatter and broadcast together'. It presents a six-step algorithm that we will follow directly.
As you saw in the slides, the algorithm is:
- The root process (e.g., rank 0) initializes the full matrix
Aand vectorx. - The root scatters the rows of matrix
Aamong all processes. - The root broadcasts the vector
xto all processes. - Each process computes the matrix-vector product for its local rows, producing a partial result vector.
- The root gathers the partial results from all processes.
- The root now holds the complete final result vector
y.
2. Mapping the Algorithm to MPI Collectives
Now, let's map this algorithm to the MPI functions you know. Your proficiency in C++ and understanding of memory layouts will be helpful here, especially when dealing with the 2D matrix.
Step 1: Handling the Matrix Data
A 2D matrix in C++ is often represented as a pointer-to-pointer (int**) or allocated as a single contiguous block of memory (a 1D array). For MPI, a single contiguous block is strongly preferred because MPI scatter/gather operations work most easily with flat memory buffers.
You can allocate an matrix as a 1D array and access element using A[i * n + j].
Step 2: Distributing the Data (MPI_Scatter & MPI_Bcast)
- Distributing Matrix
A: The root process has the full matrix. We want to send a unique block of rows to each process. This is a perfect job forMPI_Scatter. If we havepprocesses and anm x nmatrix, each process will receivem/prows, which corresponds to(m/p) * nelements from the flattened 1D array. - Distributing Vector
x: Every process needs an identical copy of the entire vectorx. The right tool for this isMPI_Bcast.
Step 3: Local Computation
Each process now has its local rows of A (let's call it A_local) and the full vector x. It can now perform a standard sequential matrix-vector multiplication to produce its piece of the result, y_local.
Step 4: Collecting the Results (MPI_Gather)
Each process holds a partial result vector y_local. To form the final vector y, we need to collect these pieces and concatenate them in rank order. This is the exact function of MPI_Gather. The root process will receive all the y_local vectors into its main y buffer.
This complete process is a beautiful demonstration of how different MPI collectives work together to solve a larger problem.
[PDF] Chapter 8: Matrix-Vector Multiplication
For a more detailed look at the row-wise block-striped algorithm, let's turn to this chapter on matrix-vector multiplication. It provides formal pseudo-code and analysis.
Please read the first few paragraphs of section 8.5, 'Row-Wise Block-Striped Matrix Decomposition', stopping before section 8.5.1. Focus on the numbered list that outlines the algorithm steps. Notice its similarity to the 6-step plan we just discussed. This resource uses MPI_Allgatherv for the final collection, which is a powerful alternative to MPI_Gather if all processes need the final result, but for our case, MPI_Gather is sufficient.
3. Hands-on Practice: Implementing the Algorithm
Now it's time to translate this design into a working C++ program. Your task is to implement parallel matrix-vector multiplication using the scatter-broadcast-gather pattern.
Your Task:
Assume you have a matrix A of size M x N and a vector x of size N. For simplicity, ensure that M is perfectly divisible by the number of MPI processes.
-
Initialization (Root Process):
- Let the root process (rank 0) define the matrix dimensions (e.g.,
M=12,N=4). - Dynamically allocate a 1D array for matrix
Aof sizeM * Nand a 1D array for vectorxof sizeN. - Initialize
Aandxwith some values (e.g.,A[i*N + j] = iandx[j] = 1.0). - Allocate a 1D array for the final result vector
yof sizeM.
- Let the root process (rank 0) define the matrix dimensions (e.g.,
-
Data Distribution (All Processes):
- Calculate
rows_per_proc = M / world_size. - Each process allocates a local buffer
A_localof sizerows_per_proc * Nto receive its rows of the matrix. - Each process allocates a buffer for the full vector
xof sizeN. - Use
MPI_Scatterto distributeAfrom the root. Thesendcountandrecvcountwill berows_per_proc * N. - Use
MPI_Bcastto broadcastxfrom the root. Thecountwill beN.
- Calculate
-
Local Computation (All Processes):
- Each process allocates a local result buffer
y_localof sizerows_per_proc. - Perform the matrix-vector multiplication using
A_localandx, storing the result iny_local.
// Pseudocode for local computation for (int i = 0; i < rows_per_proc; ++i) { y_local[i] = 0.0; for (int j = 0; j < N; ++j) { y_local[i] += A_local[i * N + j] * x[j]; } } - Each process allocates a local result buffer
-
Result Collection (All Processes):
- Use
MPI_Gatherto collect they_localresults from all processes into theyvector on the root. Thesendcountandrecvcountwill berows_per_proc.
- Use
-
Verification (Root Process):
- The root process should print the final vector
yto verify the result.
- The root process should print the final vector
Take your time with this exercise. Managing the buffer sizes and counts for the different MPI calls is the main challenge and a crucial skill to develop.
4. A Glimpse at a More Scalable Approach
The row-wise method is simple and effective, but it has a scalability limitation. As you increase the number of processes, every single process still receives a full copy of vector x. The MPI_Bcast operation and the memory required to store x on every node can become a bottleneck for very large-scale problems.
A more advanced technique, called 2D checkerboard decomposition, partitions both the matrix A and the vectors x and y.
[PDF] Chapter 8: Matrix-Vector Multiplication
You don't need to read this in depth, but I want you to be aware of more advanced strategies. This section of the resource introduces the checkerboard method.
Skim the first part of section 8.7, particularly Figure 8.6 and the five questions that follow. Notice how processes are arranged in a 2D grid and only need a piece of the input vector x. This significantly reduces communication overhead.
This 2D approach is more complex to implement, requiring you to organize processes into a grid using MPI's Cartesian communicator features (MPI_Cart_create, MPI_Comm_split). However, it is far more scalable and is the standard for high-performance linear algebra libraries.
Conclusion
Excellent work today! You have successfully designed and implemented a parallel algorithm for a non-trivial and important computational kernel. This exercise ties together much of what you've learned about MPI collectives.
Key Takeaways:
- Algorithm is Key: Parallel programming starts with decomposing the problem. For matrix-vector multiplication, a row-wise decomposition is a natural starting point.
- Map Algorithm to MPI Tools: We mapped our communication needs directly to MPI functions:
MPI_Scatterfor decomposing the matrix,MPI_Bcastfor replicating shared data, andMPI_Gatherfor aggregating the results. - Memory Layout Matters: Using a contiguous 1D array for your matrix simplifies communication with MPI.
- Scalability is a Goal: While our approach works, we noted its limitations and saw a preview of a more scalable 2D decomposition strategy.
Preview of the Next Lesson
In our discussion of the 2D decomposition, we mentioned that it reduces communication overhead. But how do we quantify this? In the next lesson, we will learn how to model communication overhead and analyze the performance of our parallel applications. We'll explore strategies like message aggregation to see how we can further optimize our MPI programs by minimizing the time spent sending and receiving data.