关于warp同步线程执行如何工作的直觉苦苦挣扎

时间:2013-12-04 18:16:59

标签: cuda parallel-processing gpu reduction

我是CUDA的新手。我正在使用基本的并行算法,如简化,以便了解线程执行的工作方式。我有以下代码:

__global__ void
Reduction2_kernel( int *out, const int *in, size_t N )
{
    extern __shared__ int sPartials[];
    int sum = 0;
    const int tid = threadIdx.x;
    for ( size_t i = blockIdx.x*blockDim.x + tid;
          i < N;
          i += blockDim.x*gridDim.x ) {
        sum += in[i];
    }
    sPartials[tid] = sum;
    __syncthreads();

    for ( int activeThreads = blockDim.x>>1;
              activeThreads > 32;
              activeThreads >>= 1 ) {
        if ( tid < activeThreads ) {
            sPartials[tid] += sPartials[tid+activeThreads];
        }
        __syncthreads();
    }
    if ( threadIdx.x < 32 ) {
        volatile int *wsSum = sPartials;
        if ( blockDim.x > 32 ) wsSum[tid] += wsSum[tid + 32]; // why do we need this statement, any exampele please?
        wsSum[tid] += wsSum[tid + 16];  //how these statements are executed in paralle within a warp
        wsSum[tid] += wsSum[tid + 8];
        wsSum[tid] += wsSum[tid + 4];
        wsSum[tid] += wsSum[tid + 2];
        wsSum[tid] += wsSum[tid + 1];
        if ( tid == 0 ) {
            volatile int *wsSum = sPartials;// why this statement is needed?
            out[blockIdx.x] = wsSum[0];
        }
    }
}

不幸的是,我不清楚代码是如何在if ( threadIdx.x < 32 )条件下工作的。有人可以用线程ID给出一个直观的例子以及如何执行语句吗?我认为理解这些概念很重要,所以任何帮助都会有所帮助!!

3 个答案:

答案 0 :(得分:3)

让我们看看块中的代码,并在此过程中回答您的问题:

int sum = 0;
const int tid = threadIdx.x;
for ( size_t i = blockIdx.x*blockDim.x + tid;
      i < N;
      i += blockDim.x*gridDim.x ) {
    sum += in[i];
}

上面的代码遍历大小为N的数据集。我们可以为理解目的做出的假设是N&gt; blockDim.x*gridDim.x,这最后一个术语只是网格中的线程总数。由于N大于总线程数,因此每个线程对数据集中的多个元素求和。从给定线程的角度来看,它是由线程的网格维度(blockDim.x*gridDim.x)间隔的求和元素。每个线程将其总和存储在名为{{1}的本地(可能是寄存器)变量中}。

sum

当每个线程完成时(即,因为它的for循环超过sPartials[tid] = sum; __syncthreads(); ),它将它的中间N存储在共享内存中,然后等待所有其他线程在块中完成。

sum

到目前为止,我们还没有谈到这个街区的规模 - 它并不重要。假设每个块都有32个线程的整数倍。下一步是开始将存储在共享内存中的各种中间和收集到越来越小的变量组中。上面的代码首先选择threadblock中的一半线程(for ( int activeThreads = blockDim.x>>1; activeThreads > 32; activeThreads >>= 1 ) { if ( tid < activeThreads ) { sPartials[tid] += sPartials[tid+activeThreads]; } __syncthreads(); } ),并使用这些线程中的每一个来组合共享内存中的两个部分和。因此,如果我们的threadblock开始于128个线程,我们只使用其中的64个线程将128个部分和减少为64个部分和。这个过程在for循环中重复进行,每次将线程切成两半并组合部分和,每个线程一次两个。只要blockDim.x>>1&gt;此过程就会继续。因此,如果activeThreads为64,那么这64个线程将128个部分和组合成64个部分和。但是当activeThreads变为32时, for循环终止,而不将64个部分和组合成32个。所以在这个代码块完成时,我们采取了(任意倍数) 32个线程)threadblock,并减少了我们开始使用的很多部分和,减少到64个。这个组合说256个部分和,128个部分和,64个部分和,必须在每次迭代时等待所有线程(多个warps)完成他们的工作,因此{for}循环的每次传递执行activeThreads语句。

请记住,此时,我们已将我们的主题块减少到64个部分总和。

__syncthreads();

对于此后的内核的剩余部分,我们将只使用前32个线程(即第一个warp)。所有其他线程将保持空闲状态。请注意,此后也没有if ( threadIdx.x < 32 ) { ,因为这违反了使用它的规则(所有线程必须参与__syncthreads();)。

__syncthreads();

我们现在正在创建一个指向共享内存的 volatile int *wsSum = sPartials; 指针。从理论上讲,这告诉编译器它不应该进行各种优化,例如将特定值优化到寄存器中。为什么我们之前不需要这个?因为volatile也带有a memory-fencing function__syncthreads();调用除了使所有线程在屏障上等待彼此之外,还会强制所有线程更新回到共享或全局内存中。但是,我们不能再依赖于此功能了,因为从现在开始我们不会使用__syncthreads();,因为我们已经将自己 - 对于内核的其余部分 - 限制为单个warp。

__syncthreads();

之前的减少块给我们留下了64个部分和。但是我们在这一点上限制自己使用32个线程。因此,在我们继续进行剩余的减少之前,我们必须再做一次组合,将64个部分和收集到32个部分和中。

    if ( blockDim.x > 32 ) wsSum[tid] += wsSum[tid + 32]; // why do we need this

现在我们终于进入了一些warp-synchronous编程。这行代码取决于32个线程以锁步方式执行的事实。要理解为什么(以及它如何工作),将它分解为完成这行代码所需的操作序列将是很方便的。它看起来像:

    wsSum[tid] += wsSum[tid + 16];  //how these statements are executed in paralle within a warp

所有32个线程将按锁定步骤遵循上述顺序。所有32个线程将首先将 read the partial sum of my thread into a register read the partial sum of the thread that is 16 higher than my thread, into a register add the two partial sums store the result back into the partial sum corresponding to my thread 读入(线程局部)寄存器。这意味着线程0读取wsSum[tid],线程1读取wsSum[0]等。之后,每个线程将另一个部分和读入另一个寄存器:线程0读取wsSum[1] ,主题1读取wsSum[16]等等。我们不关心wsSum[17](和更高)的值,这是真的。我们已将这些值折叠为前32个wsSum[32]值。但是,正如我们所看到的,只有前16个线程(在此步骤中)将对最终结果做出贡献,因此前16个线程将32个部分和组合成16个。接下来的16个线程将充当好吧,但他们只是做垃圾工作 - 它会被忽略。

上述步骤将32个部分和合并到wsSum[]中的前16个位置。下一行代码:

wsSum[]

以8的粒度重复此过程。同样,所有32个线程都是活动的,并且微序列是这样的:

    wsSum[tid] += wsSum[tid + 8];

因此前8个线程将前16个部分和( read the partial sum of my thread into a register read the partial sum of the thread that is 8 higher than my thread, into a register add the two partial sums store the result back into the partial sum corresponding to my thread )组合成8个部分和(包含在wsSum[0..15]中)。接下来的8个线程也将wsSum[0..7]合并到wsSum[8..23]但是在这些值被线程0读取后,对8..15的写入发生 ..8,因此有效数据不会被破坏。这只是额外的垃圾工作。同样,对于warp中的8个线程的其他块。所以在这一点上,我们将部分利息总和合并到8个地点。

wsSums[8..15]

这些代码行遵循与前两个类似的模式,将warp分为8组4个线程(只有第一个4线程组对最终结果有贡献),然后将warp分为16组2线程,只有第一个2线程组对最终结果有贡献。最后,分成32组,每组1个线程,每个线程产生一个部分和,只有第一个部分和是感兴趣的。

    wsSum[tid] += wsSum[tid + 4];  //this combines partial sums of interest into 4 locations
    wsSum[tid] += wsSum[tid + 2];  //this combines partial sums of interest into 2 locations
    wsSum[tid] += wsSum[tid + 1];  //this combines partial sums of interest into 1 location

最后,在上一步中,我们将所有部分总和减少到单个值。现在是时候将单个值写入全局内存了。我们完成了减少吗?也许,但可能不是。如果上面的内核只用1个线程块启动,那么我们就完成了 - 我们的最后一个&#34;部分&#34; sum实际上是数据集中所有元素的总和。但是如果我们启动了多个块,那么每个块的最终结果仍然是&#34; partial&#34;总和,所有块的结果必须加在一起(不知何故)。

回答你的最后一个问题?

  

我不知道为什么需要这种说法。

我的猜测是它是从还原内核的前一次迭代中遗留下来的,并且程序员忘记删除它,或者没有注意到它不是必需的。也许其他人会知道这个答案。

最后,cuda reduction sample为学习提供了非常好的参考代码,随附的pdf document很好地描述了可以在此过程中进行的优化。

答案 1 :(得分:0)

在前两个代码块之后(由__syncthreads()分隔),您可以在每个线程块中获得64个值(存储在每个线程块的sPartials []中)。因此if ( threadIdx.x < 32 )中的代码是在每个sPartials []中累积64个值。它只是为了优化减少的速度。因为累积的其余步骤的数据很小,所以减少线程和循环是不值得的。您只需在第二个代码块中调整条件

即可
for ( int activeThreads = blockDim.x>>1;
              activeThreads > 32;
              activeThreads >>= 1 )

for ( int activeThreads = blockDim.x>>1;
                  activeThreads > 0;
                  activeThreads >>= 1 )

而不是

if ( threadIdx.x < 32 ) {
        volatile int *wsSum = sPartials;
        if ( blockDim.x > 32 ) wsSum[tid] += wsSum[tid + 32]; 
        wsSum[tid] += wsSum[tid + 16]; 
        wsSum[tid] += wsSum[tid + 8];
        wsSum[tid] += wsSum[tid + 4];
        wsSum[tid] += wsSum[tid + 2];
        wsSum[tid] += wsSum[tid + 1];

为了更好地理解。

累积后,您只能获得每个sPartials []的一个值,并存储在sPartials [0]中,此处的代码为wsSum [0]。

在内核函数之后,您可以在CPU中的wsSum中累积值以获得最终结果。

答案 2 :(得分:0)

CUDA执行模型简而言之:计算在网格上的块之间划分。这些块可以有一些共享资源(共享内存)。

每个块都在单个流多处理器(SM)上执行,这使得快速共享内存成为可能。

每个块的工作再次分成32个线程的warp。您可以将warps所做的工作视为独立任务。 SM很快就在warp之间切换。例如,当一个线程访问全局内存时,SM将切换到另一个warp。

您对warp的执行顺序一无所知。所有你知道的是,在调用__syncthreads之后,所有线程都会运行到那一点,并且所有内存读写都已完成。

需要注意的重要一点是,warp中的所有线程都执行相同的指令,或者当存在分支并且不同的线程采用不同的分支时,某些线程可能会暂停。

因此,在缩小示例中,第一部分可以由多个warp执行。在最后一部分中,只剩下32个线程,因此只有一个warp处于活动状态。这条线

if ( blockDim.x > 32 ) wsSum[tid] += wsSum[tid + 32];

是否将其他经线计算出的部分和加到最终经线的部分和中。

下一行的工作原理如下。由于warp中的执行是同步的,因此可以安全地假设wsSum[tid]的写操作在下次读取之前完成,因此不需要进行__syncthreads调用。

volatile关键字让编译器知道wsSum数组中的值可能被其他线程更改,因此它将确保先前未读取wsSum[tid + X]的值,在上一条指令中的某个线程更新之前。

最后一个volatile声明似乎是多余的:您也可以使用现有的wsSum变量。