繁体   English   中英

CUDA内核中的竞争条件

[英]Race Condition in CUDA Kernel

我有一个看起来像竞争条件的CUDA内核,并试图查明此竞争条件的来源。 我知道cuda-memcheck的“竞赛检查”工具,但是racecheck告诉我,使用少量输入内容不会带来危害,这实际上也与我自己的研究一致。 对于大的输入,尽管racecheck似乎永远(从字面上看),所以我不能使用它。 简要说明d_mat_3d定义为__device__变量的一维矢量d_mat_3d填充为0并加载到全局内存中。 两个大阵列它们是内核的输入( d_Ad_v )中也被定义main并传递给籽粒。 剪切数组d_mat_3d称为mat_2d一段,将其装入共享内存中,并将对其进行一些处理。 然后,将mat_2d写回到全局存储器上的d_mat_3d

如此处所示,使用原子操作就像不使用原子操作一样, mat_2d会遇到竞争条件b / w不同的线程。

我猜我仍有某种竞争状况在继续, mat_3d是每次mat_3d的结果都不同。

关于这种比赛条件可能来自何处? 我可以采取任何清除措施(除了工具Racecheck以外)? 如果您认为没有竞争条件的证据,您能否解释为什么每次我执行内核时都将不同的值分配给d_mat_3d

CUDA 9.0 / NVidia Titan Black / Ubuntu 16.04

#include <cstdlib>
#include <sstream>
#include <cstdio>
#include <cuda.h>
#include <cuda_runtime_api.h>

#define W 7              // fix limit for loops in kernel
#define SIZE 100         // defining matrix dimension
#define N_ELEM 10000     // no of elements in each vector
#define NTPB 1024        // no of threads per block

using namespace std;

__device__ float d_mat_3d[SIZE*SIZE*SIZE]; 

__global__ void cuda_kernel(float *d_A, float *d_v){

  __shared__ float mat_2d[SIZE*SIZE]; // a 2D slice of 3D matrix d_mat_3d

  unsigned int n = blockDim.x*blockIdx.x+threadIdx.x;

  if(n >= N_ELEM)
    return;

  int x, y, z, i;
  float r;
  float A = d_A[n];
  float v = d_v[n];

  #pragma unroll
  for(x=0; x<SIZE; x++){

    // load mat_2d (on shared memory) using d_mat_3d (on global memory)
    for(i=0; i<SIZE*SIZE; i++){
      mat_2d[i] = d_mat_3d[i+x*SIZE*SIZE];
    }

    // sync threads as mat_2d is on shared memory
    __syncthreads();

    for(y=SIZE/2; y<SIZE/2+W; y++){ 
      for(z=SIZE/2; z<SIZE/2+W; z++){
        r = sqrt( pow(A,2) / v );  // no need to be in these loops. I know, but for my real case, it must be.
        atomicAdd(&mat_2d[z+y*SIZE], r); // atomically add r 
      }
    }

    __syncthreads();
    // write mat_2d (shared memory) back to mat_3d (global memory)
    for(i=0; i<SIZE*SIZE; i++){
      d_mat_3d[i+x*SIZE*SIZE] = mat_2d[i];
    }
  }
}

// this function writes h_mat_3d to disk. 
void write_image(float *h_mat_3d){
  ostringstream o_addToFile;
  o_addToFile << "mat3d.bin";
  FILE *pFile; 
  pFile = fopen(o_addToFile.str().c_str(), "wb");
  for(int i=0; i<SIZE*SIZE*SIZE; i++){ 
    fwrite(&h_mat_3d[i], sizeof(float), 1, pFile);
  }
  fclose (pFile);
}

int main(){

  int i;
  float *h_A = new float[N_ELEM]; // some large vector
  float *h_v = new float[N_ELEM]; // some other large vector
  float h_mat_3d[SIZE*SIZE*SIZE]; // will be filled w/ 0
  float *d_A; // device variables
  float *d_v;

  for(i=0; i<N_ELEM; i++){
    h_A[i] = 0.2f+(float)i/N_ELEM; // fill out with some calculations
    h_v[i] = 0.5f+2.f*i/N_ELEM;
  }
  for(i=0; i<SIZE*SIZE*SIZE; i++){
    h_mat_3d[i] = 0.f; // fill h_mat_3d with 0 
  }

  cudaMalloc((void **)&d_A, sizeof(float)*N_ELEM); // allocate variables on device
  cudaMalloc((void **)&d_v, sizeof(float)*N_ELEM);

  cudaMemcpy(d_A, h_A, sizeof(float)*N_ELEM, cudaMemcpyHostToDevice); // copy from host to device
  cudaMemcpy(d_v, h_v, sizeof(float)*N_ELEM, cudaMemcpyHostToDevice);
  cudaMemcpyToSymbol(d_mat_3d, &h_mat_3d, sizeof(float)*SIZE*SIZE*SIZE); // copy h_mat_3d to device

  cuda_kernel<<<(N_ELEM+NTPB-1)/NTPB,NTPB>>>(d_A, d_v); // execute kernel

  cudaMemcpyFromSymbol(h_mat_3d, d_mat_3d, sizeof(float)*SIZE*SIZE*SIZE); // write it back to h_mat_3d

  write_image(h_mat_3d); // write h_mat_3d to disk for checking

  cudaFree(d_A); // free memory
  cudaFree(d_v);
  delete [] h_A;
  delete [] h_v;

  return 0;
}

