block调度

由于GPU目前在各行各业的广泛应用,无论是深度学习大数据云计算等都离不开GPU的并行加速,前阵子自学了Cuda-c编程,希望将来的研究工作能够用得上。 
  Cuda系列总共有4篇,这里主要用于记录本人学习过程中的一些问题的思考和总结,及网上汇总摘录的别人的一些总结、看法等,并不适合新手入门。如有错误,欢迎各位指正。  

sm流处理器簇对blocks的调度策略

  在cuda中,GPU中的SM(GTX650M有2个sm处理器)被GPU调度器把线程块逐个分配到SM上,每个SM同时处理这些被分配的线程块block,但是每次每个时刻都只能处理一个warp线程束,由于有时会存在内存读取等操作导致等待,那么SM会转而处理其他的warp来掩盖这个延迟。并且这些warp不一定都是在同一个线程块block内的,另之所以每次同时处理多个block而不是只处理一个block并把该block内的线程个数设置成支持的最大的线程个数,一是可以允许其在支持不同线程个数的不同型号的显卡上都能处理相同的程序;二是由于gpu并行运算一般存在成千上万个线程,并且线程块个数远远大于SM,所以不会存在同一个SM内的线程块几乎同时执行完毕,而最终执行最后一个被调度的线程块而造成等待时间大于之前sm执行的时间所导致的效率低下。

附录1:

  GPU线程以网格(grid)的方式组织,而每个网格中又包含若干个线程块,在G80/GT200系列中,每一个线程块最多可包含512个线程,Fermi架构中每个线程块支持高达1536个线程。同一线程块中的众多线程拥有相同的指令地址,不仅能够并行执行,而且能够通过共享存储器(Shared memory)和栅栏(barrier)实现块内通信。这样,同一网格内的不同块之间存在不需要通信的粗粒度并行,而一个块内的线程之间又形成了允许通信的细粒度并行。这些就是CUDA的关键特性:线程按照粗粒度的线程块和细粒度的线程两个层次进行组织、在细粒度并行的层次通过共享存储器和栅栏同步实现通信,这就是CUDA的双层线程模型。 
  在执行时,GPU的任务分配单元(global block scheduler)将网格分配到GPU芯片上。启动CUDA 内核时,需要将网格信息从CPU传输到GPU。任务分配单元根据这些信息将块分配到SM上。任务分配单元使用的是轮询策略:轮询查看SM是否还有足够的资源来执行新的块,如果有则给SM分配一个新的块,如果没有则查看下一个SM。决定能否分配的因素有:每个块使用的共享存储器数量,每个块使用的寄存器数量,以及其它的一些限制条件。任务分配单元在SM的任务分配中保持平衡,但是程序员可以通过更改块内线程数,每个线程使用的寄存器数和共享存储器数来隐式的控制,从而保证SM之间的任务均衡。任务以这种方式划分能够使程序获得了可扩展性:由于每个子问题都能在任意一个SM上运行,CUDA程序在核心数量不同的处理器上都能正常运行,这样就隐藏了硬件差异。 
  对于程序员来说,他们需要将任务划分为互不相干的粗粒度子问题(最好是易并行计算),再将每个子问题划分为能够使用线程处理的问题。同一线程块中的线程开始于相同的指令地址,理论上能够以不同的分支执行。但实际上,在块内的分支因为SM构架的原因被大大限制了。内核函数实质上是以块为单位执行的。同一线程块中的线程需要SM中的共享存储器共享数据,因此它们必须在同一个SM中发射。线程块中的每一个线程被发射到一个SP上。任务分配单元可以为每个SM分配最多8个块。而SM中的线程调度单元又将分配到的块进行细分,将其中的线程组织成更小的结构,称为线程束(warp)。在CUDA中,warp对程序员来说是透明的,它的大小可能会随着硬件的发展发生变化,在当前版本的CUDA中,每个warp是由32个线程组成的。SM中一条指令的延迟最小为4个指令周期。8个SP采用了发射一次指令,执行4次的流水线结构。所以由32个线程组成的Warp是CUDA程序执行的最小单位,并且同一个warp是严格串行的,因此在warp内是无须同步的。在一个SM中可能同时有来自不同块的warp。当一个块中的warp在进行访存或者同步等高延迟操作时,另一个块可以占用SM中的计算资源。这样,在SM内就实现了简单的乱序执行。不同块之间的执行没有顺序,完全并行。无论是在一次只能处理一个线程块的GPU上,还是在一次能处理数十乃至上百个线程块的GPU上,这一模型都能很好的适用。

