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。

如何快速定位瓶颈类型

我一般用三步法:

  1. 跑一次profile:用NVIDIA Nsight或PyTorch Profiler,拿到每个算子的耗时和计算量。
  2. 算计算密度:每个算子的FLOPs除以访存量,看落在哪个区间。
  3. 对比硬件能力:如果算子的计算密度 < 硬件平衡点,那就是访存瓶颈;反之是计算瓶颈。

这里给个参考值:

硬件 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

注意,这个平衡点只是理论值。实际中因为内存层次结构、指令发射效率等因素,有效平衡点会低一些。我个人习惯取一半作为经验值。

一张图看懂核心逻辑

下面这张图是我自己总结的推理性能瓶颈分析框架,帮你快速定位问题出在哪:

推理性能瓶颈分析框架 模型推理Profile数据 计算密度 是否高于平衡点? 否 访存密集型瓶颈 优化方向 • 算子融合(减少访存次数) • 内存布局优化(合并搬运) 是 计算密集型瓶颈 优化方向 • 使用Tensor Core / SIMD • 计算图优化(消除冗余) 核心指标:计算密度 = 总计算量(FLOPs) / 总访存量(Bytes)

实际优化中的取舍

知道了瓶颈类型,优化方向就清晰了:

  • 访存密集型:核心思路是减少访存次数。算子融合就是干这个的——把多个小算子合并成一个,数据只搬一次。比如把LayerNorm和后面的Add合并,能省掉一次中间结果的读写。
  • 计算密集型:核心思路是提高计算效率。用Tensor Core、调整tile大小、利用指令级并行。说白了就是让硬件算得更快。

注意:别以为访存密集型瓶颈就好优化。我见过有人把两个访存密集型的算子硬凑在一起,结果因为数据依赖反而更慢了。融合之前一定要分析数据流图,确保没有额外的同步开销。

好了,这一章的内容就到这。记住一句话:先找瓶颈,再谈优化。别一上来就想着怎么融合算子,先搞清楚你的模型到底是被带宽卡住了,还是被算力卡住了。

下一章咱们会深入讲算子融合的具体策略,包括怎么选择融合的算子、融合后的kernel怎么写。到时候我会拿几个真实案例来拆解。