是的,您的代码中至少有2种不同的竞争条件。

  1. 由于您正在循环加载整个共享内存(即,一次又一次地循环加载所有共享内存),因此有必要使用__syncthreads()保护加载操作的开始和结束。 这样做会减少从运行到下到第6位或第7位有效十进制数字的可变性 ,这与浮点运算中的普通float可变性是一致的, 在浮点运算中操作的顺序是不重复的(通常是这种情况)这里)。

    添加以下行:

      for(x=0; x<SIZE; x++){ __syncthreads(); // add this line // load mat_2d (on shared memory) using d_mat_3d (on global memory) for(i=0; i<SIZE*SIZE; i++){ mat_2d[i] = d_mat_3d[i+x*SIZE*SIZE]; } // sync threads as mat_2d is on shared memory __syncthreads(); 

    应该可以更正此问题。 否则,当您的内核在x循环时,某些warp可能会“提前”开始加载共享内存,而先前的warp仍在忙于x的先前循环迭代(请注意下面的注释2,这可能会加剧此问题。 。)

  2. 由于每个线程块都在写入d_mat_3d的全部内容, d_mat_3d您在每个线程块尝试写入各种值时都会遇到竞争状况。 线程块的执行顺序(未由CUDA定义)将主要确定最终的结果,并且可以轻松地逐次运行。 我知道在不进行完整内核重写的情况下解决此问题的唯一简单方法是仅启动1个线程块(仍将填充d_mat_3d的相同区域)。 这种竞争状况是一个全局内存竞争,而cuda-memcheck目前无法发现这种竞争。 我不愿过多地了解这一点,但是这段代码实际上没有任何意义,或者表明缺乏对合理代码的关注,或者缺乏对CUDA执行模型的理解(尤其是与下面的项目2结合在一起)。 )

