---
# System prepended metadata

title: MPI -基本原理
tags: [MPI, HPC]

---

# 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 會先執行
:::
