CPU/GPU 同步與完整流程 (Synchronization and Full Workflow)
重點總覽
| 項目 | 重點 |
|---|---|
| Kernel launch 非同步 | <<<>>> 啟動相對於呼叫它的 CPU thread 是非同步的;host control flow 會在 kernel 完成前(甚至啟動前)就繼續往下執行 |
cudaDeviceSynchronize |
阻塞 host thread,直到 GPU 上所有先前發出的工作全部完成;本章範例足夠用 |
| Stream/Event 同步 | 大型應用有多個 stream 時,cudaDeviceSynchronize 會等所有 stream;建議改用 stream 同步 API 或 CUDA Events 只等特定工作 |
| 完整 vecAdd 流程 | 端到端:配置記憶體 → H2D copy → launch kernel → sync → D2H copy → 驗證 → 釋放 |
| Unified vs Explicit | 兩版本同一 kernel;unified 用 cudaMallocManaged 免手動 copy,explicit 用 cudaMalloc+cudaMemcpy |
| 結果驗證 | 用 CPU 序列版 serialVecAdd 算同一題,再以 vectorApproximatelyEqual(含 epsilon 容差)比對浮點結果 |
| nvcc 建置 | nvcc vecAdd.cu -o vecAdd;可傳向量長度當命令列參數(預設 1024) |
| Runtime Initialization | CUDA runtime 為每個 device 建立 primary context,於第一個需要 context 的 runtime 函式時隱式初始化;含 device code 的 JIT 編譯與載入 |
cudaSetDevice (CUDA 12.0+) |
會顯式初始化 runtime;務必檢查其回傳值以捕捉初始化錯誤 |
Kernel Launch 非同步與為何需要同步 (2.1.4)
Kernel launch 相對於呼叫它的 CPU thread 是非同步的:launch 語句送出工作後立刻回傳,host 的控制流會在 kernel 完成執行之前繼續往下跑,甚至可能在 kernel 真正啟動之前就往下執行。因此若要在 host code 中保證 kernel 已完成才繼續(例如讀取結果),就必須加入同步機制。
最簡單的方式是 cudaDeviceSynchronize:它阻塞呼叫它的 host thread,直到 GPU 上所有先前發出的工作都完成為止。
vecAdd<<<blocks, threads>>>(A, B, C, vectorLength); // 非同步,立即回傳
cudaDeviceSynchronize(); // 阻塞 host,等 GPU 全部做完
// 此後才能安全地讀取 C 的結果
- 非同步:launch 不等 kernel 跑完即回傳,讓 CPU 能與 GPU 重疊工作。
cudaDeviceSynchronize等待的是「所有先前發出的工作」,不限單一 kernel。- 本章範例因為 GPU 上只有單一操作,用
cudaDeviceSynchronize已足夠。
CPU thread GPU
─────────── ───
vecAdd<<<>>>() ──launch──▶ [ kernel 開始排程/執行 ]
(立即回傳) │ │
...可做其他事 │ │ (CPU 與 GPU 並行)
cudaDeviceSynchronize() ──┐ │
(阻塞等待) ◀──────────┘ 完成 ◀──┘
讀取 C[] ← 此時結果才保證就緒
若在 kernel launch 後未同步就讀取輸出緩衝區,可能讀到 kernel 尚未寫入的舊值,產生不確定結果。記憶體釋放(cudaFree)前同樣需確保 kernel 已完成。
cudaDeviceSynchronize
當應用有多個 stream 同時在 GPU 上執行工作時,cudaDeviceSynchronize 會等待所有 stream 的工作完成,可能過度同步、抹掉重疊效益。此時建議改用 Stream Synchronization API(如 cudaStreamSynchronize)只等特定 stream,或用 CUDA Events 做更細粒度的同步。詳見 02-Programming-GPUs/14-Async-Streams-and-Events。
完整 vecAdd 端到端流程 (2.1.5)
本節把整個向量相加範例串起來,含 kernel、host code 與驗證工具函式。範例預設向量長度 1024,也可由命令列參數指定不同長度。共有兩個版本:Unified Memory 與 Explicit Memory Management,兩者用的是同一個 kernel。
共用的 kernel 與驗證工具
__global__ void vecAdd(float* A, float* B, float* C, int vectorLength) {
int workIndex = threadIdx.x + blockIdx.x * blockDim.x;
if (workIndex < vectorLength) // 邊界檢查,避免越界
C[workIndex] = A[workIndex] + B[workIndex];
}
- 每個 thread 由
threadIdx.x + blockIdx.x*blockDim.x算出唯一全域索引,各做一筆獨立加法。 if (workIndex < vectorLength)防止當blocks*threads大於資料量時越界存取。- 驗證流程:CPU 用序列版
serialVecAdd算同一題,再用vectorApproximatelyEqual(帶epsilon=0.00001容差)比對,浮點不可用==直接比。
Unified Memory 版本(最精簡)
float *A, *B, *C;
cudaMallocManaged(&A, vectorLength*sizeof(float)); // 統一記憶體,CPU/GPU 皆可存取
cudaMallocManaged(&B, vectorLength*sizeof(float));
cudaMallocManaged(&C, vectorLength*sizeof(float));
initArray(A, vectorLength); // host 直接初始化
initArray(B, vectorLength);
int threads = 256;
int blocks = cuda::ceil_div(vectorLength, threads); // 向上取整算 grid 大小
vecAdd<<<blocks, threads>>>(A, B, C, vectorLength);
cudaDeviceSynchronize(); // 等 kernel 完成
serialVecAdd(A, B, comparisonResult, vectorLength); // CPU 對照
// ...vectorApproximatelyEqual(C, comparisonResult, ...) 驗證...
cudaFree(A); cudaFree(B); cudaFree(C); // unified memory 用 cudaFree 釋放
- 用
cudaMallocManaged配置統一記憶體,不需手動cudaMemcpy:unified memory 會確保 A、B、C 對 GPU 可存取。 - 流程精簡為:配置 → host 初始化 → launch → sync → 驗證 →
cudaFree。
Explicit Memory 版本(手動搬移)
// host 指標 A,B,C;device 指標 devA,devB,devC
cudaMallocHost(&A, n*sizeof(float)); // page-locked host 記憶體,做 copy 的最佳實踐
// ... B, C 同 ...
initArray(A, n); initArray(B, n);
cudaMalloc(&devA, n*sizeof(float)); // 配置 device 記憶體
cudaMalloc(&devB, n*sizeof(float));
cudaMalloc(&devC, n*sizeof(float));
cudaMemcpy(devA, A, n*sizeof(float), cudaMemcpyDefault); // H2D
cudaMemcpy(devB, B, n*sizeof(float), cudaMemcpyDefault);
cudaMemset(devC, 0, n*sizeof(float));
vecAdd<<<blocks, threads>>>(devA, devB, devC, n);
cudaDeviceSynchronize();
cudaMemcpy(C, devC, n*sizeof(float), cudaMemcpyDefault); // D2H 取回結果
cudaFree(devA); cudaFree(devB); cudaFree(devC); // device 用 cudaFree
cudaFreeHost(A); cudaFreeHost(B); cudaFreeHost(C); // page-locked 用 cudaFreeHost
free(comparisonResult); // 一般 malloc 用 free
- host 緩衝區用
cudaMallocHost(page-locked / pinned memory),對要與 GPU 互傳的緩衝區是最佳實踐,可提升 copy 效能、且為非同步傳輸所必需。 cudaMemcpyDefault讓 CUDA 依來源/目的指標自動判斷拷貝方向。- 對應釋放 API 要配對:
cudaMalloc↔cudaFree、cudaMallocHost↔cudaFreeHost、malloc↔free。
端到端流程 (Explicit Memory)
┌──────────┐ ┌───────────┐ ┌──────────┐ ┌──────┐ ┌──────────┐ ┌──────┐
│ 配置記憶體 │──▶│ H2D copy │──▶│ launch │──▶│ sync │──▶│ D2H copy │──▶│ 驗證/ │
│cudaMalloc│ │cudaMemcpy │ │ kernel │ │等GPU │ │cudaMemcpy│ │ free │
└──────────┘ └───────────┘ └──────────┘ └──────┘ └──────────┘ └──────┘
(Unified Memory 省略 H2D / D2H 兩步,由系統按需搬移)
建置與執行
nvcc vecAdd_unifiedMemory.cu -o vecAdd_unifiedMemory
./vecAdd_unifiedMemory # 預設長度 1024
./vecAdd_unifiedMemory 4096 # 命令列參數指定長度
# 輸出: Unified Memory: CPU and GPU answers match
此 vecAdd 中所有 thread 做獨立工作,不需互相協調或同步。實務上 thread 常需合作:同一 block 內的 thread 可透過 shared memory 共享資料,並用 __syncthreads() barrier 協調記憶體存取。block 間同步只在特定情況支援(thread block clusters、cooperative groups)。詳見 02-Programming-GPUs/06-SIMT-Basics-and-Thread-Hierarchy。
Runtime Initialization (2.1.6)
CUDA runtime 會為系統中每個 device 建立一個 CUDA context,稱為該 device 的 primary context。此 context 在「第一個需要該 device 上有 active context 的 runtime 函式」被呼叫時初始化,並由應用程式中所有 host thread 共享。
- 初始化是隱式且透明的:作為 context 建立的一部分,device code 會在必要時JIT (just-in-time) 編譯並載入 device memory,整個過程對使用者透明。
- 若在
cudaInitDevice/cudaSetDevice之前就發出 runtime API 請求,runtime 會隱式使用 device 0 並自我初始化以處理請求。 - primary context 可從 driver API 存取,以做 runtime 與 driver API 的互通。
cudaDeviceReset會銷毀當前 device 的 primary context;之後再呼叫 runtime API 會重新建立一個新的 primary context。
第一個需要 context 的 runtime 呼叫
│ (隱式觸發)
▼
建立 primary context ──▶ device code JIT 編譯 ──▶ 載入 device memory
│
▼ (此初始化會花費可觀的一次性開銷)
runtime 就緒
- CUDA 12.0+:
cudaInitDevice與cudaSetDevice會初始化 runtime 與指定 device 的 primary context;cudaSetDevice在切換 host thread 的當前 device 後會顯式初始化 runtime(若尚未初始化)。 - CUDA 12.0 之前:
cudaSetDevice不會初始化 runtime,會延遲到其後第一個 runtime 呼叫才在新 device 上初始化。 - 因此務必檢查
cudaSetDevice的回傳值以捕捉初始化錯誤。
隱式初始化帶有一次性開銷(含 JIT 與載入)。在為 runtime 函式計時時,第一次呼叫會把初始化成本算進去;在解讀首次 runtime 呼叫的錯誤碼時,該錯誤也可能其實來自初始化。最佳做法是先顯式初始化(如 cudaSetDevice)再開始計時/判讀。
CUDA 介面使用的 global state 在 host 程式啟動時初始化、終止時銷毀。在程式啟動期間或 main 之後的終止期間使用任何 CUDA 介面(無論隱式或顯式)會導致未定義行為。此外,error handling 與 version management 章節的 runtime 函式不會初始化 runtime。
考試/測驗重點
| 情境/關鍵字 | 答案 |
|---|---|
| kernel launch 對 CPU 是同步還非同步? | 非同步;launch 立即回傳,host 繼續往下執行 |
| 如何保證 kernel 完成後才讀結果? | 加 cudaDeviceSynchronize(或 stream/event 同步) |
cudaDeviceSynchronize 等什麼? |
GPU 上所有先前發出的工作(跨所有 stream),不只一個 kernel |
| 多 stream 應用該用什麼同步? | cudaStreamSynchronize 或 CUDA Events,避免過度同步 |
| 未同步就讀輸出緩衝區 | 競態,可能讀到舊值 / 未定義結果 |
| Explicit 版完整步驟順序 | 配置→H2D copy→launch→sync→D2H copy→釋放/驗證 |
| Unified 版省略哪兩步? | 手動 H2D / D2H copy(由 unified memory 按需搬移) |
cudaMallocHost 配的是什麼記憶體?為何用? |
page-locked (pinned) host 記憶體;提升 copy 效能、非同步傳輸所需 |
| 釋放 API 配對 | cudaMalloc↔cudaFree、cudaMallocHost↔cudaFreeHost、malloc↔free、cudaMallocManaged↔cudaFree |
| 浮點結果如何驗證? | 用 epsilon 容差比較(fabs(a-b) > epsilon),不可用 == |
cudaMemcpyDefault 作用 |
依來源/目的指標自動判斷拷貝方向 |
| runtime 何時初始化? | 第一個需要 active context 的 runtime 函式被呼叫時,隱式初始化 |
| 初始化包含什麼? | 建立 primary context + device code JIT 編譯 + 載入 device memory |
CUDA 12.0 前後 cudaSetDevice 差異 |
12.0+ 會初始化 runtime;之前不會(延遲到下個 runtime 呼叫) |
為何要檢查 cudaSetDevice 回傳值? |
它可能回報初始化錯誤 |
cudaDeviceReset 做什麼? |
銷毀當前 device 的 primary context;之後再呼叫會重建 |
main 之後用 CUDA 介面 |
未定義行為(global state 已銷毀) |
| 計時首次 runtime 呼叫的陷阱 | 會把一次性初始化開銷算進去 / 首個錯誤碼可能來自初始化 |
Related Notes
- 02-Programming-GPUs/01-CUDA-Cpp-Kernels-and-Launch
- 02-Programming-GPUs/02-CUDA-Cpp-Memory-Management
- 02-Programming-GPUs/04-CUDA-Cpp-Errors-and-Specifiers
- 02-Programming-GPUs/05-CUDA-Python
- 02-Programming-GPUs/06-SIMT-Basics-and-Thread-Hierarchy
- 02-Programming-GPUs/14-Async-Streams-and-Events
- 02-Programming-GPUs/16-Unified-and-System-Memory
- 02-Programming-GPUs/17-NVCC-Compiler
- 01-Introduction-to-CUDA/05-CUDA-Platform
- 02-Programming-GPUs/Practice-Programming-GPUs
- 00-Dashboard/Exam-Traps