# MPI ## Message Passing Interface (MPI) - 跨語言的 **平行程式 API** 標準,常用在 HPC 領域的程式開發 - 通常支援直接 C/C++ 及 Fortran - 透過擴充的 library 也可以在 Java、Python 等語言執行 - 定義 **processes** 之間的的 **通訊介面/協定** - 高階的訊息傳遞 API,比 POSIX 原生的 IPC 更容易使用 - 同時有極高的效率以及移植性 - MPI 可以透過各類型的網路(TCP、IB)做 **跨 nodes 的溝通** - MPI 程式可以 **直接運行在 cluster 上**,**不需要修改原始碼** - 執行時在指令中加入相關參數,就能指定要執行在哪些 nodes 上 - MPI 本身就支援多 nodes 執行的功能 - 不需要 Slurm 等管理軟體也能直接在多節點上執行,但通常還是會搭配 Slurm 使用 - 只需要基本的網路設定並啟用 SSH、NFS 服務 - 除了提供溝通的介面,MPI 本身就會讓 **程式平行化** - 用 **Multi-Process** 的方式平行化 - MPI 會依照硬體環境或指令參數,**自動啟動多個 process** - 不需要用 `fork()` 之類的 API 手動建 process - 在 cluster 中,會自動讓 process 在不同 nodes 上啟動 ### 名詞定義 MPI 環境中,有下列幾個主要的物件 - 呼叫 MPI 的 API 進行溝通前,需先透過 API 初始化這些物件 :::success **Communicator** (`MPI_Comm`) - MPI 中的一種物件,用來進行 **processes 間的溝通** - 一個 communicator 可以連結多個 processes - processes 必須在 ***同一個 communicator 中才能互相溝通*** - 用網路中的術語來比喻: - 把每個 process 當作一台電腦 - 那 communicator 就是連接所有電腦的 link (或是 switch) - 大部分 API 需要 communicator 作為參數,指定要進行傳輸工作的是哪一組 processes - **`MPI_COMM_WORLD`** 是一個 communicator 常數,會在程式開始執行時 **被系統初始化** - 連結 **本次執行時**,啟動的 **所有 processes** ::: :::info **Group** - 一個 **communicator 中的所有 processes**,所成的一個集合 - 一個 group 對應到一個 communicator - 和 communicator 不同的是 - **Group** 指的是 **processes 的集合** (Collection of processes) - **Communicator** 是 **用來進行訊息傳輸的物件**,它連結了某個 group 中的 processes ::: :::warning **Rank** - 一個整數,用來 **辨識不同 process**,由 MPI 環境自動指派 - **同一個 communicator** 中,**rank 不會重複** - Rank 會 **從 0 開始** - 若一 communicator 中共有 **n 個 processes**,那 rank 的範圍就是 **0~n-1** - **rank 加上 communicator**,即可對應 **某一個特定的 process** ::: - 除了系統自動初始化的 `MPI_COMM_WORLD`,也可以自行透過相關的 API 另外建立其他的 communicator 和 group ![](https://hpc-tutorials.llnl.gov/mpi/images/comm_group600pix.gif) ### MPI Implementations MPI 只是一個 **介面**,只定義了標準和規格,而 **沒有特定的實作** - 只是一套標準,而不是一套特定的軟體、套件或 library - 但 MPI 實作至少要滿足 MPI 定義的所有標準 所以目前有許多不同組織開發、發行的版本 (通常是套件及 library 的形式) :::success **OpenMPI** - **開源** 的 MPI Library - 支援 Unix 和 Unix-Like 作業系統 - 可以跨 (硬體) 平台執行 ::: :::info **Intel MPI** - Intel 開發、發行的 MPI - 針對 **Intel CPU 優化** ::: 以上兩種是常見的 MPI 版本,兩者沒有絕對的優劣 - 實際的效能差異,受硬體硬體平台和執行的軟體影響 ## MPI Commands ### Compile MPI 編譯器是 **gcc/g++ 的 wrapper** - MPI 原始碼和 library 被 MPI 編譯器處理好後,**最終的編譯仍是 gcc/g++ 執行** - 參數和 **使用方式** 都和 **gcc/g++ 相同** 完全用 C 開發的原始碼 - 使用 **mpicc** 編譯 - 對應 **gcc** :::info **Example** 編譯原始碼 `hello.c`,並將執行檔存檔為 `hello` ```bash mpicc hello.c -o hello ``` ::: 有用到 C++ 語法的原始碼 - 使用 **mpic++** 或是 **mpicxx** 編譯 - 對應 **g++** :::info **Example** 編譯原始碼 `hello.cc`,並將執行檔存檔為 `hello` ```bash mpic++ hello.cc -o hello # or mpicxx hello.cc -o hello ``` ::: ### Execute MPI 程式編譯後的二進位檔案(執行檔)需要用 `mpirun` 或 `mpiexec` 指令執行 :::info **Example** 啟動執行檔 `hello` ```bash mpirun hello # or mpiexec hello ``` - 以上指令沒有額外參數和選項,MPI 會開啟和 **CPU 總核心數數量** 相同的 processes - 如果是 *4 核心的 CPU*,就會啟動 *4 個 processes* - 如果 **用 Slurm 先分配好** CPU,那 **只使用分配到的 CPU** - E.g., `salloc -n 4 mpirun hello` - 分配 4 個 CPU core - 對 `mpirun` 來說,系統就只會有 4 個 CPU core,所以會啟動 4 個 processes ::: - `mpirun` 和 `mpiexec` 後面接的參數是 **執行檔的 path**,可以是絕對或相對路徑 - 所以 **`mpirun hello`** 和 **`mpirun ./hello`** 是相同的意義 - 這邊的 `./hello` 不是 "_啟動 hello 檔案_",而是檔案的相對路徑 - 代表目前目錄下的 `hello` 檔案 <!-- - 預設情況下,MPI 會自動把 **一個 process,bind 到一個 CPU core 上** - **一個 process** 只會跑在 **一個固定的 core 上** - Bind 的方式可以透過其他 MPI 指令設定 --> <!-- 實驗後發現 MPI 本身好像不會綁定 Slurm 才會 --> ### Command-line Arguments 如果要執行的程式有 command-line arguments,arguments 要放在 **執行檔名稱之後** :::info **Example** ```bash mpirun hello PJ ``` - `PJ` 為 `hello` 的 argument ::: ### 指定 Process 數量 - `-np <num>`: 啟動 `<num>` 個 processes - `-c`, `-n`, `--n` 或 `-np` 都是相同的功能 :::info **Example** 用 4 個 process 執行 `hello` ```bash mpirun -c 4 hello # or mpirun -n 4 hello # or mpirun --n 4 hello # or mpirun -np 4 hello ``` ::: ## MPI 執行原理 - 使用 `mpirun` 指令時,會自動建立多個 process,並透過這些 process 執行指定的 binary ### 實驗 - 引入 `unist.h` 以使用 `getpid()` 和 `getppid()` - 這兩個 API 可以拿到目前 process 的 PID 和 praent process 的 PID ```c // demo.c #include <mpi.h> #include <stdio.h> #include <unistd.h> int main(int argc, char** argv) { MPI_Init(&argc, &argv); int pid = getpid(); int ppid = getppid(); printf("PID: %d, PPID: %d\n", pid, ppid); MPI_Finalize(); } ``` - **Compile & Run** ``` mpicc demo.c -o demo mpirun -np 4 demo ``` - **Output** ``` PID: 3235, PPID: 3231 PID: 3236, PPID: 3231 PID: 3237, PPID: 3231 PID: 3238, PPID: 3231 ``` > 數值僅供參考,實際 PID 在執行時才會確定 - 所有 process 的 parent 都是同一個,且不是這幾個 process 的任何一個 - 可以確定額外的 process 不是由 `demo` 的 process 產生的 - 也就是說,我們寫的程式本身不會建立任何額外的 process - 和 `fork()` 的行為不同 ### 使用 `ps` 指令觀察 - 在程式中加上 `sleep()`,拉長整體的執行時間 - 在執行結束前,打開另一個 terminal,並且用 `ps` 指令觀察系統中的所有 process ```c // demo.c #include <mpi.h> #include <stdio.h> #include <unistd.h> int main(int argc, char** argv) { MPI_Init(&argc, &argv); int pid = getpid(); int ppid = getppid(); printf("PID: %d, PPID: %d\n", pid, ppid); sleep(5); // 暫停程式 5 秒 MPI_Finalize(); } ``` 執行結果如下: ![](https://hackmd.io/_uploads/By0csuunh.png) - 執行檔 `demo` 的 4 個 processes 的 parent 都是 3231 - 從 `ps` 的結果可以看到 3231 確實就是 `mpirun` - 可以確定 `demo` 的所有 process 都是由 `mpirun` 啟動的 ## MPI Hello World! - 以下範例程式會使用到一些常用或必要的 API - 詳細的 API 說明,會在其他幾篇筆記中 ### MPI 程式基本架構 - MPI 的所有 API 和常數都定義在 header 檔案: `mpi.h` - MPI 程式中一定要呼叫 `MPI_Init()` 和 `MPI_Finalize()` - 所有 MPI API 的呼叫一定要在 `MPI_Init()` 之前,`MPI_Finalize()` 之後 :::info ```c #include <mpi.h> int main(int argc, char** argv) { // non-MPI function calls // ... MPI_Init(&argc, &argv); // MPI and non-MPI function calls // ... MPI_Finalize(); // non-MPI function calls // ... } ``` ::: **`MPI_Init(int*, char***)`** - **初始化** 執行環境 - 呼叫此 function **前**,***不能*** 呼叫其他 MPI function,且此 function 在一個 process 中 **只能呼叫一次** - 通常以 `argc` 和 `argv` 的指標當引數呼叫 ```c int main(int argc, char** argv) { MPI_Init(&argc, &argv); // ... } ``` - 如果程式不需要 command line argument,可以用 `NULL` 當引數呼叫 ```c int main(int argc, char** argv) { MPI_Init(NULL, NULL); // ... } ``` **`MPI_Finalize()`** - **終止** 執行環境 - 呼叫此 function **後**,***不能*** 呼叫其他 MPI function,且此 function 在一個 process 中 **只能呼叫一次** - 如果沒有呼叫 `MPI_Finalize()`,只要有其中 **一個 process 結束** 執行,**所有 processes 都會被強制終止** - 可能導致程式有非預期的執行結果 - 部分 process 可能執行到一半就被中斷 :::success **Example: Hello, World!** ```c // hello.c #include <stdio.h> #include <mpi.h> int main(int argc, char** argv) { // init MPI environment MPI_Init(&argc, &argv); printf("Hello, World!\n"); // finalize MPI environment MPI_Finalize(); } ``` - **Compile & Run** ```bash mpicc hello.c -o hello mpirun -np 2 hello ``` - **Output** ``` Hello, World! Hello, World! ``` ::: :::warning - **不像** Linux 原生的 `fork()` 或是 OpenMP - 呼叫 `fork()` 後才會平行執行 - 使用 OpenMP,被標記為要 multi-threading 的地方才會平行執行 - MPI 程式從 **啟動到結束**,**全程都是平行執行** - 在 `MPI_Init()` 前、和 `MPI_MPI_Finalize()` 後的程式碼,也會被所有 processes 執行 - 代表 **額外的 process *不是* 在程式執行期間建立的** - 所以下面程式碼有一樣的執行結果 ```c #include <stdio.h> #include <mpi.h> int main(int argc, char** argv) { printf("Hello, World!\n"); // init MPI environment MPI_Init(&argc, &argv); // finalize MPI environment MPI_Finalize(); } ``` - 建議還是把 `MPI_Init()` 寫在最前面、`MPI_Finalize()` 寫在最後面,可讀性會更好 ::: ### 取得環境資訊 **`MPI_Comm_rank(MPI_Comm comm, int* rank)`** - 取得目前 process,在某個 communicator 中的 rank - 第一個參數是要查詢的 communicator - 第二個參數是 int pointer,指向用來存 rank 的記憶體空間 **`MPI_Comm_size(MPI_Comm comm, int* size)`** - 取得某個 communicator 的 size (有幾個 process) - 第一個參數是要查詢的 communicator - 第二個參數是 int pointer,指向用來存 size 的記憶體空間 :::success **Example: Get Rank and Communicator Size** ```c // hello.c #include <mpi.h> #include <stdio.h> int main(int argc, char** argv) { // init MPI environment MPI_Init(&argc, &argv); // 宣告儲存 rank 和 size 的變數 int rank, comm_size; // 把 rank、size 的指標傳給 MPI_Comm_rank()、MPI_Comm_size() // 讓 funciton 可以修改 rank、size 的值 (pass by pointer) MPI_Comm_rank(MPI_COMM_WORLD, &rank); MPI_Comm_size(MPI_COMM_WORLD, &comm_size); printf("Process %d-%d: Hello, World!\n", rank, comm_size); // finalize MPI environment MPI_Finalize(); } ``` - **Compile & Run** ```bash mpicc hello.c -o hello mpirun -np 2 hello ``` - **Output** ``` Process 0-2: Hello, World! Process 1-2: Hello, World! ``` > 實際輸出的順序可能不同,因為沒辦法確定哪個 process 會先執行 :::