GPU 架构学习

第十一课:GPU 的缓存、显存控制器与内存事务

这一课继续深入 GPU 的内存系统,重点解释一个 Warp 的访存请求如何变成 Cache Line、内存事务,并最终到达 GDDR 或 HBM。

目录一、逻辑访存与物理访存不同二、什么是内存事务三、Cache Line 是什么四、为什么连续访问效率高五、不连续访问为什么浪费带宽六、对齐为什么重要七、跨 Cache Line 的问题八、结构体布局如何影响访存SoA 布局九、二维数组的行跨度十、Warp 访存请求的形成过程十一、Load/Store Unit 的作用十二、L1 Cache 的基本作用十三、L1 与 Shared Memory 的关系十四、L2 Cache 是整个 GPU 的共享缓存十五、为什么不同 SM 可以从 L2 受益十六、Cache Set 和 Way 的基本概念十七、三类典型 Cache Miss1. Compulsory Miss2. Capacity Miss3. Conflict Miss十八、Store 操作如何处理十九、部分写入为什么复杂二十、什么是 Write-Through 和 Write-BackWrite-ThroughWrite-Back二十一、显存控制器是什么二十二、为什么有多个内存分区二十三、地址交织 Address Interleaving二十四、为什么某些固定步长访问很慢二十五、DRAM 内部结构二十六、Row Buffer 是什么二十七、显存控制器为什么要重排请求二十八、GDDR 是什么二十九、HBM 是什么三十、GDDR 与 HBM 的差异三十一、为什么 HBM 带宽高但延迟仍不低三十二、Burst Transfer三十三、内存并行性三十四、为什么线程太少会导致带宽跑不满三十五、缓存命中率高不一定总是性能高三十六、ECC 是什么三十七、ECC 的代价三十八、一次 Global Load 的完整路径三十九、地址转换与 TLB四十、为什么大页可能有帮助四十一、统一内存的基本概念四十二、PCIe 与显存带宽的区别四十三、零拷贝为什么不一定快四十四、如何优化 Global Memory 访问1. 让同一 Warp 访问连续地址2. 保证适当对齐3. 减少无效传输4. 增加数据复用5. 避免大跨度和规则冲突步长6. 选择合适数据布局7. 提供足够内存级并行性四十五、如何判断内存访问是否有问题本课核心结论第十二课:Block 如何被分配到 SM,以及 Occupancy 到底由什么决定

导语:这一课继续深入 GPU 的内存系统,重点解释一个 Warp 的访存请求如何变成 Cache Line、内存事务,并最终到达 GDDR 或 HBM。

前面已经知道,GPU 存储层次大致为:

第十一课:GPU 的缓存、显存控制器与内存事务

图示:第十一课:GPU 的缓存、显存控制器与内存事务

但当一个 Warp 执行:

float x = A[i];

硬件并不是为每个线程单独读取一个 4 字节浮点数。

真正发生的是:

  1. Warp 中各线程生成地址;
  2. 硬件合并这些地址;
  3. 形成一个或多个内存事务;
  4. 查询 L1、L2;
  5. 未命中时访问显存;
  6. 数据返回并写入线程寄存器。

这一课重点讲这个过程。


一、逻辑访存与物理访存不同

假设一个 Warp 有 32 个线程,每个线程读取一个 FP32:

float x = A[global_thread_id];

逻辑上发生了32次读取:

Thread 0 读取 A[0]
Thread 1 读取 A[1]
...
Thread 31 读取 A[31]

每个 FP32 为4字节,因此总有效数据量为:

32×4=128 Bytes32\times4=128\text{ Bytes}

但物理内存系统不会简单执行32个独立的4字节请求,而是将相邻地址合并成较大的事务。

理想情况下:

32 个连续的 4B 请求

少量连续内存事务

这就是访存合并。


二、什么是内存事务

Memory Transaction 是内存系统实际处理的一次数据传输请求。

