cuda 学习笔记

uodate:有毒吧。kernel中出问题原来是不会报错的。。。。

请教了组里的hust学长orz..、

学到了cuda-memcheck命令和cudaGetLastError来查看问题。。可以参考What is the canonical way to check for errors using the CUDA runtime API?

先放一波资料。

  * <del>[An Even Easier Introduction to CUDA](https://devblogs.nvidia.com/even-easier-introduction-cuda/)</del>
  * <del>[CUDA C/C++ Basics](https://drive.google.com/open?id=1kHYyM4yiJoyjkWjp7FJp0vae_TcvskjK)</del>
  * <del>[nvidia-thrust 官方文档](http://docs.nvidia.com/cuda/thrust/index.html)</del>
  * [how-access-global-memory-efficiently-cuda-c-kernels](https://devblogs.nvidia.com/how-access-global-memory-efficiently-cuda-c-kernels/)
  * [efficient-matrix-transpose-cuda-cc](https://devblogs.nvidia.com/efficient-matrix-transpose-cuda-cc/)
  * [很强大的warp内shuffle](https://devblogs.nvidia.com/faster-parallel-reductions-kepler/)
  * [cuda-GDB官方文档](http://docs.nvidia.com/cuda/cuda-gdb/index.html)
  * [cuda-c-best-practices-guide](http://docs.nvidia.com/cuda/cuda-c-best-practices-guide/)

cuda 提出的目的是能够让程序员透明地使用GPU来高效地进行并行运算。

kernel和c语言中的函数相似,函数名字前通常用global来标识。

下面考虑一个2个大小的1M的数组相加的例子。

总的思路是通过并行,来观察到计算速度的加快。

如果不考虑并行,2个数组相加的代码,如下:

#include <iostream>
#include <math.h>
1// function to add the elements of two arrays
2void add(int n, float *x, float *y)
3{
4  for (int i = 0; i < n; i++)
5      y[i] = x[i] + y[i];
6}
1int main(void)
2{
3  int N = 1<<20; // 1M elements
  float *x = new float[N];
  float *y = new float[N];
1  // initialize x and y arrays on the host
2  for (int i = 0; i < N; i++) {
3    x[i] = 1.0f;
4    y[i] = 2.0f;
5  }

如果用cuda的方式来搞,代码如下:

1#include <iostream>
2#include <math.h>
3// Kernel function to add the elements of two arrays
4__global__
5void add(int n, float *x, float *y)
6{
7  for (int i = 0; i < n; i++)
8    y[i] = x[i] + y[i];
9}
1int main(void)
2{
3  int N = 1<<20;
4  float *x, *y;
1  // Allocate Unified Memory – accessible from CPU or GPU
2  cudaMallocManaged(&x, N*sizeof(float));
3  cudaMallocManaged(&y, N*sizeof(float));
1  // initialize x and y arrays on the host
2  for (int i = 0; i < N; i++) {
3    x[i] = 1.0f;
4    y[i] = 2.0f;
5  }
  // Run kernel on 1M elements on the GPU
  add<<<1, 1>>>(N, x, y);

  // Wait for GPU to finish before accessing on host
  cudaDeviceSynchronize();
1  // Check for errors (all values should be 3.0f)
2  float maxError = 0.0f;
3  for (int i = 0; i < N; i++)
4    maxError = fmax(maxError, fabs(y[i]-3.0f));
5  std::cout << "Max error: " << maxError << std::endl;
1  // Free memory
2  cudaFree(x);
3  cudaFree(y);
  return 0;
}

除了代码注释,还有几个地方要说明:

  * 函数名字前面的global 是 cuda kernel  标识符
  * cuda kernel的调用方式是 <<<,>>>  更具体地说,是add<<<numBlocks,blockSize>>>(N,x,y)
  * .cu是 cuda C++ 文件的后缀,类似.cpp
  * nvcc是cuda C++ 的编译器,其将source code分成**host code**和**device code**两部分。前者通过c++编译器编译,后者通过nvidia编译器编译。

关于devide code 和 host code,参考下图。

现在我们单线程地跑了一个cuda kernel, 接下来是如何使它并行.关键在于«<1,1»>这部分。

这行代码告诉了cuda runtime 有多少个并行的线程要被执行。 这里有2个参数,不过我们可以先改变第二个,也就是一个线程block中线程的个数。 cuda GPu的kernel 使用的blocks中线程的个数应该是32的倍数(后面会解释32代表什么),所以256看起来很合理。

1#include <cstdio>
2#include <iostream>
3#include <math.h>
4// Kernel function to add the elements of two arrays
5    __global__
6void add(int n, float *x, float *y)
7{
1     int index = blockIdx.x * blockDim.x + threadIdx.x;
2    int stride = blockDim.x * gridDim.x;
3printf(" %d %d",index,stride);
4    for ( int i = index ; i < n ; i += stride)
5    y[i] = x[i] + y[i];
6}
1int main(void)
2{
3    int N = 1<<20;
4    float *x, *y;
1    // Allocate Unified Memory – accessible from CPU or GPU
2    cudaMallocManaged(&x, N*sizeof(float));
3    cudaMallocManaged(&y, N*sizeof(float));
1    // initialize x and y arrays on the host
2    for (int i = 0; i < N; i++) {
3    x[i] = 1.0f;
4    y[i] = 2.0f;
5    }
1    int blockSize = 256;
2    int numBlocks = (N + blockSize - 1) / blockSize;
3    add<<<numBlocks, blockSize>>>(N, x, y);
4    // Wait for GPU to finish before accessing on host
5    cudaDeviceSynchronize();
1    // Check for errors (all values should be 3.0f)
2    float maxError = 0.0f;
3    for (int i = 0; i < N; i++)
4    maxError = fmax(maxError, fabs(y[i]-3.0f));
5//    std::cout << "Max error: " << maxError << std::endl;
1    // Free memory
2    cudaFree(x);
3    cudaFree(y);
    return 0;
}

不过如果只是修改了«<1,1»> 到  «<1,256»> 那实际上是对于每个线程都算了整个array的相加,而没有将整个计算任务分给多个并行的线程。 为了解决这个问题,我们需要修改kernel的代码。 cuda C++ 提供了关键字,允许kernel得到正要执行的thread是哪一个 threadIdx.x  表示当前运行的thread是block中的哪一个 blockDim.x 表示block中的线程个数

关于threadIdx.x等 下标问题,参考下图。

我们需要观察到使用cuda的方法之后时间的变化。

可以使用nvprof命令

1➜ learn>nvprof ./add_cuda
2==9312== NVPROF is profiling process 9312, command: ./add_cuda
3Max error: 0
4==9312== Profiling application: ./add_cuda
5==9312== Profiling result:
6Time(%)      Time     Calls       Avg       Min       Max  Name
7100.00%  167.48ms         1  167.48ms  167.48ms  167.48ms  add(int, float*, float*)
1==9382== Profiling application: ./add_block
2==9382== Profiling result:
3Time(%)      Time     Calls       Avg       Min       Max  Name
4100.00%  3.5144ms         1  3.5144ms  3.5144ms  3.5144ms  add(int, float*, float*)
1==9447== Profiling application: ./add_grid
2==9447== Profiling result:
3Time(%)      Time     Calls       Avg       Min       Max  Name
4100.00%  1.8084ms         1  1.8084ms  1.8084ms  1.8084ms  add(int, float*, float*)

可以看出时间的变化,从167.48ms到3.5144ms,再到1.8084ms

我们注意到,对线程的管理实际上是三维的 :grid,block,thread.

为什么要这样设计?

一个这样做的目的是,在一个block中,thread可以通过share_memory 来共享数据。

通过在声明的变量前面添加 shared  来表示,这个变量是声明在share memory部分了。

share memory类似与缓存,容量小,但是速度快。不过这个cache是可以编程控制的。

在一个block中共享的data,对于其他block是不可见的。

我们不妨考虑一个例子,有2个数组a,b

进行如下运算:

为了加快运行速度,我们还是考虑多线程的办法。

让每一个线程处理一个输出。

然而我们发现,in中除了边界元素,每一个元素都被读了7次。

这显然是没有必要的。

问题的关键在于,不同的线程之间,不知道某个元素已经被读入了。

更进一步,不同线程之间可以共享数据吗?

答案是可以的。也就是上面提到的shared_memory

然而这样就可以了吗。。

由于线程的访问顺序是不固定的(?

会发生如下的问题:

解决办法很无脑。。。因为cuda并没有想象中得那么底层。。。

就是使用__syncthreads(); 来同步一个block中的所有进程。。。

完整代码如下:

1#include <iostream>
2#include <cstdio>
3#include <cmath>
4#include <ctime>
 1const int R=3;
 2const int N=1<<20;
 3const int BLOCK_SIZE=256;
 4    __global__ 
 5void solve( int *in,int *out)
 6{
 7    __shared__ int tmp[BLOCK_SIZE + 2*R];
 8    int gindex = threadIdx.x + blockIdx.x * blockDim.x;
 9    int lindex = threadIdx.x + R;
10//     printf ("%d %d\n",gindex,lindex-R);
11//    printf("wang\n");
12    //if (lindex < BLOCK_SIZE+2*R && gindex < N)
13      tmp[lindex] = in[gindex];
1//    if 这部分有问题。。。貌似是访问越界。。
2    if (threadIdx.x < R)
3    {
4    if (lindex>=R && gindex>=R&&lindex-R<BLOCK_SIZE+2*R&&gindex-R<N)
5       tmp[lindex-R] = in[gindex-R];
6    if (lindex + BLOCK_SIZE< BLOCK_SIZE+2*R && gindex + BLOCK_SIZE < N )
7       tmp[lindex+BLOCK_SIZE] = in[gindex + BLOCK_SIZE];
8    }
1  //  printf("miao\n");
2    __syncthreads();
3    int res = 0 ;
1    for ( int offset = -R ; offset <= R ; offset++)
2    {
3//  printf ("offset:%d\n",offset);
4//  if (lindex + offset < BLOCK_SIZE+2*R)
5    res += tmp[lindex + offset];
6    }
1    out[gindex] = res;
2    printf("res=%d\n",res);
3}
1void pr( int *A,int n)
2{
3    for ( int i = 0 ;i  <  10 ; i++) printf ("%d%c",A[i*10],i==9?'\n':' ');
4}
1int main(void)
2{
3    int *a,*b;
4    if (cudaSuccess != cudaMallocManaged(&a,N*sizeof(int)))
5    printf("Cuda Malloc error\n");
 1    if (cudaSuccess != cudaMallocManaged(&b,N*sizeof(int)))
 2    printf("Cuda Malloc error\n");
 3    for ( int i = 0 ; i < N ; i++)
 4    {
 5    a[i] = 1;
 6    }
 7    pr(a,N);
 8    pr(b,N);
 9    int numBlocks = ( N + BLOCK_SIZE -1 ) / BLOCK_SIZE;
10    //solve<<<numBlocks,BLOCK_SIZE>>>(a,b);
11    solve<<<1,256>>>(a,b);
12    if (cudaSuccess !=cudaGetLastError())
13    printf("kernel error!");
14    // prt<<<numBlocks,BLOCK_SIZE>>>();
15    cudaDeviceSynchronize();
16    pr(b,N);
1    //printf(cudaGetLastError());
2    cudaFree(a);
3    cudaFree(b);
    return 0;
}

需要特别强调的是,cuda代码的debug问题,很多错误,不用特定的工具查看是不会显示的。 以及,虽然cuda代码是在c++上添加了一些东西,但是device code部分用的是nvidia的编译器。 所以c/cpp中,对于访问非法内存不敏感的特点,在cuda代码中不存在(我猜是因为编译器…) 在访问之前,一定要check访问地址合法性。。。。

在访问之前,一定要check访问地址合法性。。。。

在访问之前,一定要check访问地址合法性。。。。