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 多核处理器缓存的两个核心原则

  1. 最近层级的缓存是每核私有的
  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 CU40642560
NVIDIA Volta SM64322048

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 CU16 KB256064 bytes6.4 bytes
AMD RDNA DCU2×16 KB2560256 bytes12.8 bytes
NVIDIA Pascal SM24 KB2048192 bytes12 bytes
NVIDIA Volta SM (A)32 KB2048256 bytes16 bytes
NVIDIA Volta SM (B)64 KB2048512 bytes32 bytes
NVIDIA Volta SM (C)96 KB2048768 bytes48 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 MB92160227.5 B22.75 B
AMD Navi 10 (RDNA)4 MB102400819.2 B40.96 B
NVIDIA GP100 (Pascal)4 MB122880546.13 B34.13 B
NVIDIA GV100 (Volta)6 MB163840614.4 B38.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 数据缓存和共享内存合并为单一组件,经历了有趣的演进:

架构设计
KeplerL1 数据缓存 + 共享内存合并,但纹理缓存独立
Maxwell / PascalL1 数据缓存 + 纹理缓存统一,共享内存独立
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 的根本差异

维度CPUGPU
并发线程数少(几十个)极多(数万到数十万)
代码复杂度大而复杂相对简单,大量线程共享相同代码
每线程数据量
缓存一致性硬件自动保证非一致性 ,需要显式同步
缓存复用模式时间域 (同一线程多次访问)空间域 (不同线程访问相同数据)

9.2 关键实践建议

  1. 着色器代码尽量小 ,确保能完全装入指令缓存,避免一次 Cache Miss 阻塞数千线程
  2. 不要依赖 GPU 数据缓存做临时存储和重复访问 ,改用寄存器和共享内存
  3. 利用数据访问的空间局部性 :确保 Wave 内线程访问连续、对齐的地址,以实现缓存行合并
  4. 理解非一致性缓存的同步需求 :在适当位置使用内存屏障、Fence、Semaphore
  5. 考虑使用 coherent 内存操作 :在生产者-消费者模式中,可避免每核缓存的刷新/失效开销,减少流水线气泡
  6. 纹理采样天然受益于缓存 :双线性过滤的重叠纹素足迹使缓存命中率高达 75%+
  7. SoA 数据布局优于 AoS :便于 Wave 内线程合并为缓存行大小的事务
  8. TBR GPU :正确配置 Vulkan Render Pass 的 Load/Store 操作以最小化外部内存访问