我正在尝试使用原子指令在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
答案 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
增加一个全局计数器。为了防止竞争条件,所有增加必须按顺序进行,因此必须将它们合并到一个关键部分。
上面的代码有两个核心函数:blockCountingKernelNoLock
和blockCountingKernelLock
。前者不使用临界区来增加计数器,并且可以看出,返回错误的结果。后者将计数器增加封装在临界区内,因此产生正确的结果。但是关键部分如何运作?
关键部分由全局状态d_state
管理。最初,州是0
。此外,两个__device__
方法lock
和unlock
可以更改此状态。 lock
和unlock
方法只能由每个块中的单个线程调用,特别是由具有本地线程索引threadIdx.x == 0
的线程调用。
在执行期间,随机地,其中一个具有本地线程索引threadIdx.x == 0
和全局线程索引的线程(例如t
)将是第一个调用lock
方法的线程。特别是,它将启动atomicCAS(d_state, 0, 1)
。从最初d_state == 0
开始,d_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_state
恢复为0
。此时,随机,另一个threadIdx.x == 0
的线程将再次锁定状态。
上面的代码还包含第三个核函数,即blockCountingKernelDeadlock
。但是,这是关键部分的另一个错误实现,导致死锁。实际上,我们记得经线是在一步一步运转的,并且它们在每次指令后都会同步。因此,当我们执行blockCountingKernelDeadlock
时,warp中的一个线程(比如具有本地线程索引t≠0
的线程)可能会锁定状态。在这种情况下,同一warp t
中的其他线程,包括threadIdx.x == 0
的线程,将执行与线程t
相同的while循环语句,同时执行线程{{1}}经历了一步一步的扭曲。因此,所有线程都会等待某人解锁状态,但没有其他线程可以这样做,代码将陷入僵局。
答案 2 :(得分:3)
顺便说一句,你必须记住全局内存写入和!如果你在代码中写下它们就没有完成读取...所以为了实践你需要添加一个全局memfence,即__threadfence()