간단한 hello world 예시를 통해 MPI에서 꼭 포함되어야 하는 것들을 살표본다.
#include <iostream>
#include <mpi.h>
using std::cout;
int main(int argc, char **argv){
int rank, size;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &size);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
if (rank == 0){
cout << "Hello, world! : from " << size << " processes\n";
}
else cout << "I am process " << rank << '\n';
MPI_Finalize();
return 0;
}
rank는 프로세스의 번호라고 생각하면 되고, size는 프로세스의 개수이다. MPI_Init은 이제부터 MPI를 사용한 병렬 프로그램을 시작하겠다고 MPI에 알리는 것이다. 그리고 MPI에서는 결과를 저장하거나 입력값을 수정해야 하는 경우가 많아서 보통 변수를 포인터로 많이 쓴다. MPI_Comm_size(MPI_COMM_WORLD, &size)는 프로그램을 실행할 때 사용자가 입력한 전체 프로세스의 개수를 size로 사용하겠다는 것이다. MPI_COMM_WORLD는 커뮤니케이터인데, 이 커뮤니케이터를 쓰면 전체 프로세스 그룹을 사용하겠다는 것이다. MPI_Comm_rank도 비슷한 기능을 한다. 그리고 if 문에 의해서 master 프로세스인 0번 프로세스만 size를 출력하고, 나머지는 자기의 번호를 출력한다.
MPI에는 Broadcast라는 것이 있다. 이건 간단하게 한 프로세스가 가진 데이터를 모든 프로세스에게 뿌리는 것이다. 인자는 다음과 같다.
MPI_Bcast(buffer, count, datatype, root, communicator);
buffer는 보내거나 받을 데이터가 할당될 메모리의 주소이고, count는 데이터의 길이이다. datatype은 MPI 전용 데이터타입이고, root는 데이터를 처음 갖는 프로세스 번호이다. communicator는 말 그대로 커뮤니케이터이다. MPI의 데이터타입은 그냥 C와 비슷하다. 다음과 같은 것들이 있다.
MPI_INT, MPI_LONG, MPI_FLOAT, MPI_DOUBLE
그리고 브로드캐스트는 한 데이터를 모든 프로세스에 뿌리는 것이었는데, 모든 프로세스의 값을 하나로 모은 뒤 어떤 연산을 수행해서 그 결과를 한 프로세스에 저장하는 것은 MPI_Reduce이다. OpenMP에서의 reduce와 동일한 기능을 한다. 인자는 다음과 같다.
MPI_Reduce(sendbuf, recvbuf, count, datatype, op, root, communicator);
sendbuf와 각 프로세스가 보내는 데이터의 주소이고, recvbuf는 최종 결과를 저장할 주소이다. op 자리에 어떤 operation을 할 건지가 들어가는데, MPI_SUM, MPI_PROD, MPI_MAX, MPI_MIN 같은 연산들을 할 수 있다. 그런데 이런 간단한 MPI에서 제공하는 연산 말고 내가 직접 연산을 정의하고 싶으면 MPI_Op_create로 가능하다. 예를 들어서 다음과 같은 구조체가 있다고 하자.
struct Particle{
double mass;
double momentum;
};
이 구조체들에 대해서 연산을 할 건데, 예를 들어서 mass끼리는 합치고 momentum끼리는 최대값을 취하고 싶을 때는 새롭게 operation을 정의해야 한다. 다음과 같이 사용할 수 있다.
MPI_Op_create(function, commute, &op);
function은 사용자가 정의한 reduction 함수이고, commute는 이 연산은 commutative한지 아닌지를 TRUE (1) 또는 FALSE (0)으로 입력하는 인자이다. op는 생성된 MPI 연산 객체이고, 이후에 MPI_Reduce에서 이걸 사용한다. 이 연산의 사용이 끝나면 MPI_Op_free(&op)를 통해서 MPI 내부 자원을 정리해줘야 한다. Bcast와 Reduce를 사용한 간단한 수치 적분 예시는 다음과 같다.
#include <iostream>
#include <mpi.h>
using namespace std;
int main(int argc, char **argv){
int numprocs, myid, n, i;
double dx, x, s, total, answer;
MPI_Init(&argc, &argv);
MPI_Comm_size(MPI_COMM_WORLD, &numprocs);
MPI_Comm_rank(MPI_COMM_WORLD, &myid);
if (myid == 0){
cout << "Enter the number of intervals : ";
cin >> n;
}
MPI_Bcast(&n, 1, MPI_INT, 0, MPI_COMM_WORLD);
dx = 1.0/(double)n;
s = 0.0;
for (i=myid+1; i<n; i+= numprocs){
x = (double)i * dx;
s += 4.0 / (1.0 + x * x);
}
MPI_Reduce(&s, &total, 1, MPI_DOUBLE, MPI_SUM, 0, MPI_COMM_WORLD);
if (myid == 0){
total += (4.0 + 2.0) / 2.0;
answer = total * dx;
cout << "Answer is " << answer <<'\n';
}
MPI_Finalize();
return 0;
}
0번 프로세스에만 입력된 구간의 개수 n을 MPI_Bcast로 모든 프로세스에 나눠주고, s=0.0을 통해 모든 프로세스에 독립적으로 생성된 s를 for문에서 계산한다. 여기서 i+=numprocs라는 것은, 예를 들어서 프로세스가 4개이면 0번 프로세스는 1, 5, 9, ... 에 해당하는 격자점을 계산하고 1번 프로세스는 2, 6, 10, ... 에 해당하는 격자점을 계산해서 각 프로세스가 다른 점에 대한 계산을 할 수 있게 해주는 것이다. 그리고 각 프로세스에서 계산된 s를 MPI_reduce를 통해 마스터 프로세스의 total이라는 변수에 저장해서 마지막에 dx를 곱해줘서 계산을 마무리하는 것이다.