MPI 实验手册 2014 年 5 月
实验环境说明 : 虚拟机 :Vmware Workstation 9 Linux 系统 :CentOS 6.3 每台机器上有 3 个未配置的虚拟机节点用于进行 MPI 环境配置实验, 有 3 个已配置好 的节点可以直接运行 MPI 程序 3 台已配置好的 Linux 虚拟机 IP 地址如下, 可以登录系统用 ifconfig 指令查看 node1 192.168.1.11 node2 192.168.1.12 node3 192.168.1.13 管理用户账户 : root, 密码 :123456 一般用户账号 :cloud, 密码 :123456 实验中用到的主要 Linux 指令 切换到 root 用户 su root 切换到 cloud 用户 su cloud 配置网络 setup 网络重启 service network restart 修改主机名 vim /etc/sysconfig/network 新建目录 mkdir 通过 vim 新建文档 vim 重启机器 reboot 删除文件夹及其中所用的文件 rm rf 解压缩文件 tar xvzf 修改文件权限, 只有所有制才有读和写的权限 chmod 600 复制文件 cp 远程复制文件 scp 查看主机名 hostname
实验 1: 在虚拟机环境下搭建 3 个节点的 MPI 集群 实验任务 在虚拟机环境下搭建拥有 4 个节点的 MPI 集群 实验步骤 1. 安装 Linux 系统 2. 保证每个节点的 sshd 服务 (Secure Shell) 能正常启动 (root 权限 ) su root service sshd start 3. 永久关闭每个节点的防火墙 (root 权限 ), 避免 MPI 不能访问网络造成程序执行出错 chkconfig iptables off// 永久性生效, 重启后不会复原 4. 为每个节点分配 IP 地址 (root 权限 ) IP 地址最好连续分配, 实验中我们将其设置为 192.168. 1.11 192.168.1.12 192.168.1.13 setup service network restart 注 : 这些 IP 地址就是要搭建的并行环境的节点对应的 IP 地址, 用户可以通过这些 IP 访问对应的节点 5. 修改机器名 (root 权限 ) vim /etc/sysconfig/network 将 HOSTNAME=localhost.localdomain 中的 localhost.localdomain 改为当前节点的 名称, 如 HOSTNAME=node1 HOSTNAME=node2 修改完后通过 hostname 指 令可以查看机器名 6. 配置 /etc/hosts 文件 (root 权限 ) 该文件可以实现 IP 地址和机器的对应解析, 所有节点的该文件均要按下面的内容修改 注意 127.0.0.1 localhost localhost.localdomain 要保留, 否则启动 mpd 服务时将出错 : 127.0.0.1 localhost localhost.localdomain 192.168.1.11 node1 192.168.1.12 node2 192.168.1.13 node3 重启系统 reboot 通过以上配置后节点之间能够通过各节点的机器名称相互访问 例如, 可以通过 ping node2 或 ssh node2 进行测试 注 : 用户配置 /etc/hosts 文件的意义, 就是给节点 IP 起了个别名 : 如第一行 192.168.1.11 node1 就是用 node1 来标识 192.168.1.11 这样我们就可以直接 ping node1 而不用输入很长的 IP 地址了 7. 挂载 NFS 文件系统 由于 MPICH 的安装目录和用户可执行程序在并行计算时需要在所有节点存副本, 而且 目录要相互对应, 每次一个节点一个节点地复制非常麻烦, 采用 NFS 文件系统后可以实现 所有节点内容与主节点内容同步更新, 并自动实现目录的对应 NFS 文件系统使得所有机 器都能以同样的路径 ( 假设为 /usr/cluster) 访问服务器上保存的文件, 访问方法如同对本地 文件的访问 这对于部分采用 MPI 进行并行计算的用户来说可能是必须的, 通常我们会将 MPICH 的安装目录及并行程序存放目录配置为 NFS 共享目录, 这样可以省去将文件向各个 节点复制的麻烦, 大大提高工作效率 NFS 文件系统的配置方法示例如下 ( 假设 NFS 服务器 IP 为 192.168.1.11, 配置需在 root 用户下完成 )
(1) 服务器端配置方法 ( 下面的配置只在主节点进行 ) 1 /etc/exports 文件配置 在文件 /etc/exports 中增加以下几行, 设定共享出去的目录信息和权限 : /usr/cluster 192.168.1.12(rw) /usr/cluster 192.168.1.13(rw) 这几行文字表明 NFS 服务器向 IP 地址为 192.168.1.12,192.168.1.13 的节点共享其 /usr/cluster 目录, 并使这些节点具有可读写权限 如有更多的节点可按此方法填写 2 启动 NFS 服务 启动 NFS 服务只需要以下两个命令, 必须先启动 rpc 才能启动 nfs, 不然端口不能注册, 无法进行启动 : service rpcbind start service nfs start 到此 IP 为 192.168.1.11 的服务器已向其他两个节点提供 /usr/cluster 目录的文件共享 可 以查看 192.168.1.12 192.168.1.13 的 /usr/cluster 查看 cd /usrcluster (2) 客户端配置方法 ( 需要在所有子节点做同样的配置 ) 1 建立共享目录 建立与服务器相同的共享目录用于共享服务器文件 :(root 权限 ) mkdir /usr/cluster 2 查看服务器已有的共享目录 ( 这步可省略 ) showmount -e 192.168.1.11 通过这条命令我们可以查看 IP 地址为 192.168.1.11 的服务器可以共享的目录情况 3 挂载共享目录 mount -t nfs 192.168.1.11:/usr /cluster /usr/cluster 这一命令将 NFS 服务器 192.168.1.11 上的共享目录挂载到本地 /usr/cluster 目录下 我们 也可在所有子节点的 /etc/fstab 文件中输入以下的代码, 使文件系统在启动时实现自动挂载 NFS: 192.168.1.11:/usr/cluster /usr/cluster nfs defaults 0 0 至此我们已可以实现对 NFS 共享目录的本地访问, 所有子节点的 /usr/cluster 文件夹都共享 了 NFS 服务器的同名文件夹的内容, 我们可以像访问本地文件一样访问共享文件 MPICH 的 安装目录和用户存放并行程序的文件夹都需实现 NFS 共享, 从而避免了每次向各节点发送程序 副本 8. 配置 ssh 实现 MPI 节点间用户的无密码访问 (1) 先在 node1 上做以下操作, 生成了私钥 id_dsa 和公钥 id_dsa.pub, 具体操作方法如 下 cd /home/cloud mkdir ~/.ssh cd ~/.ssh ssh-keygen -t dsa ssh-keygen 的作用是让 Linux 机器之间可以无密码访问 系统显示如下信息, 遇到系统询问直接回车即可 Generating public/private dsa key pair. Enter file in which to save the key (/home/user/.ssh/id_dsa): Enter passphrase (empty for no passphrase): Enter same passphrase again: Your identification has been saved in /home/user/.ssh/id_dsa. Your public key has been saved in /home/user/.ssh/id_dsa.pub. The key fingerprint is: a9:8a:c7:0b:a6:59:71:03:92:ff:ac:e9:96:c2:5a:74 (2) 将该密钥用作认证, 进行访问授权 将公钥改名为 authorized_keys, 删除群组 (g) 与其他人 (o) 对目录的读写执行权限, 按如下命令在 node1 执行
cp ~/.ssh/id_dsa.pub ~/.ssh/authorized_keys chmod go-rwx ~/.ssh/authorized_keys (3) 将 ~/.ssh 目录下的文件复制到所有节点 scp -r ~/.ssh node2: scp -r ~/.ssh node3: (4), 每个节点输入 ssh-add (5) 检查是否可以直接 ( 不需要密码 ) 登录其他节点 ssh node1 ssh node2 如能两两之间不需密码登录其他节, 则表明配置成功 这一步非常重要, 确保两两节点之间可无密码访问, 注意, 两个节点第一次无密码访问 的时候需要确认, 以后将无需再确认 9. 安装 MPICH2 (1) 主节点下载 MPICH2 压缩包, 放在到 /usr/cluster 目录下, 在该目录下解压 (root 权限 ) tar xvzf mpich2-1.0.tar.gz 解压完成后将在当前目录生成一个 MPICH 文件目录 (2) 进入 MPICH 解压后的目录 mpich2-1.2.1p1, 配置安装目录 cd /usr/cluster/mpich2-1.2.1p1./configure --prefix=/usr/cluster/mpich2 根据以上配置 MPICH 将安装在目录 /usr/cluster/mpich2, 并确保所有节点已建立针对该 目录的 NFS 共享 (3) 编译安装 MPICH2 进入解压后的 MPICH 文件目录, 分别执行 make 和 make install 指令, 这会花一段较长的时间 make make install (4) 在当前用户目录下建立并编辑配置文件 mpd.hosts, 该文件用于存放集群节点主 机名 将所有你允许访问本机进行并行计算的机器名填入, 一行一个机器名, 如果该机器上 有两个 CPU, 就将它的名字加入两次, 以此类推 例如, 在我们实例中的 mpd.hosts 文件内 容如下 :( 用户权限, 每个节点都进行操作 ) node1 node2 node3 注意, 文中包含自己 ( 即给自己放权 ) 的目的是为了在只有一个节点时也可以模拟并行 计算环境 (5) 配置环境变量 编辑 MPI 用户主目录下的 ~/.bashrc 文件, 增加一行 :( 用户权限, 每个节点都进行操作 ) export PATH="$PATH:/usr/cluster/mpich2/bin" 这一行代码将 MPI 的安装路径加入用户的当前路径列表 重新打开命令行窗口后生效 定 (6) 启动 mpd 守护进程 运行 mpirun, 首先要运行 mpd 在启动 mpd 守护进程前要 在各个节点的用户主目录下生成一个.mpd.conf 文件, 将其权限设置为只有所有者才能 访问, 具体步骤如下 : cd /home/cloud touch.mpd.conf chmod 600.mpd.conf mpd&.mpd.conf 文件的内容为 :( 用户权限 ) secretword=123456 其中, 123456 为识别口令, 在所有节点中都建立该文件并保持口令一致, 口令可自己设 mpd& 为启动本地 mpd 的命令, 我们也可以采用以下命令同时启动 mpd.hosts 中所列节
点的 mpd mpdboot n < 节点个数 > -f mpd.hosts 如 : mpdboot n 4 -f mpd.hosts 这一命令将同时在 mpd.hosts 文件中所指定的节点上启动 mpd 管理器 mpd 启动后执行 mpdtrace l 可以查看各个节点机器名 (7) 编译 运行一个简单的测试程序 cpi, 这是一个 MPICH 自带的计算 π 值的并行示 例程序, 该例程在 MPICH 的 examples 目录下 运行命令如下 : mpirun -np 3./cpi 运行结果如下 Process 0 of 3 is on node0 Process 1 of 3 is on node1 Process 2 of 3 is on node2 pi is approximately 3.1415926544231239, Error is 0.0000000008333307 wall clock time = 0.021253 得到这样的输出结果表示我们所搭建的机群系统已经可以成功地运行 MPI 作业 mpi 的编译命令为 mpicc, 如编译 test.c 可用如下命令 : mpicc o test test.c 实验 2: 编写 编译 运行对一个并行程序 hello parallel world 实验目的 熟悉最基本的 MPI 函数, 熟悉 MPI 程序结构, 编译运行第一个 MPI 程序 实验内容 本实例将介绍一段最简单的并行程序, 相信学员看了这段程序之后对并行程序的畏惧将 消失一大半, 这段程序虽然简单, 主程序只有三行, 但它确实实现了多个计算节点的共同工 作, 也是一个真正意义上的并行程序 MPI 函数说明 (1) 并行初始化函数 :int MPI_Init(int *argc, char ***argv) 参数描述 :argc 为变量数目,argv 为变量数组, 两个参数均来自 main 函数的参数 MPI_Init() 是 MPI 程序的第一个函数调用, 标志着并行程序部分的开始, 它完成 MPI 程序的初始化工作, 所有 MPI 程序并行部分的第一条可执行语句都是这条语句 该函数的 返回值为调用成功标志 同一个程序中 MPI_Init() 只能被调用一次 函数的参数为 main 函 数的参数地址, 所以并行程序和一般 C 语言程序不一样, 它的 main 函数参数是不可缺少的, 因为 MPI_Init() 函数会用到 main 函数的两个参数 (2) 并行结束函数 :int MPI_Finalize() MPI_Finalize() 是并行程序并行部分的最后一个函数调用, 出现该函数后表明并行程序 的并行部分的结束 一旦调用该函数后, 将不能再调用其他的 MPI 函数, 此时程序将释放 MPI 的数据结构及操作 这条语句之后的代码仍然可以进行串行程序的运行 该函数的调用 较简单, 没有参数 并行源代码 /* 文件名 :hello.c*/ #include "mpi.h"
#include <stdio.h> int main(int argc,char **argv) 编译指令 MPI_Init(&argc,&argv); // 并行部分开始 printf("hello parallel world!\n"); MPI_Finalize(); // 并行部分结束 mpicc o hello hello.c 运行指令 mpirun-np 3./hello 运行结果 (3 个节点 ) hello parallel world! hello parallel world! hello parallel world! 实验 3: 通过 MPI_Comm_rank() MPI_Comm_size() 获取进程标志 和机器名 实验目的 熟悉 MPI 以下的 3 个函数 (1) 获得当前进程标识函数 :int MPI_Comm_rank ( MPI_Comm comm, int *rank ) (2) 获取通信域包含的进程总数函数 :int MPI_Comm_size(MPI_Comm comm, int *size) (3) 获得本进程的机器名函数 :int MPI_Get_processor_name( char *name,int *resultlen) 并行源代码 /* 文件名 :who.c*/ #include "mpi.h" #include <stdio.h> int main(int argc,char **argv) int myid, numprocs; int namelen; char processor_name[mpi_max_processor_name]; MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD,&myid);// 获得本进程 ID MPI_Comm_size(MPI_COMM_WORLD,&numprocs);// 获得总的进程数目 MPI_Get_processor_name(processor_name,&namelen);// 获得本进程的机器名 printf("hello World! Process %d of %d on %s\n",myid, numprocs, processor_name); MPI_Finalize(); 运行结果 (3 个节点 ) Hello World! Process 0 of 3 on wang1 Hello World! Process 1 of 3 on wang2 Hello World! Process 2 of 3 on wang3
程序说明 : 本实例程序启动后会在各个节点同时执行, 各节点通过 MPI_Comm_rank() 函数取得自己的进程标识 myid, 不同的进程执行 MPI_Comm_rank() 函数后返回的值不同, 如节点 0 返回的 myid 值为 0; 通过 MPI_Comm_size() 函数获得 MPI_COMM_WORLD 通信域中的进程总数 numprocs, 通过 MPI_Get_processor_name() 函数获得本进程所在的机器名 各进程调用自己的打印语句将结果打印出来, 一般 MPI 中对进程的标识是从 0 开始的 在本例中机器名分别为 wang1 wang2 wang3 共 3 个节点 这里需要再次强调的是,MPI 并行程序中的变量是分布存储的, 每个节点都有自己独立的存储地址空间, 如 myid numprocs namelen 等变量在各个节点是独立的, 相同的变量名它们的值是可以不同的 大家在读程序时心中一定要有变量分布存储的概念, 否则将无法正确分析程序 下图解释了本实例运行时的情况 : 每个节点都有独立的变量存储空间, 程序的副本存在于所有节点并分别得到执行, 各个节点计算时的地位是平行的 变量的分布存储 节点 1 机器名 :wang1 进程标识 :0 who.c 程序副本变量值 : myid=0,numprocs=3 节点 2 机器名 :wang2 进程标识 :1 who.c 程序副本变量值 : myid=1,numprocs=3 节点 3 机器名 :wang3 进程标识 :2 who.c 程序副本变量值 : myid=2,numprocs=3 图 MPI 中变量的分布式存储方式 在 3 个节点的情况下如果我们的运行命令改为 : mpirun np 6./who 这时会在每个节点上启动两个进程, 因此程序的输出会变为 : Hello World! Process 0 of 6 on wang1 Hello World! Process 1 of 6 on wang2 Hello World! Process 2 of 6 on wang3 Hello World! Process 3 of 6 on wang1 Hello World! Process 4 of 6 on wang2 Hello World! Process 5 of 6 on wang3 这一运行结果表明程序在 3 个节点上启动了 6 个进程, 每个节点都启动了两个进程 实验 4: 有消息传递功能的并行程序 实验目的 : 熟悉 MPI 的以下函数 (1) 消息发送函数 int MPI_Send(void* buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm) (2) 消息接收函数 :int MPI_Recv(void* buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status) 并行源代码 /* 文件名 :message.c*/ #include <stdio.h> #include "mpi.h"
int main(int argc, char** argv) int myid, numprocs, source; MPI_Status status; char message[100]; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myid); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); if (myid!= 0) else strcpy(message, "Hello World!");// 为发送字符串赋值 // 发送字符串时长度要加 1, 从而包括串结束标志 MPI_Send(message,strlen(message)+1, MPI_CHAR, 0,99,MPI_COMM_WORLD); // 除 0 进程的其他进程接收来自于 0 进程的字符串数据 for (source = 1; source < numprocs; source++) MPI_Recv(message, 100, MPI_CHAR, source, 99,MPI_COMM_WORLD, &status); printf("i am process %d. I recv string '%s' from process %d.\n", myid, message,source); MPI_Finalize(); 运行结果 (3 个节点 ) I am process 0. I recv string 'Hello World!' from process 1. I am process 0. I recv string 'Hello World!' from process 2. I am process 0. I recv string 'Hello World!' from process 3. 程序说明 本实例由其他进程通过 MPI 消息传递机制向 0 进程发送 Hello World 字符串数据, 非 0 进程采用 MPI_Send() 函数发送数据,0 进程通过循环语句分别通过 MPI_Recv() 函数接 收来自其他进程的字符串数据 接收缓冲区和发送缓冲区均采用同名变量 message, 由于地 址空间是独立的, 不同进程中的 message 变量虽然名字相同但却是完全不相关的变量 程序 在进行字符串的信息传递时发送长度要加 1 以包含串结束的标志 运行结果中的 3 条打印结 果都是由进程 0 打印的 实验 5: 用蒙特卡洛方法求 π 实验任务 : 基于蒙特卡罗思想用 MPI 程序实现对 π 值的并行求解实验目的 : 掌握蒙特卡洛算法并行化的实现方法实现方法 : 根据蒙特卡罗方法的思想, 我们以坐标原点为圆心作一个直径为 1 的单位圆, 再作一个
正方形与此圆相切 在这个正方形内随机产生 count 个点, 判断是否落在圆内, 将落在圆内 的点数目计作 m, 根据概率理论,m 与 count 的比值就近似可以看成圆和正方形的面积之比, m π 0.5 由于圆的半径为 0.5, 正方形的边长为 1, 我们有 =, 则 π 值可以用以下公式计算 : count 1 4m π = 本实验就采用这一方法来计算 π 的近似值 count 并行源代码 2 据 /* 文件名 :mtpi.c*/ #include"mpi.h" #include <stdio.h> #include <stdlib.h> int main(int argc,char **argv) int myid, numprocs; int namelen,source; long count=1000000; char processor_name[mpi_max_processor_name]; MPI_Status status; MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD,&myid);// 得到当前进程的进程号 MPI_Comm_size(MPI_COMM_WORLD,&numprocs);// 得到通信域中的总进程数 MPI_Get_processor_name(processor_name,&namelen);// 得到节点主机名称 srand((int)time(0));// 设置随机种子 double y; double x; long m=0,m1=0,i=0,p=0; double pi=0.0,n=0.0; for(i=0;i<count;i++) x=(double)rand()/(double)rand_max;// 得到 0~1 之间的随机数,x 坐标 y=(double)rand()/(double)rand_max;// 得到 0~1 之间的随机数,y 坐标 if((x-0.5)*(x-0.5)+(y-0.5)*(y-0.5)<0.25)// 判断产生的随机点坐标是否在圆内 m++; n=4.0*m/1000000; printf("process %d of %d on %s pi= %f\n",myid,numprocs,processor_name,n); if(myid!=0) // 判断是否是主节点 MPI_Send(&m,1,MPI_DOUBLE,0,1,MPI_COMM_WORLD);// 子节点向主节点传送结果 else p=m; /* 分别接收来自于不同子节点的数据 */ for(source=1;source<numprocs;source++) MPI_Recv(&m1,1,MPI_DOUBLE,source,1,MPI_COMM_WORLD,&status);// 主节点接收数 p+=m1; printf("pi= %f\n",4.0*p/(count* numprocs));// 汇总计算 pi 值 MPI_Finalize();
运行结果 (3 个节点 ) Process 1 of 3 on wang2 pi= 3.141172 Process 0 of 3 on wang1 pi= 3.143284 Process 2 of 3 on wang1 pi= 3.143284 pi= 3.142580 程序说明 本例在设计时引入 numprocs 参数, 即总的节点数, 通过对该参数的使用可以实现在机群节 点个数发生变化时不用对程序作任何修改, 我们通常在编写并行程序时都要求能对节点的数 目进行动态适应, 也就是节点可扩展 在示例中各节点对落入圆内的随机点进行计数, 并将 计算结果发送到主节点, 由主节点对所有数据汇总, 并计算 π 值 系统打印出各节点计算的 π 值和汇总后的 π 值 这种 π 值计算方法收敛速度较慢, 但是非常优美, 随机数的威力是很强 大的 这类算法具有很好的并行化能力, 各节点几乎不需要作信息交换, 独立完成自己的计 算工作 实验 6: 用蒙特卡罗方法求积分 实验任务 : 采用 Monte-Carlo 法计算函数 y=x 2 在 0~10 之间的积分值 实验目的 : 熟悉 MPI_Reduce() 函数的用法 实现方法 : 该算法的思想是通过随机数把函数划分成小的矩形块, 通过求矩形块的面积和来求积分 值, 我们生成 n 个 0~10 之间的随机数, 求出该随机数所对应的函数值作为矩形的高, 由于 随机数在 n 很大时会近似平均分布在 0~10 区间, 所以矩形的宽取相同的值为 10 n, 对所有 的矩形块求和即可得函数的积分值 并行源代码 /* 文件名 inte.c*/ #define N 100000000 #include <stdio.h> #include <stdlib.h> #include <time.h> #include "mpi.h" int main(int argc, char** argv) int myid,numprocs; int i; double local=0.0; double inte,tmp=0.0,x; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myid); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); srand((int)time(0));// 设置随机数种子 /* 各节点分别计算一部分积分值 */ /* 以下代码在不同节点运行的结果不同 */
for(i=myid;i<n;i=i+numprocs) x=10.0*rand()/(rand_max+1.0);// 求函数值 tmp=10*x*x/n; local=tmp+local;// 各节点计算面积和 // 计算总的面积和, 得到积分值 MPI_Reduce(&local,&inte,1,MPI_DOUBLE,MPI_SUM,0,MPI_COMM_WORLD); if(myid==0) printf("the integal of x*x=%16.15f\n",inte); MPI_Finalize(); 运行结果 The integal of x*x=333.333451312647291 程序说明 以上程序通过随机数将积分区域划分为 100000000 个小的区域, 各节点计算一部分小矩形的 面积, 最后通过 MPI_Reduce() 函数对所有节点的计算结果进行归约求和得到最后的积分值, 归约的过程就是各节点向主节点发送数据, 由主节点接收数据并完成指定的计算操作, 这一 思想与云计算中的 Map/Reduce 思想类似, 都是将任务分配到各节点计算最后由主节点汇总 结果 程序通过 myid 和 numpros 参数的配合使同一段程序在不同的节点运行时完成不同部 分的积分工作, 这利用了 MPI 并行编程中变量分布式存储的原理, 不同的节点其 myid 值是 不同的 可见在 MPI 中会出现相同的代码在不同的节点执行时结果不一样的情况, 这在串 行程序中是不会出现的, 大家要注意理解 实验 7: 用 MPI 的 6 个基本函数实现 Reduce 函数功能 ( 选做 ) 实验任务 : 采用 Monte-Carlo 法计算函数 y=x 2 在 0~10 之间的积分值实验目的 : 用 MPI 的基本函数实现 MPI 中的 MPI_Reduce() 函数的部分功能并行源代码 /* 文件名 myreduce.c*/ #define N 100000000 #include <stdio.h> #include <stdlib.h> #include <time.h> #include "mpi.h" void Myreduce(double *sendbuf, double *recvbuf,int count,int root);// 定义自己的 reduce 函数 int main(int argc, char** argv) int myid,numprocs; int i; double local=0.0;
double inte,tmp=0.0,x; MPI_Init(&argc, &argv); MPI_Comm_rank(MPI_COMM_WORLD, &myid); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); /* 采用归约对 y=x*x 在 [1,10] 区间求积分 */ srand((int)time(0)); for(i=myid;i<n;i=i+numprocs) x=10.0*rand()/(rand_max+1.0); tmp=x*x/n; local=tmp+local; Myreduce(&local,&inte,1,0);// 调用自定义的规约函数 if(myid==0) printf("the integal of x*x=%16.15f\n",inte); MPI_Finalize(); /* 自定义的归约函数,sendbuf 为发送缓冲区,recvbuf 为接收缓冲区,count 为数据个数,root 为 指定根节点 */ /* 该函数实现归约求和的功能 */ void Myreduce(double *sendbuf,double *recvbuf,int count,int root) MPI_Status status; int i; int myid,numprocs; *recvbuf=0.0; MPI_Comm_rank(MPI_COMM_WORLD, &myid); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); double *tmp; // 非 root 节点向 root 节点发送数据 if(myid!=root) MPI_Send(sendbuf,count,MPI_DOUBLE,root,99,MPI_COMM_WORLD); //root 节点接收数据并对数据求和, 完成规约操作 if(myid==root) *recvbuf=*sendbuf; for(i=0;i<numprocs;i++) if(i!=root) MPI_Recv(tmp,count,MPI_DOUBLE,i,99,MPI_COMM_WORLD,&status); *recvbuf=*recvbuf+*tmp; 运行结果 The integal of x*x=33.332395313332192 程序说明 : 本示例程序中我们自定义了一个归约函数 Myreduce(), 所用到的 MPI 函数没有超过 6 个基本函数, 该函数包括 4 个参数, 第一个参数 sendbuf 为各节点的发送缓冲区, 第二个参 数 recvbuf 为根节点的接收缓冲区, 第三个参数 count 为每个节点发送的数据个数, 第四个
参数 root 为需要指定的接收数据并归约数据的根节点 该函数没有给出具体的归约操作, 默认为对所有数据做求和的归约操作 程序执行到这一函数后将各自节点的指定数据向根节点发送, 并由根节点完成求和操作, 其基本的信息传递工作仍由 MPI_Send() 和 MPI_Revc() 函数来完成 本例我们以对函数 y = x 2 在 [0,10] 区间求积分为例, 应用自定义的 Myreduce() 函数实现所有节点数据的求和, 计算结果与调用 MPI_Reduce() 函数的结果一样, 这说明采用 6 个基本函数是可以实现大部分 MPI 功能的 我们也可以采用 6 个基本函数编写实现其他复杂的 MPI 函数 实验 8: 数据分发实验 实验目的 : 熟悉数据的分布式存储方法, 加深对 MPI_Reduce 及消息传递的理解 并行源代码 #include "mpi.h" #include <stdio.h> int main(int argc,char **argv) int myid,num,i,temp,max; int a[5][5] = 2,99,4,35,12, 96,23,77,88,55, 56,666,78,21,11, 19,28,36,75,81, 91,100,92,6,56; int recvbuff[5]; MPI_Status status; MPI_Init(&argc,&argv); MPI_Comm_rank(MPI_COMM_WORLD,&myid); MPI_Comm_size(MPI_COMM_WORLD,&num); if(num==5) MPI_Scatter(a,5,MPI_INT,recvbuff,5,MPI_INT,0,MPI_COMM_WORLD); temp=recvbuff[0]; for(i=1;i<5;i++) if(temp<recvbuff[i]) temp=recvbuff[i]; MPI_Reduce(&temp,&max,1,MPI_INT,MPI_MAX,0,MPI_COMM_WORLD); if(myid==0) printf("the max is %d\n",max); else printf("the number must 5!\n"); MPI_Finalize(); return 0; 实验 9: 计算与通信并行实验
实验目的 : 熟悉 MPI 的以下函数 (1) 非阻塞数据发送函数 :int MPI_Isend(void *buf,int count, MPI_Datatype datatype,int dest, int tag, MPI_Comm comm, MPI_Request *request) (2) 非阻塞数据接收函数 :int MPI_Irecv(void *buf,int count,mpi_datatype datatype,int source, int tag, MPI_Comm comm, MPI_Request *request) (3) 等待非阻塞数据传输完成 :int MPI_Wait(MPI_Request *request,mpi_status *status) (4) 测试非阻塞数据传输对象 int MPI_Test(MPI_Request *request, int *flag, MPI_Status *status) 并行源代码 /* 文件名 :isend.c*/ #include <stdio.h> #include <stdlib.h> #include "mpi.h" int main(int argc,char **argv) int i, numprocs,namelen,myid; char processor_name[mpi_max_processor_name]; int buf[5];// 数据缓冲区 int flag=0;// 定义数据传输成功标志 MPI_Status status; MPI_Request r;// 定义阻塞通信对象 MPI_Init( &argc, &argv ); MPI_Comm_rank( MPI_COMM_WORLD, &myid ); MPI_Comm_size(MPI_COMM_WORLD,&numprocs); MPI_Get_processor_name(processor_name,&namelen); if (myid == 0) for(i=0;i<5;i++) buf[i]=i; /* 节点 0 调用非阻塞数据发送函数, 传送一个数组, 并立即返回 */ MPI_Isend(&buf, 5, MPI_INT, 1, 99, MPI_COMM_WORLD, &r); else /* 节点 1 调用非阻塞数据接收函数, 接收数组, 并立即返回 */ MPI_Irecv(&buf, 5, MPI_INT, 0, 99, MPI_COMM_WORLD, &r); /* 检测数据是否传输成功 */ MPI_Test(&r,&flag,&status); printf("before MPI_Wait flag=%d. The buf is can't be used!\n",flag); /* 打印数据未传输成功时的接收缓冲区数据内容 */ for(i=0;i<5;i++) printf("buf[%d]=\t%d \n",i,buf[i]); /* 此处可进行其他的计算工作, 但不能调用接收缓冲区数据 */ /* 调用 MPI_Wait 等待数据传输成功, 在调用接收缓冲区数据前一般应调用该函数 */ MPI_Wait(&r,&status); /* 测试此时数据是否传输成功, 此时一定是成功的 */ MPI_Test(&r,&flag,&status); printf("after MPI_Wait flag=%d. The buf is can be used!\n",flag);
/* 打印数据传输成功后的接收缓冲区的内容 */ for(i=0;i<5;i++) printf("buf[%d]=\t%d \n",i,buf[i]); MPI_Finalize( ); return 0; 运行结果 ( 两个节点 ) Before MPI_Wait flag=0. The buf is can't be used! buf[0]= 1073790467 buf[1]= 1073833772 buf[2]= 0 buf[3]= 24641422 buf[4]= -1073747824 After MPI_Wait flag=1. The buf is can be used! buf[0]= 0 buf[1]= 1 buf[2]= 2 buf[3]= 3 buf[4]= 4 程序说明 该程序从 0 号节点向 1 号节点传送一组整型数据, 由于我们采用了非阻塞通信方式, 因 此发送函数和接收函数在调用后就会立即返回, 而不会等待数据传输完成, 这意味着发送和 接收数据的缓冲区会在调用非阻塞消息发送和接收函数后有一段时间处于不可用的状态, 但 这段时间我们可以让系统做其他的计算工作, 但要注意的是这些计算工作不能调用发送和接 收数据缓冲区内的数据, 否则会出现不可预测的结果 我们从程序的运行结果来看当节点 1 调用非阻塞接收函数后, 通过 MPI_Test() 函数检测返回 flag = 0, 数据传输还未完成, 此时 打印接收缓冲区的数据也表明数据确实还未接收成功, 这说明 MPI_Irecv() 函数调用后不会 等待数据传输成功而立即返回, 在数据传输期间系统可以从事其他的计算工作, 但这些计算 工作不能涉及接收缓冲区, 因为接收缓冲区还处于不确定状态 为了保证结果的确定性, 我 们在调用非阻塞接收函数后, 要使用接收缓冲区中的数据前, 需要调用 MPI_Test() 函数判断 数据传输是否成功, 或调用 MPI_Wait() 函数等待数据传输成功 从程序运行结果来看我们 调用 MPI_Wait() 函数后 flag =1, 数据接收缓冲区中的数据已正常存储可以调用其中的数据 同样, 我们在调用非阻塞发送函数后, 在数据未成功传输前, 也不能马上访问并修改发送 缓冲区, 否则发送的数据会发生改变 根据我们的分析, 一个计算与通信重叠的 MPI 并行 程序流程下图所示
图 计算与通信重叠的 MPI 并行程序流程 注意事项和常见问题 注意事项 (1) 部分服务配置文件需要 root 用户权限, 如 NFS 服务器的配置, 在安装时如出现问题请 首先检查当前用户是否具备相应的权限 (2)/etc/hosts 文件需要在所有节点上修改 (3) 各节点的 MPICH 和用户程序要在相同的目录下, 所有节点都必须有 MPICH 和用户程 序的副本 (4) 如出现 mpd 无法启动请检查所有节点的 NFS 文件共享是否正常启动 (5) 启动 mpd 前要保证所有节点都正确安装了 MPICH (6)NFS 配置及取消密码配置的顺序可以交换, 但 MPICH 必需最后安装, 否则 MPI 不能 正常运行 (7) 运行 MPI 程序时出现故障请检查以下内容 : 网络是否正常, 各节点是否能无密码相互 登录,mpd 是否在所有节点都已启动,NFS 服务及共享在各节点是否已完成, 各节点.mpd.conf 文件中密码是否相同, 可执行文件是否在各节点的相同路径有副本,MPICH 的安装文件是 否在各节点的相同路径有副本 (8) 安装时无法采用 ssh 登录系统时请检查系统的防火墙设置 (9) 系统必须已安装 GCC 才能进行并行环境的配置 (10) 当 ssh 无密码访问配置出现故障时, 可将所有节点的.ssh 文件夹全部清除, 重新配置 ssh 无密码访问 常见问题 (1) 配置无密码访问时出现 :Agent admitted failure to sign using the key 解决方法 : 每个节点输入 ssh-add (2) 用虚拟机安装了一个 Centos 系统, 后来想用安装的 vmdk 文件重新克隆一个虚拟 系统 clone 成功后发现网卡无法启动成功, 报错信息如下 : Bringing up interface eth0: Device eth0 does not seem to be present, delaying initialization. [FAILED] 这是因为虚拟机分配给操作系统的虚拟网卡地址是不一样的 第一个系统的网卡地址记
录在了 /etc/udev/rules.d/70-persistent-net.rules, 命名为 eth0 新分配的系统的网卡地址也记录 在了该文件当中, 因此有了冲突 解决方法 :(root 权限 ) 第 1 步 : 修改 /etc/udev/rules.d/70-persistent-net.rules 文件, 删除第一个网卡记录, 并将 第二个的 NAME="eth1" 改为 NAME="eth0" 第 2 步 : 如果在 /etc/sysconfig/network-scripts/ifcfg-eth0 中有配置网卡信息的话, 如 :#H WADDR="00:0C:29:C8:1A:92", 将其注释 第 3 步 : 重启系统 (3) 运行 service portmap start, 显示 portmap: unrecognized service, 需要安装 rpcbind-0.2.0-8.el6.i686.rpm libtirpc-0.2.1-1.el6.i686.rpm libgssglue-0.1-8.1.el6.i686.rpm (4) 安装 MPICH2 缺少 fortran 库 解决方法 yum install compat-gcc-34-g77 compat-libf2c-34 (5) 问题 :configure: error: c++ compiler cannot create executables 解决方法 : yum install gcc gcc-c++ gcc-g77 (6) 配好的并行环境在重启之后不能运行, 提示 no mpd is running on this host, 这 时重新启动 mpd 程序即可 : mpd&