它通常以固定粒度搬运数据,例如:

  • 一个 Cache Line;
  • 一个 Sector;
  • 一个内存突发传输块。

具体粒度依架构而异,因此入门阶段不需要死记某一代 GPU 的固定数字。

重点是:

内存系统通常按块传输,而不是只传输线程请求的那几个字节。

假设硬件以32字节为一个基本数据块。

线程只需要其中4字节,系统仍可能传输整个32字节块。

因此访问效率取决于:

useful bytestransferred bytes\frac{\text{useful bytes}} {\text{transferred bytes}}

三、Cache Line 是什么

Cache 不会按单个变量保存数据,而是按 Cache Line 保存连续的一块内存。

可以简单理解为:

三、Cache Line 是什么

图示:三、Cache Line 是什么

当线程访问:

A[10]

缓存可能同时把附近的:

A[8], A[9], A[10], A[11], ...

一起加载进来。

这是利用空间局部性:

如果程序访问了某个地址,它很可能很快访问附近地址。


四、为什么连续访问效率高

假设一个 Warp 中的32个线程连续读取:

四、为什么连续访问效率高

图示:四、为什么连续访问效率高

这些地址覆盖一个连续的128字节范围。

硬件可以用少量事务完成。

有效使用率接近:

128B extuseful128B exttransferred=100%\frac{128\,\mathrm{B}\ ext{useful}} {128\,\mathrm{B}\ ext{transferred}} =100\%

虽然实际事务组织取决于对齐和缓存状态,但这是理想访问模式。


五、不连续访问为什么浪费带宽

假设每个线程读取间隔为32字节:

五、不连续访问为什么浪费带宽

图示:五、不连续访问为什么浪费带宽

每个线程仍只使用4字节,但可能分别落在不同数据块中。

假设每次至少传输32字节,那么32个线程可能引发:

32×32=1024 Bytes32\times32=1024\text{ Bytes}

的传输。

但真正需要的数据只有:

32×4=128 Bytes32\times4=128\text{ Bytes}

有效率只有:

1281024=12.5%\frac{128}{1024}=12.5\%

这时即使显存理论带宽很高,大部分传输带宽也被无用数据占用。


六、对齐为什么重要

假设一个 Warp 要读取连续的128字节。

如果起始地址刚好对齐到内存事务边界:

地址范围:
0 ~ 127

可能只需要覆盖一个完整区域。

如果从地址4开始:

地址范围:
4 ~ 131

它跨越了两个边界区域:

区域 0:0 ~ 127
区域 1:128 ~ 255

这可能需要额外事务。

因此:

即使线程访问连续,如果起始地址没有良好对齐,也可能产生额外数据传输。

CUDA 分配得到的大块显存通常具有较好的基础对齐,但二维数组行跨度、结构体字段和偏移访问仍可能破坏对齐。


七、跨 Cache Line 的问题

假设 Cache Line 边界如下:

Cache Line 0:地址 0~127
Cache Line 1:地址 128~255

一个请求访问:

地址 120~135

虽然只需要16字节,但它跨越两个 Cache Line:

120~127 位于 Line 0
128~135 位于 Line 1

于是可能需要读取两个 Cache Line。

所以性能优化中经常强调:

  • 数据连续;
  • 起始地址对齐;
  • 数据结构大小合理;
  • 避免频繁跨边界。

八、结构体布局如何影响访存

假设定义:

struct Particle {
    float x;
    float y;
    float z;
    float mass;
};

数组布局为:

Particle 0:x y z mass
Particle 1:x y z mass
Particle 2:x y z mass

这种形式称为:

Array of Structures,AoS

如果一个 Warp 只读取所有粒子的 x

八、结构体布局如何影响访存

图示:八、结构体布局如何影响访存

相邻线程地址间隔为:

16 Bytes16\text{ Bytes}

线程只使用其中4字节,其余 y、z、mass 可能暂时无用。


SoA 布局

可以改成:

