第 4 章 · GPU 硬件架构

⏱️ 50 分钟🎯 看懂 SM 内部📂 code/ch04_arch/

学习目标

前置知识

已完成 Ch03,能计算 thread 的全局索引,并理解 block、warp 与 lane 的基本关系。

核心概念

4.1 SM 解剖图

A100 (Ampere) 上有 108 个 SM,每个 SM 内部长这样:

SM 内部结构图

关键数字(A100 / sm_80):

资源每 SM说明
FP32 CUDA core64每周期吐 64 个 FP32 FMA
FP64 CUDA core32科学计算用
Tensor Core4 (3rd gen)主战场:fp16/bf16/tf32/int8 矩阵乘
寄存器 (32-bit)65536所有驻留 thread 平分
Shared mem + L1192 KB可配置分割比例
Max threads2048 (64 warps)同时驻留的上限

4.2 SIMT 执行模型

SM 调度的最小单位不是 thread 而是 warp = 32 个 thread。warp 内 32 个 thread同步执行同一条指令但操作不同的数据(寄存器/内存地址)。这就是 SIMT (Single Instruction Multiple Threads)。

graph LR
    PC["Program Counter"] --> Decode["Decode"]
    Decode -->|"1 cycle"| Disp["Dispatch"]
    Disp --> T0(("T0"))
    Disp --> T1(("T1"))
    Disp --> T2(("T2"))
    Disp --> Tn(("... T31"))
    style PC fill:#f3f1e8,stroke:#8b1538
    style Disp fill:#f3f1e8,stroke:#2f5d3a

SM 同时持有多个 warp(最多 64 个)。每周期硬件挑一个 ready 的 warp 发射指令。 当某个 warp 在等 global memory(几百周期),SM 切到别的 warp 干活——这就是"GPU 用并行隐藏延迟"。

关键代码

4.3 Warp Divergence — 同 warp 走两条路

如果 warp 内 32 个 thread 走了 if/else 两支:

if (threadIdx.x & 1) {        // 偶数 lane 走 else,奇数 lane 走 if
    x = x * 2;
} else {
    x = x + 1;
}

硬件没办法同时执行两条不同的指令,所以它串行跑两遍:先让走 if 的 lane 干活(else 的 lane 掩码屏蔽),再切到 else 的 lane。代价 = 两支的耗时之和。

运行 warp_divergence.cu,先核对两种布局输出一致,再记录目标 GPU 的时间:

uniform   (warp-aligned branch) : TODO(on GPU)
divergent (lane-aligned branch) : TODO(on GPU)
slowdown                        : TODO(on GPU)

两支工作量接近时,divergence 会让 warp 分别执行两条路径;实际代价还受指令、访存与编译器 predication 影响,必须实测。

💡 规避策略: 让分支条件与 threadIdx.x / 32(warp id)对齐,或者干脆用 ?: 让两边都算然后选一个(前提是两边都很便宜)。在 attention mask 这种场景里,常见做法是填 -∞ 而不是 if-skip。

4.4 Occupancy

定义:实际驻留 warp 数 / SM 最大支持 warp 数。高 occupancy 让 SM 有更多 warp 可以切换以隐藏延迟。

占用率受三个资源约束(取最严格的那个):

  1. 线程数:block 大小 × block/SM 数 ≤ 2048(A100)
  2. 寄存器:每 thread 用 N 个寄存器 → SM 上 64K/N 个 thread 上限
  3. Shared memory:每 block 用 M KB → SM 上 192/M 个 block 上限

运行 occupancy_probe.cu,CUDA Runtime 会替你算:

--- block = 256 ---
  light_kernel    block= 256  active blocks/SM= 8  warps/SM=64/64  occ=100.0%
  heavy_kernel    block= 256  active blocks/SM= 4  warps/SM=32/64  occ= 50.0%

heavy_kernel 因为本地数组占了很多寄存器(编译器把 float acc[64] 放到了寄存器),SM 上的驻留 block 数被腰斩。

⚠️ 高 occupancy ≠ 高性能。 ILP、寄存器复用和 occupancy 会互相制约。高性能 kernel 不一定追求最大 occupancy; FlashAttention 一类 kernel 也可能用更多寄存器换取更少的内存访问。

运行结果

4.5 架构演进 (5 分钟扫盲)

架构SM关键创新Tensor Core典型卡
Voltasm_70引入 Tensor Core (fp16 MMA)1st gen, 64 FMA/cycleV100
Turingsm_75消费级 TC,加 int8/int42nd genT4 / RTX 20
Amperesm_80/86tf32, bf16, async copy (cp.async), 稀疏3rd gen, 256 FMA/cycleA100, RTX 30
Adasm_89FP8 (E4M3/E5M2), SER4th genL40, RTX 40
Hoppersm_90TMA (硬件张量加载), DPX, async warpgroup MMA, 分布式共享内存4th genH100
Blackwellsm_100FP4, 第二代 Transformer Engine5th genB200 / GB200
Blackwell Ultra按具体 SKU 核验288 GB HBM3e, attention acceleration5th genB300 / GB300

对 LLM 推理重要程度(粗略):

4.6 工业实战:MIG、ECC、功耗管理

把 GPU 上线到生产,光会写 kernel 不够,还要会调"GPU 设置"。这一节是数据中心运维必备。

4.6.1 MIG (Multi-Instance GPU) — 一卡当七卡用

A100 / H100 / H200 支持把一张 GPU 切成最多 7 个独立的"小 GPU"(叫 MIG instance),每个有独立 SM、独立显存、独立 cache。用途:多租户推理服务,让小模型共享大卡。

# 1) 启用 MIG 模式 (需要 root + 没人在用 GPU)
sudo nvidia-smi -mig 1

# 2) 创建 MIG instances (A100-40G 可切 1g.5gb × 7)
sudo nvidia-smi mig -cgi 19,19,19,19,19,19,19 -C
#                      ^^^ profile id, 19 = 1g.5gb

# 3) 看现在的切片
nvidia-smi -L
# GPU 0: NVIDIA A100-SXM4-40GB
#   MIG 1g.5gb     Device 0: (UUID: MIG-...)
#   MIG 1g.5gb     Device 1: (UUID: MIG-...)
#   ...

# 4) 关 MIG 模式
sudo nvidia-smi -mig 0
profileSM 比例显存典型用途
1g.5gb1/7 ≈ 14%5 GBBERT-base, 小推理
2g.10gb2/7 ≈ 28%10 GB7B fp16 推理
3g.20gb3/7 ≈ 42%20 GB13B fp16
7g.40gb1 (全卡)40 GB整卡训练

陷阱:MIG 开启后,cudaGetDeviceCount 返回的是 MIG instance 数而不是物理 GPU 数;代码看到的算力是切片大小,跟物理 GPU 不一样。新手调试半天找不到原因,记得 nvidia-smi -L 看现在到底切了没。

4.6.2 ECC — 单 bit 翻转保护

数据中心 GPU 默认开 ECC(错误检测与纠正)。它给每 64 bit 数据加 8 bit 校验,能纠 1 bit 错误、检测 2 bit 错误。代价:

nvidia-smi -e 0           # 关 ECC (需重启 GPU)
nvidia-smi -e 1           # 开
nvidia-smi -q -d ECC      # 查累计错误数
# 关注:
#   Volatile Single Bit ECC Errors  (本次启动后)
#   Aggregate Single Bit ECC Errors (从出厂)

是否关 ECC

4.6.3 功耗与时钟管理

GPU 默认动态调频:温度高就降频、负载低就降频。生产服务希望延迟稳定,常用做法是锁定时钟

# 查支持的时钟
nvidia-smi -q -d SUPPORTED_CLOCKS | head -40

# 锁定 GPU 时钟到 1410 MHz, mem 1215 MHz (A100 典型最大)
sudo nvidia-smi -ac 1215,1410

# 锁定功耗墙到 300W (默认 400W, 省电用)
sudo nvidia-smi -pl 300

# 解锁
sudo nvidia-smi -rac
sudo nvidia-smi -rgc

锁频后 benchmark 数字更稳定(消除 thermal throttling 的抖动),适合 CI 性能回归测试。

4.6.4 nvidia-smi 读图:理解每列

+---------------------------------------------------------------------------------------+
| NVIDIA-SMI 535.86.05    Driver Version: 535.86.05    CUDA Version: 12.2     |   ← 驱动 + 驱动支持的最高 CUDA
|-----------------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M | Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap |         Memory-Usage | GPU-Util  Compute M. |
|                                |                      |               MIG M. |
|=========================================+======================+======================|
|   0  NVIDIA A100-SXM...  On    | 00000000:00:04.0 Off |                    0 |
| N/A   42C    P0    72W / 400W  |   1234MiB / 81920MiB |     45%      Default |
|                                |                      |             Disabled |
+-----------------------------------------+----------------------+----------------------+

