13

我正在尝试使用原子指令在 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;
}

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

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

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

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

我错过了什么?

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

nvcc -arch sm_11 -Ipath/to/cuda/C/common/inc XXX.cu
4

3 回答 3

21

好的,我想通了,这是另一个 cuda 范式的痛点。

正如任何优秀的 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);
        }
    } 
}
于 2010-01-07T15:06:16.393 回答
11

发帖人已经为自己的问题找到了答案。尽管如此,在下面的代码中,我提供了一个通用框架来实现 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));
}

代码说明

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

假设构造一个内核,其任务是计算线程网格的线程块数。一种可能的想法是让每个块中的每个线程都threadIdx.x == 0增加一个全局计数器。为了防止竞争条件,所有的增加都必须按顺序发生,因此它们必须合并到临界区中。

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

临界区由全局状态管理d_state。最初,状态是0。此外,两种__device__方法lockunlock可以改变这种状态。和方法只能由每个块内的单个线程调用,特别是由具有本地线程索引的线程lock调用。unlockthreadIdx.x == 0

在执行期间随机地,具有局部线程索引threadIdx.x == 0和全局线程索引的线程之一t将是第一个调用该lock方法的线程。特别是,它将推出atomicCAS(d_state, 0, 1). 由于最初d_state == 0, thend_state将被更新为1,atomicCAS将返回0并且线程将退出lock函数,传递给更新指令。在这样一个线程执行上述操作的同时,所有其他块的所有其他线程都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. 然而,这是另一个错误的临界区实现,导致死锁。事实上,我们记得warp 以同步方式运行,并且它们在每条指令后同步。因此,当我们执行时blockCountingKernelDeadlock,warp 中的一个线程,比如具有本地线程索引的线程t≠0,可能会锁定状态。在这种情况下,同一个 warp 中的其他线程t,包括 withthreadIdx.x == 0的线程,将执行与 thread 相同的 while 循环语句t,即同一个 warp 中的线程的执行步调一致。因此,所有线程都将等待某人解锁状态,但没有其他线程能够这样做,代码将陷入死锁。

于 2017-04-13T16:39:39.557 回答
3

顺便说一句,你必须记住全局内存写入和!读取没有完成你在代码中写它们的地方......所以为了实践你需要添加一个全局memfence,即__threadfence()

于 2010-01-20T15:34:14.147 回答