[英]CUDA shuffle instruction reduction slower than shared memory reduction?
[英]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;
};
幾乎排除了這一點。 對於這個問題,您關心vx
, vy
和vz
。 這三個項目在給定的結構(元素)中應該是連續的,但是在這些結構的數組中,它們將被其他結構項目的必要存儲區分開,至少:
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
(為示例起見,假設T
為float
)
這指出了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.