文章

GPU 读写内存实际发生了什么

GPU 读写内存实际发生了什么

GPU 读写内存实际发生了什么

本文基于 Doubleword 的两篇文章, 重点整理 GPU 读取数据, 写入数据, Cache 与显存的关系, 以及 GPU 与 CPU 之间的数据传输.
文章中的具体实验对象是 NVIDIA RTX 4090 + CUDA, 因此具体延迟和缓存策略不能直接套用到其他 GPU, 但整体内存层级的理解具有参考价值.


W - What: 结论

1. Shader 访问 Buffer, 不等于直接访问显存

从程序角度看:

1
2
float x = Buffer[id];     // 读取
Buffer[id] = value;       // 写入

看起来都是直接操作 GPU Buffer.

从硬件角度看, GPU Buffer 背后存在多级存储:

1
2
3
4
5
6
7
Shader
↓
L1 Cache
↓
L2 Cache
↓
显存 DRAM

其中:

  • L1 Cache: 离计算单元较近, 小而快.
  • L2 Cache: 更大, 是整个 GPU 内存系统的重要共享层.
  • 显存 DRAM: RTX 4090 上即 GDDR6X 芯片, 容量大, 但访问代价最高.

因此, Shader 读取或写入 Buffer 时, 数据不一定真的访问了物理显存.


2. GPU 读取数据时, 是一个”请求出去, 数据回来”的过程

例如:

1
float x = Buffer[id];

完整过程可以简化为:

1
2
3
4
5
6
7
8
读取请求:

Shader → L1 → L2 → 显存


读取结果:

Shader ← L1 ← L2 ← 显存

实际读取时, 会逐级查找:

1
2
3
4
5
6
7
8
9
先查 L1
↓
没有

再查 L2
↓
没有

最后访问显存

如果 L1 或 L2 已经有数据, 就不需要继续访问显存.

如果最终访问显存, 数据读出后会经过 L2, L1, 最后回到 Shader 使用的寄存器.

因此, 读取的关键特点是:

Shader 后续计算如果依赖这个数据, 就必须等数据回来.


3. GPU 写入数据时, 通常不需要等数据真正进入显存

例如:

1
Buffer[id] = value;

可以简化为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Shader 算出结果
↓
提交写入
↓
L1
↓
L2
↓
Shader 可以继续执行

与此同时:

L2 中的数据
↓
在之后合适的时机
↓
写回显存

也就是说:

“Shader 已经执行完写入”, “其他 GPU 工作已经能看到新数据”, “数据已经真正进入显存芯片”, 是三个不同的时刻.

这也是读和写最重要的区别:

1
2
3
4
5
Read:
请求出去 + 数据必须回来.

Write:
数据交出去, Shader 通常不必等待它最终进入显存.

4. 最新数据可以只存在 L2 中, 显存仍然是旧值

例如原始状态:

1
2
显存:
A = 5

Shader 写入:

1
A = 10

之后完全可能出现:

1
2
3
4
5
L2 Cache:
A = 10    ← 最新值

显存:
A = 5     ← 旧值

此时 GPU 内存系统仍然可以正确工作.

如果其他 GPU 工作读取 A, 可以从 L2 得到 10, 并不要求先把 10 写回显存.

这种”Cache 中的数据已经被修改, 但显存中的版本还是旧的”状态, 称为 Dirty, 即脏数据.


5. 脏数据之后才会写回显存

当 L2 中的脏数据需要被清理时:

1
2
3
4
5
6
7
L2:
A = 10

↓ Write-back, 回写

显存:
A = 10

RTX 4090 并不是简单地等 Cache 完全没有空间以后才开始回写.

作者实验发现, 一个 L2 Cache 组有 16 个位置. 当其中脏数据达到至少 8 个时, GPU 会开始主动清理其中较久没有被写过的数据.

可以理解为:

1
2
3
4
5
6
7
8
9
16 个缓存位置
↓
脏数据越来越多
↓
Dirty >= 8
↓
GPU 主动选择部分脏数据
↓
提前写回显存

这样可以避免等到 Cache 必须腾位置时, 突然产生大量显存写入.


6. CPU 获取 GPU 结果时, 数据甚至可以不先进入显存

这是两篇文章中一个很值得注意的结果.

通常容易想象为:

1
2
3
4
5
6
7
8
9
10
11
Shader
↓
L1
↓
L2
↓
显存
↓
PCIe
↓
CPU 内存

