병렬분산컴퓨팅 - MPI
병렬 분산 컴퓨팅 - MPI
원래는 분산 메모리 프로그래밍 관련 내용인데 이쪽에서 가장 유명한 라이브러리가 MPI라서 MPI 관련으로 포스팅을 하겠다.
1. 개요
이전에 포스팅 했던 OpenMP는 Shared memory programming 이었다.
하지만 이번에 포스팅할 내용은 Distributed memory programming 이다.
여기서 말하는 Distributed memory system이란 아래와 같다.
각 CPU에 Memory가 달린 상태에서 만약 어떤 CPU에서 다른 CPU에 달린 Memory로 엑세스하고 싶을 경우 Interconnect를 통해서 요청해서 가져와야하는 구조이다.
물론 위와 같은 구조에서 각 Node안에서도 Multi-thread를 이용해서 병렬화가 가능하다. 따라서 아래와 같은 구조로 구동되기도한다.
위와 같은 구조는 MPI와 OpenMP를 같이 쓰는 구조로, 여기에 GPU까지 포함되기도한다.
2. MPI 기본 문법
기본적으로 MPI는 API 스펙이다. 해당 스펙대로 구현한게 MPI 라이브러리이며,LAM/MPI나 OpenMPI등 여러 종류가 있다. 기본적으로 이기종 클러스터에 대해 지원하며, 2013년 부터는 GPU도 지원하기 시작했다.
1) Initialize
먼저 OpenMPI를 기준으로 설명하자면 사용하기 위해 아래의 코드로 시작해야한다.
1
2
3
4
5
6
#include<mpi.h>
int main(){
MPI_Init(&argc, &argv);
MPI_Finalize();
}
Init과 Fianlize 중앙의 영역에 MPI 코드를 써야한다.
2) Basic function
a. MPI_INIT
1
MPI_INIT(int *argc, char ***argv)
MPI 계산을 초기화한다.
b. MPI_FINALIZE
1
MPI_FINALIZE()
MPI 계산을 종료한다.
c. MPI_COMM_SIZE
1
MPI_COMM_SIZE(IN comm, OUT size)
- IN comm : 커뮤니케이터 핸들러이다. 그룹에 대한 context이며 모든 프로세스는 기본적으로 MPI_COMM_WORLD에 포함되어있다.
- OUT size : 그룹내 프로세서 개수를 반환한다.
d. MPI_COMM_RANK
1
MPI_COMM_RANK(IN comm, OUT rank)
- IN comm : 커뮤니케이터 핸들러이다. 그룹에 대한 context이며 모든 프로세스는 기본적으로 MPI_COMM_WORLD에 포함되어있다.
- OUT rank : 양의 정수의 유일한 값으로 프로세스의 ID 같은 것이다.
e. MPI_Send
다른 Node로 데이터를 보내는 Send 함수이다.
1
int MPI_Send(void *buf, int count, MPI_Datatype datatype, int dest, int tag, MPI_Comm comm);
- buf : 보낼 버퍼의 주소이다.
- count : 메세지 안에 아이템 개수이다.
- datatype : data의 타입이다.
- dest : 메세지를 받을 프로세스의 RANK값 즉, ID 값이다.
- tag : 메세지 종류이다. 사용자가 정의해서 사용할 수 있다.
- comm : 이전에 SIZE나 RANK에서도 봤듯이 통신에 참여하는 그룹 같은걸로 생각하면 편하다. 기본값은
MPI_COMM_WORLD이다.
f. MPI_Recv
다른 Node에서 데이터를 받는 Receive 함수이다.
1
int MPI_Recv(void *buf, int count, MPI_Datatype datatype, int source, int tag, MPI_Comm comm, MPI_Status *status)
- *buf : 메세지를 전달 받을 버퍼 주소이다.
- count : 버퍼 크기이다.
- datatype : 받을 데이터의 데이터 타입이다.
- source : 지정하면 해당 source에 해당하는 데이터만 받을 수 있다, 만약 제한을 두고 싶지 않다면
MPI_ANY_SOURCE로 지정하면 된다. - tag : 지정하면 해당 tag의 데이터만 받을 수 있다, 만약 제한을 두고 싶지 않다면
MPI_ANY_TAG로 지정하면 된다. - comm : 이전에 SIZE나 RANK에서도 봤듯이 통신에 참여하는 그룹 같은걸로 생각하면 편하다. 기본값은
MPI_COMM_WORLD이다. - *status : Send 함수와는 다르게 Recive는 해당 status 객체로 완료 여부 등을 판단할 수 있다. 세부 구조는 아래에 서술되어있다.
1
2
3
4
5
6
7
8
typedef struct ompi_status_public_t MPI_Status;
struct ompi_status_public_t {
int MPI_SOURCE;
int MPI_TAG;
int MPI_ERROR;
int _count;
int _cancelled;
}
- MPI_SOURCE : source에 해당 하는 프로세스의 rank값, 즉 ID 값이다.
- MPI_TAG : source의 tag 값이다.
- MPI_ERROR : 0이 아니라면 에러가 났음을 알리는 값이다.
- _count : 메세지의 크기이다. 위의 세 값들은 직접 엑세스가 가능하지만 이 값은
MPI_Get_Count()함수로 가져와야한다. - _cancelled : 함수가 통신 요청을 취소시 성공적으로 취소되었다면 0이 아닌 값으로 세팅된다. 이 값이 0아닌 값일시 위의 모든 값들을 유효하지 않다.
3. Hello word example
1
2
3
4
5
6
7
8
9
10
11
#include<mpi.h>
#include<stdio.h>
int main(int argc, char **argv) {
int rank, num, i;
MPI_Init (&argc, &argv);
MPI_Comm_rank(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &rum);
printf("Hello from process %i of %i /n",rank,num);
MPI_Finalize();
return 0;
}
위의 코드는 연결된 서버 그룹내에 전체에 프로세스 중에 몇번째 프로세스로 작업해서 반환하는지 출력하는 코드이다.
실행하려면 아래의 코드를 linux에서 입력한다.
1
mpicc hello.c -o hello
1
gcc hello.c -o hello -I /usr/include/mpi -1mpi
4. Hostfile
위 예시에서 실행하는 코드는 기본적으로 Master node가 제어하고, slave node로 작업을 뿌려서 처리하는 방식이다.
때문에 slave node와 ssh와 같은 방식으로 연결할 필요가 있는데, 이를 실행할때마다 입력하면 귀찮기 짝이 없다.
때문에 hostfile이라는 파일을 만들어 인자로 넘겨주면, 그 파일에 적힌 서버로 요청을 해준다.
hostfil은 아래와 같은 구조이다.
1
2
3
192.168.0.5
192.168.0.7
192.168.0.100
위와 같이 IP 형태일 수도 있고, 혹은 아래와 같이 DNS 형태일 수도 있다.
1
2
3
test1.example.com
test2.example.com
test3.example.com
뒤에 아무런 지시자도 안넣으면 ROUND ROBIN 방식으로 하나씩 프로세스를 할당하게 된다. 에를 들어 DNS Hostfile을 예시로 든다면 test1, test2, test3 가 순차적으로 프로세스를 하나씩 할당받게 되는 것이다. 그리고 이렇게 만들어진 hostfile mpirun을 통해서 아래와 같이 실행할 수 있다.
1
mpirun -np 8 -hostfile hosts ./hello
5. Slot Modifer
각 slave node들이 다수의 코어를 가지고 있는데, 프로세스를 하나씩만 할당하는건 비효율적일 수 있다.
때문에 MPI에서는 Slot modifer라는 걸 지원한다.
slot modifier는 간단하게 말해서 각 node에 몇 개씩 프로세스를 할당할 거냐는 일종의 지시자이다.
앞서 hostfile에서는 아무런 지시자도 없었는데, 이는 default로 할당하려는 프로세스를 1개만 쓰겠다는 뜻이다.
test1에 프로세스 2개, test2에 프로세스 3개, test3에 프로세스 4개를 할당하고 싶다면 아래와 같이 입력하면 된다.
1
2
3
test1.example.com slots=2
test2.example.com slots=3
test3.example.com slots=4
실행은 동일하게 실행하면 된다.
6. Program Structure
1) SPMD
한 개의 명령어를 다수의 데이터로 처리하는 프로그램 구조이다. 아래와 같이 정의할 코드를 짤 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int main(int argc, char **argv){
MPI_Init(&argc,&argv);
int rank, num, i;
MPI_Comm_rnak(MPI_COMM_WORLD, &rank);
MPI_Comm_size(MPI_COMM_WORLD, &num);
if(rank=0) {
char mess[] = "Hello world";
int len = strlen(mess)+1;
for(i=1;i<num;i++)
MPI_Send(mess,len,MPI_CHAR, i,MESSTAG,MPI_COMM_WORLD);
}
else{
char mess[MAXLEN];
MPI_Status status;
MPI_Recv (mess, MAXLEN, MPI_CHAR, 0 MESSTAG, MPI_COMM_WORLD, &status);
printf("%i received %s\n",rank,mess);
}
MPI_Finlize();
}
Master node를 하나 두고, 나머지로 하여금 메시지 수신자를 구동하게 하는 코드이다.
2) MPMD
구동하는 컴퓨터의 OS가 다를 수 있다. 이를 위해서 각기 다른 파일을 실행 시킬 수 있다.
이를 위해서 appfile을 만들어야한다. 아래는 appfile인 appconf의 예시이다.
1
2
-host 192.168.0.2 -np 8 myapp.solaris
-host 192.168.0.5 -np 2 myapp.linux
위의 host도 hostfile을 사용할 수 있다. 실행은 아래와 같다.
1
mpirun -app appconf
6. Point to point communication
앞서 설명했던 Send 함수와 Recv 함수로 어느정도 거리가 있는 두 Node 간에 통신이 있었다고 해보자.
그러면 아래와 같은 Data flow가 일어난다.
만약에 어떤 Node에서 다른 Node로 메세지를 보낸다고 할 때 어디까지 데이터가 전송되었을 때 전송되었다고 볼 수 있을까?
여러가지 정의가 있을 수 있겠지만 적어도 MPI에서는 빨간 색으로 표시된 send, receive, deliver는 아래의 정의에 따른다.
- send : user buffer에서 socket buffer로 데이터가 write 된 경우
- receive : 받는 Node에서 kernel의 socket buffer까지 데이터가 도착한 경우 receive
- deliver : user 영역의 buffer에 데이터가 도착한 경우 deliver
1) Block & Non-block
Send 함수와 Recv에 대해서 좀 더 자세히 알려면 Block과 Non-block에 대해서 알아야한다.
- blocking send는 메세지가
send가 되기전에 return이 되지 않는(locally blocking) 경우를 말한다. - blocking receive는 메세지가
receive나deliver되기 전에는 return 하지 않는 것을 말한다.
여기서 Non-blocking send/receive는 위에 설명한 것과 반대이다. 이게 무슨 소리냐 싶을 것이다.
아래의 예시를 보면 이해가 편하다. 아래의 예시를 보자.
2) 예시 : Process P, Process Q
Process P, Process Q가 있다. P는 sender고 Q는 receiver이다.
아래의 코드를 보자.
만약에 Blocking send와 Blocking receive를 사용 했을 경우 X 값은 11만 가능하다.
이는 Process P에서 buffer M 값을 Process Q로 보냈을 때, 해당 Node의 Socket buffer에 write 되고나서야 M=20; 코드가 실행되고 Process Q에서는 받은 값을 S에 넣어서 1을 더한 뒤 X에 넣는데, L1 위치의 코드도 S에 어떤값을 받지 않는다면 L2에 넘어가지 않기 때문에 11이 보장된다.
하지만 만약에 Non-Blocking send와 receive라면 어떨까? 이 경우 가능한 X의 값은 11, 21, -99이다. Blocking send/receive와 같이 구동되면 11이 반환 될 것이고, M=20으로 변경되는 시점에 Q로 전송된다면 21이 될것이며, receive 쪽에서 어떤 것도 받지 않았는데, 넘어가버리면 -99이다.
3) Communication Mode에 따른 함수의 종류
Communication Mode는 Send시 어떻게 보낼 것인가에 대한 프로토콜이라고 생각하면 된다.
총 4가지가 있으며 각 Mode에 대해 Block과 Non-block으로 나뉜다. 아래의 표를 보자.
| Communication Mode | Blocking Routines | Non-Blocking Routines |
| Standard Send | MPI_Send | MPI_Isend |
| Synchronous Send | MPI_Ssend | MPI_Issend |
| Ready Send | MPI_Rsend | MPI_Irsend |
| Buffered Send | MPI_Bsend | MPI_Ibsend |
| Receive | MPI_Recv | MPI_Irecv |
Basic function 파트에서 설명했던 Send와 Recv는 사실 Standard mode에서의 Send와 blocking Receive에서의 Receive 함수였으며 Receive의 경우 별도의 모드는 없고 Blocking인지 Non-blocking에 따라 차이만 있다.
a. Standard Send/Receive (Blocking 기준 설명)
여기서 설명하는 Standard Mode의 Send는 OpenMPI 기준이다. 다른 라이브러리의 경우 구현이 다를 수 있다.
OpenMPI에서 Standard 모드는 메세지의 길이에 따라 방식이 변한다. 모드 전환을 정하는 기준인 메시지 길이는 사용자가 정의하는 값이다.
ⓐ 메세지가 짧을 경우 (<Threshold)
Eager protocol이라고 부르며, Receive하는 쪽의 Buffer 사이즈는 고려하지 않고 그냥 보내는것이다.
별도의 동기화 과정을 거치지 않기 때문에 동기화 오버헤드가 없다.
하지만 확장성에 대해서는 떨어진다. 아무래도 Receiver의 buffer를 고려하지 않기 때문이다.
ⓑ 메세지가 길 경우 (>=Threshold)
보내기전에 Receiver한테 buffer가 충분한지 확인한다. handshaking 과정으로 확인하여 버퍼가 준비되면 전송하는 방식이다.
매우 안전하나 handshaking 과정에서 Overhead가 발생한다.
b. Synchronous Send/Receive (Blocking 기준 설명)
OpenMPI에서 Standard mode의 long과 동일한 방식인데, 좀 더 명시적으로 쓰고 싶을 때 쓰면 된다.
c. Ready Send/Receive (Blocking 기준 설명)
이미 Receiver가 initiated 되어있을 때 전송했을 때만 성공 처리되므로 Receiver가 준비되어있을때만 보내는 것과 비슷한 방식이다.
d. Buffered Send/Receive (Blocking 기준 설명)
다른 사용자 버퍼에 복사해두고 해당 버퍼에서 socket buffer로 보내서 전송시킨다음에 Sender의 프로세스는 계속 작업하는 방식이다.
동기화 오버헤드가 없으나 별도의 추가 복사가 있기 때문에 데이터가 너무 크다면 위험할 수 있다.
※ Non-blocking 설명이 없는 이유
위의 설명에서 blocking으로 기다리는 부분만 빼면 나머지는 각 모드에서 Non-blocking 방식과 동일하다.
※ Non-blocking 이 있는 이유
간단히 말해서 전송과 계산을 Overlap하여 전체적인 지연시간을 줄이기 위해서이다.
CUDA 스트림 Overlapping 이랑 비슷한 것이라고 생가갛면 편하다.
※ 상태 제어
Non-blocking 함수의 경우 도착이 보장 되지 않는데 뭐 어떻게 처리하냐는 건지 물어 볼 수 있다. 여기서 Non-block send 함수의 경우에는 끝에 MPI_Request라는 구조체로 req라는 이름의 포인터 변수를 갖고 있다. 이를 아래의 함수에 인자로 넣고 사용하면 된다.
MPI_Wait
해당 작업이 완료될까지 기다리는 함수이다. 아래와 같이 사용한다. (Blocking)
1
int MPI_Test(MPI_Request *req, MPI_Status *st)
Non-blocking send 함수에서 받은 req 핸들러를 인자로 넘기고 MPI_Status라는 구조체의 *st에 들어간 값을 읽는다.
MPI_Test
해당 작업이 완료되었는지 확인만하고 넘어간다. (Non-blocking)
1
int MPI_Test(MPI_Request *req, int *flag,MPI_Status *st)
Non-blocking send 함수에서 받은 req 핸들러를 인자로 넘기고 MPI_Status라는 구조체의 *st에 들어간 값을 읽는다. 완료시에 flag는 0이 아닌 값으로 반환받는다.
참고자료
- 서강대학교 박성용 교수님 강의자료 - 병렬 분산 컴퓨팅
원문 참고자료들
- Peter S. Pacheco, An Introduction to Parallel Programming, Elsevier Inc. (Morgan Kaufmann), 2011, ISBN 978-0-12-374260-5
- Gerassimos Barlas, Multicore and GPU Programming – An Integrated Approach, Elsevier Inc. (Morgan Kaufmann), 2015, ISBN 978-0-12-417137-4.
- G. Coulouria, J. Dollimore, T. Kindberg, and G. Blair, Distributed Systems: Concepts and Design, 5 th Edition, Pearson, 2012, ISBN 978-0-273-76059-7
- M. van Steen and A. S. Tanenbaum, Distributed Systems, 3 rd Edition, 2017
- Martin Kleppmann, Designing Data-Intensive Applications, 1 st Edition, O’Reilly Media, 2017, ISBN 978-1491903070 (또는 2nd Edition in February 2026)