附录2:

硬件基本架构 
  实际上在 nVidia 的 GPU 里,最基本的处理单元是所谓的 SP(Streaming Processor),而一颗 nVidia 的 GPU 里,会有非常多的 SP 可以同时做计算;而数个 SP 会在附加一些其他单元,一起组成一个 SM(Streaming Multiprocessor)。几个 SM 则会在组成所谓的 TPC(Texture Processing Clusters)。 
  在 G80/G92 的架构下,总共会有 128 个 SP,以 8 个 SP 为一组,组成 16 个 SM,再以两个 SM 为一个 TPC,共分成 8 个 TPC 来运作。而在新一代的 GT200 里,SP 则是增加到 240 个,还是以 8 个 SP 组成一个 SM,但是改成以 3 个 SM 组成一个 TPC,共 10 组 TPC。下面则是提供了两种不同表示方式的示意图。(可参考《NVIDIA G92终极状态!!》、《NVIDIA D10U绘图核心》)

对应到 CUDA 
  而在 CUDA 中,应该是没有 TPC 的那一层架构,而是只要根据 GPU 的 SM、SP 的数量和资源来调整就可以了。 
  如果把 CUDA 的 Grid - Block - Thread 架构对应到实际的硬件上的话,会类似对应成 GPU - Streaming Multiprocessor - Streaming Processor;一整个 Grid 会直接丢给 GPU 来执行,而 Block 大致就是对应到 SM,thread 则大致对应到 SP。当然,这个讲法并不是很精确,只是一个简单的比喻而已。

SM 中的 Warp 和 Block 
  CUDA 的 device 实际在执行的时候,会以 Block 为单位,把一个个的 block 分配给 SM 进行运算;而 block 中的 thread,又会以「warp」为单位,把 thread 来做分组计算。目前 CUDA 的 warp 大小都是 32,也就是 32 个 thread 会被群组成一个 warp 来一起执行;同一个 warp 里的 thread,会以不同的数据,执行同样的指令。此外,在 Compute Capability 1.2 的硬件中,还加入了 warp vote 的功能,可以快速的进行 warp 内的简单统计。 
  基本上 warp 分组的动作是由 SM 自动进行的,会以连续的方式来做分组。比如说如果有一个 block 里有 128 个 thread 的话,就会被分成四组 warp,第 0-31 个 thread 会是 warp 1、32-63 是 warp 2、64-95 是 warp 3、96-127 是 warp 4。 
  而如果 block 里面的 thread 数量不是 32 的倍数,那他会把剩下的 thread 独立成一个 warp;比如说 thread 数目是 66 的话,就会有三个 warp:0-31、32-63、64-65。由于最后一个 warp 里只剩下两个 thread,所以其实在计算时,就相当于浪费了 30 个 thread 的计算能力;这点是在设定 block 中 thread 数量一定要注意的事! 
  一个 SM 一次只会执行一个 block 里的一个 warp,但是 SM 不见得会一次就把这个 warp 的所有指令都执行完;当遇到正在执行的 warp 需要等待的时候(例如存取 global memory 就会要等好一段时间),就切换到别的 warp 来继续做运算,藉此避免为了等待而浪费时间。所以理论上效率最好的状况,就是在 SM 中有够多的 warp 可以切换,让在执行的时候,不会有「所有 warp 都要等待的情形发生;因为当所有的 warp 都要等待时,就会变成 SM 无事可做的状况了~ 
  下图就是一个 warp 排程的例子。一开始是先执行 thread block 1 的 warp1,而当他执行到第六行指令的时候,因为需要等待,所以就会先切到 thread block 的 warp2 来执行;一直等到存取结束,且刚好有一个 warp 结束时,才继续执行 TB1 warp1 的第七行指令。

