---
# System prepended metadata

title: 平行程式5A~5B The Latest Developments and Applications Using Parallel Programming
tags: [平行程式]

---

教材:10710周志遠教授平行程式
https://www.youtube.com/playlist?list=PLS0SUwlYe8cxqw70UHOE5n4Lm-mXFXbZT

20250823 筆記
內容可能有錯僅供參考

5A~5B The Latest Developments and Applications Using Parallel Programming

今日大綱
MPI 是什麼?
MPI 的歷史和演進
MPI 的程式設計模型
Synchronous (同步) 和 Asynchronous (非同步) 的差別
Synchronous 和 Blocking 的差別
Message Buffer 介紹
MPI API 的基本使用
溝通要有 Identity: Groups, Rank, Communicator


### MPI 是什麼?

**MPI** 這三個字代表的是 **Message Passing Interface**。
它是一個用於平行計算中，處理 Process 之間**訊息傳遞的溝通介面**。

在平行程式計算中，溝通方式主要有兩種：一種是 **共享記憶體 (shared memory)**，另一種就是**訊息傳遞 (message passing)**。MPI 屬於後者，它提供了一套 API，讓應用程式可以使用訊息傳遞的方式來進行溝通。

那麼，為什麼它只是一個「介面 (interface)」呢？ 「介面」代表它只是一個**規範**。它定義了 API 的**參數**和**回傳值**應該是什麼，但**並沒有規定這些介面要如何實際去實作**。所以，身為應用程式的程式設計師，我們透過這個介面來撰寫程式碼，呼叫這些 API。但這些 API 的實際運作，則是由更了解底層系統架構和硬體的開發者來實作的。

這些實作出來的就稱為 **MPI library**。不同的 MPI library 可能會針對特定的系統或網路拓撲 (topology) 進行優化，例如：

- 針對 **Ethernet 網路**，可能會有像 **MPICH** 這樣的實作.
- 而對於像 **InfiniBand** 這樣的高效能網路，則會有專門的實作，例如 **MVAPICH**。

這就表示 MPI 雖然是一個穩定的規範，但它的**實作可以是多樣的**，以達到最佳效能。

### MPI 的目標：可攜性、可擴展性、彈性

有了這個「介面」的概念，我們就可以來談談 MPI 帶來的幾個重要好處：

- **可攜性 (Portability)**： 程式設計師不需要知道底層複雜的網路或硬體細節。只要你的系統安裝了支援該平台的 MPI library，你今天寫的 MPI 程式碼就可以**在任何支援 MPI 的系統上執行**。你只需要重新編譯你的程式，不需要修改任何程式碼，這讓程式變得非常具有可攜性。
    
- **可擴展性 (Scalability)**： MPI 選擇訊息傳遞作為溝通方式，這是因為訊息傳遞**非常容易擴展**。它可以輕易地在數千甚至數萬台電腦上執行，而不會增加底層溝通機制的複雜度。這使得 MPI 成為處理**大型平行計算**，例如超級電腦或資料中心的首選方案。
    
- **彈性與職責分離 (Flexibility / Separation of Concerns)**： MPI 介面將程式設計師 和系統開發者的職責分開。程式設計師可以專注於應用程式的邏輯和領域知識；而底層的系統開發者則專注於針對特定硬體和系統進行優化。這種分離讓不同的人可以專注解決不同的問題。
    

### MPI 的歷史和演進

平行計算的需求其實很早就存在了。早期，大家各自為政，發展出許多不同的平行程式庫，但這些程式庫通常是**互不相容**的。這導致程式的重複開發和不便。

因此，學術界、業界的研究人員、開發者，以及平行系統的開發者們組成了一個社群，共同討論並**制定了一個大家都認同的標準介面**。這個介面就是 MPI。

**MPI 的演進時間軸 (Timeline):**

- **1980 年代：** 社群開始討論制定標準。
- **1993 年：** **MPI 1.0 版本正式發布**。它是在美國的 **Supercomputing (SC) 大會**上首次提出的。這個大會是全球高性能計算領域的重要盛會，結合了學術界和業界的力量。
- **1996 年：** **MPI 2.0 版本發布**。這個版本**主要增加了 MPIO (MPI I/O)** 的功能。因為一開始大家專注於計算和溝通，但後來發現資料的輸入/輸出 (I/O) 也越來越重要。
- MPI 2.0 也開始加入一些 **One-sided Communication** (單邊通訊) 的 API。
2012年: MPI 3.0版本發布：使其不只是純粹的訊息傳遞，也能提供類似共享記憶體的直接讀寫能力，以提升效能或便利性。
- **2021 年已經有 MPI 4.0**，並且 **MPI 5.0 正在規劃中** 。
    
有興趣可以看這個網站介紹 MPI 4.0  https://www.mpi-forum.org/mpi-40/

### MPI 的程式設計模型 (Programming Model)

MPI 主要採用的是 **訊息傳遞 (Message Passing)** 模型。 除此之外，它也屬於另一種程式設計模型，稱為 **單一程式，多重資料 (Single Program, Multiple Data, SPMD)**。

#### 什麼是 SPMD?

SPMD 的意思是，當你寫平行程式時，**你實際上只寫了一個程式碼。這個程式碼會被**執行在不同的處理器或多台電腦上。

- 在每個處理器上，你的程式碼會**建立一個 process**，在 MPI 的世界裡，我們通常稱之為 **MPI task**。
- 整個透過 MPI 執行的一系列任務，我們稱為一個 **Job**。例如，你在實驗課執行的一個 MPI 應用程式就是一個 Job。
- 這些 MPI tasks 雖然執行**相同的程式碼**，但它們會處理不同的資料。

#### Branch (分支)

為了讓這些執行相同程式碼的 tasks 能夠處理不同的資料或執行不同的行為，我們需要使用**分支 (branch)** 或**條件式語句 (conditional statements)**，例如 `if/else` 或 `switch`。

而實現這種分支的關鍵，就是它們擁有**獨特的身份識別 (Identity)**。

#### 獨特的 ID (Rank) 讓 Task 具有不同行為

一開始，所有 process (task) 都執行相同的程式碼，看起來都一樣。但有一個東西是不同的，那就是它們的 **ID**。

- 這個 ID 在 MPI 的世界裡稱為 **rank (或 task ID)**。
- 當程式在啟動時，系統會為每個 process 分配一個**獨特的 rank 值**。這個 rank 值從 **0 開始，並連續遞增。** 
- 程式設計師就可以在程式碼中利用這個 rank 值來判斷：「如果我是 rank 0，我就做這件事；如果我是 rank 1，我就做那件事。」。這樣就實現了用單一程式碼處理多重資料或執行不同行為的目的。

#### Task 數量固定且重複執行

- 在執行 MPI 程式時，**task 的數量通常是固定的**。當你使用 `mpirun -np <num_processes>` 指令啟動程式時，就已經決定了要建立多少個 tasks。雖然 MPI 2.0 之後提供了動態建立 tasks 的功能，但通常不建議使用，因為會增加複雜性並影響效能。
- 這也意味著，如果你想用不同數量的 tasks 來測試你的程式，你**不需要修改程式碼**。你只需要在執行 `mpirun` 時，給予不同的 `-np` 參數值即可。

### 溝通方法

即使 tasks 執行相同的程式碼和不同的資料，它們在計算過程中**必然需要相互溝通**。這正是 MPI 最主要的功能所在。

MPI 提供了多種類型的溝通函式 (communication functions)，例如 `Send` (傳送)、`Receive` (接收)、`Broadcast` (廣播)、`Gather` (收集) 等。但它們都建立在最基本的 **`Send` 和 `Receive` 概念**之上。

我們可以從兩個角度來看待溝通行為：

1. **溝通的同步性 (Synchronous vs Asynchronous)**
2. **函式呼叫的行為 (Blocking vs Non-blocking)**

#### Synchronous (同步) 和 Asynchronous (非同步) 的差別

這是從**溝通的觀點**來看的，強調的是**參與溝通的雙方是否同步**。

- **Synchronous Communication (同步溝通):** 就像打電話。發送方撥號，接收方必須接聽，然後雙方才能開始對話。溝通的過程是**同步的**，發送方會等待接收方準備好，並確認資料被接收。它確保了在特定時間點雙方都在進行溝通，並可以控制執行流程。
    
- **Asynchronous Communication (非同步溝通):** 就像寄電子郵件或信件。發送方發送出去後，並不知道接收方是否立即收到或何時會收到。發送方不需要等待接收方確認，就可以繼續執行自己的事情。雙方雖然可以溝通，但溝通的過程並非同步的，不能保證在同一時間點進行溝通。
    

#### Function Call 本身 Blocking  vs Non-blocking 的差別

這是從**函式呼叫者 (caller) 的角度**來看的，強調的是**函式呼叫後是否會立即回傳**。

- **Blocking Call :** 當你呼叫一個 blocking 函式時，程式會**停留在該函式呼叫處，直到該函式所代表的「事情」完全完成**，才會回傳。例如，一個 blocking `send` 函式只有在資料被完全傳送出去後才會回傳。你的程式會被 block 在那裡等待。
    
- **Non-blocking Call :** 當你呼叫一個 non-blocking 函式時，程式會**立即回傳**，而不會等待函式所代表的完整操作完成。例如，一個 non-blocking `send` 函式可能只是先把資料複製到一個 buffer 中，然後就立即回傳。實際的資料傳送會在背景進行。這樣你的程式就可以在等待資料傳送的同時，繼續執行其他運算。
    
#### Synchronous 和 Blocking 的差別

這兩者概念很像，但**實際上有區別**。

- **Synchronous** 更側重於**溝通雙方在時間上的協調和控制**。
- **Blocking** 更側重於**單一函式呼叫的行為**，即呼叫者是否等待操作完成。

雖然一個 **blocking call 通常會被用來實現 synchronous communication**，而 **non-blocking call 則常用來實現 asynchronous communication**。但實際上，你也可以用 non-blocking call 來實現 synchronous communication，反之亦然。Blocking 不僅限於溝通，它也可以應用到任何需要等待操作完成的情況。

在這門課中，為了簡化理解，我們通常會將 **blocking 呼叫等同於同步溝通**，而 **non-blocking 呼叫等同於非同步溝通**。

#### Synchronous / Blocking Message Passing 範例

以一個 **blocking send** 和 **blocking receive** 為例：

- **Sender (發送方):** 如果發送方呼叫 `send` 函式，而接收方還沒有呼叫 `receive` 函式，那麼發送方會**block 並且等待**。它會一直等待，直到接收方準備好接收資料為止。一旦接收方準備好，資料就會透過網路複製過去，然後雙方都會解除 block 並繼續執行。
    
- **Receiver (接收方):** 同樣地，如果接收方呼叫 `receive` 函式，而發送方還沒有發送資料，那麼接收方也會**block 並等待**。它會等待直到發送方將資料送達。
    

這種情況下，**「速度較慢」的一方會讓「速度較快」的一方等待**，直到兩者在某個時間點同步，然後才能完成資料傳輸。

#### Message Buffer 

message buffer 是實現 **non-blocking communication** 的關鍵。

- 當你呼叫一個 non-blocking `send` 時，資料會先被**複製到這個 message buffer 中**。函式會立即回傳，而發送方程式不需要等待接收方。
- 這個 buffer 的**實際位置 (在發送方或接收方)** 是 MPI library 實作的細節。重要的是，這個 buffer 是由 MPI library 所管理的資源。
- **需要特別注意的是：** 如果一個 non-blocking `receive` 函式在發送方還沒將資料放入 buffer 之前就被呼叫了，那麼 `receive` 函式會**立即回傳，但其內部的資料可能是空的或不正確的**。此時，程式設計師需要額外去**監控狀態**，以確認資料是否真的已經到達 buffer。

這個 message buffer 以及其他必要資源，都是在程式一開始呼叫 **`MPI_Init()`** 時，由 MPI library 進行初始化的。

### MPI API 的基本使用

所有 MPI 的程式都必須包含 **`mpi.h` 標頭檔**

#### MPI Calls 的格式

MPI 函式呼叫通常遵循以下格式： `rc = MPI_Xxx(parameter, ...)` 

