GPU 架构学习

第七课:GPU 是算力受限,还是带宽受限

这一课建立 GPU 性能分析的核心框架:先判断程序受算力限制,还是受显存带宽限制。

目录一、GPU 执行程序需要两件事二、什么是 FLOPS三、什么是显存带宽四、什么是 Arithmetic Intensity数组加法的计算强度FMA 操作的计算强度五、内存带宽能够支持多少算力六、什么是 Compute Bound七、什么是 Memory Bound八、Roofline Model九、转折点在哪里十、为什么矩阵乘法适合 GPU朴素矩阵乘法的问题使用 Tiling 后十一、计算强度不是程序语句数量十二、缓存如何影响 Roofline十三、低计算单元利用率不一定说明程序写得差十四、如何计算有效内存带宽十五、如何计算实际 FLOPS十六、为什么实际性能达不到理论 Roofline十七、一个 Kernel 可能同时经历不同阶段十八、怎样提高 Memory Bound 程序性能1. 合并访存2. 提高数据复用3. 融合 Kernel4. 使用更低位宽5. 改善数据布局十九、怎样提高 Compute Bound 程序性能1. 使用合适的执行单元2. 降低精度3. 增加指令级并行性4. 减少无效计算5. 保证足够并行度6. 减少执行管线竞争二十、Kernel Fusion 的权衡二十一、为什么神经网络中的算子表现不同矩阵乘法和大卷积ReLU、Clamp、Elementwise AddSoftmaxLayerNormAttention二十二、如何初步判断一个 Kernel 的瓶颈二十三、使用性能分析工具时看什么二十四、一个常见误区:带宽没有跑满就不是 Memory Bound二十五、另一个误区:计算强度越高一定越快二十六、从硬件架构角度理解 Roofline二十七、本课核心结论第八课:GPU 如何完成矩阵乘法

导语:这一课建立 GPU 性能分析的核心框架:先判断程序受算力限制,还是受显存带宽限制。

GPU 标称算力可能达到几十甚至几百 TFLOPS,但实际程序经常只能达到峰值算力的一小部分。

这通常不是 GPU “没有发挥作用”,而是程序受到其他因素限制。最基本的性能分类是:

Compute Bound:计算受限
Memory Bound:内存带宽受限

判断属于哪一种,是 GPU 性能分析的第一步。


一、GPU 执行程序需要两件事

任何计算都可以粗略拆成:

搬运数据
+
执行运算

例如:

C[i] = A[i] + B[i];

每个元素需要:

  1. 读取 A[i]
  2. 读取 B[i]
  3. 执行一次加法;
  4. 写回 C[i]

其中真正的算术操作只有一次,但需要传输三个浮点数。

如果使用 FP32,每个数占 4 字节,因此每个元素大约需要:

4+4+4=12 Bytes4+4+4=12\text{ Bytes}

而计算量只有:

1 FLOP1\text{ FLOP}

这说明它需要搬运很多数据,但计算很少。


二、什么是 FLOPS

FLOPS 是:

Floating Point Operations Per Second,每秒浮点运算次数。

例如:

1012 FLOPS=1 TFLOPS10^{12}\text{ FLOPS}=1\text{ TFLOPS}

一块 GPU 的 FP32 峰值算力为 60 TFLOPS,表示理想情况下每秒可以完成约:

60×101260\times10^{12}

次浮点运算。

需要注意,峰值算力通常是根据硬件资源计算出的理论值:

peak compute=execution units×frequency×ops per cycle\text{peak compute} = \text{execution units} \times \text{frequency} \times \text{ops per cycle}

例如一个 FMA:

D=A×B+CD=A\times B+C

通常被计算为两个浮点操作:

  • 一次乘法;
  • 一次加法。

因此一个执行单元每周期执行一次 FMA,通常记为每周期 2 FLOPs。


三、什么是显存带宽

显存带宽表示 GPU 每秒理论上可以从显存读取或写入多少数据。

例如:

1 TB/s1\text{ TB/s}

表示理论上每秒可以传输约 1 TB 数据。

它主要由以下因素决定:

  • 显存类型;
  • 显存频率;
  • 显存总线宽度;
  • 显存控制器数量;
  • HBM 或 GDDR 的通道组织。

理论带宽可以粗略表示为:

bandwidth=bus width×data rate\text{bandwidth} = \text{bus width} \times \text{data rate}

GPU 同时可能拥有:

很高的计算吞吐率
+
很高的显存带宽

但两者的增长速度并不总是一致。

现代 GPU 的计算能力增长往往比显存带宽增长更快,所以许多简单程序更容易受到带宽限制。


四、什么是 Arithmetic Intensity

Arithmetic Intensity 可以翻译为:

算术强度或计算强度。

定义为:

AI=FLOPsbytes transferred from memoryAI= \frac{\text{FLOPs}} {\text{bytes transferred from memory}}

单位通常为:

FLOPs/Byte\text{FLOPs/Byte}

它回答的问题是:

每从内存搬运一个字节,程序能够完成多少次计算?


数组加法的计算强度

对于:

C[i] = A[i] + B[i];

如果忽略缓存,每个元素:

  • 读取 A:4 Bytes;
  • 读取 B:4 Bytes;
  • 写入 C:4 Bytes;
  • 计算一次加法:1 FLOP。

所以:

AI=1120.083 FLOPs/ByteAI=\frac{1}{12}\approx0.083 \text{ FLOPs/Byte}

计算强度非常低。

这意味着程序每搬运大量数据,只做很少计算,通常属于显存带宽受限。


FMA 操作的计算强度

例如:

C[i] = A[i] * B[i] + C[i];

每个元素大约需要:

  • 读取 A:4 Bytes;
  • 读取 B:4 Bytes;
  • 读取 C:4 Bytes;
  • 写回 C:4 Bytes;
  • 一次乘法和一次加法:2 FLOPs。

因此:

AI=216=0.125 FLOPs/ByteAI=\frac{2}{16}=0.125 \text{ FLOPs/Byte}

虽然使用了 FMA,计算强度仍然很低。


五、内存带宽能够支持多少算力

假设某块 GPU 的显存带宽是:

1 TB/s1\text{ TB/s}

数组加法的计算强度约为:

0.083 FLOPs/Byte0.083\text{ FLOPs/Byte}

那么显存最多支持的实际计算吞吐率约为:

1×1012 Bytes/s×0.083 FLOPs/Byte1\times10^{12}\text{ Bytes/s} \times 0.083\text{ FLOPs/Byte}

得到:

83×109 FLOPs/s=83 GFLOPS83\times10^9\text{ FLOPs/s} = 83\text{ GFLOPS}

即使 GPU 的理论计算能力是 60 TFLOPS,这个数组加法也可能只能达到几十 GFLOPS 量级。

原因不是 CUDA Core 不够强,而是数据供应速度限制了计算。

峰值算力:60 TFLOPS
带宽允许数组加法达到:约 0.083 TFLOPS

大量执行单元只能等待数据。


六、什么是 Compute Bound

Compute Bound 表示主要瓶颈是计算单元。

典型特征是:

  • 每份数据要参与大量计算;
  • 数据复用率高;
  • 执行单元长时间忙碌;
  • 显存带宽尚未达到极限;
  • 增加计算能力可能提高性能。

例如大规模矩阵乘法:

C=A×BC=A\times B

每个输出元素:

Cij=kAikBkjC_{ij}=\sum_k A_{ik}B_{kj}

需要执行大量乘加运算。

如果 A 和 B 中的数据被加载到 Shared Memory 或寄存器后反复复用,则每搬运一个字节,可以进行很多次计算。

这类程序计算强度较高,更可能成为 Compute Bound。


七、什么是 Memory Bound

Memory Bound 表示主要瓶颈是数据搬运。

典型特征是:

  • 每个数据只使用一次或少数几次;
  • 算术操作很简单;
  • 显存带宽接近饱和;
  • 计算单元利用率不高;
  • 增加 CUDA Core 数量帮助很小。

典型例子包括:

数组复制
数组加法
简单逐像素运算
向量缩放
部分稀疏操作
简单激活函数

例如 ReLU:

y[i] = max(x[i], 0.0f);