这里写图片描述

  实际上,warp 也是 CUDA 中,每一个 SM 执行的最小单位;如果 GPU 有 16 组 SM 的话,也就代表他真正在执行的 thread 数目会是 32x16 个。不过由于 CUDA 是要透过 warp 的切换来隐藏 thread 的延迟、等待,来达到大量平行化的目的,所以会用所谓的 active thread 这个名词来代表一个 SM 里同时可以处理的 thread 数目。 
  而在 block 的方面,一个 SM 可以同时处理多个 thread block,当其中有 block 的所有 thread 都处理完后,他就会再去找其他还没处理的 block 来处理。假设有 16 个 SM、64 个 block、每个 SM 可以同时处理三个 block 的话,那一开始执行时,device 就会同时处理 48 个 block;而剩下的 16 个 block 则会等 SM 有处理完 block 后,再进到 SM 中处理,直到所有 block 都处理结束。 
  为一个多处理器指定了一个或多个要执行的线程块时,它会将其分成warp块,并由SIMT单元进行调度。将块分割为warp的方法总是相同的,每个warp都包含连续的线程,递增线程索引,第一个warp中包含全局线程过索引0-31。每发出一条指令时,SIMT单元都会选择一个已准备好执行的warp块,并将指令发送到该warp块的活动线程。Warp块每次执行一条通用指令,因此在warp块的全部32个线程执行同一条路径时,可达到最高效率。如果一个warp块的线程通过独立于数据的条件分支而分散,warp块将连续执行所使用的各分支路径,而禁用未在此路径上的线程,完成所有路径时,线程重新汇聚到同一执行路径下,其执行时间为各时间总和。分支仅在warp块内出现,不同的warp块总是独立执行的–无论它们执行的是通用的代码路径还是彼此无关的代码路径。

