Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 83

Direct GPU Communication: Understanding P2P Memory Access

Recommended resources

Introduction

In our last lesson, we explored the world of multi-GPU training and saw how libraries like NCCL orchestrate complex collective operations, such as All-Reduce, to keep distributed models in sync. We established that avoiding the CPU as a data-transfer middleman is critical for performance. This high-level understanding of collective communication sets the stage for a deeper, more fundamental question: how does one GPU actually talk to another directly?

This lesson delves into that fundamental mechanism: Peer-to-Peer (P2P) memory access. This is a core capability in the CUDA platform that enables a GPU to directly read from or write to the memory of another GPU on the same system. We will dissect the concept from first principles, exploring the hardware interconnects like NVLink and the crucial software abstraction of Unified Virtual Addressing (UVA) that make P2P possible. By the end, you will understand not just the API for P2P transfers, but the underlying system architecture that enables this powerful feature.


The Default Path: Staging Through Host Memory

Before we explore the direct path, let's clarify the inefficient path that P2P access is designed to avoid. Without P2P, if you need to move a block of data from GPU 0 to GPU 1, the process involves the host (CPU) as an intermediary:

  1. Copy Device to Host: You issue a cudaMemcpy to transfer the data from GPU 0's global memory to the CPU's system RAM. This transfer happens over the PCI Express (PCIe) bus.
  2. Copy Host to Device: You issue a second cudaMemcpy to transfer the same data from the CPU's RAM to GPU 1's global memory. This is another trip across the PCIe bus.

This "staging" through host memory is slow for two main reasons: it involves two separate copy operations, and data must traverse the PCIe bus twice, often competing with other system traffic. For the high-frequency communication required in AI workloads, this CPU bottleneck is unacceptable.


The Direct Path: GPUDirect Peer-to-Peer Access

NVIDIA's solution is a family of technologies under the umbrella term GPUDirect. The specific technology for direct communication within a single node (i.e., between GPUs in the same computer) is GPUDirect Peer-to-Peer (P2P).

GPUDirect P2P allows the DMA (Direct Memory Access) engine of one GPU to read from or write to the memory of another GPU directly, without involving the CPU or host memory. This creates a high-speed, low-latency data path.

There are two primary ways to leverage P2P access, as illustrated in the diagram below.

This diagram from NVIDIA illustrates the two modes of P2P communication. On the left, "Direct Access," a kernel on GPU0 can directly dereference a pointer to memory on GPU1. On the right, "Direct Transfers," an explicit copy is initiated from GPU0 to GPU1's memory using a function like `cudaMemcpy()`.

To formalize this, let's turn to a chapter from a book on HPC for AI.

[PDF] 6 Distributed GPU Programming

This text provides a formal definition of GPUDirect technologies, including P2P.

Please read the section that starts with the definition of GPUDirect. Focus on the description of GPUDirect Peer-to-Peer (P2P), which accelerates intra-node communication by allowing direct memory copies between GPUs in the same system.


Enabling Technology I: The Hardware Interconnects

This direct data path relies on a physical connection between the GPUs.

  • PCI Express (PCIe): All GPUs in a system are connected to the CPU and each other via the PCIe bus. P2P transfers can occur over PCIe, provided the GPUs are under the same "PCIe root complex" (roughly, connected to the same CPU controller). While faster than staging through RAM, the bandwidth is still a limiting factor.
  • NVLink: To overcome the limitations of PCIe for GPU-centric workloads, NVIDIA developed NVLink. This is a proprietary, point-to-point interconnect that provides a much higher bandwidth and lower latency "super-highway" directly between GPUs. Modern multi-GPU servers, like NVIDIA's DGX systems, use NVLink to create a high-speed fabric connecting all GPUs.

The following diagram illustrates the physical data path. When data moves between two NVLink-connected GPUs, it can bypass the PCIe bus entirely, flowing from one GPU's streaming multiprocessors (SMs) and memory controller directly across the NVLink bridge to the other GPU.

This architectural diagram shows the data path for a P2P transfer over NVLink (red line). Data flows from the SMs of one GPU, through its internal crossbar (XBAR) and NVLink interface, directly to the peer GPU's interface and memory hierarchy, bypassing the PCIe bus and CPU.

For a high-level overview of how NVLink provides this performance boost, the following video offers a good analogy.

What Exactly Does NVLink do for Machine Learning (featuring Exxact Workstation w/dual 3090s)

This video from Jeff Heaton provides an intuitive explanation of NVLink's role.

Watch the segment from "what this is really doing". The "massive super highway" analogy is a good mental model for contrasting NVLink with the more general-purpose PCIe bus.


Enabling Technology II: Unified Virtual Addressing (UVA)

A fast physical connection is necessary but not sufficient. A crucial software-level abstraction is also required: Unified Virtual Addressing (UVA).

In early versions of CUDA, the CPU and each GPU had their own separate and isolated address spaces. A memory address like 0xDEADBEEF on GPU 0 was completely unrelated to the same address on GPU 1 or the CPU.

