In CUDA, how does one create a barrier for all threads in a kernel to wait on, until the CPU sends a signal to that barrier that it's safe/helpful to proceed?
I would like to avoid the overhead of launching a CUDA kernel. There are two types of overhead to avoid: (1) the cost of simply launching the kernel on X blocks and Y threads, and (2) the time it takes me to reinitialize my shared memory, which will largely have the same contents between invocations.
We do recycle/re-use threads all the time in CPU workloads. And CUDA even provides event synchronization primitives. Perhaps it would be minimal hardware cost to provide a more traditional signaling object.
Here's some code that provides a hole for the concept that I seek. The reader will probably want to search for QUESTION IS HERE. Building it in Nsight requires setting the Device Linker Mode to Separate Compilation (at least, I found it necessary).
#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);
}
And here is the output from running it. Note that the outputs are "wrong", simply because they were over-written by the loop whose signal is supposed to be the blocking mechanism for the GPU threads.
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__address. I'd fix it, but it's simpler this way anyway. - interestedparty333how does one create a barrier for all blocks+threads in a kernel to wait on. If you actually want to do that, I refer you to my previous comment. In order to guarantee that all threads in a kernel will reach a barrier, and wait there (for whatever it is you want them to wait on), requires one of those mechanisms (for correctness). If that is not actually your requirement, then you may be able to get by with something less. - Robert Crovella