소프트웨어 시뮬레이션 마지막 수정: 2026-08-15 04:12:40

1. 개요[편집]

MPI
Message Passing Interface
종류표준 명세(API). 라이브러리가 아니다
제정MPI Forum · MPI-1 (1994)
주요 개정MPI-2 (1997, RMA·병렬 I/O) · MPI-3.0 (2012, 논블로킹 집합통신) · MPI-4.0 (2021)
메모리 모델분산 메모리 — 명시적 메시지 교환
기본 단위랭크(rank) · 커뮤니케이터(communicator) · 태그(tag)
주요 구현MPICH, Open MPI, MVAPICH2, Intel MPI, Cray MPICH
비용 모형$T(n)=\alpha+\beta n$ (지연 + 대역폭)

공유 메모리는 편하고, 분산 메모리는 확장된다. 그래서 슈퍼컴퓨터는 30년째 불편한 쪽을 쓴다.

MPI(Message Passing Interface)는 분산 메모리 병렬 프로그램에서 프로세스끼리 메시지를 주고받는 방식을 규정한 표준 명세다. 구현체가 아니라 명세라는 점이 핵심으로, MPICH든 Open MPI든 Cray의 벤더 구현이든 같은 소스를 다시 컴파일하면 돈다. 1994년 MPI-1이 나온 이래 30년 넘게 병렬 컴퓨팅의 사실상 표준 자리를 지키고 있고, OpenFOAM·LAMMPS·GROMACS·PETSc를 포함해 대규모 전산유체역학·유한요소법·분자동역학 코드는 거의 예외 없이 MPI 위에 서 있다.

MPI가 오래 버틴 이유는 우아해서가 아니라 아무것도 숨기지 않아서다. 어떤 데이터가 언제 어디로 가는지 프로그래머가 전부 손으로 쓴다. 고통스럽지만, 통신이 성능의 전부인 기계에서는 숨겨진 통신이 훨씬 더 고통스럽다.1

2. 랭크·커뮤니케이터·태그[편집]

MPI 프로그램은 시작 시 PP개의 프로세스로 뜨고, 각 프로세스는 랭크(rank) 0,,P10,\dots,P-1이라는 정수 하나로 자신을 식별한다. 전형적인 코드는 MPI_Comm_rank로 자기 번호를 받아 “내 번호에 해당하는 조각”만 계산한다 — SPMD(Single Program, Multiple Data) 모델이다.

커뮤니케이터(communicator)는 랭크들의 집합이자 통신이 벌어지는 무대다. 기본값 MPI_COMM_WORLD는 전체 프로세스를 담고, MPI_Comm_split으로 부분집합을 잘라내 하위 커뮤니케이터를 만든다. 커뮤니케이터가 다르면 메시지가 절대 섞이지 않기 때문에, 라이브러리는 자기만의 사설 커뮤니케이터를 복제해 쓴다(MPI_Comm_dup). 사용자 코드의 메시지가 라이브러리 내부 통신을 가로채는 사고를 원천 차단하는 장치이며, PETSc처럼 남의 코드에 얹히는 솔버가 반드시 하는 일이다.

태그(tag)는 같은 상대에게 여러 종류의 메시지를 보낼 때 구분용으로 붙이는 정수다. 실무에서는 태그를 “속도 halo는 10, 압력 halo는 11” 식으로 물리량마다 고정하는 것이 국룰이고, 태그를 재사용하다 논블로킹 통신끼리 짝이 뒤바뀌는 버그는 재현이 안 돼서 악명이 높다.

3. 점대점 통신[편집]

두 랭크가 직접 주고받는 것이 점대점(point-to-point) 통신이다.

  • 블로킹: MPI_Send / MPI_Recv. MPI_Recv는 데이터가 도착해야 반환한다. 문제는 MPI_Send인데, 표준 모드의 MPI_Send버퍼링을 해도 되고 안 해도 된다. 구현체 대부분은 작은 메시지를 즉시 복사해 반환하고(eager 프로토콜), 임계 크기를 넘으면 수신자가 받을 준비를 할 때까지 기다린다(rendezvous). 그래서 두 랭크가 서로에게 MPI_Send부터 하는 코드는 작은 격자에서는 잘 돌다가 격자를 키우는 순간 데드락에 빠진다. 신입이 처음 겪는 통과의례이자, 재현 조건이 “문제 크기”라서 제일 억울한 버그.
  • 논블로킹: MPI_Isend / MPI_Irecv가 요청 핸들만 남기고 즉시 반환하고, MPI_Wait·MPI_Waitall·MPI_Test로 완료를 확인한다. 데드락이 구조적으로 사라지고, 무엇보다 통신과 계산을 겹칠 여지가 생긴다. 현대 HPC 코드의 halo 교환은 사실상 전부 이 형태다.
  • MPI_Sendrecv: 보내고 받는 것을 한 호출로 묶어 구현체가 순서를 알아서 정하게 한다. 링 형태 교환에서 데드락 걱정 없이 쓰는 안전판.

