Welcome back. In our last lesson, you successfully built and verified a complete vector addition program. You orchestrated the entire workflow: allocating memory, copying data between the host CPU and device GPU, launching the kernel, and copying the results back. We relied on a sequence of CUDA API calls like cudaMemcpy and the kernel launch <<<...>>> to get the job done.
This raises a crucial question about the nature of these calls. When your C++ code on the host calls a function to launch a kernel, does the host program pause and wait for the thousands of GPU threads to complete their work before moving to the next line of code? Or does it simply "fire and forget," continuing with its own tasks while the GPU works in parallel?
In this lesson, we will dissect this fundamental aspect of the CUDA execution model. You will learn to distinguish between synchronous and asynchronous operations and understand the critical role of cudaDeviceSynchronize in managing the interaction between the CPU and GPU. This knowledge is essential for writing correct, efficient, and well-behaved CUDA programs.
Host and Device: An Asynchronous Relationship
At its core, the CUDA programming model treats the GPU as an independent, parallel coprocessor. The CPU (host) acts as a controller, dispatching commands to the GPU (device). To maximize overall system throughput, this interaction is designed to be asynchronous by default.
This means that when the host issues a command—such as launching a kernel or initiating a data transfer—the CUDA driver typically adds the command to a queue for the GPU and then immediately returns control to the host thread. The host is then free to perform other tasks, such as preparing the next batch of data, performing I/O, or executing other CPU-bound logic, all while the GPU is busy executing the dispatched command.
This is a powerful feature for performance, as it allows for the overlapping of CPU and GPU work.
Synchronous vs. Asynchronous APIs
Let's formalize this distinction:
-
Asynchronous (Non-Blocking) APIs: These functions return control to the host thread before the requested operation is complete on the device. Most CUDA operations, including kernel launches (
myKernel<<<...>>>) and asynchronous memory copies (cudaMemcpyAsync), fall into this category. Their purpose is to queue work for the GPU with minimal latency on the host side. -
Synchronous (Blocking) APIs: These functions do not return control to the host thread until the requested operation has finished on the device. A classic example is a memory copy from the device to the host using the standard
cudaMemcpy. By default, this call blocks because the host code will almost certainly want to use the data immediately, so the API ensures the data has actually arrived before proceeding.
The NVIDIA Developer channel has an excellent video that clarifies this distinction.
Asynchrony and CUDA Streams | CUDA C++ Class Part 2
This video from the "CUDA C++ Class" series explains the core concepts of asynchrony.
Please watch the following segments: Synchronous vs. Asynchronous: This part clearly explains the difference in behavior, contrasting a blocking API (Thrust) with a non-blocking one (CUB) and showing how the CPU's wait time is affected. Recap: This is a concise summary of the key ideas about preferring asynchrony and how non-blocking calls work.
The Need for Synchronization: cudaDeviceSynchronize()
If kernel launches are asynchronous, a problem arises: How does the host know when the GPU has finished its work? If the host needs to use the results of a kernel, or if it needs to measure the precise time a kernel took to run, it cannot simply proceed. It needs a way to wait for the device.
This is the purpose of cudaDeviceSynchronize().
cudaDeviceSynchronize() is a synchronous, blocking function that forces the host thread to pause and wait until all previously issued commands in all streams on the device have completed. Think of it as a global barrier.
You would use cudaDeviceSynchronize() in several key scenarios:
- Ensuring Correctness: Before the host accesses memory that a GPU kernel was supposed to write (e.g., after a
cudaMemcpyAsyncfrom device to host), you must synchronize to guarantee the data is ready. - Accurate Timing: To measure the wall-clock time of a GPU operation, you must place
cudaDeviceSynchronize()calls before starting and after ending your timing measurement. This ensures your measurement captures the full execution time of the GPU work, not just the time it took to launch it. - Error Handling: Because most API calls are asynchronous, an error might not be reported by the function that caused it. For example, an invalid memory access inside a kernel will not cause the
kernel<<<...>>>()call to fail. The error is latent. A subsequent call tocudaDeviceSynchronize()will fail and report the error, allowing you to pinpoint where things went wrong.
Let's look at a profiler timeline, which visualizes this behavior.

In the "CUDA Profiling Timeline" image, you can see several key features:
- The "Call to cudaMemcpyAsync" on the host returns almost instantly (in microseconds), but the actual memory copy on the GPU happens later and takes longer. This is the essence of an asynchronous call.
- The host thread executes some other code (represented by "Delay Execution").
- The host then calls
cudaDeviceSynchronize(). The brown bar shows the host thread is blocked—paused—for the duration of this call. - The call to
cudaDeviceSynchronize()only returns after all the preceding GPU work (Memcpy HtoD,Memcpy DtoH,someHeav...kernel) has finished.
For a deeper dive into the official definitions and rules, let's consult the CUDA Programming Guide.
2.3. Asynchronous Execution — CUDA Programming Guide
The official NVIDIA documentation provides the authoritative explanation of the asynchronous execution model.
Please read the following parts of the guide: First, read the introductory section, What is Asynchronous Concurrent Execution?. This section formalizes the concepts we've discussed. Next, jump to the section on Explicit Synchronization. Focus on the description of cudaDeviceSynchronize(). Finally, review the Summary of Asynchronous Execution to solidify the key takeaways.
The slides from a course by Caroline Collange also provide a concise summary.
[PDF] Advanced CUDA programming: asynchronous execution, memory ...
These presentation slides offer a quick visual summary of asynchronous execution.
Review slide 3, titled "Asynchronous execution". It highlights the key points: most calls are async, they return immediately, and this has implications for error checking and timing. Also, note the mention of cuThreadSynchronize(), which is an older name for cudaDeviceSynchronize(). Then, look at the code example on slide 34. The comment "Explicit CPU-GPU synchronization is now mandatory!" reinforces the critical role of synchronization for correctness when using certain memory management schemes.
Implicit Synchronization
While cudaDeviceSynchronize() is the most explicit barrier, it's worth noting that some operations in CUDA have implicit synchronization behavior. For example, subsequent kernel launches within the same default command queue (or "stream") will execute in order. This is explained well in this short video clip.
Intro to CUDA (part 6): Synchronization
This video briefly touches on the default in-order execution of kernels.
Watch the segment on implicit barriers between kernel launches. This clarifies that while the host is not blocked, the GPU will execute kernels sequentially by default.
We will explore this concept of streams and in-order execution much more deeply in the next lesson. For now, the key is to understand that cudaDeviceSynchronize is your tool for making the host wait for the device.
Conclusion
In this lesson, you've learned a fundamental principle of CUDA programming: the host and device operate asynchronously to enable parallelism and high performance.
Key Takeaways:
- Most CUDA API calls, especially kernel launches, are asynchronous: they queue work on the GPU and return control to the CPU immediately.
- This asynchronicity allows you to overlap CPU computation with GPU computation, which is crucial for performance.
- Synchronous APIs block the CPU until the GPU task is complete. Some operations, like device-to-host
cudaMemcpy, are synchronous by default for safety. cudaDeviceSynchronize()is a global, blocking synchronization primitive that forces the host to wait for all previously submitted work on the GPU to finish. It is essential for correctness, accurate timing, and robust error handling.
You now understand that CUDA operations are asynchronous and how to enforce a wait when needed. However, cudaDeviceSynchronize() is a very coarse tool—it waits for everything. What if you want to overlap a data transfer with a kernel execution? To do that, you need more fine-grained control. In our next lesson, we will introduce CUDA Streams and Events, which allow you to manage and synchronize specific sequences of operations, unlocking a much higher degree of concurrency.