同步原語與原子函式 (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 迴圈實作
Tip

libcu++ 推薦:fence 用 cuda::atomic_thread_fence,理由是安全與可攜性;原子操作用 Extended CUDA C++ atomic functionscuda::atomic / cuda::atomic_ref),兼顧效率、安全與可攜性

Thread Block 同步:__syncthreads 家族

void __syncthreads();
int  __syncthreads_count(int predicate);
int  __syncthreads_and(int predicate);
int  __syncthreads_or(int predicate);

語意:

帶 predicate 的變體(語意同 __syncthreads(),外加對所有未退出執行緒評估 predicate):

函式 回傳值
__syncthreads_count(p) predicate 為非零的執行緒數量
__syncthreads_and(p) 當且僅當所有執行緒 predicate 非零時回傳非零
__syncthreads_or(p) 當且僅當一個或以上執行緒 predicate 非零時回傳非零
條件呼叫必須 uniform

__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);
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];
}
Note

__syncthreadsblock 範圍 的 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 的寫入之前。

fence 只排序,不保證可見性與原子性

Memory fence 只影響記憶體操作執行的順序,不保證這些操作對其他執行緒可見。可見性需另用 volatile(繞過 L1 cache)或 atomic 達成。

跨 block 規約範例(單次 kernel 求 N 數總和):每個 block 先算部分和並存入全域記憶體,再由最後完成的 block 把所有部分和加總。判斷誰最後:每個 block 以 atomicInc(&count, gridDim.x) 遞增計數器,最後一個 block 拿到的舊值等於 gridDim.x - 1

Important

若在「寫部分和」與「遞增 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。

後綴 範例 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/162CC 8.x+);float2, float4CC 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+
128-bit 模板版(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);
}
Warning

浮點 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)

限制:

運算族: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 byteorder 不可為 RELEASE / ACQ_REL
__nv_atomic_store_n / store 原子寫 order 不可為 CONSUME / ACQUIRE / ACQ_REL
__nv_atomic_thread_fence 依 order/scope 建立排序
compare_exchange 的 weak 參數

__nv_atomic_compare_exchange*weak 參數被忽略;實作會在 success_orderfailure_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/floatdouble 只有 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 較強者