SIMT 基礎與 Thread 階層 (SIMT Basics and Thread Hierarchy)

重點總覽

項目 重點
SIMT 基礎 thread 是平行的基本單位;每個 thread 有自己的 state 與 control flow,可走不同 code path;但同一 warp 內若 thread 走 divergent path 會損失效能
Thread Hierarchy thread → block → grid 三層;grid 與 block 皆可 1/2/3 維;用 built-in 變數 gridDim/blockDim/blockIdx/threadIdx 在 kernel 內查詢
Built-in 變數 gridDim/blockDim 對所有 thread 相同(launch 時設定);blockIdx/threadIdx 因 thread 而異(標示是哪個 thread/block)
全域索引 用 built-in 變數組出每個 thread 唯一的 global index,決定它讀寫哪筆 global memory、走哪條 path
Linearization 多維只是「方便」,不影響效能;x 變化最快,y stride = blockDim.xz stride = blockDim.x * blockDim.y;此佈局決定 thread 如何分配到 warp
Thread Block 同步 __syncthreads() (C++) / cuda.syncthreads() (Python) 是 block 層級 barrier;當 thread 合作或存取同一記憶體(尤其 shared memory)時用來避免 race condition
__syncthreads 語意 block 內所有 thread 都到達 barrier 前,沒有 thread 能往下走;barrier 也保證 barrier 前的寫入順序對 barrier 後可見
同步範圍 只同步「單一 block 內」的 thread;跨 block 需 thread block clusters、Cooperative Groups 或 atomics

SIMT 基礎 (Basics of SIMT)

CUDA kernel 大致可以用寫傳統 CPU 程式的方式來撰寫,但理解 GPU 上 thread 如何被排程、如何存取記憶體、如何推進執行,能幫你寫出最大化資源利用率的 kernel。

Tip

「功能正確」與「效能良好」是兩件事:SIMT 允許每個 thread 走不同分支(功能正確),但讓同一 warp 的 thread 盡量走相同分支才有好效能。divergence 越少,warp 的執行越接近滿載。

Important

warp 是硬體排程的單位(一群 thread 一起以 SIMT 方式執行同一指令)。divergence 的成本來自「warp 必須輪流執行不同分支」,而不是「block 之間不同」。詳見 01-Introduction-to-CUDA/02-Execution-Model-and-SIMT

Thread 階層 (Thread Hierarchy)

Thread 被組織成 thread block,block 再組織成 grid。grid 與 block 都可以是 1、2 或 3 維。kernel 內用以下 built-in 變數查詢自己的位置與維度。

Built-in (C++) Python 含義 對所有 thread
gridDim.{x,y,z} cuda.gridDim.{x,y,z} grid 在各維的大小(block 數量) 相同(launch 設定)
blockDim.{x,y,z} cuda.blockDim.{x,y,z} block 在各維的大小(thread 數量) 相同(launch 設定)
blockIdx.{x,y,z} cuda.blockIdx.{x,y,z} 本 thread 所屬 block 的索引 各異(標示哪個 block)
threadIdx.{x,y,z} cuda.threadIdx.{x,y,z} 本 thread 在 block 內的索引 各異(標示哪個 thread)
Note

gridDim/blockDim 是「尺寸」,對全部 thread 一樣,屬於 launch 時的 execution configuration;blockIdx/threadIdx 是「索引」,每個 thread 不同。記法:Dim = 大小、Idx = 位置

這些變數的主要用途,是組出每個 thread 唯一的 global thread index,讓每個 thread 去讀寫 global memory 的特定資料、執行需要的 code path:

// 1D grid + 1D block 的標準慣用法
int idx = blockIdx.x * blockDim.x + threadIdx.x;
if (idx < vectorLength) {
    C[idx] = A[idx] + B[idx];
}
# Python 提供 cuda.grid(1) 直接回傳一維全域索引
work_index = cuda.grid(1)
C[work_index] = A[work_index] + B[work_index]

C++ 需手動 blockIdx.x * blockDim.x + threadIdx.x;Python 的 cuda.grid(1) 是同一公式的便捷封裝。加上 if (idx < length) 的邊界檢查,是因為 grid 通常會比資料量略大。

多維與 linearization

多維 block/grid 只是為了撰寫方便,不影響效能。block 內的 thread 以可預測的方式被線性化:

linear = threadIdx.x
       + threadIdx.y * blockDim.x
       + threadIdx.z * blockDim.x * blockDim.y
2D grid (gridDim = 3 x 2) 含 1D thread block (blockDim.x = 4)
─────────────────────────────────────────────────────────────
 blockIdx (y)
   1 │ [B(0,1)] [B(1,1)] [B(2,1)]
   0 │ [B(0,0)] [B(1,0)] [B(2,0)]
     └──────────────────────────── blockIdx (x)
          0        1        2

每個 block 內 (blockDim.x = 4),threadIdx.x 連續、x 最快:
   B(1,0):  t0   t1   t2   t3
            ↑ threadIdx.x = 0,1,2,3  (連續 thread → 連續 warp lane)
Important

linearization 順序決定 thread 如何被分配到 warp:連續的 threadIdx.x 是連續的 thread。這直接影響 memory coalescing 與 warp 效率,所以即使「多維不影響效能」,你選的 index→資料對應方式仍然影響效能。參見 02-Programming-GPUs/08-SIMT-Memory-Performance

Thread Block 同步 (__syncthreads())

在前面的 vecAdd 等範例中,每個 thread 做獨立工作,不需要互相同步。一旦 thread 之間要合作、或存取同一塊記憶體位址(尤其是 shared memory),就必須同步,以避免 race condition 與 memory hazard。

block 層級最基本的同步機制是 __syncthreads()(Python:cuda.syncthreads()),它是一個 barrier

// 假設 blockDim.x == 128
__global__ void example_syncthreads(int* input_data, int* output_data) {
    __shared__ int shared_data[128];
    shared_data[threadIdx.x] = input_data[blockDim.x * blockIdx.x + threadIdx.x];

    // barrier:保證所有對 shared_data 的寫入都完成且有序,
    // 之後才有任何 thread 被解除阻塞:
    __syncthreads();

    // 只讓單一 thread 安全地讀取整塊 shared_data:
    if (threadIdx.x == 0) {
        float sum = 0;
        for (int i = 0; i < blockDim.x; ++i)
            sum += shared_data[i];
        output_data[blockIdx.x] = sum;
    }
}

典型樣式:「各 thread 寫 shared memory → __syncthreads() → 再讀其他 thread 寫的資料」。少了中間的 barrier,thread 0 可能在其他 warp 還沒寫完 shared_data 時就開始加總,導致讀到未完成的值。

時間軸(block 內):
  warp0  ──寫 shared_data──┐
  warp1  ──寫 shared_data──┤  (barrier) ===> 全部到齊才放行 ==> 讀取/加總
  warp2  ──寫 shared_data──┤        ▲
  warp3  ──寫 shared_data──┘   沒有 barrier 時,快的 warp 會提早讀到舊值
Warning

__syncthreads() 必須被 block 內「每一個」thread 都抵達。 若把它放在只有部分 thread 會進入的 divergent 分支(如 if (threadIdx.x < N) { ... __syncthreads(); }),未進入的 thread 永遠不會到達 barrier,造成 deadlock 或未定義行為。需要條件分支時,請讓 barrier 處於所有 thread 都會執行到的位置。

Warning

__syncthreads() 只同步單一 thread block 內的 thread,不會跨 block 同步。跨 block 同步只在特定情況支援:thread block clusters(cluster 內 block 互相同步)與 Cooperative Groups API。多數情況下,把同步保持在 block 內效能最佳;不同 block 之間若要協作共同結果,改用 atomic 記憶體運算(見 02-Programming-GPUs/09-SIMT-Atomics-Cooperative-Occupancy)。

考試/測驗重點

情境 / 關鍵字 答案
平行的基本單位是什麼? CUDA thread
每個 thread 能走不同 code path 嗎? 能(SIMT 保留各自 state/control flow),但同 warp 內 divergence 會傷效能
gridDim vs blockDim 分別是 grid(block 數)與 block(thread 數)的大小,對所有 thread 相同
blockIdx vs threadIdx block 索引 與 thread 在 block 內的索引,因 thread 而異
哪些對所有 thread 都一樣? gridDimblockDim(launch 時設定);blockIdx/threadIdx 不一樣
1D global index 公式 blockIdx.x * blockDim.x + threadIdx.x(Python 可用 cuda.grid(1)
為何要 if (idx < length) grid 通常略大於資料量,需邊界檢查避免越界
多維 block/grid 影響效能嗎? 不影響,純為撰寫方便;但 index→資料對應方式會影響效能
linearization 順序 x 最快;y stride = blockDim.xz stride = blockDim.x * blockDim.y
linearization 影響什麼? thread 如何分配到 warp(連續 threadIdx.x = 連續 thread)
何時需要 __syncthreads() thread 互相合作 / 存取同一記憶體(尤其 shared memory)時,避免 race condition
__syncthreads() 是什麼? block 層級 barrier:全 block 到齊前無人放行;並保證 barrier 前寫入有序可見
Python 的等價呼叫 cuda.syncthreads()
放在 divergent 分支裡會怎樣? 部分 thread 到不了 barrier → deadlock / 未定義行為(常見陷阱題)
能跨 block 同步嗎? 不能;只同步單一 block。跨 block 用 thread block clusters / Cooperative Groups / atomics
「寫 shared → barrier → 讀」少了 barrier? 可能讀到其他 warp 尚未寫完的舊值(memory hazard)