大模型知识体系 第 15 章 🎮 交互版(含动画演示与测验)
← Scaling Laws 与算力:为什么这样配置训练🏠 回到交互版首页Distributed Training:从 DDP 到 3D 并行 →
你正在浏览静态阅读版(无 JavaScript 也可阅读,便于搜索与 AI 抓取)。交互演示与答题功能请前往 交互版。
第 15 章 · 第三部分 · 扩展 (Scale)

GPU Fundamentals:从 SM 到 Roofline

SM/Tensor Core/warp、存储层次、算术强度、Roofline、GEMM 与 kernel fusion、Profiling 入门。

对应课件:LLM Systems(新增)

本章定位:Scale 主线的地基。不理解 GPU 的算力与带宽模型,后面 FlashAttention、分布式训练、推理优化都只能「背结论」。本章目标:让你看到任何算子都能问出「它是 compute-bound 还是 memory-bound」。

📘 代码类型约定(Build / Systems 章节统一)
  • 【Runnable】可直接运行(关键逻辑已在本课程验证脚本中实测)
  • 【Skeleton】工程骨架:逻辑完整,需要自备数据/环境/权重
  • 【Pseudo-code】算法示意:用于解释思路,不保证可直接运行

15.1 为什么必须懂 GPU

先懂直觉

训练 LLaMA-2-7B 要 8.4e22 FLOPs。做一道数量级估算(注意:下面 CPU 的 1 TFLOPS 是为了直觉而设的教学假设,真实 CPU 训练吞吐取决于 SIMD 宽度、核心数与内存带宽):按 1 TFLOPS 算需要

8.4×1022÷1012=8.4×1010 秒≈2660 年8.4\times10^{22} \div 10^{12} = 8.4\times10^{10}\ \text{秒} \approx 2660\ \text{年}

而用 1000 张 A100 只要 9 天。GPU 不是「更快的 CPU」,而是完全不同的计算哲学——理解它的强项和瓶颈,才能理解后面所有的系统优化。

本章回答四个问题:

问题 答案在哪
GPU 为什么快? 15.2~15.3:吞吐优先的架构
什么限制了它? 15.4~15.5:带宽与 Roofline
为什么矩阵乘是核心? 15.6:GEMM 与 Tensor Core
怎么知道瓶颈在哪? 15.7~15.8:Kernel 融合与 Profiling

15.2 CPU vs GPU:延迟 vs 吞吐

先懂直觉

CPU 是「几个博士生」——聪明的核心,擅长复杂的串行逻辑;GPU 是「几千个小学生」——单个很笨,但几千人同时搬砖的吞吐量惊人。LLM 的计算恰好是「大量简单重复的乘加」,完美匹配 GPU。

CPU GPU(A100)
核心数 8~64 个复杂核心 108 个 SM / 6912 个 CUDA Core
单核能力 强(分支预测、乱序执行、大缓存) 弱(简单流水线)
设计目标 低延迟(一件事尽快做完) 高吞吐(单位时间做最多事)
内存带宽 50200 GB/s 2039 GB/s(A100 80GB HBM2e)
BF16 算力 0.52 TFLOPS(量级示意) 312 TFLOPS(Tensor Core,dense)
适合 操作系统、分支复杂逻辑 矩阵乘、逐元素运算

15.3 GPU 硬件结构:从 SM 到 warp

先懂直觉

GPU 的层级组织像「公司 → 部门 → 小组 → 员工」:一个 GPU 有多个 SM(部门),每个 SM 有一批 CUDA Core / Tensor Core(员工),线程按 32 个一组(warp,小组)统一行动。

GPU
├── SM 0(流多处理器)
│   ├── 128 × CUDA Core(通用浮点/整数计算)
│   ├── 4 × Tensor Core(矩阵乘专用,算力是 CUDA Core 的 8~16 倍)
│   ├── Shared Memory / L1(~192 KB,片上)
│   └── Register File(256 KB)
├── SM 1 ... SM 107
└── L2 Cache(40 MB,片内共享)
    └── HBM 显存(40~80 GB,片外)
概念 含义 一句话记忆
SM 流多处理器,GPU 的「部门」 A100 有 108 个
CUDA Core 通用计算单元 一个周期一次 FMA
Tensor Core 矩阵乘单元(4×4 / 16×16 块乘加) 同样的周期算多得多
thread / block / grid 线程 / 线程块 / 网格 软件层的组织方式
warp 32 个线程一组,统一调度 最小执行单位
SIMT Single Instruction, Multiple Threads 一条指令 32 个线程同时执行
🧮 用数字感受 Tensor Core 的威力