我还要指出其他几件事。

  1. 在最后一个线程块中,使用__syncthreads()可能是非法的。 此构造:

      if(n >= N_ELEM) return; 

    将允许(最后一个)线程块中的某些线程提前退出,这意味着它们将不参与后续的__syncthreads()语句。 这在CUDA中是非法的,并且编程指南中涵盖了限制。 这可以通过删除早期返回值并使用if (n < N_ELEM)或类似方法保护内核循环的各个部分(__syncthreads()语句if (n < N_ELEM)

  2. 正如您已经在注释中指出的那样,您的内核代码通常很奇怪。 这样的一个例子是您让块中的每个线程都执行完全相同的加载并存储到共享内存或从共享内存存储。 从性能角度来讲,这在许多方面都是浪费的。

我并不是说这涵盖了代码的每个问题,而不仅仅是我注意到的事情。 这是我用来验证发现的相对完整的测试案例。 它包括对我上面提到的项目的一些更改,以及对我来说似乎很重要的其他各种更改:

$ cat t268.cu
#include <cstdlib>
#include <sstream>
#include <cstdio>
#include <cuda.h>
#include <cuda_runtime_api.h>

#define W 7              // fix limit for loops in kernel
#define SIZE 100         // defining matrix dimension
#define N_ELEM 10000     // no of elements in each vector
#define NTPB 1024        // no of threads per block

using namespace std;

__device__ float d_mat_3d[SIZE*SIZE*SIZE];

__global__ void cuda_kernel(float *d_A, float *d_v){

  __shared__ float mat_2d[SIZE*SIZE]; // a 2D slice of 3D matrix d_mat_3d

  unsigned int n = blockDim.x*blockIdx.x+threadIdx.x;


  int x, y, z, i;
  float r;
  float A = d_A[n];
  float v = d_v[n];

  #pragma unroll
  for(x=0; x<SIZE; x++){
  __syncthreads();
if (n < N_ELEM){
    // load mat_2d (on shared memory) using d_mat_3d (on global memory)
    for(i=0; i<SIZE*SIZE; i++){
      mat_2d[i] = d_mat_3d[i+x*SIZE*SIZE];
    }
}
    // sync threads as mat_2d is on shared memory
    __syncthreads();
if (n < N_ELEM){
    for(y=SIZE/2; y<SIZE/2+W; y++){
      for(z=SIZE/2; z<SIZE/2+W; z++){
        r = sqrt( pow(A,2) / v );  // no need to be in these loops. I know, but for my real case, it must be.
        atomicAdd(&(mat_2d[z+y*SIZE]), r); // atomically add r
      }
    }
}
    __syncthreads();
    // write mat_2d (shared memory) back to mat_3d (global memory)
if (n < N_ELEM){
    for(i=0; i<SIZE*SIZE; i++){
      d_mat_3d[i+x*SIZE*SIZE] = mat_2d[i];
    }
}
  }
}

// this function writes h_mat_3d to disk.
void write_image(float *h_mat_3d){
  for (int i = 0; i < SIZE*SIZE; i++){
    for (int j = 0; j < SIZE; j++)
      if (h_mat_3d[i*SIZE+j] > 1.0f) printf("%d:%f\n ", i*SIZE+j,  h_mat_3d[i*SIZE+j]);
    printf("\n");}
}

int main(){

  int i;
  float *h_A = new float[N_ELEM]; // some large vector
  float *h_v = new float[N_ELEM]; // some other large vector
  float *h_mat_3d = new float[SIZE*SIZE*SIZE]; // will be filled w/ 0
  float *d_A; // device variables
  float *d_v;

  for(i=0; i<N_ELEM; i++){
    h_A[i] = 0.2f+i/(float)N_ELEM; // fill out with some calculations
    h_v[i] = 0.5f+2.f*i/(float)N_ELEM;
  }
  for(i=0; i<SIZE*SIZE*SIZE; i++){
    h_mat_3d[i] = 0.f; // fill h_mat_3d with 0
  }

  cudaMalloc((void **)&d_A, sizeof(float)*N_ELEM); // allocate variables on device
  cudaMalloc((void **)&d_v, sizeof(float)*N_ELEM);

  cudaMemcpy(d_A, h_A, sizeof(float)*N_ELEM, cudaMemcpyHostToDevice); // copy from host to device
  cudaMemcpy(d_v, h_v, sizeof(float)*N_ELEM, cudaMemcpyHostToDevice);
  cudaMemcpyToSymbol(d_mat_3d, h_mat_3d, sizeof(float)*SIZE*SIZE*SIZE); // copy h_mat_3d to device

  cuda_kernel<<<1,NTPB>>>(d_A, d_v); // execute kernel

  cudaMemcpyFromSymbol(h_mat_3d, d_mat_3d, sizeof(float)*SIZE*SIZE*SIZE); // write it back to h_mat_3d

  write_image(h_mat_3d); // write h_mat_3d to disk for checking

  cudaFree(d_A); // free memory
  delete [] h_A;
  delete [] h_v;

  return 0;
}
$ nvcc -arch=sm_52 -o t268 t268.cu
$ ./t268 > out1.txt
$ ./t268 > out2.txt
$ diff out1.txt out2.txt |more
51,57c51,57
< 5050:330.657715
<  5051:330.657715
<  5052:330.657715
<  5053:330.657715
<  5054:330.657715
<  5055:330.657715
<  5056:330.657715
---
> 5050:330.657654
>  5051:330.657593
>  5052:330.657593
>  5053:330.657593
>  5054:330.657593
>  5055:330.657593
>  5056:330.657593
59,65c59,65
< 5150:330.657715
<  5151:330.657715
<  5152:330.657715
<  5153:330.657715
<  5154:330.657745
<  5155:330.657745
<  5156:330.657745
---
> 5150:330.657593
>  5151:330.657593
>  5152:330.657593
>  5153:330.657593
>  5154:330.657593
>  5155:330.657593
>  5156:330.657593
67,73c67,73
< 5250:330.657745
<  5251:330.657745
<  5252:330.657745
<  5253:330.657745
<  5254:330.657715
<  5255:330.657715
<  5256:330.657715
---
> 5250:330.657593
>  5251:330.657593
>  5252:330.657623
>  5253:330.657593
>  5254:330.657593
>  5255:330.657593
>  5256:330.657593
75,81c75,81
< 5350:330.657715
<  5351:330.657715
<  5352:330.657715
<  5353:330.657715
<  5354:330.657715
<  5355:330.657745
<  5356:330.657715
---
> 5350:330.657593
>  5351:330.657593
$

可以看出,其余的变化是第7个有效十进制数字:

51,57c51,57
< 5050:330.657715
...
---
> 5050:330.657654

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM