工作原因,需要了解一下 GPU 的硬件和 CUDA 的对应关系和调度方法。由于不是专职优化 GPU 代码,所以就是个大概了解。
1. 高吞吐,低响应
2. 无需 cache (目前实际硬件有),无需复杂的指令调度(多个线程走的都是一样的指令)
3. 节约硬件空间(一次 fetch/decode/dispatch 就可以支持很多个线程,也没有 branch prediction/乱序等等优化需求,硬件空间全省)
4. 可以塞更多的 ALU,增加算力。

内存特性

访问路径
新的硬件(应该是Maxwell之后) L1 cache 和 texture cache 合并,所以没有必要再区分了。而 Global Memory 的读写只经过 L2 cache。
Cache 这块实际上会有更多细分和变化,可以参考https://arxiv.org/pdf/1804.06826.pdf。
两个补充概念
1. Bank conflict

shared memory 会通过 bank(现在硬件:32 个, 4-byte 1区分) 管理, 同一 bank 内的存取会有 bank conflicts,无法并发。所以在内存读取的时候需要考虑怎样安排计算顺序,让不同 thread 可以操作不同 bank 的数据,增加并行度。
无 bank confilict 的特殊情况:

下面有是不同严重情况下的 bank conflict 的延迟损耗(由于一个 Warp 的调度粒度是 32 threads, 所以,最多也就是 32-way bank conflict 了):

上图可见,极限情况下,同一个 warp 内的线程都访问同一个 bank 的不同地址,会有 30 倍的性能衰减 (也就是 32-way bank conflict)。
2. Half warp
由于 bank conflict 会引入 half warp 这个概念,不过新的硬件已经不需要考虑了,并行单元都是 full warp。(可以参见 https://stackoverflow.com/questions/14909241/whats-the-mechanism-of-the-warps-and-the-banks-in-cuda , 这里作者也提到了可能衰减回 half-warp 的可能性)
Cache
旧硬件是没有cache的,后续应该是为了增加局部性的性能,有了 L1,L2 cache。
L1 和 Shared Mem 共享硬件空间(可以理解为 shared mem 是程序员自己管理的 L1 cache),可以设置大小 trade off:
Cache Line 的大小:
由于这些值实际和硬件设计相关,未必能对应到新的硬件上。 (关于 cache line 参见https://zhuanlan.zhihu.com/p/37749443, CPU 的,不过在这里是通用的)
所以对于数据如果出现 misalign 的访问,性能也会掉(和 CPU 一样的逻辑),GPU 默认会做 256 bytes 的 align。
代码与存储的映射关系

从大到小的顺序,其中 SP 是基本单位:
Processor Cluster -> Steaming Multiprocessor(SM) -> Stream Processor(SP / CUDA Core)
GA102 结构(30系列游戏卡)
3090 实际硬件是 41 TPC,所以对应的硬件 sped 都是少了一个TPC的数值。
每个 SM 分成四个 block,INT32 和 FP32 可以并行处理。
具体可以参看:

软硬件概念对应
线程间的 context switching 很小,会提前预分配好很多线程的上下文。最简单粗暴的优化就是堆线程。只要等待执行的线程够多,Tensor Core 就可以不断的运行。
调度单位 Warp
目前硬件是 32 threads / warp, warp 是线程调度的基本单位,无论如何,每次必调度32线程执行,即使只执行16线程,另外的16线程也会占用硬件。
同一个 warp 里面执行的指令是一样的,所以也会共用 instruction fetch/dispatch (这就是所谓的 SIMT 模式),同一个 warp 里面的线程称作 lanes。
与 Warp 相关的一些优化,可以参见https://developer.nvidia.com/blog/cuda-pro-tip-optimized-filtering-warp-aggregated-atomics/(新的编译器已经自动优化这个场景了,看看思路就可以了)
Warp 数量
Warp 中如果出现有等待(比如读取 global mem),会调入其他 warp 执行,所以只要等待执行的 warp 足够,就可以隐藏掉数据存取的开销。如果计算密集和访存密集的程序放在一个SM上,就可以互相隐藏,获得更好的吞吐量。
原则指南:Hide latency with computation。