要关注的字段:
- Persistence-M = On    避免重复初始化开销(是否启用按部署策略决定)
- Pwr 72W/400W         功耗远低于 cap → mem-bound 或 launch 等
- 1234MiB/81920MiB     显存占用 (有时是其他进程, nvidia-smi 看不到 driver 占用)
- GPU-Util 45%         注意陷阱: 高 GPU-Util 不代表算力跑满
- MIG M. Disabled      MIG 未开

看进程占了多少:

nvidia-smi pmon -i 0 -d 1     # 每秒打印每进程的 sm/mem/enc/dec util
nvidia-smi --query-compute-apps=pid,used_memory --format=csv

4.6.5 各代 GPU 在 LLM 工业部署中的定位

架构代表卡对 LLM 关键能力2025 现状
Volta sm_70V100第一代 TC, 只有 fp16已淘汰, 偶见 EC2 老机型
Turing sm_75T4 / RTX 20TC + int8, 16 GBColab 免费 / 边缘推理
Ampere sm_80A100cp.async / bf16 / tf32, 40-80 GB训练 / 推理主力, 性价比之王
Ampere sm_86A10 / RTX 30fp16 TC 大幅强化推理中端
Ada sm_89L40S / RTX 4090FP8 + 更多 fp16 算力消费推理 / 边缘
Hopper sm_90H100 / H200TMA / wgmma / DPX / Transformer Engine训练顶配, 长 context 推理
Blackwell sm_100B200 / GB200FP4 / TC 第 5 代180 GB 与 186 GB SKU 分开核验
Blackwell UltraB300 / GB300288 GB HBM3e / attention acceleration2026-07 当前产品

对 LLM 推理来说最大的几次跳跃

  1. Volta → Turing:消费端有了 TC,inference 落地变现实
  2. Turing → Ampere:cp.async 让 FlashAttention 成为可能
  3. Ampere → Hopper:TMA + Transformer Engine + fp8 让 100B+ 模型可服务
  4. Hopper → Blackwell:NVFP4、TMEM 与更大的内存系统改变推理优化空间;成本要按质量、SLO 与真实 workload 测量

选 GPU 的时候,对应到你的模型规模和 latency 要求查上表。

4.7 研究前沿(2025-2026):Blackwell 解剖

4.7.1 Blackwell B200 关键特性

Blackwell(sm_100)是 Hopper 之后的下一代,2024 末发布,2025 大规模量产。结构变化比 Ampere→Hopper 更激进:

来源:NVIDIA Blackwell Tuning Guide 与各 SKU 官方规格。
特性Hopper (H100)Blackwell (B200)变化
制程TSMC 4NTSMC 4NP, dual-die双 die 通过 NV-HBI 10 TB/s 互联
晶体管80B208B(2 die 各 104B)2.6×
SM 数按 H100 SKU按 B200 SKU运行时用 device attributes 核验
HBM80 GB HBM3180 GB HBM3e不要与 GB200 每 GPU 186 GB 或 B300 288 GB 混写
HBM 带宽3.35 TB/s以 B200/HGX 官方规格页为准对 decode 做 roofline 后再判断
Tensor Core 峰值FP16/BF16/FP8FP16/BF16/FP8/NVFP4引用时必须同时标 dtype 与 sparse/dense
Tensor Memory (TMEM)每 SM 256 KB新增(关键创新)
2nd-gen Transformer EngineFP6 / FP4 / 动态精度
NVLinkNVLink 4, 900 GB/sNVLink 5, 1800 GB/s2× 跨卡带宽

4.7.2 Tensor Memory (TMEM) — Blackwell 最大改动

Hopper 及之前所有 GPU 用一个统一的 register file 存 fragment / accumulator。Blackwell 把matrix accumulator 抽出来到一块独立的 SRAM(叫 TMEM):

SM 内部 (Blackwell):
  ┌─ Register file       ~64 K × 32-bit  (跟 Hopper 一样)
  ├─ Shared / L1         ~228 KB         (跟 Hopper 一样)
  ├─ Tensor Memory (TMEM)   256 KB          ← 新增, 专给 MMA accumulator
  └─ Tensor Cores (5th gen) + Transformer Engine 2

