试图从复杂性和执行方面理解cuda sdk最终简化代码段

试图从复杂性和执行方面理解cuda sdk最终简化代码段,cuda,sum,gpu,reduction,Cuda,Sum,Gpu,Reduction,我试图理解Cuda中的并行归约(这非常有趣)。在我最后一个关于平行还原的问题中,Robert Crovella给出了一个非常直观和详细的解释,这对我帮助很大。非常直观。现在查看Cuda SDK的还原样本,几乎没有黑点 为什么(在下面的注释中)工作复杂性保持在O(n)中?在哪种情况下会发生相反的情况?关于步骤的复杂性,我也有同样的问题 此版本按顺序为每个线程添加多个元素。这降低了总体成本 在保持工作复杂度O(n)和步长复杂度的情况下,算法的成本 O(对数n) 有人能给出一个直观的例子来说明共享内存

我试图理解Cuda中的并行归约(这非常有趣)。在我最后一个关于平行还原的问题中,Robert Crovella给出了一个非常直观和详细的解释,这对我帮助很大。非常直观。现在查看Cuda SDK的还原样本,几乎没有黑点

  • 为什么(在下面的注释中)工作复杂性保持在O(n)中?在哪种情况下会发生相反的情况?关于步骤的复杂性,我也有同样的问题

    此版本按顺序为每个线程添加多个元素。这降低了总体成本 在保持工作复杂度O(n)和步长复杂度的情况下,算法的成本
    O(对数n)

  • 有人能给出一个直观的例子来说明共享内存的数量(“注意这个内核需要……分配
    blockSize*sizeof(T)
    bytes”)以及它与代码的关系吗

  • 为什么
    nIsPow2
    如此重要?有人能解释或举例说明吗

  • 为什么我们在下面的作业中使用
    mySum
    <代码>sdata[tid]=mySum=mySum+sdata[tid+256]而不仅仅是sdata[tid]+=data[tid+256]像在Marks Harris演示中那样

    /*This version adds multiple elements per thread sequentially.  This reduces the   
    overall cost of the algorithm while keeping the work complexity O(n) and the       
    step complexity O(log n).
    (Brent's Theorem optimization)
    
    Note, this kernel needs a minimum of 64*sizeof(T) bytes of shared memory.
    In other words if blockSize <= 32, allocate 64*sizeof(T) bytes.
    If blockSize > 32, allocate blockSize*sizeof(T) bytes.*/
    
    template <class T, unsigned int blockSize, bool nIsPow2> 
    __global__ void
    
    reduce6(T *g_idata, T *g_odata, unsigned int n)
    
    {
    T *sdata = SharedMemory<T>();
    
    // perform first level of reduction,
    // reading from global memory, writing to shared memory
    unsigned int tid = threadIdx.x;
    unsigned int i = blockIdx.x*blockSize*2 + threadIdx.x;
    unsigned int gridSize = blockSize*2*gridDim.x;
    
    T mySum = 0;
    
    // we reduce multiple elements per thread.  The number is determined by the
    // number of active thread blocks (via gridDim).  More blocks will result
    // in a larger gridSize and therefore fewer elements per thread
    while (i < n)
    {
        mySum += g_idata[i];
    
        // ensure we don't read out of bounds -- this is optimized away for powerOf2 sized arrays
        if (nIsPow2 || i + blockSize < n)
            mySum += g_idata[i+blockSize];
    
        i += gridSize;
    }
    
    // each thread puts its local sum into shared memory
    sdata[tid] = mySum;
    __syncthreads();
    
    
    // do reduction in shared mem
    if (blockSize >= 512)
    {
        if (tid < 256)
        {
            sdata[tid] = mySum = mySum + sdata[tid + 256];
        }
    
        __syncthreads();
    }
    
    if (blockSize >= 256)
    {
        if (tid < 128)
        {
            sdata[tid] = mySum = mySum + sdata[tid + 128];
        }
    
        __syncthreads();
    }
    
    if (blockSize >= 128)
    {
        if (tid <  64)
        {
            sdata[tid] = mySum = mySum + sdata[tid +  64];
        }
    
        __syncthreads();
    }
    
    if (tid < 32)
    {
        // now that we are using warp-synchronous programming (below)
        // we need to declare our shared memory volatile so that the compiler
        // doesn't reorder stores to it and induce incorrect behavior.
        volatile T *smem = sdata;
    
        if (blockSize >=  64)
        {
            smem[tid] = mySum = mySum + smem[tid + 32];
        }
    
        if (blockSize >=  32)
        {
            smem[tid] = mySum = mySum + smem[tid + 16];
        }
    
        if (blockSize >=  16)
        {
            smem[tid] = mySum = mySum + smem[tid +  8];
        }
    
        if (blockSize >=   8)
        {
            smem[tid] = mySum = mySum + smem[tid +  4];
        }
    
        if (blockSize >=   4)
        {
            smem[tid] = mySum = mySum + smem[tid +  2];
        }
    
        if (blockSize >=   2)
        {
            smem[tid] = mySum = mySum + smem[tid +  1];
        }
    }
    
    // write result for this block to global mem
    if (tid == 0)
        g_odata[blockIdx.x] = sdata[0];
    
    /*此版本按顺序为每个线程添加多个元素。这降低了成本
    在保持工作复杂度O(n)和
    步骤复杂度O(logn)。
    (布伦特定理优化)
    注意,这个内核至少需要64*sizeof(T)字节的共享内存。
    换句话说,如果blockSize为32,则分配blockSize*sizeof(T)字节*/
    模板
    __全局无效
    reduce6(T*g_-idata,T*g_-odata,无符号整数n)
    {
    T*sdata=SharedMemory();
    //执行第一级还原,
    //从全局内存读取,写入共享内存
    unsigned int tid=threadIdx.x;
    无符号整数i=blockIdx.x*blockSize*2+threadIdx.x;
    unsigned int gridSize=blockSize*2*gridDim.x;
    T mySum=0;
    //我们减少了每个线程的多个元素。数量由
    //活动线程块数(通过gridDim)。将产生更多的块
    //在更大的网格大小中,因此每个线程的元素更少
    而(i=512)
    {
    如果(tid<256)
    {
    sdata[tid]=mySum=mySum+sdata[tid+256];
    }
    __同步线程();
    }
    如果(块大小>=256)
    {
    如果(tid<128)
    {
    sdata[tid]=mySum=mySum+sdata[tid+128];
    }
    __同步线程();
    }
    如果(块大小>=128)
    {
    如果(tid<64)
    {
    sdata[tid]=mySum=mySum+sdata[tid+64];
    }
    __同步线程();
    }
    如果(tid<32)
    {
    //现在我们正在使用warp同步编程(如下)
    //我们需要声明共享内存为volatile,以便编译器
    //不会将存储重新排序到它并导致不正确的行为。
    挥发性T*smem=sdata;
    如果(块大小>=64)
    {
    smem[tid]=mySum=mySum+smem[tid+32];
    }
    如果(块大小>=32)
    {
    smem[tid]=mySum=mySum+smem[tid+16];
    }
    如果(块大小>=16)
    {
    smem[tid]=mySum=mySum+smem[tid+8];
    }
    如果(块大小>=8)
    {
    smem[tid]=mySum=mySum+smem[tid+4];
    }
    如果(块大小>=4)
    {
    smem[tid]=mySum=mySum+smem[tid+2];
    }
    如果(块大小>=2)
    {
    smem[tid]=mySum=mySum+smem[tid+1];
    }
    }
    //将此块的结果写入全局mem
    如果(tid==0)
    g_odata[blockIdx.x]=sdata[0];
    
    }


  • 子问题1:

    reduce6可能会增加更多的工作量——每个线程将几个元素相加。但是,线程总数减少了相同的系数,因此总工作量仍然为O(n)。至于“步骤复杂性”——这应该是内核调用的数量。它实际上是O(log(n)),因为内核调用的数量已经减少(每次调用都会将数据向量减少一个更大的因子)。如果我没记错网格尺寸是如何设置的,我想它仍然是θ(log(n))

    子问题2

    那么,内核假设每个扭曲都是“满的”,即有足够的输入数据供其所有线程使用。一个warp是32个线程,每个线程读取至少2个输入元素,因此内核假设至少有64个输入元素(其大小为64*sizeof(T))。这也适用于共享内存,因为内核的单个warp部分从共享内存获取其输入,从中读取32*2个sizeof(T)字节元素

    子问题3

    nIsPow2
    设置为true时-请注意,这不是内核获得的参数,而是单独编译的不同版本的代码,而不是为
    nIsPow2
    编译的代码,如果为false-条件
    nIsPow2 | i+blockSize
    始终保持不变,并且编译器(假设它足够聪明)将完全避免绑定支票。因此,每个线程将(安全地)执行
    mySum+=g_idata[i+blockSize]

    子问题4

    mysum
    是每个扭曲的总和。在我们完成计算后,每个扭曲中的一些线程需要与其他扭曲共享它-这是通过共享内存完成的(块中的所有扭曲都可以访问)。现在,请注意,“双重赋值”——对
    mysum
    和共享内存位置的赋值——都是由于不同线程的进一步使用:每个线程计算自己的
    mysum
    值,并将其反复相加。在将
    mysum
    最终分配给共享m之后