CS336 · 第 5 课
同样数量的 FLOPs,为什么有的算子接近硬件峰值,有的却大部分时间都在等待数据?本讲从计算单元、memory hierarchy 和执行模型出发,建立判断 kernel 瓶颈的硬件视角,并比较 GPU 与 TPU 的共同目标和不同取舍。
Spring 2026 官方来源:Lecture 5 PDF · 课程日程
本页是中文学习导读;讲次主题、教师与材料版本以 Stanford 官方页面和讲义为准。
Lecture 2 已经教会我们核算 FLOPs 和 bytes,但公式还没有解释:为什么相同 FLOPs 在不同算子上会得到完全不同的实际时间?答案取决于数据在硬件中的位置,以及程序能否让足够多的计算同时进行。
并行 workload → SM / tensor core 执行 → registers / shared memory / HBM 数据移动 → arithmetic intensity 与 roofline → coalescing / tiling / occupancy → CUDA、Triton 和 TPU 如何映射同一组约束。
GPU 快不是因为单个 core 神奇,而是大量线程用相似控制流处理数据。先区分 FLOPs(工作量)、FLOP/s(计算速率)、bytes(容量/流量)与 bytes/s(带宽),本课才回答“慢在算,还是慢在搬”。
HBM → L2 → shared memory / L1 → registers → arithmetic units
大/慢 小/快
kernel workload
├─ math FLOPs
├─ memory traffic bytes
├─ parallel work / synchronization
└─ resource use per block(registers, shared memory)
↓
occupancy 与 achieved throughput对 BF16 的 \(y=x+z\),每元素约 1 FLOP,却至少读 \(x,z\) 各 2 bytes、写 \(y\) 2 bytes,算术强度约 \(1/6\) FLOP/byte;再多 ALUs 也无法消除这 6 bytes。相比之下,tile matmul 可让加载的元素参与多次 multiply-add,提高 reuse。Roofline 的意义正是把 toy traffic 推到真实 hardware ceiling。
一个 bandwidth-bound kernel 可以持续占用 GPU,使 utilization 显示 100%,却只实现很低的峰值 FLOP/s;occupancy 也不是越高越好,若更大的 tile 用更多 registers、减少 occupancy,却显著增加数据复用,最终反而可能更快。
| 特性 | CPU | GPU |
|---|---|---|
| 核心数 | ~16-64 复杂核心 | ~1000s 简单核心 |
| 优化目标 | 低延迟(latency) | 高吞吐(throughput) |
| 适合任务 | 串行、分支密集 | 并行、计算密集 |
| 内存 | 低延迟,大容量 | 高带宽,较大容量 |
A100 SM (Streaming Multiprocessor) 结构:
├── 108 个 SM
├── 每个 SM 包含:
│ ├── 64 FP32 CUDA cores (INT32 也可)
│ ├── 4 个 Tensor Cores (第三代)
│ │ └── 支持 TF32, FP16, BF16, FP64 等矩阵指令模式
│ ├── 192 KB L1 cache / Shared Memory (可配置)
│ └── 寄存器文件 (256 KB)
├── 40 MB L2 cache (共享)
└── 80 GB HBM2e (高带宽内存)
带宽: 2 TB/s
H100 SM (Streaming Multiprocessor) 结构:
├── H100 SXM 产品启用 132 个 SM
├── 每个 SM 包含:
│ ├── 128 FP32 CUDA cores
│ ├── 4 个 Tensor Cores (第四代)
│ │ └── 支持 FP8, FP16, BF16, TF32
│ ├── 256 KB unified L1 / Texture / Shared Memory
│ │ └── 其中 shared-memory carveout 最高 228 KB/SM
│ └── 寄存器文件 (256 KB)
├── 50 MB L2 cache
└── 80 GB HBM3
带宽: 3.35 TB/s
Spring 2026 官方讲义把 Blackwell 作为新的硬件案例:B200 提供 148 个 SM、192 GB HBM 与约 8 TB/s HBM 带宽,并引入更强的低精度矩阵计算路径。这里最重要的不是背峰值数字,而是理解 MXFP8 / MXFP4 使用多个局部 scale,用额外元数据换取低 bit 宽下更可用的动态范围。
新鲜度边界:MXFP4/NVFP4 的支持不代表任意 PyTorch kernel 会自动得到 Blackwell 峰值。布局、scale recipe、累加精度、Tensor Core 指令与库版本必须同时匹配。
| 层次 | H100 SXM 容量示例 | 访问范围 | 性能特征 |
|---|---|---|---|
| HBM (Global Memory) | 80 GB,峰值 3.35 TB/s | 整张 GPU | 容量最大,片外访问;延迟和带宽受访问模式影响 |
| L2 Cache | 50 MB | 所有 SM | 片上缓存,命中时避免 HBM 流量 |
| L1 / Shared Memory | 合计 256 KB/SM;shared 最多 228 KB/SM | SM / thread block | 软件管理的 shared memory 可用于显式数据复用 |
| Registers | 每 SM 64K 个 32-bit registers | 单个 thread | 最快但稀缺;过多使用会限制 occupancy 或发生 spill |
仓库 vs 工厂类比(Horace He):
HBM = 仓库:容量大,但搬运慢(带宽有限)
SRAM = 工厂车间:容量小,但搬运快(带宽极高)
Registers = 工人手:容量极小,但最快
优化目标:最小化从"仓库"(HBM)搬运数据的次数。
H100 (SXM5) 关键数字:
计算峰值 (bf16): 989 TFLOP/s
内存带宽: 3.35 TB/s
Ridge point: 989e12 / 3.35e12 ≈ 295 FLOP/byte
这意味着: 在只考虑峰值算力与 HBM 带宽的理想 Roofline 中,
算术强度约高于 295 FLOP/byte 才可能进入 compute-bound 区域。
它描述的是吞吐平衡点,不是单次访存延迟。
# GPU 编程的基本抽象
Grid
└── Thread Block (CTA) ← 在单个 SM 上执行
└── Warp (32 threads) ← 以 SIMD 方式执行
└── Thread ← 基本执行单位
# 为什么是 Warp?
# Warp 调度器向 32 个线程发出共同指令,各线程保有独立状态
# NVIDIA 将这种抽象称为 SIMT,而不是传统向量机意义的 SIMD
SIMT = Single Instruction, Multiple Thread
Warp 中的 32 个线程同时执行同一条指令
但操作不同的数据
例如: 向量加法
Thread 0: a[0] + b[0] → c[0]
Thread 1: a[1] + b[1] → c[1]
...
Thread 31: a[31] + b[31] → c[31]
从编程模型看是一条 warp 指令;实际完成周期取决于指令、流水线和硬件吞吐。
分支发散(Branch Divergence):如果 warp 中的线程走不同控制流路径,硬件通常需要用不同 active mask 分阶段执行这些路径,因此有效吞吐可能下降;具体代价取决于分支长度、编译结果和架构。
# 坏: 分支发散
if threadIdx.x % 2 == 0:
do_A() # 16 个线程执行
else:
do_B() # 16 个线程执行
# 两条路径可能被分别执行,不能再让全部 lane 同时做有效工作
# 好: 无分支
# 让所有线程走相同路径
Occupancy = 活跃 warp 数 / 最大支持 warp 数
高 occupancy 意味着 SM 有更多的 warp 可以调度——当一个 warp 等待内存时,SM 可以切换到另一个 warp。
影响 occupancy 的因素:
1. 每个 thread 使用的寄存器数
2. 每个 block 使用的 shared memory
3. block 大小
实践原则:
- block size 通常取 warp size (32) 的倍数
- 128 或 256 threads/block 是常见起点,不是固定最优值
- 更高 occupancy 有助于隐藏等待,但最高 occupancy 不保证最高性能;
还要平衡寄存器、shared memory、指令级并行和数据复用
Roofline model 直观地展示一个操作的性能上限:
性能 (FLOP/s)
↑
│ ┌──────── 计算上限 (compute ceiling)
│ /
│ / ← 斜率 = 内存带宽
│ /
│ /
│───/───────────────→ 算术强度 (FLOP/byte)
│ ridge point
| 操作 | 算术强度 | 瓶颈 |
|---|---|---|
| Element-wise | 通常很低:每个元素只做少量运算 | 常见为 Memory-bound |
| Reduction / normalization | 取决于融合、重读次数和统计量计算 | 常见为 Memory-bound |
| 矩阵-向量乘(如 batch=1 decode) | 权重复用有限 | 常见为 Memory-bound |
| 大矩阵乘 | 随 M、N、K 和数据复用增加 | 尺寸足够大且形状友好时可 Compute-bound |
Memory-bound 操作的优化:
Compute-bound 操作的优化:
Tensor Core 是 GPU 上的专用硬件单元,专门做矩阵乘法:
Tensor Core 执行小矩阵分块的 multiply-accumulate:
D = A × B + C
支持的指令形状与输入/累加精度取决于 GPU 架构
H100 第四代 Tensor Core:
- 支持 FP8, FP16, BF16, TF32, FP64
- 每个 SM 有 4 个 Tensor Core
- 峰值吞吐还取决于数据类型、稠密/2:4 稀疏模式和时钟
TF32 (TensorFloat-32): A100 引入
1 位符号 + 8 位指数 + 10 位尾数 = 19 bits
范围和 FP32 一样(8 位指数),精度稍低(10 vs 23 尾数)
但用 Tensor Core 加速!
PyTorch 中应显式选择期望的 float32 matmul 精度策略:
torch.set_float32_matmul_precision("high")
旧接口也可控制 CUDA matmul 的 TF32 行为;默认值会随版本变化,
不应假定所有环境都自动开启。
Global memory 请求会被合并为尽可能少的内存事务;现代 NVIDIA GPU 的缓存行由 32-byte sectors 组成。warp 访问连续且对齐的地址时,所需 sector 更少:
Good: Coalesced (连续访问)
Thread 0 → address 0
Thread 1 → address 4
Thread 2 → address 8
... → 通常只需覆盖一个连续的 128B 区域
Bad: Strided (跨步访问)
Thread 0 → address 0
Thread 1 → address 1024
Thread 2 → address 2048
... → 可能触及大量分散的 sectors,浪费带宽
Shared memory 被分为 32 个 bank,每个 bank 宽 4 字节:
如果 warp 中多个线程访问同一 bank 的不同地址
会发生 bank conflict,串行化访问
Good: 无 conflict
Thread 0 → bank 0
Thread 1 → bank 1
...
Bad: 2-way bank conflict
Thread 0 → bank 0, address 0
Thread 16 → bank 0, address 1024
这两个访问会被串行化
| 规格 | A100 80GB(dense) | H100 SXM 80GB(dense) | H100 SXM(2:4 sparse) |
|---|---|---|---|
| FP64 CUDA cores | 9.7 TFLOP/s | 34 TFLOP/s | — |
| FP64 Tensor Core | 19.5 TFLOP/s | 67 TFLOP/s | — |
| FP32 CUDA cores | 19.5 TFLOP/s | 67 TFLOP/s | — |
| TF32 | 156 TFLOP/s | 494 TFLOP/s | 989 TFLOP/s |
| BF16 | 312 TFLOP/s | 989 TFLOP/s | 1979 TFLOP/s |
| FP8 | — | 1979 TFLOP/s | 3958 TFLOP/s |
| HBM | 80 GB | 80 GB | 80 GB |
| HBM 带宽 | 2.0 TB/s | 3.35 TB/s | 3.35 TB/s |
| L2 Cache | 40 MB | 50 MB | 50 MB |
| SM 数 | 108 | 132 | 132 |
| TDP | 400W | 700W | 700W |
表中 Tensor Core 稀疏列要求满足 NVIDIA 的 2:4 structured sparsity;不能把该峰值当作任意稀疏模型都能获得的吞吐。
Spring 2026 官方 Lecture 5 明确把 TPU 放进 GPU 旁边比较。两者都把轻量控制、大型矩阵单元、片上快速存储与 HBM组合在一起,但 TPU 的核心抽象更偏向编译器调度的大块矩阵程序,而不是 GPU 的 warp/SIMT 线程层次。
| 维度 | GPU | TPU |
|---|---|---|
| 主要计算单元 | SM 内 CUDA cores + Tensor Cores | MXU(Matrix Multiply Unit,通常以 systolic-array 思路实现)+ VPU |
| 执行抽象 | thread → warp → block → grid;SIMT 与分支发散很重要 | 较粗粒度 block/program;没有 GPU 式 warp 抽象 |
| 片上存储 | register、shared memory/L1、L2 | vector memory / on-chip buffers,围绕 MXU 数据流组织 |
| 软件栈 | CUDA、Triton、PyTorch compiler | JAX/XLA 常把模型与 sharding 一起交给编译器 |
| 扩展方式 | NVLink/NVSwitch + InfiniBand/RoCE | TPU interconnect / pod;同样需要拓扑感知的 sharding |
| Workload fit | 从 irregular custom kernels 到 dense matmul 都有较成熟的显式控制路径 | 规则、静态、矩阵密集且可由 compiler 整体规划的数据流更容易发挥矩阵单元 |
CourseStack 扩展边界:systolic array、JAX/XLA、ICI/NVLink 细节是用于建立工程对照的补充,不应被误读为 Lecture 5 对具体 TPU generation 的完整规格说明。官方讲义的核心结论是 GPU 与 TPU 都受矩阵单元、HBM 带宽和互联约束,但编程抽象与网络组织不同。具体 sharding 选择在第 7–8 课继续展开。
仅用于展示 units:GPT-3 175B 报告的训练计算量约 3.14e23 FLOPs,
# 1024 张 GPU 全程保持 MFU=0.5,且忽略停机、评估和 I/O。
# A100 (312 TFLOP/s dense bf16, MFU=0.5):
# 单卡实际: 156 TFLOP/s
# 1024 卡: 1.6e17 FLOP/s
# 时间: ~22.8 天
# H100 (989 TFLOP/s dense bf16, MFU=0.5):
# 单卡实际: 495 TFLOP/s
# 1024 卡: 5.07e17 FLOP/s
# 时间: ~7.2 天
# 峰值 dense bf16 比值约 3.17×;真实端到端加速不会只由峰值算力决定。
NVLink (节点内 GPU 间)
H100: 18 条 NVLink 4.0, 每条 50 GB/s
总带宽: 900 GB/s (双向)
NVSwitch fabric
在 HGX/DGX H100 中连接多张 SXM GPU,支持 GPU 间高带宽通信
900 GB/s 是每张 H100 的聚合双向 NVLink 带宽,
不能直接解释为任意一对 GPU 的单向 payload 带宽。
InfiniBand / RoCE (节点间)
一条 400 Gb/s 链路的理论 line rate = 50 GB/s;
实际节点可有多张 NIC,应用带宽还会受协议与拓扑影响。
这意味着:
通信密集的 tensor/expert parallel 通常优先放在高带宽域内;
data parallel 是否跨节点仍取决于通信量、网络和并行分组。
| 层次 | 带宽 | 典型用途 |
|---|---|---|
| H100 NVLink (节点内) | 每 GPU 聚合双向 900 GB/s | Tensor / Expert Parallel |
| NVSwitch fabric | 取决于系统拓扑与交换结构 | 节点内 collective 与 peer traffic |
| InfiniBand (节点间) | 按 NIC 数量与代际变化;400 Gb/s = 50 GB/s line rate | 跨节点 collective |
// 最底层,最灵活,最复杂
__global__ void vector_add(float *a, float *b, float *c, int n) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i < n) {
c[i] = a[i] + b[i];
}
}
// 启动 kernel
vector_add<<<num_blocks, block_size>>>(d_a, d_b, d_c, n);
# Python 风格的 GPU kernel DSL;仍需设计 program shape 与访存
@triton.jit
def vector_add_kernel(x_ptr, y_ptr, out_ptr, n, BLOCK: tl.constexpr):
pid = tl.program_id(0)
offsets = pid * BLOCK + tl.arange(0, BLOCK)
mask = offsets < n
x = tl.load(x_ptr + offsets, mask=mask)
y = tl.load(y_ptr + offsets, mask=mask)
tl.store(out_ptr + offsets, x + y, mask=mask)
# 不手写 kernel,让编译器尝试图优化与融合
@torch.compile
def fused_operation(x, w):
return gelu(x @ w) # 是否做 epilogue/kernel fusion 取决于后端与形状
| 方法 | 抽象级别 | 控制范围 | 主要取舍 |
|---|---|---|---|
| 手写 CUDA | 最低 | 最大 | 工程成本高;不保证自动快于成熟库 |
| Triton | 中 | program/block 级 | 更易实验自定义 kernel,仍需调优 |
| torch.compile | 高 | 计算图与编译后端 | 低改造成本,收益依赖图、形状和后端 |
| PyTorch eager | 高 | 算子组合 | 开发直观;算子边界可能产生额外 launch 与中间流量 |
问题 1:按 dense BF16 峰值估算,H100 SXM 的理想 ridge point 大约是多少?
问题 2:为什么 kernel fusion 能加速 memory-bound 操作?
问题 3:GPU 中的 warp 是什么?
问题 4:TF32 和 FP32 的区别是什么?
问题 5:为什么 expert parallel 通常优先放在节点内的高带宽域?
BF16 y=x+z 每元素约 1 FLOP,最少读写 6 bytes。算术强度是多少,增加 Tensor Cores 能直接解决吗?
答案:约 \(1/6\) FLOP/byte;主要瓶颈是数据流量时,增加计算峰值不会消除 HBM bytes。
若只因 FLOPs 少便判断“必然快”,你遗漏了 roofline 的 bandwidth ceiling。
硬件层次给出了限制;下一课把一个 PyTorch expression 拆成 kernel launches,亲手用 benchmark、fusion 和 tiling改变数据路径。