跳转到内容

算术强度与 Roofline

上一章给了两个数:算力和带宽。这一章把它们相除,得到全书最有用的一个判断。

问题是这样的:一个算子跑得慢,是因为运算做不完,还是因为数据搬不动?两种情况的优化手段完全相反——前者要减少运算或用更快的单元,后者要减少数据搬运或压缩数据。搞错方向,努力全部白费。

Roofline 模型用一个数回答这个问题。

算术强度(arithmetic intensity)是一个算子每读写一字节所做的浮点运算数:

I=浮点运算次数必须读写的字节数[FLOPs/byte]I = \frac{\text{浮点运算次数}}{\text{必须读写的字节数}} \quad [\text{FLOPs/byte}]

分母是必须读写的字节数——理想情况下每个数据只从显存读一次。实际实现如果反复读同一个数据,有效算术强度会更低。

例一:逐元素加法 C=A+BC = A + B,nn 个元素,16 位数值(2 字节)。

  • 运算:nn 次加法
  • 读写:读 AA、BB,写 CC,共 3×2n=6n3 \times 2n = 6n 字节
I=n6n≈0.17I = \frac{n}{6n} \approx 0.17

例二:矩阵乘 C=ABC = AB,尺寸 M×KM \times K 乘 K×NK \times N,16 位数值。

  • 运算:2MNK2MNK(每个输出元素做 KK 次乘加)
  • 读写:2(MK+KN+MN)2(MK + KN + MN) 字节
I=2MNK2(MK+KN+MN)=MNKMK+KN+MNI = \frac{2MNK}{2(MK + KN + MN)} = \frac{MNK}{MK + KN + MN}

当 M=N=K=4096M = N = K = 4096 时:

I=409633×40962=40963≈1365I = \frac{4096^3}{3 \times 4096^2} = \frac{4096}{3} \approx 1365

矩阵乘的算术强度随尺寸线性增长——这就是为什么它是唯一能打满算力的算子,也是为什么"把小矩阵合并成大矩阵"是常见优化。

例三:自回归解码时的权重读取。 每生成一个 token,要把全部权重读一遍,每个权重参与约 2 次运算:

I=2N2N=1(16 位数值)I = \frac{2N}{2N} = 1 \quad (\text{16 位数值})

算术强度约等于 1——彻底的访存受限。

硬件也有一个对应的比值,叫机器平衡点:

Ibalance=PpeakBpeakI_{\text{balance}} = \frac{P_{\text{peak}}}{B_{\text{peak}}}

它的含义是:要打满算力,每从显存读一个字节至少要做这么多次运算。

代入上一章的数字(H100,16 位稠密):

Ibalance=989×1012 FLOP/s3.35×1012 B/s≈295 FLOPs/byteI_{\text{balance}} = \frac{989 \times 10^{12} \ \text{FLOP/s}}{3.35 \times 10^{12} \ \text{B/s}} \approx 295 \ \text{FLOPs/byte}

几代硬件的机器平衡点:

芯片算力(16 位)带宽平衡点
V100125 TFLOPS0.9 TB/s~139
A100312 TFLOPS2.0 TB/s~156
H100989 TFLOPS3.35 TB/s~295
H200989 TFLOPS4.8 TB/s~206
B2002250 TFLOPS8 TB/s~281

把两者合起来,一个算子的可达性能是:

P=min⁡(Ppeak⏟算力屋顶, I×Bpeak⏟带宽斜坡)P = \min\left(\underbrace{P_{\text{peak}}}_{\text{算力屋顶}},\ \underbrace{I \times B_{\text{peak}}}_{\text{带宽斜坡}}\right)

画成图(横轴算术强度,纵轴可达性能,双对数坐标):

性能 ↑
│ ┌────────────── 算力屋顶 P_peak
│ ╱
│ ╱
│ ╱ ← 带宽斜坡,斜率 = B_peak
│ ╱
│ ╱
└────────┴───────────────────→ 算术强度 I
I_balance
访存受限 │ 计算受限
  • I<IbalanceI < I_{\text{balance}}:落在斜坡上,受限于带宽。性能 =I×Bpeak= I \times B_{\text{peak}}。
  • I>IbalanceI > I_{\text{balance}}:落在屋顶下,受限于算力。性能上限 =Ppeak= P_{\text{peak}}。

以 H100(平衡点 295)为参照:

算子算术强度归类算力利用率上限
大矩阵乘(M,N,K≥4096M,N,K \ge 4096)~1000+计算受限接近 100%
中等矩阵乘(1024 级)~340刚过平衡点~100%
小矩阵乘(256 级)~85访存受限~29%
卷积(大通道数)数百接近平衡高
注意力(长序列,批处理)数十–数百接近平衡中高
注意力(解码,单请求)~1严重访存受限~0.3%
LayerNorm / Softmax~1访存受限~0.3%
激活函数(逐元素)~0.17严重访存受限~0.06%
稀疏查表≈ 0彻底访存受限≈ 0