버퍼는 반드시 MPI_Wait가 끝난 뒤에 건드려야 한다. MPI_Isend를 걸어 놓고 그 배열을 바로 덮어쓰는 것은 정의되지 않은 동작이며, 대개 값이 조용히 깨진다.

4. 집합통신[편집]

커뮤니케이터의 모든 랭크가 함께 참여하는 것이 집합통신(collective)이다.

연산하는 일전형적 용도
MPI_Bcast한 랭크의 데이터를 전원에게입력 파라미터·형상 배포
MPI_Reduce / MPI_Allreduce합·최대·최소 등으로 축약내적, 잔차 노름, Δt\Delta t 결정
MPI_Gather / MPI_Scatter모으기 / 흩뿌리기I/O, 초기 분할 결과 배포
MPI_Alltoall각자가 각자에게 다른 조각을3D FFT 전치, 입자 재배치
MPI_Barrier전원 동기화대개 불필요하다(타이밍 측정 정도)

집합통신을 손으로 짠 루프보다 우선하는 이유는 구현체가 토폴로지에 맞는 알고리즘을 골라 주기 때문이다. 예컨대 MPI_Allreduce는 메시지가 짧으면 재귀 배가(recursive doubling)로 log2P\lceil \log_2 P \rceil 단계에 끝내고, 길면 reduce-scatter + allgather(Rabenseifner 방식)로 바꿔 전송량을 줄인다. 같은 일을 사용자가 별 모양 루프로 짜면 O(P)O(P)가 된다.

주의할 함정 하나. 집합통신은 커뮤니케이터의 모든 랭크가 같은 순서로 호출해야 한다. if (rank == 0) 안에 MPI_Allreduce를 넣는 순간 나머지가 영원히 기다린다. 그리고 부동소수점 합의 결과는 축약 트리 모양에 따라 달라지므로, 랭크 수를 바꾸면 잔차 마지막 자리가 흔들린다 — 버그가 아니라 부동소수점 연산의 비결합성이다.2

5. 유령셀 교환 — CFD/FEM의 실제 패턴[편집]

실전에서 MPI 코드의 90%는 사실 유령셀(halo) 교환 하나다. 격자영역 분할법으로 쪼개 각 랭크가 한 조각을 맡으면, 조각 경계의 스텐실이 이웃 조각의 값을 요구한다. 그래서 조각 테두리에 이웃 값을 복사해 두는 유령층을 두고 매 스텝 그것만 갱신한다.

분할을 정하는 것은 METIS·ParMETIS 같은 그래프 분할기의 몫이고, 이때 최소화하는 edge-cut이 곧 halo 메시지의 총량에 대응한다. 여기서 MPI와 부하 분산이 만난다 — 분할이 나쁘면 어떤 랭크는 계산이 늦게 끝나고(불균형), 어떤 랭크는 이웃이 너무 많아 메시지 수가 폭증한다(통신 과다). 랭크당 이웃 수는 대개 6~30 정도로 유지되도록 잡는다.

구현 디테일 두 가지가 성능을 가른다.

  • 패킹. 3차원 배열의 한 면은 메모리상 연속이 아니다. 손으로 버퍼에 모아 담거나 MPI_Type_create_subarray로 파생 데이터형을 정의해 넘긴다. 스트라이드 접근을 구현체에 맡길지 직접 패킹할지는 벤치마크로 정할 문제.
  • 영속 통신. 매 스텝 같은 상대에게 같은 크기를 보내므로, MPI_Send_init/MPI_Start로 통신을 한 번 등록해 두고 재사용하면 요청 생성 오버헤드가 사라진다. 수천 스텝을 도는 시간전진 루프에서 공짜로 얻는 몇 퍼센트.

6. 통신-계산 중첩[편집]

논블로킹의 진짜 목적은 데드락 회피가 아니라 중첩(overlap)이다. 표준 레시피는 이렇다.

  1. MPI_Irecv를 먼저 전부 건다(수신 버퍼를 미리 등록해야 rendezvous가 빨라진다).
  2. MPI_Isend로 경계 데이터를 보낸다.
  3. halo가 필요 없는 내부 셀을 계산한다.
  4. MPI_Waitall.
  5. 경계 셀을 계산한다.

