Even when NVLink does the heavy lifting, every GPU still hangs off PCIe — and what the system board does there decides whether your tensor parallelism scales, your storage paths bypass the CPU, and your driver finds peer GPUs at all. A grounded look at lanes, ReBAR, IOMMU, NUMA, P2P, and GPUDirect Storage.
Every GPU sits on PCIe, even the ones that mostly talk to siblings over NVLink. The board layer underneath the driver decides whether peer-to-peer works, how fast cold loads run, and whether your eight-GPU box is actually one fabric or two halves stitched through a CPU socket.
PCI Express is bidirectional, packet-switched, point-to-point serial. Each lane is one differential pair per direction; a link is a bundle of lanes (x1, x4, x8, x16). Every generation roughly doubles the per-lane raw rate; the encoding evolves alongside (8b/10b on Gen1/2, 128b/130b from Gen3, PAM4 on Gen6).
| Gen | Per-lane rate | Per-lane bandwidth | x16 link (per direction) | Encoding |
|---|---|---|---|---|
| PCIe 3.0 | 8 GT/s | ~1 GB/s | ~16 GB/s | 128b/130b NRZ |
| PCIe 4.0 | 16 GT/s | ~2 GB/s | ~32 GB/s | 128b/130b NRZ |
| PCIe 5.0 | 32 GT/s | ~4 GB/s | ~64 GB/s | 128b/130b NRZ |
| PCIe 6.0 | 64 GT/s | ~8 GB/s | ~128 GB/s | PAM4 + FLIT + FEC |
PCIe is bidirectional and point-to-point but the headline numbers always quote per direction. A "32 GB/s PCIe 4 x16 link" pushes 32 GB/s up and 32 GB/s down at the same time; aggregate is 64 GB/s. Useful for P2P where two GPUs send and receive in parallel.
Each GPU architecture pins itself to one PCIe generation at the host edge. The interesting twist: NVLink bandwidth on the same card has been pulling away from PCIe ever since Volta — the bus is the cheap path, NVLink is the fast one.
| Architecture | Examples | PCIe gen | x16 BW (per direction) | NVLink (where present) |
|---|---|---|---|---|
| Pascal | P100 | PCIe 3.0 | ~16 GB/s | NVLink 1, 4 links, 160 GB/s aggregate |
| Volta | V100 | PCIe 3.0 | ~16 GB/s | NVLink 2, 6 links, 300 GB/s aggregate |
| Turing | T4, RTX 20-series | PCIe 3.0 | ~16 GB/s | NVLink bridge on RTX 2080 / 2080 Super / 2080 Ti, Titan RTX, Quadro RTX |
| Ampere | A100, RTX 30-series | PCIe 4.0 | ~32 GB/s | NVLink 3, 600 GB/s on A100 SXM |
| Ada Lovelace | L40S, RTX 40-series | PCIe 4.0 | ~32 GB/s | None on consumer; none on L40S |
| Hopper | H100, H200 | PCIe 5.0 | ~64 GB/s | NVLink 4, 18 links × 50 GB/s = 900 GB/s on SXM5 |
| Blackwell | B200, RTX 50-series | PCIe 5.0 | ~64 GB/s | NVLink 5, 1.8 TB/s on B200; NVL72 domains |
| Rubin (expected) | Vera Rubin platform | PCIe 6.0 expected | ~128 GB/s | NVLink 6 expected |
An H100 SXM5 talks to its host over PCIe 5 x16 at ~64 GB/s per direction (~128 GB/s bidirectional). The same card talks to its seven NVSwitch-connected siblings at 900 GB/s aggregate bidirectional (18 NVLink-4 links × 25 GB/s/dir, or equivalently 50 GB/s bidirectional per link). That is roughly 7× the host bus. Anything you care about — tensor-parallel all-reduce, KV migration, expert routing — should ride NVLink, not PCIe. PCIe is for boot, weight loading, NVMe traffic, NIC traffic, and management.
A "PCIe edition" H100 (no NVLink bridge across cards) ganged with another via PCIe 5 x16 sees ~64 GB/s peer-to-peer. The same pair of H100 SXM5 cards on a HGX baseboard sees 900 GB/s. Tensor parallelism on the PCIe pair is bandwidth-starved on every all-reduce; that's why DGX/HGX exist.
A PCIe device exposes its memory to the host through Base Address Registers (BARs). Each BAR is a window into the device's address space that the host CPU can map into its own virtual memory. For an NVIDIA GPU, BAR1 is the window onto VRAM — the aperture through which the CPU and other devices see GPU memory.
Historically, BAR1 was a fixed 256 MB window. To cudaMemcpy a 24 GB tensor, the driver chunks the transfer through that window in pieces, doing 96 round trips. Each chunk costs setup latency. The full bandwidth of PCIe is rarely reached on cold loads.
PCIe specification feature, marketed as "Above 4G Decoding" or "Smart Access Memory". The BAR is renegotiated at boot to cover the entire VRAM in one window: 24 GB on an RTX 4090, 80 GB on an H100, 192 GB on a B200. The CPU sees all of VRAM in one mapping. No chunking.
Look for "Above 4G Decoding" + "Re-Size BAR Support". On consumer Intel boards (12th gen+) and AMD AM4/AM5/SP3 platforms it's a toggle. On older boards it may be missing entirely — ReBAR depends on system memory map cooperation that pre-2020 boards simply can't provide. Verify after boot:
lspci -vvv -s 01:00.0 | grep -A1 'Region 1:'
# Expect: Region 1: Memory at ... (64-bit, prefetchable) [size=24G]
# Bad : Region 1: Memory at ... (64-bit, prefetchable) [size=256M]
# Or via NVIDIA driver:
nvidia-smi -q | grep -i bar1
# BAR1 Memory Usage
# Total : 24576 MiB ← full VRAM = ReBAR on
# Total : 256 MiB ← legacy = ReBAR off
The IOMMU (Intel VT-d, AMD-Vi) is the device-side equivalent of the CPU's MMU. It virtualises DMA addresses: a device's view of memory is translated by the IOMMU before reaching the memory controller. Two reasons it exists: VM passthrough, and protection from buggy or malicious DMA.
iommu=pt (passthrough)The IOMMU is enabled for VM use cases but the host itself uses an identity map — DMAs go through real physical addresses, no translation overhead. Default on most distros. P2P-friendly. Fastest. Use this for bare-metal GPU serving.
iommu=on (full translation)Every DMA is remapped. Required for VFIO / GPU passthrough into VMs (and for confidential computing). Slower — small but measurable. Can break P2P if peer windows aren't mapped into the same domain. Use only when you need passthrough.
ACS is a PCIe feature implemented by switches and root ports. It tells the fabric whether peer-to-peer traffic between two downstream devices should be routed directly (peer-to-peer DMA) or upstream-redirected (forced through the host root complex). ACS exists because the IOMMU can only enforce isolation on traffic it actually sees — if two devices talk peer-to-peer behind a switch, the IOMMU is bypassed.
# Which IOMMU mode is active?
dmesg | grep -E 'IOMMU|DMAR|AMD-Vi' | head -20
# Which devices share an IOMMU group?
for g in /sys/kernel/iommu_groups/*; do
echo "Group ${g##*/}:"
ls $g/devices
done
# Is ACS enabled on root ports / switches?
sudo lspci -vvv | grep -E 'IOMMU|ACSCtl'
# ACSCtl: SrcValid+ TransBlk- ReqRedir+ CmpltRedir+ ... ← redirect on = blocks P2P
# ACSCtl: SrcValid- TransBlk- ReqRedir- CmpltRedir- ... ← clean for P2P
Consumer boards (cheap ACS) tend to make P2P trivially work but VM passthrough hard. Server boards behind PLX/Microchip switches often block P2P by default to satisfy ACS-strict virtualization, even though the silicon supports it. If P2P is mysteriously broken on a fancy server, check ACS before blaming the driver.
Peer-to-peer DMA lets one GPU read or write the memory of another GPU directly across PCIe (or NVLink, when present), without staging through host RAM. The path is: GPU A → PCIe root complex (or switch) → GPU B. CPU is uninvolved beyond setting up the mappings.
cudaDeviceCanAccessPeer(0,1) returns false, this is usually why.cudaDeviceEnablePeerAccess(peer, 0) is required after the capability check.// Check capability
int can_p2p;
cudaDeviceCanAccessPeer(&can_p2p, 0, 1);
if (!can_p2p) { /* fall back to host-staged */ }
// Enable both directions
cudaSetDevice(0); cudaDeviceEnablePeerAccess(1, 0);
cudaSetDevice(1); cudaDeviceEnablePeerAccess(0, 0);
// Direct copy
cudaSetDevice(0);
cudaMemcpyPeerAsync(dst1, 1, src0, 0, n, stream);
// Or use the simpleP2P sample for a calibrated bandwidth measurement:
// cuda-samples/Samples/0_Introduction/simpleP2P
P2P bandwidth on PCIe 4 x16 tops out around 26-28 GB/s effective per direction (out of 32 GB/s raw, after PCIe overhead). On PCIe 5 x16, ~52-56 GB/s per direction. Latency is ~3-5 μs setup. NVLink-4 delivers 25 GB/s per direction per link (50 GB/s bidirectional per link), and an H100 SXM5 has 18 of them for 900 GB/s bidirectional aggregate — ~7× the headline P2P-on-PCIe-5 number.
Two-CPU servers (and the few four-socket beasts that survive) are NUMA — non-uniform memory access. Each socket owns half the DRAM channels and half the PCIe lanes. Each GPU is wired to one socket. Cross-socket GPU traffic hops the inter-socket link — UPI on Intel, Infinity Fabric on AMD — which is slower and adds latency.
nvidia-smi topo -m labels the link SYS. Most drivers refuse to enable P2P across sockets; those that allow it run at a tiny fraction of intra-socket speed.# Inspect topology
numactl --hardware
nvidia-smi topo -m
nvidia-smi topo -p2p r # P2P read capability matrix
# Pin a serving process to NUMA 0 with GPUs on socket 0
CUDA_VISIBLE_DEVICES=0,1 \
numactl --cpunodebind=0 --membind=0 \
vllm serve meta-llama/Llama-3.1-70B-Instruct --tensor-parallel-size 2
# For multi-process MPI / torchrun: rank-to-NUMA mapping by local rank
for rank in 0 1 2 3; do
node=$((rank / 2)) # 2 GPUs per socket on this box
numactl --cpunodebind=$node --membind=$node \
python worker.py --rank $rank &
done
nvidia-smi topo -m is the single most useful diagnostic for multi-GPU performance debugging. It prints a matrix of how every GPU pair is connected, using a fixed vocabulary of link classes.
| Label | Meaning | Practical perf |
|---|---|---|
NV# | Direct NVLink, # = number of links | Best. e.g. NV18 = 18 NVLink-4 links via NVSwitch |
PIX | Single PCIe bridge between the two GPUs | Good. P2P at full PCIe x16 speed |
PXB | Multiple PCIe bridges traversed | OK. Adds latency; bandwidth still bus-limited |
PHB | PCIe + a PCIe host bridge (typically the CPU root complex) | Worse. Traffic turns round in the CPU; lower BW, higher latency |
NODE | Within NUMA node but no direct PCIe path between | Very poor. Often goes through CPU root complex |
SYS | Crosses CPU sockets (UPI / Infinity Fabric) | Worst. P2P typically disabled. Avoid TP across this |
GPU0 GPU1 GPU2 GPU3 GPU4 GPU5 GPU6 GPU7 CPU Affinity NUMA
GPU0 X NV18 NV18 NV18 NV18 NV18 NV18 NV18 0-55,112-167 0
GPU1 NV18 X NV18 NV18 NV18 NV18 NV18 NV18 0-55,112-167 0
GPU2 NV18 NV18 X NV18 NV18 NV18 NV18 NV18 0-55,112-167 0
GPU3 NV18 NV18 NV18 X NV18 NV18 NV18 NV18 0-55,112-167 0
GPU4 NV18 NV18 NV18 NV18 X NV18 NV18 NV18 56-111,168-223 1
GPU5 NV18 NV18 NV18 NV18 NV18 X NV18 NV18 56-111,168-223 1
GPU6 NV18 NV18 NV18 NV18 NV18 NV18 X NV18 56-111,168-223 1
GPU7 NV18 NV18 NV18 NV18 NV18 NV18 NV18 X 56-111,168-223 1
Legend: X = self NV# = N NVLinks PIX = single PCIe bridge ...
Every pair shows NV18 — the NVSwitch fabric makes the 8 GPUs look like a single uniform NVLink domain. CPU affinity columns split 4/4 across two sockets, but the cross-socket SYS hop is hidden because all GPU-to-GPU traffic goes over NVSwitch, not over PCIe + UPI.
GPU0 GPU1 CPU Affinity NUMA
GPU0 X PIX 0-15 0
GPU1 PIX X 0-15 0
# PIX = both GPUs hang off the same PCIe root port. P2P works.
# If instead you saw PHB or NODE, the second slot is on PCH-attached lanes
# and P2P will be slow or unreliable.
Run, in order: nvidia-smi topo -m (link classes), nvidia-smi topo -p2p r (read-side P2P), nvidia-smi nvlink -s (NVLink up/down per card), then ./bandwidthTest --device=0 --dtod (measured number). The matrix tells you what should work; the bandwidth test tells you what does work.
"GPUDirect" is NVIDIA's umbrella for any path that lets a non-CPU device DMA into GPU memory or vice versa, bypassing host buffers. Three flavours actually matter for LLM and HPC work.
GPU↔GPU within a node, over PCIe or NVLink. The case from slide 5. Used by NCCL all-reduces, vLLM TP shards, and explicit cudaMemcpyPeer. Same-host, two GPUs, no host RAM hop.
Triggered by: cudaDeviceEnablePeerAccess, NCCL, IPC handles.
GPU↔GPU across nodes via InfiniBand or RoCE. The NIC DMAs straight into GPU VRAM — no copy through host RAM. Required for any cross-node TP, and for the bandwidth claims in HGX/DGX SuperPOD literature to be true.
Stack: Mellanox/CX NIC + nvidia_peermem.ko kernel module (formerly nv_peer_mem) + UCX or NCCL.
Storage↔GPU directly. NVMe SSD or distributed FS reads land in GPU VRAM, bypassing the host page cache and CPU bounce buffer. ~10× faster on checkpoint loading; large-batch dataset streaming.
API: cuFile. Kernel: nvidia-fs.ko. FS support: Lustre, GPFS/Spectrum Scale, WekaIO, BeeGFS, plus local NVMe.
// Without GDS (standard POSIX read + cudaMemcpy):
NVMe SSD → PCIe → host page cache → user buffer → cudaMemcpyHtoD → GPU VRAM
^ ^
| one PCIe traversal upstream | another PCIe traversal back down
| CPU touches every byte CPU touches every byte again
// With GDS (cuFile API):
NVMe SSD → PCIe → GPU VRAM
^
| single PCIe traversal, NVMe DMAs straight into VRAM
| CPU only manages the queue
| ~10x faster on a 70 GB checkpoint, >90% of NVMe headline BW
Checkpoint loading on big-model training, multi-node serving with shared distributed FS, and large dataset streaming. For day-to-day inference (weights stay resident in VRAM after first load), GDS is irrelevant. The clearest win is in TRT-LLM / vLLM cold-start time on a 70B+ FP8 checkpoint when host page cache isn't warm.
The bus discussion has knock-on effects on chassis, power, and driver state. Three operational details worth covering because they intersect directly with PCIe trade-offs.
sudo nvidia-smi -pm 1. Without persistence, the kernel driver unloads the GPU each time the last process holding it exits, and re-initialises on the next open — ~30 s of overhead per cold start. For containerised serving where the orchestrator restarts pods, persistence is mandatory. Datacenter cards have it on by default; consumer cards don't.
sudo nvidia-smi -pl 350 (watts). Cap a 4090 at 350 W and you lose ~5% throughput but ~15% power. Useful in dense racks where total wall-power is the constraint. Inspect envelopes with nvidia-smi -q -d POWER — cards have min/max enforced limits and a default.
H100/H200/A100/L40S are passive — no fan on the card. They require 35+ CFM front-to-back airflow from chassis fans. Drop one into a quiet workstation case and it will throttle within minutes. The CFM number is on the card datasheet; do not improvise.
Old-style consumer "blower" cards (3090 turbo edition, A6000) self-cool front-to-back — rack-friendly. Modern flow-through Founders Edition (RTX 4090 FE, 5090 FE) recirculate hot air through the heatsink — bad in 4U racks where the next card breathes the exhaust. Pick blower-style for any rack-mount build.
Because chassis design and PCIe slot topology share the same constraint: dense multi-GPU boxes pack cards close together, which kills airflow, and put them on cheap PCH-attached lanes, which kills P2P. Server-class chassis (HGX, custom 4U with 8×PCIe) solve both at once — right cooling and right lane routing — and that's why they cost what they cost.
"I bolted a second 4090 in and now everything's slow." Here is the standard menu of failure modes, in roughly the order you should check them.
| # | Symptom | Cause | Fix |
|---|---|---|---|
| a | Second GPU runs at PCIe x4 / x8 instead of x16 | Board has 1×x16 + 1×x4 slot; or two x8/x8 lane split when a second card is installed | Read board manual; check lspci -vv | grep LnkSta. May require BIOS bifurcation toggle |
| b | cudaDeviceCanAccessPeer returns false |
All slots share one root port and ACS forces upstream redirection; or different IOMMU groups | Check lspci -vvv | grep ACSCtl; consider ACS override patch (dev only); or rewire to a PLX-switched server board |
| c | Cold model load takes 30+ s | BIOS Resizable BAR / Above 4G Decoding off | Enable in BIOS, verify with nvidia-smi -q | grep -A2 BAR1 |
| d | Periodic stutter under light load | ASPM L1 substate aggressive power saving putting links to sleep | Kernel: pcie_aspm=off or per-device tuning |
| e | "Second GPU is dead" / not detected | Power: USB-C PD, NVMe, second GPU all share one 12V rail; PSU droops under transient | Better PSU, separate rails per GPU, dedicated 12V-2x6 / EPS pigtails |
| f | P2P latency huge, topo -m shows PHB or NODE |
Slot is on PCH-attached lanes, traffic hops chipset DMI | Move the card to a CPU-direct slot; if none, accept it or change platform |
| g | Massive thermal throttling on second card | Two flow-through-cooled cards stacked; second card breathes first card's exhaust | Riser cable, bigger gap, or blower-style cards |
If you've outgrown one consumer GPU, the next jump is either a single workstation card with much more VRAM (RTX PRO 6000 Blackwell, 96 GB) or moving to a workstation/server platform with proper lane split. Bolting a second consumer GPU onto a desktop ATX board to do TP is technically possible and almost always disappointing.
Configure a hypothetical box. The lint tool reports per-GPU effective lanes, whether P2P will work, an estimated TP scaling factor, and the warnings you'd hit at deployment time.
The TP scaling estimate is a heuristic, not a benchmark. Real numbers depend on model size, batch, and kernel choice. The lint's value is in the warnings: if it complains, you have a real problem to fix before you measure anything.