CUDA系统拆解-05-内存层级与访存本质:性能瓶颈为什么常卡在数据
CUDA系统拆解-05-内存层级与访存本质:性能瓶颈为什么常卡在数据
本文是「CUDA系统拆解」系列第 05 篇。
系列导读:CUDA系统拆解-00-导读:从编程模型到 AI 推理系统的学习路线
上一篇:CUDA系统拆解-04-warp、SIMT 与 SM:真实执行不是“线程各跑各的”
下一篇:CUDA系统拆解-06-共享内存与Coalescing:访存优化先抓哪几个抓手
1. 这篇解决什么问题
- 为什么很多 CUDA 程序明明计算不复杂,却还是很慢。
- 为什么 CUDA 必须设计成分层内存,而不是“所有内存都一样”。
register、local memory、shared memory、global memory各自解决什么问题。- 什么叫
register spill,为什么local memory往往不是你想象中的“近”。 - 为什么理解内存层级,是后面分析 coalescing、shared memory、fusion、GEMM、attention 的前提。
2. 先记住的核心结论
- 很多 CUDA kernel 首先是
memory-bound,也就是先受内存访问和数据搬运限制,而不是先受算术能力限制。 - CUDA 内存层级的核心矛盾是:越靠近计算单元,通常越快,但容量越小、成本越高。
register最快,但线程私有、数量有限;shared memory适合 block 内协作和数据复用;global memory容量大,但常常是主要瓶颈。local memory的“local”说的是可见性,不代表物理上更近;它经常意味着寄存器不够或线程私有对象太大。register spill的本质是本来想放在寄存器里的数据,被迫落到更慢的内存路径。- 真正该建立的不是“哪种内存快”的静态记忆,而是“数据应该怎样在这些层级之间流动”的思维。
3. 正文讲解
3.1 为什么访存常常比计算更关键
很多人一想到 GPU,会先想到:
- 核很多
- 并行强
- FLOPS 高
这些都对,但不够。
对 CUDA 来说,更关键的现实是:
算术运算本身未必贵,昂贵的往往是把数据拿过来、等它回来、再把结果写回去。
所以很多 kernel 的真实性能瓶颈并不是“乘加太慢”,而是:
- 从
global memory读得太多 - 访问模式不好
- 本该复用的数据被反复读取
- 中间结果过早写回,再被重新读出
这就是为什么很多 CUDA 程序首先是 memory-bound,而不是 compute-bound。
前者可以先理解成“受数据搬运和带宽限制”,后者则是“受计算单元本身限制”。
3.2 为什么一定要有分层内存
如果理想一点,我们当然希望:
- 所有线程都访问同一块大内存
- 它像寄存器一样快
- 容量还无限大
现实做不到,因为这里有天然冲突:
- 速度
- 容量
- 带宽
- 功耗
- 距离计算单元的远近
通常规律是:
- 越靠近计算单元,延迟越低、带宽越高
- 但容量越小、成本越高
所以 CUDA 的内存层级,本质上是在不同层之间做取舍。
它不是为了让你背术语,而是为了给你一个更清晰的数据流组织方式。
3.3 先把 CUDA 内存层级总图建立起来
初学阶段你最该抓住的是四层:
registerlocal memoryshared memoryglobal memory
可以先这样理解:
register
- 每个线程私有
- 最快
- 最稀缺
local memory
- 也是线程私有可见
- 但通常不是片上快存储
- 常常意味着寄存器放不下了
shared memory
- 一个 block 内线程共享
- 片上
- 适合协作和数据复用
global memory
- 所有线程都能访问
- 容量大
- 延迟高,带宽宝贵
除此之外还有 constant memory、texture memory 这类更特殊的路径,但对你当前这条学习线来说,优先级不如前四者高。
3.4 register:最快,但最稀缺
register 可以理解成线程手边的临时工作区。
它最适合放:
- 当前线程频繁使用的小变量
- 中间结果
- 累加值
为什么它这么重要?因为它体现了一个很核心的优化原则:
能不去更远的内存拿数据,就不要去。
如果数据已经在寄存器里:
- 不需要额外同步
- 不需要走更慢的内存路径
- 单线程局部计算会更顺畅
但寄存器有两个硬约束:
- 它只能线程私有,不能共享
- 数量有限,会影响一个 SM 上能同时挂多少线程和多少 warp
所以寄存器不是越多越好。
每线程寄存器占用一旦太高,就会:
- 压低并发
- 影响 occupancy
- 甚至触发
register spill
3.5 local memory 和 register spill 到底是什么
这是最容易被名字误导的地方。
local memory 的 local,说的是:
- 数据对线程私有可见
它不代表:
- 物理上就在这个线程附近
- 访问很快
很多时候,local memory 出现的原因是:
- 寄存器不够
- 线程私有数组太大
- 编译器无法把某些对象稳定留在寄存器里
这时就会发生 register spill。
也就是本来想待在寄存器里的数据,被挤到了更慢的路径上。
所以你要牢牢记住一句话:
local memory是“可见性局部”,不是“性能局部”。
理解这一点很重要,因为后面很多 kernel 变慢,不是因为算法逻辑变复杂了,而是因为寄存器压力太大,变量开始 spill。
3.6 shared memory:为什么它是 CUDA 最有代表性的设计之一
如果说寄存器是线程自己的口袋,那 shared memory 更像是 block 共用的一张工作台。
它的关键特性是:
- block 内线程共享
- 片上,延迟低、带宽高
- 需要程序员显式管理
它最核心的价值有两个。
第一,做数据复用。
如果一块数据会被一个 block 内很多线程反复用到,那么:
- 每个线程都去
global memory读一遍,会很浪费 - 先搬到
shared memory,再复用,通常更划算
第二,重组访问模式。
有时原始数据布局对 global memory 不友好,这时常见做法是:
- 先用较好的方式从
global memory读进来 - 放到
shared memory - 在 block 内按更合适的方式重排和消费
所以 shared memory 的本质不是“它更快,所以总要用”,而是:
当存在明显复用或访存重组需求时,它特别有价值。
3.7 为什么 global memory 常常是瓶颈
global memory 是大部分真实数据的主战场:
- 大张量在这里
- 权重在这里
- activation 在这里
- KV cache 也在这里
你不可能绕开它。
真正的问题从来不是“不要用 global memory”,而是:
必须用时,能不能少用一点、用得更整齐一点、让每次访问更值一点。
这也是为什么很多推理算子虽然数学不复杂,却依然很慢。
例如:
- LayerNorm
- RMSNorm
- softmax
- 一些 elementwise 算子
它们常常不是算不动,而是读写太多,典型地属于 memory-bound。
3.8 速度、容量、距离之间到底怎么取舍
把内存层级真正串起来看,核心就是三件事:
- 快的,通常小
- 大的,通常远
- 远的,通常贵
所以一个成熟的 CUDA 视角不是“哪种内存最快”,而是:
- 哪些数据必须长期放在
global memory - 哪些数据值得临时搬到
shared memory - 哪些中间值应该尽量留在
register - 哪些对象大到可能导致
local memory或 spill
一旦你开始这样想,内存层级就不再是静态定义,而变成了动态的数据流设计问题。
3.9 用“数据怎么流动”来理解 kernel
这篇最重要的思维切换是:
不只看线程在算什么,还要看数据在哪一层、接下来会往哪一层走。
很多高性能 kernel 背后的典型路径都是:
global memory -> shared memory -> register -> global memory
这条路径背后的含义是:
- 大块原始数据先在
global memory - 当前 block 需要的一小块搬进
shared memory - 每个线程再把自己最频繁用的值放进
register - 算完后结果再写回
global memory
这几乎就是后面 GEMM、卷积、attention、fused kernel 的基本骨架。
3.10 为什么理解内存层级是后续优化的前提
从这一篇开始,你后面很多知识点都会自动串起来:
- 为什么 coalescing 重要:因为
global memory访问方式直接决定带宽利用率 - 为什么 bank conflict 会拖慢
shared memory:因为片上共享存储也有自己的硬件约束 - 为什么 fusion 常能提速:因为减少了中间结果写回
global memory再读回的次数 - 为什么 occupancy 不能脱离寄存器和 shared memory 看:因为这些资源会限制一个 SM 上能挂多少活跃 warp
所以“内存层级”不是一个独立章节,而是后面几乎所有优化章节的地基。
4. 和 AI 推理的关系
这篇和 AI 推理的关系非常直接,因为很多推理优化,本质上都在减少慢速数据流量。
例如:
fusion:减少中间结果写回global memory的次数- tiled GEMM:把可复用数据先搬到
shared memory - FlashAttention:尽量避免把过大的中间结果落回慢速内存
- decode 阶段的很多 kernel:常常更受访存和带宽限制,而不是受乘加能力限制
所以你后面看推理系统时,真正该问的是:
- 数据主要卡在哪一层
- 哪些数据可以复用
- 哪些搬运是不必要的
- 现在这个 kernel 更像
memory-bound还是compute-bound
5. 常见误区
- GPU 算力很强,所以性能问题主要来自计算。不是,很多 kernel 首先卡在数据搬运。
local memory听起来很近,所以应该很快。不是,它常常意味着寄存器放不下了。- 寄存器越多越好。不是,寄存器太多会压并发,甚至 spill。
shared memory一定值得手动用。不是,只有在复用和重组访问明显值得时才划算。global memory很慢,所以应该完全避免。不是,大数据离不开它,关键是减少不必要访问并优化访问方式。- 内存层级就是背几个名词。不是,真正要学的是数据怎样在不同层级之间流动。
6. 复习自测
- 为什么很多 CUDA 程序首先受访存限制,而不是先受计算限制?
- CUDA 为什么必须设计成分层内存?
register、local memory、shared memory、global memory各自解决什么问题?- 为什么
local memory不一定快? - 什么是
register spill,它为什么会拖慢程序? shared memory最核心的两个价值是什么?- 为什么
global memory常常是推理算子的主要瓶颈? - 为什么说真正重要的不是“哪种内存快”,而是“数据应该怎样流动”?