struct Particles {
    float* x;
    float* y;
    float* z;
    float* mass;
};

内存布局为:

x0 x1 x2 x3 ...
y0 y1 y2 y3 ...
z0 z1 z2 z3 ...

称为:

Structure of Arrays,SoA

此时 Warp 读取 x

Thread 0 → x[0]
Thread 1 → x[1]
Thread 2 → x[2]

地址连续,更容易合并。

因此 GPU 程序中经常偏好 SoA。

不过如果每个线程总是同时读取一个粒子的全部字段,AoS 也可能合理。布局应根据访问模式选择。


九、二维数组的行跨度

假设二维矩阵按行存储:

row 0:连续
row 1:连续
row 2:连续

每行可能不一定紧密衔接,因为为了对齐和提高访问效率,系统可能在每行末尾增加 Padding。

因此二维数组可能有一个 Pitch:

next row address=current row address+Pitch\text{next row address} = \text{current row address}+\text{Pitch}

Pitch 通常大于或等于:

width×element size\text{width}\times\text{element size}

访问二维数据时不能总是假设下一行紧接上一行,必须使用正确行跨度。


十、Warp 访存请求的形成过程

考虑:

float x = A[index];

一个 Warp 执行时,简化过程为:

1. 每个线程计算 index
2. 地址生成单元形成32个地址
3. 判断哪些线程当前活跃
4. 按地址所在的数据块分组
5. 合并为若干内存事务
6. 查询缓存
7. 数据返回后分发给各线程

其中第4步和第5步决定访存是否高效。

例如:

32 个地址都在连续区域
→ 少量事务

32 个地址完全随机
→ 大量事务

十一、Load/Store Unit 的作用

Load/Store Unit,简称 LSU,负责的不只是“读取数据”。

它通常要处理:

  • 地址计算;
  • 活跃线程掩码;
  • Warp 地址合并;
  • 缓存请求;
  • Shared Memory 访问;
  • Global Memory 访问;
  • Local Memory 访问;
  • Store 数据组织;
  • 部分原子操作。

可以简化为:

十一、Load/Store Unit 的作用

图示:十一、Load/Store Unit 的作用

因此一个 Kernel 即使显存带宽没有跑满,也可能受 LSU 吞吐率限制。


十二、L1 Cache 的基本作用

每个 SM 附近通常有 L1 Cache。

线程访问 Global Memory 时,可能先检查 L1:

Warp 发出 Load

L1 命中?
 ├── 是:快速返回
 └── 否:访问 L2

L1 的优势:

  • 距离执行单元近;
  • 延迟较低;
  • 保存最近访问的数据;
  • 利用空间和时间局部性。

但是 L1 容量有限,而且 SM 上同时有大量线程,因此数据可能很快被替换。


十三、L1 与 Shared Memory 的关系

在部分 NVIDIA GPU 架构中,L1 Cache 和 Shared Memory 会共享或统一使用一部分片上 SRAM 资源。

可以抽象为:

一块片上存储资源
├── 一部分作为 L1
└── 一部分作为 Shared Memory

具体容量配置和组织随架构变化。

二者的本质区别仍然是:

L1:硬件自动管理
Shared Memory:程序显式管理

Shared Memory 的数据位置和生命周期由程序控制,因此更可预测。

L1 使用方便,但命中与替换通常不能完全由程序员保证。


十四、L2 Cache 是整个 GPU 的共享缓存

所有 SM 通常共享 L2 Cache:

SM 0 ─┐
SM 1 ─┤
SM 2 ─┼→ L2 Cache → 显存控制器
SM 3 ─┘

L2 的作用包括:

  • 缓存显存数据;
  • 减少重复片外访问;
  • 汇聚不同 SM 的请求;
  • 连接内存控制器;
  • 支持原子和一致性相关功能;
  • 缓存写回数据。

L2 容量通常比 L1 大,但延迟也更高。


十五、为什么不同 SM 可以从 L2 受益

