繁体   English   中英

CUDA - 为什么基于warp的并行减少更慢?

[英]CUDA - why is warp based parallel reduction slower?

我有关于基于warp的并行缩减的想法,因为warp的所有线程都是按照定义同步的。

因此,我们的想法是输入数据可以减少64倍(每个线程减少两个元素),而不需要任何同步。

与Mark Harris的原始实现相同,减少应用于块级别,数据应用于共享内存。 http://gpgpu.org/static/sc2007/SC07_CUDA_5_Optimization_Harris.pdf

我创建了一个内核来测试他的版本和基于warp的版本。
内核本身完全相同地将BLOCK_SIZE元素存储在共享内存中,并将其结果输出到输出数组中的唯一块索引。

算法本身工作正常。 测试完整的一个数组以测试“计数”。

实现的功能体:

/**
 * Performs a parallel reduction with operator add 
 * on the given array and writes the result with the thread 0
 * to the given target value
 *
 * @param inValues T* Input float array, length must be a multiple of 2 and equal to blockDim.x
 * @param targetValue float 
 */
__device__ void reductionAddBlockThread_f(float* inValues,
    float &outTargetVar)
{
    // code of the below functions
}

1.执行他的版本:

if (blockDim.x >= 1024 && threadIdx.x < 512)
    inValues[threadIdx.x] += inValues[threadIdx.x + 512];
__syncthreads();
if (blockDim.x >= 512 && threadIdx.x < 256)
    inValues[threadIdx.x] += inValues[threadIdx.x + 256];
__syncthreads();
if (blockDim.x >= 256 && threadIdx.x < 128)
    inValues[threadIdx.x] += inValues[threadIdx.x + 128];
__syncthreads();
if (blockDim.x >= 128 && threadIdx.x < 64)
    inValues[threadIdx.x] += inValues[threadIdx.x + 64];
__syncthreads();

//unroll last warp no sync needed
if (threadIdx.x < 32)
{
    if (blockDim.x >= 64) inValues[threadIdx.x] += inValues[threadIdx.x + 32];
    if (blockDim.x >= 32) inValues[threadIdx.x] += inValues[threadIdx.x + 16];
    if (blockDim.x >= 16) inValues[threadIdx.x] += inValues[threadIdx.x + 8];
    if (blockDim.x >= 8) inValues[threadIdx.x] += inValues[threadIdx.x + 4];
    if (blockDim.x >= 4) inValues[threadIdx.x] += inValues[threadIdx.x + 2];
    if (blockDim.x >= 2) inValues[threadIdx.x] += inValues[threadIdx.x + 1];

    //set final value
    if (threadIdx.x == 0)
        outTargetVar = inValues[0];
}

Ressources:

使用4个syncthreads
如果使用了12个语句
11读取+添加+写入操作
1最后写操作
5注册用法

性能:

五次试运行平均值:~19.54 ms

2.基于Warp的方法:(与上面相同的功能体)

/*
 * Perform first warp based reduction by factor of 64
 *
 * 32 Threads per Warp -> LOG2(32) = 5
 *
 * 1024 Threads / 32 Threads per Warp = 32 warps
 * 2 elements compared per thread -> 32 * 2 = 64 elements per warp
 *
 * 1024 Threads/elements divided by 64 = 16
 * 
 * Only half the warps/threads are active
 */
if (threadIdx.x < blockDim.x >> 1)
{
    const unsigned int warpId = threadIdx.x >> 5;
    // alternative threadIdx.x & 31
    const unsigned int threadWarpId = threadIdx.x - (warpId << 5);
    const unsigned int threadWarpOffset = (warpId << 6) + threadWarpId;

    inValues[threadWarpOffset] += inValues[threadWarpOffset + 32];
    inValues[threadWarpOffset] += inValues[threadWarpOffset + 16];
    inValues[threadWarpOffset] += inValues[threadWarpOffset + 8];
    inValues[threadWarpOffset] += inValues[threadWarpOffset + 4];
    inValues[threadWarpOffset] += inValues[threadWarpOffset + 2];
    inValues[threadWarpOffset] += inValues[threadWarpOffset + 1];
}

// synchronize all warps - the local warp result is stored
// at the index of the warp equals the first thread of the warp
__syncthreads();

