将函子应用于设备数组子集的最有效方法是什么?

What is the most efficient way to apply a functor to a subset of a device array?

我正在重写一个库,该库对存储在连续内存块中的数据执行计算和其他操作,以便它可以在使用 CUDA 框架的 GPU 上工作。数据表示存在于 4 维网格中的信息。网格的总大小可以从数千个到数百万个网格点不等。沿着每个方向,网格可能有少至 8 个或多至 100 个点。我的问题是关于在网格的子集上实施操作的最佳方式是什么。例如,假设我的网格是 [0,nx)x[0,ny)x[0,nz)x[0,nq),我想实现一个变换,将所有索引属于 [1 的点相乘,nx-1)x[1,ny-1)x[1,nz-1)x[0,nq-1) 减 1.

现在,我所做的是通过嵌套循环。这是代码的骨架

{ 
int nx,ny,nz,nq;
nx=10,ny=10,nz=10,nq=10;
typedef thrust::device_vector<double> Array;
Array A(nx*ny*nz*nq);
thrust::fill(A.begin(), A.end(), (double) 1);

for (auto q=1; q<nq-1; ++q){
for (auto k=1; k<nz-1; ++k){
for (auto j=1; j<ny-1; ++j){
int offset1=+1+j*nx+k*nx*ny+q*nx*ny*nz;
int offset2=offset1+nx-2;
thrust::transform(A.begin()+offset1, 
                  A.begin()+offset2, 
                  thrust::negate<double>());
      }
    }
  }
}

但是,我想知道这是否是最有效的方法,因为在我看来,在这种情况下最多只能同时 运行 nx-2 个线程。所以我在想,也许更好的方法是生成一个序列迭代器(返回数组中的线性位置),使用 zip 迭代器将其压缩到数组中,并定义一个仿函数来检查元组的第二个元素(位置值),如果该值落在可接受的范围内,则修改元组的第一个元素。但是,可能有更好的方法来做到这一点。我是 CUDA 的新手,更糟糕的是,我真的很喜欢 Fortran,所以我很难跳出 for-loop 框思考...

我不确定有效的方法是什么。我可以提出我认为比您的框架代码更有效的建议。

你在文中的提议是朝着正确的方向前进的。我们不应该使用一组可能会迭代很多次的嵌套 for 循环,而应该寻求在一次 thrust 调用中完成所有事情。但是我们仍然需要让那个 thrust 调用只修改要操作的 "cubic" 体积内索引处的数组值。

但是,我们不想使用涉及根据有效索引量测试生成的索引的方法,正如您所建议的那样。这将需要我们启动一个与我们的阵列一样大的网格,即使我们只想修改它的一小部分。

相反,我们启动了一个大到足以覆盖需要修改的元素数量的操作,并且我们创建了一个仿函数来执行线性索引 -> 4D 索引 -> 调整后的线性索引转换。然后,该仿函数在变换迭代器内运行,将从 0、1、2 等开始的普通线性序列转换为开始并停留在要修改的体积内的序列。然后将置换迭代器与此修改后的序列一起使用 select 要修改的数组的值。

下面的示例显示了嵌套循环方法 (1) 与我的 (2) 对于 64x64x64x64 数组和修改后的 62x62x62x62 体积的时间差异:

$ cat t39.cu
#include <thrust/device_vector.h>
#include <thrust/transform.h>
#include <thrust/iterator/permutation_iterator.h>
#include <thrust/iterator/zip_iterator.h>
#include <thrust/iterator/counting_iterator.h>
#include <thrust/functional.h>
#include <thrust/equal.h>
#include <cassert>
#include <iostream>

struct my_idx
{
  int nx, ny, nz, nq, lx, ly, lz, lq, dx, dy, dz, dq;
  my_idx(int _nx, int _ny, int _nz, int _nq, int _lx, int _ly, int _lz, int _lq, int _hx, int _hy, int _hz, int _hq) {
    nx = _nx;
    ny = _ny;
    nz = _nz;
    nq = _nq;
    lx = _lx;
    ly = _ly;
    lz = _lz;
    lq = _lq;
    dx = _hx - lx;
    dy = _hy - ly;
    dz = _hz - lz;
    dq = _hq - lq;
    // could do a lot of assert checking here
  }

  __host__ __device__
  int operator()(int idx){
    int rx = idx / dx;
    int ix = idx - (rx * dx);
    int ry = rx / dy;
    int iy = rx - (ry * dy);
    int rz = ry / dz;
    int iz = ry - (rz * dz);
    int rq = rz / dq;
    int iq = rz - (rq * dq);
    return (((iq+lq)*nz+iz+lz)*ny+iy+ly)*nx+ix+lx;
  }
};

#include <time.h>
#include <sys/time.h>
#define USECPSEC 1000000ULL

unsigned long long dtime_usec(unsigned long long start){

  timeval tv;
  gettimeofday(&tv, 0);
  return ((tv.tv_sec*USECPSEC)+tv.tv_usec)-start;
}


int main()
{
  int nx,ny,nz,nq,lx,ly,lz,lq,hx,hy,hz,hq;
  nx=64,ny=64,nz=64,nq=64;
  lx=1,ly=1,lz=1,lq=1;
  hx=nx-1,hy=ny-1,hz=nz-1,hq=nq-1;
  thrust::device_vector<double> A(nx*ny*nz*nq);
  thrust::device_vector<double> B(nx*ny*nz*nq);
  thrust::fill(A.begin(), A.end(), (double) 1);
  thrust::fill(B.begin(), B.end(), (double) 1);
  // method 1
  unsigned long long m1_time = dtime_usec(0);
  for (auto q=lq; q<hq; ++q){
    for (auto k=lz; k<hz; ++k){
      for (auto j=ly; j<hy; ++j){
        int offset1=lx+j*nx+k*nx*ny+q*nx*ny*nz;
        int offset2=offset1+(hx-lx);
        thrust::transform(A.begin()+offset1,
                  A.begin()+offset2, A.begin()+offset1,
                  thrust::negate<double>());
      }
    }
  }
  cudaDeviceSynchronize();
  m1_time = dtime_usec(m1_time);

  // method 2
  unsigned long long m2_time = dtime_usec(0);
  auto p = thrust::make_permutation_iterator(B.begin(), thrust::make_transform_iterator(thrust::counting_iterator<int>(0), my_idx(nx, ny, nz, nq, lx, ly, lz, lq, hx, hy, hz, hq)));
  thrust::transform(p, p+(hx-lx)*(hy-ly)*(hz-lz)*(hq-lq), p, thrust::negate<double>());
  cudaDeviceSynchronize();
  m2_time = dtime_usec(m2_time);
  if (thrust::equal(A.begin(), A.end(), B.begin()))
    std::cout << "method 1 time: " << m1_time/(float)USECPSEC << "s method 2 time: " << m2_time/(float)USECPSEC << "s" << std::endl;
  else
    std::cout << "mismatch error" << std::endl;
}
$ nvcc -std=c++11 t39.cu -o t39
$ ./t39
method 1 time: 1.6005s method 2 time: 0.013182s
$