Kernel Functions 與 Threading
重點總覽 (Overview)
本節說明「如何撰寫 kernel 本體」:用限定詞標記函式、理解 grid/block/thread 階層、用內建變數算出每個 thread 的全域索引,再以 if(i<n) 防止越界。啟動語法 <<<>>> 與編譯流程屬於 02-Heterogeneous-Data-Parallel-Computing/04-Calling-Kernels-Compilation-and-Summary。
| 元素 (Element) | 角色 (Role) | 關鍵點 (Key point) |
|---|---|---|
__global__ |
宣告 kernel function | 在 device 執行、由 host 呼叫,呼叫後啟動一個 grid |
__device__ |
宣告 device function | device 執行、僅能被 kernel/device function 呼叫,不啟動新 thread |
__host__ |
宣告 host function | 預設值;無任何限定詞的函式都是 host function |
blockDim |
內建變數 (struct, x/y/z) | 每個 block 的 thread 數 (本例 blockDim.x = 256) |
blockIdx |
內建變數 | block 在 grid 內的座標 (= 電話「區碼」) |
threadIdx |
內建變數 | thread 在 block 內的座標 (= 電話「本地號碼」) |
global index i |
blockIdx.x*blockDim.x + threadIdx.x |
thread → data 的唯一映射 |
if(i < n) |
邊界檢查 | 關閉多出來的 thread,支援任意長度 n |
一個 kernel 的所有 thread 執行完全相同的程式碼,靠內建變數區分自己要處理哪一份資料。這就是 SPMD (Single-Program Multiple-Data) 風格。
Kernel Function 與 SPMD 模型 (Kernel Functions and the SPMD Model)
- Kernel function 定義「在平行階段中,每個 thread 要執行的程式碼」。
- 由於所有 thread 跑同一份程式碼,CUDA 屬於 SPMD 平行風格。
- SPMD:所有處理單元跑「同一支程式」處理不同資料,但不要求同一時刻執行同一條指令。
- SIMD (Single-Instruction Multiple-Data):所有處理單元在任一瞬間都執行同一條指令。
- CUDA 程式設計是 SPMD;硬體在 warp 層級才呈現 SIMD 特性 (見 04-Compute-Architecture-And-Scheduling/02-Warps-SIMD-and-Control-Divergence)。
向量加法 kernel (書中 Fig. 2.10) —— 與序列版 (Fig. 2.4) 相比,for-loop 不見了:
// Compute vector sum C = A + B
// Each thread performs one pair-wise addition
__global__
void vecAddKernel(float* A, float* B, float* C, int n) {
int i = threadIdx.x + blockDim.x * blockIdx.x;
if (i < n) {
C[i] = A[i] + B[i];
}
}
原本序列迴圈的「每一次迭代」被「grid 中的每一個 thread」取代了。整個 grid 等價於那個 loop,這稱為 loop parallelism。kernel 內無 _h/_d 命名慣例,因為 kernel 不會碰到 host memory,沒有混淆風險。
kernel 內的區域變數 (如 i) 對每個 thread 各有一份。若啟動 10,000 個 thread,就有 10,000 份 i,彼此不可見。其儲存細節見 05-Memory-Architecture-And-Data-Locality/01-Memory-Access-Efficiency-and-CUDA-Memory-Types。
三種函式限定詞 (Function Declaration Qualifiers)
| Qualifier | 執行於 (Executed on) | 可被呼叫 (Callable from) | 啟動新 grid? |
|---|---|---|---|
__host__ (預設) |
Host | Host function | No |
__global__ |
Device | Host (支援 dynamic parallelism 時亦可由 Device) | Yes — 啟動一個 grid |
__device__ |
Device | Kernel 或另一 device function | No (由呼叫它的 thread 執行) |
__global__前後各兩個底線,回傳型別必須是void。- 無任何限定詞 → 預設為
__host__。CUDA 程式多由 CPU-only 程式移植而來,讓原函式自動成為 host function 可省去大量改寫。
__host__ __device__ 同時使用兩者並用會請編譯器產生兩份 object code:一份給 host、一份給 device。常見於可同時在 CPU/GPU 重用的函式庫函式 —— 不必把同一份程式碼手抄兩遍 (這正是書末 Exercise 10「實習生抱怨要寫兩次」的標準解答)。
__device__ ─ device 執行,device 呼叫,不開新 thread
__global__ void K(...) ─ device 執行,host 呼叫,launch 一個 grid
__host__ ─ host 執行,host 呼叫 (default)
Thread 階層:Grid → Block → Thread (Thread Hierarchy)
呼叫 kernel 時,runtime 啟動一個組織成兩層階層的 grid:
┌───────────────────────── Grid (一次 kernel 呼叫的全部 thread) ─────────────────────────┐
│ │
Block 0 Block 1 Block 2 ... Block (gridDim-1)
┌──────────────────┐ ┌──────────────────┐ ┌──────────────────┐
│ t0 t1 ... t255 │ │ t0 t1 ... t255 │ │ t0 t1 ... t255 │ 每個 block 同樣大小
└──────────────────┘ └──────────────────┘ └──────────────────┘ (本例 blockDim.x = 256)
blockIdx.x = 0 blockIdx.x = 1 blockIdx.x = 2
- Grid = 一次 kernel 呼叫啟動的所有 thread。
- Grid = 一個由 block (thread block) 組成的陣列;同一 grid 內所有 block 大小相同。
- 每個 block 在目前硬體上最多 1024 threads (CUDA 3.0+;舊版僅 512)。
把每條電話線看成一個 thread:blockIdx = 區碼,threadIdx = 本地號碼。兩者合起來給全國每條線一個唯一號碼,同時保留「同區撥號只需本地號碼」的 locality。
內建變數與全域索引 (Built-in Variables and the Global Index)
CUDA 提供一組唯讀內建變數,由 runtime 預先初始化,讓 thread 找到自己:
| 變數 (Variable) | 型別 | 意義 (Meaning) |
|---|---|---|
blockDim |
struct (x, y, z) | 每個 block 的 thread 數;維度反映資料維度 (1D 只用 .x) |
blockIdx |
struct (x, y, z) | block 在 grid 內的座標 (同 block 內所有 thread 相同) |
threadIdx |
struct (x, y, z) | thread 在 block 內的座標 (block 內第 0 個為 0,第 1 個為 1...) |
每個 thread 用以下公式算出整個 grid 內唯一的全域索引:
i = blockIdx.x * blockDim.x + threadIdx.x
以 blockDim.x = 256 為例,前三個 block 連續覆蓋 i = 0 .. 767:
blockDim.x = 256
Block 0 (blockIdx.x=0) Block 1 (blockIdx.x=1) Block 2 (blockIdx.x=2)
threadIdx.x: 0 ... 255 threadIdx.x: 0 ... 255 threadIdx.x: 0 ... 255
│ │ │
i = 0*256+tIdx i = 1*256+tIdx i = 2*256+tIdx
▼ ▼ ▼
i = 0 ... 255 i = 256 ... 511 i = 512 ... 767
───────────────────────────────────────────────────────────────────────► 連續覆蓋 0..767
- 每個 thread 用
i去存取A[i],B[i],C[i]—— 即 thread → data 的一對一映射。 - 第
b個 block 覆蓋的索引區間 =[b*blockDim.x, b*blockDim.x + blockDim.x - 1]。 - 啟動 ≥
n個 thread,即可處理長度n的向量。多維索引推廣見 03-Multidimensional-Grids-And-Data/02-Mapping-Threads-to-Multidimensional-Data。
硬體以 warp (32 threads) 為排程單位,因此 block 各維度的 thread 數最好是 32 的倍數,以避免浪費 (細節見 04-Compute-Architecture-And-Scheduling/02-Warps-SIMD-and-Control-Divergence)。
邊界檢查 if(i < n) (Boundary Check)
並非所有向量長度都能被 block size 整除,因此 grid 通常會多啟動一些 thread:
- 例:
n = 100,block size 取 32 → 需ceil(100/32) = 4個 block = 128 threads。 - 多出的
128 - 100 = 28個 thread 必須被關閉,否則會存取超出陣列的記憶體。
n = 100, block size = 32, launch 4 blocks = 128 threads
thread i: 0 ............................... 99 | 100 ............ 127
└──── i < n → 執行 C[i]=A[i]+B[i] ──┘ └─ i >= n → 跳過 ─┘
(前 100 個) (後 28 個)
- 所有 thread 都跑同一份程式碼,因此都會測試
if (i < n):i < n的 thread → 執行加法;i >= n的 thread → 什麼都不做。
- 這讓同一個 kernel 能處理任意長度的向量。
if(i < n)?
因為 grid 的 thread 總數 = ceil(n / blockDim.x) * blockDim.x ≥ n,幾乎總是大於 n。少了邊界檢查,尾端 thread 會越界讀寫 device global memory,導致錯誤結果或 runtime error。
考試/面試重點 (Exam / Test Patterns)
| 情境 / 關鍵字 (Scenario / Keyword) | 答案 / 技巧 (Answer / Technique) |
|---|---|
每個 thread 算 1 個元素,求 i |
i = blockIdx.x * blockDim.x + threadIdx.x (Ex.1 → C) |
每個 thread 算 2 個相鄰 元素,求第一個 i |
i = (blockIdx.x*blockDim.x + threadIdx.x) * 2 (Ex.2 → C) |
| 每 thread 算 2 元素、分兩個 section(各掃一遍) | i = blockIdx.x * blockDim.x * 2 + threadIdx.x (Ex.3 → D) |
為什麼需要 if(i < n)? |
grid threads = ceil(n/blk)*blk ≥ n;關閉多餘 thread 防越界,支援任意 n |
__global__ vs __device__ vs __host__ |
global:device 執行/host 呼叫/啟動 grid;device:device 執行/device 呼叫;host:預設 |
| 函式無任何限定詞 | 預設為 host function |
| 抱怨「同函式要寫兩次 (host+device)」 | 用 __host__ __device__ 同時標記,編譯器自動產生兩版 (Ex.10) |
| 每個 block 最多幾個 thread? | 1024 (current systems) |
| block 每維 thread 數的建議值 | 32 的倍數 (warp 大小) |
kernel 內 i 等 automatic variable 的可見範圍 |
thread-private,每 thread 一份,互不可見 |
| 「序列迴圈去哪了?」 | 被 grid of threads 取代 → loop parallelism |
| SPMD vs SIMD | SPMD 同程式不同資料、不必同指令;SIMD 任一瞬間同指令 |
N=200000, block=128, (N+127)/128 blocks |
blocks=1563,grid threads=1563*128=200064;算 i 的 thread=200064;進入 if body 的 thread=200000 (Ex.9) |
Related Notes
- 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/04-Calling-Kernels-Compilation-and-Summary
- 03-Multidimensional-Grids-And-Data/02-Mapping-Threads-to-Multidimensional-Data
- 04-Compute-Architecture-And-Scheduling/02-Warps-SIMD-and-Control-Divergence
- 05-Memory-Architecture-And-Data-Locality/03-Boundary-Checks-and-Memory-Occupancy