动机:fp4 / fp8 MMA 的 accumulator 是 fp32,每个 MMA 要消耗几十个寄存器持有 acc。把 acc 放 TMEM 释放了寄存器给其他用途,让 ILP 大幅提升。

用 TMEM 的新指令: TCGEN05

// PTX-level (CUTLASS 4.6.1 / CuTe DSL(2026-07 快照) 自动用)
tcgen05.mma.async        // 异步 MMA, accumulator 写 TMEM
tcgen05.ld               // 从 TMEM 读出 accumulator
tcgen05.cp               // shared memory ↔ TMEM

CUTLASS/CuTe 封装了许多 TCGEN05 与 TMEM 细节;研究 kernel 也可能直接使用更底层原语。是否接近峰值必须按 dtype、dense/sparse、shape 与完整 benchmark 记录判断。

4.7.3 2nd-gen Transformer Engine:动态精度

Hopper 的 1st-gen TE 只能在 fp16 / fp8 之间切。Blackwell 的 2nd-gen 可以:

4.7.4 GB200 与 NVL72:rack 即 GPU

单 B200 已经很强,但 NVIDIA 的真正杀手锏是 NVL72

来源:NVIDIA GB200 NVL72


1 个 GB200 Grace Blackwell Superchip = 1 Grace CPU + 2 Blackwell GPU
每 GPU 186 GB HBM3e;Superchip 共 372 GB、16 TB/s
1 个 GB200 NVL72 = 36 个 Superchip = 72 GPU
整柜 13.4 TB HBM3e、576 TB/s;峰值按 NVIDIA 表中的 sparse | dense 口径阅读

能干什么:

4.7.5 Rubin(2026 中后期)— 下一代

NVIDIA 已公布 Rubin 路线图:

核心趋势:单卡再难有 2× 跳跃,整柜带宽与 cluster 调度是新战场

4.7.6 AMD / Google TPU 现状(2026)

方案对应 NVIDIA差距优势
AMD MI325X (CDNA 3.5)~H200fp8 算力略低256 GB HBM3e — 显存王
AMD MI350 (CDNA 4, 2025)~B200fp4 支持开源 ROCm 生态长期投资
Google TPU v6 Trillium~H200 (BF16)fp4 弱整 pod 训练效率高
Google TPU v7 (2025-26)~B200仅 GCPGemini 系列自用
Cerebras WSE-3晶圆级单卡不同范式900K core, 训练大模型快
Groq LPU专攻推理训练弱700+ tokens/s/user — 最快推理
SambaNova SN40L专攻 MoE 推理大 SRAM, 适合 1T+ MoE

2026 工业现实:推理硬件与软件栈已有多种选择;本教程不保留无一手来源的市场份额或竞品等价数字。CUDA 仍是本课程的研发起点,但选型应以可移植性、容量、SLO、成本和 workload benchmark 为依据。

自检清单

Q1: 一个 SM 同时能跑多少 thread?多少 warp?

A100 上 2048 thread = 64 warp(同时驻留);同时执行的只有 4 个 warp(因为有 4 个 processing block,每周期挑 4 个 warp 发射)。

Q2: 我的 block 用了 32 thread,能跑满 GPU 吗?

不能。32 thread = 1 warp,但 SM 想要 8-16 个 warp 才能隐藏延迟。block 太小 → 单 SM 上 warp 太少 → occupancy 上不去。

Q3: FP32 算力高还是 FP16 算力高?差多少?

A100:FP32 19.5 TFLOPS(用 CUDA core),FP16+TC 312 TFLOPS(用 Tensor Core)。差 16 倍。所以 LLM 推理用 fp16/bf16/fp8。

Q4: cooperative_groups 是什么?

CUDA 9+ 引入的"任意粒度同步"API。可以细到 warp 内 8 thread 同步、粗到 grid 内所有 block 同步(grid sync 需要 sm_60+ 且专用 launch API)。第 7 章会用到 warp-level reduce。

Q5: 寄存器溢出 (register spill) 是什么?

当编译器发现一个 thread 用的寄存器超过硬件上限(A100 上 thread 最多 255 寄存器),多出的 "本地变量" 会被放到 local memory——其实是 global memory 的私有分区,奇慢。Nsight 报告里看 "Stack Spills" 行。

