教材: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)