내부 셀 계산 시간이 통신 시간보다 길면 통신이 통째로 숨는다. 다만 냉정하게 말하면, 중첩은 생각만큼 잘 안 된다. MPI 구현체가 별도 진행 스레드를 돌리지 않으면 통신은 MPI 함수 안에 들어갔을 때만 진행되므로, 3단계 동안 실제로는 아무것도 전송되지 않다가 MPI_Waitall에서 몰아서 하는 일이 흔하다. 벤더 옵션(비동기 진행 스레드)이나 하드웨어 오프로드가 켜져 있는지 확인하고, 켜기 전후를 측정하지 않은 “중첩했습니다”는 믿을 게 못 된다.

7. 비용 모형과 확장성 한계[편집]

메시지 하나를 보내는 시간은 통상 지연-대역폭 모형으로 근사한다.

T(n)=α+βnT(n) = \alpha + \beta n

α\alpha는 메시지당 고정 비용(지연, latency), β\beta는 바이트당 비용(대역폭의 역수), nn은 메시지 크기다. 현대 노드 간 인터커넥트에서 대략 α12μs\alpha \sim 1\text{–}2\,\mu\text{s}, 1/β1025 GB/s1/\beta \sim 10\text{–}25\ \mathrm{GB/s} 수준이다. 두 항이 같아지는 크기는

n1/2=αβ1050 kBn_{1/2} = \frac{\alpha}{\beta} \approx 10\text{–}50\ \mathrm{kB}

이다. 즉 수십 kB보다 작은 메시지는 아무리 짧게 만들어도 α\alpha가 지배한다. 1 kB짜리를 100번 보내는 것과 100 kB를 한 번 보내는 것의 전송량은 같지만 시간은 자릿수가 다르다. “메시지를 합쳐라(message aggregation)“가 MPI 최적화의 1번 조언인 이유이고, 반대로 랭크를 늘려 조각을 잘게 쪼갤수록 메시지가 작아져 α\alpha의 늪에 빠진다는 뜻이기도 하다.

halo 교환은 이웃하고만 하므로 PP가 늘어도 랭크당 메시지 수가 거의 일정하다. 문제는 **MPI_Allreduce**다. 전원이 참여하므로 비용이 최소 O(αlogP)O(\alpha \log P)이고, 무엇보다 가장 늦은 랭크에 전체가 묶인다. 크리로프 부분공간법은 반복마다 내적을 여러 번 하는데, GMRES의 그람-슈미트 직교화는 스텝당 O(m)O(m)번의 전역 축약을 요구한다. 랭크가 수만을 넘어가면 부동소수점 연산이 아니라 이 동기화가 벽이 되고, 여기서 통신 회피 알고리즘이라는 분야 전체가 튀어나온다.

확장성의 정량화는 병렬 컴퓨팅 문서의 암달의 법칙·강/약 스케일링이 담당한다. MPI 문맥에서 하나 덧붙이면 등효율 함수(isoefficiency)가 있다. 효율을 일정하게 유지하려면 문제 크기 WW를 총 오버헤드 To(W,P)T_o(W,P)에 비례해 키워야 한다는 관계 W=KTo(W,P)W = K\,T_o(W,P)인데, 통신 오버헤드가 PP에 완만하게 의존하는 코드일수록 등효율 함수가 낮고 “확장성이 좋다”고 말한다.

8. MPI-3 이후 — 논블로킹 집합통신과 단측통신[편집]

MPI-3.0(2012)이 가져온 것 중 시뮬레이션 코드에 실제로 영향을 준 둘.

논블로킹 집합통신. MPI_Iallreduce·MPI_Ibcast·MPI_Ialltoall이 생겨, 전역 축약을 걸어 두고 그동안 다른 계산을 할 수 있게 됐다. 파이프라인 크리로프 부분공간법(KSPPIPECG 계열)이 이걸로 내적 지연을 행렬-벡터 곱 뒤에 숨긴다. MPI_Ibarrier는 “누가 나에게 보낼지 미리 모르는” 희소 데이터 교환 패턴을 짜는 데 쓰이는 의외의 주력 도구다.

