当前位置:   article > 正文

求topk——CUDA on ARM Platform夏令营 Day04 课后作业_topk cuda

topk cuda

前言:对相关基础知识了解的朋友可跳过第一部分,本文求topk的思想是在向量元素求和思想的基础上得到的,读者理清楚后会更容易理解如何求topk。
有图解哦~ ヾ(✿゚▽゚)ノ

1. 相关基础知识回顾

1.1 grid与block与thread

主要讲解线程层次和相关内置变量的含义,这对后面的坐标计算很重要。
在这里插入图片描述
(上图来自夏令营PPT,可以很清楚地看出线程层次)

  1. Dim表示其所包含的下一层次元素的个数。比如blockDim.[x y z]表示一个block包含多少个thread。
  2. Idx表示其在上一层次中的索引值。比如threadIdx.[x y z]表示当前thread在block中的索引值。注意此索引不是thread在所有thread中的索引,全局索引计算方法如下(下图来自夏令营PPT):
    在这里插入图片描述

1.2 shared memory与register

主要介绍shared memory与register所在位置,这对理解后面的计算任务思想很重要。
在这里插入图片描述
(上图来自夏令营PPT)

  1. 每个thread拥有一个属于自己的register。
  2. 每个block拥有一块属于自己的shared memory,其中的thread都能访问,可以借此协作完成一些任务。

1.3 C语言指针与数组

这里主要是介绍C语言指针与数组,这两者间的转换也会用于后续计算坐标,本人在用到的时候开始有些遗忘,所以在这里记录一下。(部分知识点参考于谭浩强老师C程序设计(第五版))
对于一维数组int a[10]来说,变量名a即为数组首元素的地址。所以int *p=a;即将该地址赋给了指针变量p。
那么此时要想通过指针p访问数组a中其他元素的值怎么办呢?如下图所示(来自谭浩强老师C程序设计(第五版)):在指针变量上加上对应的偏移量,指针变量就能指向对应的数组元素。
在这里插入图片描述

2. 向量元素求和

本部分代码来自夏令营所给例程。
核函数如下:

__global__ void _sum_gpu(int *input, int count, int *output)
{
    __shared__ int sum_per_block[BLOCK_SIZE];

    int temp = 0;
    for (int idx = threadIdx.x + blockDim.x * blockIdx.x;
         idx < count; idx += gridDim.x * blockDim.x
	)
    {
        temp += input[idx];
    }

    sum_per_block[threadIdx.x] = temp;  //the per-thread partial sum is temp!
    __syncthreads();

    //**********shared memory summation stage***********
    for (int length = BLOCK_SIZE / 2; length >= 1; length /= 2)
    {
        int double_kill = -1;
	if (threadIdx.x < length)
	{
	    double_kill = sum_per_block[threadIdx.x] + sum_per_block[threadIdx.x + length];
	}
	__syncthreads();  //why we need two __syncthreads() here, and,
	
	if (threadIdx.x < length)
	{
	    sum_per_block[threadIdx.x] = double_kill;
	}
	__syncthreads();  //....here ?
	
    } //the per-block partial sum is sum_per_block[0]

    if (blockDim.x * blockIdx.x < count) //in case that our users are naughty
    {
        //the final reduction performed by atomicAdd()
        if (threadIdx.x == 0) atomicAdd(output, sum_per_block[0]);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

要说清楚这个过程需要各种图(都是夏令营PPT的图或有一定修改)!

  1. 声明位于shared memory的变量:__shared__ int sum_per_block[BLOCK_SIZE];用于存放每个block中每个thread计算的求和值(具体是什么和将会随程序推进发生变化)。
  2. 第一部分循环充分利用了每个thread的寄存器空间,用单个thread计算了向量元素中跨步长gridDim.x * blockDim.x(此相乘结果即为所有的thread数)元素的和。此思想即为总thread不够用时,一个thread处理多个元素~
    一个thread计算了上述对应位置的元素之和放在temp变量中,然后将计算的结果放在该thread在sum_per_block中对应的位置上,注意这里使用的索引是threadIdx.x,是该thread在所在block中的坐标!
    具体过程可见下图,如线程10就计算了a[10],a[42],a[74]这三个元素的和。
    在这里插入图片描述
    还有记得写shared memory用__syncthreads();同步!!
  3. 下面将进行block中thread间暂加值的再求和,为了提高效率可以进行如下图所示的两两相加。
    在这里插入图片描述
    所以随着累加的进行,每次两两相加间的步长需要对半分,直到最后结果存放在了每个block的threadIdx.x=0的thread在sum_per_block中对应的位置,即sum_per_block[0]。这个过程中也不要忘记同步!
    也可见上述过程中每次只会有上次一半的thread继续工作,因此上一步骤中充分利用寄存器减少这个过程最开始需要的thread数,可以更好地利用thread资源~
  4. 那么现在只需要将所有block中的sum_per_block[0]相加就可得到最终结果了。
    在这里插入图片描述
    为了避免此累加过程相互影响,这里用到了原子操作atomicAdd(output, sum_per_block[0])实现了对多线程共享的output变量的互斥保护,保证了计算结果的正确性。

3. 向量元素求topk

本部分代码除核函数外来自夏令营所给例程。

3.1 元素求和思想转变

由上述思想我们可以尝试将其用在求topk的任务中,将原来的求和过程变为相互之间比较大小且保留前topk元素,难点在于需要维护数组变量和找到正确的索引值。

3.2 insert_value函数

这个函数是实现寻找topk的关键之一。代码如下:

__device__ __host__ void insert_value(int* array, int k, int data)
{
    for (int i = 0; i < k; i++)
    {
        if (array[i] == data)
        {
            return;
        }
    }
    if (data < array[k - 1])
        return;
    for (int i = k - 2; i >= 0; i--)
    {
        if (data > array[i])
            array[i + 1] = array[i];
        else {
            array[i + 1] = data;
            return;
        }
    }
    array[0] = data;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  1. 第一个for循环寻找array的topk中是否有与输入data相同的元素,有则不再重复记录直接return。
  2. 接下来的if判断data是否比array的topk中最小的一个数还要小,是则直接return。
  3. 然后从topk的倒数第二个元素开始比较,若data比之更大,则该元素向后一个位置复制,再循环直到比较出data是最大的放在array[0]或者比某个数更小了放在它后面然后return。

3.3 kernel函数及坐标计算

相关代码如下:

#define topk 10

__managed__ int source_array[N];
__managed__ int _1pass_results[topk * GRID_SIZE];
__managed__ int final_results[topk];

__global__ void top_k(int* input, int length, int* output, int k)
{
    __shared__ int topk_per_block[BLOCK_SIZE*topk];
    
    int top_kt[topk] = {0};
    
    for(int i=blockDim.x*blockIdx.x+threadIdx.x; i<length; i+=gridDim.x*blockDim.x){
        insert_value(top_kt, k, input[i]);
    }
    
    for(int i=0; i<topk; i++)
        topk_per_block[threadIdx.x*k+i] = top_kt[i]; 
    
    __syncthreads();
    
    for(int len=BLOCK_SIZE/2; len>=1; len/=2){

        if(threadIdx.x<len){
            for(int i=0; i<k; i++)
                insert_value(topk_per_block+threadIdx.x*k, k, topk_per_block[(threadIdx.x+len)*k+i]); 
        }
        __syncthreads();
        
    }
    
    if(blockDim.x * blockIdx.x < length)
        if (threadIdx.x == 0){
            for(int i=0; i<k; i++)
                output[k*blockIdx.x+i] = topk_per_block[i];
        }
    
}
int main(int argc, char const* argv[])
{
	...
    top_k <<<GRID_SIZE, BLOCK_SIZE >>>(source_array, N, _1pass_results, topk);
   
    top_k <<<1, BLOCK_SIZE >>>(_1pass_results, topk * GRID_SIZE, final_results, topk);
	...
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  1. 由于需要存放topk个元素,所以声明变量大小为BLOCK_SIZE*topk。
  2. 这里大致思路也相同,主要是数组top_kt中的元素需要循环放入对应位置,且由于每个thread对应需要在topk_per_block中存topk个元素,所以每个thread对应存放的起始位置为threadIdx.x*k。
  3. 同样类似地,循环比较两两间的topk元素,注意比较时步长len也要加在*k的括号里。这一步就将每个block的topk放在topk_per_block数组的前topk中了。
  4. 然后再将每个block的前topk结果再赋值给output对应的位置,对应的起始位置是k*blockIdx.x。
  5. 显然经过此役还没有得到最终的结果,只得到了每个block中的topk,所以还需要再调用一次核函数,只再用一个block重复上述过程就可以得到最终整个topk的结果啦~

最终运行结果:
在这里插入图片描述

其实还可以加上topk在原向量中坐标的寻找哦ヽ(* ̄▽ ̄*)ノ
读者朋友们可以试试在此基础上修改各函数,增加向量中坐标的相关变量完成上述任务。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/花生_TL007/article/detail/617294
推荐阅读
相关标签
  

闽ICP备14008679号