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 的結果
   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 MemoryExplicit 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];
}

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 釋放

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
   端到端流程 (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
本範例 thread 彼此獨立

此 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 的 runtime 呼叫
            │  (隱式觸發)
            ▼
   建立 primary context ──▶ device code JIT 編譯 ──▶ 載入 device memory
            │
            ▼   (此初始化會花費可觀的一次性開銷)
        runtime 就緒
CUDA 12.0 起的行為變化

  • CUDA 12.0+cudaInitDevicecudaSetDevice初始化 runtime 與指定 device 的 primary contextcudaSetDevice 在切換 host thread 的當前 device 後會顯式初始化 runtime(若尚未初始化)。
  • CUDA 12.0 之前cudaSetDevice 不會初始化 runtime,會延遲到其後第一個 runtime 呼叫才在新 device 上初始化。
  • 因此務必檢查 cudaSetDevice 的回傳值以捕捉初始化錯誤。

計時與首次錯誤碼的陷阱

隱式初始化帶有一次性開銷(含 JIT 與載入)。在為 runtime 函式計時時,第一次呼叫會把初始化成本算進去;在解讀首次 runtime 呼叫的錯誤碼時,該錯誤也可能其實來自初始化。最佳做法是先顯式初始化(如 cudaSetDevice)再開始計時/判讀。

例外:global state 與 main 前後

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 配對 cudaMalloccudaFreecudaMallocHostcudaFreeHostmallocfreecudaMallocManagedcudaFree
浮點結果如何驗證? 用 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 呼叫的陷阱 會把一次性初始化開銷算進去 / 首個錯誤碼可能來自初始化