3、访存瓶颈分析:冯诺依曼瓶颈、计算密度与访存密度、Roofline模型简介
好,咱们今天聊一个绕不开的话题——访存瓶颈。
做算子优化这些年,我最大的体会就是:很多时候,你的计算单元在“等饭吃”,而不是在“干活”。你想想看,GPU 里几千个核心,理论算力高得吓人,但实际跑起来,利用率可能连 20% 都不到。为什么?数据没喂上来。
这就是冯诺依曼架构的“原罪”。
3.1 冯诺依曼瓶颈:计算和存储的“速度鸿沟”
冯诺依曼架构把计算和存储分开了。CPU/GPU 负责算,内存负责存。两者之间靠总线传输数据。
问题来了:处理器算得越来越快,但内存的读写速度,几十年来提升非常有限。
我打个比方。你是个顶级大厨(计算单元),刀工火候样样精通,一秒钟能炒十盘菜。但你的助手(内存)递菜过来,得走一条窄窄的过道(总线),一次只能端一盘,而且走路还慢。结果呢?你大部分时间都在等菜,真正炒菜的时间少得可怜。
这个过道的宽度和速度,就是所谓的“冯诺依曼瓶颈”。
核心矛盾: 处理器的算力每年增长 ~50%,而内存带宽每年只增长 ~10%。差距越拉越大。
我记得刚入行时,调一个矩阵乘法的 kernel,怎么优化都跑不满理论峰值。后来一分析,发现 90% 的时间花在了从全局内存搬运数据上。那一刻我才真正理解:优化访存,往往比优化计算更重要。
3.2 计算密度与访存密度:两个关键指标
要量化这个瓶颈,我们需要两个工具:计算密度 和 访存密度。
说白了,就是看你的算子到底是“算得多”还是“搬得多”。
3.2.1 计算密度(Compute Intensity)
定义很简单:
计算密度 = 总计算量(FLOPs) / 总访存量(Bytes)
单位是 FLOPs/Byte。它表示:每从内存里读一个字节的数据,你能做多少次计算。
- 计算密集型算子:计算密度高。比如大矩阵乘法(GEMM)、卷积(Conv)。数据读进来,反复用很多次。
- 访存密集型算子:计算密度低。比如逐元素操作(ReLU、Add)、数据拷贝。数据读进来,只用一次就扔了。
你想想看,ReLU 就是读一个数,比较一下,写回去。计算量几乎为零,但访存量很大。这种算子,瓶颈一定在带宽上。
我的经验: 在优化时,我习惯先算一下算子的计算密度。如果小于 10 FLOPs/Byte,基本可以断定是访存瓶颈。这时候堆算力没用,得想办法减少访存。
3.2.2 访存密度(Memory Access Density)
这个指标和计算密度是“镜像”关系:
访存密度 = 总访存量(Bytes) / 总计算量(FLOPs)
单位是 Byte/FLOP。它表示:每做一次计算,需要搬多少字节的数据。
这两个指标,你选一个用就行。我个人更习惯用计算密度,因为和后面的 Roofline 模型直接挂钩。
3.3 Roofline模型简介:一眼看出瓶颈在哪
Roofline 模型,是我见过最直观的性能分析工具。没有之一。
它把计算密度作为横轴,把实际性能(FLOPs/s)作为纵轴,画出一条“屋顶”形状的线。
这张图,我建议你刻在脑子里。
这张图里,有两条线决定了性能天花板:
- 水平线:计算上限。由处理器的理论峰值算力决定。比如 A100 的 FP32 算力是 19.5 TFLOPS。
- 斜线:带宽上限。由内存带宽 × 计算密度决定。斜率就是带宽。
两条线相交的地方,叫 ridge point。这个点对应的计算密度,就是“分水岭”。
- 在 ridge point 左边(计算密度低):性能被带宽限制。你算得再快也没用,数据送不过来。这叫 访存受限(Memory-bound)。
- 在 ridge point 右边(计算密度高):性能被算力限制。数据管够,但处理器算不过来了。这叫 计算受限(Compute-bound)。
关键结论: 你的算子落在 Roofline 的哪个区域,就决定了你应该用什么策略去优化。
- 访存受限:想办法减少访存(算子融合、数据复用、使用更快的存储层级)。
- 计算受限:想办法提高计算效率(指令级并行、向量化、降低精度)。
3.4 实战:如何用 Roofline 指导算子融合
好,理论讲完了。咱们来点实际的。
假设你有一个简单的计算图:A = Conv(X),然后 B = ReLU(A)。
如果不融合,流程是这样的:
- Conv 从全局内存读 X,计算结果 A,写回全局内存。
- ReLU 从全局内存读 A,计算结果 B,写回全局内存。
这里 A 被写了两次、读了两次。对于 ReLU 这种计算密度极低的算子,这完全是浪费带宽。
如果做算子融合,把 ReLU 直接“嵌入”到 Conv 的最后一个步骤里:
- Conv 从全局内存读 X,计算得到 A(在寄存器或共享内存里)。
- 直接对 A 做 ReLU,得到 B。
- 只把 B 写回全局内存。
你看,A 这个中间结果,根本没出过芯片。访存量直接减半。
我的习惯: 拿到一个计算图,我会先画出每个算子的计算密度,然后标在 Roofline 模型上。如果发现连续几个访存受限的算子,就优先考虑融合它们。这招在优化 Transformer 模型时特别管用。
3.5 避坑指南
我曾经踩过的坑:
- 盲目融合:不是所有算子都适合融合。如果两个算子都是计算受限的,融合后寄存器压力暴增,反而可能变慢。融合前一定要算清楚。
- 忽略 ridge point 的位置:不同硬件(CPU vs GPU vs NPU)的 ridge point 差别很大。GPU 的 ridge point 通常很高(几十甚至上百 FLOPs/Byte),而 CPU 的 ridge point 很低(个位数)。在 GPU 上访存受限的算子,在 CPU 上可能反而是计算受限的。
- 只看理论带宽:实际带宽往往远低于理论值。因为内存访问有延迟、有 bank conflict、有非对齐访问。我建议用 profiler 工具(比如 ncu、perf)先测一下实际带宽,再画 Roofline。
3.6 小结
访存瓶颈是算子优化的头号敌人。冯诺依曼架构决定了这个矛盾会长期存在。
计算密度和 Roofline 模型,就是你的“诊断工具”。它们能帮你快速定位:瓶颈在计算还是在访存?然后对症下药。
记住一句话:别让计算单元闲着。如果它闲了,大概率是数据没跟上。
想要更多资料,联系可可