管什么:线程之间怎么等、怎么原子地改同一块内存,以及”别的线程 / 别的卡什么时候能看到我写的数据”。
线程同步
block 内同步
跨 block 与跨卡同步
原子操作
atom 与 red
atom:读-改-写,返回旧值,要等结果回来。red:同样的读-改-写交给 L2 原子单元做,不返回旧值,发出去就不用等,比atom便宜。atomicAdd(float*, float)不用返回值时,编译器一般会自动生成red.global.add.f32。
red{.sem}{.scope}{.global|.shared}.add.type [addr], val;
__device__ __forceinline__ void red_add_f16x2(__half2* p, __half2 v) {
unsigned int u = *reinterpret_cast<unsigned int*>(&v);
asm volatile("red.relaxed.gpu.global.add.noftz.f16x2 [%0], %1;"
:: "l"(p), "r"(u) : "memory");
}坑:
- 浮点
red累加顺序不固定,结果不能逐位复现。 - 目标地址在别的卡上(P2P / ICN 映射)时,依赖互联支持远程原子;ICN 是否支持未确认。
各类型支持
按 NVIDIA PTX 规范;810E 按 sm80 算。
| 类型 | 最低 sm | sm80 |
|---|---|---|
.u32 .s32 .u64 .f32 | 早就有 | 能 |
.f64 | sm60 | 能 |
.noftz.f16x2 | sm60 | 能 |
.noftz.f16 | sm70 | 能 |
.bf16 / .bf16x2 | sm90 | 不能 |
向量版 .v2/.v4.f32 等 | sm90 | 不能 |
sm90 的规约扩展
cp.reduce.async.bulk:TMA 整块写回并累加。multimem.red:在 NVSwitch 上做规约(NVLS)。red.async:往 cluster 里别的 CTA 的 smem 累加,配 mbarrier。
内存模型
语义 sem
.relaxed:只保证这次操作本身是原子的,不和别的读写排先后。适合纯计数。- “先写数据再发信号”:信号用
.release,读端用.acquire,对端才保证看到新数据。
作用域 scope
.cta / .gpu / .sys:这次操作要对多大范围的线程可见、可排序。跨卡通信一般要 .sys。
fence
fence.acq_rel.gpu/.sys:单独插一道硬件内存屏障。编译器层面的 "memory" clobber 管不了硬件乱序,见 volatile 与 memory clobber。