假设 SM 0 和 SM 1 都读取相同权重。

SM 0 第一次访问时:

L2 未命中
→ 从显存读取
→ 数据进入 L2

随后 SM 1 访问:

L2 命中
→ 不必再次访问显存

因此 L2 可以支持跨 SM 的数据复用。

在神经网络中,权重可能被多个 Block 使用,因此大容量 L2 对性能非常重要。


十六、Cache Set 和 Way 的基本概念

缓存不能简单保存任意内存地址。

内存地址通常被拆成:

Tag
Index
Offset

其中:

  • Offset:选择 Cache Line 内的字节;
  • Index:选择缓存中的某个 Set;
  • Tag:判断当前缓存块是不是目标地址。

一个组相联缓存可以表示为:

Set 0:Way 0、Way 1、Way 2、Way 3
Set 1:Way 0、Way 1、Way 2、Way 3
...

如果多个活跃地址映射到同一 Set,而 Way 数量不足,就会发生冲突替换。

这称为:

Conflict Miss,冲突未命中

程序员通常不直接控制具体 Set,但规则步长访问有时会形成不理想的映射。


十七、三类典型 Cache Miss

1. Compulsory Miss

第一次访问某数据,缓存中不可能已经存在。

也称冷启动未命中。

2. Capacity Miss

程序工作集大于缓存容量,旧数据被替换。

3. Conflict Miss

多个地址反复映射到同一缓存组,即使总容量足够,也可能相互驱逐。

理解这三种未命中有助于判断:

  • 数据是否值得缓存;
  • Tile 是否太大;
  • 访问步长是否不合理;
  • 数据布局是否需要调整。

十八、Store 操作如何处理

线程执行:

C[i] = value;

时,Warp 同样会生成多个写地址。

硬件需要:

  1. 合并相邻写请求;
  2. 形成写事务;
  3. 更新缓存或写缓冲;
  4. 最终写入显存。

连续写入通常最理想:

Thread 0 → C[0]
Thread 1 → C[1]
...

随机写入可能导致:

  • 更多事务;
  • 写合并效率低;
  • Cache Line 部分更新;
  • 原子冲突;
  • 内存系统压力增大。

十九、部分写入为什么复杂

假设内存系统按较大数据块处理,但线程只修改其中4字节。

硬件可能需要维护:

  • 哪些字节有效;
  • 哪些字节被修改;
  • 是否需要读取旧数据;
  • 如何合并多个线程写入;
  • 何时写回显存。

这会使用 Byte Enable、Write Mask 或类似机制。

如果一个 Warp 连续写满一个数据块,通常比零散地修改很多数据块更高效。


二十、什么是 Write-Through 和 Write-Back

缓存写策略常见有两类。

Write-Through

写缓存时,同时把数据向下一层写出。

优点:

  • 下层数据较新;
  • 控制相对直接。

缺点:

  • 写流量较大。

Write-Back

先只更新缓存,并把该 Cache Line 标记为 Dirty。

以后被替换时再写回下一层。

优点:

  • 多次修改可以合并;
  • 降低下层写流量。

缺点:

  • 控制更复杂;
  • 需要维护脏状态。

GPU 不同缓存路径和访问类型可能使用不同策略,不能用单一模型概括所有操作。

入门时只需要理解:

Store 不一定立即穿透到物理显存,可能经过缓存和写缓冲。

这也是并行程序需要内存屏障和一致性语义的原因之一。


二十一、显存控制器是什么

L2 Cache 后面连接一个或多个显存控制器。

简化结构:

SM 请求

L2 Cache

内存分区

Memory Controller

GDDR / HBM

显存控制器负责:

  • 接收读写请求;
  • 调度访问顺序;
  • 映射到内存通道、Bank 和 Row;
  • 管理读写切换;
  • 生成 DRAM 时序;
  • 处理刷新;
  • 进行 ECC 相关操作。

它是 GPU 芯片与外部显存之间的接口。


