在CUDA中实现关键部分

时间:2010-01-07 14:44:15

标签: cuda synchronization 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;
}

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

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循环,因为它们将读取{ {1}} locks[id]。然后当线程完成关键部分时,它会将锁重置为1u,以便另一个线程可以进入。

我错过了什么?

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

0u

3 个答案:

答案 0 :(得分: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);
        }
    } 
}

答案 1 :(得分:5)

海报已经找到了他自己问题的答案。不过,在下面的代码中,我提供了一个在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可以更改此状态。 lockunlock方法只能由每个块中的单个线程调用,特别是由具有本地线程索引threadIdx.x == 0的线程调用。

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

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

答案 2 :(得分:3)

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