1.流水线前传
流水线前传(forwarding)是一种解决数据冒险(data hazard)的技术,它可以在流水线中将一个阶段的输出直接传递给后续阶段的输入,而不需要等待结果写回寄存器。这样可以避免流水线的暂停或插入气泡(bubble),提高CPU的效率。
ADD R1, R2, R3 // R1 = R2 + R3
SUB R4, R1, R5 // R4 = R1 - R5
对于一条指令,我们可以将其执行划分为以下五个步骤:
- 取指(IF,Instruction Fetch)
- 译码(ID,Instruction Decode)
- 执行(EX,Execute)
- 访存(MEM,Memory Access)
- 写回(WB,Write Back)
如果不使用流水线前传,那么第二条指令必须等待第一条指令将R1的结果写回寄存器后才能读取R1的值,这样就会造成数据冒险和流水线暂停。如果使用流水线前传,那么第一条指令在执行阶段(EX)就可以将R1的结果直接传递给第二条指令的执行阶段(EX),而不需要等待写回阶段(WB),这样就可以避免数据冒险和流水线暂停。
CPU流水线技术是一种将指令分解为多步,并让不同指令的各步操作重叠,从而实现几条指令并行处理,以加速程序运行过程的技术。
指令的每步有各自独立的电路来处理,每完成一步,就进到下一步,而前一步则处理后续指令,属于CPU硬件电路层面的并发。
引入流水线,利用空闲的硬件资源来提升并发性,获得更高的指令吞吐量。流水线前传机制可以使CPU的效率显著增加,因为它可以减少数据冒险导致的流水线暂停或插入气泡。流水线暂停或插入气泡会浪费CPU的时间和资源,降低吞吐率和性能。流水线前传机制可以使流水线更加充满和连续,提高指令的并行度和执行速度。
2.CPU的三级缓存
CPU中的三级缓存是一种高速缓存(临时存储器),它位于CPU和内存之间,用来存储CPU近期要用到的数据和指令,以提高访问速度和效率。(补充:流水线设计中的结构冒险本质上是一种硬件冲突,例如五级流水线,指令读取IF阶段和取数操作MEM,都需要进行内存数据的读取,然而内存只有一个地址译码器,只能在一个时钟周期里面读取一条数据。对于MEM阶段和IF阶段的冲突,一个解决方案就是把内存分成两部分:存放指令的内存和存放数据的内存,让它们有各自的地址译码器,从而通过增加硬件资源来解决冲突。这种将指令和数据分开储存的结构就是哈佛结构,指令和数据放在一起的就是冯诺依曼结构。这两种结构都有各自的优缺点,现代的CPU借鉴了两种架构采用一种混合结构,并且引入了高速缓存,来降低CPU运算速度和内存读写速度不匹配的问题。)
三级缓存通常分为L1 Cache、L2 Cache和L3 Cache,它们有以下的特点:
- L1 Cache是最快的缓存,也是最小的缓存,一般只有几十KB。它位于CPU核心内部,分为指令缓存和数据缓存,分开存放CPU使用的指令和数据。这样可以避免取指令单元和取数据单元争夺访缓存的问题,也可以利用程序的局部性原理提高命中率。
- L2 Cache是次快的缓存,也是次小的缓存,一般有几百KB到几MB。它也位于CPU核心内部,但是尺寸比L1更大。它采用统一缓存的策略,将指令和数据统一存放在一个缓存中。这样可以动态调节分配比例,最大化利用缓存空间。
- L3 Cache是最慢的缓存,也是最大的缓存,一般有几MB到几十MB。它位于CPU核心外部,所有CPU核心共享同一个L3缓存。它也采用统一缓存的策略,将指令和数据统一存放在一个缓存中。它可以增加不同核心之间的数据共享和协作能力,也可以减少对内存的访问压力。
三级缓存的作用是为了弥补CPU和内存之间的速度差异,提高CPU的吞吐率和性能。当CPU访问数据或指令时,会先从L1 Cache中查找,如果命中则直接使用;如果不命中,则从L2 Cache中查找,如果命中则直接使用;如果还不命中,则从L3 Cache中查找,如果命中则直接使用;如果都不命中,则从内存中读取,并将读取的数据或指令加载到三级缓存中。这样可以减少对内存的访问次数和延迟,提高程序的运行速度。
3.GPU的控制单元和计算单元
GPU的控制单元通常包括以下几个部分:
- Warp Scheduler:线程束调度器,它负责将一个或多个线程块中的线程组织成32个一组的线程束(warp),并将它们分配给计算单元中的CUDA核心(core)执行。一个SM(流多处理器)中可以有多个warp scheduler,每个warp scheduler可以同时调度多个warp。
- Register File:寄存器文件,它负责为每个线程提供一定数量的寄存器空间,用来存储线程的状态和数据。寄存器文件是一种高速缓存,它可以提供快速的数据访问和操作。一个SM中有一个寄存器文件,它被所有的warp scheduler和CUDA core共享。
- Shared Memory:共享内存,它负责为每个线程块提供一定数量的内存空间,用来存储线程块内部的数据和变量。共享内存是一种低延迟的缓存,它可以实现同一个线程块中不同线程之间的数据通信和同步。一个SM中有一个共享内存,它被所有的warp scheduler和CUDA core共享。
GPU的计算单元通常包括以下几个部分:
- CUDA Core:CUDA核心,它负责执行warp scheduler分配给它的指令和数据。CUDA核心是GPU最基本的处理单元,它可以执行各种算术、逻辑、移位等操作。一个SM中可以有多个CUDA core,每个CUDA core可以同时执行一个warp中的一个线程。
- Special Function Unit:特殊功能单元,它负责执行一些特殊的数学运算,比如平方根、三角函数、指数函数等。特殊功能单元是一种辅助的处理单元,它可以加速一些复杂的运算。一个SM中可以有多个特殊功能单元,每个特殊功能单元可以同时执行一个warp中的一个线程。
- Tensor Core:张量核心,它负责执行一些高效的矩阵运算,比如矩阵乘法、矩阵加法等。张量核心是一种专门针对深度学习应用设计的处理单元,它可以大幅提升神经网络训练和推理的性能。一个SM中可以有多个张量核心,每个张量核心可以同时执行一个warp中的四个线程。
GPU控制单元和计算单元之间通过以下几种方式进行结合:
- Warp Scheduling:线程束调度,即控制单元中的warp scheduler根据不同的策略选择合适的warp,并将其发送给计算单元中空闲的CUDA core或特殊功能单元或张量核心执行。(不同架构的GPU可能采用不同类型和数量的warp scheduler,比如Fermi架构使用两种类型(greedy和loose round-robin)共四个warp scheduler;Kepler架构使用两种类型(greedy dual-issue和loose round-robin)共四个warp scheduler;Maxwell架构使用一种类型(greedy dual-issue)共四个warp scheduler;Pascal架构使用一种类型(greedy dual-issue)共两个warp scheduler;Volta架构使用一种类型(greedy dual-issue)共四个warp scheduler。)
- Memory Access:内存访问,即控制单元和计算单元之间通过寄存器文件和共享内存进行数据的读写和交换。寄存器文件和共享内存是GPU中最快的内存层次,它们可以为计算单元提供高速的数据源和目的地。寄存器文件和共享内存的大小和带宽会影响GPU的性能和资源利用率。(不同架构的GPU可能有不同的寄存器文件和共享内存配置,比如Fermi架构每个SM有32KB的寄存器文件和64KB的共享内存;Kepler架构每个SM有64KB的寄存器文件和48KB的共享内存;Maxwell架构每个SM有64KB的寄存器文件和96KB的共享内存;Pascal架构每个SM有64KB的寄存器文件和96KB的共享内存;Volta架构每个SM有256KB的寄存器文件和96KB的共享内存。)
- Latency Hiding:延迟隐藏,即控制单元和计算单元之间通过上下文切换来避免或减少由于内存访问或分支发散等原因造成的延迟。当一个warp在等待内存访问或分支执行时,控制单元会切换到另一个warp,让计算单元继续执行其他指令。这样可以提高GPU的吞吐量和效率,但也需要更多的资源来保存每个warp的状态。(不同架构的GPU可能有不同的延迟隐藏能力,比如Fermi架构每个SM最多可以同时调度48个warp;Kepler架构每个SM最多可以同时调度64个warp;Maxwell架构每个SM最多可以同时调度64个warp;Pascal架构每个SM最多可以同时调度64个warp;Volta架构每个SM最多可以同时调度64个warp。)
线程束(warp)是在软件和硬件端被执行的最基本单元,因为它符合GPU采用的SIMT(单指令多线程)模型,即一个warp中的32个线程以不同数据资源执行相同的指令。这样可以简化指令流水线和控制逻辑,提高指令并行度和执行效率。另外,线程束也是GPU中最小的上下文切换单元,即当一个warp遇到延迟时,控制单元会切换到另一个warp,而不是单独切换某些线程。这样可以减少上下文切换的开销,提高资源利用率和吞吐量。
1.内存模型
- CUDA中的内存模型分为以下几个层次:
- 每个线程处理器(SP)都用自己的registers(寄存器)
- 每个SP都有自己的local memory(局部内存),register和local memory只能被线程自己访问
- 每个多核处理器(SM)内都有自己的shared memory(共享内存),shared memory 可以被线 程块内所有线程访问
- 一个GPU的所有SM共有一块global memory(全局内存),不同线程块的线程都可使用
2.软件
- CUDA中的内存模型分为以下几个层次:
- 线程处理器(SP)对应线程(thread)
- 多核处理器(SM)对应线程块(thread block)
- 设备端(device)对应线程块组合体(grid)
- 一个kernel其实由一个grid来执行
- 一个kernel一次只能在一个GPU上执行
3.哪些程序适合使用GPU?
- 访问内存次数少
- 控制简单,没有复杂的分支预测和数据转发机制
- 计算简单
- 并行度高(每一行执行的控制指令是同一个)
矩阵乘法,图像处理,深度学习神经网路,排序
4.每个程序分为哪3个part?
- 从主机端申请显存、内存,拷贝到设备端
- 在设备端完成计算等操作
- 将设备端的结果返回到主机端,释放掉申请的主机端和设备端的内存、显存
5.BLOCK_SiZE的选取
- BLOCK_SIZE越大,每个线程块中线程间共享数据的速度越快,计算效率越高。但受限于硬件的共享内存大小(每个线程块使用共享内存大小不能超过硬件限制,一般为48KB。),BLOCK_SIZE不宜设置过大,对于float类型,假设限制为48KB,则 BLOCK_SIZE最大可取为48*1024/4/32 = 384。
- BLOCK_SIZE越小,可以启动更多的线程块,充分利用GPU处理器资源。但每个线程块效率下降。
- BLOCK_SIZE的设定需要与线程束启动配置匹配,一般设置为2的幂,能够被网格大小整除。
- 对于矩阵乘法,BLOCK_SIZE可以设置为16,32,64等,最好是线程束大小(warp size)的整数倍,一般32效果较好。
- 最佳BLOCK_SIZE取决于硬件配置,需要测试不同值的实际效果找出最优点。
- 除BLOCK_SIZE外,还可以适当增加网格大小来提高并行程度,并用Occupancy计算器优化。
具体:
1. 计算功耗比(Compute-to-Global Memory Access Ratio)
- 矩阵乘法中,计算量与全局内存访问量之比越高, BLOCK_SIZE取越大越好。
- 计算量与内存访问量之比约等于 2*K / (M+N),其中K为中间维度。
- 一般当比值大于10时,可以取较大的BLOCK_SIZE,例如32或64。
2. 硬件配置限制
- 每个线程块使用共享内存大小不能超过硬件限制,一般为48KB。
- 对于float类型,假设限制为48KB,则 BLOCK_SIZE最大可取为48*1024/4/32 = 384。
3. 线程束配置要求
- BLOCK_SIZE最好是线程束大小(warp size)的整数倍,通常为32。
- 这样可以避免线程束内部发生分裂,提高执行效率。
4. 调优建议
- - 从32开始,测试不同的BLOCK_SIZE,评估性能,找到一个最优点。
- - 尝试将BLOCK_SIZE设为16、32、64等指数级增长的值。
- - 也可以细粒度测试BLOCK_SIZE周围的数值,如28、36等。
6.Serial和CUDA stream
Serial和CUDA stream都是CUDA程序中管理运算顺序的机制。
CUDA stream是一个先入先出的命令队列,里面的命令会按照插入的顺序串行执行。每个CUDA stream都是独立的,不同stream中的命令可以并行执行。如果不特别指定stream,CUDA函数会在默认的stream 0中执行。
Serial则表示串行执行,没有并行能力。把kernel launch放在Serial中,相当于把kernel插入到默认的stream 0进行串行执行。
总结一下:
- CUDA stream可以并行执行,Serial只能串行。
- Serial实际上就是默认的stream 0。
- 不指定stream的CUDA调用默认在stream 0(即Serial)中串行执行。
- 指定不同stream可以并行执行kernel。
- Serial对于不需要并行的程序可以简化代码,不需要管理stream。
- Stream对于追求并行的程序很关键,可以明确定义执行流水线。
所以Serial和CUDA stream都是控制执行顺序的机制,Serial是默认的串行方式,CUDA stream则引入了并行执行的能力。
7.CUDA stream显式流
CUDA stream中的指令(比如kernel launch)从CPU端是顺序发出的,但是GPU端的执行是异步并行的。也就是说,在CPU端我们会顺序地对不同的stream发出指令,但这些指令进入GPU后,会由GPU的调度器并行调度不同stream中的指令执行。
举个例子:
- CPU端按顺序对stream 1发出kernel 1
- CPU端接着按顺序对stream 2发出kernel 2
- GPU端接收到指令后,会并行地执行kernel 1和kernel 2,而不是严格按顺序串行。
所以CUDA stream可以看作是一条从CPU到GPU的“管道”,管道里的指令是顺序的,但是不同管道之间可以并行执行。
明确几点:
1. 显式流(CUDA Stream)中指令是按顺序发出的。
2. 不同流之间的指令可以并行执行。
3. 单个流内的指令是串行执行的。
4. 指令的执行是异步的(非阻塞的)。
具体来说:
- CPU端对不同流的指令发出是串行的。比如先发出stream 1的指令,然后发出stream 2的指令。
- GPU端可以并行执行不同流中的指令。比如stream 1和stream 2的指令可以同时执行。
- 单个流内的指令是串行的,按顺序执行。比如stream 1中的指令a会先于指令b串行执行。
- 指令的执行是非阻塞的,CPU发出指令后不会等待GPU执行完成才发出下个指令。
所以综合来说,显式流中指令发出是串行的,指令执行可以并行(针对不同流),但单流内还是串行。异步非阻塞机制可以提高执行效率。
异步执行的主要原因:
1. CUDA Stream的机制是异步非阻塞的。
CPU在向显式流提交完任务后,不会等待GPU执行完成就可以提交下一个任务。所以从CPU的角度来看,任务提交是异步的。
2. GPU有自己的调度机制。
GPU收到来自不同流的任务后,会根据自身的调度逻辑并行地执行这些任务。所以从GPU的角度来看,不同流的任务也是异步执行的。
3. 任务执行互不依赖。
不同流中的任务都是相对独立的,不存在执行顺序依赖关系,所以GPU可以采用异步调度,提高并行程度。
4. 等待同步的话会降低效率。
如果非要等待同步,会使得CPU等待GPU执行完再提交新任务,这会大大降低执行效率。
强总结:
1. 单个流内的指令是串行执行的
这个语句的意思是:对于一个CUDA stream,这个stream内部所有发出的指令/任务都会严格按顺序串行执行。
例如在stream1内,任务A会先执行,然后任务B才能执行。stream1内部是串行的。
2. 不同显式流内的GPU任务执行是异步的
这个语句的意思是:对于多个不同的CUDA stream,他们之间可以并行异步执行。
例如stream1中的任务A可以与stream2中的任务B同时异步执行,不需要串行等待。
综上所述:
- 每个stream内部是串行的,保证顺序。
- 不同stream之间可以异步并行。
所以这两句语句在不同的层面描述了CUDA stream的执行模型,并没有冲突。
串行保证每个stream内的顺序性。
异步并行提高了不同stream之间的并发性。
这种组合就是CUDA stream的执行机制,既保证了顺序,又实现了并行。
8.PCIe总线
1. PCIe总线上的操作是顺序的。
不同流中的H2D(Host to Device)复制和D2H(Device to Host)复制不能重叠,只能串行执行。
2. 但是,kernel的执行可以和H2D重叠。
也就是说,当一个流中正在进行H2D复制时,另一个流中的kernel可以启动运行。
3. 为什么kernel可以和H2D重叠?
原因在于kernel是在GPU上运行的运算任务,和PCIe上的数据传输相独立。PCIe传输和GPU执行是异步的。
4. 所以CPU主机可以同时进行“传数据”和“执行计算”两个动作,相互重叠,提高效率。
5. 但D2H复制需要使用PCIe总线,不能和H2D重叠,只能串行等待。
综上,不同CUDA流中的“执行”类操作(kernel)可以与“传输”类操作(H2D)重叠,但PCIe上的传输类操作间不能重叠。
总结:PCIe的顺序性限制了流之间的传输操作不能重叠,但执行操作与传输操作可以重叠,这是CUDA流并行的关键。
9.流和线程的关系
1. 线程(Thread)是CUDA进行并行计算的基本单位,是GPU上的执行资源。一个kernel会启动成百上千个线程并行执行。
2. 流(Stream)是管理GPU上tasks执行顺序的机制。一个流中tasks按顺序执行,不同流间可并行。
3. 一个kernel launch会向一个指定流提交任务。该流会管理该kernel的所有线程的执行。
4. 一个流同时只能执行一个kernel。但一个kernel的所有线程会并行执行。
5. 多个流可并行执行多个kernel。每个kernel又会并行执行多个线程。
6. 所以线程数取决于kernel的配置;流数取决于程序设定的流数量。
7. 线程粒度更细,重点在并行执行单个任务。流粒度更粗,控制任务间先后顺序。
8. 线程关注任务内部的并行;流关注任务间的并行。线程和流共同支持CUDA并行。
综上,线程是严格的执行单元;流是较为宽松的执行管理单元。线程与流两级并行控制配合使用,让CUDA程序实现高效的并行计算。
10.CUDA 中不同类型的内存
- 全局内存
- 常量内存
- 纹理内存和表面内存
- 寄存器
- 局部内存
- 共享内存
- L1 和 L2 缓存
11.SM 及其占有率
一个GPU是由多个SM构成的。SM 的构成:
- 一定数量的寄存器
- 一定数量的共享内存
- 常量内存的缓存
- 纹理和表面内存的缓存
- L1 缓存
- 两个(计算能力6.0)或4个(其他计算能力)线程束调度器,用于在不同线程的上下文之间迅速地切换,以及为准备就绪的线程束发出执行指令
- 执行核心:
- 若干整型数运算的核心(INT32)
- 若干单精度浮点数运算的核心(FP32)
- 若干双精度浮点数运算的核心(FP64)
- 若干单精度浮点数超越函数的特殊函数单元
- 若干混合精度的张量核心(tensor cores)