二十二、为什么有多个内存分区

单个内存控制器无法提供整个 GPU 所需的带宽。

因此 GPU 通常把内存系统划分成多个并行分区:

二十二、为什么有多个内存分区

图示:二十二、为什么有多个内存分区

不同地址会映射到不同分区。

如果请求均匀分散到多个分区,就可以并行传输。

如果大量请求集中到同一个分区,其他分区空闲,就可能形成 Partition Camping。


二十三、地址交织 Address Interleaving

为了让连续地址均匀利用多个内存控制器,地址通常会交织映射到不同分区。

例如一个简化的四分区系统:

二十三、地址交织 Address Interleaving

图示:二十三、地址交织 Address Interleaving

这样连续的大块访问可以同时使用多个控制器。

实际地址映射通常更复杂,可能结合:

  • 地址位;
  • XOR;
  • 通道;
  • Bank;
  • Row;
  • Column。

厂商一般不会完全公开所有细节。


二十四、为什么某些固定步长访问很慢

假设地址映射中的某些低位决定内存分区。

如果访问步长恰好使所有线程落到同一分区:

地址 0
地址 1024
地址 2048
地址 3072

可能出现:

Partition 0:非常繁忙
Partition 1:空闲
Partition 2:空闲
Partition 3:空闲

即使总显存带宽很高,实际也只能使用其中一部分。

这与 Shared Memory Bank Conflict 思想相似:

Shared Memory:
请求集中到同一 Bank

Global Memory:
请求集中到同一分区或通道

二十五、DRAM 内部结构

无论 GDDR 还是 HBM,本质上都属于 DRAM。

可以粗略理解为:

二十五、DRAM 内部结构

图示:二十五、DRAM 内部结构

访问一个数据通常涉及:

  1. 选择通道;
  2. 选择 Bank;
  3. 激活 Row;
  4. 读取或写入 Column;
  5. 必要时关闭或切换 Row。

不同 Bank 可以一定程度并行工作。


二十六、Row Buffer 是什么

DRAM Bank 中,一整行数据会先被激活到 Row Buffer。

如果后续请求访问同一行:

Row Hit

可以直接读写相应列,效率较高。

如果访问不同的行:

Row Miss / Row Conflict

需要:

  1. 关闭当前行;
  2. 激活新行;
  3. 再进行列访问。

因此具有局部性的连续访问通常比完全随机访问效率高。


二十七、显存控制器为什么要重排请求

假设请求顺序为:

请求 A:访问 Row 0
请求 B:访问 Row 5
请求 C:访问 Row 0

严格按顺序执行可能是:

打开 Row 0
切换到 Row 5
再切回 Row 0

效率较低。

控制器可能在满足正确性约束的前提下调整顺序:

先处理 A 和 C
再处理 B

这样可以增加 Row Hit。

但重排还需要考虑:

  • 读写依赖;
  • 原子操作;
  • 公平性;
  • 请求等待时间;
  • 内存一致性。

二十八、GDDR 是什么

GDDR 是 Graphics Double Data Rate Memory。

常见特征:

  • 每颗显存芯片接口较宽;
  • 工作速率高;
  • 通常分布在 GPU 芯片周围;
  • 通过 PCB 走线连接;
  • 常用于消费级和专业图形 GPU。

简化结构:

GDDR 芯片 ─┐
GDDR 芯片 ─┼→ PCB 走线 → GPU
GDDR 芯片 ─┘

优势:

  • 技术成熟;
  • 容量和成本较灵活;
  • 适合显卡板级设计。

限制:

  • PCB 走线多;
  • 功耗较高;
  • 总线扩展占用封装和板级空间;
  • 带宽提升难度逐渐增加。

二十九、HBM 是什么

HBM 是 High Bandwidth Memory。

它把多层 DRAM Die 垂直堆叠,并通过很宽的接口连接到 GPU。

简化结构:

二十九、HBM 是什么