但 RTX 4090 的实验中, 某些刚计算出的结果仍然只以最新版本存在于 L2 中时, cudaMemcpyDeviceToHost 就可以直接把结果取走:

1
2
3
4
5
6
7
8
9
Shader
↓
L1
↓
L2
   ↓
   PCIe
   ↓
CPU 内存

因此:

CPU 要的是”这个 GPU 内存地址对应的最新数据”, 而不是要求”这份数据必须已经物理写入 GDDR6X”.

但这不意味着 CPU 可以直接访问 GPU 的 L2 Cache.

更准确的说法是:

GPU 的内存/传输系统可以从 L2 取得最新数据, 然后通过 PCIe 传输到 CPU 内存.


I - Insight: 这对图形开发意味着什么

1. “访问 GPU Buffer”和”访问显存”不是同一个概念

例如:

1
float value = Buffer[index];

这次读取可能来自:

1
2
3
4
5
L1
或
L2
或
显存

因此, Shader 代码中读取了多少数据, 并不等价于真正产生了多少显存流量.

真正的显存压力还取决于 Cache 是否能够命中.


2. 数据访问方式会直接影响性能

GPU 会同时执行很多线程.

在 NVIDIA GPU 上, 通常将 32 个线程组成一个执行组, 称为 Warp.

假设这 32 个线程分别写:

1
2
3
4
5
Thread 0  → Buffer[0]
Thread 1  → Buffer[1]
Thread 2  → Buffer[2]
...
Thread 31 → Buffer[31]

这些地址连续, GPU 可以把很多线程产生的访问合并成较少的实际内存请求.

而如果访问变成:

1
2
3
4
Thread 0  → Buffer[9281]
Thread 1  → Buffer[17]
Thread 2  → Buffer[40002]
...

访问难以合并, Cache 的利用也通常更差.

因此, 对 Compute Shader, GraphicsBuffer, RWStructuredBuffer, GPU Culling, GPU Mesh Generation 等场景来说:

数据布局和访问顺序本身就是性能设计的一部分.


3. Cache 的意义不是单纯”加速读取”

Cache 同时承担:

1
2
3
4
5
读取:
尽量避免访问显存.

写入:
暂时保存最新结果, 延后真正的显存写入.

因此可以把 GPU 内存系统理解为:

1
2
3
4
5
                    ┌─ 读取时尽量在这里找到数据
Shader ↔ L1 ↔ L2 ──┤
                    └─ 写入时也可以先把最新数据留在这里
              ↕
             显存

Cache 不只是显存前面的一层”读取加速器”, 它也是整个内存一致性和数据流动体系的一部分.


4. GPU 为什么需要大量并行线程

访问显存的延迟很高.

RTX 4090 上作者测得:

1
2
3
L1 命中:   约 15.4 ns
L2 命中:   约 127.4 ns
显存访问:  约 255.4 ns

如果某组线程正在等待显存:

1
2
3
线程组 A
↓
等待数据

GPU 不会让整个芯片停下来, 而是去执行其他准备好的线程组:

1
2
3
4
5
6
7
8
9
A 等待
↓
执行 B
↓
执行 C
↓
A 的数据回来
↓
继续执行 A

因此大量并行线程不仅用于”同时计算很多数据”, 也用于隐藏内存访问等待.


S - System: 建立完整心智模型

1. 读取流程

以:

1
float x = Buffer[id];

为例.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Shader
↓
产生读取请求
↓
L1 查找
├─ 找到 → 返回 Shader
└─ 没找到
      ↓
     L2 查找
     ├─ 找到 → 返回 L1 → Shader
     └─ 没找到
           ↓
          显存读取
           ↓
          L2
           ↓
          L1
           ↓
          Shader

用最短形式记忆:

1
2
3
4
5
请求:
Shader → L1 → L2 → 显存

结果:
Shader ← L1 ← L2 ← 显存

2. 写入流程

以:

1
Buffer[id] = value;

为例.

1
2
3
4
5
6
7
8
9
10
11
12
13
Shader 计算结果
↓
提交写入请求
↓
GPU 整理多个线程的内存访问
↓
L1
↓
L2
↓
数据成为最新版本
↓
Shader 已经可以继续工作

之后 L2 再自行处理:

1
2
3
4
5
6
7
L2 中的脏数据
↓
需要清理 / 主动清理
↓
Memory Controller
↓
显存

