使用managedCuda汇总数组中的元素

时间:2018-04-15 18:50:00

标签: cuda managed-cuda

问题描述

我尝试让内核总结一个数组的所有元素。内核旨在以每块256个线程和任意数量的块启动。作为a传入的数组的长度总是512的倍数,实际上它是#blocks * 512.内核的一个块应该总结为它的' 512个元素(256个线程可以使用此算法将512个元素相加),将结果存储在out[blockIdx.x]中。 out中的值的最终总和,以及块的结果,将在主机上完成。
这个内核适用于最多6个块,最多可达3072个元素。但是使用超过6个块启动它会导致第一个块计算比其他块(即out = {572, 512, 512, 512, 512, 512, 512})严格更大的错误结果,这个错误的结果是可重现的,多次执行的错误值是相同的。
我想这意味着我的代码中某处存在结构性错误,这与blockIdx.x有关,但唯一用于计算blockStart,这也是一个正确的计算,也是对于第一个街区。
我验证了我的主机代码是否为内核计算了正确的块数并传入了正确大小的数组。那不是问题。
当然,我在stackoverflow上阅读了很多类似的问题,但似乎没有一个描述我的问题(参见我。herehere
内核是通过managedCuda(C#)调用的,我不知道这可能是个问题。

硬件

我使用的MX150具有以下规格:

  • 修订号:6.1
  • 全球总记忆:2147483648
  • 每个块的共享内存总量:49152
  • 每个块的总寄存器:65536
  • 翘曲尺寸:32
  • 每个块的最大线程数:1024
  • Max Blocks:2147483648
  • 多处理器数量:3

代码

内核

__global__ void Vector_Reduce_As_Sum_Kernel(float* out, float* a)
{   
int tid = threadIdx.x;
int blockStart = blockDim.x * blockIdx.x * 2;
int i = tid + blockStart;

int leftSumElementIdx =  blockStart + tid * 2;

a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];

__syncthreads();

if (tid < 128) 
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if(tid < 64)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid < 32)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid < 16)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid < 8)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid < 4)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid < 2)
{
    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
}

__syncthreads();

if (tid == 0)
{
    out[blockIdx.x] = a[blockStart] + a[blockStart + 1];
}
}

内核调用

//Get the cuda kernel
//PathToPtx and MangledKernelName must be replaced
CudaContext cntxt = new CudaContext();
CUmodule module = cntxt.LoadModule("pathToPtx");    
CudaKernel vectorReduceAsSumKernel = new CudaKernel("MangledKernelName", module, cntxt);

//Get an array to reduce
float[] array = new float[4096];
for(int i = 0; i < array.Length; i++)
{
    array[i] = 1;
}

//Calculate execution info for the kernel
int threadsPerBlock = 256;
int numOfBlocks = array.Length / (threadsPerBlock * 2);

//Memory on the device
CudaDeviceVariable<float> m_d = array;
CudaDeviceVariable<float> out_d = new CudaDeviceVariable<float>(numOfBlocks);

//Give the kernel necessary execution info
vectorReduceAsSumKernel.BlockDimensions = threadsPerBlock;
vectorReduceAsSumKernel.GridDimensions = numOfBlocks;

//Run the kernel on the device
vectorReduceAsSumKernel.Run(out_d.DevicePointer, m_d.DevicePointer);

//Fetch the result
float[] out_h = out_d;

//Sum up the partial sums on the cpu
float sum = 0;
for(int i = 0; i < out_h.Length; i++)
{
    sum += out_h[i];
}

//Verify the correctness
if(sum != 4096)
{
    throw new Exception("Thats the wrong result!");
}

更新

非常有帮助且唯一的答案确实解决了我的所有问题。谢谢!问题是无法预料的竞争状况。

重要提示:

在评论中,managedCuda的作者指出所有核电厂方法确实已经在managedCuda(using ManagedCuda.NPP.NPPsExtensions;)中实施。我没有意识到这一点,我想很多人都在阅读这个问题。

1 个答案:

答案 0 :(得分:1)

您没有正确地将代码整合到您的代码中,即每个块将处理整个数组中的512个元素。根据我的测试,您需要至少进行2次更改才能解决此问题:

  1. 在内核中,您错误地计算了每个块的起点:

    int blockStart = blockDim.x * blockIdx.x;
    

    因为blockDim.x是256,但是每个块处理512个元素,你必须将它乘以2.(在leftSumElementIdx的计算中乘以2不会解决这个问题 - 因为它只会乘以tid)。

  2. 在您的主机代码中,您的块数计算不正确:

    vectorReduceAsSumKernel.GridDimensions = array.Length / threadsPerBlock;
    

    array.Length的值为2048,threadsPerBlock的值为256,这将创建8个块。但正如您已经指出的那样,您的目的是为块启动(2048/512)。所以你需要将分母乘以2:

    vectorReduceAsSumKernel.GridDimensions = array.Length / (2*threadsPerBlock);
    
  3. 此外,您的减少扫描模式已被破坏。它取决于warp-execution-order,以提供正确的结果,而CUDA不指定warp执行顺序。

    要了解原因,我们举一个简单的例子。让我们只考虑一个单独的线程块,数组的起点全部为1,就像你初始化它一样。

    现在,warp 0由线程0-31组成。您的减少扫描操作如下:

    a[i] = a[leftSumElementIdx] + a[leftSumElementIdx + 1];
    

    因此warp 0中的每个线程将收集另外两个值并添加它们并存储它们。线程31将采用值a[62]a[63]并将它们添加到一起。如果a[62]a[63]的值仍为1,则初始化后,这将按预期工作。但a[62]a[63] 的值由warp 1写入,由线程32-63组成。因此,如果warp 1在warp 0之前执行(完全合法),那么你将获得不同的结果。这是一个全局内存竞争条件。它是由于您的输入数组既是中间结果的源和目标,而__syncthreads()也不会为您排序。它不会强制warp以任何特定顺序执行。

    一种可能的解决方案是修复扫描模式。在任何给定的缩减周期中,让我们有一个扫描模式,其中每个线程在该周期内写入和读取任何其他线程未触及的值。以下对内核代码的修改可以实现:

    __global__ void Vector_Reduce_As_Sum_Kernel(float* out, float* a)
    {
      int tid = threadIdx.x;
      int blockStart = blockDim.x * blockIdx.x * 2;
      int i = tid + blockStart;
    
      for (int j = blockDim.x; j > 0; j>>=1){
        if (tid < j)
          a[i] += a[i+j];
    
        __syncthreads();}
    
      if (tid == 0)
      {
        out[blockIdx.x] = a[i];
      }
    }
    

    对于通用缩减,这仍然是一种非常缓慢的方法。这个tutorial涵盖了如何编写更快的缩减。并且,正如已经指出的那样,managedCuda可能有避免编写内核的方法。