// use first warp to reduce the 16 warp results to the final one
if (threadIdx.x < 8)
{
    // get first element of a warp
    const unsigned int warpIdx = threadIdx.x << 6;

    if (blockDim.x >= 1024) inValues[warpIdx] += inValues[warpIdx + 512];
    if (blockDim.x >= 512) inValues[warpIdx] += inValues[warpIdx + 256];
    if (blockDim.x >= 256) inValues[warpIdx] += inValues[warpIdx + 128];
    if (blockDim.x >= 128) inValues[warpIdx] += inValues[warpIdx + 64];

    //set final value
    if (threadIdx.x == 0)
        outTargetVar = inValues[0];
}

Ressources:

使用了1个syncthread
7如果陈述
10读取添加写入操作
1最后写操作
5注册用法

5位移位
1加
1分

性能:

五次试运行平均值:~20.82 ms

Geforce 8800 GT 512 mb上多次测试两个内核,浮点值为256 mb。 并运行每块256个线程的内核(100%占用率)。

基于warp的版本慢了约1.28毫秒。

如果未来的卡允许更大的块大小,基于warp的方法仍然不需要进一步的同步语句,因为最大值是4096,它减少到64,最终扭曲减少到1

为什么它不会更快?或者内核的缺陷在哪里?

从资源使用来看,warp方法应该领先?

Edit1:纠正了内核,只有一半的线程处于活动状态而不会导致绑定读取,添加了新的性能数据

我认为你的代码比我的代码慢的原因是,在我的代码中,第一阶段中每个ADD的一半warp是活跃的。 在您的代码中,所有warp都在第一阶段的所有阶段都处于活动状态。 总的来说,您的代码执行更多的warp指令。 在CUDA中,重要的是要考虑执行的总“warp指令”,而不仅仅是一个warp执行的指令数。

此外,仅使用一半经线是没有意义的。 启动warp只是为了让它们评估两个分支并退出而产生开销。

另一个想法是使用unsigned charshort实际上可能会损害你的性能。 我不确定,但它肯定不会保存寄存器,因为它们没有打包成单个32位变量。

另外,在我的原始代码中,我用模板参数BLOCKDIM替换了blockDim.x,这意味着它只使用了5个运行时if语句(编译器消除了第二阶段的ifs)。

BTW,一种更便宜的计算你的threadWarpId

const int threadWarpId = threadIdx.x & 31;

您可以查看本文以获取更多想法。

编辑:这是一个替代的基于warp的块减少。

template <typename T, int level>
__device__
void sumReduceWarp(volatile T *sdata, const unsigned int tid)
{
  T t = sdata[tid];
  if (level > 5) sdata[tid] = t = t + sdata[tid + 32];
  if (level > 4) sdata[tid] = t = t + sdata[tid + 16];
  if (level > 3) sdata[tid] = t = t + sdata[tid +  8];
  if (level > 2) sdata[tid] = t = t + sdata[tid +  4];
  if (level > 1) sdata[tid] = t = t + sdata[tid +  2];
  if (level > 0) sdata[tid] = t = t + sdata[tid +  1];
}

template <typename T>
__device__
void sumReduceBlock(T *output, volatile T *sdata)
{
  // sdata is a shared array of length 2 * blockDim.x

  const unsigned int warp = threadIdx.x >> 5;
  const unsigned int lane = threadIdx.x & 31;
  const unsigned int tid  = (warp << 6) + lane;

  sumReduceWarp<T, 5>(sdata, tid);
  __syncthreads();

  // lane 0 of each warp now contains the sum of two warp's values
  if (lane == 0) sdata[warp] = sdata[tid];

  __syncthreads();

  if (warp == 0) {
    sumReduceWarp<T, 4>(sdata, threadIdx.x);
    if (lane == 0) *output = sdata[0];
  }
}

这应该快一点,因为它使用了在第一阶段启动的所有warp,并且在最后阶段没有分支,代价是新的中间阶段的额外分支,共享加载/存储和__syncthreads() 我还没有测试过这段代码。 如果您运行它,请告诉我它是如何执行的。 如果你在原始代码中使用blockDim的模板,它可能会再次更快,但我认为这段代码更简洁。

注意使用临时变量t是因为Fermi和后来的体系结构使用纯加载/存储体系结构,因此+=从共享内存到共享内存会导致额外的负载(因为sdata指针必须是易失性的)。 明确加载到临时一次避免这种情况。 在G80上,它对性能没有任何影响。

您还应该检查SDK中的示例。 我记得一个非常好的例子,它实现了几种缩减方式。 其中至少有一个也使用基于经线的缩减。

(我现在无法查找名称,因为我只在其他机器上安装了它)

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM