跳转至

小作业零:pow_a

实验任务

使用 MPIOpenMP 并行化下述代码,代码的作用是计算 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/2024/exp0

注意事项

  1. 本次作业仅需要通过并行化外层循环 for (int i = 0; i < n; i++) 对代码进行加速,不需要对代码进行进一步优化。在实现正确的情况下,本次作业不会对性能进行评分。
  2. 此代码的计算结果可能会超出 int 的范围,但不会影响正确性检验。

实验步骤

一、运行程序

首先将实验文件 拷贝到自己的目录 ,并进入 exp0 目录:

cp -R /home/course/hpc/assignments/2024/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.shMakefile 两个文件中各行命令的具体含义,尤其是如何加载 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 ./helloOMP_NUM_THREADS=3 ./hello 运行以上代码,并观察输出。

在本次作业中,你需要使用 #pragma omp parallel for 并行实验框架 openmp_pow.cpppow_a 函数的外层循环。如果实现成功,在运行 ./submit.sh 后,你可以看到接近线性的加速。

三、 MPI 并行

进程之间存在数据隔离,即一个进程无法直接访问其他进程的数据。因此一般而言,使用 MPI 进行并行会比使用 OpenMP 复杂。但使用 MPI 编写的程序可以运行在多台机器上,而使用 OpenMP 的程序仅能在一台机器上运行。

本次作业实现 MPI 并行的方式为:

  1. 0 号进程使用 MPI_Scatter 函数将数组 a 均匀分发至各个进程
  2. 各个进程分别对拥有的 \displaystyle \frac{n}{总进程数} 个数据计算 b[i]=a[i]^m
  3. 各个进程使用 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 文件 提交至网络学堂(无需代码)。包含以下内容:

  1. openmp_pow.cppmpi_pow.cpp 中修改后函数 pow_a 的源代码。
  2. openmp 版本,报告使用 1, 7, 14, 28 线程在 n=112000m=100000 下的运行时间,及相对单线程的加速比。
  3. MPI 版本,报告 1\times11\times71\times141\times282\times28 进程(N\times P 表示 N 台机器,每台机器 P 个进程)在 n=112000m=100000 下的运行时间,及相对单进程的加速比。

最后更新: 2024年3月12日
作者: Harry Chen (22.29%), heheda (76.43%), 翟明书 (1.27%)