使用 Cuda 进行有效的连续松弛?

计算科学 并行计算 显卡 库达
2021-12-18 16:19:08

我最近使用 Cuda 实现了 Successive Over Relaxation 作为我的课程项目的一部分,我很想知道如何使代码更高效。

我正在使用红/黑 SOR 方案,这是并行 SOR 的流行版本。要更新网格点的值,我需要访问 4 个相邻的值,这些值在内存中通常不连续,因此 GPU 无法使用 cuda 的内存合并功能,这使得算法内存受限。为了克服这个问题,我将奇数和偶数网格点的存储重新排序在单独的向量中,如本文所述:http: //link.springer.com/chapter/10.1007%2F978-3-642-31464-3_60这需要内存合并的优势。

实施论文中提到的技术,我发现与 1 个 CPU 相比,速度提高了约 15 倍(与传统的红/黑 SOR 相比约 10 倍)。我应该期待多少性能加速?我觉得〜15的加速非常低?

我正在调用内核函数以以下方式从主机 CPU 依次更新奇数值和偶数值:

for(size_t it = 0; it < itmax; it++){
      cuda_odd_update <<<dimGrid_odd,dimBlock>>> (odd,even);
      cuda_even_update <<<dimGrid_even,dimBlock>>> (odd,even);
}

其中oddeven是指向设备内存中重新排序的向量的指针。我正在使用这种方法,因为我不知道有什么方法可以同步设备中的所有线程。有没有更好的方法来实现执行循环?

下面的代码实现了论文中提到的技术,而不是传统的红/黑 SOR:

__global__ void cuda_odd_update(double* odd,double* even){

  size_t tx = blockIdx.x*blockDim.x + threadIdx.x;
  size_t ty = blockIdx.y*blockDim.y + threadIdx.y;
  size_t odd_index = tx*height_odd+ty;
  size_t even_index = tx*height_even+ty;

  if (( (ty == 0 && tx%2 == 0) || (ty == height_odd-1 && tx%2 == 1) || (ty > 0 && ty < height_odd-1) ) && (tx > 0 && tx < width-1 && ty < height_odd) ){

      odd[odd_index] = (1.0 - omega)*odd[odd_index] + omega/(2*(1+beta))
                     * ( (tx%2)*even[even_index]+(1-tx%2)*even[even_index+1]
                       + (tx%2)*even[even_index-1]+(1-tx%2)*even[even_index]
                       + beta * ( even[even_index-height_even]
                                + even[even_index+height_even]   )) ;

  }

}


__global__ void cuda_even_update(double* odd,double* even){

  size_t tx = blockIdx.x*blockDim.x + threadIdx.x;
  size_t ty = blockIdx.y*blockDim.y + threadIdx.y;
  size_t odd_index = tx*height_odd+ty;
  size_t even_index = tx*height_even+ty;

  if(( (ty == 0 & tx%2 == 1) || (ty == height_even-1 && tx%2 == 0) || (ty > 0 && ty < height_even-1) ) && ( tx > 0 && tx < width-1 && ty < height_even)){


      even[even_index] = (1.0 - omega)*even[even_index] + omega/(2*(1+beta))
                       * ( (1-tx%2)*odd[odd_index] + (tx%2)*odd[odd_index+1]
                         + (1-tx%2)*odd[odd_index-1] + (tx%2)*odd[odd_index]
                         + beta * ( odd[odd_index-height_odd]
                                  + odd[odd_index+height_odd]  )) ;


  }

}

我目前正在使用 Intel Xeon CPU E5-2660-2.20GHz(8 核)和 NVIDIA TESLA M2090。给出以下nvprof结果(1000 次迭代):

CPU time : 62.69
==27665== NVPROF is profiling process 27665, command: ./sor
GPU time : 3.73
End!
==27665== Profiling application: ./sor
==27665== Profiling result:
Time(%)      Time     Calls       Avg       Min       Max  Name
 49.81%  1.83752s      1000  1.8375ms  1.8333ms  1.8422ms  cuda_even_update(double*, double*)
 49.67%  1.83226s      1000  1.8323ms  1.8290ms  1.8364ms  cuda_odd_update(double*, double*)
  0.26%  9.6281ms         2  4.8141ms  4.6306ms  4.9975ms  [CUDA memcpy DtoH]
  0.26%  9.4569ms         2  4.7284ms  4.7065ms  4.7504ms  [CUDA memcpy HtoD]
1个回答

同步。 您无法在同一网格中的不同块之间同步线程。另一方面,如果您按照您的方式执行内核,则它们可以保证按顺序执行,因此其中的所有线程都将看到同一迭代中even_update所做的所有更新。odd_update(请参阅NVIDIA 自己的文档。)

据我所知,没有什么好的方法可以在整个内核中同步线程,例如,请参阅此讨论),因此让主循环按顺序执行内核似乎很好。你可以测量

加速。 相对于 CPU 的加速并不是一个有用的性能指标。我知道它总是被报告,等等,但这可能有点误导。例如,有人使用相同的 GPU 和相同的内存,但更快的 CPU 将获得较低的加速,但这并不能说明他们的代码性能。NVIDIA 最佳实践指南关于如何衡量代码的性能有一些话要说。由于您的算法受内存带宽限制(每次内存访问执行的操作数量相对较少),我要说的数量比加速更重要的是内存吞吐量,即程序实际读取的每秒字节数/写入(有效带宽),尤其是作为 GPU 理论峰值带宽的一部分。了解由于合并要求而导致内核读取/写入的内存量比严格要求的内存量也有帮助。

你试过测量这些东西吗?相对于 CPU 的加速比,理论峰值带宽的分数对于优化来说是更有用的指导。

表现。 正如@GodricSeer指出的那样,执行大量小内核可能是个问题。

立即尝试的一件事是为每个线程分配更多工作。所以不是每个线程都为一个索引工作j, 让每个线程为m指数j0:m1, 适当地展开,以便在每一步都有连续的线程到连续索引的工作。这将调度程序必须完成的工作量减少了一个因子m.

另一个问题可能与诸如even[even_index±1],其中当前线程可能正在读取已被同一块中的线程读取的内存。我还没有运行你的代码,但要尝试的一件事是加载部分even进入共享内存,以便线程与threadIdx.y区别于1可以使用对方的全局内存读取。如果块的大小太大,则可能存在使用多少共享内存的问题,因此需要仔细的显式测试。

当您在代码上运行 NVIDIA 的分析器时,它是否会突出显示任何有趣的问题?