每个元素只需要:

  • 读取 x;
  • 比较;
  • 写回 y。

计算很少,数据搬运较多,因此通常也是带宽受限。


八、Roofline Model

Roofline Model 是分析计算性能的常用模型。

它使用两个硬件上限:

  1. 峰值计算性能;
  2. 峰值内存带宽。

程序理论性能受到下面公式限制:

Pmin(Ppeak,Bmemory×AI)P \leq \min \left( P_{\text{peak}}, B_{\text{memory}}\times AI \right)

其中:

  • PP 是实际性能;
  • PpeakP_{\text{peak}} 是峰值计算性能;
  • BmemoryB_{\text{memory}} 是显存带宽;
  • AIAI 是计算强度。

直观表示为:

八、Roofline Model

图示:八、Roofline Model

左边是斜线区域:

P=B×AIP=B\times AI

计算强度越高,内存能够支持的计算性能越高。

右边是水平区域:

P=PpeakP=P_{\text{peak}}

即使继续增加计算强度,也不能超过硬件峰值算力。


九、转折点在哪里

Roofline 的转折点满足:

B×AI=PpeakB\times AI=P_{\text{peak}}

因此:

AIridge=PpeakBAI_{\text{ridge}} = \frac{P_{\text{peak}}} {B}

这个值称为 Ridge Point,可以理解为从带宽受限转向计算受限所需的最低计算强度。

假设 GPU:

Ppeak=60 TFLOPSP_{\text{peak}}=60\text{ TFLOPS}

显存带宽:

B=1 TB/sB=1\text{ TB/s}

则:

AIridge=60×10121×1012=60 FLOPs/ByteAI_{\text{ridge}} = \frac{60\times10^{12}} {1\times10^{12}} = 60\text{ FLOPs/Byte}

因此:

  • 如果程序计算强度明显小于 60 FLOPs/Byte,倾向于带宽受限;
  • 如果明显大于 60 FLOPs/Byte,才可能达到计算受限区域。

数组加法只有约:

0.083 FLOPs/Byte0.083\text{ FLOPs/Byte}

远低于 60,因此几乎不可能跑满计算峰值。


十、为什么矩阵乘法适合 GPU

假设计算两个 N×NN\times N 矩阵:

C=A×BC=A\times B

总计算量大约是:

2N3 FLOPs2N^3\text{ FLOPs}

因为每个输出元素需要 NN 次乘法和 NN 次加法。

三个矩阵的数据量大约与:

N2N^2

成正比。

因此理想情况下,计算强度大致随 NN 增加:

AINAI\propto N

矩阵越大,计算量增长速度比数据量更快。

这也是大规模矩阵乘法能够充分使用 GPU 算力的重要原因。


朴素矩阵乘法的问题

如果每做一次乘法都从 Global Memory 读取:

sum += A[row][k] * B[k][column];

那么 A 和 B 中相同的数据会被不同线程重复读取很多次。

这种实现的数据复用率较低。


使用 Tiling 后

可以把 A 和 B 的小块加载到 Shared Memory:

使用 Tiling 后

图示:使用 Tiling 后

假设一份数据从显存读取一次,却参与了 16、32 或更多次乘加,就显著提高了计算强度。

未优化:
读取数据一次 → 计算一次

分块优化:
读取数据一次 → 计算多次

这就是矩阵乘法优化的核心。


十一、计算强度不是程序语句数量

计算强度不只是看代码中有多少加法和乘法,还必须考虑实际的数据传输。

例如:

for (int k = 0; k < 100; ++k) {
    sum += A[i];
}

如果 A[i] 第一次加载后一直保存在寄存器中,那么只需要从显存读取一次,却执行 100 次累加。

计算强度较高。

如果编译器或程序每次都从显存重新加载,则数据传输量会增加,计算强度下降。

所以计算强度取决于:

  • 缓存命中;
  • 寄存器复用;
  • Shared Memory 复用;
  • 数据布局;
  • 实际访存事务;
  • 编译器生成的代码。

十二、缓存如何影响 Roofline

最简单的 Roofline 使用显存带宽。

但 GPU 有多个存储层次:

Register
Shared Memory / L1
L2
Global Memory

