CUDA并行归约

最近在学习GPU并行计算,对针对全局内存的并行归约和共享内存的并行归约的理解做一个小总结。以下代码出自<<CUDA C编程权威指南>>。

归约与线程分支分化

图1 分化的归约(相邻配对)和未分化的归约(交错配对)

线程束分化

  1. 一个线程束(32个线程)在同一周期中必须执行相同的指令;
  2. 如果一个线程束分化(如tid为偶数时执行一个分支,tid为奇数时执行另一个分支),那么则整个线程束将会连续执行每一个分支路径,在执行时不满足这个分支的线程则会被禁用(这就浪费了线程)。

归约中的线程束分化

图2 相邻配对中的线程束分化
  1. 如上图所示,在进行相邻配对中,执行一步后,则只有tid=0,2,4,6...等偶数线程保存了下一步的值,并需要执行下一步归约,下一步则只有tid=0,4,8等间隔更大的线程执行归约。这就造成了每进行一次归约,线程束分化中有效的线程数越少
  2. 另外,这样也会影响到全绝内存的加载(见第4章:全局内存),由于内存加载时是32个相邻内存单元(对应于32个线程)同时加载(算一个内存事务)。如果不相邻则需要多个内存加载事务,上面的线程束分化会造成内存加载的多个事务。

线程束分化的归约示例

相邻配对的归约

造成线程束分化的相邻配对归约

// Neighbored Pair Implementation with divergence
__global__ void reduceNeighbored(int* g_idata, int* g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int* idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if (idx >= n) return;

    // in-place reduction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2)
    {
        if ((tid % (2 * stride)) == 0)//造成了线程束的分化
        {
            idata[tid] += idata[tid + stride];
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

上面的 if ((tid % (2 * stride)) == 0)语句会造成线程束的分化,stride越大,分化越严重。

消除线程束分化的相邻配对的规约

__global__ void reduceNeighboredLess(int* g_idata, int* g_odata,
    unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int* idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if (idx >= n) return;

    // in-place reduction in global memory
    for (int stride = 1; stride < blockDim.x; stride *= 2)
    {
        // convert tid into local array index
        int index = 2 * stride * tid;

        if (index < blockDim.x)
        {
            idata[index] += idata[index + stride];//而这里stride为定值,index为非连续值,内存加载会出现多次加载,效率低
        }

        // synchronize within threadblock
        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

注意如上面代码中中文注释部分,由于不需要进行if判断,故未造成线程束分化。通过测试每个线程束执行的指令束平均值可知,未线程束分化的函数拥有更少的平均指令数。


图3 每个线程束上执行的平均指令数

但是相邻配对本身仍然存在缺陷由于线程id不连续,会需要多个内存加载事务,因而会使得速度变慢。
这里比如stride=1时,需要加载的内存的线程id为0,2,4,6,8,10... 等偶数,而内存加载则是加载0-31号线程的所有内存,造成了内存加载的浪费,越往后归约浪费的越多。交错配对的归约则解决了这个问题。代码如下:

交错配对的归约

__global__ void reduceInterleaved(int* g_idata, int* g_odata, unsigned int n)
{
    // set thread ID
    unsigned int tid = threadIdx.x;
    unsigned int idx = blockIdx.x * blockDim.x + threadIdx.x;

    // convert global data pointer to the local pointer of this block
    int* idata = g_idata + blockIdx.x * blockDim.x;

    // boundary check
    if (idx >= n) return;

    // in-place reduction in global memory
    for (int stride = blockDim.x / 2; stride > 0; stride >>= 1)
    {
        if (tid < stride)
        {
            idata[tid] += idata[tid + stride];
                        //这里stride为定值,
            //tid连续,所以32个内存事务是连续的,内存加载连续,所以即使吞吐量比上一个小,其效率          高,无冲突,因此快
        }

        __syncthreads();
    }

    // write result for this block to global mem
    if (tid == 0) g_odata[blockIdx.x] = idata[0];
}

而交错配对的归约则不然,内存加载事务是连续的,提高了指令的效率。从下图4中可以看出交错配对(reduceInterleaved)的内存吞吐量相较于前两个函数是降低的,但是其内存加载的都是需要用的有效内存,因而效率高,从执行时间来看其执行速度也更快。

图4 内存吞吐量

图5 上述三种方法的运行时间对比

循环展开

由以上知道,我们通过改善:

  1. 减少线程束分化;
  2. 减少内存事务的加载(增加内存的有效加载)
    获得了运行速度的增加。但是还有没有更近一步的提高速度的手段呢?有,那就是减少指令消耗(展开循环)增加更多的独立指令调度(独立的内存加载事务等)来提高性能。这样能提高速度的原因是能够通过以上此举使得更多的线程达到可以符合执行条件的状态(指令就位或者内存数据就位),从而帮助隐藏指令延迟或者内存延迟。

在优化开始前,需要了解下延迟隐藏。

延迟隐藏

延迟隐藏包括算术指令隐藏和内存指令隐藏

延迟的定义

  1. 算术指令延迟是一个算术操作从开始到它产生输出之间的时间;
  2. 内存指令延迟是送出的加载或存储操作和数据到达目的地之间的时间。
    所需要的线程数量有个计算公式:
    所需线程数量=延迟*吞吐量

算术运算所需的并行

如对于float算术运算(a+bc),Fermi gpu模型需要20个指令延迟,而一条指令是对于一个线程束而言的,即32个操作,因此在每个SM中需要的线程束数量为:2032/32=20个线程束。
对于Turing架构来说,每个sm上有64个cuda核,则需要20个线程束。
因此提高指令级并行,有如下两个方法:

  1. 指令级别并行:一个线程中有很多独立的指令;
  2. 线程级并行:很多并发的符合条件的线程。
    循环展开减少了指令的执行,我的理解是循环展开一次指令操作了更多的数据块,从而减少了总的指令执行次数,从而加快了速度。

内存所需要的并行

内存所需的并行可以表示为在每个周期内隐藏内存延迟所需的字节数。
以答主的RTX 2070显卡为例,其吞吐量448GB/s,指令延迟800(假设),内存频率:7001MHz。
则把带宽转换为字节每周期为:
448GB/s/(7001MHz/1024)=66字节/周期
那么,用字节/周期(吞吐量)乘以延迟则得到需要的内存并行,即800*66/1024=51KB。
假如一个线程操作的数据为4字节大小,那么共需要的线程数量为:
51KB/4字节/线程=13105线程=13105/32=410个线程束,对于RTX2070来说,共有36个SM,则每个SM上分配的线程束为410/36=11.4线程束/SM。
而对于循环展开来说,展开的循环进行了多个独立的内存加载操作,我的理解是此时能够尽可能的利用带宽,同时传输更多的数据。书上(p152页)说展开并不影响内存操作的数量,但是会增大并发执行的数量,从而加速了运行。
对于以上计算,可以看出对于2070显卡来说,算术延迟每个sm要20个线程束,内存延迟要11.4个线程束,所以在以上假设情况下取其最大值,即20个线程束,即每个块的大小需要大于640个线程才能隐藏算术延迟(20个指令延迟)。

基于全局内存的展开

//todo

基于共享内存的展开

//todo
--2020.11.29

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,509评论 6 504
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,806评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,875评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,441评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,488评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,365评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,190评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,062评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,500评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,706评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,834评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,559评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,167评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,779评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,912评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,958评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,779评论 2 354

推荐阅读更多精彩内容