1. 引言
测试算子?这不是很简单么?我一开始也是这么想的。但是实际大规模地测试后,才发现有很多坑点,。比如下面的 10 问:
- 硬件是否可靠?卡有没有降频?
- 你的循环测试kernel 接口时,有没有考虑到撞到功耗墙导致降频?
- 有没有考虑到监控温度,避免撞功耗墙?
- 有没有测试卡之前先摸底,探一下卡的实际的算力/带宽峰值?
- 计时手段是在 CPU 侧计时,还是在 GPU 侧用 events?
- 计时过程是一个 for 循环,不断地 launch kernel 最后求平均值,还是使用CUDA Graph 捕图后 replay 来估算时间?
- 有没有考虑刷新 L2 的缓存?
- 输入是模拟真实场景,数据间有空间相关性,还是输入完全打乱,导致 cache miss?
- 测试出来的时间数据,进行后加工计算 MFU/MBU 时,你的 FLOPs 和 IOs 计算公式是否对齐业界?
- 测试出来的时间数据,是取平均值还是中位数?还是两者结合考虑,相差不大时采用平均数,相差较大时采用中位数?
- GEMM 算子,deep_gemm 是调用 1d1d,还是 1d2d?
- gemm 算子,一次 fp8 gemm 包含了 scale 转换的前置步骤,这段时间算不算进 kernel 时间?如果用 cuda graph 捕获后重放来计时,虽然稳定了,但是仍然会导致一些 overhead,从而和 nsys 对不上,有没有更高级的方法?
以上任意一个点没有做好,都会导致你测试出来的算子的性能和别人实际使用中的性能对不上。会怀疑你的基础架构没有做好,严重点会怀疑测试方式等等。所以下面的部分,会从算子自己的性能优化入手,先了解算子是怎么 work 的,才能有的放矢地对算子性能做系统性地评估。
2. 以 Attention 算子为例
真实 case:在部署的引擎中,bs=160,q_len=3, kv_len=5003+200 约等于 5k 时,nsys 抓取出来的图表中显示,kernel 的耗时为 430 us。 使用测试框架/脚本进行算子单测时,最终时间耗时为 501 us 左右。这中间存在许多 gap,需要逐一排查。
2.1 测试框架撞到功耗墙:误差 60us