在CUDA中实现关键部分

Joh*_*ohn 13 synchronization cuda locking critical-section

我正在尝试使用原子指令在CUDA中实现一个关键部分,但我遇到了一些麻烦.我创建了测试程序来显示问题:

#include <cuda_runtime.h>
#include <cutil_inline.h>
#include <stdio.h>

__global__ void k_testLocking(unsigned int* locks, int n) {
    int id = threadIdx.x % n;
    while (atomicExch(&(locks[id]), 1u) != 0u) {} //lock
    //critical section would go here
    atomicExch(&(locks[id]),0u); //unlock
}

int main(int argc, char** argv) {
    //initialize the locks array on the GPU to (0...0)
    unsigned int* locks;
    unsigned int zeros[10]; for (int i = 0; i < 10; i++) {zeros[i] = 0u;}
    cutilSafeCall(cudaMalloc((void**)&locks, sizeof(unsigned int)*10));
    cutilSafeCall(cudaMemcpy(locks, zeros, sizeof(unsigned int)*10, cudaMemcpyHostToDevice));

    //Run the kernel:
    k_testLocking<<<dim3(1), dim3(256)>>>(locks, 10);

    //Check the error messages:
    cudaError_t error = cudaGetLastError();
    cutilSafeCall(cudaFree(locks));
    if (cudaSuccess != error) {
        printf("error 1: CUDA ERROR (%d) {%s}\n", error, cudaGetErrorString(error));
        exit(-1);
    }
    return 0;
}
Run Code Online (Sandbox Code Playgroud)

不幸的是,这段代码很难冻结我的机器几秒钟,最后退出,打印出来的消息:

fcudaSafeCall() Runtime API error in file <XXX.cu>, line XXX : the launch timed out and was terminated.
Run Code Online (Sandbox Code Playgroud)

这意味着其中一个while循环没有返回,但似乎这应该工作.

作为提醒,atomicExch(unsigned int* address, unsigned int val)以原子方式设置存储在地址中的内存位置valold值并返回该值.所以我的锁定机制背后的想法是它最初是0u,因此一个线程应该通过while循环,所有其他线程应该等待while循环,因为它们将读取locks[id]1u.然后当线程完成临界区时,它会重置锁,0u以便另一个线程可以进入.

我错过了什么?

顺便说一下,我正在编译:

nvcc -arch sm_11 -Ipath/to/cuda/C/common/inc XXX.cu
Run Code Online (Sandbox Code Playgroud)

Joh*_*ohn 20

好吧,我把它想出来了,这是另一种 - 最重要的 - 范例 - 痛苦.

正如任何优秀的cuda程序员都知道的那样(注意我不记得这使我成为一个糟糕的cuda程序员,我认为)warp中的所有线程必须执行相同的代码.如果不是因为这个事实,我写的代码将完美地工作.但是,实际上,同一个warp中可能有两个线程访问同一个锁.如果其中一个获取了锁,它只会忘记执行循环,但它不能继续循环,直到其warp中的所有其他线程都完成循环.不幸的是,其他线程永远不会完成,因为它正在等待第一个线程解锁.

这是一个内核,可以毫无错误地执行操作:

__global__ void k_testLocking(unsigned int* locks, int n) {
    int id = threadIdx.x % n;
    bool leaveLoop = false;
    while (!leaveLoop) {
        if (atomicExch(&(locks[id]), 1u) == 0u) {
            //critical section
            leaveLoop = true;
            atomicExch(&(locks[id]),0u);
        }
    } 
}
Run Code Online (Sandbox Code Playgroud)


Jac*_*ern 6

发布者已经找到了解决自己问题的答案。不过,在下面的代码中,我提供了一个通用框架来实现CUDA中的关键部分。更详细地,该代码执行块计数,但是可以很容易地修改以托管要在关键部分中执行的其他操作。下面,我还报告了一些代码解释,以及在CUDA关键部分的实现中存在一些“典型”错误。

代码

#include <stdio.h>

#include "Utilities.cuh"

#define NUMBLOCKS  512
#define NUMTHREADS 512 * 2

/***************/
/* LOCK STRUCT */
/***************/
struct Lock {

    int *d_state;

    // --- Constructor
    Lock(void) {
        int h_state = 0;                                        // --- Host side lock state initializer
        gpuErrchk(cudaMalloc((void **)&d_state, sizeof(int)));  // --- Allocate device side lock state
        gpuErrchk(cudaMemcpy(d_state, &h_state, sizeof(int), cudaMemcpyHostToDevice)); // --- Initialize device side lock state
    }

    // --- Destructor
    __host__ __device__ ~Lock(void) { 
#if !defined(__CUDACC__)
        gpuErrchk(cudaFree(d_state)); 
#else

#endif  
    }

    // --- Lock function
    __device__ void lock(void) { while (atomicCAS(d_state, 0, 1) != 0); }