练习题

  1. warp_divergence.cu,改 iters 看耗时如何线性变化。
  2. occupancy_probe.cu 加一个 shared_kernel,用 __shared__ float buf[16384](= 64 KB),看 SM 上能驻留几个 block。
  3. 查阅你 GPU 的 Compute Capability 表,记下:每 SM 最大寄存器、shared mem、warp 数。

常见坑

4.10 CUDA 官方手册精讲(CUDA Programming Guide 13.2(核验:2026-07-20))

SIMT 深读、Warp Scheduler、Thread Scope、寄存器堆

本节定位:把 NVIDIA 官方 CUDA Programming Guide 13.2(核验:2026-07-20) 当前版中和本章直接相关的硬核细节抽出来——概念、API、踩坑点、版本兼容性—— 让你不必通读官方手册也能掌握本章主题的"标准答案"。引用按命名章节回查,避免把旧版编号当作稳定接口。

深入 SIMT — Independent Thread Scheduling

4.2 节给出的"32 lane 同步执行同一条指令"是 Pascal (sm_60) 及之前的描述。 Volta (sm_70) 起 NVIDIA 给每个 thread 都加了独立的 program counter 和 call stack,称为 Independent Thread Scheduling (ITS)

架构Warp 调度模型同步要求
≤ sm_60 (Pascal)单 PC + active mask,warp 内永远 lock-stepwarp 内可省略显式同步
≥ sm_70 (Volta+)每 thread 独立 PC + call stack,子-warp 粒度可分歧必须 __syncwarp() 才能依赖 warp 内一致性

ITS 带来的能力:

⚠️ 旧代码兼容性陷阱:在 sm_60 上正确的 warp-synchronous reduce(不写 __syncwarp),到 sm_70+ 上行为未定义。 CUDA 9 起所有 __shfl_* 都被废弃,必须改用 __shfl_sync / __ballot_sync / __any_sync,并显式传 32-bit 的 lane mask。
// ✅ 正确(sm_70+ 必须)
    unsigned mask = __activemask();         // 当前 warp 内活跃的 lane bitmask
    int v = __shfl_sync(mask, val, 0);       // 显式同步 + broadcast lane 0 的值
    __syncwarp(mask);                        // 任何依赖 warp-内一致性的点都补一次

    // ❌ 旧写法 (deprecated, 在 sm_70+ 上行为未定义)
    int v = __shfl(val, 0);
    

所以本教程后续章节里你会看到大量 __shfl_xor_sync(0xffffffff, ...)——0xffffffff 表示"全 warp 参与",这是约定俗成的安全 mask。

warp scheduler 与零开销切换

"GPU 用并行隐藏延迟"是口号。具体怎么做?关键在两点:

  1. 每个 warp 的 context(PC + 寄存器 + call stack)常驻片上。SM 上有最多 64 个 warp 的 context 同时存在,切换不需要 push/pop(不像 CPU 上下文切换)。
  2. 每个 SM 配备多个 warp scheduler。A100 上每 SM 有 4 个 scheduler,每周期每个 scheduler 各挑一个 ready warp 发射 1 条指令——所以 A100 SM 每周期最多同时执行 4 个 warp
架构warp scheduler 数 / SM每周期可发射指令数最大驻留 warp / SM
Volta (sm_70)4464
Ampere (sm_80)4464
Hopper (sm_90)4464
Blackwell (sm_100)4464

所以"驻留 64 warp / 实际同时执行 4 warp"的真正含义是:scheduler 在 64 个候选里挑 4 个最 ready 的。这就是延迟隐藏:

    sequenceDiagram
        participant W0 as Warp 0
        participant W1 as Warp 1
        participant Wn as ...Warp 63
        participant SM as Scheduler
        SM->>W0: 发射 LDG (~500 cyc)
        Note over W0: 等 HBM
        SM->>W1: 发射 FMA
        SM->>Wn: 发射 FMA
        Note over W0: HBM 数据到达
        SM->>W0: 发射 FMA (用 LDG 结果)
    

结论:

cuOccupancyMaxActiveBlocksPerMultiprocessor(runtime API:cudaOccupancyMaxActiveBlocksPerMultiprocessor)在编译期之外、launch 之前就能让 driver 替你算出当前 kernel 在当前 GPU 上能跑几个 active block:

int max_blocks_per_sm;
    cudaOccupancyMaxActiveBlocksPerMultiprocessor(
        &max_blocks_per_sm,
        my_kernel,        // __global__ 函数指针
        /*blockSize=*/256,
        /*dynamicSMemSize=*/0);
    printf("active blocks / SM = %d\n", max_blocks_per_sm);

    // 进阶: 让 CUDA 帮你挑最优 block size
    int min_grid, best_block;
    cudaOccupancyMaxPotentialBlockSize(
        &min_grid, &best_block, my_kernel, 0, 0);
    

分歧、谓词执行与 ILP(指令级并行)

4.5.1 编译器何时把分支编成谓词(predication)

不是所有 if/else 都会变成"两支都跑"的串行。如果两支都很短(典型阈值:每支 <= 7 条指令),nvcc 会用 PTX 的 predicate 寄存器把两支都编译成无跳转的指令流:

// 源码
    if (cond) x = a * b;
    else      x = a + b;

    // nvcc 编译后 (伪 PTX, 一条 setp + 两条带谓词的指令)
    setp.ne.f32   %p1, %cond, 0f00000000;       // p1 = (cond != 0)
    @%p1   fma.f32     %x, %a, %b, 0f00000000;   // p1=true 时执行
    @!%p1  add.f32     %x, %a, %b;               // p1=false 时执行
    

谓词执行的代价:两支的指令都流过流水线,但只有匹配 lane 写回。所以哪怕没分歧,"两支都跑"的代价也存在。Nsight Compute 的 smsp__sass_branch_targets_threads_uniform.pct 接近 100% 说明几乎全部走了谓词路径。

4.5.2 当两支不等长,divergence 代价不是 2×

4.3 节实验比较等长 if/else 分支;一般情况下:

实际耗时 ≈ max(T_if, T_else) × 1   if 全 warp 走同一支
             ≈ T_if + T_else           if warp 内分歧
    

所以最坏情况:if 路径 95%、else 路径 5%,但只要 warp 内有一个 lane 走了 else,整 warp 等到两支都跑完。这就是为什么大语言模型 attention mask 实现里"用 -∞ 填充"比"用 if 跳过"快——x + (-inf) 永远是无分歧的算术。

4.5.3 ILP — 单 thread 内的并行

SIMT 之外还有 thread 内的指令级并行。同一 thread 内独立指令可以并行流水:

// ILP 友好: 4 个独立 FMA, 编译器可重排
    float a0 = x[0]*y[0] + b[0];
    float a1 = x[1]*y[1] + b[1];
    float a2 = x[2]*y[2] + b[2];
    float a3 = x[3]*y[3] + b[3];

    // ILP 不友好: 串行依赖
    float a = 0;
    a = a + x[0]*y[0];
    a = a + x[1]*y[1];
    a = a + x[2]*y[2];
    a = a + x[3]*y[3];
    

NVIDIA SM 是 in-order 发射,不做 branch prediction 也不做 speculative execution(不像 CPU)。靠的就是 ILP + warp 切换两条路一起隐藏延迟。这也是为什么 FlashAttention 故意展开内层循环——让编译器排出更宽的 ILP 窗口,每 thread 持有更多寄存器中的 accumulator。

💡 Nsight 指标速查smsp__inst_executed_pipe_* 显示哪条流水线(fp32/fp64/ld/st/tensor)打满;smsp__warps_active.avg.pct_of_peak_sustained_active 是真正的"warp 占用率"(不是 occupancy;occupancy 是上限)。

Thread Scope 与 scoped atomic

"原子操作"四个字隐藏着一个问题:对谁原子? 同一个 block 内的 lane?整个 GPU?跨 GPU?scope 越大、硬件需要走的 cache coherence path 越长、代价越高。

CUDA 12+ 的 libcu++ 提供四种 scope,对应内存层级中的不同 "coherency point":

C++ ScopePTX Scope可见性一致性边界
cuda::thread_scope_thread仅本 thread
cuda::thread_scope_block.cta同 block 内 threadL1 / shared
cuda::thread_scope_cluster (sm_90+).cluster同 cluster 的 blockL2 (DSMEM)
cuda::thread_scope_device.gpu整个 GPU 上的 threadL2
cuda::thread_scope_system.sys跨 GPU + CPUL2 + 互联 cache

4.6.1 block-scope 计数器示例

