簡體   English   中英

多陣列共享內存上的CUDA減少

[英]CUDA Reduction on Shared Memory with Multiple Arrays

我目前正在使用以下歸約函數對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];
    }
}

但是,從函數參數可以看出,我希望能夠對一個歸約函數內的三個獨立的數組求和。 現在,顯然,執行此操作的一個簡單方法是啟動內核3次並每次傳遞不同的數組,這當然可以正常工作。 我現在只是將其編寫為測試內核,真正的內核最終將接受一個結構數組,並且我將需要對每個結構的所有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並將其存儲。

還是有更好的方法只具有一個歸約函數但能夠返回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 這三個項目在給定的結構(元素)中應該是連續的,但是在這些結構的數組中,它們將被其他結構項目的必要存儲區分開,至少:

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個數組。 “三合一”內核可能看起來像這樣:

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];
    }
}

(在瀏覽器中編碼-未經測試-只是對您顯示為“參考內核”的擴展)

上面的AoS-> SoA數據轉換也可能會在代碼中的其他地方帶來性能優勢。 由於建議的內核將一次處理3個陣列,因此網格和塊尺寸應在單陣列情況下用於參考內核的尺寸完全相同 每個塊的共享內存存儲將需要增加(三倍)。

Robert Crovella給出了一個很好的答案,突出了AoS-> SoA布局轉換的重要性,該轉換通常可以提高GPU的性能,我只想提出一個可能更方便的中間立場。 CUDA語言提供了幾種矢量類型,僅用於您描述的目的(請參閱CUDA編程指南的這一部分 )。

例如,CUDA定義了int3,這是一種存儲3個整數的數據類型。

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

浮點數,字符,雙精度數等也存在類似的類型。這些數據類型的優點是可以用一條指令加載它們,這可能會給您帶來很小的性能提升。 有關問題的討論,請參見NVIDIA博客文章 在這種情況下,它也是一種更“自然”的數據類型,它可能使代碼的其他部分更易於使用。 您可以定義,例如:

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

使用此數據類型的歸約內核可能看起來像這樣(改編自Robert's)。

__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;
    }
}

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM