同步原語與原子函式 (Synchronization Primitives and Atomic Functions)
重點總覽
CUDA 採用弱記憶體序模型 (weakly ordered memory model):一個執行緒寫入的順序,不保證是另一個執行緒觀察到的順序。要安全地在執行緒間共享資料,必須靠三類工具:block/warp 同步原語(讓執行緒在某點集合並建立 happens-before)、memory fence(只排序、不保證可見性與原子性)、以及atomic 原子函式(不可分割的 read-modify-write)。三者解決的問題不同,常需組合使用。
| 項目 | 重點 |
|---|---|
__syncthreads*() |
同 block 內所有未退出執行緒在同一呼叫點集合;提供記憶體排序;條件呼叫須 block 內一致 (uniform) |
__syncthreads_count/and/or |
在同步之餘對 predicate 做 count / 全為真 / 至少一真 的規約 |
__syncwarp(mask) |
同 warp 內 mask 指名執行緒的同步與記憶體排序 |
| Memory fence | 三層 scope:block / device / system;只排序不保證可見性(可見性靠 volatile/atomic) |
| Atomic 五種提供方式 | Extended、Standard、Built-in (__nv_atomic_*)、Tile、Legacy (atomic<Op>) |
| Legacy scope 後綴 | 無後綴=device、_block=block、_system=system |
| 型別與 CC 門檻 | __nv_bfloat16/162 需 CC 8.x+;float2/float4、128-bit 模板需 CC 9.x+ |
atomicCAS |
萬用基底:任何原子運算都可用 CAS 迴圈實作 |
libcu++ 推薦:fence 用 cuda::atomic_thread_fence,理由是安全與可攜性;原子操作用 Extended CUDA C++ atomic functions(cuda::atomic / cuda::atomic_ref),兼顧效率、安全與可攜性。
Thread Block 同步:__syncthreads 家族
void __syncthreads();
int __syncthreads_count(int predicate);
int __syncthreads_and(int predicate);
int __syncthreads_or(int predicate);
語意:
- 集合 (barrier):等到 block 內所有未退出 (non-exited) 執行緒都到達同一個
__syncthreads*()呼叫點,或已退出。 - 記憶體排序:呼叫
__syncthreads*()strongly happens-before(C++ 規範 [intro.races])任何參與執行緒被解除等待或退出 — 因此同步前的共享/全域記憶體寫入,對解除等待後的執行緒都已排序完成。
帶 predicate 的變體(語意同 __syncthreads(),外加對所有未退出執行緒評估 predicate):
| 函式 | 回傳值 |
|---|---|
__syncthreads_count(p) |
predicate 為非零的執行緒數量 |
__syncthreads_and(p) |
當且僅當所有執行緒 predicate 非零時回傳非零 |
__syncthreads_or(p) |
當且僅當一個或以上執行緒 predicate 非零時回傳非零 |
__syncthreads*() 可放在條件式中,但只有當條件在整個 block 一致 (uniform) 時才合法;否則可能 kernel hang 或產生非預期的副作用 (unintended side effects)。
// 假設 blockDim.x == 128
// 正確:條件對 block 內所有執行緒一致
if (blockIdx.x > 0) { // CORRECT, uniform condition
__syncthreads();
output_data[threadIdx.x] = shared_data[128 - threadIdx.x];
}
// 錯誤:條件依 threadIdx.x 而異 → 非 uniform
if (threadIdx.x > 0) { // WRONG, non-uniform condition
__syncthreads(); // Undefined Behavior / hang
}
// 錯誤:迴圈中 i == threadIdx.x 只對單一執行緒成立 → 非 uniform
for (int i = 0; i < blockDim.x; ++i) {
if (i == threadIdx.x) { // WRONG, non-uniform condition
__syncthreads(); // Undefined Behavior
}
}
典型用法:先各執行緒寫 __shared__ 陣列 → __syncthreads() → 由 thread 0 安全讀取整個陣列求和(保證所有寫入已排序在解除等待之前)。
Warp 同步:__syncwarp(mask)
void __syncwarp(unsigned mask = 0xFFFFFFFF);
- 協調同一 warp 內的執行緒通訊;用於避免同 warp 對共享/全域記憶體同址的 RAW / WAR / WAW 資料危害。
- 提供 mask 指名執行緒間的記憶體排序:
__syncwarp(mask)strongly happens-before 任何 mask 指名的 warp 執行緒被解除等待或退出。 - 受 Warp
__syncIntrinsic Constraints 約束(與其他 warp__syncintrinsics 相同的參與規則)。
if (threadIdx.x < warpSize) {
__shared__ int shared_data[warpSize];
shared_data[threadIdx.x] = input_data[threadIdx.x];
__syncwarp(); // 等同 __syncwarp(0xFFFFFFFF)
if (threadIdx.x == 0)
output_data[0] = shared_data[1];
}
__syncthreads 是 block 範圍 的 barrier,__syncwarp 只同步 warp 內 mask 指名的執行緒;後者粒度更細,預設 mask 0xFFFFFFFF 代表 warp 全員參與。
Memory Fence Functions(記憶體柵欄)
弱記憶體序下,若不加 fence 或同步就對同一位置讀寫,結果為未定義行為。Fence 強制記憶體存取呈現循序一致 (sequentially consistent) 的排序;差別只在於強制排序的 thread scope,而與存取的記憶體空間(shared / global / page-locked host / peer device)無關。
三層 scope 對照(intrinsic 與 libcu++ 等價寫法):
| Scope | Intrinsic | libcu++ 等價 | 排序對哪些觀察者生效 |
|---|---|---|---|
| Block | __threadfence_block() |
cuda::atomic_thread_fence(seq_cst, thread_scope_block) |
呼叫執行緒所在 block 內所有執行緒 |
| Device | __threadfence() |
..., thread_scope_device |
本 device 內任一執行緒 |
| System | __threadfence_system() |
..., thread_scope_system |
device 內所有執行緒 + host 執行緒 + 所有 peer device 的執行緒 |
語意(以 block-level 為例):fence 前的所有寫入,被觀察者視為發生在 fence 後的所有寫入之前;fence 前的所有讀取也排序在 fence 後的讀取之前。
如何選 scope:
thread 1 寫 X,Y ──fence── thread 2 讀 Y,X
同一 block → block-level fence 即可
同 device 不同 block → device-level fence
不同 device → system-level fence
無 fence 時,writeXY/readXY 對同址 X、Y 同時讀寫構成 data race(UB),A、B 可為任意組合(含 A=1,B=20)。加了 device-level fence 後,A=1,B=2 / A=10,B=20 / A=10,B=2 都可能發生,但 A=1,B=20 不可能——fence 保證對 X 的寫入可見於對 Y 的寫入之前。
Memory fence 只影響記憶體操作執行的順序,不保證這些操作對其他執行緒可見。可見性需另用 volatile(繞過 L1 cache)或 atomic 達成。
跨 block 規約範例(單次 kernel 求 N 數總和):每個 block 先算部分和並存入全域記憶體,再由最後完成的 block 把所有部分和加總。判斷誰最後:每個 block 以 atomicInc(&count, gridDim.x) 遞增計數器,最後一個 block 拿到的舊值等於 gridDim.x - 1。
若在「寫部分和」與「遞增 counter」之間沒有 fence,counter 可能搶先遞增,導致最後一個 block 在部分和尚未寫入記憶體前就開始讀取。順序:寫 result[blockIdx.x] → cuda::atomic_thread_fence(seq_cst, thread_scope_device) → atomicInc(&count, gridDim.x)。result 宣告為 volatile 以確保跨 block 可見。
Atomic Functions:五種提供方式
原子函式對共享資料做 read-modify-write,使其「要嘛全做、要嘛不做」,給所有參與執行緒一致的資料視圖。CUDA 提供五種:
| 方式 | host/device | 記憶體序語意 | 可指定 thread scope | 備註 |
|---|---|---|---|---|
Extended cuda::atomic / cuda::atomic_ref |
皆可 | C++ 標準 atomic | 可 | 推薦,效率/安全/可攜 |
Standard cuda::std::atomic / cuda::std::atomic_ref |
皆可 | C++ 標準 atomic | 否 | |
Built-in __nv_atomic_<op>() |
device only | C++ 標準 memory order | 可 | 自 CUDA 12.8;支援子集型別(不含 128-bit);不可用於 tile code |
Tile cuda::tiles::atomic_* |
tile code only | C++ 標準 memory order | 可(cuda::tiles::thread_scope) |
僅 tile code |
Legacy atomic<Op>() |
device only | 僅 memory_order_relaxed |
由函式名後綴指定 | 只保證原子性、不引入 fence;型別為 built-in 子集(atomicAdd 多支援額外型別) |
Legacy 原子函式:運算、型別與 scope
對 global 或 shared memory 中的 32 / 64 / 128-bit word 做原子 RMW。
- 僅能用於 device 函式。
- 向量型別(
__half2、__nv_bfloat162、float2、float4):RMW 對每個元素分別保證原子,整個向量在單次存取中不保證原子。 - 記憶體序為
memory_order_relaxed,原子性只在特定 thread scope 成立:
| 後綴 | 範例 | scope |
|---|---|---|
| 無 | atomicAdd |
thread_scope_device |
_block |
atomicAdd_block |
thread_scope_block |
_system |
atomicAdd_system |
thread_scope_system(需符合特定條件,如 concurrentManagedAccess) |
各運算的計算式與型別支援:
| 函式 | 計算(回傳 old) | 支援型別 |
|---|---|---|
atomicAdd |
old + val |
int, unsigned, unsigned long long, float, double, __half2, __half;__nv_bfloat16/162(CC 8.x+);float2, float4(CC 9.x+,僅 global 位址) |
atomicSub |
old - val |
int, unsigned |
atomicInc |
old >= val ? 0 : (old + 1) |
unsigned |
atomicDec |
(old == 0 || old > val) ? val : (old - 1) |
unsigned |
atomicAnd |
old & val |
int, unsigned, unsigned long long |
atomicOr |
old | val |
int, unsigned, unsigned long long |
atomicXor |
old ^ val |
int, unsigned, unsigned long long |
atomicMin |
min(old, val) |
int, unsigned, unsigned long long, long long |
atomicMax |
max(old, val) |
int, unsigned, unsigned long long, long long |
atomicExch |
寫入 val |
int, unsigned, unsigned long long, float;128-bit 模板需 CC 9.x+ |
atomicCAS |
old == compare ? val : old |
int, unsigned, unsigned long long, unsigned short;128-bit 模板需 CC 9.x+ |
atomicExch / atomicCAS)要求
CC 9.x+;alignof(T) >= 16(16-byte 對齊);T 須 trivially copyable(std::is_trivially_copyable_v<T>);C++03 及更舊版另須 trivially constructible。
atomicCAS 慣用法(萬用基底)
任何原子運算都可用 atomicCAS() (Compare-And-Swap) 迴圈實作,例如自製單精度 atomicAdd:
__device__ float customAtomicAdd(float* d_ptr, float value) {
volatile unsigned* p = reinterpret_cast<unsigned*>(d_ptr);
unsigned old_value = *p, assumed;
do {
assumed = old_value;
float expected = cuda::std::bit_cast<float>(assumed) + value;
unsigned expected_u = cuda::std::bit_cast<unsigned>(expected);
// 用「整數比較」做 CAS,避免 NaN != NaN 造成永久 hang
old_value = atomicCAS(p, assumed, expected_u);
} while (assumed != old_value);
return cuda::std::bit_cast<float>(old_value);
}
浮點 CAS 迴圈必須以整數位元樣式比較(bit_cast 後比 unsigned),因為 NaN != NaN,直接以浮點比較會導致迴圈永遠無法收斂而 hang。
Built-in 原子函式 __nv_atomic_*
CUDA 12.8+ 提供,遵循 GNU atomic built-in 函式簽章並多一個 thread scope 參數;支援時 nvcc 定義巨集 __CUDACC_DEVICE_ATOMIC_BUILTINS__。
memory order 與 thread scope 列舉(須為整數字面量):
order : __NV_ATOMIC_RELAXED, CONSUME, ACQUIRE, RELEASE, ACQ_REL, SEQ_CST
scope : __NV_THREAD_SCOPE_THREAD, BLOCK, CLUSTER, DEVICE, SYSTEM
(預設參數通常為 __NV_THREAD_SCOPE_SYSTEM)
__NV_ATOMIC_CONSUME目前以較強的__NV_ATOMIC_ACQUIRE實作。__NV_THREAD_SCOPE_THREAD目前以較寬的__NV_THREAD_SCOPE_BLOCK實作。__NV_THREAD_SCOPE_CLUSTER需 sm_90 及更高架構。
限制:
- 僅能用於 device 函式;不可作用於 local memory。
- 不可取函式位址(不能當模板預設引數、不能放進建構子初始化列表等)。
order與scope引數必須是整數字面量,不能是變數。
運算族:fetch_* 回傳 old;無 fetch 的版本無回傳值。
| 函式族 | 運算 | 支援型別 |
|---|---|---|
__nv_atomic_fetch_add / add |
old + val |
int, unsigned, unsigned long long, float, double |
__nv_atomic_fetch_sub / sub |
old - val |
同上 |
fetch_and/or/xor 及對應無 fetch 版 |
位元運算 | 任何 4 或 8 byte 整數型別 |
fetch_min / min |
min(old, val) |
unsigned, int, unsigned long long, long long |
fetch_max / max |
max(old, val) |
同上 |
__nv_atomic_exchange_n / exchange |
交換 | 任何 4 / 8 / 16 byte(16-byte 需 CC 9.x+) |
compare_exchange_n / compare_exchange |
比較交換 | 任何 2 / 4 / 8 / 16 byte(16-byte 需 CC 9.x+) |
__nv_atomic_load_n / load |
原子讀 | 任何 1 / 2 / 4 / 8 / 16 byte;order 不可為 RELEASE / ACQ_REL |
__nv_atomic_store_n / store |
原子寫 | order 不可為 CONSUME / ACQUIRE / ACQ_REL |
__nv_atomic_thread_fence |
依 order/scope 建立排序 | — |
__nv_atomic_compare_exchange* 的 weak 參數被忽略;實作會在 success_order 與 failure_order 之間取較強者執行比較交換。相等時回傳 true 並把 desired 寫入 address;否則回傳 false 並把 old 寫回 expected。
考試/測驗重點
| 題型 | 關鍵答案 |
|---|---|
__syncthreads() 等什麼? |
等 block 內所有未退出執行緒到達同一呼叫點或退出;並建立 happens-before 記憶體排序 |
__syncthreads_and vs _or vs _count? |
and=全為真、or=至少一真、count=非零執行緒數量 |
__syncthreads 放 if 內合法條件? |
條件須對整個 block uniform;否則 hang/未定義 |
if(threadIdx.x>0) __syncthreads() 對不對? |
錯,非 uniform,未定義行為 |
__syncwarp 預設 mask? |
0xFFFFFFFF(warp 全員);只同步 mask 指名執行緒 |
__syncthreads 與 __syncwarp 範圍差異? |
前者 block 全體,後者僅同 warp 內 mask 成員 |
| CUDA 記憶體模型? | weakly ordered;無 fence/sync 對同址讀寫=未定義行為 |
| fence 三層 scope? | block (__threadfence_block)、device (__threadfence)、system (__threadfence_system) |
| system-level fence 觀察者? | device 內所有執行緒 + host 執行緒 + 所有 peer device 執行緒 |
| fence 保證可見性嗎? | 不,只保證排序;可見性靠 volatile 或 atomic |
| 同 device 不同 block 該用哪層 fence? | device-level(__threadfence) |
| 跨 device 該用哪層 fence? | system-level(__threadfence_system) |
| 最後完成的 block 拿到的 counter 舊值? | gridDim.x - 1 |
| 原子五種提供方式哪種推薦? | Extended cuda::atomic / cuda::atomic_ref |
| Legacy atomic 的記憶體序? | 僅 memory_order_relaxed,且不引入 fence |
| Legacy scope 後綴? | 無=device、_block=block、_system=system |
atomicInc(addr,val) 計算? |
old >= val ? 0 : old+1(環繞) |
atomicDec(addr,val) 計算? |
(old==0 || old>val) ? val : old-1 |
atomicCAS 計算與回傳? |
old==compare ? val : old;回傳 old |
atomicAdd 哪些型別需 CC 門檻? |
__nv_bfloat16/162 需 CC 8.x+;float2/float4 需 CC 9.x+ 且僅 global |
哪些原子運算支援帶號 long long? |
僅 atomicMin/atomicMax;位元運算 atomicAnd/atomicOr/atomicXor 只支援 unsigned long long |
atomicExch 支援 double 嗎? |
否,只支援 int/unsigned/unsigned long long/float;double 只有 atomicAdd 支援 |
哪個原子運算支援 unsigned short? |
只有 atomicCAS(也是唯一支援 unsigned short 的運算) |
atomicSub 支援哪些型別? |
僅 int/unsigned(無 64-bit、無浮點) |
| 向量型別原子性? | 逐元素原子,整個向量非單次原子 |
| 浮點 CAS 迴圈為何用整數比較? | 避免 NaN != NaN 導致迴圈無法收斂而 hang |
__nv_atomic_* 自哪版起、限制? |
CUDA 12.8+;device only、不可 local memory、不可取位址、order/scope 須整數字面量 |
Built-in atomic 的 weak 參數? |
被忽略,取 success/failure order 較強者 |
Related Notes
- 05-Technical-Appendices/06-Function-and-Variable-Annotations
- 05-Technical-Appendices/08-Warp-Functions-and-Macros
- 05-Technical-Appendices/12-Memory-Barrier-Pipeline-Cooperative-APIs
- 02-Programming-GPUs/09-SIMT-Atomics-Cooperative-Occupancy
- 03-Advanced-CUDA/05-Thread-Scopes-and-Scoped-Atomics
- 05-Technical-Appendices/14-CUDA-Cpp-Memory-Model
- 05-Technical-Appendices/Practice-Technical-Appendices
- 00-Dashboard/Exam-Traps