算术强度与 Roofline
上一章给了两个数:算力和带宽。这一章把它们相除,得到全书最有用的一个判断。
问题是这样的:一个算子跑得慢,是因为运算做不完,还是因为数据搬不动?两种情况的优化手段完全相反——前者要减少运算或用更快的单元,后者要减少数据搬运或压缩数据。搞错方向,努力全部白费。
Roofline 模型用一个数回答这个问题。
算术强度(arithmetic intensity)是一个算子每读写一字节所做的浮点运算数:
分母是必须读写的字节数——理想情况下每个数据只从显存读一次。实际实现如果反复读同一个数据,有效算术强度会更低。
例一:逐元素加法 , 个元素,16 位数值(2 字节)。
- 运算: 次加法
- 读写:读 、,写 ,共 字节
例二:矩阵乘 ,尺寸 乘 ,16 位数值。
- 运算:(每个输出元素做 次乘加)
- 读写: 字节
当 时:
矩阵乘的算术强度随尺寸线性增长——这就是为什么它是唯一能打满算力的算子,也是为什么"把小矩阵合并成大矩阵"是常见优化。
例三:自回归解码时的权重读取。 每生成一个 token,要把全部权重读一遍,每个权重参与约 2 次运算:
算术强度约等于 1——彻底的访存受限。
硬件也有一个对应的比值,叫机器平衡点:
它的含义是:要打满算力,每从显存读一个字节至少要做这么多次运算。
代入上一章的数字(H100,16 位稠密):
几代硬件的机器平衡点:
| 芯片 | 算力(16 位) | 带宽 | 平衡点 |
|---|---|---|---|
| V100 | 125 TFLOPS | 0.9 TB/s | ~139 |
| A100 | 312 TFLOPS | 2.0 TB/s | ~156 |
| H100 | 989 TFLOPS | 3.35 TB/s | ~295 |
| H200 | 989 TFLOPS | 4.8 TB/s | ~206 |
| B200 | 2250 TFLOPS | 8 TB/s | ~281 |
Roofline 模型
Section titled “Roofline 模型”把两者合起来,一个算子的可达性能是:
画成图(横轴算术强度,纵轴可达性能,双对数坐标):
性能 ↑ │ ┌────────────── 算力屋顶 P_peak │ ╱ │ ╱ │ ╱ ← 带宽斜坡,斜率 = B_peak │ ╱ │ ╱ └────────┴───────────────────→ 算术强度 I I_balance 访存受限 │ 计算受限- :落在斜坡上,受限于带宽。性能 。
- :落在屋顶下,受限于算力。性能上限 。
常见算子落在哪里
Section titled “常见算子落在哪里”以 H100(平衡点 295)为参照:
| 算子 | 算术强度 | 归类 | 算力利用率上限 |
|---|---|---|---|
| 大矩阵乘() | ~1000+ | 计算受限 | 接近 100% |
| 中等矩阵乘(1024 级) | ~340 | 刚过平衡点 | ~100% |
| 小矩阵乘(256 级) | ~85 | 访存受限 | ~29% |
| 卷积(大通道数) | 数百 | 接近平衡 | 高 |
| 注意力(长序列,批处理) | 数十–数百 | 接近平衡 | 中高 |
| 注意力(解码,单请求) | ~1 | 严重访存受限 | ~0.3% |
| LayerNorm / Softmax | ~1 | 访存受限 | ~0.3% |
| 激活函数(逐元素) | ~0.17 | 严重访存受限 | ~0.06% |
| 稀疏查表 | ≈ 0 | 彻底访存受限 | ≈ 0 |
这张表解释了一个反复出现的困惑:"为什么我的算力利用率只有 3%?"——如果你跑的是一串逐元素算子,3% 已经接近物理上限了。硬件没坏,是负载性质如此。
怎么提高算术强度
Section titled “怎么提高算术强度”既然算术强度决定了上限,提高它就是最直接的优化。四种手段:
一、算子融合
Section titled “一、算子融合”两个逐元素算子分开做:读 A 写 B,读 B 写 C,共四次显存往返。融合后:读 A 写 C,两次。算术强度翻倍,时间减半。
这是编译器最主要的优化,也是手写内核(如 FlashAttention)最主要的收益来源。融合的极限是把整个子图变成一个内核。
二、分块与片上复用
Section titled “二、分块与片上复用”矩阵乘时把矩阵切成小块,装进片上存储反复使用。每块数据从显存读一次,参与多次运算。
分块尺寸的选择是经典权衡:块越大复用越多(算术强度越高),但片上存储装不下。这是所有高性能矩阵乘库的核心。
三、增大批量
Section titled “三、增大批量”很多算子的算术强度随批量增长。最典型的是自回归解码:
单请求时权重读一遍只服务一个 token();批量 64 时同一次权重读取服务 64 个 token()。这就是推理批处理能带来数十倍吞吐提升的原因——不是算得更快,是每个字节被用了更多次。
四、降低精度
Section titled “四、降低精度”把数据从 2 字节压到 1 字节,分母减半,算术强度翻倍。同时矩阵单元的峰值算力也翻倍,所以对计算受限的算子也有效。这是唯一同时改善两侧的手段,下一章专讲。
三个完整算例
Section titled “三个完整算例”算例一:一个 4096×4096 矩阵乘要多久
Section titled “算例一:一个 4096×4096 矩阵乘要多久”- FLOPs:
- 算术强度:~1365,远大于 295 → 计算受限
- 理论时间:
- 实际(按 70% 效率):约 200 μs
算例二:对一个 10 GB 张量做 GELU 激活要多久
Section titled “算例二:对一个 10 GB 张量做 GELU 激活要多久”- 算术强度约 0.17(读一次写一次,每元素几次运算)→ 严重访存受限
- 数据量:读 10 GB + 写 10 GB = 20 GB
- 理论时间:
- 注意:算力完全不参与这个计算。换一块算力翻倍的卡,时间一点不变。
算例三:70B 模型单请求解码的速度上限
Section titled “算例三:70B 模型单请求解码的速度上限”- 每 token 需要读全部权重:140 GB(16 位存储)
- 算术强度 ≈ 1 → 访存受限
- 速度上限:
- 与算力无关。 这直接推出推理优化的两条主线:量化(减小分子)和批处理(提高算术强度)。
Roofline 的局限
Section titled “Roofline 的局限”它是一阶模型,有意忽略了很多东西:
| 忽略了什么 | 后果 |
|---|---|
| 缓存命中 | 实际带宽可能高于 HBM 峰值(数据在 L2 里) |
| 有效带宽 < 峰值 | 访问模式差时,斜坡的实际斜率更低 |
| 延迟没被隐藏 | 并发不足时,两个屋顶都够不到 |
| 内核启动开销 | 小算子密集时,开销可能主导 |
| 通信 | 多卡时要用扩展的 roofline(把网络带宽也画进去) |
所以 Roofline 给的是上界与方向,不是精确预测。它的价值在于:在动手优化之前,用五分钟判断优化方向是否正确。
拿到一个慢的算子,按这个顺序:
- 数一遍 FLOPs。
- 数一遍必须读写的字节数。
- 相除得算术强度 。
- 与机器平衡点比较,判断受限类型。
- 算出理论时间,与实测对比:
- 实测 ≈ 理论 → 已经到顶,要改就改算法或精度。
- 实测 ≫ 理论 → 实现有问题(访存不合并、并发不足、没融合)。
- 按受限类型选优化手段:
- 计算受限 → 用更好的矩阵单元、降精度、减少冗余计算。
- 访存受限 → 融合、分块、增大批量、降精度、提高缓存命中。
- 算术强度 = FLOPs ÷ 必须读写的字节数;机器平衡点 = 峰值算力 ÷ 峰值带宽。
- H100 的平衡点约 295 FLOPs/byte;平衡点逐代升高,意味着越来越多算子变成访存受限("内存墙")。
- Roofline:可达性能 = min(峰值算力, 算术强度 × 峰值带宽)。
- 矩阵乘的算术强度随尺寸线性增长,是唯一能打满算力的算子;逐元素算子的利用率上限只有千分之几,这不是缺陷。
- 提高算术强度的四种手段:融合、分块复用、增大批量、降低精度。
- Roofline 是一阶模型,给方向不给精确值;但它能在动手前五分钟判断方向对不对。