图示:二十九、HBM 是什么

HBM 的核心特点:

  • 接口非常宽;
  • 单个引脚速率不一定极端高;
  • 总带宽很高;
  • 每比特能耗较低;
  • 与 GPU 封装距离近。

三十、GDDR 与 HBM 的差异

对比GDDRHBM
连接方式PCB 板级连接先进封装内连接
总线宽度相对较窄、速率高非常宽
带宽通常更高
能效相对较低通常较高
成本相对低较高
封装复杂度较低
常见应用消费显卡、工作站AI、HPC、数据中心

HBM 特别适合高端 AI GPU,因为 Tensor Core 的算力非常高,需要巨大的数据供应能力。


三十一、为什么 HBM 带宽高但延迟仍不低

HBM 的优势主要是宽接口和高并行度。

这提高的是:

单位时间传输的数据总量

也就是带宽。

但一次访问仍需要经过:

SM
→ L1
→ L2
→ 内存控制器
→ DRAM Bank
→ 数据返回

所以单次随机访问延迟仍然较高。

因此 GPU 仍然需要:

  • Cache;
  • 大量 Warp;
  • 预取;
  • 双缓冲;
  • 数据复用。

HBM 并没有消除内存延迟。


三十二、Burst Transfer

DRAM 通常不会每次只传一个字节,而是按 Burst 连续传输多个数据。

例如一个读命令可能触发:

连续若干个数据拍

这适合连续访问。

如果程序只使用 Burst 中很少的数据,其余数据就浪费了带宽。

因此 GPU 最喜欢:

连续
对齐
大块
规则

的数据访问。


三十三、内存并行性

显存延迟高,但可以同时保留许多未完成请求。

例如:

Warp 0 → 请求 A
Warp 1 → 请求 B
Warp 2 → 请求 C
Warp 3 → 请求 D

这些请求可以分布到:

  • 不同内存通道;
  • 不同 Bank;
  • 不同分区。

这称为 Memory-Level Parallelism。

GPU 通过大量 Warp 产生大量并行内存请求,从而提高显存系统利用率。

所以隐藏显存延迟不仅依靠 Warp 切换,还依靠:

同时在途的大量内存请求

三十四、为什么线程太少会导致带宽跑不满

假设只有一个 Warp 发起显存请求。

它等待数据时,内存系统中可能只有少量请求。

显存的多个通道和 Bank 无法充分利用。

如果有大量 Warp:

Warp 0:请求分区 0
Warp 1:请求分区 1
Warp 2:请求分区 2
Warp 3:请求分区 3
...

内存系统可以并行处理更多请求。

因此达到高显存带宽需要:

  • 合并访存;
  • 足够多并发 Warp;
  • 足够多在途请求;
  • 请求均匀分布;
  • 较少依赖阻塞。

三十五、缓存命中率高不一定总是性能高

高命中率通常有利,但不能单独判断性能。

例如:

L1 命中率很高

但所有访问高度串行,并且存在长依赖链,性能仍可能不高。

或者:

缓存命中率较低

但访问完全连续、显存带宽充分利用,程序可能仍然很快。

因此需要综合观察:

  • 命中率;
  • 有效带宽;
  • 内存事务数量;
  • Warp 停顿原因;
  • 数据复用;
  • 并发请求数量。

三十六、ECC 是什么

ECC 是 Error-Correcting Code,用于检测和纠正存储数据中的位错误。

显存可能受到:

  • 电气噪声;
  • 宇宙射线;
  • 器件老化;
  • 传输错误;

等影响。

对于长时间运行的大型 AI 和 HPC 系统,单比特错误可能造成:

  • 计算结果错误;
  • 模型参数损坏;
  • 程序崩溃;
  • 静默数据错误。

ECC 可以为数据增加冗余校验位。


三十七、ECC 的代价

ECC 不是免费的。