#include <cuda/atomic>

    __global__ void block_scoped_counter() {
        // Block-scoped atomic avoids unnecessary wider-scope ordering.
        __shared__ cuda::atomic<int, cuda::thread_scope_block> counter;

        if (threadIdx.x == 0) {
            counter.store(0, cuda::memory_order_relaxed);
        }
        __syncthreads();

        // 每 thread 加 1, 取旧值
        int old = counter.fetch_add(1, cuda::memory_order_relaxed);
        // ... 用 old ...
    }
    

4.6.2 producer-consumer:acquire/release 排序

当一个 thread 要把"数据准备好了"通知给另一个 thread,relaxed 不够,要用 release(写端)+ acquire(读端)保证数据写在 ready 信号之前对消费者可见

__global__ void producer_consumer() {
        __shared__ int data;
        __shared__ cuda::atomic<bool, cuda::thread_scope_block> ready;

        if (threadIdx.x == 0) {
            data = 42;
            ready.store(true, cuda::memory_order_release);   // 写端
        } else {
            while (!ready.load(cuda::memory_order_acquire)) { // 读端
                // spin
            }
            int v = data;   // 保证看见 42 (acquire 同步了 release 之前的写)
        }
    }
    
实战经验(PDF 3.2.4.1.2):
  1. 能用 block 就别用 device。block-scoped atomic 走 shared/L1,~30 cycle;device-scoped 走 L2,~200 cycle
  2. 能用 relaxed 就别用 acq_rel。计数、纯 reduce 用 relaxed
  3. shared memory atomic > global atomic。先在 block 内 reduce,再用一个 device-scope atomic 累加到 global

这套 API 在 LLM 实现里到处出现:FlashAttention 的 row-max 协调(block scope)、PagedAttention 的 ref count(device scope)、跨 GPU NCCL all-reduce 的 barrier(system scope)。

寄存器堆:占用率与 spill 的真正瓶颈

4.7.1 每代 GPU 寄存器堆容量

架构SM寄存器堆容量32-bit reg 数每 thread 上限
Pascal sm_60P100256 KB65 536255
Volta sm_70V100256 KB65 536255
Turing sm_75T4 / RTX 20256 KB65 536255
Ampere sm_80A100256 KB65 536255
Ampere sm_86A10 / RTX 30256 KB65 536255
Ada sm_89L40 / RTX 40256 KB65 536255
Hopper sm_90H100256 KB65 536255
Blackwell sm_100B200 / GB200256 KB65 536255

关键点:每 SM 一共 64K 个 32-bit 寄存器,所有驻留 thread 共享。所以如果你每 thread 用 64 个寄存器,SM 上最多驻留 1024 thread = 32 warp(不到上限 64)。寄存器用得越多,occupancy 越低,但 ILP 越强。

4.7.2 spill 到底贵多少?

当编译器发现一个 thread 用的寄存器超过 -maxrregcount 或 hard limit (255),多出的局部变量会被放到 local memory——名字叫"local",其实是每 thread 独占的一段 global memory 分区。被 L1 cache 兜底,但延迟从 1 cycle 暴增到 30+ (L1 命中) 或 500+ (L1 miss)。

# 编译时显示每 kernel 的寄存器/local memory/shared mem 用量
    nvcc -Xptxas -v -arch=sm_80 my_kernel.cu

    # 输出节选:
    # ptxas info    : Used 96 registers, 0 stack, 16 bytes smem, 332 bytes cmem[0]
    #                                    ^^^^^^^^^ stack > 0 = spill 到 local !
    

4.7.3 三种控制寄存器用量的方式

// 1) 编译期硬限制 (整个编译单元)
    //    nvcc -maxrregcount=64 -arch=sm_80 my.cu

    // 2) per-kernel 软提示: __launch_bounds__(maxThreadsPerBlock, minBlocksPerSM)
    //    告诉编译器"我准备每 SM 跑 minBlocksPerSM 个 block"
    //    编译器据此反推每 thread 寄存器预算 = 64K / (maxThreadsPerBlock * minBlocksPerSM)
    __global__ __launch_bounds__(256, 4)
    void my_kernel(/*...*/) {
        // 256 thread * 4 block/SM = 1024 thread → 每 thread 最多 64 reg
    }

    // 3) 用 const + 编译期 unroll 让编译器复用寄存器
    #pragma unroll
    for (int i = 0; i < 8; ++i) acc[i] += x * y[i];
    
⚠️ FlashAttention 的反直觉做法:实现可能用 __launch_bounds__ 和较大的寄存器预算保存 accumulator,以较低 occupancy 换数据复用。应同时检查 registers/thread、spill、active warps、HBM traffic 与时间,不能单看 occupancy 下结论。

