CUDA 08 · 性能模型与优化方法

优化是一个证据循环

NVIDIA Best Practices 用 APOD 概括过程:Assess、Parallelize、Optimize、Deploy。落实到 kernel:

建立正确性与基线
      ↓
找出端到端热点(Systems)
      ↓
判断 kernel 上限:内存、计算、延迟、并行度、提交开销
      ↓
只改一个主要假设
      ↓
重新验证正确性 + benchmark + profile
      ↓
收益可复现?──否→回退/记录  是→进入下一瓶颈

没有 profile 的“优化”通常只是把复杂度搬到别处。

先定义测量边界

目标应测什么
单 kernel 计算效率CUDA Event 包住重复 kernel;输入已在 device
GPU pipelineEvent 或 Systems 时间线;包含相关 memcpy/kernel
API/launch overheadHost timer + Systems;短任务批量重复
用户端延迟Host wall-clock;包含同步、分配、传输和框架开销
吞吐稳态下单位时间处理的样本/字节/边/元素

一个优化可能使 kernel 快 20%,却因新增预处理让端到端变慢。先写清包含哪些阶段。

Roofline 模型

算术强度:

AI=FLOPsBytes transferred from target memory levelAI=\frac{\text{FLOPs}}{\text{Bytes transferred from target memory level}}

可达性能上界:

P≤min⁡(Ppeak,  BWpeak×AI)P\le \min(P_{peak},\; BW_{peak}\times AI)

性能 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:

BWeff=12NtBW_{eff}=\frac{12N}{t}

GEMM:

GFLOP/s=2MNKt×109GFLOP/s=\frac{2MNK}{t\times10^9}

这些是算法口径。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 数大致受最小约束:

Bactive=min⁡(BblockLimit,BthreadLimit,BregisterLimit,BsmemLimit)B_{active}=\min(B_{blockLimit}, B_{threadLimit}, B_{registerLimit}, B_{smemLimit})

进一步得到 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 memorycache hit、合并、并发 warp、预取
Short scoreboard等较近的数据通路/shared 等依赖bank conflict、流水线、依赖距离
Barrier等同 block 其他 warp工作不均、barrier 频度、active blocks
Not selectedwarp 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 规范

  1. 固定并记录 GPU、驱动、Toolkit、编译选项和功耗/时钟环境。
  2. 预热 context、JIT、内存池、库 heuristic 和 cache 相关路径。
  3. 数据规模覆盖小、中、大和真实 shape;不要只测一个整齐尺寸。
  4. 重复足够多次,报告 median、p90/p99 或均值与方差。
  5. 把分配、传输、同步是否计入写清楚。
  6. 每次优化后先做 correctness,避免“少算了所以更快”。
  7. 与 CPU reference、朴素 GPU baseline 和成熟库分别比较。
  8. 记录冷启动与稳态;它们回答不同问题。
  9. 避免其他进程、温度、功耗和动态频率造成不可控噪声。
  10. 结果用有效 GB/s、GFLOP/s 或业务吞吐解释,不只报毫秒。

一个完整诊断案例

假设 elementwise pipeline 有三个 kernel:scale、bias、ReLU。

Nsight Systems:三个 20 µs kernel,中间有明显 launch gap
Nsight Compute:每个 kernel DRAM 吞吐高、计算吞吐低

推理:

  1. 三步均为低 AI,受内存带宽限制;
  2. 每个中间数组都写回再读取;
  3. kernel 很短,launch gap 也不可忽略;
  4. 将三步融合后,理想上从多轮读写降为一次输入读 + 一次输出写,同时少两个 launch;
  5. 新 kernel 若 register 仍低且 grid 足够大,应显著改善端到端;
  6. 用实际 DRAM bytes、时间线与误差测试验证预测。

这个过程先建立可证伪的性能假设,再写代码。

优化完成标准

  • 正确性覆盖真实分布、边界、NaN/Inf 和多架构;
  • 瓶颈由指标解释,不只是“试出来这个参数快”;
  • 与理论/实测 roof 的差距有合理原因;
  • 参数不依赖某一 GPU 的隐含常数,或有显式 dispatch;
  • 性能回归测试有容忍噪声的阈值;
  • 复杂优化的收益大于维护成本。

官方参考