零、前言
近期由于一些原因接触到了并行计算,对于这个陌生的领域我最先接触到的是MPI框架。MPI(Message Passing Interface),可以理解为是一种独立于语言的信息传递标准。目前它有两种具体的实现OpenMPI和MPICH,也就是说如果我们要使用MPI标准进行并行计算,就需要安装OpenMPI或MPICH库。本文以MPICH为例,在ubantu中安装MPI的环境,并对vscode进行配置。
一、MPI安装
1.1 前置准备
安装 mpich 之前需要安装好相应的编译器,可以通过查看是否安装了
$ gcc --version
$ fortran --version
$ gfortran --version
如果没有安装则使用 sudo apt-get install gcc
安装即可(gcc替换成你没有的编译器)。
1.2 下载MPI
可以去这里下载一个MPI的程序包,选择你要下载的版本即可,其中Platform要选择MPICH的。
下好了之后将其放到你要安装的目录下,强烈建议在home目录下建一个新的文件夹比如mpi来放置
1.3 安装
将下载的安装包进行解压,可以在窗口中选中右键解压,也可以 cd
到目录下用 tar xzf +文件名
来解压。
然后配置安装路径,cd到解压的文件夹,我的解压文件夹名称为mpich-3.4.3
,所以我先cd mpich-3.4.3
,然后输入./configure -prefix=/home/[username]/mpi
,其中 -prefix=
后写的是你的mpich的安装路径。
然后进行编译 make
,这一步很久(我大概用了十几分钟),需要耐心等待。
然后进行安装 make install
。
安装完成后添加环境变量,先输入sudo gedit ~/.bashrc
,然后会要求你输入密码,输入后会弹出一个文本框,输入下列路径:
export MPIPATH=/home/fang/mpi
export MPIPATHBIN=$MPIPATH/bin
export MPIPATHINCLUDE=$MPIPATH/include
export MPIPATHLIB=$MPIPATH/lib
export MPIPATHSHARE=$MPIPATH/share
export PATH=$PATH:$MPIPATHBIN:$MPIPATHINCLUDE:$MPIPATHLIB:$MPIPATHSHARE
注意: 第一行的 MPIPATH
需要写你安装的MPI的那个文件夹,其他不用改动
然后在终端中输入 source .bashrc
激活环境变量
1.4 测试
首先输入 which mpicc
可以查看你的mpich的安装路径。
然后打开终端cd进入你所下载的压缩包的解压文件夹,该路径下有个 example
文件夹,里面是mpich官方的示例代码,终端中输入:
mpirun -np 10 ./examples/cpi
mpiexec -n 4 ./examples/cpi
就完成了!
二、运行MPICH
3.1 命令行大法
如果用C++编写则用第一条,如果用C编写则用第二条,其中xxx是你要编译的文件名,yyy是你编译完成后生成的exe文件的文件名
mpic++ xxx.cpp -o yyy
mpigcc xxx.c -o yyy
然后运行可执行文件,需要先cd到可执行文件的路径下,yyy
是你的可执行文件夹名字,千万不能漏掉 ./
, 前面的参数 4
表示分配4个进程并行运行
mpirun -np 4 ./yyy
3.2 vscode配置
使用code runner插件运行,进入插件设置页,然后点击 在settings.json中编辑
,自动进入settings.json
文件
文件结构如下所示:
可以复制我的配置:
{
"code-runner.runInTerminal": true,
"files.autoSave": "afterDelay",
"code-runner.executorMap": {
"javascript": "node",
"java": "cd $dir && javac $fileName && java $fileNameWithoutExt",
"c": "cd $dir && gcc $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
//"cpp": "cd $dir && g++ $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
"cpp" : "cd $dir && mpic++ $fileName -o /home/fang/code/papercode/exe/$fileNameWithoutExt &&cd $dir && mpirun -np 4 /home/fang/code/papercode/exe/$fileNameWithoutExt",
"objective-c": "cd $dir && gcc -framework Cocoa $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
"php": "php",
"python": "python -u",
"perl": "perl",
"perl6": "perl6",
"ruby": "ruby",
"go": "go run",
"lua": "lua",
"groovy": "groovy",
"powershell": "powershell -ExecutionPolicy ByPass -File",
"bat": "cmd /c",
"shellscript": "bash",
"fsharp": "fsi",
"csharp": "scriptcs",
"vbscript": "cscript //Nologo",
"typescript": "ts-node",
"coffeescript": "coffee",
"scala": "scala",
"swift": "swift",
"julia": "julia",
"crystal": "crystal",
"ocaml": "ocaml",
"r": "Rscript",
"applescript": "osascript",
"clojure": "lein exec",
"haxe": "haxe --cwd $dirWithoutTrailingSlash --run $fileNameWithoutExt",
"rust": "cd $dir && rustc $fileName && $dir$fileNameWithoutExt",
"racket": "racket",
"scheme": "csi -script",
"ahk": "autohotkey",
"autoit": "autoit3",
"dart": "dart",
"pascal": "cd $dir && fpc $fileName && $dir$fileNameWithoutExt",
"d": "cd $dir && dmd $fileName && $dir$fileNameWithoutExt",
"haskell": "runhaskell",
"nim": "nim compile --verbosity:0 --hints:off --run",
"lisp": "sbcl --script",
"kit": "kitc --run",
"v": "v run",
"sass": "sass --style expanded",
"scss": "scss --style expanded",
"less": "cd $dir && lessc $fileName $fileNameWithoutExt.css",
"FortranFreeForm": "cd $dir && gfortran $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
"fortran-modern": "cd $dir && gfortran $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
"fortran_fixed-form": "cd $dir && gfortran $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt",
"fortran": "cd $dir && gfortran $fileName -o $fileNameWithoutExt && $dir$fileNameWithoutExt"
},
"window.zoomLevel": 2,
"explorer.confirmDelete": false
}
其中的cpp
表示当运行的文件为c++时使用的命令,你也可以自己配置,可用的参数如下:
$workspaceRoot
$dir
$dirWithoutTrailingSlash
$fullFileName
$fileName
$fileNameWithoutExt
以我所配置的mpich的运行方式为例进行解释:
"cpp" : "cd $dir && mpic++ $fileName -o /home/fang/code/papercode/exe/$fileNameWithoutExt &&cd $dir && mpirun -np 4 /home/fang/code/papercode/exe/$fileNameWithoutExt"
- 首先 cd 到
$dir$
表示进入当前文件所在路径
&&
表示并的关系,即只有前面的命令运行成功后才运行后面的命令。
- 然后是
mpic++ $fileName -o /home/fang/code/papercode/exe/$fileNameWithoutExt
,这里的$fileName
是当前需要运行的代码文件名称,$fileNameWithoutExt
表示不带后缀的文件名称,这一行的命令意思是将文件使用MPICH进行编译,然后存放到/home/fang/code/papercode/exe/
路径下,编译出来的文件名为$fileNameWithoutExt
- 最后是
mpirun -np 4 /home/fang/code/papercode/exe/$fileNameWithoutExt
就是使用MPICH运行可执行文件,不在解释。
三、MPI编程框架
1.MPI_Init
任何MPI程序都应该首先调用该函数。 此函数不必深究,只需在MPI程序开始时调用即可(必须保证程序中第一个调用的MPI函数是这个函数)。
MPI_Init(&argc, &argv)
Fortran版本调用时不用加任何参数,而C和C++需要将main函数里的两个参数传进去,因此在写main函数的主程序时,应该加上这两个形参。
int main(int *argc,char* argv[])
{
MPI_Init(&argc,&argv);
}
2.MPI_Finalize
任何MPI程序结束时,都需要调用该函数。 该函数同第一个函数,都不必深究,只需要求格式去写即可。
MPI_Finalize()
3.MPI_COMM_RANK
int MPI_Comm_Rank(MPI_Comm comm, int *rank)
该函数是获得当前进程的进程标识,如进程0在执行该函数时,可以获得返回值0(即rank = 0)。可以看出该函数接口有两个参数,前者为进程所在的通信域,后者为返回的进程号。通信域可以理解为给进程分组,比如有0-5这六个进程。可以通过定义通信域,来将比如 [0,1,5] 这三个进程分为一组,这样就可以针对该组进行“组”操作,MPI_COMM_WORLD
是MPI已经预定义好的通信域,是一个包含所有进程的通信域,目前只需要用该通信域即可。
在调用该函数时,需要先定义一个整型变量如myid,不需要赋值。将该变量传入函数中,会将该进程号存入myid变量中并返回。
4.MPI_COMM_SIZE
该函数是获取该通信域内的总进程数,如果通信域为MP_COMM_WORLD
,即获取总进程数,使用方法和MPI_COMM_RANK
相近。
MPI_COMM_SIZE(comm, size)
int MPI_Comm_Size(MPI_Comm, int *size)
5.MPI_SEND
该函数为发送函数,用于进程间发送消息,如进程0计算得到的结果A,需要传给进程1,就需要调用该函数。
call MPI_SEND(buf, count, datatype, dest, tag, comm)
int MPI_Send(type* buf, int count, MPI_Datatype, int dest, int tag, MPI_Comm comm)
该函数参数过多,不过这些参数都很有必要存在。
这些参数均为传入的参数,其中buf
为你需要传递的数据的起始地址,比如你要传递一个数组A,长度是5,则buf
为数组A的首地址。count
即为长度,从首地址之后count
个变量。datatype
为变量类型,注意该位置的变量类型是MPI预定义的变量类型,比如需要传递的是C++的int型,则在此处需要传入的参数是MPI_INT,其余同理。dest
为接收的进程号,即被传递信息进程的进程号。tag
为信息标志,同为整型变量,发送和接收需要tag
一致,这将可以区分同一目的地的不同消息。比如进程0给进程1分别发送了数据A和数据B,tag
可分别定义成0和1,这样在进程1接收时同样设置tag0和1去接收,避免接收混乱。
6.MPI_RECV
该函数为MPI的接收函数,需要和MPI_SEND
成对出现。
call MPI_RECV(buf, count, datatype, source, tag, comm,status)
int MPI_Recv(type* buf, int count, MPI_Datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
参数和MPI_SEND
大体相同,不同的是source
这一参数,这一参数标明从哪个进程接收消息。最后多一个用于返回状态信息的参数status
。
在C和C++中,status
的变量类型为MPI_Status
,分别有三个域,可以通过status.MPI_SOURCE
,status.MPI_TAG
和status.MPI_ERROR
的方式调用这三个信息。这三个信息分别返回的值是所收到数据发送源的进程号,该消息的tag值和接收操作的错误代码。
SEND和RECV需要成对出现,若两进程需要相互发送消息时,对调用的顺序也有要求,不然可能会出现死锁或内存溢出等比较严重的问题。
7. MPI_Barrier
该函数为一个阻塞函数
MPI_Barrier(MPI_Comm comm);
填入的参数为通信域,当进程执行该函数并且属于该通信域时,则停止执行进入等待状态,当该通信域的所有进程都执行到该函数后才继续往下进行。
7. 例子
int main(int argc, char** argv) {
MPI_Init(NULL, NULL); // MPI启动 一直到Finalize之间的都会并行执行
int rank;
MPI_Comm_rank(MPI_COMM_WORLD, &rank); // 获取当前进程id
if (rank == 0) {
...... // 对0号进程进行操作
}else if(rank == 1) {
...... // 对1号进程进行操作
}
MPI_Barrier(MPI_COMM_WORLD); //等待函数,所有进程都调用这个函数后才继续往下运行
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
printf("rank%d\n", rank);
if(rank == 1){
......
}
MPI_Finalize();// MPI结束
return 0;
}
四、总结
这次配置MPICH的过程还是收获颇丰的,第一次领略到了用Linux安装环境的快捷与舒适,还了解vscode的很多配置原理(之前都是无脑配置的),最后还入门了一种全新的编程方式,并行计算。