基于NVIDIA Multi-GPU技术的CUDA多GPU编程入门

       由于最近新买了4个GPU而开始接触学习GPU编程,因为我原来的程序使用的是C++语言,故在仅仅看完Nvidia官网Mark Harris写的一篇简短的An Even Easier Introduction to CUDA后开始琢磨如何使用多个GPU同时计算来进一步加速程序。

        在查找有关资料和官方手册后,了解到Nvidia有专门的SLI Multi-GPU技术来实现这个过程,N卡的多卡互联要求有两块或以上完全相同的显卡,并且其显示核心的工作主频也相同,才能使用这项技术。正好我这儿是4块完全相同的GeForce GTX 1080ti,满足互联的要求。于是我在参考了CUDA自带的Samples里的simpleMultiGPU后对Mark Harris给出的简单数组求和程序进行了改写:

#include 
#include 
#include 
#include 
// This application demonstrates how to use CUDA API to use mutiple GPUs(4 Nvidia Geforce GTX 1080 ti)
// Function to add the elements of two arrays

//Mutiple-GPU Plan Structure
typedef struct
{
    //Host-side input data
    float *h_x, *h_y;
	  
    //Result copied back from GPU
	  float *h_yp;
    //Device buffers
    float *d_x, *d_y;

    //Stream for asynchronous command execution
    cudaStream_t stream;

} TGPUplan;

// CUDA Kernel function to add the elements of two arrays on the GPU
__global__
void add(int n, float *x, float *y)
{
  int index = threadIdx.x + blockIdx.x * blockDim.x;
  int stride = blockDim.x * gridDim.x;
  for (int i = index; i < n; i += stride)
      y[i] = x[i] + y[i];
}

int main(void)
{
  int N = 1<<20; // 1M elements
  
  //Get the numble of CUDA-capble GPU
  int N_GPU;
  cudaGetDeviceCount(&N_GPU);

  //Arrange the task of each GPU
  int Np = (N + N_GPU - 1) / N_GPU;
  
  //Create GPU plans
  TGPUplan plan[N_GPU];

  //Initializing 
  for(int i = 0; i < N_GPU; i++)
  {
    cudaSetDevice(i);
    cudaStreamCreate(&plan[i].stream);

    cudaMalloc((void **)&plan[i].d_x, Np * sizeof(float));
    cudaMalloc((void **)&plan[i].d_y, Np * sizeof(float));
    plan[i].h_x = (float *)malloc(Np * sizeof(float));
    plan[i].h_y = (float *)malloc(Np * sizeof(float));
    plan[i].h_yp = (float *)malloc(Np * sizeof(float));

	  for(int j = 0; j < Np; j++)
    {
      plan[i].h_x[j] = 1.0f;
      plan[i].h_y[j] = 2.0f;
    }
  }

  int blockSize = 256;
  int numBlock = (Np + blockSize - 1) / blockSize;

  for(int i = 0; i < N_GPU; i++)
  {
    //Set device
    cudaSetDevice(i);

    //Copy input data from CPU
    cudaMemcpyAsync(plan[i].d_x, plan[i].h_x, Np * sizeof(float), cudaMemcpyHostToDevice, plan[i].stream);
    cudaMemcpyAsync(plan[i].d_y, plan[i].h_y, Np * sizeof(float), cudaMemcpyHostToDevice, plan[i].stream);
    
    //Run the kernel function on GPU
    add<<>>(Np, plan[i].d_x, plan[i].d_y);
    
    //Read back GPU results
    cudaMemcpyAsync(plan[i].h_yp, plan[i].d_y, Np * sizeof(float), cudaMemcpyDeviceToHost, plan[i].stream);
  }

  //Process GPU results
  float y[N];
  for(int i = 0; i < N_GPU; i++)
  {
    //Set device
    cudaSetDevice(i);

    //Wait for all operations to finish
    cudaStreamSynchronize(plan[i].stream);

    //Get the final results
	  for(int j = 0; j < Np; j++)
		  if(Np * i + j < N)
			   y[Np * i + j]=plan[i].h_yp[j];
	  
    //shut down this GPU
    cudaFree(plan[i].d_x);
    cudaFree(plan[i].d_y);
    free(plan[i].h_x);
    free(plan[i].h_y);
  	cudaStreamDestroy(plan[i].stream); //Destroy the stream
  }

  // Check for errors (all values should be 3.0f)
  float maxError = 0.0f;
  for (int i = 0; i < N; i++)
    maxError = fmax(maxError, fabs(y[i]-3.0f));
  std::cout << "Max error: " << maxError << std::endl;

  return 0;
}

可以看到其中最重要的是一个structure的定义:

typedef struct
{
    //Host-side input data
    float *h_x, *h_y;
	  
    //Result copied back from GPU
	  float *h_yp;
    //Device buffers
    float *d_x, *d_y;

    //Stream for asynchronous command execution
    cudaStream_t stream;

} TGPUplan;

里面涉及到一个stream(流)的概念,大概就是不同的流可以并行计算的(我是这么理解的)。

       这其中比较麻烦的过程就是要先将任务尽可能的平均分配,然后将数据从CPU拷贝到GPU上,再在GPU上对kernel函数进行运算,计算结束后还要将数据从GPU拷回CPU进行最后的处理。Mark Harris原程序里用的是cudaMallocManaged来统一寻址,我不清楚这在Multi-GPU时是否生效(而且据说这样会减慢速度),于是我参考的还是Samples里给出的方案。

虽然程序看上去复杂了很多,但是计算结果还是不错。

在终端输入:

nvcc -o add_test add.cu

nvprof ./add_test

        从 nvprof 得到的结果可以看到如果直接运行Mark Harris的程序大概需要接近4ms,改进后的程序用4个GPU去算最终的时间大概是1ms多一些,这个提升还是很明显的,如果在大量计算的情况下,这种加速还是很可观的。

你可能感兴趣的:(GPU)