CUDA中的Dot Product使用原子操作 - 得到错误的结果

时间:2013-04-04 21:52:10

标签: cuda gpu gpgpu

我正在尝试在CUDA中实现点积,并将结果与​​MATLAB返回的结果进行比较。我的CUDA代码(基于this tutorial)如下:

#include <stdio.h>

#define N (2048 * 8)
#define THREADS_PER_BLOCK 512
#define num_t float

// The kernel - DOT PRODUCT
__global__ void dot(num_t *a, num_t *b, num_t *c) 
{
  __shared__ num_t temp[THREADS_PER_BLOCK];
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  temp[threadIdx.x] = a[index] * b[index];
  __syncthreads(); //Synchronize!
  *c = 0.00;
  // Does it need to be tid==0 that
  // undertakes this task?
  if (0 == threadIdx.x) {
    num_t sum = 0.00;
    int i;
    for (i=0; i<THREADS_PER_BLOCK; i++)
      sum += temp[i];
    atomicAdd(c, sum);        
    //WRONG: *c += sum; This read-write operation must be atomic!
  }
}


// Initialize the vectors:
void init_vector(num_t *x)
{
  int i;
  for (i=0 ; i<N ; i++){
    x[i] = 0.001 * i;
  }
}

// MAIN
int main(void)
{
  num_t *a, *b, *c;
  num_t *dev_a, *dev_b, *dev_c;
  size_t size = N * sizeof(num_t);

  cudaMalloc((void**)&dev_a, size);
  cudaMalloc((void**)&dev_b, size);
  cudaMalloc((void**)&dev_c, size);

  a = (num_t*)malloc(size);
  b = (num_t*)malloc(size);
  c = (num_t*)malloc(size);

  init_vector(a);
  init_vector(b);

  cudaMemcpy(dev_a, a, size, cudaMemcpyHostToDevice);
  cudaMemcpy(dev_b, b, size, cudaMemcpyHostToDevice);

  dot<<<N/THREADS_PER_BLOCK, THREADS_PER_BLOCK>>>(dev_a, dev_b, dev_c);

  cudaMemcpy(c, dev_c, sizeof(num_t), cudaMemcpyDeviceToHost);

  printf("a = [\n");
  int i;
  for (i=0;i<10;i++){
    printf("%g\n",a[i]);
  }
  printf("...\n");
  for (i=N-10;i<N;i++){
    printf("%g\n",a[i]);
  }
  printf("]\n\n");
  printf("a*b = %g.\n", *c);


  free(a); free(b); free(c);

  cudaFree(dev_a);
  cudaFree(dev_b);
  cudaFree(dev_c);

}

我用以下代码编译它:

/usr/local/cuda-5.0/bin/nvcc -m64  -I/usr/local/cuda-5.0/include -gencode arch=compute_20,code=sm_20 -o multi_dot_product.o -c multi_dot_product.cu
g++ -m64 -o multi_dot_product multi_dot_product.o -L/usr/local/cuda-5.0/lib64 -lcudart

有关我的NVIDIA显卡的信息,请访问http://pastebin.com/8yTzXUuK。我尝试使用以下简单代码在MATLAB中验证结果:

N = 2048 * 8;
a = zeros(N,1);
for i=1:N
    a(i) = 0.001*(i-1);
end

dot_product = a'*a;

但是随着N的增加,我得到了明显不同的结果(例如,N = 2048 * 32 CUDA reutrns 6.73066e + 07而MATLAB返回9.3823e + 07.N = 2048 * 64 CUDA给出3.28033e + 08而MATLAB给出7.5059e + 08)。我倾向于认为差异源于在我的C代码中使用float,但如果我用double替换它,编译器会抱怨atomicAdd不支持双参数。我该如何解决这个问题?

更新:此外,对于N的高值(例如2048 * 64),我注意到CUDA返回的结果在每次运行时都会发生变化。如果N较低(例如2048 * 8),则不会发生这种情况。

与此同时,我有一个更基本的问题:变量temp是一个大小为THREADS_PER_BLOCK的数组,并在同一个块中的线程之间共享。它是否也在块之间共享,或者每个块在此变量的不同副本上运行?我应该将方法dot视为每个块的指令吗?有人可以详细说明如何分割作业以及如何在此示例中共享变量

1 个答案:

答案 0 :(得分:2)

在你的内核中注释掉这一行:

//   *c = 0.00;

在内核调用之前(在dev_c的cudaMalloc之后)将这些行添加到主机代码中:

num_t h_c = 0.0f;
cudaMemcpy(dev_c, &h_c, sizeof(num_t), cudaMemcpyHostToDevice);

我相信你会得到与matlab相匹配的结果,或多或少。

你的内核中的这一行没有受到任何同步的保护,这一事实让你感到困惑。每个块的每个线程,无论何时执行,都会在您编写它时将其归零{。{1}}。

顺便说一句,通过使用经典的并行约简方法,我们可以通过这种操作做得更好。基本(未优化)插图是here。如果将该方法与共享内存的使用和最后的单个atomicAdd(每个块一个atomicAdd)结合使用,您将获得显着改进的实现。虽然它不是点积,this example结合了这些想法。

编辑:回复评论中的以下问题:

内核函数是一组指令, the grid 中的所有线程(根据定义,与内核启动相关联的所有线程)都会执行。但是,将执行视为由threadblock管理是合理的,因为线程块中的线程在很大程度上一起执行。但是,即使在一个线程块中,执行也不是必须在所有线程上完美地锁定。通常,当我们考虑锁步执行时,我们会想到一个 warp ,它是一个32个线程在一个线程块中的一组。因此,由于块内的warp之间的执行可能会发生偏差,因此即使对于单个线程块也存在这种危险。但是,如果只有一个threadblock,我们可以使用适当的同步和控制机制(例如c__syncthreads()等来消除代码中的危险。但是这些机制对于一般情况来说是无用的。控制跨多个线程块的执行。多个线程块可以按任何顺序执行。整个网格中唯一定义的同步机制是内核启动本身。因此,为了解决您的问题,我们必须在内核启动之前将(if threadIdx.x == 0)清零。