GPU 缓存体系深度解析
Understanding GPU caches – RasterGrid | Software Consultancy
一、缓存的基本概念与存在意义
1.1 为什么需要缓存
在计算机发展历史中,处理器算力的增长速度远超内存访问速度 ,两者之间的差距(即 "内存墙" 问题)使得引入高速中间存储成为必要。
1.2 缓存的核心作用
缓存与缓冲区(Buffer)有相似之处,但缓存具备额外优势:
- 降低延迟(Decrease Latency) :以较大块( 缓存行 / Cache Line )为单位从内存读取数据,利用 空间局部性(Spatial Locality) ,期望后续访问的数据就在附近
- 提升吞吐量(Increase Throughput) :将多次小的传输合并为更大、更高效的内存请求
- 加速重复访问 :缓存保留内存中数据的子集副本,后续对已缓存数据的访问无需再次执行昂贵的内存事务
1.3 缓存行(Cache Line)
- 缓存以 缓存行 为单位存储数据,典型大小为 32~512 字节
- 内存事务以缓存行为单位进行,而处理器代码的单次访问通常远小于此(如 4 字节)
- 缓存容量远小于系统内存,当前缓存的数据集根据访问模式和 替换策略(Replacement Policy) 不断变化
1.4 缓存命中率
- 缓存命中率(Cache Hit Rate) :数据访问能从缓存中获取的百分比
- 对 GPU 而言尤为关键:GPU 需要同时为 数千个并发线程 提供数据,而 CPU 通常最多只需服务几十个线程
二、缓存层级结构基础(以 CPU 为参照)
2.1 Intel Core 2 Duo 的经典层级
选用此例是因为其缓存层级捕捉了多核处理器缓存组织的本质:
- L1 缓存(每核私有) :
- L1 指令缓存(L1 I-Cache) :专门缓存指令,因为代码对处理器核心而言是只读的,且指令流遵循特定访问模式
- L1 数据缓存(L1 D-Cache) :专门缓存数据,数据是可读写的,访问模式可以是任意的
- L2 缓存(所有核心共享) :所有核心共享的 末级缓存(Last Level Cache, LLC)
2.2 多核处理器缓存的两个核心原则
- 最近层级的缓存是每核私有的
- 末级缓存是所有核心共享的
现代 CPU 更复杂(每核 L2 + 共享 L3,甚至 L4),但 GPU 的缓存层级与此高度相似,关键区别在于:GPU 拥有远远更多的核心 ,这带来了一系列深远影响。
三、缓存一致性(Cache Coherency)
3.1 非一致性问题
缓存保存了数据的本地副本,而数据的 规范版本(Canonical Version) 存储在主内存中,这引入了 数据非一致性(Data Non-Coherence) 问题:
- 一个核心修改了其私有数据缓存中的数据,其他核心可能看不到更新
- 外部设备修改了内存中的数据,处理器核心可能从缓存中读取到 过期数据(Stale Data)
3.2 CPU 的解决方案
CPU 使用 缓存一致性协议 ,如:
- MOESI 协议
- MESIF 协议
通过 总线侦听(Bus Snooping) 等机制检测数据更新,自动同步/失效缓存数据。这对核心数较少的 CPU 是合理的,且支持在任意核心/线程之间高效共享数据。
3.3 GPU 的策略:非一致性缓存
GPU 核心数量远大于 CPU,实现一致性协议的 芯片面积和性能开销过高 ,因此:
GPU 缓存通常是非一致性的(Non-Coherent) ,需要 显式刷新(Flush)和/或失效(Invalidate) 缓存来重新恢复一致性。
这在实践中不成问题,因为 GPU 编程模型中,不同核心上运行的着色器/内核之间很少需要高频数据共享。具体规则:
- 单次 Draw/Dispatch 内部 :不同核心上的着色器调用可能看到不一致的视图,除非使用 coherent 资源 或在着色器内发出 内存屏障(Memory Barrier) 刷新/失效每核缓存
- 连续的 Draw/Dispatch 命令之间 :可能看到不一致的视图,除非通过 API 发出适当的 内存屏障
- GPU 命令与其他设备操作之间(如 CPU 读/写):需要使用 同步原语 (如 Fence 或 Semaphore),它们会隐式插入必要的内存屏障
四、每核指令缓存(Per Core Instruction Cache)
4.1 GPU 执行模型回顾
- GPU 使用 SIMD 处理单元 ,但采用 SIMT(Single Instruction Multiple Thread)执行模型 :每条 lane 实际运行一个独立线程(着色器调用)
- 一组以 SIMT 方式共同执行的线程形成一个 Wave (不同厂商叫法不同):
- AMD: Wavefront
- NVIDIA: Warp
- Vulkan: Subgroup
4.2 多 Wave 并发
类似 CPU 的 SMT(同步多线程,如超线程) ,GPU 单个核心可以同时运行多个 Wave,主要好处是当某个 Wave 等待内存数据时,可以切换执行其他 Wave 的指令。
单核线程数示例:
| 架构 | 最大 Wave 数/核 | 线程数/Wave | 总线程数/核 |
|---|---|---|---|
| AMD GCN CU | 40 | 64 | 2560 |
| NVIDIA Volta SM | 64 | 32 | 2048 |
4.3 指令缓存的特点
尽管单核线程数巨大,但它们 通常都运行相同的代码 :
- 同一 Wave 内:SIMT 定义决定了必然如此
- 跨 Wave / 跨核心:典型 GPU 工作负载的工作项数量远超单核容量(如全高清全屏 Pass 产生 超过 200 万个线程调用 )
因此:
- 小型的每核指令缓存已足够满足需求
- 部分 GPU 甚至 跨多个核心共享指令缓存 (如 AMD GCN 架构在最多 4 个 CU 的集群间共享一个 32KB 指令缓存 )
4.4 性能提示
GPU 上一次指令缓存未命中(Cache Miss)可能同时阻塞 数千个线程 ,因此强烈建议着色器/内核代码尽量 小到能完全装入指令缓存 。
五、每核数据缓存(Per Core Data Cache)
5.1 历史演变
- 早期 GPU:着色器只能 从内存读取 数据(纹理和缓冲输入),数据缓存为 只读
- 后期架构(GPGPU 时代):需要支持 散列写入(Scattered Writes) ,出现 读写数据缓存 ,通常采用 写穿策略(Write-Through Policy) ,即写入立即传播到下一级缓存
5.2 容量分析
GPU 每核数据缓存典型大小为 16KB~128KB ,与 CPU 的 32KB L1 数据缓存相当。但考虑到 GPU 单核服务的线程数远多于 CPU,其 每线程有效容量 极低:
| 处理器核心 | 数据缓存大小 | 最大线程数 | 每线程容量(4 Wave) | 每线程容量(最大 Wave) |
|---|---|---|---|---|
| AMD GCN CU | 16 KB | 2560 | 64 bytes | 6.4 bytes |
| AMD RDNA DCU | 2×16 KB | 2560 | 256 bytes | 12.8 bytes |
| NVIDIA Pascal SM | 24 KB | 2048 | 192 bytes | 12 bytes |
| NVIDIA Volta SM (A) | 32 KB | 2048 | 256 bytes | 16 bytes |
| NVIDIA Volta SM (B) | 64 KB | 2048 | 512 bytes | 32 bytes |
| NVIDIA Volta SM (C) | 96 KB | 2048 | 768 bytes | 48 bytes |
5.3 数据缓存的实际角色
在高 Wave 占用率下,每核数据缓存通常 更像一个合并缓冲区(Coalescing Buffer) 而非传统缓存:
- 不同 Wave 的线程会不可避免地互相驱逐缓存数据
- 例:32 宽 Wave 在 RDNA 核心上执行,每线程访问一个 4 字节数组元素 → 合并为单次 128 字节缓存行读取
5.4 纹理访问中缓存的真正价值
数据缓存真正发挥 传统缓存优势 的场景:同一 Wave 或同一核心不同 Wave 的线程访问的内存位置有重叠 。
最典型的例子是 带过滤的纹理查找 :
- 双线性过滤需要 2×2 纹素区域
- 一个片段着色器线程的纹素足迹会与最多 8 个"相邻"片段着色器线程 的纹素足迹重叠
- 此时平均缓存命中率可达 约 75% 甚至更高
5.5 GPU 缓存复用的时空特征
CPU 缓存复用发生在时间域 :同一线程的后续指令多次访问相同数据
GPU 缓存复用发生在空间域 :同一核心上不同线程的指令访问相同数据
因此:在 GPU 上使用内存作为临时存储并依赖缓存实现快速重复访问是不明智的 ,这与 CPU 的最佳实践完全不同。GPU 通过更大的 寄存器空间 和 共享内存(Shared Memory) 来弥补这一点。
5.6 统一数据缓存 / 标量缓存(Uniform Data Cache / Scalar Cache / Constant Cache)
所有线程读取同一地址是极端的数据复用场景(着色器经常访问常量数据或低频数据),许多 GPU 配备了专用的 统一数据缓存 :
- 独立缓存确保低频数据不会被高频数据访问驱逐
- 这类数据通常跨多核共享 → 可跨核心共享此缓存(AMD GCN:4 个 CU 共享 16KB 标量缓存 )
六、设备级缓存(Device Wide Cache)
6.1 核心特征
- 为所有每核及低层级缓存提供服务的 全设备共享缓存
- 提供 跨核心一致性的缓存数据
- 通常是层级中的 末级缓存(Last Level Cache)
- 支持 读写和原子操作(Atomic Operations)
6.2 原子操作的处理
- 所有对内存数据的原子操作在 设备级缓存 处理,跳过低层级缓存
- 因此原子操作在不同核心的线程间天然一致
- 但 普通读取默认仍从每核数据缓存服务 ,不会自动看到原子操作的结果
6.3 一致性内存操作(Coherent Memory Operations)
为了支持对跨核心共享可变数据的一致性访问,GPU 提供了 绕过非一致性每核缓存 的特殊指令:
- GLSL 中使用
coherent限定符 标记资源,使所有对该资源的访问绕过非一致性缓存 - 好处不仅限于单次 Draw/Dispatch 内的数据共享,也适用于 连续命令间的数据共享 :通过设备级一致性内存操作,可以避免命令间刷新/失效每核缓存,减少同步相关的流水线停顿/气泡
6.4 一致性操作的性能考量
使用一致性内存操作会丧失低层级缓存的好处,但在以下典型场景中 没有额外开销 :
- 每线程数据访问中,Wave 内请求合并为 完整缓存行大小的事务
- 如图像处理着色器中每个 Wave 处理对应的 2D 图块
- 数据以 SoA(Structure-of-Arrays)布局 排列,线程读写连续、对齐的数据范围
优化技巧 :当"生产者"和"消费者"工作负载之间有足够的非依赖工作可调度时,可以使用 仅完成依赖(Completion-Only Dependency) 而无需缓存控制操作(如 Vulkan 的 Event Wait 不带 Memory Barrier )。
6.5 设备级缓存大小
| GPU | 设备级缓存大小 | 最大线程数 | 每线程容量(4 Wave/核) | 每线程容量(最大 Wave) |
|---|---|---|---|---|
| AMD Polaris 20 (GCN) | 2 MB | 92160 | 227.5 B | 22.75 B |
| AMD Navi 10 (RDNA) | 4 MB | 102400 | 819.2 B | 40.96 B |
| NVIDIA GP100 (Pascal) | 4 MB | 122880 | 546.13 B | 34.13 B |
| NVIDIA GV100 (Volta) | 6 MB | 163840 | 614.4 B | 38.4 B |
- 每线程容量在设备级缓存层面更适合 传统缓存用途
- AMD "Big Navi" 的额外 128MB Infinity Cache 是特例,主要用于弥补其较窄的显存总线
- APU/SoC 上的 GPU 通常没有独立的设备级缓存,而是与 CPU 共享末级缓存
七、共享内存(Shared Memory)
7.1 基本概念
- 每个 GPU 核心拥有自己的 共享内存(Shared Memory) ,主要用于同一 Workgroup 的计算线程之间共享数据
- 支持 原子操作
- 不是缓存,而是一种暂存器内存(Scratchpad Memory) :缓存自动管理数据集,而暂存器内存由 应用程序软件显式管理 ,着色器/内核代码可显式地在内存和暂存器之间传输数据
7.2 NVIDIA 的统一方案演进
NVIDIA 将 L1 数据缓存和共享内存合并为单一组件,经历了有趣的演进:
| 架构 | 设计 |
|---|---|
| Kepler | L1 数据缓存 + 共享内存合并,但纹理缓存独立 |
| Maxwell / Pascal | L1 数据缓存 + 纹理缓存统一,共享内存独立 |
| Volta 及之后 | 统一数据缓存 + 共享内存全部合并为单一组件 |
优势 :
- 动态分区 :可在数据缓存和共享内存之间灵活调整分配。如 Ampere 架构可将全部 128KB(或 192KB) 用作数据缓存(纯图形工作负载无需共享内存时)
- 可能支持 从 L2 缓存直接加载数据到共享内存 (因共享内存和 L1 数据缓存由同一组件服务),但 NVIDIA ISA 非公开,无法验证
7.3 AMD 的 GDS(Global Data Share)
- AMD GCN/RDNA GPU 除了每核的 LDS(Local Data Share,即共享内存) 外,还有设备级的 GDS(Global Data Share)
- GDS 允许 所有核心上的所有线程共享数据 ,支持快速的全局无序原子操作
- 遗憾的是:目前没有 GPU 编程 API 直接暴露 GDS 访问
八、其他缓存
8.1 固定功能硬件缓存
GPU 还有其他缓存服务于图形管线各阶段的 固定功能硬件 :
- 顶点属性缓存(Vertex Attribute Cache) :传统上用于在分发顶点着色器调用前缓存顶点属性值。现代架构中因几何处理阶段可编程性增强(曲面细分、几何着色器、网格着色器),通常不再需要专用缓存
8.2 ROP / RB 缓存(光栅操作缓存)
位于图形管线末端,为 帧缓冲附件(Render Target)操作 提供缓存:
- 仍存在于现代 IMR(即时模式渲染器)GPU 上
- 帧缓冲操作因光栅化方式具有 特定的访问模式 ,足以证明定制方案的合理性
- 帧缓冲内存访问通常与片段着色器执行 分开进行 ,由专用 ROP 单元发出(还提供混合等功能,并确保帧缓冲更新按图元顺序进行)
ROP 缓存的组织 :
- GPU 上通常有 多个 ROP 缓存 ,每个服务一个或多个 ROP 单元
- 通常分为独立的 深度/模板缓存 和 颜色缓存 (因两者需求不同)
- 与兄弟缓存和每核着色器缓存通常 非一致性
- 可能由与每核缓存相同或不同的设备级缓存支持
8.3 ROP 缓存与着色器缓存的同步
- 最常见的 GPU 缓存同步操作:ROP 缓存的写入与着色器缓存的读取之间的同步 (渲染到纹理后读取结果,如阴影贴图、反射贴图)
- 因此 ROP 缓存在缓存层级中应尽量 接近每核着色器缓存
- AMD GCN 架构的演进证明了这一点:第 1~4 代 GCN 的 ROP 缓存不经过设备级缓存;第 5 代 GCN 将 ROP 缓存也纳入了设备级缓存的管辖
8.4 TBR(基于图块的渲染器)GPU
在移动 SoC 上普遍采用的 TBR GPU 工作方式不同,没有 IMR 意义上的 ROP 缓存:
- 将帧缓冲空间分割为 图块(Tile)网格
- 对每个图块分别执行光栅化和片段处理
- 处理前加载图块帧缓冲数据到 片上图块内存(On-Chip Tile Memory) ,处理后存回内存
- 图块内存实际上是 暂存器内存 ,可能与共享内存使用相同的物理存储
Vulkan API 的控制 :
- 通过 Render Pass 对象 的每附件 Load/Store 操作 和其他参数控制何时、如何、什么数据在图块内存和显存之间传输
8.5 TLB(Translation Lookaside Buffer)
- 现代 GPU 使用 虚拟内存寻址 ,因此有独立的 TLB 层级 加速虚拟到物理地址的转换
- 集成方案 :TLB 层级可能与 CPU 共享
- 独立 GPU :芯片上始终有专用的 TLB 层级
九、核心总结与实践启示
9.1 GPU 与 CPU 的根本差异
| 维度 | CPU | GPU |
|---|---|---|
| 并发线程数 | 少(几十个) | 极多(数万到数十万) |
| 代码复杂度 | 大而复杂 | 相对简单,大量线程共享相同代码 |
| 每线程数据量 | 大 | 小 |
| 缓存一致性 | 硬件自动保证 | 非一致性 ,需要显式同步 |
| 缓存复用模式 | 时间域 (同一线程多次访问) | 空间域 (不同线程访问相同数据) |
9.2 关键实践建议
- 着色器代码尽量小 ,确保能完全装入指令缓存,避免一次 Cache Miss 阻塞数千线程
- 不要依赖 GPU 数据缓存做临时存储和重复访问 ,改用寄存器和共享内存
- 利用数据访问的空间局部性 :确保 Wave 内线程访问连续、对齐的地址,以实现缓存行合并
- 理解非一致性缓存的同步需求 :在适当位置使用内存屏障、Fence、Semaphore
- 考虑使用 coherent 内存操作 :在生产者-消费者模式中,可避免每核缓存的刷新/失效开销,减少流水线气泡
- 纹理采样天然受益于缓存 :双线性过滤的重叠纹素足迹使缓存命中率高达 75%+
- SoA 数据布局优于 AoS :便于 Wave 内线程合并为缓存行大小的事务
- 对 TBR GPU :正确配置 Vulkan Render Pass 的 Load/Store 操作以最小化外部内存访问