使用多个阵列减少共享内存的CUDA

时间:2016-02-25 16:30:15

标签: c++ arrays cuda

我目前正在使用以下Reduction函数来对CUDA中数组中的所有元素求和:

__global__ void reduceSum(int *input, int *input2, int *input3, int *outdata, int size){
    extern __shared__ int sdata[];

    unsigned int tID = threadIdx.x;
    unsigned int i = tID + blockIdx.x * (blockDim.x * 2);
    sdata[tID] = input[i] + input[i + blockDim.x];
    __syncthreads();

    for (unsigned int stride = blockDim.x / 2; stride > 32; stride >>= 1)
    {
        if (tID < stride)
        {
            sdata[tID] += sdata[tID + stride];
        }
        __syncthreads();
    }

    if (tID < 32){ warpReduce(sdata, tID); }

    if (tID == 0)
    {
        outdata[blockIdx.x] = sdata[0];
    }
}

但是,正如您从函数参数中可以看到的,我希望能够在一个简化函数中对三个单独的数组求和。现在显然,一个简单的方法是启动内核三次并每次传递一个不同的数组,这当然可以正常工作。我现在只是把它作为测试内核编写,真正的内核最终会得到一个结构数组,我需要为每个结构的所有X,Y和Z值执行添加,这就是为什么我需要在一个内核中总结它们。

我已为所有三个阵列进行了初始化和分配内存

    int test[1000];
    std::fill_n(test, 1000, 1);
    int *d_test;

    int test2[1000];
    std::fill_n(test2, 1000, 2);
    int *d_test2;

    int test3[1000];
    std::fill_n(test3, 1000, 3);
    int *d_test3;

    cudaMalloc((void**)&d_test, 1000 * sizeof(int));
    cudaMalloc((void**)&d_test2, 1000 * sizeof(int));
    cudaMalloc((void**)&d_test3, 1000 * sizeof(int));

我不确定我应该为这种内核使用什么网格和块尺寸,我不完全确定如何修改减少循环以根据需要放置数据,即 输出数组:

Block 1 Result|Block 2 Result|Block 3 Result|Block 4 Result|Block 5 Result|Block 6 Result|

      Test Array 1 Sums              Test Array 2 Sums            Test Array 3 Sums           

我希望这是有道理的。或者是否有更好的方法只有一个简化函数但能够返回Struct.X,Struct.Y或struct.Z的总和?

这是结构:

template <typename T>
struct planet {
    T x, y, z;
    T vx, vy, vz;
    T mass;
};

我需要添加所有VX并存储它,所有VY并存储它和所有VZ并存储它。

2 个答案:

答案 0 :(得分:4)

  

或者是否有更好的方法只有一个缩减函数但能够返回Struct.X,Struct.Y或struct.Z的总和?

通常,加速计算的主要焦点是速度。 GPU代码的速度(性能)通常在很大程度上取决于数据存储和访问模式。因此,尽管正如您在问题中指出的那样我们可以通过多种方式实现解决方案,但请注意应该相对较快的事情。

这样的减少不具有很多算术/操作强度,因此我们对性能的关注主要围绕数据存储以实现高效访问。访问全局内存时,GPU通常会以大块(32字节或128字节块)执行此操作。为了有效利用内存子系统,我们希望在每个请求中使用所请求的所有32或128个字节。

但结构隐含的数据存储模式:

template <typename T>
struct planet {
    T x, y, z;
    T vx, vy, vz;
    T mass;
};

几乎排除了这一点。对于此问题,您关心的是vxvyvz。这3个项目在给定结构(元素)中应该是连续的,但在这些结构的数组中,它们将被其他结构项目的必要存储空间分隔,至少:

planet0:       T x
               T y
               T z               ---------------
               T vx      <--           ^
               T vy      <--           |
               T vz      <--       32-byte read
               T mass                  |
planet1:       T x                     |
               T y                     v
               T z               ---------------
               T vx      <--
               T vy      <--
               T vz      <--
               T mass
planet2:       T x
               T y
               T z
               T vx      <--
               T vy      <--
               T vz      <--
               T mass

