如何编写一个有效的CUDA程序,分为两个阶段

时间:2017-04-11 20:29:24

标签: c++ parallel-processing cuda

我想写一个轻量级的PIC(粒子在单元格)程序。 “轻量级”我的意思是它不需要扩展:只是假设所有数据都可以适合单个GPU设备的内存和主机系统的内存。但是我希望它尽可能快。

问题是,PIC的典型结构是两个阶段的相互作用:场解算器和粒子推动器。工作流程如下: 初始化系统 - >推动粒子 - >求解字段 - >推动粒子 - >解决领域...... - >输出

下一个推动粒子或求解场必须等到前一个求解场或推动粒子完成。可能需要数百万次迭代才能获得最终输出。

作为测试,省略场解算器,粒子推动器可以写成:

 __device__
 void push(Particle &par) {
   // some routines to move a particle. same excecutiong time for every particle.
 }

并使用像这样的kernel_1来超越它:

__global__
void kernel_1(int n, Particle* parlist)
{
  int i = blockIdx.x*blockDim.x + threadIdx.x;
  if (i < n) {
      push(parlist[i]);
  }
}

在主循环中

for (int i=0;i<M;i++) {
    kernel_1<<<(n+255)/256, 256>>>(n, parlist);
}

M是所需的迭代次数。但是,性能非常慢:在我的八核Intel E5-2640 v3和Nvidia Quadro m4000系统上,CUDA使用openmp提供与纯CPU版本相似的性能。对于粒子数10,000,000和M = 1000,需要大约10秒。

但是,如果我将循环移动到内核中:

void kernel_2(int n, Particle* parlist)
{
  int i = blockIdx.x*blockDim.x + threadIdx.x;
  if (i < n) {
      for (int i=0;i<M;i++) {
          push(parlist[i]);
      }
  }
}

kernel_2<<<(n+255)/256, 256>>>(n, parlist);

对于相同的M = 1000,它只需要100ms,这是10000%的加速。我已经验证了两种情况下结果相同且正确。也许M次运行内核的调用成本太高了。

将循环移入内核的性能提升是如此令人难以置信,但却是如此。对于第一种情况,可以很容易地添加字段求解器:只需编写一个新内核并在主循环中按顺序执行两个内核。但是表现应该是医学上的。

我发现很难将字段求解程序添加到第二种情况中:在没有多次调用内核的情况下,块之间似乎没有同步机制,但是场解算器必须等到所有粒子都被推送,必须分配到不同的块(因为粒子的数量非常高)。

那么可以在一个内核中实现两阶段迭代吗?性能提升太多而不容忽视。

修改 我发现性能差异非常混乱:100ms和10s的差异只是一行代码甚至是循环序列。我已经将push()修改为更复杂(2d Boris推动器):

class Particle
{
public:
    float x, y;        //m
    float vx, vy;      //m/s
    float m;           //kg
    float q;           //ee
};

__device__
void run(Particle& par, float B)
{
    float t, s, vpx, vpy;
    t = (par.q*ee*B/par.m)*dt/2;
    s = 2*t/(1+t*t);
    vpx = par.vx+t*par.vy;
    vpy = par.vy-t*par.vx;
    par.vx += s*vpy;
    par.vy -= s*vpx;
    par.x += par.vx*dt;
    par.y += par.vy*dt;
}

我为Particle创建了1个n元素数组,为B创建了1个n元素浮点数组。它们在主机和cudaMemcpy上创建到设备。然后我检查了以下三个内核的性能:

__global__
void kernel_A(int n, int m, Particle* parlist, float* Blist)
{
    int i = blockIdx.x*blockDim.x + threadIdx.x;
    int j;
    if (i<n) {
        for (j=0;j<m;j++) {
            run(parlist[i], Blist[i]);
        }
    }
}

__global__
void kernel_B(int n, int m, Particle* parlist, float* Blist)
{
    int i = blockIdx.x*blockDim.x + threadIdx.x;
    int j;
    float B;
    if (i<n) {
        B = Blist[i];
        for (j=0;j<m;j++) {
            run(parlist[i], B);
        }
    }
}

__global__
void kernel_C(int n, int m, Particle* parlist, float* Blist)
{
    int i = blockIdx.x*blockDim.x + threadIdx.x;
    int j;
    float B;
    if (i<n) {
        B = Blist[i];
        for (j=0;j<m;j++) {
            run(parlist[i], B);
            __syncthreads();
        }
    }
}

__global__    
void kernel_D(int n, int m, Particle* parlist, float* Blist)
{
    int i = blockIdx.x*blockDim.x + threadIdx.x;
    int j;
    float B;
    if (i<n) {
        B = Blist[i];
    }
    for (j=0;j<m;j++) {
        if (i<n) {
            run(parlist[i], B);
        }
    }
}

__global__
void kernel_E(int n, int m, Particle* parlist, float* Blist)
{
    int i = blockIdx.x*blockDim.x + threadIdx.x;
    int j;
    float B;
    if (i<n) {
        for (j=0;j<m;j++) {
            run(parlist[i], Blist[i]);
            __syncthreads();
        }
    }
}

运行时间完全不同。对于n = 10,000,000且m = 1000:

  • Kernel_A:7.6s
  • Kernel_B:66ms
  • Kernel_C:9.9s
  • Kernel_D:10.0s
  • Kernel_E:10.0s

三个内核的结果完全相同且正确(根据CPU版本进行检查)。

我从官方的CUDA编程指南中了解到分支是昂贵的,因此kernel_C应该比kernel_B慢,但我怀疑差异是两个数量级。我不明白的是为什么kernel_B比kernel_A好得多。当kernel_A执行时,Kernel_B不必访问Blist 1000次,但是他们都需要访问parlist 1000次吗?为什么访问Blist这么慢?

Kernel_A,kernel_D和kernel_E有类似的性能,这让我更加困惑:所以与kernel_B相比的额外时间用于访问Blist或同步?

我想在PIC程序中实现kernel_B的性能。

1 个答案:

答案 0 :(得分:1)

不,无法在块之间进行同步。通常,内核调用带来的开销并不重要。我可以想象,你的内核不够大,无法在很大程度上利用你的设备。如果你想检查这个,你可以使用nvprof来分析你的程序并找出瓶颈。

实现快速PIC代码并不容易。您是否考虑使用PIConGPU等库?您可以在以下链接中找到它:https://github.com/ComputationalRadiationPhysics/picongpu