C++ 在CUDA中加倍缓冲,以便CPU可以对持久内核生成的数据进行操作

C++ 在CUDA中加倍缓冲,以便CPU可以对持久内核生成的数据进行操作,c++,concurrency,cuda,C++,Concurrency,Cuda,我有一个蒙特卡罗模拟,系统的状态是一个位串(大小为N),位被随机翻转。为了加速模拟,代码被修改为使用CUDA。然而,由于我需要从系统状态计算出大量统计数据(N^2),这部分需要在内存较多的CPU上完成。当前算法如下所示: loop CUDA kernel making 10s of Monte Carlo steps Copy system state back to CPU Calculate statistics 这是低效的,我想让内核持续运行,而CPU偶尔查询系统的状态,并在

我有一个蒙特卡罗模拟,系统的状态是一个位串(大小为N),位被随机翻转。为了加速模拟,代码被修改为使用CUDA。然而,由于我需要从系统状态计算出大量统计数据(N^2),这部分需要在内存较多的CPU上完成。当前算法如下所示:

loop
  CUDA kernel making 10s of Monte Carlo steps
  Copy system state back to CPU
  Calculate statistics
这是低效的,我想让内核持续运行,而CPU偶尔查询系统的状态,并在内核继续运行时计算统计数据

根据Tom对问题的回答,我认为答案是双重缓冲,但我还没有找到一个解释或例子来说明如何做到这一点


如何为CUDA/C++代码设置Tom答案第三段中描述的双缓冲?

这不是对您问题的直接回答,但可能会有所帮助

我正在使用一个CUDA生产者-消费者代码,它的基本结构似乎与您的类似。我希望通过使CPU和GPU同时运行来加快代码的速度。我试图通过重构代码来实现这一点,这是为什么

Launch kernel
Copy data
Loop
  Launch kernel
  CPU work
  Copy data
CPU work
这样,CPU可以在生成下一组数据时处理上一次内核运行的数据。这减少了我代码运行时间的30%。我想,如果GPU/CPU的工作能够平衡,那么它们所花费的时间大致相同,情况可能会变得更好


我仍然多次发布相同的内核。如果反复启动内核的开销很大,那么寻找一种方法来完成我用一次启动所完成的任务是值得的。否则,这可能是最好(最简单)的解决方案。

下面是一个完整的“持久”内核示例,生产者-消费者方法,从设备(生产者)到主机(消费者)具有双缓冲接口

持久性内核设计通常意味着启动内核时最多可以同时驻留在硬件上的块数(请参阅幻灯片16中的第1项)。为了最有效地使用机器,我们通常希望最大限度地利用这一点,同时仍保持在上述限制范围内。这涉及到对特定内核的占用率研究,它会因内核而异。因此,我选择在这里走捷径,只需启动尽可能多的块就可以了。这种方法总是可以保证工作的(它可以被认为是持久内核要启动的块数的“下限”),但(通常)不是机器的最有效使用。然而,我认为占用率研究与你的问题无关。此外,具有保证向前进度的适当“持久内核”设计实际上相当棘手-需要仔细设计CUDA线程代码和放置线程块(例如,每个SM仅使用1个线程块)以保证向前进度。但是,我们不需要深入到这一级别来解决您的问题(我不认为),我在这里提出的持久内核示例每个SM只放置1个线程块

我还假设设置正确,因此可以跳过在非UVA设置中安排正确映射内存分配的细节

基本的想法是,我们将在设备上有2个缓冲区,以及2个“邮箱”,每个缓冲区一个。设备内核将用数据填充缓冲区,然后将“邮箱”设置为一个值(本例中为2),该值指示主机可能“使用”缓冲区。然后,设备转到另一个缓冲区,并在缓冲区之间以乒乓方式重复该过程。为了实现这一点,我们必须确保设备本身没有溢出缓冲区(任何线程都不允许在任何其他线程之前超过一个缓冲区),并且在设备填充缓冲区之前,主机已经消耗了以前的内容

在主机端,它只是等待邮箱指示“已满”,然后将缓冲区从设备复制到主机,重置邮箱,并对其执行“处理”(验证功能)。然后它以乒乓球的方式进入下一个缓冲区。设备的实际数据“生产”只是用迭代编号填充每个缓冲区。然后主机检查是否收到了正确的迭代编号

我已经构建了调用实际设备“工作”函数(
my_compute_函数
)的代码,您可以在其中放置任何蒙特卡罗代码。如果您的代码很好地独立于线程,那么这应该很简单。因此,设备端
my_compute_函数
是生产者函数,主机端
validate
是消费者函数。如果您的设备生产者代码不是简单的线程独立的,那么您可能需要围绕调用点稍微重新构造一些东西,以
my\u compute\u function

这样做的净效果是,当主机“消耗”前一个缓冲区中的数据时,设备可以“抢先”并开始填充下一个缓冲区

由于持久性内核设计对内核启动中的块(和线程)数量施加了上限,因此我选择在跨网格循环中实现“work”producer函数,以便根据给定的网格宽度处理任意大小的缓冲区


下面是一个充分发挥作用的示例:

$ cat t942.cu
#include <stdio.h>

#define ITERS 1000
#define DSIZE 65536
#define nTPB 256

#define cudaCheckErrors(msg) \
    do { \
        cudaError_t __err = cudaGetLastError(); \
        if (__err != cudaSuccess) { \
            fprintf(stderr, "Fatal error: %s (%s at %s:%d)\n", \
                msg, cudaGetErrorString(__err), \
                __FILE__, __LINE__); \
            fprintf(stderr, "*** FAILED - ABORTING\n"); \
            exit(1); \
        } \
    } while (0)