这里的 Memory Controller, 内存控制器, 是 GPU 中负责真正和 GDDR6X 显存芯片通信的硬件.


3. RTX 4090 的 L2 清理规则

作者通过实验推测出的策略可以简化为两部分.

缓存需要腾位置时

GPU 会优先选择”近期不太可能再使用”的数据.

如果选中的数据是 Clean, 即 Cache 与显存内容一致:

1
直接丢弃 Cache 副本

如果是 Dirty:

1
2
3
4
5
先启动写回显存
↓
不能立即丢失
↓
再寻找可以释放的位置

脏数据太多时

一个 L2 组有 16 个位置.

当:

1
Dirty >= 8

新的写入到来时, GPU 会主动选择一个较久没有被写过的脏数据进行清理.

目的不是单纯腾空间, 而是让显存写入更加平滑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
没有主动清理:

平时几乎不写
↓
Cache 被脏数据塞满
↓
突然大量写回显存


主动清理:

脏数据积累到一定程度
↓
陆续写回
↓
降低突发写入压力

4. CPU 与 GPU 的数据传输

独立显卡和 CPU 通常通过 PCIe 通信.

但”通过 PCIe 通信”并不意味着数据必须先进入物理显存.

例如 GPU 刚算出的结果:

1
2
3
4
5
L2:
Result = 最新值

显存:
Result = 旧值

如果 CPU 此时需要读取 Result:

1
2
3
4
5
6
7
L2
↓
GPU 数据传输系统
↓
PCIe
↓
CPU 内存

即可完成传输.

因此需要区分:

1
GPU Buffer 的最新数据在哪里

和:

1
这个数据是否已经写入物理显存

这不是同一个问题.


E - Evidence: 两篇文章实际验证了什么

两篇文章都以 RTX 4090 为实验对象, 通过 CUDA 指令, 性能计数器和针对缓存结构设计的实验, 反推出实际硬件行为.

主要实验结果包括:

项目RTX 4090 实验结果
NVIDIA 一个 Warp32 个线程
L1 读取命中约 15.4 ns
L2 读取命中约 127.4 ns
显存完整读取往返约 255.4 ns
连续 32 × 4 Byte 写入可整理成 4 个 32 Byte 数据块
Store 从执行单元发出约 6.1 cycles
Store 到达 L2 并获得确认约 140 ns
L2 Cache 组16 个位置
主动清理阈值至少 8 个 Dirty 数据
GPU → CPU实验中可直接从 L2 经 PCIe 传到 CPU 内存

这些数字属于 RTX 4090 的具体微架构结果.

不能直接认为 Adreno, Mali, PowerVR 或其他 NVIDIA GPU 都完全相同.

更值得保留的是以下通用心智模型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
GPU Read:
请求出去
↓
逐级查 Cache
↓
必要时访问显存
↓
数据返回
↓
Shader 才能继续依赖该数据的计算


GPU Write:
提交数据
↓
数据进入 Cache / 内存系统
↓
Shader 通常可以继续
↓
之后再根据需要写回显存

以及:

1
2
3
4
5
Shader 已经执行写入
≠
所有位置已经看到新数据
≠
数据已经物理进入显存

术语速查

术语本文中的含义
ThreadGPU 上执行 Shader 的单个线程
WarpNVIDIA 将 32 个线程组成的执行组
Load从内存读取数据
Store向内存写入数据
CacheGPU 芯片内部的小容量高速缓存
L1 Cache离计算单元较近的一级缓存
L2 Cache更大的共享二级缓存, GPU 内存系统的重要汇合层
DirtyCache 中是最新值, 但显存中仍是旧值
Write-back将 Dirty 数据从 Cache 写回显存
Memory ControllerGPU 中负责和显存芯片通信的硬件
PCIe独立 GPU 与 CPU 之间常用的数据通信总线
DRAM实际保存大量数据的动态随机存取存储器, 本文对应 RTX 4090 的 GDDR6X 显存

文献

  1. Fergus Finn, Doubleword, What happens when a GPU reads memory
    https://blog.doubleword.ai/what-happens-when-a-gpu-reads-memory

  2. Fergus Finn, Doubleword, What happens when a GPU writes memory
    https://blog.doubleword.ai/what-happens-when-a-gpu-writes-memory

本文由作者按照 CC BY 4.0 进行授权