起因#
最近在做 CS336(Language Modeling from Scratch)的 resource accounting 作业,里面要求逐个列出 Transformer forward pass 中所有的 matmul,然后按 \(2MKN\) 计算 FLOPs。
算完之后我自然会想:算出来的 FLOPs,到底能在多大程度上代表真实的推理性能?
这不只是作业里的好奇。工作中我也碰到过类似问题:要把多个模型调度到一组 GPU 上,希望最慢那个模型的 inference latency 尽可能小。
最直觉的做法是:用模型 FLOPs 表示它需要多少 GPU 算力,然后按 FLOPs 分配。
但实际 profiling 很快打脸——就算两个模型的理论 FLOPs 很接近,不同的 GEMM shape 也可能让 inference latency 差很多。
对 MoE、GNN 这类模型,这个问题更明显。
于是我想把这件事理清楚:FLOPs 和 latency 之间到底隔着什么。
先把几句 high-level idea 钉住#
如果后面细节都忘了,希望至少还记得这几句:
- FLOPs 是 workload metric,不是 performance metric。
- 正确链路不是 FLOPs → Latency,而是 Theoretical FLOPs → Hardware Efficiency → Actual Latency。
- Same FLOPs ≠ Same Latency。 shape、parallelism、Arithmetic Intensity 都会改 Achieved FLOPS。
- Latency = FLOPs / Achieved FLOPS。 Work / Efficiency。
- Roofline 给的是 upper bound,不是 latency predictor。
- Theory explains. Profiling predicts.
flowchart TB
Wrong["错误直觉
FLOPs → Latency"]
Right["正确 mental model"]
F["Theoretical FLOPs
Work"]
E["Hardware Efficiency
GEMM Shape / AI / Parallelism"]
A["Achieved FLOPS"]
L["Actual Latency
Performance"]
Wrong -.->|不要这样| L
Right --> F --> E --> A --> L
一个看似合理的公式#
假设模型做一次 forward 需要 \(F\) FLOPs,GPU peak 是 \(P\) FLOP/s,最容易想到:
$$T = \frac{F}{P}$$比如 10 GFLOPs / 100 TFLOPS = 0.1 ms。这只是很粗的 theoretical lower bound,实际 latency 往往远大于它。
所以问题变成:为什么 GPU 明明标称 100 TFLOPS,却经常只能跑出其中很小一部分?
引入 Achieved FLOPS:
$$\text{Achieved FLOPS} = \frac{\text{Actual FLOPs}}{\text{Execution Time}}$$真正该用的关系是:
$$\text{Latency} = \frac{\text{FLOPs}}{\text{Achieved FLOPS}}$$问题从"怎么算 FLOPs",变成了"什么决定 Achieved FLOPS"。
FLOPs 统一口径#
GEMM:\(A \in \mathbb{R}^{M \times K}\),\(B \in \mathbb{R}^{K \times N}\),\(C \in \mathbb{R}^{M \times N}\)。
每个输出元素大约 \(K\) 次 multiply + \(K\) 次 add,因此:
$$\text{FLOPs}_{\text{GEMM}} = 2MKN$$我这边口径:multiply = 1 FLOP,add = 1 FLOP,一个 MAC = 2 FLOPs。有些 profiler/论文把 MAC 当 1 operation,可能差 2x。CS336 作业也是 \(2MKN\)。
关键点:\(2MKN\) 只取决于三个维度的乘积。只要 \(M_1 K_1 N_1 \approx M_2 K_2 N_2\),两个 GEMM 的 FLOPs 就几乎一样——但 latency 未必。
Same FLOPs, Different Latency#
我 profiling 时最先撞上的现象,就是这个。
构造几组 GEMM,控制 \(2MKN\) 差不多,但 shape 明显不同:
| Shape | M | K | N | FLOPs |
|---|---|---|---|---|
| balanced | 2048 | 2048 | 2560 | ~21.5G |
| medium-skinny | 32 | 4096 | 81920 | ~21.5G |
| tall-skinny | 1 | 4096 | 2621440 | ~21.5G |
FLOPs 几乎一样。但在 GPU 上跑一下(torch.matmul + CUDA Event),差距很夸张:balanced 的 Achieved TFLOPS 可能接近 peak 的 70-80%;skinny(尤其 M=1)可能只有个位数百分比。
flowchart LR
subgraph SameFLOPs["FLOPs ≈ 相同"]
BA["balanced
M/K/N 都大"]
SK["skinny
M=1 / 很小"]
end
BA --> HA["Achieved FLOPS 高
Latency 低"]
SK --> LA["Achieved FLOPS 低
Latency 高"]
Same FLOPs ≠ Same Latency。 后面想搞清楚的,就是这些 FLOPs 到底是怎么被 GPU 执行的。
GEMM Shape 与 GPU Parallelism#
第一个原因:GPU 需要足够的并行度。
GPU 有大量 SM、warp、Tensor Core。FLOPs 告诉你一共有多少工作;但如果没法同时拆给很多工人,工人再多也没用。Peak FLOPS 只是"工作足够适合并行"时的上限。
GEMM 是 tiled execution#
GPU 不是逐个算 \(C_{ij}\)。kernel 把输出矩阵切成 tile,每个 tile 映射到 thread block,再拆到 warp / Tensor Core。
flowchart TB
subgraph LargeM["M 很大:tile 多"]
T1[tile] --- T2[tile] --- T3[tile] --- T4[tile]
T5[tile] --- T6[tile] --- T7[tile] --- T8[tile]
end
subgraph SmallM["M 很小:tile 少"]
S1[tile] --- S2[tile]
end
LargeM --> Fill["容易填满 SM"]
SmallM --> Idle["大量 SM 空闲"]
所以 \(M/K/N\) 不只决定 FLOPs,还决定 tile 数量、edge tile、warp utilization、occupancy。\(MKN\) 相同 ≠ 硬件执行效率相同。
Skinny GEMM 为什么尤其低效#
\(M=1\)(单 sample inference)时,输出变成 \([1,N]\),tile 急剧减少:
- available parallelism 下降,填不满 SM
- data reuse 变差
- kernel launch / sync 等固定开销占比变大
这对低 batch inference 特别重要——online serving 的 batch 经常是 1 或个位数。
Shape alignment 也有影响:不同 arch / dtype / cuBLAS / CUTLASS 对 tile 偏好不同。实践里 1024、2048、4096 常见,但不存在所有 GPU 通用的"必须是 8/16/32 倍数"规则。
Arithmetic Intensity#
并行度够了,还得把数据搬进来。搬得慢,计算单元就在等。
$$\text{Arithmetic Intensity} = \frac{\text{FLOPs}}{\text{Bytes Transferred}}$$单位 FLOPs/Byte:每搬 1 byte,能干多少次计算?
flowchart TB
subgraph HighAI["High AI:算得多,搬得少"]
H1[HBM load] --> H2[shared mem / register]
H2 --> H3[反复 reuse]
H3 --> H4[很多 FMA]
end
subgraph LowAI["Low AI:搬一次,算一两次"]
L1[HBM load] --> L2[算一两次]
L2 --> L3[写回 HBM]
L3 --> L4[再 load 新数据]
end
要注意:Bytes 是实际 memory traffic,不是简单的 input + weight + output tensor size。 traffic 受 cache、tiling、reuse、fusion 影响。分析时常用 theoretical AI 做近似,真测再用 Nsight Compute。
为什么 GEMM 通常 AI 较高#
矩阵乘法天然有 reuse:\(A_{ik}\) 参与一整行输出,\(B_{kj}\) 参与一整列输出。tiling 把一小块 A/B load 进 shared memory / register,反复做大量 FMA。
粗略理论 AI:
$$\text{AI}_{\text{GEMM}} \approx \frac{2MKN}{s(MK + KN + MN)}$$\(M\) 很小时,分母里 \(KN\)(大 weight 只用一次)占主导,AI 下降。
batch size / GEMM shape 不只改 FLOPs,也改 Arithmetic Intensity,从而改硬件效率。
Roofline Model#
有了 AI,就可以用 Roofline:
$$\text{Achievable FLOPS} \le \min(\text{Peak FLOPS},\ \text{BW} \times \text{AI})$$$$\text{AI}_{\text{ridge}} = \frac{\text{Peak FLOPS}}{\text{BW}}$$
flowchart LR
subgraph MemoryBound["Memory-bound
AI 低于 ridge"]
M1["上限 ≈ BW × AI"]
end
subgraph ComputeBound["Compute-bound
AI 高于 ridge"]
C1["上限 ≈ Peak FLOPS"]
end
AI["Arithmetic Intensity"] --> MemoryBound
AI --> ComputeBound
Ridge["ridge point
Peak / BW"]
数字感受一下。Peak = 100 TFLOPS,BW = 2 TB/s,则 ridge = 50 FLOPs/Byte。
- AI = 5 → 上限约 \(2 \times 5 = 10\) TFLOPS,远低于名义 100
- AI = 100 → bandwidth roof 200 已超过 peak,受 compute roof 限制
Peak TFLOPS 是硬件属性;真正能用多少,要看 workload 落在 Roofline 的什么位置。
Roofline 的局限#
Roofline 给的是 upper bound,不是 latency predictor。
两个 GEMM 都可能 AI 很高、都在 compute-bound 区域,但 M=4096 和 M=4 的 Achieved FLOPS 仍可能差很多——因为 Roofline 没描述 insufficient parallelism、occupancy、tile efficiency、launch overhead、load imbalance。
Roofline 必要,但远远不是完整的 GPU performance model。
三层 Mental Model#
到这里收成一个我自己用的框架:
flowchart TB
W["Model / Workload"]
F["① Theoretical FLOPs
数学上有多少工作"]
EP["② Execution Properties
GEMM Shape · Parallelism · AI
Memory Access · Kernel Impl"]
A["③ Achieved FLOPS
硬件每秒能完成多少"]
L["Actual Latency
最终性能"]
W --> F --> EP --> A --> L
$$\text{Latency} = \frac{\text{FLOPs}}{\text{Achieved FLOPS}} = \frac{\text{Work}}{\text{Efficiency}}$$FLOPs = Work;Achieved FLOPS = Efficiency;Latency = Performance。 不要把 FLOPs → Latency 想成一步。
MLP → MoE → GNN:FLOPs 解释能力的光谱#
我不想把 FLOPs 批判成"完全没用"。对 large dense GEMM、shape 稳定的 MLP,FLOPs 确实是很好的 first-order metric。
但适用条件会变。
MLP / Batch Size#
\([B,D]\times[D,H]\) 的 FLOPs \(\propto B\)。B=1 → B=128,FLOPs 涨 128 倍,latency 往往涨不到 128 倍——因为 Achieved FLOPS 也大幅上升。
做更多 FLOPs,反而可能让 GPU 跑得更高效。
MoE:routing 改变 GEMM shape#
同一批 128 tokens、top-k=1,总 FLOPs 很接近,但:
flowchart TB
subgraph Conc["Routing 集中"]
E0["Expert0: 64 tokens
GEMM [64,D]×[D,H]"]
E1["Expert1: 64 tokens
GEMM [64,D]×[D,H]"]
end
subgraph Disp["Routing 分散"]
D0["Expert: 1~4 tokens
大量 skinny GEMM"]
end
Conc --> Fast["Achieved FLOPS 高"]
Disp --> Slow["Achieved FLOPS 低
+ dispatch / gather / imbalance"]
Same total FLOPs,不同 routing → 不同 Achieved FLOPS → 不同 latency。 关于 MoE 结构,见之前的 MoE 结构学习笔记。
GNN:FLOPs 更不可靠#
Feature transform \(HW\) 还是 GEMM;aggregation(dst[edge_dst] += src[edge_src])FLOPs 极少,但要 load edge index、散乱读写、可能 atomic——AI 极低,latency 却不低。
光谱小结#
flowchart LR
MLP["Large dense MLP
FLOPs 解释力强"] --> SB["Small-batch MLP
skinny GEMM"]
SB --> MoE["MoE
routing 改 shape"]
MoE --> GNN["GNN aggregation
memory / irregular"]
| Workload | FLOPs 解释力 | 主要原因 |
|---|---|---|
| Large dense MLP | 较强 | GEMM efficiency 稳定 |
| Small-batch MLP | 中等 | skinny GEMM |
| MoE | 更弱 | expert batch 改 shape |
| GNN aggregation | 很弱 | memory-bound + irregular access |
越远离"大的、规整的 dense GEMM",FLOPs 对 latency 的预测能力越弱。
Training vs Inference#
对 Linear:forward \(2BKN\),backward 约 \(4BKN\),合计约 \(3\times\) forward。CS336 用的 Kaplan/Hoffmann convention 也是这个粗估,对 GEMM-heavy dense 还挺好用;但不是 universal law。
更关键的是:训练和推理的 Achieved FLOPS 也可能完全不同。 训练 batch 大、shape healthy;online inference batch 常是 1/2/4,skinny GEMM 一多,utilization 可能远低于训练。
MFU#
$$\text{MFU} = \frac{\text{Achieved FLOPS}}{\text{Peak FLOPS}}$$比如 10 GFLOPs / 0.5 ms = 20 TFLOPS;相对 100 TFLOPS peak,MFU = 20%。
低 MFU 不代表实现一定差。 AI 远低于 ridge 时,本来就不可能接近 compute peak——那是 Roofline 决定的。别把 MFU 当唯一优化目标。
回到模型调度#
如果只拿 ModelProfile { flops },认为 20 GFLOPs 大约要 10 GFLOPs 的两倍 GPU 时间——只在 hardware / dtype / shape / AI / achieved efficiency 都相近时才靠谱。
否则 \(\text{FLOPs}_A / \text{FLOPs}_B\) 推不出 \(\text{Latency}_A / \text{Latency}_B\)。
我会怎么升级 performance model:
flowchart TB
L0["Level 0: flops only
极粗估计"]
L1["Level 1: + operator / gemm_shapes"]
L2["Level 2: + AI / Roofline"]
L3["Level 3: + measured_latency
achieved_flops per hardware"]
L0 --> L1 --> L2 --> L3
到 Level 3(offline profiling),才比较能做 latency-aware scheduling。
结论#
还是那几句:
- FLOPs 是 workload metric,不是 performance metric。
- Theoretical FLOPs → Hardware Efficiency → Actual Latency。
- Same FLOPs ≠ Same Latency。
- Latency = Work / Efficiency = FLOPs / Achieved FLOPS。
- Roofline 是 upper bound,不是完整 performance model。
- Theory explains. Profiling predicts.
对模型调度:FLOPs 适合做第一阶估计和 workload 描述;真要 latency-aware,最终还是得靠 workload-aware profiling。
参考资料#
- CS336: Language Modeling from Scratch — Assignment 1 (Resource Accounting)
- Williams, Waterman, Patterson. Roofline: An Insightful Visual Performance Model for Multicore Architectures. Communications of the ACM, 2009.
- NVIDIA. CUDA C++ Programming Guide — Warp-level Matrix Multiply-Accumulate (WMMA) and Tensor Cores.
- Chowdhery et al. PaLM: Scaling Language Modeling with Pathways. 2022.(MFU 的定义和使用)