2. 性能瓶颈分析
做推理优化这些年,我见过太多团队把精力花在错误的地方。有人拼命压缩模型大小,结果推理速度纹丝不动;有人疯狂调优算子实现,却发现瓶颈根本不在那儿。
为什么会这样?说白了,就是没搞清楚性能瓶颈到底在哪。
今天咱们就来聊聊推理中的两类核心瓶颈——访存密集型和计算密集型。以及它们背后的两个“墙”:带宽墙和计算墙。
先看一个真实案例
我之前接手过一个项目,模型是BERT-base,部署在T4 GPU上。团队花了三周优化各个算子,把矩阵乘法的计算效率从60%提到了85%。结果呢?端到端推理延迟只降了5%。
我当时就觉得不对劲。一分析才发现,真正吃时间的不是矩阵乘法,而是LayerNorm和Softmax这些看起来“不起眼”的算子。它们占用了40%以上的时间,但计算量其实很小。
嗯,这就是典型的访存瓶颈。
两类性能瓶颈的本质
咱们先给个直观的定义:
| 类型 | 核心特征 | 典型算子 | 瓶颈在哪 |
|---|---|---|---|
| 计算密集型 | 计算时间 >> 访存时间 | 矩阵乘法、卷积 | 算力(FLOPS) |
| 访存密集型 | 访存时间 >> 计算时间 | LayerNorm、Softmax、Reshape | 带宽(GB/s) |
你想想看,一个算子如果计算量很大,比如大矩阵乘法,那GPU的Tensor Core就能派上用场,计算效率很高。但如果计算量很小,比如一个LayerNorm,计算本身可能只需要几微秒,但数据从显存搬到寄存器就要几十微秒——那瓶颈自然就在访存上。
带宽墙与计算墙
这两个概念其实很好理解:
- 计算墙:硬件的算力上限。比如T4 GPU的FP16算力是65 TFLOPS。你优化得再好,也不可能超过这个数。
- 带宽墙:硬件的访存带宽上限。T4的显存带宽是320 GB/s。数据搬运速度就这么多,算子再快也没用。
我个人的习惯是,拿到一个模型后,先算一个指标——计算密度(Compute Intensity)。
公式很简单:
计算密度 = 总计算量(FLOPs) / 总访存量(Bytes)
单位是 FLOPs/Byte。这个值越大,说明算子越偏向计算密集型;越小,越偏向访存密集型。
举个例子:
- 一个 1024x1024 的矩阵乘法,计算量约 2G FLOPs,访存量约 8MB(两个输入矩阵 + 一个输出矩阵)。计算密度 ≈ 250 FLOPs/Byte。这明显是计算密集型。
- 一个 LayerNorm,输入是 1024 个元素,计算量约 3K FLOPs,访存量约 4KB。计算密度 ≈ 0.75 FLOPs/Byte。妥妥的访存密集型。
关键判断标准:如果算子的计算密度低于硬件平台的“平衡点”(通常为 10-50 FLOPs/Byte),那它就是访存瓶颈。反之则是计算瓶颈。
我在项目中遇到的典型场景
我记得有一次优化一个GPT-style的生成模型。模型本身不大,但推理特别慢。我一开始以为是矩阵乘法没优化好,结果profile一看——
你猜怎么着?
模型里有个频繁的Reshape + Transpose操作,每次生成一个token都要做一次。这个操作计算量几乎为零,但每次都要搬运整个KV Cache。KV Cache有多大呢?序列长度2048,隐藏维度1024,精度FP16——每次搬运就是 2048 * 1024 * 2 = 4MB。
生成100个token,就是400MB的数据搬运。而T4的带宽才320 GB/s,光这个操作就占了1.25ms。整个推理才5ms,你说这算不算瓶颈?
避坑指南:我曾经犯过一个错误——只盯着计算量大的算子优化。后来发现,很多模型里访存密集型的算子虽然单个耗时短,但数量多、频率高,加起来反而成了大头。所以profile一定要做全,别只看Top 1。
如何快速定位瓶颈类型
我一般用三步法:
- 跑一次profile:用NVIDIA Nsight或PyTorch Profiler,拿到每个算子的耗时和计算量。
- 算计算密度:每个算子的FLOPs除以访存量,看落在哪个区间。
- 对比硬件能力:如果算子的计算密度 < 硬件平衡点,那就是访存瓶颈;反之是计算瓶颈。
这里给个参考值:
| 硬件 | FP16算力 | 显存带宽 | 平衡点(FLOPs/Byte) |
|---|---|---|---|
| T4 | 65 TFLOPS | 320 GB/s | ~203 |
| A100 | 312 TFLOPS | 1.6 TB/s | ~195 |
| V100 | 125 TFLOPS | 900 GB/s | ~139 |
注意,这个平衡点只是理论值。实际中因为内存层次结构、指令发射效率等因素,有效平衡点会低一些。我个人习惯取一半作为经验值。
一张图看懂核心逻辑
下面这张图是我自己总结的推理性能瓶颈分析框架,帮你快速定位问题出在哪:
实际优化中的取舍
知道了瓶颈类型,优化方向就清晰了:
- 访存密集型:核心思路是减少访存次数。算子融合就是干这个的——把多个小算子合并成一个,数据只搬一次。比如把LayerNorm和后面的Add合并,能省掉一次中间结果的读写。
- 计算密集型:核心思路是提高计算效率。用Tensor Core、调整tile大小、利用指令级并行。说白了就是让硬件算得更快。
注意:别以为访存密集型瓶颈就好优化。我见过有人把两个访存密集型的算子硬凑在一起,结果因为数据依赖反而更慢了。融合之前一定要分析数据流图,确保没有额外的同步开销。
好了,这一章的内容就到这。记住一句话:先找瓶颈,再谈优化。别一上来就想着怎么融合算子,先搞清楚你的模型到底是被带宽卡住了,还是被算力卡住了。
下一章咱们会深入讲算子融合的具体策略,包括怎么选择融合的算子、融合后的kernel怎么写。到时候我会拿几个真实案例来拆解。