减少CUDA:翘曲展开(学校)

时间:2018-03-08 00:24:48

标签: cuda volatile reduction gpu-warp

我目前正在开展一个项目,我正在展开减少的最后一次扭曲。我已经完成了上面的代码;然而,一些修改是通过猜测完成的,我想解释原因。我编写的代码只是函数kernel4

// in is input array, out is where to store result, n is number of elements from in
// T is a float (32bit)
__global__ void kernel4(T *in, T *out, unsigned int n)

这是一种减少算法,其余代码已经提供。

代码:

#include <stdlib.h>
#include <stdio.h>

#include "timer.h"
#include "cuda_utils.h"

typedef float T;

#define N_ (8 * 1024 * 1024)
#define MAX_THREADS 256
#define MAX_BLOCKS 64

#define MIN(x,y) ((x < y) ? x : y)
#define tid threadIdx.x 
#define bid blockIdx.x 
#define bdim blockDim.x
#define warp_size 32

unsigned int nextPow2( unsigned int x ) {
    --x;
    x |= x >> 1;
    x |= x >> 2;
    x |= x >> 4;
    x |= x >> 8;
    x |= x >> 16;
    return ++x;
}

void getNumBlocksAndThreads(int whichKernel, int n, int maxBlocks, int maxThreads, int &blocks, int &threads)
{
    if (whichKernel < 3) {
        threads = (n < maxThreads) ? nextPow2(n) : maxThreads;
        blocks = (n + threads - 1) / threads;
    } else {
        threads = (n < maxThreads*2) ? nextPow2((n + 1)/ 2) : maxThreads;
        blocks = (n + (threads * 2 - 1)) / (threads * 2);
    }
    if (whichKernel == 5)
        blocks = MIN(maxBlocks, blocks);
}

T reduce_cpu(T *data, int n) {
    T sum = data[0];
    T c = (T) 0.0;
    for (int i = 1; i < n; i++)
    {
        T y = data[i] - c;
        T t = sum + y;
        c = (t - sum) - y;
        sum = t;
    } 
    return sum;
}

__global__ void
kernel4(T *in, T *out, unsigned int n)
{
    __shared__ volatile T d[MAX_THREADS];

    unsigned int i = bid * bdim + tid;

    n >>= 1;
    d[tid] = (i < n) ? in[i] + in[i+n] : 0;
    __syncthreads ();

    for(unsigned int s = bdim >> 1; s > warp_size; s >>= 1) {
        if(tid < s)
            d[tid] += d[tid + s];
        __syncthreads ();
    }

    if (tid < warp_size) {
        if (n > 64) d[tid] += d[tid + 32];
        if (n > 32) d[tid] += d[tid + 16];
        d[tid] += d[tid + 8];
        d[tid] += d[tid + 4];
        d[tid] += d[tid + 2];
        d[tid] += d[tid + 1];
    }

    if(tid == 0)
        out[bid] = d[0];
}

int main(int argc, char** argv)
{
    T *h_idata, h_odata, h_cpu;
    T *d_idata, *d_odata;   

    struct stopwatch_t* timer = NULL;
    long double t_kernel_4, t_cpu;

    int whichKernel = 4, threads, blocks, N, i;
    if(argc > 1) {
        N = atoi (argv[1]);
        printf("N: %d\n", N);
    } else {
        N = N_;
        printf("N: %d\n", N);
    }

    getNumBlocksAndThreads (whichKernel, N, MAX_BLOCKS, MAX_THREADS, blocks, threads);

    stopwatch_init ();
    timer = stopwatch_create ();

    h_idata = (T*) malloc (N * sizeof (T));
    CUDA_CHECK_ERROR (cudaMalloc (&d_idata, N * sizeof (T)));
    CUDA_CHECK_ERROR (cudaMalloc (&d_odata, blocks * sizeof (T)));

    srand48(time(NULL));
    for(i = 0; i < N; i++)
        h_idata[i] = drand48() / 100000;
    CUDA_CHECK_ERROR (cudaMemcpy (d_idata, h_idata, N * sizeof (T), cudaMemcpyHostToDevice));

    dim3 gb(blocks, 1, 1);
    dim3 tb(threads, 1, 1);

    kernel4 <<<gb, tb>>> (d_idata, d_odata, N);
    cudaThreadSynchronize ();

    stopwatch_start (timer);

    kernel4 <<<gb, tb>>> (d_idata, d_odata, N);
    int s = blocks;
    while(s > 1) {
        threads = 0;
        blocks = 0;
        getNumBlocksAndThreads (whichKernel, s, MAX_BLOCKS, MAX_THREADS, blocks, threads);

        dim3 gb(blocks, 1, 1);
        dim3 tb(threads, 1, 1);

        kernel4 <<<gb, tb>>> (d_odata, d_odata, s);
        s = (s + threads * 2 - 1) / (threads * 2);
    }
    cudaThreadSynchronize ();

    t_kernel_4 = stopwatch_stop (timer);
    fprintf (stdout, "Time to execute unrolled GPU reduction kernel: %Lg secs\n", t_kernel_4);

    double bw = (N * sizeof(T)) / (t_kernel_4 * 1e9);   // total bits / time
    fprintf (stdout, "Effective bandwidth: %.2lf GB/s\n", bw);
    CUDA_CHECK_ERROR (cudaMemcpy (&h_odata, d_odata, sizeof (T), cudaMemcpyDeviceToHost));

    stopwatch_start (timer);
    h_cpu = reduce_cpu (h_idata, N);
    t_cpu = stopwatch_stop (timer);
    fprintf (stdout, "Time to execute naive CPU reduction: %Lg secs\n", t_cpu);

    if(abs (h_odata - h_cpu) > 1e-5)
        fprintf(stderr, "FAILURE: GPU: %f  CPU: %f\n", h_odata, h_cpu);
    else
        printf("SUCCESS: GPU: %f  CPU: %f\n", h_odata, h_cpu);
    return 0;
}

我的第一个问题是:宣布时

__shared__ volatile T d[MAX_THREADS];

我想验证我对volatile的理解。 Volatile阻止编译器错误地优化我的代码,并承诺通过缓存而不仅仅是寄存器来完成加载/存储(如果错误,请纠正我)。为了减少,如果部分减少总和仍然存储在寄存器中,为什么这是一个问题呢?

我的第二个问题是:在进行实际的减少扭曲时

    if (tid < warp_size) { // Final log2(32) = 5 strides
        if (n > 64) d[tid] += d[tid + 32];
        if (n > 32) d[tid] += d[tid + 16];
        d[tid] += d[tid + 8];
        d[tid] += d[tid + 4];
        d[tid] += d[tid + 2];
        d[tid] += d[tid + 1];
    }

在没有(n> 64)和(n> 32)条件的情况下,减少和将产生不正确的结果。我得到的结果是:

FAILURE: GPU: 41.966557  CPU: 41.946209

通过5次试验,GPU减少始终产生0.0204的误差。我很担心这是一个浮点操作错误。

老实说,我的老师的助手建议这个更改添加(n> 64)和(n> 32)条件,但没有解释为什么它会修复代码。

由于我的试验中的n超过64,为什么这个条件会改变结果。我无法追溯问题,因为我不能像在CPU中那样使用打印功能。

2 个答案:

答案 0 :(得分:3)

在我们解决你的两个问题之前,我们先从一些前言评论开始:

  • 我鼓励您阅读NVIDIA的canonical reduction tutorial
  • 这样写的缩减做了几个假设,其中一个假设是块大小是2的幂(对于&#34;正确性&#34;)。
  • 您的代码在最终缩减阶段使用 warp-synchronous 编程。你似乎知道自己在做什么,所以我不会提供详细的描述,但这对于理解这一点肯定是相关的。如果需要,您可以谷歌并获取说明。它与下面的讨论有关,但我不打算在每种情况下都说出它的相关性。

好的,现在你的问题:

  

我想验证我对volatile的理解。 Volatile阻止编译器错误地优化我的代码,并承诺通过缓存而不仅仅是寄存器来完成加载/存储(如果错误,请纠正我)。为了减少,如果部分减少总和仍然存储在寄存器中,为什么这是一个问题呢?

关于volatile的定义,我会将您推荐给the CUDA programming guide。我已经看到摘要描述,因为它阻止了寄存器优化或阻止了加载和存储的重新排序。我更喜欢前者,并将其用作工作定义。

基本思想是volatile强制对该变量的任何引用(读或写)实际上转到内存子系统。我的意思是它将执行读或写,并且不会尝试使用先前加载到寄存器中的值。如果没有此限定符,编译器可以自由地从实际内存位置加载一次值(例如),然后在寄存器中保持该值(以及对它的任何更新),只要它认为合适。编译器着眼于性能,这样做。 (顺便说一句,请注意你在这里使用了&#34; cache&#34;这里我会避免这种用法。共享内存在它和处理器加载/存储机制之间没有插入缓存。)

在这种类型的warp-synchronous编码中没有volatile,如果我们允许编译器优化&#34;我们将遇到问题。 (即维持)中间值到寄存器中。这主要是由于线程间通信引起的。为了清楚地了解原因,让我们看一下最后一步减少的最后两步:

    d[tid] += d[tid + 2];
    d[tid] += d[tid + 1];

我们只考虑tid值为0-1的线程。在倒数第二步中,线程0将获取d[2]值并将其添加到d[0]值,而线程1将获取d[3]值并将其添加到d[1]值{1}}价值。此时,如果我们不使用volatile,编译器没有义务将线程1累积的d[1]值写回共享内存。允许将其保留在寄存器中。因此,在共享内存中看到的d[1]值不是&#34;最新的&#34;。

现在让我们进入最后一步。在此步骤中,线程0从共享内存中读取d[1] 并将其添加到d[0]值。但是如果没有volatile,我们在上一步中看到d[1]的共享内存内容不再准确。 OTOH,如果我们使用volatile,那么上一步中对共享内存的写入将实际发生,并且在最后一步中,线程0将在读取d[1]时获取正确的值。 CUDA线程是一个独立的模型。由此,我的意思是一个线程不能直接访问属于另一个线程的寄存器中包含的值。因此,warp级别的线程间通信通常可以通过共享内存或通过warp-shuffle操作来完成。

__syncthreads()具有类似的行为:它强制将所有类似寄存器优化的值写入内存,以便它们可见&#34;可见&#34;到块中的其他线程。因此,更复杂的优化是仅当减少从基于循环驱动volatile的减少切换到最终的经线同步减少时才切换到__syncthreads()限定指针。您可以在本答案开头链接的教程幻灯片中看到一个示例。

另外,在CUDA 9中,这种类型的经线同步编程(更正式)已被弃用。相反,您应该使用cooperative groups

  

如果没有(n> 64)和(n> 32)条件,减少总和将产生不正确的结果。

主要使用这些条件,因为代码设计为&#34;正确&#34;对于任何具有2次幂大小的块配置。如果我们假设块大小(每块的线程数)是2的幂,并且大于64,则它必须是128或更大。您的n变量以块大小开头,但随后乘以2:

n >>= 1;

因此,如果我们想确保这行代码的正确性:

d[tid] += d[tid + 32];

然后我们应该只在线程块大小为64(至少)时应用该操作,这就像说n大于64:

    if (n > 64) d[tid] += d[tid + 32];

关于此问题,声称如果包含if (n > 64),则发布的代码的行为会有所不同。原因是发布的代码包含一个循环,它会在减少进行时重新计算线程数和块数:

int s = blocks;
while(s > 1) {
    threads = 0;
    blocks = 0;
    getNumBlocksAndThreads (whichKernel, s, MAX_BLOCKS, MAX_THREADS, blocks, threads);

此循环最终导致块大小小于128,这意味着省略if条件会导致破损。 (在此循环期间,只需打印出threads变量)。

对此:

  

我无法追溯问题,因为我不能像在CPU中那样使用打印功能。

我不确定那里有什么问题。 printf应该在内核代码中工作。

答案 1 :(得分:0)

共享变量不能在其声明according to this answer中进行初始化。 因此,如果n <64,我们将一些随机共享内存数组数据添加到总和中,这将导致错误。