性能分析基础:FLOPs、MACs、内存带宽、算术强度、Roofline模型入门
各位同学,咱们今天聊点实在的。做AI算子优化,最怕什么?怕你调了半天代码,跑起来一看,性能还不如人家没优化的版本。我刚开始带团队那会儿,就犯过这种错——闷头调了三天kernel,结果发现瓶颈根本不在计算上,而是数据搬来搬去把时间全耗光了。
所以,在动手写代码之前,你得先学会「看」。看你的算子到底在干什么,瓶颈在哪。这就是我们今天要讲的性能分析基础。
1. 先搞清楚几个核心指标
做性能分析,你得手里有把尺子。我常用的尺子就这几把:FLOPs、MACs、内存带宽、算术强度。咱们一个一个说。
1.1 FLOPs 和 MACs
FLOPs(Floating Point Operations),浮点运算次数。说白了,就是你的模型或算子做了多少次浮点加减乘除。这个指标直接反映了计算量的大小。
MACs(Multiply-Accumulate Operations),乘累加操作次数。一次 MAC 包含一次乘法和一次加法,通常算作 2 FLOPs。在卷积和全连接层里,MACs 比 FLOPs 更常用,因为硬件对乘累加有专门的优化指令。
举个例子,一个全连接层:输入维度 D,输出维度 N,那么它的 MACs 就是 D × N,FLOPs 就是 2 × D × N。嗯,这里要注意,很多框架和工具统计的口径不一样,有的报 FLOPs,有的报 MACs,你得看清楚。
我个人习惯:看卷积层时,我更喜欢用 MACs,因为 Tensor Core 和 CUDA core 的吞吐量通常以 MACs/cycle 为单位。看整个模型时,用 FLOPs 更直观。
1.2 内存带宽
内存带宽,就是你的 GPU 或 CPU 从显存/内存里读数据的速度,单位通常是 GB/s。这个指标有多重要?我跟你讲,很多算子的瓶颈根本不在计算,而在数据搬运。
我记得有一次优化一个 LayerNorm 算子,算力利用率死活上不去。我盯着 profiler 看了半天,发现 kernel 的算术强度低得可怜,每次计算只读几个数,却要写回一大片。说白了,就是被内存带宽卡死了。
内存带宽的典型值:A100 大约是 2 TB/s,V100 是 900 GB/s,RTX 3090 是 936 GB/s。嗯,这些数字你得记在心里,做性能分析时经常要用到。
1.3 算术强度
算术强度(Arithmetic Intensity),这个指标是我最看重的。它等于 FLOPs 除以访存量(Bytes)。单位是 FLOPs/Byte。
公式很简单:
算术强度 = 计算量(FLOPs) / 访存量(Bytes)
这个值越大,说明你的算子越「计算密集」;越小,说明越「访存密集」。为什么重要?因为它直接决定了你的算子是被计算限制还是被访存限制。
避坑指南:我曾经在优化一个 GELU 激活函数时,发现算术强度只有 0.5 FLOPs/Byte。这意味着无论我怎么优化计算部分,性能上限都被内存带宽锁死了。后来我改成融合到前一个卷积里一起做,算术强度一下子提到了 8 以上,性能翻了一倍。
2. Roofline 模型:一张图看懂瓶颈
好了,指标都介绍完了。但光有指标还不够,你得把它们串起来看。Roofline 模型就是干这个的。
Roofline 模型把性能(FLOPs/s)画成纵轴,算术强度(FLOPs/Byte)画成横轴。然后画两条线:
- 天花板线:硬件的峰值算力(比如 A100 的 312 TFLOPS)。
- 带宽线:内存带宽 × 算术强度,斜率为带宽。
这两条线相交的地方,就是「拐点」。拐点左侧,性能被带宽限制;拐点右侧,性能被算力限制。
你想想看,你的每个算子都可以在这个图上画一个点。如果点在带宽线下方,说明你没有充分利用带宽;如果点在算力天花板下方,说明你没有充分利用算力。
我建议:每次优化一个算子前,先算一下它的算术强度,然后在 Roofline 模型上定位。这样你一眼就能看出瓶颈在哪,该往哪个方向使劲。
3. 知识体系结构图
下面这张图,是我自己总结的性能分析知识体系。你看一眼,心里就有谱了。
4. 实战:如何计算算术强度
光说不练假把式。咱们拿一个实际的卷积层来算算。
假设输入是 64×64×128,卷积核是 3×3×128×256,stride=1,padding=1,输出是 64×64×256。
计算量(FLOPs):
FLOPs = 2 × 输出尺寸 × 卷积核尺寸 × 输入通道 × 输出通道
= 2 × (64×64) × (3×3) × 128 × 256
= 2 × 4096 × 9 × 128 × 256
= 2,418,483,200 ≈ 2.42 GFLOPs
访存量(Bytes):
输入: 64×64×128 × 4 (float32) = 2,097,152 Bytes
权重: 3×3×128×256 × 4 = 1,179,648 Bytes
输出: 64×64×256 × 4 = 4,194,304 Bytes
总访存 ≈ 7.47 MB
算术强度:
算术强度 = 2.42 GFLOPs / 7.47 MB ≈ 324 FLOPs/Byte
这个值远高于 A100 的拐点(大约 100 FLOPs/Byte 左右),说明这个卷积是计算密集型的。优化重点应该放在利用 Tensor Core、提高 occupancy 上,而不是折腾数据搬运。
注意:上面的访存量计算是理想情况。实际上,由于缓存、tiling 等因素,实际访存量可能更小。但作为初步分析,这个估算已经足够帮你判断方向了。
5. 用 Roofline 模型做决策
好了,现在你有了算术强度,也有了硬件参数。怎么用 Roofline 模型做决策?
假设你的算子算术强度是 50 FLOPs/Byte,而 A100 的峰值算力是 312 TFLOPS,带宽是 2 TB/s。那么:
- 带宽限制下的理论性能 = 2 TB/s × 50 FLOPs/Byte = 100 TFLOPS
- 这个值远低于 312 TFLOPS,说明你的算子被带宽限制了。
那怎么办?我建议你从这几个方向入手:
- 算子融合:把多个小算子合并成一个,减少中间结果的读写。
- 内存布局优化:比如把 NHWC 改成 NCHW,或者用 tiling 提高缓存命中率。
- 降低精度:从 float32 降到 float16,访存量直接减半。
反过来,如果你的算子算术强度是 500 FLOPs/Byte,那瓶颈就在计算上。这时候你应该:
- 用 Tensor Core 替代 CUDA core
- 提高 occupancy,隐藏指令延迟
- 考虑使用更激进的编译器优化
我个人经验:很多新手一上来就调 kernel launch 参数,其实大部分时候瓶颈都在访存。你先用 Roofline 模型看一眼,能省下大量试错时间。
6. 总结一下
今天咱们聊了性能分析的几个核心概念。说白了,就是三件事:
- 算清楚:FLOPs、MACs、访存量,这些数字你得会算。
- 看明白:算术强度决定了你的算子属于哪一类。
- 找对方向:Roofline 模型帮你一眼看出瓶颈在哪。
嗯,这些工具用熟了,你再看 profiling 结果,就不会一头雾水了。下一节咱们会深入讲怎么用 NVIDIA Nsight 和 AMD ROCProfiler 做实际 profiling,到时候这些概念都会用上。
想要更多资料,联系可可