[新手]如何选择最优的block数

我不知道标题是否很准确,我尽量把问题描述清楚。

我有一个GTX 660Ti的卡,cuda core有1344个。

现在我要跑个算法,针对1600组数据做处理。
我的设计是在每1个thread里处理1组数据。
我测试了使用不同的blockNum,gpu计算的耗时并不一致,大致结果如下:
使用2个block时,耗时大约0.5s,
使用20-60个block时,耗时大约0.4s,
当使用几百个block时,耗时又增加了,大约会到0.6甚至更多。

我的问题,我该如何根据显卡的cuda core数目,来预测并选择最优的block number,
使得gpu的负载能到最高呢?

能否达到最佳的效率并不光看BLOCK的数量,楼主可以用visual profile来跑一次你的程序然后分析一下,比如占有率,分支,内存吞吐的情况等,都可以在visual profile里面分析出来。然后根据分析结果再考虑如何优化你的程序!

LZ您好,block选多少个,每个block内部有多少thread,每个thread跑多少任务等等,都是需要综合考虑的,而不是简单地说可以如何判断出来。

1:比如说cuda里面的线程,您是可以认为有一个类似于CPU的全功能的处理器来处理这个线程的任务;但同时这个处理器又是轻量级的,最适合跑很小的任务(kernel),也就是最适合细粒度的任务。具体到您的问题,您有1600组数据,那么每组数据是大还是小?如果比较小,那么可以一个线程搞定一组;如果比较大,可以多个线程完成一组,甚至一个block完成一组。

2:假定按照您说的,一个线程处理一组数据,如果您只选择2个block,那么您的660Ti上应该有多个SMX处于不干活的状态;选择几十个block,相对合理一点,(您的计时也反映了这一点)。但是考虑到您一共只有1600个线程,如果分成20个block,那么每个block只有80个线程,这样的每block线程规模,偏小了。一般按照尽量增加线程的安排思路,用一个SM/SMX上最大resident 线程数量除以最大resident block数量,可以得到单一block最小线程数量。对于您的GTX 660Ti显卡,查阅programming guide的Appendix F的表格可以得到为:2048/16=128。至于几百个block,那么每个block内部只能有几个线程,这会大大地降低GPU的使用效率,假定您是200个block,那么每个block内部只有8个线程,而一个SMX上最大只能resident 16个block(SM 3.X),那么您每个SMX上实际只加载了16*8=128个线程,还没有SP数量多。

3:综合2的分析,您的总线程数量太少,怎么分都不是很合适。如果您的总体计算规模就比较小,那么似乎不建议使用GPU,或者说无法调整优化。如果您的总体计算规模是比较大的,那么我建议您考虑下将一组数据用多个线程完成,也就是说进一步拆分并行,用更多的线程铺开为您服务,或许可以得到较好的效果。

4:其他细节问题,如合并访问情况,访存情况,寄存器占用,shared memory等等,建议您拿到NVVP的分析之后,再做进一步考虑。目前第一步,还请考虑3中所述内容。

以上4点供您参考,祝您春节愉快~

谢谢ice斑竹!
这个算法确实不是细粒度的任务,而是包含有大量的循环,分支判断。
而且,根据处理的每组数据不同,在每个thread里,走的分支可能有很大不同。
请问这样的算法,使用gpu并行计算该怎么优化才能尽可能的提高速度呢?

另外,我想再问2个问题。

  1. 我用cpu跑算法,处理一组数据大概要花费0.00018s,
    而用gpu跑算法,假如只处理一组数据,也就是kernel调用是f<<<1,1>>>>(),大概需要0.0075s
    性能差别达到40倍。请问这个结果合理吗?为什么gpu比cpu速度会慢那么多。
  2. 根据计算看,1600组数据处理,因此不能充分使用gtx660ti的计算能力。
    不过事实上,我这个算法的实际工作情况是:每次任务有1600组数据,连续成百上千的任务输入进来。因此,是不是可以用stream的方法将多个任务overlap,使得gpu的计算能力被充分使用?
    谢谢!

LZ您好,不客气的。

