![](/img/trans.png)
[英]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.