A100 单个 SM 的 BF16 算力:4 个 Tensor Core 每秒约 2.9 TFLOPS → 108 个 SM ≈ 312 TFLOPS。 如果用 CUDA Core 做同样的矩阵乘:只有约 19.5 TFLOPS(FP32)。 16 倍差距——这就是「大模型必须用 Tensor Core」的原因,也是低精度(bf16)训练流行的原因。

📘 SIMT 与分支

warp 里的 32 个线程执行同一条指令。如果代码有 if 分支导致线程走不同路径,warp 会串行执行两个分支(利用率减半)——所以 GPU 讨厌复杂控制流,喜欢整齐的向量/矩阵运算。

15.4 存储层次:GPU 世界的「内存墙」

先懂直觉

GPU 算得极快,但数据住在离计算单元很远的 HBM 里。计算单元等数据的时间可能比算的时间还长——这就是「内存墙」。很多高性能 kernel(FlashAttention 等)的优化思路正是:让数据尽量待在离计算近的地方。

                容量        带宽         延迟
Register      256 KB/SM   ~20 TB/s     1 cycle        ← 最快,线程私有
Shared/L1     192 KB/SM   ~19 TB/s     ~30 cycles     ← 片上共享
L2 Cache      40 MB        ~7 TB/s     ~200 cycles    ← 片内全局
HBM 显存      40~80 GB    2 TB/s       ~400 cycles    ← 最慢,但最大

把「1 次 HBM 访问」当作单位时间

操作 相对耗时 类比(若 HBM = 1 秒)
寄存器访问 0.0025 瞬间
Shared memory 0.075 眨眼
L2 0.5 半秒
HBM 1 1 秒
一次矩阵乘(算 100 次) — 干活的时间

关键洞察:从 HBM 读一个数的时间,够计算单元算几十次。所以优化方向往往是「减少 HBM 读写」而不是「减少计算量」。

⚠️ 常见误判

「卡是因为 GPU 算不过来」——多数时候是错的。绝大多数 LLM 算子(softmax、LayerNorm、激活、逐元素加法、朴素 attention)都是带宽瓶颈,算力在闲置。判断方法就是下一节的 Roofline。

15.5 算术强度与 Roofline 模型

先懂直觉

一个算子到底受限于带宽还是算力?答案取决于它的「算术强度」:每搬运 1 字节数据,能做多少次浮点运算。算术强度低 → 等数据(memory-bound);高 → 等计算(compute-bound)。

公式

算术强度 AI=FLOPsBytes moved,性能上限=min⁡(Ppeak, BW×AI)\text{算术强度 } AI = \frac{\text{FLOPs}}{\text{Bytes moved}}, \qquad \text{性能上限} = \min\left(P_{peak},\ BW \times AI\right)

关键概念:拐点(ridge point)

AI∗=PpeakBW=312×10122039×109≈153 FLOPs/ByteAI^* = \frac{P_{peak}}{BW} = \frac{312 \times 10^{12}}{2039 \times 10^9} \approx 153\ \text{FLOPs/Byte}

常见算子的算术强度(教学量级近似)

假设约定:以下按 bf16、计入 HBM 读+写流量估算;不同实现差异很大(融合程度、是否物化中间矩阵等),表中数值只用于判断瓶颈方向,不是精确测量值。

算子 AI(量级) 判定 优化方向
向量加法(读 2 写 1) ~0.2 memory-bound 融合、减少读写
GELU / 逐元素激活 ~1 memory-bound 融合进邻近算子
Softmax(每行) ~1 memory-bound 在线 softmax、融合
LayerNorm ~1.3 memory-bound 融合
朴素 Attention 数 ~ 数十 偏 memory-bound FlashAttention
FlashAttention 数十以上 接近/越过拐点 —
大 GEMM(M=N=K 大) 100~1000+ compute-bound Tensor Core、低精度
🎮 交互:算术强度与瓶颈判断交互演示 · 静态阅读版

拖动滑块改变算子算术强度,或直接点选常见算子,观察它在 Roofline 图上的位置:左侧「斜线墙」是带宽瓶颈,右侧「平顶」是算力瓶颈。(演示中的 AI 数值同为教学近似。)

在交互版中打开这个演示 →

🧮 一个具体算例:为什么 LayerNorm 慢(理想下界估算)

LayerNorm 处理 [B, S, d] = [8, 512, 4096],bf16:

  • 元素数 = 8×512×4096≈1.68×1078 \times 512 \times 4096 \approx 1.68\times10^7
  • 内存流量(读入 + 写出)= 2×1.68×107×2 B≈67 MB2 \times 1.68{\times}10^7 \times 2\ \text{B} \approx 67\ \text{MB}
  • 计算量 ≈ 每元素约 5 FLOPs → ≈8.4×107\approx 8.4\times10^7 FLOPs
  • AI=84e6/67e6≈1.25AI = 84\text{e}6 / 67\text{e}6 \approx 1.25 → 远低于拐点 153 → memory-bound
  • 内存时间下界 = 67 MB ÷ 2039 GB/s ≈ 33 微秒
  • 计算时间 = 84 MFLOPs ÷ 312 TFLOPS ≈ 0.27 微秒(比内存时间小约 120 倍)

(这是假设带宽 100% 利用率的理想下界;真实 kernel 通常达到峰值带宽的 60%~80%。)

结论:LayerNorm 再省计算也没用,优化方向主要是少读写(融合进邻近算子、避免多余的中间张量)。

15.6 为什么 GEMM 快:分块与 Tensor Core

先懂直觉

在典型 dense Transformer 训练中,大型 GEMM 通常是主要的 compute-bound 工作负载之一。它快的秘诀是「分块(tiling)」:把大矩阵切成小块,反复复用搬到 shared memory / register 里的小块——同一份数据用很多次,掩盖了 HBM 的慢。

朴素做法:每算一个输出元素读 2K 个数 → AI 很低
分块做法:把 M×K 和 K×N 切成 B×B 小块
          每块加载一次到 shared memory,被复用 B×B 次
          → AI ≈ B(块越大复用越多)
技术 作用
分块(tiling) 数据复用,提高算术强度
Tensor Core 一次算 4×4 / 16×16 的乘加块
双缓冲(double buffering) 算当前块时预取下一块
cuBLAS / CUTLASS 官方高度优化实现(大模型都直接调用)
📘 从 GEMM 视角看「为什么矩阵乘快」(标准概念,不用黑话)
  • GEMM 的三个维度 M / N / K:决定计算量(2MNK2MNK FLOPs)与数据复用空间;
  • Tensor Core 可用条件:数据类型(如 bf16/fp16)与矩阵形状满足对齐要求(常见实现偏好 16 或 64 的倍数)——形状不对齐会退回到更慢的路径;
  • Tile(分块)大小:决定 shared memory / register 的复用率与 occupancy(占用率),是 cuBLAS / CUTLASS 调优的核心旋钮;
  • 算术强度:大 GEMM(M、N、K 都大)的 AI 高,属于 compute-bound;这也是它成为训练主力的原因。
  • 实际训练中,GEMM 类 kernel 通常占前向+反向耗时的大部分(经验值约 70%~90%,随模型结构与实现变化)。

15.7 Kernel 与融合:省的是带宽

先懂直觉

每次算子启动(kernel launch)都要把数据从 HBM 读进来、算完写回去。很多小算子叠在一起,数据被反复搬运。Kernel 融合把它们合成一个大 kernel,数据在片上就完成多步计算——省的全是带宽。

例子:GELU + 残差加法的两种写法

# ① 不融合:两次读 HBM、两次写 HBM
h = gelu(x)          # 读 x(4MB) → 写 h(4MB)
y = h + residual     # 读 h(4MB) + 读 residual(4MB) → 写 y(4MB)  = 共 16MB 流量

# ② 融合:一次读、一次写
y = gelu_and_add(x, residual)   # 读 8MB → 写 4MB = 12MB 流量(省 25%)
现象 原因
torch.compile 让模型变快 自动做 kernel fusion
FlashAttention 快 把 QKᵀ、softmax、×V 融合为一个 kernel(FlashAttention 章节将在第二批上线,见第 16 章末的路线图)
逐元素操作「太慢」 每个算子各搬一次 HBM

15.8 Profiling 入门:找到瓶颈

先懂直觉

不要猜性能。用工具看:时间花在哪个 kernel、它是带宽还是算力瓶颈。这是把「训练慢」变成「知道为什么慢」的唯一途径。

两把常用工具

工具 看什么 用法
torch.profiler 每个算子的耗时占比、CUDA kernel 时间 with torch.profiler.profile(...)
Nsight Systems / Nsight Compute 时间线、kernel 细节(利用率/带宽) 命令行/图形界面
import torch
from torch.profiler import profile, ProfilerActivity

with profile(activities=[ProfilerActivity.CPU, ProfilerActivity.CUDA]) as prof:
    for _ in range(10):
        y = model(x)
        y.sum().backward()

print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=15))

判断清单(背下来)

观察 结论 行动
大量小 kernel、时间分散 启动开销/逐元素瓶颈 融合(torch.compile)
GPU 利用率低但显存带宽满 memory-bound 减少读写、提高复用
Tensor Core 利用率低 精度/形状问题 检查 bf16、矩阵尺寸对齐(64 的倍数)
等通信时间长 分布式通信瓶颈 见第 16 章(重叠、拓扑)
batch 太小 无法打满 增大 batch / 梯度累积

15.9 为什么 Transformer 特别适合 GPU

适合的部分:

结构 为什么适合
QKV 投影、FFN、LM Head 大 GEMM,compute-bound,Tensor Core 友好
批量多头注意力 形状整齐([B,H,S,D]),极易并行
序列内并行(训练) 各位置独立,没有 RNN 的串行依赖

不适合的部分(优化的重点):

结构 瓶颈
Softmax / LayerNorm / Dropout memory-bound,AI ≈ 0.3~2
朴素 Attention 的 S×S 矩阵 显式物化 S² 张量:读写 O(S²)
自回归生成 每步计算量小、kernel 启动多、KV 读写量大

一句话总结本章:GPU 是「吞吐机器」,矩阵乘是它的主场;但 LLM 里大量算子输在内存带宽上。理解 min(峰值, 带宽×AI) 这一个公式,就理解了后面 FlashAttention、量化、融合、显存优化的全部动机。

🔑 本章必须记住
概念 一句话
CPU vs GPU 延迟优先 vs 吞吐优先
SM / warp / Tensor Core 部门 / 32 线程组 / 矩阵乘单元
存储层次 Register > Shared > L2 > HBM(带宽差百倍)
算术强度 AI = FLOPs/Bytes
Roofline 性能 = min(峰值, 带宽×AI),拐点 AI*≈153
memory-bound AI 低 → 等数据(softmax/LayerNorm/朴素 attention)
compute-bound AI 高 → 等计算(大 GEMM)
Kernel 融合 省 HBM 读写,torch.compile 的核心收益
Profiling 先看时间线再优化,不猜
📝 小测验
A100 的 Roofline 拐点 AI* = 峰值 ÷ 带宽 ≈ 153 FLOPs/Byte。一个算子的算术强度是 2,它属于?
Acompute-bound
Bmemory-bound
C两者都不是
D取决于 batch size
显示答案与解析

正确答案:B

AI=2 远低于拐点 153,性能由带宽×AI 决定,属于 memory-bound。Softmax(约 1)、LayerNorm(约 1.3)、逐元素操作都在这一象限。
📝 小测验
为什么 FlashAttention 能大幅加速注意力计算,却没有改变 O(S²) 的复杂度?
A它用了近似算法
B它通过分块 + 在线 softmax,不在 HBM 中物化完整的 S×S 注意力矩阵,显著减少 HBM ↔ SRAM 的数据搬运
C它减少了 Q/K/V 的维度
D它跳过了 softmax
显示答案与解析

正确答案:B

朴素 attention 要把 S×S 的中间矩阵写入再读出 HBM,数据搬运量很大。FlashAttention 用 tiling + 在线 softmax 让中间结果留在片上 SRAM 中完成计算,避免物化完整 S×S 矩阵,HBM 访问大幅减少。注意:计算复杂度仍是 O(S²)——本课程只讲「减少 HBM 搬运」这个正确边界,精确的 I/O 复杂度分析留到第二批 FlashAttention 章节。
📝 小测验
代码里两个连续的逐元素算子(如 GELU 后接残差加法)比融合版本慢,主要原因是?
A计算量多了
B每个算子都要独立读写一遍 HBM,内存流量翻倍
CGPU 不支持多个算子
D精度损失
显示答案与解析

正确答案:B

逐元素算子算术强度极低,瓶颈在 HBM 读写。融合后中间结果留在片上,流量从 16MB 降到 12MB。
← Scaling Laws 与算力:为什么这样配置训练🏠 回到交互版首页Distributed Training:从 DDP 到 3D 并行 →
本章完。原始 Markdown:content/15-gpu.md