UVA, introduced with CUDA 4.0, changed this by creating a single, unified virtual address space for the entire system. With UVA, any memory allocated on the host or any GPU receives a unique pointer value within this 48-bit (or larger) virtual address space.

This is the key that unlocks P2P access. When a kernel on GPU 0 is given a pointer, the CUDA driver can now determine from the pointer's value alone whether it points to memory on GPU 0, GPU 1, or the host. This allows the hardware to route memory requests to the correct physical device. Without UVA, a pointer to another GPU's memory would be meaningless.

Multi-GPU Communication Libraries for Scaling HPC and AI Workloads | NVIDIA GTC 2025

This clip from an NVIDIA GTC talk explains the role of UVA as a foundational technology for P2P transfers.

Watch the segment from "The most important fundamental building block". Pay close attention to how UVA allows a library (or the driver) to query a pointer's physical location, which then enables the system to choose the most efficient data path, like a direct P2P transfer over NVLink.


Using P2P Access in CUDA Code

Now that we've covered the underlying principles, let's look at the CUDA API for using P2P. The process involves three main steps: checking for compatibility, enabling access, and then performing the transfer.

[PDF] Multi-GPU Programming - Semantic Scholar

These slides from an NVIDIA presentation provide a concise, code-centric guide to the P2P API.

Please read the following three sections from the presentation slides: First, review slide 10. This introduces the two key functions: cudaDeviceCanAccessPeer to query if a P2P path exists, and cudaDeviceEnablePeerAccess to activate it. It also notes the hardware requirements. Next, study slide 11, which describes the cudaMemcpyPeerAsync function. Note the crucial point: if peer access is enabled, the bytes are transferred directly; if not, the driver falls back to the slow staging method through CPU memory. Finally, examine the powerful code snippet on slide 42. This demonstrates "Direct Access": after enabling peer access from GPU_B to GPU_A, a kernel launched on GPU_B can directly operate on the d_A pointer, which points to memory physically located on GPU_A. This is a fundamental concept for a systems programmer.

Let's summarize the workflow for a P2P memcpy:

int gpu0 = 0;
int gpu1 = 1;

// 1. Check if a P2P link exists
int can_access_peer;
cudaDeviceCanAccessPeer(&can_access_peer, gpu0, gpu1);

if (can_access_peer) {
    // 2. Enable P2P access from the current device (gpu0) to the peer (gpu1)
    cudaSetDevice(gpu0);
    cudaDeviceEnablePeerAccess(gpu1, 0); // Second argument is reserved, must be 0

    // Symmetrically, enable access from gpu1 to gpu0
    cudaSetDevice(gpu1);
    cudaDeviceEnablePeerAccess(gpu0, 0);

    // Set active device back to whichever you're launching from
    cudaSetDevice(gpu0);

    // ... allocate memory d_ptr0 on gpu0 and d_ptr1 on gpu1 ...

    // 3. Perform the peer-to-peer copy
    // This will use the direct hardware path (NVLink or PCIe)
    cudaMemcpyPeerAsync(d_ptr1, gpu1, d_ptr0, gpu0, num_bytes, stream);

} else {
    // Fallback to manual staging through host memory
    // ... cudaMemcpy(h_ptr, d_ptr0, ...) followed by cudaMemcpy(d_ptr1, h_ptr, ...)
}

The performance gains from using this direct path are substantial, often saturating the bandwidth of the underlying interconnect. This is precisely what allows high-level libraries like NCCL and CUDA-aware MPI to achieve near-linear scaling on multi-GPU systems.


Conclusion

This lesson completes our journey from the fundamentals of C++ to the intricacies of multi-GPU communication, providing a complete stack of knowledge for HPC in AI. We've seen that Peer-to-Peer (P2P) access is the low-level mechanism that breaks the CPU bottleneck, enabling direct and efficient data exchange between GPUs.

Key Takeaways:

  • Peer-to-Peer (P2P) access allows one GPU to directly read from or write to another GPU's memory, bypassing the slow path of staging through host RAM.
  • This is enabled by two key technologies:
    1. Hardware Interconnects: Fast physical links like NVLink or the PCIe bus provide the data highway.
    2. Unified Virtual Addressing (UVA): A single system-wide address space that allows the CUDA driver to identify the physical location of any pointer.
  • P2P communication can be used via explicit copies, like cudaMemcpyPeerAsync, or through direct memory access, where a kernel on one GPU can dereference a pointer to another GPU's memory.
  • Before using P2P, you must query for compatibility with cudaDeviceCanAccessPeer and explicitly enable it with cudaDeviceEnablePeerAccess.

By understanding P2P, you now grasp the foundational link between high-level distributed training frameworks and the low-level hardware architecture. You have seen how kernels are written, how their performance is tied to the memory hierarchy and warp execution, and finally, how multiple GPUs are orchestrated to work together as a cohesive, high-performance computing engine. This comprehensive view, from bits and pointers to distributed systems, forms the bedrock of a GPU kernel engineer's expertise.