【发布时间】:2018-04-29 17:21:37
【问题描述】:
在 CUDA 中,如何为内核中的所有线程创建一个等待等待的屏障,直到 CPU 向该屏障发送一个信号表明它可以安全/有帮助地继续?
我想避免启动 CUDA 内核的开销。有两种类型的开销需要避免:(1) 简单地在 X 块和 Y 线程上启动内核的成本,以及 (2) 我重新初始化共享内存所花费的时间,这在调用之间将基本上具有相同的内容.
我们一直在 CPU 工作负载中回收/重用线程。 CUDA 甚至提供event 同步原语。提供一个更传统的信号对象可能是最低的硬件成本。
这里有一些代码为我所寻求的概念提供了一个漏洞。读者可能想搜索QUESTION IS HERE。在 Nsight 中构建它需要将设备链接器模式设置为单独编译(至少,我认为这是必要的)。
#include <iostream>
#include <numeric>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>
#include <cuda_runtime_api.h>
#include <cuda.h>
static void CheckCudaErrorAux (const char *, unsigned, const char *, cudaError_t);
#define CUDA_CHECK_RETURN(value) CheckCudaErrorAux(__FILE__,__LINE__, #value, value)
const int COUNT_DOWN_ITERATIONS = 1000;
const int KERNEL_MAXIMUM_LOOPS = 5; // IRL, we'd set this large enough to prevent hitting this value, unless the kernel is externally terminated
const int SIGNALS_TO_SEND_COUNT = 3;
const int BLOCK_COUNT = 1;
const int THREADS_PER_BLOCK = 2;
__device__ void count_down(int * shared_location_to_ensure_side_effect) {
int x = *shared_location_to_ensure_side_effect;
for (int i = 0; i < COUNT_DOWN_ITERATIONS; ++i) {
x += i;
}
*shared_location_to_ensure_side_effect = x;
}
/**
* CUDA kernel waits for events and then counts down upon receiving them.
*/
__global__ void kernel(cudaStream_t stream, cudaEvent_t go_event, cudaEvent_t done_event, int ** cuda_malloc_managed_int_address) {
__shared__ int local_copy_of_cuda_malloc_managed_int_address; // we always start at 0
printf("Block %i, Thread %i: entered kernel\n", blockIdx.x, threadIdx.x);
for (int i = 0; i < KERNEL_MAXIMUM_LOOPS; ++i) {
printf("Block %i, Thread %i: entered loop; waitin 4 go_event\n", blockIdx.x, threadIdx.x);
// QUESTION IS HERE: I want this to block on receiving a signal from the
// CPU, indicating that work is ready to be done
cudaStreamWaitEvent(stream, go_event, cudaEventBlockingSync);
printf("Block %i, Thread %i: in loop; received go_event\n", blockIdx.x, threadIdx.x);
if (i == 0) { // we have received the signal and data is ready to be interpreted
local_copy_of_cuda_malloc_managed_int_address = cuda_malloc_managed_int_address[blockIdx.x][threadIdx.x];
}
count_down(&local_copy_of_cuda_malloc_managed_int_address);
printf("Block %i, Thread %i: finished counting\n", blockIdx.x, threadIdx.x);
cudaEventRecord(done_event, stream);
printf("Block %i, Thread %i: recorded event; may loop back\n", blockIdx.x, threadIdx.x);
}
printf("Block %i, Thread %i: copying result %i back to managed memory\n", blockIdx.x, threadIdx.x, local_copy_of_cuda_malloc_managed_int_address);
cuda_malloc_managed_int_address[blockIdx.x][threadIdx.x] = local_copy_of_cuda_malloc_managed_int_address;
printf("Block %i, Thread %i: exiting kernel\n", blockIdx.x, threadIdx.x);
}
int main(void)
{
int ** data;
cudaMallocManaged(&data, BLOCK_COUNT * sizeof(int *));
for (int b = 0; b < BLOCK_COUNT; ++b)
cudaMallocManaged(&(data[b]), THREADS_PER_BLOCK * sizeof(int));
cudaEvent_t go_event;
cudaEventCreateWithFlags(&go_event, cudaEventBlockingSync);
cudaEvent_t done_event;
cudaEventCreateWithFlags(&done_event, cudaEventBlockingSync);
cudaStream_t stream;
cudaStreamCreate(&stream);
CUDA_CHECK_RETURN(cudaDeviceSynchronize()); // probably unnecessary
printf("CPU: spawning kernel\n");
kernel<<<BLOCK_COUNT, THREADS_PER_BLOCK, sizeof(int), stream>>>(stream, go_event, done_event, data);
for (int i = 0; i < SIGNALS_TO_SEND_COUNT; ++i) {
usleep(4 * 1000 * 1000); // accepts time in microseconds
// Simulate the sending of the "next" piece of work
data[0][0] = i; // unrolled, because it's easier to read
data[0][1] = i + 1; // unrolled, because it's easier to read
printf("CPU: sending go_event\n");
cudaEventRecord(go_event, stream);
cudaStreamWaitEvent(stream, done_event, cudaEventBlockingSync); // doesn't block even though I wish it would
}
CUDA_CHECK_RETURN(cudaDeviceSynchronize());
for (int b = 0; b < BLOCK_COUNT; ++b) {
for (int t = 0; t < THREADS_PER_BLOCK; ++t) {
printf("Result for Block %i and Thread %i: %i\n", b, t, data[b][t]);
}
}
for (int b = 0; b < BLOCK_COUNT; ++b)
cudaFree(data[b]);
cudaFree(data);
cudaEventDestroy(done_event);
cudaEventDestroy(go_event);
cudaStreamDestroy(stream);
printf("CPU: exiting program");
return 0;
}
/**
* Check the return value of the CUDA runtime API call and exit
* the application if the call has failed.
*/
static void CheckCudaErrorAux (const char *file, unsigned line, const char *statement, cudaError_t err)
{
if (err == cudaSuccess)
return;
std::cerr << statement<<" returned " << cudaGetErrorString(err) << "("<<err<< ") at "<<file<<":"<<line << std::endl;
exit (1);
}
这是运行它的输出。请注意,输出是“错误的”,仅仅是因为它们被循环覆盖,其信号应该是 GPU 线程的阻塞机制。
CPU: spawning kernel
Block 0, Thread 0: entered kernel
Block 0, Thread 1: entered kernel
Block 0, Thread 0: entered loop; waitin 4 go_event
Block 0, Thread 1: entered loop; waitin 4 go_event
Block 0, Thread 0: in loop; received go_event
Block 0, Thread 1: in loop; received go_event
Block 0, Thread 0: finished counting
Block 0, Thread 1: finished counting
Block 0, Thread 0: recorded event; may loop back
Block 0, Thread 1: recorded event; may loop back
Block 0, Thread 0: entered loop; waitin 4 go_event
Block 0, Thread 1: entered loop; waitin 4 go_event
Block 0, Thread 0: in loop; received go_event
Block 0, Thread 1: in loop; received go_event
Block 0, Thread 0: finished counting
Block 0, Thread 1: finished counting
Block 0, Thread 0: recorded event; may loop back
Block 0, Thread 1: recorded event; may loop back
Block 0, Thread 0: entered loop; waitin 4 go_event
Block 0, Thread 1: entered loop; waitin 4 go_event
Block 0, Thread 0: in loop; received go_event
Block 0, Thread 1: in loop; received go_event
Block 0, Thread 0: finished counting
Block 0, Thread 1: finished counting
Block 0, Thread 0: recorded event; may loop back
Block 0, Thread 1: recorded event; may loop back
Block 0, Thread 0: entered loop; waitin 4 go_event
Block 0, Thread 1: entered loop; waitin 4 go_event
Block 0, Thread 0: in loop; received go_event
Block 0, Thread 1: in loop; received go_event
Block 0, Thread 0: finished counting
Block 0, Thread 1: finished counting
Block 0, Thread 0: recorded event; may loop back
Block 0, Thread 1: recorded event; may loop back
Block 0, Thread 0: entered loop; waitin 4 go_event
Block 0, Thread 1: entered loop; waitin 4 go_event
Block 0, Thread 0: in loop; received go_event
Block 0, Thread 1: in loop; received go_event
Block 0, Thread 0: finished counting
Block 0, Thread 1: finished counting
Block 0, Thread 0: recorded event; may loop back
Block 0, Thread 1: recorded event; may loop back
Block 0, Thread 0: copying result 2497500 back to managed memory
Block 0, Thread 1: copying result 2497500 back to managed memory
Block 0, Thread 0: exiting kernel
Block 0, Thread 1: exiting kernel
CPU: sending go_event
CPU: sending go_event
CPU: sending go_event
Result for Block 0 and Thread 0: 2
Result for Block 0 and Thread 1: 3
CPU: exiting program
【问题讨论】:
-
请忽略所有线程都将其结果写入同一个
__shared__地址的事实。我会修复它,但无论如何这样更简单。 -
同步网格中所有线程的规范方法是 1. 内核启动本身,或 2. CUDA 协作内核启动,CUDA cooperative groups API 的一部分,以及使用网格-在这种启动中启用的广泛同步。请注意,协作内核启动仅在 Pascal 或 Volta 设备上的某些受支持的场景中可用,基本上是 linux 或 Windows TCC 模式。
-
这看起来很有趣,答案可能就在其中。我只是想澄清一下,这基本上是 CPU 和 GPU 之间(而不是线程之间)的同步问题。
-
两者都可能涉及。我从你的第一句话中回复了这个片段:
how does one create a barrier for all blocks+threads in a kernel to wait on。如果你真的想这样做,我建议你参考我之前的评论。为了保证内核中的所有线程都会到达障碍,并在那里等待(无论您希望它们等待什么),需要其中一种机制(为了正确性)。如果这实际上不是您的要求,那么您也许可以少用一些东西。 -
你是在windows上还是在linux上?你在什么GPU上运行?您使用的是哪个 CUDA 版本?
标签: cuda