Recommended resources
The freely available resources this lesson will direct you to.
Introduction
Welcome to our next lesson on optimizing large-scale training. In our previous session, we examined how to manage communication overhead by reducing its frequency with gradient accumulation and its volume with gradient compression. We noted that in modern systems, the utility of gradient compression can be limited because its latency is often hidden by another powerful optimization.
Today, we will explore that very optimization: communication-computation overlap. This is a fundamental technique used by all major deep learning frameworks to hide the time spent on network communication, thereby maximizing GPU utilization and training throughput.
This lesson will cover:
- The core concept of communication-computation overlap as a form of software pipelining.
- The underlying system mechanisms that enable it, namely CUDA streams and asynchronous collective operations.
- How these components are orchestrated during the backward pass of distributed data-parallel training.
By the end, you will understand how frameworks keep the GPU busy with useful computation, even while large amounts of data are being transferred across the network.
The Core Concept: Software Pipelining
At its heart, communication-computation overlap is an application of a classic computer science technique: software pipelining. Given your background, you'll recognize this as a method to overlap asynchronous operations that have data dependencies, preventing hardware from stalling while waiting for data.
In the context of GPU programming, the primary goal is to overlap data transfers (from high-latency main memory or network) with computation (executed by the ALUs). To understand this conceptually, let's first review the memory hierarchy.
Software Pipelining - JAX documentation
This article from the JAX documentation provides an excellent conceptual introduction to software pipelining for hardware accelerators. It explains the motivation by starting with the memory hierarchy.
Please read the sections 'Memory Hierarchies', 'Pipelining Basics', and 'Deriving a Double-Buffered Pipeline'. Focus on: The trade-offs between different memory types (SRAM vs. DRAM/HBM). The visual diagram that breaks a problem into 'copy_in', 'compute', and 'copy_out' stages and shows how they can be overlapped in time. The pseudocode walkthrough that transforms a synchronous, blocking loop into an asynchronous, pipelined one using multiple buffers. This illustrates the logic of pre-fetching.
As the article explains, a naive approach executes these stages sequentially: copy data, compute, copy results back. This leaves the compute units idle during data transfers. The key insight of pipelining is that while the GPU is computing on data chunk i, it can simultaneously fetch data chunk i+1 from memory and write back the results from chunk i-1.
This creates a "steady state" where the expensive data transfers are effectively hidden behind computation, maximizing the utilization of the processor's floating-point units (FLOP/s). The initial and final phases, where the pipeline is "filling" and "draining," are known as "bubbles." The goal is to make the steady-state phase as long as possible.
The Mechanism: Asynchrony and CUDA Streams
Now, let's move from the conceptual "what" to the implementation "how". The ability to execute operations concurrently on the GPU is managed by CUDA streams.
A CUDA stream is a sequence of operations (like kernel launches or memory copies) that execute on the GPU in the order they are issued by the host (CPU).
- Operations within a single stream are serialized.
- Operations in different streams can execute concurrently, provided there are no resource dependencies.
This is the fundamental mechanism for achieving overlap on a GPU. You can issue a memory copy on one stream and a compute kernel on another; if the GPU has the necessary hardware resources, it will execute them in parallel.

To achieve this, operations must be launched asynchronously. For example, instead of cudaMemcpy, which blocks the CPU until the transfer is complete, you use cudaMemcpyAsync, which returns control to the CPU immediately after the transfer is initiated. This function, along with kernel launches, takes a stream identifier as an argument to direct the work to the correct queue on the GPU.
Asynchrony and CUDA Streams | CUDA C++ Class Part 2
This NVIDIA Developer video provides a clear, practical explanation of asynchrony and CUDA streams.
Please watch the segment from 25:23 to 30:22. It explains how to use multiple streams to enable concurrent execution of memory copies and computations, and differentiates between synchronizing the whole device versus a single stream.
A crucial detail for achieving true asynchrony with host-to-device memory transfers is the use of pinned (or page-locked) memory. The GPU's memory controller can only transfer data from physical RAM pages that are guaranteed not to be swapped to disk by the operating system. By allocating host memory as "pinned," you allow the CUDA driver to initiate a direct memory access (DMA) transfer without needing an intermediate staging buffer, which would otherwise serialize the copy.
Application: Overlap in Distributed Training
We can now connect these concepts directly to distributed data-parallel training. The backward pass of a neural network offers a perfect opportunity for pipelining. Gradients are computed layer by layer, starting from the last layer and moving backward to the first.
The Naive (Synchronous) Approach:
- Execute the entire backward pass, computing gradients for all model parameters.
- Wait for all gradients to be ready.
- Initiate a single, large
AllReduceoperation to sum the gradients across all GPUs. - Wait for the
AllReduceto complete. - Update the model weights.
In this scenario, the GPU compute units are idle during step 3 and 4, waiting for the network communication to finish.
The Pipelined (Asynchronous) Approach:
This is where the magic happens. Modern frameworks like PyTorch (using its DDP module) and Horovod partition the model's parameters into buckets. As soon as the backward pass has computed the gradients for all parameters in a bucket, the framework can immediately initiate an asynchronous AllReduce for that bucket on a dedicated communication stream.
While this communication is happening in the background, the GPU continues the backward pass on its main computation stream, calculating gradients for the next bucket of parameters.
This video from the GPU MODE channel provides an excellent visualization and explanation of how this overlap is implemented in PyTorch DDP with a NCCL backend.
Please watch from 09:37 to 23:05. Pay close attention to: The profiler trace, which clearly shows two different CUDA streams: one for computation kernels (like gemm for matrix multiplication) and another for nccl:all_reduce kernels. The explanation of why it's faster to start the AllReduce for the last layer's gradients as soon as they are ready, rather than waiting for the entire backward pass to finish. This is the core of the pipelining logic.
Asynchronous Collectives and NCCL
The video highlights the role of asynchronous collectives. Communication libraries like the NVIDIA Collective Communications Library (NCCL) are fundamental to this process. NCCL provides highly optimized implementations of collective operations like AllReduce, AllGather, and Broadcast.
Crucially, these NCCL operations are stream-aware. When you call ncclAllReduce, you provide a CUDA stream as an argument. This enqueues the collective operation on the specified stream and returns control to the host immediately, allowing other computations to proceed on different streams. This non-blocking behavior is what makes the entire pipeline possible.

Conclusion
In this lesson, we demystified the concept of communication-computation overlap. Far from being a minor tweak, it is a cornerstone of high-performance distributed training, allowing systems to hide the otherwise significant latency of network communication.
Key Takeaways:
- Communication-computation overlap is a software pipelining technique that executes network transfers concurrently with GPU computation to hide communication latency.
- It is implemented using CUDA streams, which act as independent work queues on the GPU, allowing operations on different streams to run in parallel.
- The key enablers are asynchronous operations, such as non-blocking memory copies and, most importantly, stream-aware asynchronous collectives provided by libraries like NCCL.
- In distributed data parallelism, frameworks overlap the
AllReduceof gradients from earlier parts of the backward pass with the gradient computation for later parts, dramatically improving GPU utilization.
Preview of the Next Lesson:
Our discussion has centered on decentralized communication patterns where all workers participate equally in an AllReduce operation. However, this is not the only architecture for distributed training. In our next lesson, we will explore an alternative: the parameter server architecture. We will contrast this centralized approach with the decentralized methods we've studied and analyze their respective trade-offs in terms of communication patterns, scalability, and fault tolerance.