因此可以建立分层 Roofline:

显存带宽上限
L2 带宽上限
L1/Shared Memory 带宽上限
计算峰值上限

如果数据已经命中 L2,就不需要重新访问显存。

此时性能可能受 L2 带宽限制,而不是显存带宽限制。

如果数据在 Shared Memory 中反复使用,可能受到 Shared Memory 带宽或 Bank Conflict 限制。

所以“Memory Bound”不一定只意味着外部显存带宽受限,也可能是:

  • L1 带宽受限;
  • L2 带宽受限;
  • Shared Memory 带宽受限;
  • Load/Store Unit 吞吐率受限;
  • 地址计算受限。

十三、低计算单元利用率不一定说明程序写得差

假设一个数组复制 Kernel:

B[i] = A[i];

它几乎没有算术运算。

因此 CUDA Core 利用率很低是正常的。

正确的评价指标不是:

CUDA Core 有没有达到 100%?

而是:

显存带宽是否接近该程序可实现的上限?

对于数组复制,如果有效带宽已经很高,即使计算利用率很低,程序也可能已经接近最优。

不同类型程序应使用不同指标评价。


十四、如何计算有效内存带宽

假设复制 NN 个 FP32 数据:

B[i] = A[i];

每个元素:

  • 读取 4 Bytes;
  • 写入 4 Bytes。

总传输量约为:

8N Bytes8N\text{ Bytes}

如果执行时间为 tt,有效带宽为:

Beffective=8NtB_{\text{effective}} = \frac{8N}{t}

例如复制 1 GB 数据,读写总量约为 2 GB,执行时间为 2 ms:

Beffective=2 GB0.002 s=1000 GB/sB_{\text{effective}} = \frac{2\text{ GB}}{0.002\text{ s}} = 1000\text{ GB/s}

这比只计算输入数组大小更准确,因为需要同时考虑读和写。


十五、如何计算实际 FLOPS

假设矩阵乘法:

CM×N=AM×K×BK×NC_{M\times N} = A_{M\times K} \times B_{K\times N}

总浮点操作数大约为:

2MNK2MNK

如果执行时间为 tt,实际性能为:

P=2MNKtP= \frac{2MNK}{t}

例如:

M=N=K=4096M=N=K=4096

总操作数约为:

2×409632\times4096^3

再除以执行时间,就能得到实际 FLOPS。

需要确保使用的操作数统计规则与峰值算力统计规则一致,例如 FMA 按 2 FLOPs 计算。


十六、为什么实际性能达不到理论 Roofline

Roofline 给出的仍然是上限,而不是保证值。

程序还可能受到以下限制:

分支发散
访存不合并
缓存未命中
Shared Memory Bank Conflict
寄存器不足
Occupancy 太低
指令依赖链过长
执行单元分布不匹配
同步等待
原子操作竞争
Kernel 启动开销

例如一个程序计算强度很高,但所有计算都是特殊函数,受 SFU 数量限制,它仍然无法达到 FP32 峰值算力。

又例如大量整数地址计算可能使 INT 管线成为瓶颈。

因此更准确的说法是:

Pactualmin(all relevant hardware limits)P_{\text{actual}} \leq \min(\text{all relevant hardware limits})

Roofline 只是先抓住最重要的计算和内存两类上限。


十七、一个 Kernel 可能同时经历不同阶段

假设某个 Kernel:

  1. 从显存读取数据;
  2. 在寄存器中进行大量计算;
  3. 将结果写回显存。

加载阶段可能是 Memory Bound,计算阶段可能是 Compute Bound。

从整体测量时,最终表现取决于各阶段占比,以及它们能否通过流水线重叠。

所以一个 Kernel 并不一定能被绝对地贴上单一标签。

但通常可以判断它的主导瓶颈。


十八、怎样提高 Memory Bound 程序性能

对于内存受限程序,优化方向不是继续增加计算单元,而是减少数据搬运或提高有效带宽。

1. 合并访存

让同一个 Warp 中线程访问连续地址:

x = A[global_thread_id];

避免大跨度、随机访问。

2. 提高数据复用

将频繁使用的数据保存在:

  • 寄存器;
  • Shared Memory;
  • Cache。