建议的数值? 
  在 Compute Capability 1.0/1.1 中,每个 SM 最多可以同时管理 768 个 thread(768 active threads)或 8 个 block(8 active blocks);而每一个 warp 的大小,则是 32 个 thread,也就是一个 SM 最多可以有 768 / 32 = 24 个 warp(24 active warps)。到了 Compute Capability 1.2 的话,则是 active warp 则是变为 32,所以 active thread 也增加到 1024。 
  在这里,先以 Compute Capability 1.0/1.1 的数字来做计算。根据上面的数据,如果一个 block 里有 128 个 thread 的话,那一个 SM 可以容纳 6 个 block;如果一个 block 有 256 个 thread 的话,那 SM 就只能容纳 3 个 block。不过如果一个 block 只有 64 个 thread 的话,SM 可以容纳的 block 不会是 12 个,而是他本身的数量限制的 8 个。 
  因此在 Compute Capability 1.0/1.1 的硬件上,决定 block 大小的时候,最好让里面的 thread 数目是 warp 数量(32)的倍数(可以的话,是 64 的倍数会更好);而在一个 SM 里,最好也要同时存在复数个 block。如果再希望能满足最多 24 个 warp 的情形下,block 里的 thread 数目似乎会是 96(一个 SM 中有 8 个 block)、128(一个 SM 中有 6 个 block)、192(一个 SM 中有 4 个 block)、256(一个 SM 中有 3 个 block) 这些数字了~ 
  Compute Capability 1.0/1.1 的硬件上,每个grid最多可以允许65535×65535个block。每个block最多可以允许512个thread,但是第三维上的最大值为64。而官方的建议则是一个 block 里至少要有 64 个 thread,192 或 256 个也是通常比较合适的数字(请参考 Programming Guide)。 
  但是是否这些数字就是最合适的呢?其实也不尽然。因为实际上,一个 SM 可以允许的 block 数量,还要另外考虑到他所用到 SM 的资源:shared memory、registers 等。在 G80 中,每个 SM 有 16KB 的 shared memory 和 8192 个 register。而在同一个 SM 里的 block 和 thread,则要共享这些资源;如果资源不够多个 block 使用的话,那 CUDA 就会减少 Block 的量,来让资源够用。在这种情形下,也会因此让 SM 的 thread 数量变少,而不到最多的 768 个。 
  比如说如果一个 thread 要用到 16 个 register 的话(在 kernel 中宣告的变量),那一个 SM 的 8192 个 register 实际上只能让 512 个 thread 来使用;而如果一个 thread 要用 32 个 register,那一个 SM 就只能有 256 个 thread 了~而 shared memory 由于是 thread block 共享的,因此变成是要看一个 block 要用多少的 shread memory、一个 SM 的 16KB 能分给多少个 block 了。 
  所以虽然说当一个 SM 里的 thread 越多时,越能隐藏 latency,但是也会让每个 thread 能使用的资源更少。因此,这点也就是在优化时要做取舍的了。


  对线程块的设置:

     

  1. 尽量满足每个sm上面的最大线程数。

  2. 每个sm支持的线程块有上限,所以每个线程块的线程数不能太少,并且最好为32或16的倍数。

  3. 当一个sm中的Block的所有线程形成阻塞(同步或者等待)时,相应的SM会闲置导致效率下滑。通常让block的数目是sm的2倍以上,方便sm对其进行调度,使其在时间轴上重叠来提高利用率。

  4. 对每个线程块进行设置时,如果使用了寄存器,需要明确每个线程最大支持的寄存器个数,防止因为每个线程使用过多寄存器而导致每个时刻SM所能处理的线程数急剧下降。

  5. 当一维的线程块不足以满足计算需求或是在处理图像等二维数据时,可以使用二维线程块。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
进程调度算法模拟。通过对进程调度算法的模拟,进一步理解进程的基本概念,加深对进程运行状态和进程调度过程、调度算法的理解。 (1) 用C、C++、Java语言编程实现对5个进程采用动态优先权调度算法进行调度的过程。数据如下: 5个进程的到达时刻和服务时间见下表,忽略I/O以及其它开销时间,使用动态优先权算法进行调度,优先权初始值为100,请输出各个进程的完成时刻、周转时间、带权周转时间。 进程 到达时刻 服务时间 A 0 3 B 2 6 C 4 4 D 6 5 E 8 2 (2)每个用来标识进程的进程控制块PCB可用结构来描述,包括以下字段(用不到的字段可以不定义)。  进程标识数ID。  进程优先数PRIORITY,并规定优先数越大的进程,其优先权越高。  进程已占用CPU时间CPUTIME。  进程还需占用的CPU时间ALLTIME。当进程运行完毕时,ALLTIME变为0。  进程的阻塞时间STARTBLOCK,表示当进程再运行STARTBLOCK个时间片后,进程将进入阻塞状态。  进程被阻塞的时间BLOCKTIME,表示已阻塞的进程再等待BLOCKTIME个时间片后,将转换成就绪状态。  进程状态STATE。  队列指针NEXT,用来将PCB排成队列。 (3)优先数改变的原则:  进程在就绪队列中呆一个时间片,优先数增加1。  进程每运行一个时间片,优先数减3。 (4)为了清楚地观察每个进程的调度过程,程序应将每个时间片内的进程的情况显示出来,包括正在运行的进程,处于就绪队列中的进程和处于阻塞队列中的进程。 (5)分析程序运行的结果,谈一下自己的认识。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值