단측통신(RMA). MPI_Win_allocate로 메모리 창(window)을 열어 두면, 다른 랭크가 MPI_Put·MPI_Get·MPI_Accumulate상대의 참여 없이 그 메모리를 읽고 쓴다. 송수신 짝을 맞출 필요가 없으므로 비정형·비대칭 통신(적응 격자, 동적 부하 분산, 그래프 순회)에 잘 맞고, RDMA 하드웨어에 그대로 매핑된다. 대가는 동기화가 어렵다는 것 — MPI_Win_fence 같은 능동 타깃, MPI_Win_lock/MPI_Win_flush 같은 수동 타깃 중 무엇을 쓸지, 메모리 모델(separate/unified)이 어떻게 되는지를 알아야 해서 진입 장벽이 높다. MPI-4.0(2021)은 여기에 영속 집합통신과 분할 통신(partitioned communication)을 더해, 스레드 여러 개가 하나의 큰 메시지를 조각별로 채워 넣는 GPU 친화적 패턴을 정식화했다.

9. 하이브리드 MPI+OpenMP[편집]

노드 하나에 코어가 수십~수백 개인 시대에 코어마다 랭크를 하나씩 띄우면, 노드 안에서도 유령셀이 생기고 랭크 수가 폭증해 α\alpha 비용과 메모리 중복이 함께 늘어난다. 그래서 노드(또는 소켓/NUMA 도메인)당 랭크 하나 + 내부는 스레드로 가는 하이브리드가 표준이 됐다.

이때 MPI_Init 대신 MPI_Init_thread로 스레드 지원 수준을 요청해야 한다. MPI_THREAD_FUNNELED(주 스레드만 MPI 호출), MPI_THREAD_SERIALIZED(한 번에 하나씩), MPI_THREAD_MULTIPLE(아무나 동시에) 순으로 자유도가 커지는데, MULTIPLE은 구현체 내부 락 때문에 성능이 오히려 떨어지는 경우가 많다. 실무에서 제일 흔한 절충은 통신은 주 스레드가 전담하는 FUNNELED이고, 스레드 바인딩(OMP_PROC_BIND, 랭크별 코어 고정)을 안 하면 하이브리드는 순수 MPI보다 느려진다. GPU가 붙으면 여기에 GPU-aware MPI(디바이스 포인터를 그대로 넘겨 호스트 복사를 건너뛰는 것)가 추가되며, 자세한 것은 GPU 컴퓨팅 참고.

10. 현업 체크리스트[편집]

  • 프로파일러(mpiP, Score-P, 벤더 도구) 없이 “통신이 병목인 것 같다”고 말하지 않는다. 대개 병목은 부하 불균형인데 대기 시간이 MPI_Wait에 계상되어 통신처럼 보인다.3
  • 랭크 수를 바꿔 가며 같은 결과가 나오는지 확인한다. 안 나오면 halo 폭이 스텐실보다 좁거나, 축약 순서 의존성이 있거나, 둘 중 하나다.
  • 작은 메시지를 합치고, 영속 통신을 쓰고, MPI_Barrier를 지운다. 이 셋으로 대개 두 자릿수 퍼센트가 나온다.
  • 랭크 수천 이상에서 강 스케일링이 꺾이면 halo가 아니라 MPI_Allreduce 횟수를 먼저 센다.4

11. 관련 문서[편집]

12. Footnotes[편집]

  1. MPI를 대체하겠다며 등장한 PGAS 계열(UPC, Coarray Fortran, Chapel)의 공통 판매 포인트가 “통신을 안 보이게 해 준다”였는데, 정작 성능을 뽑으려면 어디서 통신이 일어나는지 알아야 해서 결국 MPI만큼 어려워진다는 것이 30년간 반복된 교훈이다.

  2. 그래서 “128랭크 결과와 256랭크 결과가 12번째 자리부터 다릅니다”라는 문의는 대부분 정상이다. 문제가 되는 건 그 차이가 카오스적으로 증폭되는 경우인데, 그건 MPI 탓이 아니라 문제 자체가 그런 것이다. 비트 재현성이 필요하면 결정론적 축약 순서를 강제하는 라이브러리를 따로 써야 하고, 그 대가는 당연히 속도다.

  3. 랭크 512개 중 하나가 남들보다 5% 더 일하면, 나머지 511개의 대기 시간이 전부 MPI_Waitall에 쌓인다. 통신 프로파일이 새빨갛게 나와서 인터커넥트를 의심하다가, 알고 보니 분할기 ufactor가 헐렁했던 것으로 밝혀지는 전개는 이 바닥의 클래식이다.

  4. 실제로 “잔차 노름을 매 반복 찍어 보려고” 넣은 MPI_Allreduce 하나가 대규모 실행에서 20% 넘게 먹는 사례가 나온다. 수렴 판정을 몇 반복에 한 번만 하도록 바꾸는 것만으로 해결되는데, 그 사실을 알아내는 데 이틀이 걸린다.