GPUDirect is not one feature. It is a family of four related mechanisms — peer-to-peer between GPUs, RDMA to and from a network adapter, Storage from an NVMe device, and Async — that share a single idea and almost nothing else. Each has its own kernel module, its own API, its own topology requirements, and its own way of quietly not working. That last part is what makes GPUDirect unusual to operate: when the preconditions are not met, you rarely get an error. You get correct data, delivered through a staging buffer in host DRAM, at a fraction of the bandwidth you budgeted for — and nothing in your application says so. This article is a map: what each path is, what it bypasses, what must be true for it to engage, and how to prove that it did rather than assuming.

One idea, four products — and the bounce buffer they delete

The unifying idea is narrow. A GPU exposes part of its device memory through a PCIe base address register — a window in the machine's physical address space that other devices on the bus can target with ordinary DMA. GPUDirect is the plumbing that lets a third party (another GPU, a network adapter, an NVMe controller) obtain addresses inside that window and read or write them directly. Everything else about the four members differs: different drivers, different APIs, different parts of the topology.

What they all delete is the same. Without GPUDirect, a block moves from an NVMe drive into a pinned buffer in host DRAM, then takes a second DMA across PCIe into the GPU; the network and GPU-to-GPU cases have the identical shape. That intermediate buffer costs an extra traversal of the root complex, twice the host memory bandwidth, CPU cycles to orchestrate and often to memcpy, and latency no queue depth fully hides. Delete the buffer and all four go with it — and if none of the four was your bottleneck, GPUDirect will not help.

Advertisement

The family at a glance

The figure below is the mental model to carry around. Three of the four members are data-path features: they change where the bytes physically land. The fourth, Async, is a control-path feature and does not appear in the same picture — it changes who initiates and completes the transfer, not where it goes.

GPUDirect familyGPUDirect StorageNVMe → GPUGPUDirect RDMANIC → GPUGPUDirect P2PGPU → GPUSame invariant: no host DRAM staging buffer in the data path
The three data-path members. Async, the fourth, changes who issues the transfer, not where the bytes land.

Read it as a routing question, not a feature list: whatever sits on the far end of a transfer decides which member is in play.

P2P — GPU to GPU inside the node

Peer-to-peer is the oldest and most commonly relied-upon member: one GPU reads or writes another GPU's memory with no host involvement. In CUDA you ask cudaDeviceCanAccessPeer whether the pair supports it and call cudaDeviceEnablePeerAccess to turn it on; after that, a peer copy or even a direct pointer dereference from a kernel crosses the link.

Which link depends on the hardware. Where NVLink connects the pair, peer access rides the scale-up fabric and behaves close to a memory operation. Where it does not, the transfer goes over PCIe and becomes a property of the topology: both devices generally need to sit under a common switch or root complex, and PCIe Access Control Services on an intervening switch forces the traffic upstream and breaks the direct path outright. Lanes, switches and ACS belong to the PCIe treatment; the consequence belongs here — P2P is not a property of a GPU, it is a property of the pair.

RDMA — the network adapter writes straight into HBM

GPUDirect RDMA lets an RDMA-capable adapter (InfiniBand, or RoCE over Ethernet) place incoming payload directly into GPU memory and pull outgoing payload directly from it. The enabling piece is a kernel module — nvidia_peermem on current stacks, with newer kernels also supporting a dma-buf based route — that teaches the adapter's driver how to pin and translate GPU addresses so a memory region can be registered against device memory rather than host memory.

Topology bites hardest here, because the path involves two peripheral devices rather than one. An adapter and a GPU hanging off the same PCIe switch talk efficiently; a pair split across sockets forces traffic through the inter-socket link and can perform badly enough to erase the benefit. That is why large clusters are built rail-aligned, each GPU paired to a specific adapter. The fabric, the collectives and the rail design are covered in the InfiniBand article; the map entry is: peer is a NIC, module is nvidia_peermem.

Storage — NVMe straight into GPU memory

GPUDirect Storage completes the set by making a drive, or a remote filesystem client, the DMA peer. Reads land in GPU memory without a page-cache round trip. It is the member with the most moving parts: a kernel module (nvidia-fs), a user-space library reached through the cuFile API, a JSON configuration file, and a filesystem that is on the supported list — local NVMe or NVMe-oF, and specific distributed filesystems, not any mount you happen to have.