0:关于循环和分支。循环的话,要具体分析,有些算法中的循环没有依赖性或者说没有时间上的因果关系,这种循环一般可以拆掉变成并行的。但也有些循环无法拆分的。分支判断的话,一般的说法是,如果分支判断的条件是已知的和固定的,那么如果让分支对warp对齐(也就是一个warp都走同一个分支),那么也是没有影响的。不过,也有无法这样实现的情况,比如说要根据无法先验的数据进行分支判断。

所以,循环的话,建议LZ分析一下,看看能不能拆;分支的话,如果分支的路径不是非常多,那么可以考虑先不管,实现出来看看情况再说。

1:假定这里面的计时各方面没有其他问题,我要说,这种情况下,GPU远远比CPU慢是正常的,也是必然的。
因为:
a)GPU启动一个kernel有很多开销的,而CPU跑一个函数开销要小得多。
b)GPU跑一个线程,绝对不是CPU的对手。GPU的每个核都很弱,靠的是大量的核心并行,以及大量线程互相掩盖延迟。你一个线程跑,他既跑在很弱的核心上,同时也没有其他线程为他掩盖延迟,以及还有大量的kernel启动开销,焉能不慢?而同时CPU则不同,CPU是靠少数几个高主频而强大的核心工作的,跑一个线程,得心应手。
c)其实b)的内容换个说法就是:“GPU是为吞吐量优化的,CPU是为延迟优化的”(此句来自某未知来源)。CPU要求尽快地得到结果,但是只能做较少的吞吐量;GPU从开始拿到命令到最后输出结果,这个时间延迟较长,但是有较大的吞吐量。

让我来打个比方,假定我需要订一批货物,现在有A和B两个工厂揽活。其中A工厂一周即可拿出产品,但是每天最大产量是4件货物;B工厂需要一个月才能拿出产品,但是每天最大产量是100件货物。考虑我只要少数的几件货物,A工厂一周后即可开始交付,而B工厂最快也要一个月以后才能交付;而如果我需要1000件货物,B工厂在一个月零两天的时候就能超过A工厂的供货进度,那么1000件货物平均下来每件货物的加工时间,B工厂必然小于A工厂。——这就是延迟和吞吐量的关系,这个比喻里面,A——CPU,B——GPU。

d)让我们在c)的基础上,回到您的测试,您的测试完全是测试延迟,所以GPU比CPU慢是必然的。

2:如果您每次的任务能进一步拆分细化的话,可能一次运行的内容也可以并行的更好一些;如果不行,您需要考虑,“连续成百上千的任务输入进来”,这些任务有没有前后的因果联系?如果有因果关系,那就只能前面的算完再算后面的;如果没有,这相当于您的总计算规模得以扩大,那么您可以通过使用stream实现kernel 并发来让GPU同时多跑几个kernel,以及,我觉得,您也可以直接将多次的任务合并为一次kernel调用处理,使得您每次调用kernel时,grid都够大,以便充分利用GPU的资源。

当然,需要指出的是,扩大grid规模或者多kernel并行只是挖掘GPU资源的一个方面,正如前面讨论过的那样,您的kernel要能充分并行,要适合GPU的处理方式,才能最终取得较好的结果。否则,虽然GPU都跑满了,但是每个SM都以较低的效率在运行(比如说大量分支导致大量无效结果被丢弃等),这样最终结果一样不会很好。

最后重申一下,您需要理解GPU运作的特点,并将您的算法尽可能改写为适合GPU的实现,以及用NVVP的结果作为进一步优化的参考,这是建议的做法。

您的问题大致回复如上,供您参考。

祝您春节快乐~

谢谢斑竹!

  1. 每个任务是独立的。使用多个stream来达到Overlap任务,和合并多个任务为一个grid
    这2种方法性能差别大吗?

  2. 我的算法里,大量分支的执行是根据输入的数据相关的,难以预测,因此也难以将多个有相同分支路径的数据合并为一个warp。
    根据cuda编程指南硬件章节说明,gpu是simt,即1个指令,多个线程执行。
    gpu碰到这种分支不同的情况,会顺序执行每个thread。那么这里的顺序执行是什么意思呢?
    例如下面的代码:
    if(a)
    {
    many instructions;
    }
    else
    {
    many instructions;
    }
    sm是把这段代码的每个指令依次发射1次,由每个thread的分支路径决定是否执行;
    还是sm依次执行每个thread,根据thread分支发射不同的指令(这种情况同1个指令会被发射多次)。

  3. 举1个简单的kenel函数,如下
    define N 1024
    global void test1(char *buf)
    {
    for (int i = 0; i < N; i++)
    {
    some works on buf[i][i]【i】;(可能是php代码的限制,这里的中括号总被删除,不得不改为中文的中括号)
    }
    }
    global void test2(char *buf)
    {
    some works on buf[0];
    some works on buf[1];
    some works on buf[2];
    。。。。
    some works on buf[N-1];

    }

    在test1函数里有1个循环,每次循环都要判断一下i是否大于N。
    gpu为每个thread执行判断语句选择分支,判断一个warp里所有的thread的分支是否相同,这个开销是否很大?如果很大的话,我想就有必要改为test2的形式。
    [/i][/i]

