異質資料平行運算 練習題 (Practice - Data Parallelism and CUDA C Program Structure)
Related Concepts
- 02-Heterogeneous-Data-Parallel-Computing/01-Data-Parallelism-and-CUDA-Program-Structure
- 02-Heterogeneous-Data-Parallel-Computing/02-Vector-Addition-Host-Code-Device-Memory-and-Data-Transfer
- 02-Heterogeneous-Data-Parallel-Computing/03-Kernel-Functions-and-Threading
- 02-Heterogeneous-Data-Parallel-Computing/04-Calling-Kernels-Compilation-and-Summary
| 關鍵字 / 情境 | 答案 / 公式 |
|---|---|
| 平行擴展性 (scalability) 主要來源 | Data parallelism(資料越多、可平行度越大);task parallelism 為輔助 |
| 灰階亮度 (luminance) 公式 | L = 0.21*r + 0.72*g + 0.07*b;每個 O[i] 只依賴 I[i],完全獨立 |
| SPMD vs SIMD | SPMD 同程式不同資料、不要求同瞬間同指令;SIMD 任一瞬間同指令 |
| global thread index(每 thread 算 1 元素) | i = blockIdx.x * blockDim.x + threadIdx.x |
| 每 thread 算 2 個相鄰元素 | i = (blockIdx.x*blockDim.x + threadIdx.x) * 2 |
| 每 thread 算 2 元素、分兩個 section | i = blockIdx.x * blockDim.x * 2 + threadIdx.x |
為何要 if (i < n) |
grid threads = ceil(n/blk)*blk ≥ n;關閉多餘 thread 防越界、支援任意 n |
| block 數 / ceiling division | ceil(n/256.0);務必用浮點 256.0,整數 n/256 會捨去漏掉尾端元素 |
| grid 總 thread 數 | ceil(n/blk) * blk(≥ n) |
cudaMalloc 兩個參數 |
(void**)&A_d(指標的位址)、size(bytes = n*sizeof(float)) |
cudaFree 傳什麼 |
指標值 cudaFree(A_d),不傳位址 |
cudaMemcpy 參數順序 |
(dst, src, size, kind)——目的地在前 |
| 搬入 / 搬回的 kind | cudaMemcpyHostToDevice / cudaMemcpyDeviceToHost |
| 接收 API 回傳值的型別 | cudaError_t err;(比對 cudaSuccess) |
| 三種 function qualifier | __global__(device 執行/host 呼叫/啟動 grid)、__device__、__host__(預設) |
| 同函式要在 host+device 跑 | 加 __host__ __device__,編譯器自動產生兩版,不必寫兩次 |
| device code 中間檔 | PTX(virtual binary),再由 NVCC runtime 編成真正 object code |
| 每個 block 最多 thread 數 | 1024(current systems);每維建議為 32 的倍數(warp 大小) |
| 為何 vecAdd GPU 可能比 CPU 慢 | arithmetic intensity 太低(1 FLOP / 12 bytes);搬資料 overhead > 計算量 |
Question 1 - Data Parallelism 的定義與灰階範例 [recall]
情境/題目:請定義 data parallelism,並寫出彩色轉灰階 (color-to-grayscale) 的亮度公式;為何說此計算「天生可平行」?
Data parallelism:對資料集不同部分要做的計算彼此獨立 (independent),因此可同時平行進行。
亮度公式:L = 0.21*r + 0.72*g + 0.07*b。每個輸出 O[i] 只依賴對應的輸入 pixel I[i],不依賴任何其他 pixel,故所有 pixel 可一對一指派給 thread 同時計算。
Question 2 - Data Parallelism vs Task Parallelism [recall]
情境/題目:data parallelism 與 task parallelism 的拆解依據各是什麼?哪一個是平行程式「可擴展性 (scalability)」的主要來源,為什麼?
Data parallelism 依資料 (data decomposition) 拆解;task parallelism 依任務 (task decomposition) 拆解(例:vector add 與 matrix-vector mult 為兩個獨立 task,或 I/O、資料傳輸)。
Data parallelism 是主要來源:資料集越大可平行度越大,足以餵飽大規模平行硬體,效能能隨每一代擁有更多執行資源的硬體一起成長。task 數量有限,無法提供這種隨硬體放大的擴展。
Question 3 - SPMD 是不是 SIMD? [recall]
情境/題目:CUDA kernel 的程式設計風格是哪一種?它與 SIMD 有何不同?
CUDA 屬 SPMD (Single-Program Multiple-Data):所有 thread 跑同一份 kernel 程式碼、處理不同資料。
差別:SIMD 要求所有處理單元在任一瞬間都執行同一條指令;SPMD 不要求各單元同時跑到同一條指令。(GPU 硬體在 warp 層級才呈現 SIMD 特性。)
Question 4 - 三種 Function Declaration Qualifiers [recall]
情境/題目:
__global__、__device__、__host__三個限定詞分別表示函式在何處執行、可被誰呼叫、是否會啟動新 grid?沒有任何限定詞的函式預設是什麼?
__global__:device 執行、host 呼叫(dynamic parallelism 時也可 device 呼叫)、呼叫即啟動一個新 grid,回傳須為void。__device__:device 執行、只能被 kernel 或另一 device function 呼叫、不啟動新 thread。__host__:host 執行、只能被 host function 呼叫。
無任何限定詞 → 預設為__host__(host function)。
Question 5 - cudaMalloc / cudaFree 參數 [recall]
情境/題目:要讓
float *A_d指向 device 上n個 float 的空間,cudaMalloc的兩個參數各應填什麼?為何第一參數要傳(void**)&A_d而非A_d?cudaFree又傳什麼?
cudaMalloc((void**)&A_d, n*sizeof(float));
- 第一參數 = 指標變數的位址
(void**)&A_d:因為cudaMalloc要把配置好的位址寫回你的指標變數;它是泛型函式故轉型為 generic pointer。(return value 留給回報錯誤,所以才是兩參數格式。) - 第二參數 = bytes 數
n*sizeof(float) = n*4(不是元素數)。 cudaFree(A_d)傳指標值即可,不需位址。
Question 6 - cudaMemcpy 參數順序與方向 [recall]
情境/題目:要把 host 陣列
A_h搬到 device 的A_d,以及把結果C_d搬回C_h,分別怎麼寫cudaMemcpy?參數順序是什麼?
參數順序為 cudaMemcpy(dst, src, size, kind)——目的地在前(與英文「copy A to B」直覺相反)。
- 搬入:
cudaMemcpy(A_d, A_h, size, cudaMemcpyHostToDevice); - 搬回:
cudaMemcpy(C_h, C_d, size, cudaMemcpyDeviceToHost);
同一函式靠調換 dst/src 與選對 kind 即可雙向傳輸;寫反方向是經典 bug。
Question 7 - Global Index 與邊界檢查 [recall]
情境/題目:在 1D vector addition kernel 中,每個 thread 的全域索引
i如何計算?為何 kernel 內幾乎一定要加if (i < n)?
i = blockIdx.x * blockDim.x + threadIdx.x(每個 thread 一對一對應一個資料元素 → loop parallelism)。
需要 if (i < n) 是因為 grid 總 thread 數 = ceil(n/blockDim.x) * blockDim.x ≥ n,幾乎總是大於 n。若不檢查,尾端多出的 thread 會越界讀寫 device global memory,造成錯誤結果或 runtime error。此檢查讓同一 kernel 能處理任意長度 n。
Question 8 - 編譯流程 NVCC 與 PTX [recall]
情境/題目:CUDA C 程式用 NVCC 編譯時,host code 與 device code 分別走哪條路徑?device code 會先被編成什麼中間檔?
NVCC 依 CUDA 關鍵字分離 host / device code:
- Host code(純 ANSI C)→ 交給 host 標準 C/C++ compiler → 以傳統 CPU process 執行。
- Device code(
__global__/__device__等)→ NVCC 編成 PTX(virtual binary 虛擬二進位),再由 NVCC 的 runtime component 進一步編成特定 GPU 的真正 object code 後在 CUDA-capable GPU 上執行。
Question 9 - 計算 grid 的 thread 數 (Ex.4) [application]
情境/題目:vector length = 8000,每個 thread 算一個輸出元素,block size = 1024,採用「最少 block 數覆蓋所有元素」的配置。grid 內共有多少 thread?
block 數 = ceil(8000 / 1024) = ceil(7.8125) = 8 個 block。
grid threads = 8 × 1024 = 8192(其中 8000 個做事,尾端 192 個被 if (i < n) 擋掉)。答案為 8192。
Question 10 - 分析 kernel launch 的 thread 計數 (Ex.9) [application]
情境/題目:
foo_kernel<<<(N+128-1)/128, 128>>>(...),N = 200000,kernel 第 02 行算i、第 04 行在if (i<N)內做運算。求:(a) 每 block thread 數;(b) grid 總 thread 數;(c) block 數;(d) 執行第 02 行的 thread 數;(e) 執行第 04 行的 thread 數。
(a) 每 block = 128。
(b)(c) block 數 = (200000+127)/128 = 1563(整數除法達成 ceiling division);grid threads = 1563 × 128 = 200064,故 (c) = 1563、(b) = 200064。
(d) 第 02 行(算 i)在 if 之前,所有 thread 都執行 → 200064。
(e) 第 04 行在 if (i<N) 內,只有通過邊界檢查的 thread 執行 → 200000。
Question 11 - 每個 thread 算多個元素的索引映射 (Ex.2 vs Ex.3) [application]
情境/題目:(a) 若每個 thread 處理「2 個相鄰」元素,其第一個元素的 index
i為何?(b) 若每個 block 把2*blockDim.x個元素分成兩個 section(所有 thread 先掃完第一段、再一起掃第二段),其第一個元素的 indexi又為何?
(a) 相鄰兩元素:每個 thread 拿一段連續的 2 格 → i = (blockIdx.x*blockDim.x + threadIdx.x) * 2(Ex.2 答案 C)。
(b) 兩個 section:每個 block 覆蓋 2*blockDim.x 連續元素,thread 在每段內只取一格 → i = blockIdx.x*blockDim.x*2 + threadIdx.x,第二格為 i + blockDim.x(Ex.3 答案 D)。
關鍵差異:(a) 同一 thread 取的兩格相鄰;(b) 同一 thread 取的兩格相隔 blockDim.x(利於後面章節討論的 memory coalescing)。
Question 12 - 為何 GPU vecAdd 可能比 CPU 還慢 [analysis]
情境/題目:vector addition 是「平行版的 Hello World」,但把它搬到 GPU 後實測往往比序列 CPU 版更慢。請用 arithmetic intensity 解釋原因,並說明真實應用如何讓 GPU 划得來。
對每組運算,需搬 2 個 float 輸入 + 1 個 float 輸出(共 12 bytes)卻只做 1 次加法:
Arithmetic Intensity ≈ 1 FLOP / 12 bytes ≈ 0.083 FLOP/byte,屬重度 memory-bound。再加上 cudaMalloc 配置、H→D 搬入、D→H 搬回、cudaFree 等 overhead,整體可能比 CPU 版慢。
划算的做法:選計算量相對資料量大很多的 kernel(高 arithmetic intensity),並讓大型資料常駐 device memory 跨多次 kernel 呼叫重複使用,攤平搬運 overhead。
Question 13 - Transparent Scalability 與 block 執行順序 [analysis]
情境/題目:CUDA 規定「programmer 不可假設 block 的執行順序」。這個限制如何帶來 transparent scalability?對比同一份 binary 在「小 GPU」與「大 GPU」上的行為,並說明
<<<gridDim, blockDim>>>中的 block 數是由什麼決定。
因為各 block 操作不同資料區段、彼此無順序相依,runtime 才能依硬體可用資源自由排程。
- 小 GPU:一次只能跑 1~2 個 block,其餘序列化 → 同一份程式跑得較慢。
- 大 GPU:一次可跑 64~128 個 block,平行度高 → 較快。
兩者不需重新編譯或改寫,這就是 transparent scalability。<<<>>>的 block 數由資料量 n 決定(ceil(n/256.0)),而非由硬體決定;硬體只決定「同時跑幾個 block」。若程式對 block 順序做假設,就會破壞這種跨硬體可攜性。
| 主題 | 必記重點 |
|---|---|
| Data parallelism | 計算對不同資料部分獨立 → 可平行;是 scalability 主要來源(task parallelism 為輔) |
| 灰階公式 | L = 0.21r + 0.72g + 0.07b,O[i] 只依賴 I[i] |
| SPMD vs SIMD | SPMD 同程式不同資料、不必同瞬間同指令;SIMD 任一瞬間同指令 |
| Function qualifiers | __global__(device 跑/host 呼/啟 grid)、__device__、__host__(預設);__host__ __device__ 產生兩版 |
| global index | i = blockIdx.x*blockDim.x + threadIdx.x;2 相鄰元素 *2;2 section *blockDim.x*2 +tIdx |
| 邊界檢查 | grid threads = ceil(n/blk)*blk ≥ n → 必加 if (i < n) 防越界、支援任意 n |
| ceiling division | ceil(n/256.0)(浮點除數);整數 n/256 會捨去、漏元素 |
cudaMalloc / cudaFree |
((void**)&A_d, bytes);cudaFree(A_d) 傳值;size 以 bytes 計 |
cudaMemcpy |
(dst, src, size, kind) 目的地在前;H→D 搬入、D→H 搬回 |
| Error handling | API 回傳 cudaError_t,比對 cudaSuccess |
| Compilation | NVCC 分離 host/device;device → PTX → real object code |
| 效能直覺 | vecAdd memory-bound(1 FLOP/12 bytes),overhead 大 → 資料常駐 device、選高 intensity kernel |
| Transparent scalability | block 無順序相依 → 小 GPU 慢、大 GPU 快,免改 code;block 數由 n 決定 |