可能带来:

  • 一部分额外存储位;
  • 编码和校验逻辑;
  • 功耗;
  • 轻微延迟;
  • 可用显存容量或有效带宽开销。

具体开销取决于:

  • 显存类型;
  • ECC 组织方式;
  • 是否采用片上 ECC;
  • 是否有专用校验数据通路。

高端数据中心 GPU 通常更重视可靠性,因此会支持更完整的 ECC 和 RAS 机制。


三十八、一次 Global Load 的完整路径

现在把整个过程串起来。

Warp 执行:

float x = A[i];

可能经历:

1. Warp Scheduler 发射 Load 指令
2. 各线程从寄存器读取索引
3. INT/地址单元计算虚拟地址
4. 地址转换为物理地址
5. LSU 根据 Active Mask 收集地址
6. 地址按数据块合并为内存事务
7. 查询 L1 Cache
8. L1 未命中则请求 L2
9. L2 未命中则发送到对应内存分区
10. 显存控制器调度请求
11. DRAM 激活目标 Row 和 Column
12. 数据通过 GDDR/HBM 接口返回
13. 数据进入 L2
14. 可能进入 L1
15. 返回给对应 Warp
16. 写入各线程目标寄存器
17. Scoreboard 将寄存器标记为 Ready
18. Warp 可以继续执行依赖指令

整个过程中,其他 Warp 可以继续执行。


三十九、地址转换与 TLB

GPU 程序看到的通常是虚拟地址。

硬件需要将虚拟地址转换为物理地址。

为了避免每次都查询页表,会使用 TLB:

Translation Lookaside Buffer

可以理解为地址转换缓存:

虚拟页号

TLB 命中?
 ├── 是:快速得到物理页号
 └── 否:进行页表遍历

如果访问模式跨越大量内存页,TLB 命中率下降,也可能影响性能。

这类问题在:

  • 超大张量;
  • 随机访问;
  • 稀疏计算;
  • 图计算;

中更常见。


四十、为什么大页可能有帮助

假设普通页较小,大型数组会跨越很多页:

Page 0
Page 1
Page 2
...

TLB 需要保存大量映射。

使用更大的内存页后,同样的数据范围需要更少页表项:

页数量减少
→ TLB 覆盖范围增加
→ TLB Miss 可能减少

不过页大小、统一内存和操作系统管理涉及更复杂的软件栈,应用通常不会直接手工控制所有细节。


四十一、统一内存的基本概念

统一内存为 CPU 和 GPU 提供统一的虚拟地址空间。

程序可以使用同一个指针访问数据。

但统一地址不代表数据同时存在于 CPU 和 GPU 的物理存储中。

数据可能需要在:

CPU 内存

GPU 显存

之间迁移。

如果 GPU 访问的数据当前在 CPU 内存,可能发生 Page Fault 和迁移。

这种迁移开销可能远高于普通显存访问。

因此统一内存简化了编程,但性能仍需要关注数据驻留位置和预取。


四十二、PCIe 与显存带宽的区别

CPU 向独立 GPU 传输数据通常使用 PCIe 或其他互连。

需要区分:

GPU 内部显存带宽

和:

CPU ↔ GPU 传输带宽

GPU 内部 HBM/GDDR 带宽通常远高于 PCIe。

例如程序流程:

CPU 数据
   ↓ PCIe
GPU 显存
   ↓ HBM/GDDR
GPU 计算核心

如果频繁把小批数据在 CPU 和 GPU 之间来回复制,PCIe 可能成为瓶颈。

因此通常希望:

数据一次传入 GPU

在 GPU 上完成尽可能多的计算

最后只传回结果

四十三、零拷贝为什么不一定快

某些情况下 GPU 可以直接访问主机内存,避免显式复制。

这通常称为零拷贝或映射主机内存。

优点:

  • 简化数据传输;
  • 适合少量、一次性或流式数据。

缺点:

  • GPU 每次访问可能通过 PCIe;
  • 延迟高;
  • 带宽远低于本地显存;
  • 不适合大量重复访问。

