CUDA 3.1 CUDA执行模型概述
这一篇开始,就要接近CUDA最核心的部分,有关硬件和程序的执行模型的部分。
前面几篇我们学了编写,启动核函数,计时,统计时间,然后学习了线程,内存模型,线程内存部分。接下来我们将继续学习最底层最优理论指导意义的知识。
一、概述
GPU架构是围绕一个流式多处理器(SM)的可扩展阵列搭建的。可以通过复制这种架构的构建块来实现GPU的硬件并行。

上图包括关键组件:
- CUDA核心
- 共享内存/一级缓存
- 寄存器文件
- 加载/存储单元
- 特殊功能单元
- 线程束调度器
1.1 流式多处理器(SM)
GPU中每个流式多处理器(SM)都能支持数百个线程并发执行,每个GPU通常有多个SM,当一个核函数的网格被启动的时候,多个block会被同时分配给可用的SM上执行。
注意: 当一个block被分配给一个SM后,他就只能在这个SM上执行了,不可能重新分配到其他SM上了,多个线程块可以被分配到同一个SM上。
在SM上同一个块内的多个线程进行线程级别并行,而同一线程内,指令利用指令级并行将单个线程处理成流水线。
1.2 线程束
CUDA采用单指令多线程(SIMT)架构来管理和执行线程,每32个线程为一组,被称为线程束(warp)。 不同设备有不同的线程束大小。从机器的角度,在某时刻T,SM上只执行一个线程束,也就是32个线程在同时同步执行,线程束中的每个线程执行同一条指令,包括有分支的部分,这个我们后面会讨论。
1.3 SIMD vs SIMT
单指令多数据(SIMD)的执行就像四匹马排成一排拉马车,比如有四个数字要加上四个数字,那么使用这种单指令多数据(SIMD)的指令一次完成原本要做四次的运算。
- 这种机制的问题就是过于死板,不允许每个分支有不同的操作,所有分支必须同时执行相同的指令,必须执行没有例外。
单指令多线程(SIMT)就更灵活,虽然两者都是将相同指令广播给多个执行单元,但(SIMT)的某些线程可以选择不执行。
- 同一时刻下所有线程被分配给相同的指令,SIMD规定所有人必须执行,而SIMT则规定有些人可以根据需要不执行,这样SIMT就保证了线程级别的并行,而SIMD更像是指令级别的并行。
SIMT包括以下SIMD不具有的关键特性:
每个线程都有自己的指令地址计数器
每个县城都有自己的寄存器状态
每个线程可以有一个独立的执行路径
而上面这三个特性在编程模型可用的方式就是给每个线程一个唯一的标号(blckIdx,threadIdx),并且这三个特性保证了各线程之间的独立
1.4 最常用的数字 32
32在CUDA程序里是一个神奇的数字。它来自于硬件系统,也对软件的性能有着重要的影响。
从概念上讲,32是SM以SIMD方式同时处理的工作粒度,一个SM上在某一个时刻,有32个线程在执行同一条指令,这32个线程可以选择性执行,虽然有些可以不执行,但是他也不能执行别的指令,需要另外需要执行这条指令的线程执行完,然后再继续下一条,
这里参考大佬谭升举的例子:像老师给小朋友们分水果:
第一次分苹果🍎,分给所有32个人,你可以不吃,但是不吃也没别的,你就只能在那看别人吃,等别人吃完了,老师会把没吃的苹果回收,防止浪费。
第二次分橘子🍊,你很爱吃,可是有别的小朋友不爱吃,当然这时候他也不能干别的,只能看你吃完。吃完后老师继续回收刚才没吃的橘子。
第三次分桃子🍑,大家都很爱吃,大家一起吃,吃完了老师发现没有剩下的,继续发别的水果,一直发到所有种类的水果都发完了。今天就可以放学了。
1.5 CUDA编程的组件与逻辑
下图从逻辑角度和硬件角度描述了CUDA编程模型对应的组件。

一个线程块只能在一个SM上被调度。一旦线程块在一个SM上被调度,就会保存在该SM上直到执行完成。在同一时间,一个SM可以容纳多个线程块。
SM中共享内存,和寄存器是关键的资源,线程块中线程通过共享内存和寄存器相互通信协调。
寄存器和共享内存的分配可以严重影响性能!
尽管线程块里的所有线程都可以逻辑地并行运行,但是并不是所有线程都可以同时在物理层面执行。因此,线程块里的不同线程可能会以不同的速度前进。
二、Fermi 架构

