Lesson 06 · CUDA 性能调优 · 回收"延迟瓶颈"
前五节把访存优化讲透了。但还记得 第 2 节那个最易看漏的诊断吗——访存和计算利用率两个都低? 那通常不是访存或计算的问题,而是并行度不够、延迟没被藏住。这一节回收它,讲 GPU 怎么靠 occupancy 把延迟藏起来。
访问全局内存要 ~400-800 周期(第 4 节的金字塔)。 CPU 遇到这种停顿会用乱序执行、大缓存硬扛。GPU 的哲学完全不同:不等,直接切到另一个 warp 干活。[1]
一个 warp 因为等内存停住(stall),调度器立刻挑另一个就绪的 warp 发射指令。 只要随时有就绪的 warp,SM 就一直在干活,内存延迟被"藏"在了其他 warp 的计算后面。
所以藏延迟的前提是:SM 上得有足够多的 warp 可供切换。warp 太少,大家都在等内存、没人能补上, SM 就空转——这正是延迟瓶颈:两个利用率都低。
它衡量的是调度器有多少候选 warp 可切换。occupancy 越高,可切换的 warp 越多,越能藏住延迟。 但"坐满席位"靠的是资源——而资源是有限的。
一个 SM 的资源是固定的,三种资源各自会成为"先用完的那个",从而卡住 occupancy:[1]
| 资源 | 怎么卡住 occupancy | Nsight 字段 |
|---|---|---|
| 寄存器/线程 | 每线程用太多寄存器 → SM 寄存器池装不下那么多线程 | limit due to registers |
| shared memory/block | 每 block 用太多 shared → 同时驻留的 block 变少 | limit due to shared mem |
| block 大小 | block 太小 → 撞上"每 SM 最多 N 个 block"的上限,warp 席位坐不满 | limit due to block size |
典型场景:你的 kernel 每线程要 64 个寄存器,而 SM 寄存器池只够 1024 个线程同时用 64 个—— 那 occupancy 就被锁在 50%(1024/2048),与 block 怎么配无关。这是"寄存器压力"限制 occupancy。
新手最大的误区是把 occupancy 当成"分数",拼命冲 100%。官方明确说:occupancy 高不一定快。[1]
原因有二:
但有一条是确定的:occupancy 过低一定伤性能——候选 warp 太少,延迟藏不住。[1] 所以它是个"下限要保证、上限别强求"的指标。
kernel 启动时你要定 <<<gridDim, blockDim>>>。落地经验:
| 选择 | 建议 |
|---|---|
| block 大小 | 取 32 的倍数(warp 对齐),常用 128 / 256;别太小(撞 block 数上限) |
| grid 大小 | 足够多 block 覆盖全部数据,且 ≥ SM 数量,让每个 SM 都有活干 |
| 不确定时 | 用 cudaOccupancyMaxPotentialBlockSize() 让运行时推荐一个 block 大小[1] |
// 让 CUDA 替你算出能最大化 occupancy 的 block 大小
int minGrid, blockSize;
cudaOccupancyMaxPotentialBlockSize(&minGrid, &blockSize, myKernel, 0, 0);
// 再据此算 grid:覆盖 n 个元素
int grid = (n + blockSize - 1) / blockSize;
myKernel<<>>(...);
Nsight Compute 的 Occupancy 段会同时给你理论 occupancy(资源算出的上限)和 实测 achieved occupancy。两者差距大,通常是负载不均(尾部效应、block 数不够)。[1]
混合了 occupancy 与前几节的诊断。点选项看反馈。
-Xptxas -v)?ILP 具体怎么写?
理论 vs 实测 occupancy 差很多怎么办?什么是尾部效应(tail effect)?我是你的老师,尽管问。
📘 CUDA C++ Best Practices Guide — Occupancy ——讲清 occupancy 定义、计算与"不是越高越好"。配合 Nsight Compute 的 Occupancy 段实践。