小作业零:pow_a¶
实验任务¶
使用 MPI
和 OpenMP
并行化下述代码,代码的作用是计算 b[i]=a[i]^m,其中 a[i] 和 b[i] 是两个长度为 n 的数组。
void pow_a(int *a, int *b, int n, int m) {
for (int i = 0; i < n; i++) {
int x = 1;
for (int j = 0; j < m; j++)
x *= a[i];
b[i] = x;
}
}
实验文件在集群上的位置为 /home/course/hpc/assignments/2025/exp0
。
注意事项¶
- 本次作业仅需要通过并行化外层循环
for (int i = 0; i < n; i++)
对代码进行加速,不需要对代码进行进一步优化。在实现正确的情况下,本次作业不会对性能进行评分。 - 此代码的计算结果可能会超出
int
的范围,但不会影响正确性检验。
实验步骤¶
一、运行程序¶
首先将实验文件 拷贝到自己的目录 ,并进入 exp0
目录:
cp -R /home/course/hpc/assignments/2025/exp0 ~/
cd ~/exp0/
直接运行 ./submit.sh
,应当得到与下面类似的输出:
g++ openmp_pow.cpp -O3 -std=c++11 -fopenmp -o openmp_pow
mpicxx mpi_pow.cpp -O3 -std=c++11 -o mpi_pow
openmp_pow: n = 112000, m = 100000, thread_count = 1
Congratulations!
Time Cost: 14022364 us
openmp_pow: n = 112000, m = 100000, thread_count = 7
Congratulations!
Time Cost: 14004209 us
openmp_pow: n = 112000, m = 100000, thread_count = 14
Congratulations!
Time Cost: 14004001 us
openmp_pow: n = 112000, m = 100000, thread_count = 28
Congratulations!
Time Cost: 14007161 us
mpi_pow: n = 112000, m = 100000, process_count = 1
Wrong answer at position 34133: 0 != -259604863
srun: error: conv1: task 0: Exited with exit code 1
前两行分别对实验框架中的 OpenMP
版本 (openmp_pow.cpp
)和 MPI
版本(mpi_pow.cpp
)进行编译,对应于 submit.sh
中的 make -j4
命令。
之后 16 行,每 4 行表示一次提交 OpenMP
版本代码后的输出,对应于 submit.sh
中的 OMP_NUM_THREADS=xxx srun -N 1 ./openmp_pow 112000 100000 0
。即便不进行代码修改,框架中的 OpenMP
版本也能计算出正确的结果,因此你能在输出中看到表示结果正确的 Congratulations
。然而框架中的原始代码是串行的,所以这 4 次提交会得到相同的运行时间。在本次实验的后续步骤中,你需要通过并行加速这个程序,使得使用不同线程数的运行时间有较显著的区别。
最后 3 行,表示提交了一次 MPI
版本代码后的输出,对应于 submit.sh
中的 srun -N 1 -n 1 --cpu-bind sockets ./mpi_pow 112000 100000 0
。框架中MPI
版本的原始代码无法得到正确的结果,你需要通过后续步骤使之正确运行,输出 Congratulations!
。如果所有测试全部通过,脚本的最后会输出一行 All done!
。
由于机器数量有限,在输出中有可能额外包含以下内容,分别表示作业正在排队/开始运行,请耐心等待:
srun: job 271 queued and waiting for resources
srun: job 271 has been allocated resources
为了能够顺利进行之后的若干次实验,请理解 submit.sh
、Makefile
两个文件中各行命令的具体含义,尤其是如何加载 Spack、调用编译器、使用 srun
命令提交作业。
二、OpenMP 并行¶
在课上,我们使用 #pragma omp parallel for num_threads(NUM_THREADS)
对一个循环进行了并行,其中 NUM_THREADS
表示执行这个循环的线程数量。你也可以通过设置 OMP_NUM_THREADS
环境变量的方式指定 OpenMP
使用的线程数量,即编译命令不变,使用 OMP_NUM_THREADS=3 ./hello
运行程序,此时指导语句仅需要写成 #pragma omp parallel for
,如下所示。
#include <omp.h>
#include <stdio.h>
static const int N = 3;
int main() {
#pragma omp parallel for
for (int tid = 0; tid < N; ++tid) {
printf("[%d/%d] Hello\n", omp_get_thread_num(), omp_get_num_threads());
}
}
即使循环的迭代次数与使用的线程数不相等,OpenMP
也能正确处理。例如,你可以尝试使用 OMP_NUM_THREADS=2 ./hello
和 OMP_NUM_THREADS=3 ./hello
运行以上代码,并观察输出。
在本次作业中,你需要使用 #pragma omp parallel for
并行实验框架 openmp_pow.cpp
的 pow_a
函数的外层循环。如果实现成功,在运行 ./submit.sh
后,你可以看到接近线性的加速。
三、 MPI 并行¶
进程之间存在数据隔离,即一个进程无法直接访问其他进程的数据。因此一般而言,使用 MPI
进行并行会比使用 OpenMP
复杂。但使用 MPI
编写的程序可以运行在多台机器上,而使用 OpenMP
的程序仅能在一台机器上运行。
本次作业实现 MPI
并行的方式为:
- 0 号进程使用
MPI_Scatter
函数将数组a
均匀分发至各个进程 - 各个进程分别对拥有的 \displaystyle \frac{n}{总进程数} 个数据计算 b[i]=a[i]^m
- 各个进程使用
MPI_Gather
函数将运算结果发送回 0 号进程
核心代码如下:
// 进程 i 获取进程 0 中位于
// [my_rank * (n / comm_sz), (my_rank + 1) * (n / comm_sz)) 位置的数据
MPI_Scatter(
// 待分发的数据, 向每个进程发送的数据个数, 数据类型
root_a, n / comm_sz, MPI_INT,
// 接收到的数据的存储位置,每个进程收到的数据个数,数据类型
a, n / comm_sz, MPI_INT,
// 进行分发的进程编号, 参与此次通信的进程集合(MPI_COMM_WORLD表示所有进程)
0, MPI_COMM_WORLD
);
// 计算 b[i] = a[i]^(m)
pow_a(a, b, n, m, comm_sz);
// 进程 0 收集各进程的运算结果
MPI_Gather(
// 待发送的数据, 每个进程发送的数据个数, 数据类型
b, n / comm_sz, MPI_INT,
// 接收到的数据的存储位置, 从每个进程收到的数据个数, 数据类型
root_b, n / comm_sz, MPI_INT,
// 进行接收的进程编号, 参与此次通信的进程集合(MPI_COMM_WORLD表示所有进程)
0, MPI_COMM_WORLD
);
你需要填充 pow_a
函数,以使程序正确运行,输出 Congratulations!
。一种可行的填充方式如下,其中 local_n
表示进程拥有的数据个数。为了实现简便,你只需要考虑 n 能被进程数整除的情况。
for (int i = 0; i < local_n; i++) {
int x = 1;
for (int j = 0; j < m; j++)
x *= a[i];
b[i] = x;
}
实验提交¶
此实验仅需提交实验报告,将 PDF 文件 提交至网络学堂(无需代码)。包含以下内容:
openmp_pow.cpp
和mpi_pow.cpp
中修改后函数pow_a
的源代码。- 对
openmp
版本,报告使用 1, 7, 14, 28 线程在 n=112000,m=100000 下的运行时间,及相对单线程的加速比。 - 对
MPI
版本,报告 1\times1,1\times7,1\times14,1\times28,2\times28 进程(N\times P 表示 N 台机器,每台机器 P 个进程)在 n=112000,m=100000 下的运行时间,及相对单进程的加速比。