(例如,假设Tfloat

这指出了GPU中结构阵列(AoS)存储格式的主要缺点。由于GPU的访问粒度(32字节),从连续结构访问相同的元素是无效的。在这种情况下,通常的性能建议是将AoS存储转换为SoA(阵列结构):

template <typename T>
struct planets {
    T x[N], y[N], z[N];
    T vx[N], vy[N], vz[N];
    T mass[N];
};

以上只是一个可能的例子,可能不是你实际使用的例子,因为结构没有什么用处,因为我们只有一个N行星的结构。关键是,现在当我为连续的行星访问vx时,各个vx元素在内存中都是相邻的,所以32字节的读取给了我32个字节的vx数据,没有浪费或未使用的元素。

通过这种转换,从代码组织的角度来看,还原问题再次变得相对简单。您可以使用与单个数组缩减代码基本相同的内容,可以连续调用3次,也可以直接扩展内核代码,以便独立地处理所有3个数组。 A&#34; 3合1&#34;内核可能看起来像这样:

template <typename T>
__global__ void reduceSum(T *input_vx, T *input_vy, T *input_vz, T *outdata_vx, T *outdata_vy, T *outdata_vz, int size){
    extern __shared__ T sdata[];

    const int VX = 0;
    const int VY = blockDim.x;
    const int VZ = 2*blockDim.x;

    unsigned int tID = threadIdx.x;
    unsigned int i = tID + blockIdx.x * (blockDim.x * 2);
    sdata[tID+VX] = input_vx[i] + input_vx[i + blockDim.x];
    sdata[tID+VY] = input_vy[i] + input_vy[i + blockDim.x];
    sdata[tID+VZ] = input_vz[i] + input_vz[i + blockDim.x];
    __syncthreads();

    for (unsigned int stride = blockDim.x / 2; stride > 32; stride >>= 1)
    {
        if (tID < stride)
        {
            sdata[tID+VX] += sdata[tID+VX + stride];
            sdata[tID+VY] += sdata[tID+VY + stride];
            sdata[tID+VZ] += sdata[tID+VZ + stride];
        }
        __syncthreads();
    }

    if (tID < 32){ warpReduce(sdata+VX, tID); }
    if (tID < 32){ warpReduce(sdata+VY, tID); }
    if (tID < 32){ warpReduce(sdata+VZ, tID); }

    if (tID == 0)
    {
        outdata_vx[blockIdx.x] = sdata[VX];
        outdata_vy[blockIdx.x] = sdata[VY];
        outdata_vz[blockIdx.x] = sdata[VZ];
    }
}

(在浏览器中编码 - 未经过测试 - 仅仅是您所显示的&#34;参考内核&#34的扩展;)

上述AoS - &gt; SoA数据转换也可能在代码的其他地方带来性能优势。由于建议的内核将同时处理3个数组,因此网格和块维度应与完全相同,就像在单数组情况下用于参考内核的那样。共享内存存储需要增加(三倍)每个块。

答案 1 :(得分:1)

Robert Crovella给出了一个很好的答案,强调了AoS的重要性 - &gt; SoA布局转换通常可以提高GPU的性能,我只是想提出一个可能更方便的中间地带。 CUDA语言仅为您描述的目的提供了一些矢量类型(参见this section of the CUDA programming guide)。

例如,CUDA定义了int3,一种存储3个整数的数据类型。

 struct int3
 {
    int x; int y; int z;
 };

浮点数,字符,双精度等存在类似的类型。这些数据类型的优点是可以使用单个指令加载它们,这可以为您提供小的性能提升。有关此问题的讨论,请参阅this NVIDIA blog post。它也是一个更自然的&#34;这种情况的数据类型,它可能使代码的其他部分更容易使用。您可以定义,例如:

struct planets {
    float3 position[N];
    float3 velocity[N];
    int mass[N];
};

使用此数据类型的简化内核可能看起来像这样(改编自Robert的#)。

__inline__ __device__ void SumInt3(int3 const & input1, int3 const & input2, int3 & result)
{
    result.x = input1.x + input2.x;
    result.y = input1.y + input2.y;
    result.z = input1.z + input2.z;
}

__inline__ __device__ void WarpReduceInt3(int3 const & input, int3 & output, unsigned int const tID)
{
    output.x = WarpReduce(input.x, tID);
    output.y = WarpReduce(input.y, tID);
    output.z = WarpReduce(input.z, tID);    
}

__global__ void reduceSum(int3 * inputData, int3 * output, int size){
    extern __shared__ int3 sdata[];

    int3 temp;

    unsigned int tID = threadIdx.x;
    unsigned int i = tID + blockIdx.x * (blockDim.x * 2);

    // Load and sum two integer triplets, store the answer in temp.
    SumInt3(input[i], input[i + blockDim.x], temp);

    // Write the temporary answer to shared memory.
    sData[tID] = temp;

    __syncthreads();

    for (unsigned int stride = blockDim.x / 2; stride > 32; stride >>= 1)
    {
        if (tID < stride)
        {
            SumInt3(sdata[tID], sdata[tID + stride], temp);
            sData[tID] = temp;
        }
        __syncthreads();
    }

    // Sum the intermediate results accross a warp.
    // No need to write the answer to shared memory,
    // as only the contribution from tID == 0 will matter.
    if (tID < 32)
    {
        WarpReduceInt3(sdata[tID], tID, temp);
    }

    if (tID == 0)
    {
        output[blockIdx.x] = temp;
    }
}
相关问题