It is also the member with the most explicit fallback. The configuration carries a compatibility mode that, when a request cannot be served by the direct path, transparently performs a POSIX read into a host buffer and copies it to the GPU. Your call returns success and the right bytes either way. Alignment, threadpools, filesystem support and where the throughput comes from live in the dedicated GPUDirect Storage article; the map entry is: peer is a drive, API is cuFile.

Advertisement

Async — taking the CPU out of the control path too

The three paths above remove the CPU from the data path but leave it in the control path: the host still posts the work request, rings the adapter's doorbell, and polls for completion. In a latency-sensitive loop that residual CPU involvement, and the synchronization it forces between stream and host, can dominate what is left after the copies are gone.

GPUDirect Async addresses exactly that, letting communication operations be triggered and completed from the GPU's own execution stream — in the kernel-initiated variants, from inside a kernel — so a dependent send need not wait for the host to notice a kernel finished. It is the least universally available member and usually reaches you indirectly, through a communication library that opts into it. For the map, the distinction is the point: Async is not a fourth place for bytes to go, it is a different party issuing the instruction.

Preconditions, path by path

Each member fails for its own reasons. Keeping them in one table stops you from checking the wrong thing:

PathPeer deviceNeedsTypical blocker
P2PAnother GPUPeer access enabled; NVLink or a shared PCIe switchACS on; devices on different root complexes
RDMARDMA NICnvidia_peermem (or dma-buf); registrable GPU memoryModule not loaded; NIC and GPU across sockets
StorageNVMe / NVMe-oFnvidia-fs; cuFile API; supported filesystemUnsupported mount; unaligned I/O; compat mode on
AsyncNIC (control)Library and adapter support for GPU-initiated opsSilently unused by the comms library

Two entries in the last column deserve emphasis because they are conditions, not bugs: an unloaded module, and a topology that never supported the path at all. Neither announces itself at runtime, and both are checkable before you run anything — which is why the routine below starts with static checks and only then looks at counters.

The failure mode is silence, not an error

This is the single most important operational fact about GPUDirect. Every one of these APIs is designed to keep working when the fast path is unavailable, because the alternative — an application that crashes on a machine with the wrong PCIe layout — would be worse. So the fallbacks are correct, transparent, and quiet.

Concretely: cudaMemcpyPeer succeeds whether or not peer access was ever enabled, staging through host memory when it was not. A cuFile read succeeds in compatibility mode with a POSIX read and a device copy behind it. A collective library that cannot register GPU memory with the adapter falls back to a host-staged transport and carries on. In all three cases your return codes are clean, your results are bit-identical, and your bandwidth is a multiple below plan. Absence of an error is not evidence the fast path engaged; it is evidence of nothing at all.

How to prove the fast path engaged

Verification comes in three layers, cheapest first. Static capability: is the machine even able to do this? nvidia-smi topo -m prints the connection matrix so you can see whether a GPU pair is NVLink-connected, under one switch, or crossing sockets, and whether a NIC is adjacent to the GPU you paired it with; lsmod tells you whether nvidia_peermem and nvidia-fs are loaded; the storage package ships a checker that reports driver status and which mounts are supported.

Runtime evidence: ask the software what it chose. Collective libraries log their selected transport when debug output is on, naming explicitly whether a GPU-direct network path or a host-staged one was used. The storage stack exposes counters separating bytes served by the direct path from bytes served in compatibility mode — a nonzero compat counter is the answer on its own. For P2P, query peer access rather than assuming the enabling call was made.

Physical evidence, the layer that cannot be argued with: on a real fast path, host DRAM traffic and CPU utilization do not scale with transfer volume, and a system-level profile shows no host-to-device copies shadowing the transfer.

GPUDirect is four mechanisms wearing one name, and the only thing they share is the invariant: a peer device DMAs into a window onto GPU memory, so no staging buffer in host DRAM sits in the data path. Which member applies is decided by what is on the other end — another GPU means P2P and a topology question, a network adapter means RDMA and a peer-memory module, a drive means Storage and the cuFile stack, and Async changes who issues the transfer rather than where it lands. Because every one of these paths falls back silently and correctly to a host bounce buffer, configuring a path is never evidence you are on it. Check the topology matrix and the loaded modules before you run, read the library's transport logs and the compat-mode counters while you run, and confirm afterwards that host memory bandwidth stayed flat. If you did not measure it, you are not on the fast path — you are hoping.