CUDA 08 · 性能模型与优化方法
优化是一个证据循环
NVIDIA Best Practices 用 APOD 概括过程:Assess、Parallelize、Optimize、Deploy。落实到 kernel:
建立正确性与基线
↓
找出端到端热点(Systems)
↓
判断 kernel 上限:内存、计算、延迟、并行度、提交开销
↓
只改一个主要假设
↓
重新验证正确性 + benchmark + profile
↓
收益可复现?──否→回退/记录 是→进入下一瓶颈没有 profile 的“优化”通常只是把复杂度搬到别处。
先定义测量边界
| 目标 | 应测什么 |
|---|---|
| 单 kernel 计算效率 | CUDA Event 包住重复 kernel;输入已在 device |
| GPU pipeline | Event 或 Systems 时间线;包含相关 memcpy/kernel |
| API/launch overhead | Host timer + Systems;短任务批量重复 |
| 用户端延迟 | Host wall-clock;包含同步、分配、传输和框架开销 |
| 吞吐 | 稳态下单位时间处理的样本/字节/边/元素 |
一个优化可能使 kernel 快 20%,却因新增预处理让端到端变慢。先写清包含哪些阶段。
Roofline 模型
算术强度:
可达性能上界:
性能 FLOP/s
^ ───────── compute roof
| /
| /
| / 转折点 AI* = Peak FLOP/s / Peak BW
| /
|_______________/____________________________> arithmetic intensity
memory-bound compute-bound示例:某设备持续带宽 1 TB/s、目标精度持续计算 20 TFLOP/s,则平衡点约 20 FLOP/B。AI 明显低于 20 的 kernel,仅做指令微调通常收效有限,应先减少字节或增加复用。
“Peak”最好用同机 microbenchmark 或可靠实测,而非只用宣传规格。Roofline 也可针对 L1/L2/shared 层级建立 hierarchical roofline。
有效带宽与吞吐
向量加法 c=a+b 的最小显式流量约为每元素读 8 B、写 4 B:
GEMM:
这些是算法口径。Profiler 的 DRAM bytes 是实际硬件流量口径,包括缓存效果和可能的额外事务。二者差异本身就是诊断信息。
延迟、带宽和并行度不是同一件事
- Bandwidth-bound:内存管线吞吐接近上限,更多 warp 不会增加带宽。
- Latency-bound:单次访问长,但并发请求不足以填满管线;long scoreboard 高而 DRAM 吞吐不高。
- Compute-bound:目标算术管线接近上限。
- Instruction/issue-bound:地址计算、转换、分支、依赖链或调度限制发射。
- Launch-bound:kernel 太短,CPU/driver 提交与调度占主要时间。
同样“计算吞吐低、内存吞吐低”可能是网格太小、同步太多、依赖链太长、分支低效或 launch overhead,不能直接判为“还有很多优化空间”。
Occupancy 的资源账本
一个 SM 可驻留 block 数大致受最小约束:
进一步得到 active warps 与 theoretical occupancy。资源按硬件粒度分配,实际公式和上限随 CC 变化,应使用 occupancy API/Calculator。
调参思路:
| 限制 | 可能动作 | 代价 |
|---|---|---|
| Registers | 减少 live range、拆函数/阶段、调整 tile | 可能 spill、减少 ILP/复用 |
| Shared memory | 缩小 tile/stages、改变布局 | 复用下降、同步增多 |
| Threads/block | 改 block shape/size | 映射、合并访问、负载变化 |
| Grid 太小 | split 工作、批处理、多 kernel 并发 | 归约或调度成本增加 |
追求 100% occupancy 常是错误目标。计算密集 kernel 可能需要大量寄存器以保持 accumulator 和 ILP,较低 occupancy 仍更快。
Warp stall 的读法
常见 stall 类别的含义依架构和 Nsight Compute 版本变化,阅读时关注“为什么这个 warp 下一条指令不能发射”:
| 现象 | 常见解释 | 先检查 |
|---|---|---|
| Long scoreboard | 等较长延迟的数据依赖,常见于 global/local memory | cache hit、合并、并发 warp、预取 |
| Short scoreboard | 等较近的数据通路/shared 等依赖 | bank conflict、流水线、依赖距离 |
| Barrier | 等同 block 其他 warp | 工作不均、barrier 频度、active blocks |
| Not selected | warp ready 但调度器选了别人 | 可能只是并发充足,不必“修” |
| Wait / math pipe throttle | 指令依赖或执行管线饱和 | 指令 mix、ILP、目标管线吞吐 |
| MIO/LG throttle | 内存指令队列或相关管线压力 | 访问次数、向量化、shared 操作 |
Stall 指标是线索,不是自动处方。结合 Speed of Light、Memory Workload、Scheduler、Source/SASS 才能闭环。
从症状到实验
| 症状 | 假设 | 最小实验 |
|---|---|---|
| DRAM 高、AI 低 | 必然 memory-bound | 融合或减少一次读写,预测节省字节 |
| DRAM 低、long scoreboard 高 | 并发请求不足/访问离散 | 增加 grid/occupancy;重排地址;比较 L2 hit |
| shared conflict 高 | bank 映射冲突 | padding 或 swizzle,只改布局 |
| local traffic 高 | register spill/局部数组 | 查 ptxas 和 Source;缩短 live range |
| barrier stall 高 | warp 工作不均 | 重分任务、减少循环差异、增加 active block |
| 短 kernel 密集 | launch-bound | 融合、批处理、CUDA Graph |
| tensor pipe 低 | shape/layout/类型或供数不足 | 对照 cuBLASLt;查 MMA 指令与数据管线 |
| 尾部变慢 | 负载不均或 wave quantization | 改 block 数/任务粒度,观察 SM wave |
Wave quantization 与尾效应
假设 GPU 能同时驻留 R 个 block,而 grid 有 R+1 个 block:前 R 个形成一整 wave,最后只有一个 block 独占设备跑第二 wave,总时间接近两 wave。实际 R 由 SM 数与每 SM active blocks 共同决定。
这解释了某些 block size “理论 occupancy 相同但时间跳变”的现象。检查 grid size、active blocks/SM 和每个 block 时长,不要只看百分比 occupancy。
指令级优化应放在最后
可能手段:
- 常量除法/模运算让编译器化简;
- 使用合适 intrinsic、FMA、向量 load/store;
- 展开短固定循环;
- 减少重复地址计算和类型转换;
- 在误差允许时选择 fast math 或低精度;
- 增加独立累加器以提升 ILP。
风险:代码尺寸、寄存器压力、指令缓存、数值误差和可移植性。必须检查生成代码与实测,不要假定 #pragma unroll 必然更快。
Kernel 融合的边界
融合的收益:
- 消除中间 global read/write;
- 减少 launch;
- 在寄存器中传递中间值。
融合的代价:
- register/shared 压力上升;
- occupancy 降低;
- 不能独立调度或复用中间结果;
- 编译时间和代码复杂度增加;
- 不同阶段的最佳线程映射可能冲突。
判断标准:节省的字节/launch 是否大于资源和映射代价。用端到端时间回答。
可靠 Benchmark 规范
- 固定并记录 GPU、驱动、Toolkit、编译选项和功耗/时钟环境。
- 预热 context、JIT、内存池、库 heuristic 和 cache 相关路径。
- 数据规模覆盖小、中、大和真实 shape;不要只测一个整齐尺寸。
- 重复足够多次,报告 median、p90/p99 或均值与方差。
- 把分配、传输、同步是否计入写清楚。
- 每次优化后先做 correctness,避免“少算了所以更快”。
- 与 CPU reference、朴素 GPU baseline 和成熟库分别比较。
- 记录冷启动与稳态;它们回答不同问题。
- 避免其他进程、温度、功耗和动态频率造成不可控噪声。
- 结果用有效 GB/s、GFLOP/s 或业务吞吐解释,不只报毫秒。
一个完整诊断案例
假设 elementwise pipeline 有三个 kernel:scale、bias、ReLU。
Nsight Systems:三个 20 µs kernel,中间有明显 launch gap
Nsight Compute:每个 kernel DRAM 吞吐高、计算吞吐低推理:
- 三步均为低 AI,受内存带宽限制;
- 每个中间数组都写回再读取;
- kernel 很短,launch gap 也不可忽略;
- 将三步融合后,理想上从多轮读写降为一次输入读 + 一次输出写,同时少两个 launch;
- 新 kernel 若 register 仍低且 grid 足够大,应显著改善端到端;
- 用实际 DRAM bytes、时间线与误差测试验证预测。
这个过程先建立可证伪的性能假设,再写代码。
优化完成标准
- 正确性覆盖真实分布、边界、NaN/Inf 和多架构;
- 瓶颈由指标解释,不只是“试出来这个参数快”;
- 与理论/实测 roof 的差距有合理原因;
- 参数不依赖某一 GPU 的隐含常数,或有显式 dispatch;
- 性能回归测试有容忍噪声的阈值;
- 复杂优化的收益大于维护成本。