3. 融合 Kernel

假设原来有:

Kernel 1:读取 A,生成 B
Kernel 2:读取 B,生成 C
Kernel 3:读取 C,生成 D

每个 Kernel 都要把中间结果写入显存,再读回来。

融合后:

一个 Kernel:
读取 A
→ 在寄存器中生成 B
→ 继续生成 C
→ 生成 D
→ 最后只写一次

这叫 Kernel Fusion。

它可以显著减少 Global Memory 访问。

但融合也可能增加寄存器使用量,降低 Occupancy,因此需要权衡。

4. 使用更低位宽

例如把 FP32 改为 FP16:

FP32:4 Bytes
FP16:2 Bytes
INT8:1 Byte

相同带宽下可以传输更多元素。

但前提是精度允许,并且硬件支持高效处理该数据格式。

5. 改善数据布局

例如选择行优先、列优先、通道优先或其他布局,使线程访问更连续。


十九、怎样提高 Compute Bound 程序性能

如果程序计算受限,重点是提高执行单元利用率。

常见方向包括:

1. 使用合适的执行单元

矩阵计算尽可能使用 Tensor Core,而不是全部拆成普通 FP32 指令。

2. 降低精度

在允许的情况下使用:

TF32
FP16
BF16
FP8
INT8
FP4

低精度通常可以显著提高吞吐率。

3. 增加指令级并行性

减少长依赖链,使用多个独立累加器。

4. 减少无效计算

避免大量被谓词屏蔽的指令和严重分支发散。

5. 保证足够并行度

提供足够多 Block 和 Warp,使所有 SM 都有工作。

6. 减少执行管线竞争

避免所有指令集中使用某一类稀缺资源,例如 SFU 或原子单元。


二十、Kernel Fusion 的权衡

Kernel Fusion 常用于减少内存访问,但并不是总是有利。

融合前:

Kernel A:
寄存器使用少
并行度高

Kernel B:
寄存器使用少
并行度高

融合后:

Kernel AB:
需要同时保存更多中间变量
寄存器使用增加
Occupancy 下降

因此可能出现:

显存访问减少
但并行度也下降

最终性能要看哪种影响更大。

GPU 优化经常不是单一指标越高越好,而是在以下资源之间权衡:

  • 寄存器;
  • Shared Memory;
  • Occupancy;
  • 内存流量;
  • 指令数量;
  • 数据复用。

二十一、为什么神经网络中的算子表现不同

不同神经网络算子的计算强度差异很大。

矩阵乘法和大卷积

数据可以被大量复用,通常计算强度较高,容易使用 Tensor Core。

ReLU、Clamp、Elementwise Add

每个元素计算很少,通常更偏向显存带宽受限。

Softmax

包含:

  • 归约;
  • 指数计算;
  • 数据读取和写回;
  • 多阶段同步。

它可能同时受显存带宽、SFU、归约和同步限制。

LayerNorm

需要读取数据、计算均值和方差、再次处理数据,通常涉及多次数据访问和归约,常偏向带宽受限。

Attention

不同阶段表现不同:

QKᵀ 矩阵乘法:偏计算密集
Softmax:偏带宽和特殊函数
Softmax×V:矩阵乘法

FlashAttention 的核心思想之一,就是通过分块和融合减少中间矩阵写回显存,从而降低内存流量。


二十二、如何初步判断一个 Kernel 的瓶颈

可以按以下顺序分析。

第一步,统计计算量:

FLOPs\text{FLOPs}

第二步,统计需要从显存传输的数据量:

Bytes\text{Bytes}

第三步,计算算术强度:

AI=FLOPsBytesAI=\frac{\text{FLOPs}}{\text{Bytes}}

第四步,计算硬件转折点:

AIridge=peak computememory bandwidthAI_{\text{ridge}} = \frac{\text{peak compute}} {\text{memory bandwidth}}

第五步,进行比较:

AI 远小于转折点
→ 大概率带宽受限

AI 远大于转折点
→ 可能计算受限

之所以说“可能”,是因为还需要检查其他瓶颈。


二十三、使用性能分析工具时看什么

以 NVIDIA Nsight Compute 为例,分析时通常关注:

DRAM Throughput
L2 Throughput
SM Throughput
Tensor Core Utilization
Achieved Occupancy
Eligible Warps
Warp Stall Reasons
Branch Efficiency
Memory Coalescing

如果看到:

显存带宽接近峰值
SM 计算利用率不高

通常说明程序偏向带宽受限。

如果看到:

SM 或 Tensor Core 利用率很高
显存带宽没有饱和

通常说明程序偏向计算受限。

如果两者都不高,则可能存在:

  • 并行度不足;
  • 长延迟依赖;
  • 分支发散;
  • 非合并访存;
  • 同步等待;
  • 指令吞吐瓶颈。

二十四、一个常见误区:带宽没有跑满就不是 Memory Bound

不一定。

程序可能因为访存不合并,只实现了很低的有效带宽。

例如理论显存带宽是 1 TB/s,但随机访问导致每个内存事务只利用很少一部分数据。

性能仍然受内存访问限制,只是没有高效使用总线。

因此要区分:

带宽受限

和:

高效达到峰值带宽

前者表示程序被数据访问限制,后者表示内存系统已经被良好利用。


二十五、另一个误区:计算强度越高一定越快

计算强度高只说明程序每搬运一个字节执行的计算更多。

但如果计算本身非常复杂,执行时间仍然可能很长。

例如大量 expsin、除法或复杂整数操作,计算强度可能很高,但受特殊执行单元吞吐率限制。

计算强度主要用于判断资源瓶颈,而不是直接说明程序绝对速度。


二十六、从硬件架构角度理解 Roofline

GPU 芯片面积主要分配给:

计算单元
寄存器
缓存
片上互连
内存控制器
调度和控制逻辑

如果增加更多 CUDA Core,峰值计算能力提高。

但如果显存带宽没有同步提升,低计算强度程序不会明显加速。

相反,如果增加 HBM 通道和内存控制器,带宽提高,但高计算强度程序仍可能受计算单元限制。

因此 GPU 架构设计本质上需要平衡:

compute throughputanddata supply\text{compute throughput} \quad\text{and}\quad \text{data supply}

这也是为什么 GPU 架构不仅讨论 CUDA Core 数量,还要讨论:

  • HBM;
  • L2 Cache;
  • Shared Memory;
  • Tensor Core;
  • NVLink;
  • 数据压缩;
  • 内存控制器。

二十七、本课核心结论

第一,GPU 性能首先受到计算能力和数据搬运能力共同限制。

第二,计算强度定义为:

AI=operationsbytes transferredAI= \frac{\text{operations}} {\text{bytes transferred}}

第三,数组加法等逐元素操作计算强度低,通常属于带宽受限。

第四,矩阵乘法通过数据复用提高计算强度,更容易成为计算受限。

第五,Roofline 的核心公式是:

Pmin(Ppeak,B×AI)P \leq \min \left( P_{\text{peak}}, B\times AI \right)

第六,转折点为:

AIridge=PpeakBAI_{\text{ridge}} = \frac{P_{\text{peak}}}{B}

第七,Memory Bound 程序应重点减少数据搬运、改善访存和提高数据复用。

第八,Compute Bound 程序应重点提高执行单元利用率、减少依赖和使用 Tensor Core。

第九,缓存、Shared Memory 和寄存器会改变实际数据传输量,因此会影响有效计算强度。

第十,实际性能还可能受分支、同步、原子操作、执行管线和并行度限制。

下一课将学习 GPU 中最核心的计算任务之一:

第八课:GPU 如何完成矩阵乘法

重点包括:

朴素矩阵乘法如何映射到线程
为什么每个线程计算一个输出元素
Tiling 如何减少显存访问
Shared Memory 如何保存矩阵块
寄存器如何保存累加结果
线程块与输出 Tile 如何对应
Tensor Core 和普通 CUDA Core 有什么区别

第二十课:如何使用 Nsight Compute 分析 CUDA Kernel

这一课进入实际性能诊断:如何从 Nsight Compute 的报告中判断 Kernel 是计算受限、带宽受限,还是因为延迟与调度停顿而变慢。

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

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

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

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