需要帮助来解释一些CUDA性能结果

时间:2014-06-18 14:40:01

标签: performance cuda histogram

我们一直在CUDA GPU上尝试不同的直方图算法。我可以解释大多数结果,但我们注意到一些非常奇怪的功能,我不知道是什么导致它们。

内核

奇怪的事情发生在数据并行实现中。这意味着数据通过线程分发。每个线程查看数据的子集(理想情况下只是1),并将其贡献添加到全局内存中的直方图,这需要原子操作。

__global__ void histogram1(float *data, uint *hist, uint n, float xMin, float binWidth, uin\
t nBins)
{
    uint const nThreads = blockDim.x * gridDim.x;
    uint const tid = threadIdx.x + blockIdx.x * blockDim.x;

    uint idx = tid;
    while (idx < n)
    {
        float x = data[idx];
        uint bin = (x - xMin) / binWidth;
        atomicAdd(hist + bin, 1);

        idx += nThreads;
    }
}

作为第一个优化,每个块首先在共享内存中构建部分直方图,然后减少部分直方图以获得全局内存中的最终结果。代码非常简单,我相信它与Cuda By Example中使用的代码非常相似。

__global__ void histogram2(float *data, uint *hist, uint n, 
                           float xMin, float binWidth, uint nBins)
{
    extern __shared__ uint partialHist[]; // size = nBins * sizeof(uint)                   

    uint const nThreads = blockDim.x * gridDim.x;
    uint const tid = threadIdx.x + blockIdx.x * blockDim.x;

    // initialize shared memory to 0                                                             
    uint idx = threadIdx.x;
    while (idx < nBins)
    {
        partialHist[idx] = 0;
        idx += blockDim.x;
    }

    __syncthreads();

    // Calculate partial histogram (in shared mem)                                               
    idx = tid;
    while (idx < n)
    {
        float x = data[idx];
        uint bin = (x - xMin) / binWidth;
        atomicAdd(partialHist + bin, 1);

        idx += nThreads;
    }

    __syncthreads();

    // Compute resulting total (global) histogram                                                
    idx = threadIdx.x;
    while (idx < nBins)
    {
        atomicAdd(hist + idx, partialHist[idx]);
        idx += blockDim.x;
    }
}

结果

加速与n

我们对这两个内核进行了基准测试,以了解它们的行为与n的函数关系,后者是数据点的数量。数据随机分布均匀。在下图中,HIST_DP_1是未经优化的普通版本,而HIST_DP_2是使用共享内存来加快速度的版本:

Benchmarks of histogramming kernels, relative to CPU performance

时间相对于CPU性能而言,并且非常大的数据集会发生奇怪的事情。优化功能,而不是像未经优化的版本一样扁平化,开始再次(相对)改进。我们期望对于大型数据集,我们的卡的占用率将接近100%,这意味着从那时起,性能将线性扩展,如CPU(实际上是未经优化的蓝色曲线)。

这种行为可能是由于两个线程在共享/全局内存中对同一个bin执行原子操作的机会为大数据集变为零的事实,但在这种情况下我们预计会下降到在不同的nBins不同的地方。这不是我们观察到的,在所有三个面板中的下降是大约10 ^ 7个箱。这里发生了什么?一些复杂的缓存效果?或者我们错过了一些明显的东西?

加速与nBins

为了仔细研究作为箱数的函数的行为,我们将数据集修复为10 ^ 4(在一种情况下为10 ^ 5),并运行许多不同bin-number的算法。

Histogram benchmark as a function of nBins

作为参考,我们还生成了一些非随机数据。红色图表显示完美排序数据的结果,而浅蓝色线对应于每个值相同的数据集(原子操作中的最大拥塞)。问题很明显:那里的不连续性是什么?

系统设置

NVidia Tesla M2075, driver 319.37
Cuda 5.5
Intel(R) Xeon(R) CPU E5-2603 0 @ 1.80GHz

感谢您的帮助!

编辑:复制案例

根据要求:编译,可运行的复制案例。代码很长,这就是我没有首先包含它的原因。该代码段位于snipplr。为了让您的生活更轻松,我将包含一个小shell脚本来运行我使用的相同设置,以及Octave脚本来生成图表。

Shell脚本

#!/bin/bash                                                                                                                                                                                       
runs=100

# format: [n] [nBins] [t_cpu] [t_gpu1] [t_gpu2]                                                                                                                                                   
for nBins in 100 1000 10000
do
    for n in 10 50 100 200 500 1000 2000 5000 10000 50000 100000 500000 1000000 10000000 100000000
    do
        echo -n "$n $nBins "
        ./repro $n $nBins $runs
    done
done

八度脚本

T = load('repro.txt');

bins = unique(T(:,2));

t = cell(1, numel(bins));
for i = 1:numel(bins)
  t{i} = T(T(:,2) == bins(i), :);

  subplot(2, numel(bins), i);
  loglog(t{i}(:,1), t{i}(:,3:5))

  title(sprintf("nBins = %d", bins(i)));
  legend("cpu", "gpu1", "gpu2");

  subplot(2, numel(bins), i + numel(bins));
  loglog(t{i}(:,1), t{i}(:,4)./t{i}(:,3), ...
         t{i}(:,1), t{i}(:,5)./t{i}(:,3));

  title("relative");
  legend("gpu1/cpu", "gpu2/cpu");
end

绝对计时

绝对时间表明它不会降低CPU的速度。相反,GPU相对加速:

Absolute timings

1 个答案:

答案 0 :(得分:4)

关于问题1

  

这不是我们观察到的,在所有三个面板上的下降是大约10 ^ 7个箱。这里发生了什么?一些复杂的缓存效果?或者我们错过了一些明显的东西?

此下降是由于您在最大块数上设置的限制(1 <&lt; 14 == 16384)。在n = 10 ^ 7 gpuBench2时,限制已经开始,每个线程开始处理多个元素。在n = 10 ^ 8时,每个线程在12个(有时是11个)元素上工作。如果你删除这个上限,你可以看到你的表现继续平稳。

为什么这会更快?每个线程有多个元素可以更好地隐藏数据加载的延迟,特别是在10000个垃圾箱的情况下,由于共享内存使用率很高,您只能在每个SM上安装一个块。在这种情况下,块中的每个元素将在大约相同的时间到达全局负载,并且在它完成其加载之前都不能继续。通过使用多个元素,我们可以管理这些负载,每个线程获取许多元素,延迟为一个。

(你没有在gupBench1中看到这个,因为它不是延迟限制,而是绑定到L2的带宽。如果你将nvprof的输出导入到可视化分析器中,你可以很快看到这一点)

关于问题2

  

问题很明显:那里的不连续性是什么?

我手上没有Fermi,我无法在我的Kepler上重现这一点,所以我认为这是费米特有的。我认为这是用两部分回答问题的危险!