教材:10710周志遠教授平行程式
https://www.youtube.com/playlist?list=PLS0SUwlYe8cxqw70UHOE5n4Lm-mXFXbZT
20250824 筆記
內容可能有錯僅供參考
6A~6B Communication Routines and Parallel Function Code
今日大綱
MPI 通訊 API 介紹
點對點通訊 API 和參數介紹
集體通訊的核心概念
Communicator 和 Group 管理
### MPI 通訊 API 概覽
通訊的 API 呢,你可以發現它大致可以分成兩種:
* **點對點 (Point-to-Point) 通訊**:這表示只有一個傳送者 (Sender) 和一個接收者 (Receiver) 之間進行通訊。
* **集體 (Collective) 通訊**:這表示有多個接收者或多個傳送者參與通訊,通常涉及一個群組裡的所有處理器。
最後,我們也會稍微介紹如何建立新的群組 (Group) 和通訊器 (Communicator) 的 API。
---
### 點對點通訊 (Point-to-Point Communication)
點對點的 API 相對比較少,也比較單純,主要就是 `Send` 和 `Receive`。
#### 核心概念:Blocking 與 Non-blocking
這點非常重要!之前我們教過的概念,這些 Function Call 其實可以分成 **Blocking** 和 **Non-blocking** 兩種。
* **Blocking Code** 通常是用來做 **同步 (Synchronous) 通訊**。
* **Non-blocking Code** 通常是用來做 **非同步 (Asynchronous) 通訊**。
命名方式通常是這樣:如果是 Non-blocking 的話,它會在前面加一個 `I`,所以你可能會看到 `ISend` 和 `IRecv`。
#### `Send` 和 `Receive` 的主要參數
不論是 `Send` 或 `Receive`,它們都會跟著一堆參數。`Send` 和 `Receive` 之間會有些不一樣,而 Blocking 和 Non-blocking 之間也會有一些差異。
我們來一個一個稍微解釋這些參數:
1. **Buffer**
* 這是第一個也是最重要的參數,它是一個 **Pointer**,指向一段記憶體空間。
* MPI 的目的很單純,就是讓你透過一個 Message 來傳遞資訊。而所謂的 Message,其實就是一段記憶體。
* 所以 `Send` 和 `Receive` 的 Buffer 參數,對於 `Send` 來說,就是指向你要送出去的記憶體位置;對於 `Receive` 來說,就是指向你要接收資料的記憶體位置。
* **非常重要的一點:這個記憶體空間是** **需要你自己去 `allocate` (配置) 的**!如果你沒有配置,就直接把一個指標指向沒有配置的記憶體空間,那麼在執行時**非常有可能會碰到錯誤**。因為 MPI Library 不知道這段記憶體有沒有被配置,它會直接開始讀寫,所以要先配置好記憶體再把指標傳進去。
2. **Type 和 Count**
* 這兩個參數的目的是因為 Buffer 只是一個起始位置的指標。對於這個 Buffer 到底有多長,有多少空間讓你寫資料,其實是由 `Type` 和 `Count` 這兩個相乘來決定的。
* 簡單來說,就是這個 Buffer 裡面存的是什麼 `Type` 的資料,然後有多少個這樣的資料元素 (Element)。所以相乘就是它的 Size。
* 這個 `Type` 參數在 Library 內部會做 Size 的判斷,然後知道要傳輸多少 byte。
* 各位會發現 `Type` 不是隨便的什麼 `int`,它是 **`MPI_Datatype`**,也就是大家所知道的 `enum` 。例如,你可能用 `MPI_INT`。
* **為什麼要用 `MPI_Datatype` 呢?** 因為我們在安裝 MPI Library 的時候,它會自動配置在電腦上,例如 `integer` 會定義成幾個 byte。有些系統的 `integer` 可能是 4 個 byte,有些可能是 8 個 byte,它的定義可能會不一樣。所以用 `MPI_Datatype` 會比較保險。如果以為 `integer` 是 4 個 byte,但實際上在電腦上是 8 個 byte,那麼配置的空間可能就會不夠,可能就會出問題。
* **建議大家:這個 `Type` 的選擇非常重要,不要跳過。** 不要想說 `int` 和 `long` 這裡一定差兩倍,不一定!要用 `MPI_Datatype` 來指定。當然配置這些放進去的元素的資料型態,也應該要和這個 `MPI_Datatype` 相符,不然都會有風險存在。
* 對於 MPI Library 來說,它其實沒有 **資料** 的概念,它只有 **記憶體大小** 的概念。所以它只關心那個空間有多少個 byte,它不會去檢查你放進去的資料是什麼。
* **常見的錯誤:** 有時候程式出錯,通常就是因為 `Type` 選錯了,或者跟放進去的資料型態不匹配,導致讀取或寫入了不對的空間。
3. **Comm**
* 所有 MPI 的 API 都有這個 `Comm` 參數。它就是 **Communicator Token**。
* 預設就是 `MPI_COMM_WORLD`,除非你自己建立新的。
4. **Source 或 Destination**
* 如果是 `Send` 或 `Receive`,會有一個 `Source` 或 `Destination`。
* 這很好理解,如果要 `Send`,就是要 `Send` 到哪裡去;如果要 `Receive`,就是從哪裡接收。
* 這個值是一個 `integer`,也就是 **`Rank ID`**。
* 所以之前取得 `Rank ID` 的時候,它的 `Comm` 應該要跟這裡相同,才會是同一個群組。
5. **Tag (可選)**
* 這個參數可能在比較複雜的應用中會用到。
* 主要目的是,當 `Send` 和 `Receive` 的時候,只說從誰那邊拿到訊息,或者送到哪一個目的地。但有時候我們的訊息是有分類的,例如這是通知 (notification) 還是計算用的資料。
* 這時候用 `Tag` 就很方便。`Tag` 在傳送時也是一個 `integer`。
* **`Send` 和 `Receive` 的 `Tag` 必須匹配**,那個訊息才會真的被傳送。
* 可以用 `Tag` 很簡單地過濾訊息類型。這樣即使同時送很多種訊息過來,也可以透過正確的 `Tag` 過濾出你真的想接收的訊息。
* **注意:** 訊息在傳輸的過程中可能會有一個 Buffer 暫存資料。網路有時候很複雜,它有很多路徑,不代表先送的資料會先到。所以 `Receiver` 其實不會按照順序,它就是**先到先拿**。
* 如果不限制 `Tag`,或者把它留空,通常就是用 **`MPI_ANY_TAG`**,表示任何 `Tag` 的訊息都可以接收。
6. **Request (Non-blocking 獨有)**
* 對於 `Non-blocking` 的 `ISend` 和 `IRecv`,你會發現它多了一個 `Request` 參數。
* 如果今天是 `Non-blocking` 的 Code,你呼叫了 `IRecv` 之後,這個動作沒有完成,它其實會立刻回傳,然後執行下一個 Statement。
* 所以很多時候,如果下一個 Statement 依賴於上面這個 `Non-blocking` Code 的結果,就需要檢查當初這個 `IRecv` 到底有沒有做完。
* 這時候就可以透過這個 `Request` 這個 Token 來查詢。可以用 `Request` 去查詢當時的狀態到底是什麼。
* 可以有一個迴圈一直檢查,直到它的 `Request` 狀態變成完成,你才能去取裡面的資料。不然你的記憶體緩衝區還是沒有分析出來的資料。
* `Request` 的目的就是這樣,它可以在後面的時候查詢它的狀態,尤其 `Non-blocking` 需要。
7. **Status (Blocking Receive 獨有)**
* 如果是 `Blocking` 的 `Receive`,會看到一個 `Status` 參數。
* 它很單純,裡面會記錄 `Receive` 這個動作的一些資訊,可能是接收了多少個 byte,這個 API Call 有沒有成功等等。
* 但在 `Blocking` 模式下,我們通常只檢查它的 Return Code 就夠了,因為它回傳就代表動作做完了。
#### 點對點通訊的常見錯誤與注意事項
* **匹配 `Send` 和 `Receive`:** 撰寫 MPI 程式時遇到的第二個常見問題就是:「我的程式不會結束!」 這通常是因為 `Send` 和 `Receive` 沒有匹配起來。例如,`Send` 了卻沒有人 `Receive`,或者 `Receive` 了卻沒有人 `Send`,它就會卡在那邊。這種情況很難除錯,因為程式卡在中間,你不知道卡在哪一行。
* **記憶體獨立性:** 即使不同處理器上,變數名稱都叫 `X`,但它們的記憶體空間是獨立的。`Rank 0` 的 `X` 和 `Rank 1` 的 `X` 是完全不同的變數,它們在不同的機器上,不同的記憶體空間裡。所以,要透過 `Send` 和 `Receive` 把資料從這邊複製到那邊。**不要忘記每個 Process 你要獨立去看待它的所有變數,都不能共享,只能透過通訊來複製資料讓它們可以溝通**。
* **傳遞指標而非值:** 當你傳送一個單一變數 (例如 `int x`) 時,請傳遞它的 **(address, `&x`)**,而不是它的值 (`x`)。這是一開始大家寫程式時常常會弄錯的地方。
#### Non-blocking 通訊的例子與優化
Non-blocking 的用法會比較 Tricky。當你呼叫 `ISend` 或 `IRecv` 時,它會立刻回傳。
* 例如,呼叫 `IRecv` 之後,程式會馬上執行下一行,但此時並不能保證 `X` 的值已經被更新成 Sender 端傳來的。
* 為了確保資料正確,你可以使用 **`MPI_Wait`**。`MPI_Wait` 的意思就是它會一直卡在那邊,持續 Block 在這行,直到 `Request` 狀態變成完成,這個 API Call 才會回傳。換句話說,如果你下面要 `print X`,它就應該寫在 `MPI_Wait` 之後,那時 `X` 的值才會是正確的。
* `MPI_Wait` 是用來 Block,而 Sender 端如果其實不關心這個通訊的結果,甚至可以不呼叫 `MPI_Wait`,讓它繼續做自己的事情。
除了 `MPI_Wait`,你也可以用 **`MPI_Test`**。
* `MPI_Wait` 會一直 Block,直到狀態完成。
* `MPI_Test` 則不會 Block,它只是單純查詢 `Request` 的狀態是什麼,然後立刻回傳,不論完成與否。所以你可以用迴圈搭配 `MPI_Test` 來檢查。
**為什麼大家還會用 Non-blocking Code 呢?**
* 因為它的效能一定比較好!
* Non-blocking Code 通常是用來做一個很重要的優化,就是去 **重疊通訊和計算**。
* 在平行程式中,任何不能被平行化以及會浪費時間的通訊,如果能把它們優化掉就很好。
* 透過非同步通訊的方式,如果你的計算跟 `Send` 或 `Receive` 動作是獨立的 (沒有 Dependency),那麼當 `Send` 正在進行時,Sender 可以立刻開始做它自己的計算。
* 這就等同於在等待接收端接收訊息的通訊時間,你的計算時間和通訊時間就可以重疊。這能大幅減少總執行時間。
* 這個優化技巧雖然比較複雜,但會發現這能有效減少整個計算過程中通訊和計算這兩個階段的交替時間。
---
### 集體通訊 (Collective Communication)
事實上,在寫 MPI 程式時,會發現更多時候你會用到集體通訊。因為點對點通訊通常只適用於一對一的通訊,但在大型平行程式中,有很多處理器需要共同作業。
#### 集體通訊的核心概念
* 集體通訊的差別在於它會牽涉到一個 **群組 (Group)** 裡的所有處理器。
* 這個群組裡面的所有處理器,**都必須要同時呼叫 (Call) 這個函式,然後同時回傳**。這就是 Collective 的意思,大家一起來做。
* 這個「大家」其實是對 **Comm (Communicator) 所定義的群組**。如果你用預設的 Communicator (例如 `MPI_COMM_WORLD`),那就是所有處理器。
* 因為它是 Collective Code,大家要一起呼叫,所以基本上 **所有的集體通訊 API 都是 Blocking 的**。而且它會被任何一個處理器給 block 住。
* 雖然集體通訊很容易使用,但它往往成為效能瓶頸。
#### 各種集體通訊 API
1. **`MPI_Barrier`**
* **目的:** 主要是為了 **同步化 (Synchronize)** 這些處理器。
* **定義:** 它沒有做任何有意義的資料傳輸。它就只是個屏障。
* **機制:** 所有的處理器都必須呼叫到這個 `MPI_Barrier` 時,它們才能從這個函式回傳。
* **用途:** 確保群組內的所有處理器從這個時間點開始一起做下面的事情。
* **例子:** 如果有檔案 I/O,某個處理器先處理完事情,把資料寫到檔案之後,另一個處理器再從檔案讀出來。這就形成了一個依賴關係,這時候就需要 `MPI_Barrier` 來確保大家都已經把之前該做的事情做完,資料都已經更新,然後大家才開始往下做。
* **參數:** 非常簡單,只有一個 `Comm` (Communicator) 參數。
2. **`MPI_Bcast`**
* **目的:** 從 **一個 Root Process** 把資料複製一份,傳送給群組內 **所有其他處理器**。
* **參數:** `buffer`, `count`, `datatype`, **`root`** (Root Process 的 Rank ID), `comm`。
* **機制:** Root Process buffer 內的資料會被廣播出去,覆蓋掉所有接收端 buffer 內的資料。所有群組內的處理器呼叫完這個函式後,它們buffer 的值都會變成 Root Process 廣播出來的資料。
* **注意:** 所有處理器都必須同時呼叫這個函式,即使是 Root Process 也不例外。如果任何一個處理器沒有呼叫,所有人都會被 block。
3. **`MPI_Scatter`**
* **目的:** 從 **一個 Root Process**,把一個陣列的資料**分散給群組內所有處理器**,每個處理器會收到陣列的不同部分。
* **參數:**
* `sendcount`:Root Process 發送給「每個」處理器的元素數量**。
* `recvcount`:**每個處理器接收的元素數量** (通常等於 `sendcount`)。
* **機制:** `Scatter` 會按照 `Rank ID` 的順序來分發資料,例如 `Rank 0` 拿到陣列的第一段,`Rank 1` 拿到第二段,依此類推。
* **注意:** 接收端處理器 **不需要分配與 `sendbuf` 同等大小的記憶體空間**。它只需要為自己的 `recvbuf` 分配足夠空間即可。
* **用途:** 讀取大量資料後,由一個處理器讀取並分散給其他處理器進行平行計算。
4. **`MPI_Gather`**
* **目的:** 將群組內 **所有處理器** 的資料收集到 **一個 Root Process** 上,並將其連接成一個陣列。
* **行為與 `MPI_Scatter` 相反**。
* **參數:** `sendbuf` , `sendcount` , `sendtype` (), `recvbuf` , `recvcount` , `recvtype` , `root` , `comm` 。
* `sendbuf`:在每個處理器上,指向要發送的資料。
* `recvbuf`:在 Root Process 上,指向收集所有資料的 buffer。
* **注意:** Root Process 的 `recvbuf` 必須足夠大,以容納所有收集到的資料。
* **用途:** 平行計算完成後,將所有處理器的結果收集到一個處理器上,例如寫入檔案。
5. **`MPI_Reduce`**
* **目的:** 與 `Gather` 很像,但 `Reduce` 不只是收集資料,它還會透過一個 **運算子 (Operator)** 將所有處理器的資料進行Aggregation 運算,最終得到一個 **單一結果**,並存放在 **一個Root Process** 上。
* **參數:** `sendbuf` , `recvbuf` , `count` , `datatype` , `op`, `root` , `comm` 。
* `op`:內建的運算子,例如 `MPI_SUM` , `MPI_MAX` , `MPI_MIN` , `MPI_LAND` , `MPI_LOR` 。
* 你也可以定義自己的運算子,但這會比較複雜。
* **用途:** 加總所有處理器的部分結果,找出最大值等。
6. **`MPI_Allreduce`**
* **目的:** 與 `MPI_Reduce` 類似,但 reduce 後的 **結果會寫到群組內所有處理器的 `recvbuf` 裡面**。
* **參數:** `sendbuf` , `recvbuf` , `count` , `datatype` , `op` , `comm` 。**沒有 `root` 參數**。
* **概念上等同於 `MPI_Reduce` 加上 `MPI_Bcast`**。
* **重要觀念:** 如果你能夠使用一個內建的 API 來完成功能,**千萬不要自己去寫或把多個 API 組合起來**。因為 MPI Library 在實作這些 API 時,通常會內部做一些優化。`MPI_Allreduce` 的實作會比你自己呼叫 `MPI_Reduce` 再呼叫 `MPI_Bcast` 更有效率。而且程式碼也會更簡潔。
* **建議:** 寫 MPI 程式時,多查一下還有哪些 API Code,看有沒有更適合直接拿來使用的,會很有幫助。
#### 變形集體通訊 API (`V` 結尾)
* 除了這些基本的集體通訊 API 之外,還可以找到更多變形。例如 `MPI_Gatherv` 或 `MPI_Scatterv`。
* 這些帶有 `v` 結尾的 API (例如 `Gatherv`) 允許 **每個處理器發送/接收不同數量的資料**。
* 例如,當資料數量無法被處理器數量整除時,又不希望浪費空間或傳輸時間,就可以讓它們傳輸不同數量。
* 這些 `v` 結尾的 API 會多出一些參數,用來指定每個 Rank 的位置 (Displacement) 和數量。
* **原則:** 按照需求去看看有沒有符合的 API,有就直接用,沒有才想辦法自己實現。
---
### Communicator 和 Group 管理
最後,我們要介紹的是 MPI 的管理 API,特別是如何建立群組 (Group) 和通訊器 (Communicator)。
#### Group 與 Communicator 的區別
* **Group (群組)**:定義了一組處理器 (Process)。
* **Communicator (通訊器)**:是一個 Token,所有通訊 API 都需要它來在群組內進行通訊。
* 它們兩個是不同的東西。所有我們剛才看到的 API,你都一定需要 Communicator。
#### 建立新群組和通訊器的流程
它的過程有一點點繁瑣,因為 MPI 的設計就是這樣。
你需要先建立群組,然後才能透過這個群組取得它的 Communicator,才能開始透過它在群組裡面做溝通。
1. **`MPI_Comm_group`**:
* 從一個現有的 Communicator (例如 `MPI_COMM_WORLD`) **取回它對應的 Group Token**。
* 這樣就取得了該群組的結構。
2. **`MPI_Group_incl`**:
* 有了 Group 之後,要建立新的群組,通常是建立 **子群組 (Subgroup)**。
* 它會告訴 Library 說,這個新的群組要包含原來群組裡面的哪幾個 Rank ID 的處理器。
* 可以從最初的 `MPI_COMM_WORLD` 對應的群組開始建立子群組。
3. **`MPI_Comm_create`**:
* 建立了新的 Group 之後,需要透過這個 Group 來建立新的 **Communicator**。
* 這樣你才能透過這個新的 Communicator 在新的群組裡面開始溝通。
* 這個 API 會需要新的 Group,以及一個 Key (通常是從原 Communicator 來的)。
#### 重要的注意事項:**所有管理 API 都是 Collective 的**
* 只要是涉及到群組管理的 API (例如 `MPI_Comm_group`, `MPI_Group_incl`, `MPI_Comm_create`),**所有涉及這個 Communicator 的處理器都必須呼叫這些 API**。
* 不論這個處理器會不會被包進新的群組裡面,它都得呼叫。
* 如果有一個處理器沒有呼叫,它就會卡在那裡,因為這些都是屬於邏輯上的同步操作。
**舉例來說:** 如果你原來有 9 個處理器 (Rank 0-8),你想要把 Rank 0-3 設為一個新的群組,把 Rank 5-8 設為另一個新群組。Rank 4 雖然沒有被包含在任何一個新群組中,但它也必須呼叫 `MPI_Comm_group`、`MPI_Group_incl`、`MPI_Comm_create` 這些 API。最後,新的 Communicator 可以被用來呼叫 `MPI_Barrier` 等,這樣 Rank 0-3 就會獨立地同步,Rank 5-8 也會獨立地同步。
---
### MPI 程式設計的真正挑戰
**真正困難的地方在於:**
* **演算法的設計**
* **如何處理資料依賴性**
* **什麼時候去呼叫 `Send` 和 `Receive`**
* **如何減少 `Send` 和 `Receive` 的資料量,減少通訊時間**
* **如何將通訊與計算進行重疊 ,以優化效能**
API 本身只是提供了一個發送和接收訊息的機制,方便使用。但如何巧妙地運用這些機制來設計高效能的平行程式,才是真正的挑戰!
---
其他課程連結
[平行程式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)