这张表解释了一个反复出现的困惑:"为什么我的算力利用率只有 3%?"——如果你跑的是一串逐元素算子,3% 已经接近物理上限了。硬件没坏,是负载性质如此。

既然算术强度决定了上限,提高它就是最直接的优化。四种手段:

两个逐元素算子分开做:读 A 写 B,读 B 写 C,共四次显存往返。融合后:读 A 写 C,两次。算术强度翻倍,时间减半。

这是编译器最主要的优化,也是手写内核(如 FlashAttention)最主要的收益来源。融合的极限是把整个子图变成一个内核。

矩阵乘时把矩阵切成小块,装进片上存储反复使用。每块数据从显存读一次,参与多次运算。

分块尺寸的选择是经典权衡:块越大复用越多(算术强度越高),但片上存储装不下。这是所有高性能矩阵乘库的核心。

很多算子的算术强度随批量增长。最典型的是自回归解码:

I≈2NB2N+(每请求的状态)I \approx \frac{2NB}{2N + (\text{每请求的状态})}

单请求时权重读一遍只服务一个 token(I≈1I \approx 1);批量 64 时同一次权重读取服务 64 个 token(I≈64I \approx 64)。这就是推理批处理能带来数十倍吞吐提升的原因——不是算得更快,是每个字节被用了更多次。

把数据从 2 字节压到 1 字节,分母减半,算术强度翻倍。同时矩阵单元的峰值算力也翻倍,所以对计算受限的算子也有效。这是唯一同时改善两侧的手段,下一章专讲。

算例一:一个 4096×4096 矩阵乘要多久

Section titled “算例一:一个 4096×4096 矩阵乘要多久”
  • FLOPs:2×40963≈1.37×10112 \times 4096^3 \approx 1.37 \times 10^{11}
  • 算术强度:~1365,远大于 295 → 计算受限
  • 理论时间:1.37×1011/(989×1012)≈139 μs1.37 \times 10^{11} / (989 \times 10^{12}) \approx 139\ \mu s
  • 实际(按 70% 效率):约 200 μs

算例二:对一个 10 GB 张量做 GELU 激活要多久

Section titled “算例二:对一个 10 GB 张量做 GELU 激活要多久”
  • 算术强度约 0.17(读一次写一次,每元素几次运算)→ 严重访存受限
  • 数据量:读 10 GB + 写 10 GB = 20 GB
  • 理论时间:20/3350≈6 ms20 / 3350 \approx 6\ ms
  • 注意:算力完全不参与这个计算。换一块算力翻倍的卡,时间一点不变。

算例三:70B 模型单请求解码的速度上限

Section titled “算例三:70B 模型单请求解码的速度上限”
  • 每 token 需要读全部权重:140 GB(16 位存储)
  • 算术强度 ≈ 1 → 访存受限
  • 速度上限:3350 GB/s÷140 GB≈24 tokens/s3350 \ \text{GB/s} \div 140 \ \text{GB} \approx 24 \ \text{tokens/s}
  • 与算力无关。 这直接推出推理优化的两条主线:量化(减小分子)和批处理(提高算术强度)。

它是一阶模型,有意忽略了很多东西:

忽略了什么后果
缓存命中实际带宽可能高于 HBM 峰值(数据在 L2 里)
有效带宽 < 峰值访问模式差时,斜坡的实际斜率更低
延迟没被隐藏并发不足时,两个屋顶都够不到
内核启动开销小算子密集时,开销可能主导
通信多卡时要用扩展的 roofline(把网络带宽也画进去)

所以 Roofline 给的是上界与方向,不是精确预测。它的价值在于:在动手优化之前,用五分钟判断优化方向是否正确。

拿到一个慢的算子,按这个顺序:

  1. 数一遍 FLOPs。
  2. 数一遍必须读写的字节数。
  3. 相除得算术强度 II。
  4. 与机器平衡点比较,判断受限类型。
  5. 算出理论时间,与实测对比:
    • 实测 ≈ 理论 → 已经到顶,要改就改算法或精度。
    • 实测 ≫ 理论 → 实现有问题(访存不合并、并发不足、没融合)。
  6. 按受限类型选优化手段:
    • 计算受限 → 用更好的矩阵单元、降精度、减少冗余计算。
    • 访存受限 → 融合、分块、增大批量、降精度、提高缓存命中。
  • 算术强度 = FLOPs ÷ 必须读写的字节数;机器平衡点 = 峰值算力 ÷ 峰值带宽。
  • H100 的平衡点约 295 FLOPs/byte;平衡点逐代升高,意味着越来越多算子变成访存受限("内存墙")。
  • Roofline:可达性能 = min(峰值算力, 算术强度 × 峰值带宽)。
  • 矩阵乘的算术强度随尺寸线性增长,是唯一能打满算力的算子;逐元素算子的利用率上限只有千分之几,这不是缺陷。
  • 提高算术强度的四种手段:融合、分块复用、增大批量、降低精度。
  • Roofline 是一阶模型,给方向不给精确值;但它能在动手前五分钟判断方向对不对。