记住这条经验法则:看 Nsight Compute 的 Stack Frame 字段,> 0 就该 debug。

异步执行:硬件 barrier 与 async proxy

Ampere 起 NVIDIA 在 SM 内增加了三种真正并行的硬件单元,把"等数据"和"算"在物理上拆开:

硬件单元引入版本用途
LDGSTS (global→shared 异步 load)sm_80 (Ampere)小数据 element-wise 异步搬运
TMA (Tensor Memory Accelerator)sm_90 (Hopper)批量多维 tile 搬运(带边界处理 + swizzle)
STAS (register→DSMEM 异步)sm_90 (Hopper)cluster 内 block 间共享

4.8.1 Async Thread / Async Proxy 概念

这是 PDF §3.2.2.3.1 的核心定义,看 CUTLASS 源码必备:

    graph LR
        T["CUDA thread"]
        T --"普通 LDG/STS"--> GP[Generic Proxy]
        T --"cp.async (LDGSTS)"--> AT1["Async thread\n@ generic proxy"]
        T --"cp.async.bulk (TMA)"--> AT2["Async thread\n@ async proxy"]
        GP --> L1[L1/Shared]
        AT1 --> L1
        AT2 --> L1
        style AT2 fill:#f3f1e8,stroke:#8b1538
    

4.8.2 Asynchronous barrier(split-arrive-wait)

普通 __syncthreads() 把"到达"和"等待"合二为一。Ampere 起硬件支持 split barrier——arrive 不阻塞,可以先做别的事,最后再 wait:

#include <cuda/barrier>
    #include <cooperative_groups.h>

    __global__ void split_arrive_wait(int n_iter, float* data) {
        using barrier_t = cuda::barrier<cuda::thread_scope_block>;
        __shared__ barrier_t bar;
        auto block = cooperative_groups::this_thread_block();

        if (block.thread_rank() == 0)
            init(&bar, block.size());
        block.sync();

        for (int i = 0; i < n_iter; ++i) {
            // 1) arrive — 不阻塞, 返回 token
            auto token = bar.arrive();

            // 2) 此时可以做与 barrier 无关的工作 (隐藏 barrier 等待开销!)
            compute_unrelated(data, i);

            // 3) wait — 真正阻塞直到全 block 都 arrive
            bar.wait(std::move(token));
        }
    }
    

用途:在 producer-consumer 流水线里,producer 提交完异步 copy 立刻 arrive,然后干别的;consumer 真正需要数据时才 wait。FlashAttention v3 整套 pipeline 就建立在这个机制上。

4.8.3 Pipeline — 多 stage producer-consumer 抽象

libcu++ 提供 cuda::pipeline 把 split barrier + async copy 封装成 producer/consumer FIFO:

#include <cuda/pipeline>
    #include <cooperative_groups/memcpy_async.h>

    template <size_t STAGES = 2>
    __global__ void double_buffer_kernel(int* g_out, const int* g_in, int n) {
        extern __shared__ int smem[];     // STAGES * block.size() * sizeof(int)
        auto block = cooperative_groups::this_thread_block();
        auto pipe  = cuda::make_pipeline();

        // 预先填满 STAGES-1 个 stage
        for (size_t s = 0; s < STAGES - 1; ++s) {
            pipe.producer_acquire();
            cuda::memcpy_async(block, smem + s * blockDim.x,
                               g_in + s * blockDim.x,
                               sizeof(int) * blockDim.x, pipe);
            pipe.producer_commit();
        }

        // 主循环: 一边 consume, 一边异步拉下一批
        for (int i = 0; i < n; ++i) {
            pipe.consumer_wait();
            compute(smem + (i % STAGES) * blockDim.x);
            pipe.consumer_release();

            // 立刻发起下一次 copy
            pipe.producer_acquire();
            cuda::memcpy_async(/*...*/);
            pipe.producer_commit();
        }
    }
    
实战验证:GEMM 主循环可用多阶段流水把数据搬运与 compute 重叠;Hopper TMA + warp specialization 还能把 producer / consumer 分工。用 timeline、long-scoreboard stall、tensor-pipe utilization 和 kernel 时间验证 overlap,不预设固定收益。

下一章导览

第 5 章进入性能优化的核心:内存层级与合并访问。高性能 kernel 往往需要大量精力减少和重排访存。