读 FlashAttention、KV Cache 这类文章,你会反复碰到「线程」「核心」「寄存器」「内存」。这一节从头讲清楚这些概念,不需要任何硬件背景。
1. 程序是怎么运行的?
你写了一行 Python:c = a * b,计算机到底做了什么?
所以「内存→寄存器→运算→结果」是所有计算的基本流程。寄存器就是运算单元手边的小格子,数字在里面才能被运算器直接操作。
2. CPU 是什么?
CPU(Central Processing Unit,中央处理器)就是一块能按顺序执行指令的芯片。你可以把它理解为:
每个「核心」(Core)相当于一个厨师,能处理复杂的任务(条件判断、循环、函数调用…),
现代 CPU 有 4–128 个核,每个核一次只做一件事,但做得非常快、非常灵活。
CPU 很快的原因之一是有很大的「L1/L2/L3 Cache」(缓存),这和 GPU 的 SRAM 是类似的概念——把最近用过的数据缓存在芯片上,减少去内存取数的次数。
3. 线程是什么?
「线程」(Thread)是程序执行的最小单位——你可以把它理解为:
每个线程有自己的「当前在执行哪行代码」(程序计数器)和「私有变量」(寄存器/栈)。
多个线程可以同时运行——比如浏览器打开网页时,一个线程下载 HTML,另一个线程渲染页面,互不干扰。
4. GPU 的线程怎么组织的?
GPU 里线程是分级管理的,从小到大依次是:
5. 为什么 GPU 需要这么多线程?
从 HBM 取一个数需要 ~600 cycle(相当于 CPU 等 600 个时钟周期什么都不做)。 GPU 的解法是:准备几千个 Warp,当 Warp A 在等内存时,调度器立刻切换去执行 Warp B、C、D…… 等 A 的数据到了,再切回来继续。靠「轮流等待、从不空闲」来填满这 600 cycle 的空档。
上一节讲了概念,这里用一个极简但真实的例子走完全程:
C[i] = A[i] + B[i],计算 4096 个元素的向量加法。
这是 GPU 上最典型的操作,理解了这个,后面 FlashAttention 的数据流就都清楚了。
第一步:你写了一段 Python / CUDA 代码
C = A + B # A, B, C 都是 shape=(4096,) 的 tensor,在 GPU 显存里
# 背后 CUDA kernel 做的事(伪代码):
__global__ void vector_add(float* A, float* B, float* C) {
int i = blockIdx.x * blockDim.x + threadIdx.x; # 每个线程知道自己的编号
C[i] = A[i] + B[i]; # 每个线程只负责一个位置
}
关键思路:4096 个元素,GPU 同时启动 4096 个线程,每个线程算一个。不是一个接一个算,是全部同时算。
第二步:GPU 把线程按层级组织起来
第三步:SM 是什么?(它才是 GPU 里真正的「核」)
第四步:线程 #5 计算 C[5] = A[5] + B[5] 的完整旅程
以线程 #5(在 Warp 0,Block 0,被调度到 SM #3)为例,走完所有步骤:
第五步:时间线视角——Warp 调度如何隐藏延迟
刚才我们说第①步需要 600 cycle,第③步只需要 1 cycle。那 600 cycle 里 SM 在干什么?答案是:切换去执行其他 Warp。
CPU/GPU 内部有一个「时钟」,每隔固定时间发出一次「滴答」信号,驱动所有电路同步工作。这个「滴答一次」就是 1 cycle。
A100 GPU 时钟频率 ≈ 1.4 GHz,即每秒 14 亿次滴答,1 cycle ≈ 0.7 纳秒。
计算机里所有数字都是二进制(只有 0 和 1)。加法电路的核心是一个叫 全加器(Full Adder)的电路,由几个逻辑门(AND / XOR / OR)搭成,不需要任何程序指令,纯粹是电压传播。下面是 1+1=10(二进制)的完整过程:
两个输入相同时输出 0
0 XOR 1 = 1 | 1 XOR 1 = 0
否则输出 0
0 AND 1 = 0 | 1 AND 1 = 1
两个都是 0 才输出 0
0 OR 1 = 1 | 1 OR 1 = 1
二进制写法:10 = 十进制 2 ✓ (1 + 1 = 2,正确!)
整个过程就是电压在 5 个逻辑门之间依次传播,没有任何程序指令,1 个 cycle 内完成。
为什么叫「纯电路」?因为加法的答案不是「查表」「执行指令」得到的,而是电压在导线和晶体管里传播,到达输出端时物理上就是正确答案。整个 A100 GPU 同时有数千个这样的加法器在工作。
32-bit 浮点数的加法器比 1-bit 版本复杂得多(需要处理符号位、指数位、尾数位),但原理一样:都是逻辑门的组合电路,不经过任何「软件」。
你问的「寄存器是不是多个二极管」——方向对,但材料是晶体管(不是二极管)。层级关系如下:
汇总:各步骤时间成本对比
| 步骤 | 做什么 | 延迟 | 谁在等 |
|---|---|---|---|
| ① 从 HBM 读数据 | A[5], B[5] → 进入 SM(寄存器或 SRAM) | ~600 cycle | Warp 0 挂起,SM 去执行其他 Warp |
| ② SRAM → 寄存器 | 从片上缓存装入线程私有寄存器 | ~20 cycle | 极短,几乎可忽略 |
| ③ 加法运算 | R1 + R2 → R3,纯电路(见下方解释) | 1 cycle ⚡ | 无等待,不碰任何存储 |
| ④ 写回 HBM | R3(结果)→ HBM C[5] | ~600 cycle | 同样 Warp 挂起,SM 去执行其他 Warp |
真正的计算(第③步)只要 1 cycle,但数据搬运(第①④步)各需要 600 cycle。 优化 GPU 性能 = 减少①④的次数,或者用多 Warp 轮流来填满①④的等待时间。 这就是为什么 FlashAttention 的核心改进是「减少 HBM 读写次数」而不是「让乘法更快」。
理解 LLM 推理优化(FlashAttention、KV Cache 量化等)首先需要搞清楚一件事:数据存在哪里,如何到达计算单元。
| 层级 | 名称 | 容量 | 带宽 / 延迟 | 位置 | 类比 |
|---|---|---|---|---|---|
| ① 寄存器 | Register File | 每线程 ~256 个 32-bit | 无限快(直接接运算器) | SM 内部,每线程私有 | 手里正拿着的东西 |
| ② SRAM | 片上 L1 Cache / Shared Memory | 每 SM ~256 KB;全 GPU ~20 MB | 10–20 TB/s · <5 ns | SM 内部,同 SM 线程共享 | 工作台 |
| ③ HBM | High Bandwidth Memory(显存) | 40–80 GB | ~2 TB/s · ~100 ns | GPU 封装内,所有 SM 共享 | GPU 的仓库 |
| ④ 主机内存 | DRAM / RAM | 16–512 GB | 50–100 GB/s · ~100 ns | CPU 主板上 | CPU 的仓库 |
| ⑤ 磁盘 | SSD / NVMe | TB 级 | 2–10 GB/s · ~100 μs | 独立存储设备 | 档案室 |
因为 Tensor Core 算力达到 312 TFLOPS(A100,fp16),即每秒 312 万亿次运算。假设每次运算消耗 2 bytes 数据(fp16),理论上需要 312×2 = 624 TB/s 的带宽才能喂饱算力——但 HBM 只有 2 TB/s,相差 312 倍。
所以 GPU 大量时间在空转等待读数据,而非真正在算。这就是「内存墙(Memory Wall)」。FlashAttention 的本质就是减少无谓的 HBM 读写,让 Tensor Core 少等待。
一块 GPU 不是一个单一的大型计算器,而是由数百个独立的「小型计算单元」组成,每个叫 SM(Streaming Multiprocessor,流式多处理器)。
- 若干 CUDA Core(通用运算)
- 若干 Tensor Core(矩阵加速)
- 专属 SRAM(L1 Cache + Shared Memory)
- 寄存器文件(每个线程私有)
- 加减乘除(标量运算)
- softmax、激活函数(逐元素)
- 条件判断、数据类型转换
- 矩阵乘法 Q@K.T → Tensor Core
- 线性层 X@W → Tensor Core
- softmax、ReLU → CUDA Core
GPU vs CPU 的根本差异
适合:复杂的控制流(if/else/while),单线程延迟敏感任务,不规则数据访问。
适合:所有线程做完全相同的操作(如矩阵乘法),数据规则、分支极少的大规模并行计算。
GPU 里的线程不是单独调度的,而是以 32 个线程为一组,称为一个 Warp。同一个 Warp 里的所有线程在任意时刻都执行 同一条指令(SIMT:Single Instruction Multiple Threads)。
如矩阵乘法,每个线程计算输出矩阵的一个元素,操作完全相同。32 个线程同时发出同一条乘加指令,Tensor Core 以最高吞吐量运行。
线程 1: C[1] += A[1] * B[1]
……(32线程同时执行同一指令)
如果 32 个线程里有些走 if、有些走 else,GPU 只能串行处理:
result = sqrt(x)
else: ← 其余线程等待
result = 0
↓ 先跑 if 分支,else 线程等待
↓ 再跑 else 分支,if 线程等待
→ 总时间 = if时间 + else时间
RabitQ 的格点搜索需要 binary search + recursive subdivision,充满条件分支。大量 Warp Divergence 让 GPU 变成「串行执行器」。而 FlashAttention / TurboQuant 的量化/矩阵乘法几乎没有分支,所有线程做完全相同的操作,GPU 满负荷并行。
计算机体系结构中,「内存墙」是指 CPU/GPU 的算力增长速度远快于内存带宽的增长速度,导致处理器大量时间在等待数据,而非真正在计算。
具体数字(A100)
| 指标 | 数值 | 含义 |
|---|---|---|
| Tensor Core 算力(fp16) | 312 TFLOPS | 每秒 3120 亿次 fp16 乘加 |
| HBM 带宽 | 2 TB/s | 每秒最多读 2 TB 数据 |
| 喂饱算力需要的带宽 | ~624 TB/s | 312T ops × 2B = 624 TB |
| 差距 | ~312 倍 | 算力是带宽的 312 倍 |
「算术强度」= 计算量(FLOPs) / 数据量(Bytes),单位是 FLOP/Byte。
A100 的「峰值算术强度」(roofline 拐点)约为:312 TFLOPS / 2 TB/s = 156 FLOP/Byte。
- 大矩阵乘法(如 m=n=k=4096):算术强度 ≈ 2×4096 = 8192 FLOP/Byte → 远超 156,Tensor Core 是瓶颈,内存足够
- 向量加法(C = A + B):算术强度 = 1 FLOP / 6 Byte ≈ 0.17 → 远低于 156,带宽是瓶颈
- 标准 Attention(n=4096):S 矩阵 32 MB,O 矩阵 1 MB,反复读写 HBM → 带宽密集型,是瓶颈
FlashAttention 通过 Tiling 把 Attention 变成「计算密集型」操作,让 Tensor Core 不再等带宽。
寄存器是什么?
寄存器(Register)是 GPU 里离运算单元最近、速度最快的存储——事实上它根本不是「内存」, 而是运算电路本身的一部分,是直接连在加法器/乘法器旁边的几个「小格子」, 用来临时存放当前正在参与运算的数字。
寄存器和 SRAM 有什么区别?
- 每个线程私有,别的线程看不到
- 存放「这个线程正在算的中间值」
- 就在乘法器旁边,0 延迟传输
- A100 每个 SM 约 256KB 寄存器文件
- 每线程最多 255 个(超出则 spill 到 SRAM)
- 同一 SM 内所有线程共享
- 存放「这批线程都要用的数据块」(如 Q/K tile)
- 需要显式加载(__shared__ 变量)
- A100 每个 SM 最大 192 KB
- 比寄存器慢一点,但远快于 HBM
图2:一次矩阵乘法里,寄存器在哪里工作?
乘法电路只操作寄存器里的值——寄存器就在乘法器旁边,相当于「随手一摸就到」。 真正慢的是第①步(从 HBM 把数据搬来),这需要 600+ cycle; 第③步只需要 1 个 cycle,快 600 倍。
所以「优化 GPU 计算」的本质就是:减少去 HBM 取数据的次数。
寄存器溢出(Register Spilling)是什么?
每个线程最多用 255 个寄存器。如果一个线程需要的中间变量太多,放不下,就会「溢出」到 SRAM(更慢),SRAM 也放不下则溢出到 HBM(更更慢)。这叫 register spilling,会显著拖慢 kernel 速度。
~1 cycle
~20 cycle
~600 cycle
跟 FlashAttention 的关系
每次只算一个 tile(如 64×64),tile 内的 S_tile、P_tile 结果只有几 KB,寄存器 + SRAM 放得下, 计算完直接继续,永不写 HBM。
S 矩阵是 4096×4096 fp16 = 32 MB,寄存器和 SRAM 根本装不下, 必须写到 HBM,下次 softmax 时再从 HBM 读回——往返 2 次,这才是瓶颈。
HBM 是书架(大但远),SRAM 是桌上的便签本(小但近),寄存器是手里拿着的那张纸(最小但随手就用)。 GPU 优化的终极目标:让运算员尽量只用手里的草稿纸,少去书架取书。
CPU(主机)和 GPU(设备)通过 PCIe(Peripheral Component Interconnect Express)总线 连接。
| 通信链路 | 带宽 | 典型场景 |
|---|---|---|
| PCIe 4.0 × 16(CPU ↔ GPU) | ~32 GB/s 双向 | 模型权重上传、推理结果下载 |
| PCIe 5.0 × 16(新平台) | ~64 GB/s 双向 | 下一代服务器 |
| NVLink(GPU ↔ GPU,A100) | ~600 GB/s 双向 | 多 GPU All-Reduce 梯度同步 |
| NVLink(H100) | ~900 GB/s 双向 | 超大模型张量并行 |
把一个 7B 参数模型(fp16,~14 GB)从 CPU 内存上传到 GPU 显存,通过 PCIe 4.0 至少需要 14 GB / 32 GB/s = ~0.44 秒。这就是为什么模型一般只在启动时加载一次,推理期间权重常驻 GPU。
多 GPU 训练为什么需要 NVLink?
以 8 块 A100 训练一个大模型,每次反向传播需要同步所有 GPU 的梯度(All-Reduce)。假设梯度数据量 14 GB:
- 走 PCIe:14 GB / 32 GB/s × 8 = 3.5 秒(严重拖慢训练)
- 走 NVLink:14 GB / 600 GB/s × 8 = 0.19 秒(可接受)
NVLink 让多 GPU 之间的通信带宽接近单 GPU 内部的 HBM 带宽,这是大模型训练能 Scale 到千卡的关键基础设施。
| 指标 | 数值 | 备注 |
|---|---|---|
| Tensor Core 算力(fp16) | 312 TFLOPS | 矩阵乘法峰值 |
| HBM 容量 | 80 GB | 存模型权重、KV Cache 等 |
| HBM 带宽 | 2 TB/s | 实际利用率约 70–80% |
| SRAM 总量 | ~40 MB | 108 SM × ~256 KB(含 L2 Cache ~40 MB) |
| SRAM 带宽 | ~10–20 TB/s | HBM 的 5–10 倍 |
| SM 数量 | 108 | 每 SM 64 FP32 CUDA Core + 4 Tensor Core |
| 线程总数(最大) | 221,184 | 108 SM × 2048 线程/SM |
| Warp 大小 | 32 线程 | 固定,所有 NVIDIA GPU 相同 |
| PCIe 带宽 | ~64 GB/s | PCIe 4.0 × 16,双向 |
| NVLink 带宽 | 600 GB/s | 双向,多 GPU 互联 |
- LLM 推理优化笔记 — FlashAttention 如何利用 SRAM Tiling,TurboQuant 量化如何降低 HBM 带宽压力