3、计算瓶颈分析:FLOPs利用率、算子耗时分布、矩阵乘法与卷积的优化空间
好,咱们进入第三个核心话题——计算瓶颈分析。
说实话,很多同学跑模型的时候,一看GPU利用率100%,就觉得“嗯,跑满了”。但你真的跑满了吗?我见过太多项目,GPU利用率看着高,实际算力只用了不到30%。那剩下的70%去哪了?这就是我们今天要聊的——FLOPs利用率。
3.1 FLOPs利用率:别被数字骗了
FLOPs利用率,说白了就是你的硬件到底发挥了多大本事。
公式很简单:
FLOPs利用率 = 实际达到的FLOPs / 硬件理论峰值FLOPs
举个例子。一块A100,FP16的理论峰值是312 TFLOPS。你跑一个BERT-Large,实测只有80 TFLOPS。那利用率就是80/312 ≈ 25.6%。
嗯,这个数字其实挺常见的。我刚开始做推理优化时,看到25%的利用率还觉得正常。后来才发现,这中间有巨大的优化空间。
关键认知:FLOPs利用率低,不代表模型小,而是说明计算单元在“等数据”。
为什么会这样?原因主要有三个:
- 内存带宽瓶颈——数据从HBM搬到SRAM太慢,计算单元闲着等数据
- 算子启动开销——小算子太多,每次启动kernel都有固定开销
- 计算密度不足——矩阵太小,无法填满GPU的SM(流式多处理器)
我个人习惯,拿到一个模型先看FLOPs利用率。低于30%的,基本都有优化空间。低于10%的,那简直就是“开着超跑在胡同里遛弯”。
3.2 算子耗时分布:找到那个“罪魁祸首”
光看整体利用率还不够。你得知道时间花在哪了。
我建议的做法是——做一次profiling。用NVIDIA的Nsight Systems或者PyTorch Profiler都行。
下面是我常用的分析流程:
- 跑一次完整推理,记录每个算子的耗时
- 按耗时从大到小排序
- 看Top 5算子占总耗时的比例
- 分析这些算子的计算密度(FLOPs / 访存量)
给你看个真实案例。我之前优化一个GPT-like模型,profiling结果是这样的:
| 算子 | 耗时占比 | 计算密度 | 优化建议 |
|---|---|---|---|
| MatMul (QKV投影) | 32% | 低 | 使用cuBLASLt或Triton手写 |
| Softmax | 18% | 极低 | 融合到Attention中 |
| LayerNorm | 12% | 极低 | 融合到前一个MatMul |
| Reshape + Transpose | 8% | 极低 | 尽量消除,或使用layout优化 |
| ElementWise Add | 5% | 极低 | 融合到前一个算子 |
看到没?MatMul占了32%,但剩下的68%里,大部分是“计算密度极低”的算子。这些算子才是真正的性能杀手。
我的经验:很多时候,优化一个MatMul只能提升5%,但把三个小算子融合成一个,能省下15%的时间。别只盯着大算子。
3.3 矩阵乘法的优化空间
矩阵乘法是深度学习的心脏。几乎所有模型的计算量都集中在MatMul上。
那MatMul的优化空间在哪?我总结了几点:
3.3.1 分块(Tiling)策略
GPU的共享内存只有几十KB。一次算完大矩阵是不可能的。必须分块。
关键参数是块大小(block size)。太小了,SM利用率低;太大了,共享内存不够用。
// 常见的分块大小
// A100: 128x128 或 128x64
// V100: 64x64 或 128x32
// T4: 64x64 或 32x32
我记得有一次,一个客户在A100上跑一个M=512, N=4096, K=1024的MatMul。默认的cuBLAS实现只用了40%的利用率。我手动调整了分块策略,把利用率提到了65%。
3.3.2 数据布局(Layout)
矩阵在内存中的排列方式,直接影响访存效率。
- 行主序(Row-major):C/C++默认,连续行
- 列主序(Col-major):Fortran默认,连续列
- 平铺(Tiled):按块排列,适合GPU
我建议,对于推理场景,尽量使用平铺布局。虽然转换有开销,但一次转换,多次使用,整体收益很大。
3.3.3 使用Tensor Core
从V100开始,NVIDIA引入了Tensor Core。这东西专门做矩阵乘法,速度是普通CUDA Core的几倍。
但要注意:Tensor Core对数据精度和形状有要求。
// Tensor Core要求:
// 1. 数据类型:FP16, BF16, INT8, INT4
// 2. 维度:M, N, K 必须是16的倍数(最好64的倍数)
// 3. 布局:必须使用特定的数据布局(如row-major for A, col-major for B)
避坑指南:我曾经遇到一个模型,所有MatMul的K维度都是17。结果Tensor Core根本用不上,性能直接腰斩。后来我把K padding到32,利用率从28%提到了52%。
3.4 卷积的优化空间
虽然Transformer火了之后,卷积用得少了,但在视觉模型里,卷积依然是主力。
卷积的优化,核心思路和矩阵乘法类似,但更复杂一些。
3.4.1 Im2Col + GEMM
这是最经典的方法。把卷积转换成矩阵乘法,然后调用优化好的GEMM库。
优点:简单,复用MatMul优化。缺点:内存开销大,Im2Col会复制数据。
3.4.2 Winograd算法
对于小卷积核(3x3, 5x5),Winograd能减少乘法次数。
我记得在优化ResNet-50时,用Winograd把3x3卷积的FLOPs减少了约30%。但要注意,Winograd对精度有影响,FP16下偶尔会出问题。
3.4.3 直接卷积(Direct Convolution)
对于大卷积核(7x7以上),或者特殊步长(stride=2),直接卷积反而更快。
为什么?因为Im2Col的内存开销太大了,直接卷积可以避免这个开销。
下面是我总结的卷积优化选择策略:
| 卷积类型 | 推荐方法 | 适用场景 |
|---|---|---|
| 3x3, stride=1 | Winograd | ResNet, MobileNet |
| 1x1 | Im2Col + GEMM | Inception, ShuffleNet |
| 7x7, stride=2 | Direct Convolution | ResNet初始层 |
| Depthwise | 手写优化 | MobileNet, EfficientNet |
3.5 知识体系总览
说了这么多,咱们用一张图把整个计算瓶颈分析的知识体系串起来。
这张图把整个计算瓶颈分析的核心逻辑串起来了。从左到右,从发现问题(FLOPs利用率低),到定位问题(算子耗时分布),再到解决问题(矩阵乘法与卷积优化)。
我的建议:每次拿到一个新模型,先按这个流程走一遍。花半天时间做profiling,比盲目优化一周更有效。
好了,计算瓶颈分析就聊到这。记住一句话:别只看GPU利用率,要看FLOPs利用率;别只看大算子,要关注那些“小但多”的算子。
想要更多资料,联系可可