Fermi架构逻辑图如上,具体数据如下:
- 512个加速核心,CUDA核
- 每个CUDA核心都有一个全流水线的整数算数逻辑单元ALU,和一个浮点数运算单元FPU
- CUDA核被组织到16个SM上
- 6个384-bits的GDDR5 的内存接口
- 支持6G的全局机栽内存
- GigaThread疫情,分配线程块到SM线程束调度器上
- 768KB的二级缓存,被所有SM共享
而SM则包括下面这些资源:
- 执行单元(CUDA核)
- 调度线程束的调度器和调度单元
- 共享内存,寄存器文件和一级缓存
每个多处理器SM有16个加载/存储单元所以每个时钟周期内有16个线程(半个线程束)计算源地址和目的地址
特殊功能单元SFU执行固有指令,如正弦,余弦,平方根和插值,SFU在每个时钟周期内的每个线程上执行一个固有指令。
每个SM有两个线程束调度器,和两个指令调度单元,当一个线程块被指定给一个SM时,线程块内的所有线程被分成线程束,两个线程束选择其中两个线程束,在用指令调度器存储两个线程束要执行的指令(就像上面例子中分水果的水果一样,我们这里有两个班,两个班的老师各自控制的自己的水果,老师就是指令调度器)
像第一张图上的显示一样,每16个CUDA核心为一个组,还有16个加载/存储单元或4个特殊功能单元。当某个线程块被分配到一个SM上的时候,会被分成多个线程束,线程束在SM上交替执行:

上面曾经说过,每个线程束在同一时间执行同一指令,同一个块内的线程束互相切换是没有时间消耗的。
Fermi上支持同时并发执行内核。并发执行内核允许执行一些小的内核程序来充分利用GPU,如图:

3 Kepler 架构
Kepler架构作为Fermi架构的后代,有以下技术突破:
- 强化的SM
- 动态并行
- Hyper-Q技术
技术参数也提高了不少,比如单个SM上CUDA核的数量,SFU的数量,LD/ST的数量等:


kepler架构的最突出的一个特点就是内核可以启动内核了,这使得我们可以使用GPU完成简单的递归操作,流程如下。

Hyper-Q技术主要是CPU和GPU之间的同步硬件连接,以确保CPU在GPU执行的同事做更多的工作。Fermi架构下CPU控制GPU只有一个队列,Kepler架构下可以通过Hyper-Q技术实现多个队列如下图。

计算能力概览:

4 使用Profile进行优化(Profile-Driven Optimization)
中文翻译的这个标题是配置文件驱动优化,驱动这个词在这里应该是个动词,或者翻译的人直接按照字面意思翻译的,其实看完内容以后的意思是根据profile这个文件内的信息对程序进行优化。
性能分析通过以下方法来进行:
- 应用程序代码的空间(内存)或时间复杂度
- 特殊指令的使用
- 函数调用的频率和持续时间
程序优化是建立在对硬件和算法过程理解的基础上的,如果都不了解,靠试验,那么这个结果可想而知。理解平台的执行模型也就是硬件特点,是优化性能的基础。
开发高性能计算程序的两步:
- 保证结果正确,和程序健壮性
- 优化速度
Profile可以帮助我们观察程序内部。
一个原生的内核应用一般不会产生最佳效果,也就是我们基本不能一下子就写出最好最快的内核,需要通过性能分析工具分析性能。找出性能瓶颈
CUDA将SM中的计算资源在该SM中的多个常驻线程块之间进行分配,这种分配方式可能导致一些资源成为性能限制因素,性能分析工具可以帮我们找出来这些资源是如何被使用的
CUDA提供了一个硬件架构的抽象。它能够让用户控制线程并发。性能分析工具可以检测和优化,并肩优化可视化
总结起来一句话,想优化速度,先学好怎么用性能分析工具。
- nvvp
- nvprof
限制内核性能的主要包括但不限于以下因素
- 存储带宽
- 计算资源
- 指令和内存延迟