所以零拷贝中的“零”是指减少显式复制,不是指访问没有成本。


四十四、如何优化 Global Memory 访问

可以总结成几个原则。

1. 让同一 Warp 访问连续地址

x = A[global_thread_id];

通常优于随机索引。

2. 保证适当对齐

避免连续访问跨越不必要的事务边界。

3. 减少无效传输

让实际使用的数据占传输块的较大比例。

4. 增加数据复用

将反复使用的数据放进:

  • Cache;
  • Shared Memory;
  • Register。

5. 避免大跨度和规则冲突步长

防止请求集中到少数分区或造成大量事务。

6. 选择合适数据布局

根据访问方式选择:

  • AoS;
  • SoA;
  • NCHW;
  • NHWC;
  • 特殊打包格式。

7. 提供足够内存级并行性

让 GPU 同时保持足够多的在途请求。


四十五、如何判断内存访问是否有问题

性能分析时可以关注:

Global Load/Store Efficiency
Memory Transactions
L1/L2 Hit Rate
DRAM Throughput
Warp Stall on Memory Dependency
Memory Partitions Utilization
Requested Bytes
Transferred Bytes

如果:

请求的有效数据量很少
实际传输数据量很大

说明访存合并或对齐可能有问题。

如果:

DRAM 带宽很低
但 Warp 大量等待内存

可能原因包括:

  • 随机访问;
  • 并行度不足;
  • 地址依赖链;
  • 分区不均衡;
  • TLB Miss;
  • 小事务过多。

本课核心结论

第一,线程发出的单个字节或标量访问,最终会被组织成 Cache Line、Sector 或内存事务。

第二,同一 Warp 中连续且对齐的地址最容易形成高效合并访存。

第三,不连续访问会产生更多事务,降低有效带宽。

第四,跨 Cache Line 和未对齐访问可能增加额外数据传输。

第五,AoS 和 SoA 的优劣取决于 Warp 实际读取哪些字段。

第六,L1 位于 SM 附近,L2 由整个 GPU 共享。

第七,缓存按 Cache Line 管理数据,而不是按单个变量管理。

第八,显存系统通常包含多个内存分区和控制器,地址会交织映射。

第九,请求集中到少数分区会降低总带宽利用率。

第十,GDDR 通过板级高速接口连接,HBM 通过先进封装提供极宽接口。

第十一,HBM 主要提高带宽和能效,并没有消除单次访问延迟。

第十二,GPU 依靠大量在途内存请求和 Warp 调度提高内存级并行性。

第十三,ECC 提高数据可靠性,但会增加存储和逻辑开销。

第十四,一次 Global Load 会经过地址生成、合并、Cache、内存控制器、DRAM 和寄存器写回等多个阶段。

第十五,GPU 内部显存带宽与 CPU—GPU 之间的 PCIe 带宽不是同一个概念。

下一课进入 GPU 的任务调度与资源分配:

第十二课:Block 如何被分配到 SM,以及 Occupancy 到底由什么决定

将包括:

SM 能同时驻留多少 Block
寄存器限制
Shared Memory 限制
Warp 数量限制
线程数量限制
Block 数量上限
Occupancy 计算
为什么 100% Occupancy 不一定最快
如何选择每个 Block 的线程数量
Persistent Kernel 是什么

第三课:GPU 的存储层次

这一部分讲 GPU 的存储层次,以及为什么同一个程序只是改变数据访问方式,性能就可能相差数倍。

第二十七课:GPU 架构学习总结与统一分析框架

最后一课将把前面所有内容压缩成一套统一分析框架,并给出面向 GPU 架构与 RTL 设计的后续实践路线。

第二十六课:如何从零设计一个简化的 SIMT GPU

这一课从 RTL 设计角度搭建一颗最小可运行的 SIMT GPU。重点是模块边界、状态机和实现顺序,而不是一开始复制完整商用 GPU。