3、访存瓶颈分析:冯诺依曼瓶颈、计算密集型 vs 访存密集型算子、带宽与延迟的影响
好,咱们今天聊聊算子融合背后最核心的驱动力——访存瓶颈。
说实话,我刚开始做推理加速那会儿,也天真地以为只要把算力堆上去,模型就跑得快。结果呢?一上板子,发现GPU利用率低得可怜,大部分时间都在等数据。嗯,这就是典型的“冯诺依曼瓶颈”在作祟。
3.1 冯诺依曼瓶颈:搬数据比算数据慢得多
冯诺依曼架构大家都熟悉,计算单元和存储单元是分开的。CPU/GPU负责算,内存负责存,中间靠总线传数据。问题就出在这条总线上。
你想想看,算一个浮点乘法,可能只需要1个时钟周期。但从内存里把数据搬过来,可能要几百个周期。这就是所谓的“存储墙”。
核心矛盾:计算速度的增长速度(摩尔定律)远快于内存访问速度的增长速度。结果就是,处理器越来越“饿”,经常空转等数据。
我在项目中遇到过这样一个场景:一个简单的卷积层,理论算力能跑到10 TFLOPS,但实际测下来只有2 TFLOPS。为什么?因为权重和输入数据反复从DDR里搬进搬出,总线带宽成了瓶颈。说白了,计算单元大部分时间都在“摸鱼”。
这里我画了一张图,帮你直观理解这个瓶颈:
我的经验:判断一个算子是否受限于访存,最简单的方法就是看它的计算强度(Compute Intensity)。如果计算强度小于1,那基本就是访存瓶颈。我曾经优化过一个逐元素相加算子,算力只用了5%,剩下的95%都在等数据。
3.2 计算密集型 vs 访存密集型算子
搞清楚了瓶颈在哪,咱们就能把算子分成两类:
| 类型 | 特点 | 典型算子 |
|---|---|---|
| 计算密集型 | 计算时间 >> 访存时间 算力利用率高 |
大卷积核(7x7)、大矩阵乘法、全连接层 |
| 访存密集型 | 访存时间 >> 计算时间 带宽利用率高 |
逐元素操作(ReLU、Add)、归一化、小卷积核(1x1) |
为什么会这样?说白了,就是计算强度和访存量的比值不同。
举个例子,一个 3x3 卷积,输入是 256 通道,输出也是 256 通道。每次计算一个输出像素,需要读 3x3x256 = 2304 个输入值,写 1 个输出值。但计算量呢?3x3x256x256 = 589824 次乘加。算下来计算强度很高,属于计算密集型。
但如果你换成 ReLU 激活函数呢?读一个数,算一个 max(0, x),写一个数。计算强度几乎为 0。这就是典型的访存密集型。
注意:千万不要以为计算密集型就一定好。我曾经在一个项目中,把所有的算子都往计算密集型方向优化,结果发现小模型根本跑不满算力,反而因为数据搬移太多,整体延迟更高了。访存密集型算子也有它的价值,关键是要匹配硬件特性。
3.3 带宽与延迟的影响
带宽和延迟,这两个概念经常被混为一谈。我简单区分一下:
- 带宽:单位时间内能搬多少数据。好比高速公路的车道数,车道越多,同时能过的车越多。
- 延迟:从发起请求到拿到数据的时间。好比从入口到出口的通行时间,跟车道数关系不大,主要看路程。
在实际推理中,这两个指标对性能的影响完全不同:
带宽受限的场景:当你连续读取大量数据时,比如加载一整层权重。这时候带宽决定了你能多快把数据喂给计算单元。我遇到过用 DDR4 和 HBM 的对比,带宽从 50 GB/s 提升到 900 GB/s,推理速度直接翻了 3 倍。
延迟受限的场景:当你频繁随机访问小数据时,比如注意力机制中的索引操作。这时候延迟成了主要矛盾。我记得有一次优化 Transformer 模型,发现大部分时间都花在了 gather 操作上,每次都要等几百个周期才能拿到数据。
这里有个实用的经验公式:
实际性能 = min(算力上限, 带宽上限 × 计算强度)
其中:
- 算力上限 = 硬件理论峰值 (TFLOPS)
- 带宽上限 = 内存带宽 (GB/s)
- 计算强度 = 总计算量 / 总访存量 (FLOPs/Byte)
当计算强度大于某个阈值(Roofline 模型中的“拐点”)时,性能受算力限制;反之,受带宽限制。算子融合的核心目标,就是通过合并多个访存密集型算子,提高整体的计算强度,把性能从带宽受限区拉到算力受限区。
一句话总结:算子融合的本质,就是把多次小数据搬移合并成一次大数据搬移,减少对带宽的占用,降低延迟的影响。说白了,就是让计算单元少等一会儿,多干点活。
嗯,这一节的内容就到这里。访存瓶颈是理解算子融合的基石,搞懂了它,后面讲具体融合策略的时候,你就能明白为什么某些算子要绑在一起,某些要拆开。