算子优化与显存协同调优实战 - 第3节

一、算子的两种“性格”:计算密集型 vs 访存密集型

做算子优化这么多年,我最大的体会就是:你得先搞清楚你的对手是谁。

什么意思呢?每个算子都有它的“性格”。有的算子天生爱算数,比如矩阵乘法、卷积——它们把数据拉进来,翻来覆去地算,算完才放出去。这种算子,我们叫它计算密集型。

有的算子呢?恰恰相反。比如元素级操作(像 ReLU、加法、乘法),或者某些 reduce 操作。它们几乎不怎么算,就是把数据从显存搬到寄存器,简单处理一下,再搬回去。这种算子,我们叫它访存密集型。

你可能会问:“知道这个有什么用?”

用处大了。我刚开始做优化时,花了两周去优化一个 ReLU 算子的计算逻辑,结果性能纹丝不动。后来才发现——瓶颈根本不在计算,而在显存带宽。说白了,你优化了一个它根本不需要的东西。

1.1 怎么区分?看计算密度

区分这两种算子,有一个很直观的指标:计算密度(Arithmetic Intensity)。

公式很简单:

计算密度 = 总计算量(FLOPs) / 总访存量(Bytes)

单位是 FLOPs/Byte。这个值越大,说明算子越“爱算”;越小,说明算子越“爱搬数据”。

我个人的经验是:

  • 计算密度 > 10:大概率是计算密集型。比如大矩阵乘法(GEMM),密度轻松上百。
  • 计算密度 < 1:基本是访存密集型。比如 ReLU,密度可能只有 0.1 左右。
  • 中间地带(1~10):需要具体分析,看硬件特性。

一个常见的误区:很多人觉得“计算量大”就是计算密集型。不对。你得看计算量和访存量的比值。一个算子计算量很大,但访存量更大,它依然是访存密集型的。

1.2 两类算子的优化思路完全不同

搞清楚了算子的性格,优化方向就清晰了:

算子类型 瓶颈在哪 优化重点
计算密集型 计算单元(ALU) 提高计算并行度、利用向量化指令、减少流水线停顿
访存密集型 显存带宽 减少访存次数、合并访存、利用缓存、数据复用

嗯,这里要注意:千万别用优化计算密集型的方法去优化访存密集型算子。我曾经见过有人给 ReLU 算子做循环展开、指令重排,折腾半天,性能提升不到 5%。后来我建议他把重点放在数据排布和访存合并上,直接翻了一倍。

二、Roofline 模型:一眼看穿算子的瓶颈

光靠感觉区分还不够。我们需要一个定量分析的工具。这就是 Roofline 模型。

Roofline 模型,说白了就是一张图。横轴是计算密度,纵轴是性能(FLOPs/s)。它画出了硬件的两个天花板:

  • 计算天花板:硬件的最大算力(比如 GPU 的 TFLOPS)
  • 访存天花板:硬件的最大带宽(比如显存带宽 GB/s)

这两条线交叉的地方,就是所谓的“拐点”。拐点左侧,性能受限于带宽;拐点右侧,性能受限于算力。

我的一个小习惯:每次拿到一个新的算子,第一件事就是算它的计算密度,然后扔到 Roofline 模型里看它在哪个区域。这能帮我快速判断优化方向,避免走弯路。

2.1 Roofline 模型的可视化

下面我用 SVG 画了一张典型的 Roofline 模型图。你可以直观地看到算子的位置和瓶颈:

Roofline 模型示意图 计算密度 (FLOPs/Byte) 0.1 1 10 100 性能 (FLOPs/s) 1G 10G 100G 1T 访存天花板 (带宽限制) 计算天花板 (算力限制) 拐点 访存受限区 优化带宽 计算受限区 优化算力 ReLU VecAdd Conv 3x3 GEMM

你看,ReLU 和向量加法落在访存受限区,离计算天花板还远着呢。你就算把 GPU 的算力翻一倍,它们也快不起来——因为瓶颈在带宽。而大矩阵乘法(GEMM)已经顶到了计算天花板,这时候你该想的是怎么提高计算效率。

2.2 怎么用 Roofline 指导优化?

我一般分三步走:

  1. 定位:把算子放到 Roofline 图上,看它在哪个区域。
  2. 分析:如果是访存受限,看看能不能减少访存量(比如用更小的数据类型、做数据复用)。如果是计算受限,看看能不能提高并行度(比如用 Tensor Core、做算子融合)。
  3. 验证:改完之后再测一次,看它是不是往拐点方向移动了。

我曾经踩过的一个坑:有一次优化一个卷积算子,我拼命做计算优化,结果性能提升很小。后来用 Roofline 一分析,发现它其实在访存受限区。问题出在数据排布上——我用的 NHWC 格式导致访存不连续。改成 CHWN 之后,带宽利用率从 30% 提升到了 80%,性能直接翻倍。

三、实战:用 Roofline 分析一个简单算子

光说不练假把式。我们拿一个具体的例子来走一遍流程。

假设我们要优化一个 向量加法:C = A + B,每个向量有 1024 个 float 元素。

先算计算密度:

  • 计算量:1024 次加法 = 1024 FLOPs
  • 访存量:读 A (1024×4=4096 Bytes) + 读 B (4096 Bytes) + 写 C (4096 Bytes) = 12288 Bytes
  • 计算密度 = 1024 / 12288 ≈ 0.083 FLOPs/Byte

这个值非常小。放到 Roofline 模型里,它妥妥地在访存受限区。

那优化方向就很明确了:

  • 减少访存量:能不能用 half 精度?或者把多次向量加法融合成一个 kernel?
  • 提高带宽利用率:保证访存是连续的,用向量化加载指令(比如 float4)。

我个人的经验是,对于这种极低计算密度的算子,算子融合往往是性价比最高的优化手段。比如把 A+B 和后面的 ReLU 融合成一个 kernel,这样 A 和 B 只需要读一次,中间结果不用写回显存。

一个小技巧:当你不知道从何下手时,先算计算密度。如果小于 1,优先考虑访存优化;如果大于 10,优先考虑计算优化。中间地带,那就得具体分析了。

四、总结

好了,这一章的核心内容就这些。我帮你捋一下:

  • 计算密集型 vs 访存密集型:看计算密度,决定优化方向。
  • Roofline 模型:一个可视化工具,帮你快速定位瓶颈。
  • 优化原则:访存受限优化带宽,计算受限优化算力。

记住,做算子优化,方向比努力更重要。用 Roofline 模型找准方向,你的优化才能事半功倍。

可可精品资料库