【问题标题】:Race Condition in CUDA KernelCUDA 内核中的竞争条件
【发布时间】:2018-06-13 15:42:43
【问题描述】:

我有一个似乎有竞争条件的 CUDA 内核,我试图查明这种竞争条件的来源。我知道 cuda-memcheck 的“racecheck”工具,但是racecheck 告诉我使用小输入时没有危险,这实际上也与我自己的调查一致。对于大量输入,尽管racecheck 似乎需要永远(字面意思),所以我不能使用它。 简单解释一下,定义为__device__ 变量的一维向量d_mat_3d 用0 填充并加载到全局内存中。作为内核输入的两个大数组(d_Ad_v)也在main 中定义并传递给内核。数组d_mat_3d,称为mat_2d 的一段被剪切,加载到共享内存中,并对其进行一些处理。然后,mat_2d 将被写回到全局内存上的d_mat_3d

如此处所示,使用原子操作是因为不使用原子操作mat_2d 会遇到不同线程的竞争条件。

我想我仍然有某种竞争条件的原因是mat_3d 的结果每次都不同。

关于这种竞争条件可能来自哪里的任何想法?我可以采取任何步骤来清除它(除了工具竞赛检查)?如果您认为没有竞争条件的证据,您能解释一下为什么每次执行内核时都会为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;
}

【问题讨论】:

  • 实际上对于此类问题,SO 希望您提供minimal reproducible example。阅读第 1 项here。注意“必须”这个词的用法。如果您的声明是正确的,则不需要很多额外的代码行来构建一个完整的程序来证明您的声明。

标签: c++ cuda race-condition


【解决方案1】:

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

  1. 由于您在循环中加载整个共享内存(即在循环中一遍又一遍地加载它),因此有必要使用__syncthreads() 保护加载操作的开始和结束。这样做会将run-to-run的可变性降低到第6或第7位有效十进制数字,这与ordinary float variability in floating-point operations一致,其中操作顺序不重复(这里通常是这种情况)。

    添加以下行:

      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. 1234563线程块执行的顺序(由 CUDA 未定义)将主要决定最终的结果,这很容易在运行之间改变。我所知道的在不完全重写内核的情况下解决这个问题的唯一简单方法是简单地启动 1 个线程块(它仍将填充 d_mat_3d 的相同区域)。这种竞争条件是全局内存竞争,cuda-memcheck 目前无法发现这种竞争。我不愿对此进行过多的阅读,但是这段代码实际上没有任何意义,并且要么表明对合理的代码缺乏关注,要么表明对 CUDA 执行模型缺乏了解(尤其是与下面的第 2 项相结合。 )

我还要指出一些其他的事情。

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

      if(n >= N_ELEM)
        return;
    

    将允许(最后一个)线程块中的一些线程提前退休,这意味着它们不会参与后续的__syncthreads() 语句。这在 CUDA 中是非法的,限制在 the programming guide 中进行了介绍。这可以通过移除提前返回并使用if (n &lt; N_ELEM) 或类似方法保护内核循环的各个部分(__syncthreads() 语句除外)来解决。

  2. 您的内核代码通常很奇怪,正如您已经在 cmets 中指出的那样。这方面的一个例子是,您让块中的每个线程执行完全相同的加载和存储到/从共享内存。从几个方面来看,这在性能方面是浪费的。

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

$ 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

【讨论】:

  • 尽管如此,您提供的代码中的每个线程块都会写入整个 d_mat_3d 数组。由于每个线程块正在写入不同的数据,因此这是一个竞争条件。我并不是说切换到单个线程块是一个明智的解决方案,我用它来证明结果可变性在我这样做时大部分都消失了,这有助于确认竞争条件(尽管通过检查很明显代码)。所以简单的答案是肯定的,你有竞争条件,我已经指出了其中的两个。
  • 除此之外,您的代码毫无意义,但这是一个单独的问题,而不是您提出的问题。如果您的实际代码在我提到的方面与您提供的代码相似,则需要大量重写。
  • 原子交换不会修复它。线程块执行的顺序会影响原子交换的顺序。最后一个线程块将“获胜”,并且可能会因运行而异。如果您在最终数组中对所有线程块结果进行了atomicAdd,则可能主要消除了与线程块到全局写入竞争相关的可变性。我仍然声称这些都没有任何意义,而且你是否理解执行模型也不是很清楚。原子交换如何做任何有用的事情?就像您现在所做的那样,您仍然会丢弃大量计算结果。
  • 我确定可能有。但我不可能建议它,因为您没有描述您实际想要执行的算法。您已经假装提供的代码是对算法的有用描述。我声称不是,原因已经讨论过了。如果要将多个线程块的结果组合到一个数组中,则必须准确定义要执行的算术运算,以及结果将去哪里。如果你有一个新问题,我建议你问一个新问题。让我们不要再把这个弄得乱七八糟了。
猜你喜欢
  • 1970-01-01
  • 2020-01-16
  • 1970-01-01
  • 2022-01-18
  • 1970-01-01
  • 2016-03-07
  • 2011-07-17
  • 2013-02-27
  • 2021-04-16
相关资源
最近更新 更多