    // --- Unlock function
    __device__ void unlock(void) { atomicExch(d_state, 0); }
};

/*************************************/
/* BLOCK COUNTER KERNEL WITHOUT LOCK */
/*************************************/
__global__ void blockCountingKernelNoLock(int *numBlocks) {

    if (threadIdx.x == 0) { numBlocks[0] = numBlocks[0] + 1; }
}

/**********************************/
/* BLOCK COUNTER KERNEL WITH LOCK */
/**********************************/
__global__ void blockCountingKernelLock(Lock lock, int *numBlocks) {

    if (threadIdx.x == 0) {
        lock.lock();
        numBlocks[0] = numBlocks[0] + 1;
        lock.unlock();
    }
}

/****************************************/
/* BLOCK COUNTER KERNEL WITH WRONG LOCK */
/****************************************/
__global__ void blockCountingKernelDeadlock(Lock lock, int *numBlocks) {

    lock.lock();
    if (threadIdx.x == 0) { numBlocks[0] = numBlocks[0] + 1; }
    lock.unlock();
}

/********/
/* MAIN */
/********/
int main(){

    int h_counting, *d_counting;
    Lock lock;

    gpuErrchk(cudaMalloc(&d_counting, sizeof(int)));

    // --- Unlocked case
    h_counting = 0;
    gpuErrchk(cudaMemcpy(d_counting, &h_counting, sizeof(int), cudaMemcpyHostToDevice));

    blockCountingKernelNoLock << <NUMBLOCKS, NUMTHREADS >> >(d_counting);
    gpuErrchk(cudaPeekAtLastError());
    gpuErrchk(cudaDeviceSynchronize());

    gpuErrchk(cudaMemcpy(&h_counting, d_counting, sizeof(int), cudaMemcpyDeviceToHost));
    printf("Counting in the unlocked case: %i\n", h_counting);

    // --- Locked case
    h_counting = 0;
    gpuErrchk(cudaMemcpy(d_counting, &h_counting, sizeof(int), cudaMemcpyHostToDevice));

    blockCountingKernelLock << <NUMBLOCKS, NUMTHREADS >> >(lock, d_counting);
    gpuErrchk(cudaPeekAtLastError());
    gpuErrchk(cudaDeviceSynchronize());

    gpuErrchk(cudaMemcpy(&h_counting, d_counting, sizeof(int), cudaMemcpyDeviceToHost));
    printf("Counting in the locked case: %i\n", h_counting);

    gpuErrchk(cudaFree(d_counting));
}
Run Code Online (Sandbox Code Playgroud)

代码说明

关键部分是必须由CUDA线程顺序执行的操作序列。

假设构造一个内核,该内核的任务是计算线程网格的线程块数。一种可能的想法是让每个块中的每个线程都有threadIdx.x == 0一个全局计数器。为了防止出现竞赛情况,所有增加必须按顺序进行,因此必须将其合并到关键部分。

上面的代码具有两个内核函数:blockCountingKernelNoLockblockCountingKernelLock。前者不使用关键部分来增加计数器,并且可以看到返回错误的结果。后者将反增量封装在一个关键部分,因此产生正确的结果。但是关键部分如何工作?

关键部分由全局状态控制d_state。最初,状态为0。此外,有两种__device__方法lockunlock可以更改此状态。的lockunlock方法只能由每个块内的单个线程和,特别是通过具有局部线程索引的线程的调用,threadIdx.x == 0

在执行期间,具有本地线程索引threadIdx.x == 0和全局线程索引的线程之一将随机t地首先调用该lock方法。特别是它将启动atomicCAS(d_state, 0, 1)。从最初开始d_state == 0,然后d_state将被更新为1atomicCAS将返回0,并且线程将退出lock函数,并传递给update指令。同时,这样的线程执行上述操作,所有其他块中的所有其他线程threadIdx.x == 0将执行该lock方法。但是,它们将找到d_state等于的值1,因此atomicCAS(d_state, 0, 1)将不执行任何更新并返回1,从而使这些线程运行while循环。在那个线程之后t完成更新,然后执行unlock功能,即atomicExch(d_state, 0)恢复d_state0。此时,随机地,另一个具有的线程threadIdx.x == 0将再次锁定状态。

上面的代码还包含第三个内核函数,即blockCountingKernelDeadlock。但是,这是关键部分的另一种错误实现,导致死锁。的确,我们记得扭曲在锁步中运行,并且它们在每条指令后都会同步。因此,当我们执行时blockCountingKernelDeadlock,线程束中的一个线程(例如具有本地线程索引的线程t?0)可能会锁定状态。在这种情况下,处于相同warp中的其他线程t(包括带有with的threadIdx.x == 0线程)将执行与thread相同的while循环语句t,即以锁步执行的处于相同warp中的线程的执行。因此,所有线程将等待某人解锁状态,但是其他线程将无法这样做,并且代码将陷入死锁。