__device__ volatile int blkcnt1 = 0;
__device__ volatile int blkcnt2 = 0;
__device__ volatile int itercnt = 0;

__device__ void my_compute_function(int *buf, int idx, int data){
  buf[idx] = data;  // put your work code here
}

__global__ void testkernel(int *buffer1, int *buffer2, volatile int *buffer1_ready, volatile int *buffer2_ready,  const int buffersize, const int iterations){
  // assumption of persistent block-limited kernel launch
  int idx = threadIdx.x+blockDim.x*blockIdx.x;
  int iter_count = 0;
  while (iter_count < iterations ){ // persistent until iterations complete
    int *buf = (iter_count & 1)? buffer2:buffer1; // ping pong between buffers
    volatile int *bufrdy = (iter_count & 1)?(buffer2_ready):(buffer1_ready);
    volatile int *blkcnt = (iter_count & 1)?(&blkcnt2):(&blkcnt1);
    int my_idx = idx;
    while (iter_count - itercnt > 1); // don't overrun buffers on device
    while (*bufrdy == 2);  // wait for buffer to be consumed
    while (my_idx < buffersize){ // perform the "work"
      my_compute_function(buf, my_idx, iter_count);
      my_idx += gridDim.x*blockDim.x; // grid-striding loop
      }
    __syncthreads(); // wait for my block to finish
    __threadfence(); // make sure global buffer writes are "visible"
    if (!threadIdx.x) atomicAdd((int *)blkcnt, 1); // mark my block done
    if (!idx){ // am I the master block/thread?
      while (*blkcnt < gridDim.x);  // wait for all blocks to finish
      *blkcnt = 0;
      *bufrdy = 2;  // indicate that buffer is ready
      __threadfence_system(); // push it out to mapped memory
      itercnt++;
      }
    iter_count++;
    }
}

int validate(const int *data, const int dsize, const int val){

  for (int i = 0; i < dsize; i++) if (data[i] != val) {printf("mismatch at %d, was: %d, should be: %d\n", i, data[i], val); return 0;}
  return 1;
}

int main(){

  int *h_buf1, *d_buf1, *h_buf2, *d_buf2;
  volatile int *m_bufrdy1, *m_bufrdy2;
  // buffer and "mailbox" setup
  cudaHostAlloc(&h_buf1, DSIZE*sizeof(int), cudaHostAllocDefault);
  cudaHostAlloc(&h_buf2, DSIZE*sizeof(int), cudaHostAllocDefault);
  cudaHostAlloc(&m_bufrdy1, sizeof(int), cudaHostAllocMapped);
  cudaHostAlloc(&m_bufrdy2, sizeof(int), cudaHostAllocMapped);
  cudaCheckErrors("cudaHostAlloc fail");
  cudaMalloc(&d_buf1, DSIZE*sizeof(int));
  cudaMalloc(&d_buf2, DSIZE*sizeof(int));
  cudaCheckErrors("cudaMalloc fail");
  cudaStream_t streamk, streamc;
  cudaStreamCreate(&streamk);
  cudaStreamCreate(&streamc);
  cudaCheckErrors("cudaStreamCreate fail");
  *m_bufrdy1 = 0;
  *m_bufrdy2 = 0;
  cudaMemset(d_buf1, 0xFF, DSIZE*sizeof(int));
  cudaMemset(d_buf2, 0xFF, DSIZE*sizeof(int));
  cudaCheckErrors("cudaMemset fail");
  // inefficient crutch for choosing number of blocks
  int nblock = 0;
  cudaDeviceGetAttribute(&nblock, cudaDevAttrMultiProcessorCount, 0);
  cudaCheckErrors("get multiprocessor count fail");
  testkernel<<<nblock, nTPB, 0, streamk>>>(d_buf1, d_buf2, m_bufrdy1, m_bufrdy2, DSIZE, ITERS);
  cudaCheckErrors("kernel launch fail");
  volatile int *bufrdy;
  int *hbuf, *dbuf;
  for (int i = 0; i < ITERS; i++){
    if (i & 1){  // ping pong on the host side
      bufrdy = m_bufrdy2;
      hbuf = h_buf2;
      dbuf = d_buf2;}
    else {
      bufrdy = m_bufrdy1;
      hbuf = h_buf1;
      dbuf = d_buf1;}
    // int qq = 0; // add for failsafe - otherwise a machine failure can hang
    while ((*bufrdy)!= 2); // use this for a failsafe:  if (++qq > 1000000) {printf("bufrdy = %d\n", *bufrdy); return 0;} // wait for buffer to be full;
    cudaMemcpyAsync(hbuf, dbuf, DSIZE*sizeof(int), cudaMemcpyDeviceToHost, streamc);
    cudaStreamSynchronize(streamc);
    cudaCheckErrors("cudaMemcpyAsync fail");
    *bufrdy = 0; // release buffer back to device
    if (!validate(hbuf, DSIZE, i)) {printf("validation failure at iter %d\n", i); exit(1);}
    }
 printf("Completed %d iterations successfully\n", ITERS);
}


$ nvcc -o t942 t942.cu
$ ./t942
Completed 1000 iterations successfully
$
$cat t942.cu
#包括
#定义ITERS 1000
#定义DSIZE65536
#定义nTPB 256
#定义cudaCheckErrors(msg)\
做{\
cudaError\u t\u err=cudaGetLastError()\
如果(_err!=cudaSuccess){\
fprintf(标准,“致命错误:%s(%s位于%s:%d)\n”\
msg,cudaGetErrorString(_err)\
__文件(行)\
fprintf(stderr,“***失败-中止\n”)\