- `MPI_Xxx` 代表不同的 MPI 函式 (例如 `MPI_Init`, `MPI_Comm_size` 等)。
- `rc` 是**回傳碼 (return code)**。它會告訴你這個函式呼叫是成功還是失敗。
- 如果函式呼叫成功，`rc` 的值會是 **`MPI_SUCCESS`** 
- 由於回傳值 `rc` 用來指示錯誤碼，所以如果函式需要傳回其他資料，通常會透過**指標 (pointer)** 的方式，將參數傳入函式，並在函式內部修改該指標所指向的記憶體位址，以此來「帶回」結果 (by reference)。


- **`MPI_Init(&argc, &argv)`:** 這是**所有 MPI 函式呼叫中必須是第一個**的函式 。
- 它的作用是**初始化 MPI 執行環境**，包括設定必要的資源和資料結構，例如 Message Buffer。
    
- **`MPI_Finalize()`:** 這是**所有 MPI 函式呼叫中必須是最後一個**的函式 。
- 它的作用是**終止 MPI 環境**，並釋放所有 MPI 相關的資源。如果在程式結束前沒有呼叫 `MPI_Finalize()`，可能會導致 resource leak 或其他問題。
    
### 溝通要有 Identity: Groups, Rank, Communicator

為了能夠進行溝通，每個 process 都需要有一個「身份」。

- **Group (群組):** **群組定義了哪些 processes 之間可以相互溝通** 。
有點像不同論壇或聊天群組，你只能在同一個群組內發言。
    
- **Rank (任務 ID):** **Rank 是每個 process 在其所屬 communicator 中獨特的識別符號 (task ID)** 。這個 ID 是系統在 process 初始化時分配的。Rank 值是**連續的，並且從零開始** 。
    
- **Communicator (通訊器):** 每個群組都關聯著一個**通訊器** 。**通訊器是實際用於執行溝通函式呼叫的實體**。當你呼叫 MPI 溝通函式時，你會把通訊器傳進去，這樣 MPI library 就知道你是在哪一個群組內進行溝通。

#### `MPI_COMM_WORLD`

當你第一次撰寫 MPI 程式時，通常不會太在意 communicator 的設定，因為 MPI 提供了一個**預設的通訊器**，叫做 **`MPI_COMM_WORLD`** 。

- `MPI_COMM_WORLD` 代表了**所有啟動的 processes (tasks)**。也就是說，所有透過 `mpirun` 啟動的 processes 都會自動包含在這個 `MPI_COMM_WORLD` 通訊器中，並可以在其中進行溝通。
- 在 `MPI_COMM_WORLD` 中，每個 process 都會有一個獨特的 rank ID。

你可以透過 API 創建新的群組和對應的通訊器，將 `MPI_COMM_WORLD` 中的 processes 分割成更小的群組。但無論如何，新創建的群組都一定是從 `MPI_COMM_WORLD` 這個總群組中分出來的。

### 兩個最基本的 MPI API

除了 `MPI_Init()` 和 `MPI_Finalize()` 之外，還有兩個非常重要的 API 可以幫助你獲取每個 process 的身份資訊：

1. **`MPI_Comm_size(comm, &size)`:**
    
    - **目的：** 用來**確定指定通訊器 (`comm`) 中有多少個 processes (total size)**。
    - `comm`: 你想要查詢的通訊器，例如 `MPI_COMM_WORLD`。
    - `&size`: 一個整數指標，函式會將該通訊器中的 processes 總數儲存到這個位址。
2. **`MPI_Comm_rank(comm, &rank)`:**
    
    - **目的：** 用來**確定目前呼叫該函式的 process 在指定通訊器 (`comm`) 中的 rank (唯一的 ID)**。
    - `comm`: 你想要查詢的通訊器。
    - `&rank`: 一個整數指標，函式會將目前 process 的 rank 值儲存到這個位址。
    - 這個 `rank` 值就是我們前面提到的，讓每個 process 能夠執行不同行為的關鍵身份識別。

所以，一個基本的 MPI 程式通常會先 `MPI_Init()`，然後呼叫 `MPI_Comm_size()` 獲取總任務數，再呼叫 `MPI_Comm_rank()` 獲取自己的 rank ID，最後在程式結束前 `MPI_Finalize()`。

---
#### MPI 程式的Hello World

```
#include <stdio.h>  
#include "mpi.h"    // 包含 MPI 標頭檔 

int main(int argc, char *argv[]) {

    // Serial code 部分
    // ...

    // 宣告變數來儲存 process 的 rank (ID) 和總 process 數量 (size) [
    int world_rank;
    int world_size;

    // 初始化 MPI 環境 
    // 這是第一個必須被呼叫的 MPI 函數 
    MPI_Init(&argc, &argv);

    // Parallel code 部分開始
    // ...
    // 執行工作並進行訊息傳遞呼叫
    // ...

    // 取得預設通訊群 (communicator) MPI_COMM_WORLD 中的總 process 數量
    // MPI_COMM_WORLD 代表程式中所有的 process 
    MPI_Comm_size(MPI_COMM_WORLD, &world_size);

    // 取得當前 process 在 MPI_COMM_WORLD 中的 rank (ID) 
    // Rank 是 process 的唯一識別碼，從 0 開始編號 
    MPI_Comm_rank(MPI_COMM_WORLD, &world_rank);

    // 每個 process 都會執行這段程式碼，並印出自己的 rank 和總 process 數量
    printf("Hello world from process rank %d of %d\n", world_rank, world_size);

    // 終止 MPI 環境 
    // 這是結束程式前必須呼叫的函數，用於釋放 MPI 資源 
    MPI_Finalize();

    // Parallel code 部分結束
    
    // Serial code 部分
    // ...

    return 0;
}
```

**程式說明：**


• **#include "mpi.h"**：這是使用 MPI 庫時必需包含的標頭檔，它包含了所有 MPI 函數的原型和常數定義。

• **MPI_Init(&argc, &argv)**：

    ◦ 在任何其他 MPI 函數被呼叫之前，必須先呼叫此函數來**初始化 MPI 環境**。

    ◦ 它負責初始化所需的資源和資料結構，例如 message buffer 。

• **MPI_Comm_size(MPI_COMM_WORLD, &world_size)**：

    ◦ 此函數用於**獲取預設通訊群** **MPI_COMM_WORLD** **中的 process 總數量**。

    ◦ `MPI_COMM_WORLD` 是一個預設通訊群，包含了所有啟動的 MPI process。

    ◦ `world_size` 變數將會儲存這個總數量。

• **MPI_Comm_rank(MPI_COMM_WORLD, &world_rank)**：

    ◦ 此函數用於**獲取當前 process 在** **MPI_COMM_WORLD** **中的唯一 ID (Rank)**。

    ◦ Rank 是一個整數，從 0 開始編號。每個 process 都會有不同的 `world_rank` 值。

    ◦ `world_rank` 變數將會儲存這個 ID。

• **printf("Hello world from process rank %d of %d\n", world_rank, world_size);**：

    ◦ 每個 MPI process 都會執行這段程式碼。由於每個 process 的 `world_rank` 不同，它們將會印出獨特的訊息，顯示自己是第幾個 process 以及總共有多少個 process 。

    ◦ 這種「每個 process 執行相同的程式碼，但處理不同的資料或根據其 Rank 執行不同分支」的模式稱為 **Single Program Multiple Data (SPMD)** 模型，這是 MPI 的主要程式設計模型之一。

• **MPI_Finalize()**：

    ◦ 在程式結束之前，必須呼叫此函數來**終止 MPI 環境並釋放所有已分配的 MPI 資源**。如果沒有呼叫此函數，可能會導致 resource leak。

    ◦ 這是程式結束前必須呼叫的最後一個 MPI 函數。

當編譯並執行這個程式時（例如使用 `mpicc` 編譯，再用 `mpirun -np 4 ./your_program` 執行 4 個 process），會看到類似以下的輸出：

```
Hello world from process rank 0 of 4
Hello world from process rank 1 of 4
Hello world from process rank 2 of 4
Hello world from process rank 3 of 4
```

請注意，輸出的順序可能不會固定，因為 process 的執行順序可能因系統而異

---
其他課程連結

[平行程式1C~2B Introduction parallel programming](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/Syxh3H7Kxe)
[平行程式3A~3D The Latest Developments and Applications Using Parallel Programming](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/HJh7QFVKle)
[平行程式4A~4B IO Parallel IO and Program Analysis](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/HJLMsuHFgg)
[平行程式5A~5B The Latest Developments and Applications Using Parallel Programming](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/SJh57hIFle)
[平行程式6A~6B Communication Routines and Parallel Function Code](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/r1X9kX_Fle)
[平行程式 6C~6D Communication Routines and Parallel Function Code](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/S1DPjoYFlx)
[平行程式 7A~8A Pthread:Synchronization Problem & Tools](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/HJu-_0tKge)
[平行程式 8B~8D Synchronization Tools & Open Multi-Processing(OpenMP)](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/H1ki4E2Fee)
[平行程式 9A~9B Synchronization Construct](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/BJTYMrpKlx)
[平行程式 10A~10B Synchronization Tools & Open Multi-Processing Synchronization Construct](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/B1cY6M1qee)
[平行程式 10C~10D Synchronization Tools & Open Multi-Processing Synchronization Construct](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/BkgFaNg5gg)
[平行程式 11A~11B Parallel Work Pool and Termination / Parallel Sorting](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/H1hfOw-5xl)
[平行程式 12A~12B Parallel Sorting and Pipelined Computations](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/Symo-zQ9eg)
[平行程式 12C~12D Parallel Sorting and Pipelined Computations](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/BJYNKDVceg)
[平行程式 13A-13B  Sychronous Parallelism](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/HJ2UJ2Bqex)
[平行程式 14A~14B Heterogeneous Computing](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/BksS4yP5eg)
[平行程式 14C~14D Heterogeneous Computing](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/BJrfTUd9xx)
[平行程式 15A~15B Parallel Programming Model on GPU](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/ByWnl-t5gg)
[平行程式 16A~16B What is Compute Unified Device Architecture(CUDA)?](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/HyYpsjcqgl)
[平行程式 17A~18A 平行運算的CUDA](https://hackmd.io/@6FOC2dvARe-Vz0kVSyajew/H1dUeBT5lg)
[平行程式 18B~19A  記憶體層級 / CUDA的優化](https://hackmd.io/@JuitingChen/HyF44e1jge)
[平行程式 19B~19D 記憶體層級 / CUDA的優化 ](https://hackmd.io/@JuitingChen/ryPEu4lieg)
[平行程式 20A~20B CUDA優化全域和區域記憶體/共享記憶體](https://hackmd.io/@JuitingChen/r1X659Zoxl)
[平行程式 21A~21B Parallel Reduction / Distributed Computing Framework](https://hackmd.io/@JuitingChen/HyiOpozjxl)
[平行程式 NTHU-PP-Chap10-Big Data-Part1 ](https://hackmd.io/@JuitingChen/Hyc-e3Golx)
[平行程式 NTHU-PP-Chap10-Big Data-Part2 ](https://hackmd.io/@JuitingChen/ryC_QTXoxl)
[平行程式 NTHU-PP-Chap11-MapReduce](https://hackmd.io/@JuitingChen/HJgBXJOsge)
[平行程式 NTHU-PP-Chap12-Distributed Training-Part1](https://hackmd.io/@JuitingChen/ryh5hBtsge)
[平行程式 NTHU-PP-Chap12-Distributed Training-Part2](https://hackmd.io/@JuitingChen/rJ2G7kdjxg)
[平行程式 NTHU-PP-Chap12-Distributed Training-Part3](https://hackmd.io/@JuitingChen/HkA471dilx)
[平行程式 NTHU-PP-Chap13-UCX-Part1](https://hackmd.io/@JuitingChen/rJbq103ieg)
[平行程式 NTHU-PP-Chap13-UCX-Part2](https://hackmd.io/@JuitingChen/SJpNmk_ixl)
[平行程式 NTHU-PP-Chap13-UCX-Part3](https://hackmd.io/@JuitingChen/HkIUYa13xe)