(1)差别大,差别很大。如果无可避免的要执行多个不同的任务,那么建议1个grid, 然后里面分开(例如:按block或者warp)执行,这种安排好了基本无性能损失。而不要选择多个stream里的多个grid。后者当然也能用,但是硬件能同时执行的小任务(小grid)有限。

(2)“ gpu碰到这种分支不同的情况,会顺序执行每个thread。那么这里的顺序执行是什么意思呢?” — 如同你的例子,warp内假设有2个分支,其中有n个线程走的是分支1, 32-n个线程是走的分支2。那么实际上会串行的执行这2个分支,走分支1的时候,暂停或者假执行那些不走的线程(中的代码序列);走分支2则屏蔽或者假执行另外一部分不走的线程中的指令序列。

你的问题实际上是对手册的描述的中文转述,而这个转述是翻译不当的,影响了你的理解。原文说法应该是"execute serially",也就是“串行的执行”,即这2个warp内的分支不能同时并行执行。直接看原文相当容易理解。

(3)对于你给出的用来说明(2)的例子,我看出的含义和你的上文说法不同。
你这里我“的确看到了判断是否>=N", 但这个for(int i = 0; i < N; i ++)对i<N的判断,其实只是一个简单的循环(控制)的问题。

在这个循环的判断上,test1和test2唯一的区别就是对循环的展开,后者(test2)通过将循环展开为n次独立的运算,去除了对循环条件的判断和跳转。

后者(test2)的确会加快速度,但这个for (int i = 0; i < N; i++)导致的加速不是因为"判断一个warp里所有的thread的分支是否相同",而是因为减少了判断和跳转所需要的指令。

你的表达摧毁了你的愿意。或者直白的说:
要么(1)你给出一个正确可以加速的,但是和你的理论不搭边的例子,是无心的。
要么(2)你用其他的效果正确,但和你的理论无关例子,在试图混淆和诡辩。

不过真诚的说,如果去掉例子3中强加的你的理论,那么你的例子3是一个好例子。
说到开销,是否for(…条件…)导致的判断和跳转是“开销很大的”的,需要看你的循环体,
如果你的循环体很小,那么这个开销就不能忽略。如果循环体较大,那么可以无视这个。

举个例子,循环判断和跳转占用3条指令,而循环体只有1条,那么不展开循环可以会导致额外的75%的开销。同如果循环体编译出100条语句,那么展开不展开就只有3%的开销差异。此时不展开的确可以。

但展开循环与否,和你的“warp内分支的“理论无关,这个还是需要楼主注意的。

感谢莅临CUDAZone China,
热情为您服务的横扫千军祝您春节快乐!

需要特别指出的是,如果循环体非常大,此时展开反而可能会导致额外的性能下降。

举个例子:
你的循环编译为3条(循环控制)+ 8KB的指令,而你需要循环N次。此时如果展开,那么会占用约8N KB的指令,如果N较大,会对指令Cache带来额外的负担。从而可能影响性能。

而此时不展开,多出3条指令对于这个庞大的循环体来说,无所谓,却大大让cache减负,避免了性能影响。

所以楼主在展开循环的时候,要根据7#和本楼的说法,自细斟酌。
简单的说,
楼主可以分开测试展开和不展开,看性能变化。即可轻松确定主意。

非常感谢版主的细致讲解!
因为是新手,对cuda有很多不明的地方,因此在上面的问题里确实包含了几个不同角度的问题,版主火眼金睛!

能够服务你是我们的荣幸!