Your Pod says Running. Kubernetes says the GPU is allocated. Yet the training job crawls, or vLLM fails the moment it spreads across eight GPUs.

Nothing in Kubernetes looks broken. So where is the problem?

Usually in a layer Kubernetes can’t see: the driver, the CPU socket the GPU is wired to, the network card, or the link between GPUs.

This guide climbs those layers in the order your data crosses them, from one GPU in one Pod to a job spread across nodes:

GPU in a Pod → topology inside the node → network between nodes → GPU-to-GPU traffic

For each layer you get two things: what it is, and what to check when it breaks.

GPU access first#

Start with the checkpoints that make a GPU usable inside a Pod. This is a troubleshooting checklist; the components do not all sit on one runtime data path.

Eight GPU access checkpoints from hardware to the CUDA application, highlighted in sequence

Eight checkpoints from hardware visibility to application performance. Start at the first failing check.

Once the application can use the GPU, the question changes: can it move data fast enough? That takes us beyond this checklist into CPU, memory, GPU, and network locality.

1. NVIDIA GPU Operator and GPU scheduling#

The pieces#

PieceWhat it does
NVIDIA driverLets the OS talk to the physical GPU
Container ToolkitLets the container runtime expose the allocated GPU and NVIDIA libraries inside a container
Device PluginDiscovers GPUs and tells kubelet about them
GPU Feature DiscoveryAdds node labels describing the GPU
GPU OperatorInstalls and manages most of the above

Think of it as a hand-off:

flowchart LR A["Host GPU works
(nvidia-smi)"] e1@--> B["Container Toolkit"] B e2@--> C["Container can
access its GPU"] A e3@--> D["Device Plugin"] D e4@--> E["kubelet advertises
nvidia.com/gpu: 8"] E e5@--> F["Scheduler can
count GPUs"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } classDef k8s fill:#13264a,stroke:#38bdf8,color:#e6f6ff; classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class A,C gpu; class B,D,E,F k8s;
Each box hands the GPU to the next one. If a hand-off fails, everything after it fails too.

The driver#

The OS needs a working NVIDIA driver to talk to the GPU. If nvidia-smi fails, start below Kubernetes:

hardware → PCIe → OS → driver

The device plugin and the scheduling request#

The device plugin exposes GPUs as a Kubernetes extended resource:

nvidia.com/gpu: 8

Kubelet then reports that capacity to the control plane. A Pod asks for a GPU like this:

resources:
  limits:
    nvidia.com/gpu: 1

Now the scheduler can choose a node with enough available GPU resources. Kubelet and the device plugin handle allocation of the actual devices on that node.

GPU model awareness#

nvidia.com/gpu is just a count. It does not say H100, H200, or L40S. To target a specific model, use GPU Feature Discovery labels with node affinity, so a rule such as “requires H200 nodes” becomes possible.

The Operator does not schedule#

The GPU Operator does not schedule GPUs.

It installs and manages the software stack (driver, Container Toolkit, Device Plugin, Feature Discovery, monitoring, MIG-related parts, depending on configuration). The Kubernetes scheduler still does the scheduling, using the resources the device plugin advertises.

GPU taints#

You may taint GPU nodes so ordinary workloads stay off them. GPU workloads then need:

GPU request + toleration + affinity or node selection

A toleration means allowed, not forced onto that node.

One Pod, many nodes?#

No. Ordinary Kubernetes scheduling does not split a Pod across nodes. A Pod asking for 8 GPUs needs all 8 on one node. Multi-node workloads use several Pods coordinated by a framework or operator.

MIG#

MIG (Multi-Instance GPU) splits a supported NVIDIA GPU into isolated instances. It helps with smaller workloads, better utilization, and isolation. A job that needs the whole device may still prefer the full GPU.

Troubleshooting tree#

SymptomWhat to check
nvidia-smi failsHardware, PCIe, driver, OS
nvidia-smi works, nvidia.com/gpu missingDevice plugin, kubelet registration, GPU Operator integration, runtime integration
nvidia.com/gpu exists, Pod PendingGPU capacity, CPU/RAM, taints, node affinity, topology, storage, cordon, other scheduler constraints
Pod scheduled, no GPU inside the containerContainer Toolkit, runtime configuration, device allocation, driver compatibility
GPU visible, but NCCL or vLLM failsCUDA libraries, NCCL, topology, distributed network, RDMA

Also watch more than utilization: GPU memory, temperature, power, health or errors, and allocation.

Burn this into memory#

nvidia-smi          = host sees GPU
Device Plugin       = Kubernetes sees GPU
nvidia.com/gpu      = scheduler can account for GPU
Container Toolkit   = container can access GPU
GPU Operator        = manages the NVIDIA Kubernetes GPU software lifecycle

2. NUMA and PCIe topology#

A visible GPU is only the first check. Next, look at the route data takes to reach it.

What is NUMA?#

NUMA means Non-Uniform Memory Access. Many large servers have several CPU sockets, each with its own local memory. A socket can also contain more than one NUMA node; the two-socket example below is a simplified layout. Reading local memory is generally faster than reading memory attached to the other socket.

NUMA Node 0                 NUMA Node 1

CPU Socket 0                CPU Socket 1
   │                            │
Local RAM                    Local RAM

Why it matters for AI#

GPUs and NICs hang off a specific socket too:

CPU Socket 0                 CPU Socket 1
 ├── RAM 0                    ├── RAM 1
 ├── GPU 0                    ├── GPU 2
 ├── GPU 1                    ├── GPU 3
 └── NIC 0                    └── NIC 1

A good path keeps everything together: CPU0 + RAM0 + GPU0 + NIC0. A bad path makes data cross sockets, adding latency and eating inter-socket bandwidth.

Two CPU sockets. A short cyan path connects GPU 0 and NIC 0 on socket 0. A long amber path loops from GPU 0 across the inter-socket link to NIC 1.

Same GPU, same NIC count. The route is what changes.

PCIe#

PCIe is the general-purpose high-speed connection between the CPU and devices such as GPUs, NICs, and NVMe drives.

CPU
 ├── PCIe → GPU
 ├── PCIe → NIC
 └── PCIe → NVMe

Kubernetes and topology#

With the conventional device-plugin setup described here, the default scheduler mainly sees resource counts, rather than the socket of each GPU. Fine-grained locality is handled on the node, by kubelet and its configured resource managers:

PieceRole
Topology ManagerCollects hints from resource managers so CPU and device allocations line up when possible
CPU ManagerWith the static policy, containers in Guaranteed Pods with whole-number CPU requests can get exclusive CPUs
Memory ManagerWith the Static policy, provides NUMA-aware memory allocation for Guaranteed Pods
Guaranteed QoSCPU and memory requests equal their limits in every container; required for exclusive CPU allocation

Guaranteed QoS alone does not align resources. The node needs the relevant manager policies and device topology hints. A strict Topology Manager policy can reject a Pod on the node if its locality requirements cannot be met. See the Kubernetes resource manager guide and Topology Manager policies.

Two more places locality shows up:

  • SR-IOV: a VF inherits locality from its parent NIC. If the GPU is near NUMA 0 but the VF is near NUMA 1, traffic crosses sockets.
  • GPUDirect RDMA: it benefits from a sensible GPU and NIC PCIe/NUMA layout.

Useful commands#

lscpu
numactl --hardware
nvidia-smi topo -m

nvidia-smi topo -m prints a matrix of how every GPU and NIC connects to the others. It is the quickest way to see the real topology.

Performance troubleshooting order#

flowchart TD A["GPU health"] e1@--> B["GPU topology"] B e2@--> C["CPU affinity"] C e3@--> D["NUMA"] D e4@--> E["PCIe"] E e5@--> F["NIC locality"] F e6@--> G["NVLink / NVSwitch"] G e7@--> H["NCCL"] H e8@--> I["RDMA"] I e9@--> J["Network fabric"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } e6@{ animate: true } e7@{ animate: true } e8@{ animate: true } e9@{ animate: true } classDef k8s fill:#13264a,stroke:#38bdf8,color:#e6f6ff; classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class A,B,G,H gpu; class C,D,E host; class F,I,J net;
A useful investigation order, rather than a strict dependency chain. Compare topology and transport evidence as you go.

Burn this into memory#

NUMA              = CPU/memory locality
PCIe              = device connection path
Topology Manager  = coordinates resource locality on the node
CPU Manager       = can assign exclusive CPUs
Memory Manager    = can align memory with NUMA locality

3. RDMA, InfiniBand, RoCE, ConnectX#

Locality matters within a server. Once a job spans servers, the network becomes part of the GPU communication path too.

What is RDMA?#

RDMA means Remote Direct Memory Access. It lets one server read or write another server’s registered memory with very little CPU and kernel involvement.

Server A memory ↔ RDMA ↔ Server B memory

The key point:

RDMA is a communication capability, not a network type by itself.

Why distributed AI benefits from it#

Distributed GPU jobs move huge amounts of data. If the network is slow, the GPU waits, and an expensive accelerator sits idle. This matters for tensor parallelism, training collectives, model parallelism, and large inference workloads.

The names#

NameOne line
RDMAThe communication model
InfiniBandA purpose-built HPC fabric that supports RDMA
RoCERDMA over Converged Ethernet: RDMA on an Ethernet network
ConnectXNVIDIA’s family of high-performance network adapters
NCCLNVIDIA Collective Communications Library, the software that moves GPU data

GPUDirect RDMA#

Without GPUDirect RDMA, data is staged through host memory on both servers:

Server A: GPU memory → host memory → RDMA NIC
                                       ↓
                              InfiniBand / RoCE
                                       ↓
Server B: GPU memory ← host memory ← RDMA NIC

With GPUDirect RDMA, the sending NIC reads GPU memory directly and the receiving NIC writes directly into the other GPU’s memory. Both paths below use RDMA across the network; the difference is whether they need host-memory staging.

flowchart LR subgraph S["Without GPUDirect RDMA"] direction TB G1["Server A
GPU memory"] e1@-->|GPU-to-host copy| H1["Server A
Host memory"] H1 e2@--> N1["Server A
RDMA NIC"] N1 e3@--> F1["InfiniBand / RoCE fabric"] F1 e4@--> N2["Server B
RDMA NIC"] N2 e5@--> H2["Server B
Host memory"] H2 e6@-->|Host-to-GPU copy| G2["Server B
GPU memory"] end subgraph D["With GPUDirect RDMA"] direction TB G3["Server A
GPU memory"] e7@-->|Direct PCIe access| N3["Server A
RDMA NIC"] N3 e8@--> F2["InfiniBand / RoCE fabric"] F2 e9@--> N4["Server B
RDMA NIC"] N4 e10@-->|Direct PCIe access| G4["Server B
GPU memory"] end e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } e6@{ animate: true } e7@{ animate: true } e8@{ animate: true } e9@{ animate: true } e10@{ animate: true } S ~~~ D classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class G1,G2,G3,G4 gpu; class H1,H2 host; class N1,N2,N3,N4,F1,F2 net;
Full path from Server A's GPU to Server B's GPU. Left: staging through host memory on both servers. Right: GPUDirect RDMA avoids those copies.

Topology still matters here. A GPU and NIC far apart lose much of the benefit.

RoCE needs a careful network#

RDMA over Ethernet only behaves well on a carefully engineered network. Terms worth recognizing: PFC, ECN, DCQCN. The idea is congestion and loss management so RDMA stays fast and predictable.

Pod networking for AI#

One common design for a distributed AI Pod uses two interfaces:

eth0 = normal Kubernetes networking
net1 = high-performance RDMA interface

One way to build that second path is Multus + SR-IOV + an RDMA-capable VF. Other deployments use shared RDMA devices or host networking. The next section explains the VF design; section 5 shows how NCCL uses the available paths.

Troubleshooting distributed performance#

SymptomThink about
Single node works, multi-node is badNCCL transport, RDMA interfaces, ConnectX, link speed, errors and drops, congestion, GPUDirect, NUMA/PCIe topology, fabric
Normal Pod network works, distributed GPU communication failsThe secondary RDMA path may be broken while ordinary CNI is fine: VF allocation, RDMA device visibility, ConnectX health, RoCE or InfiniBand, NCCL

Burn this into memory#

RDMA            = low-overhead remote memory communication
InfiniBand      = HPC fabric supporting RDMA
RoCE            = RDMA over Ethernet
ConnectX        = high-performance adapter
GPUDirect RDMA  = efficient GPU memory ↔ RDMA NIC communication

4. SR-IOV, Multus, DPU, SmartNIC#

SR-IOV#

SR-IOV means Single Root I/O Virtualization. One physical PCIe device exposes many hardware-backed virtual functions.

Physical NIC
    │
    ├── PF
    ├── VF1
    ├── VF2
    ├── VF3
    └── VF4
  • PF (Physical Function): the full physical function, visible to the host. It configures SR-IOV and creates VFs.
  • VF (Virtual Function): a lightweight PCIe function that can be handed to a workload.

A NIC exposes a finite number of VFs, and Kubernetes treats them as schedulable resources.

The Kubernetes SR-IOV stack#

flowchart TD A["NIC PF"] e1@--> B["VFs"] B e2@--> C["SR-IOV Network Device Plugin"] C e3@--> D["Kubernetes extended resources"] D e4@--> E["Scheduler selects a node
with VF capacity"] E e5@--> K["kubelet + Device Plugin
allocate a VF"] K --> F["SR-IOV CNI"] F e6@--> G["VF configured in the Pod network namespace"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } e6@{ animate: true } classDef k8s fill:#13264a,stroke:#38bdf8,color:#e6f6ff; classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class A,B,G net; class C,D,E,K,F k8s;
Two different jobs: the Device Plugin allocates the VF, the SR-IOV CNI attaches it.

Do not mix up the two:

Device Plugin = discover / advertise / allocate the device resource
SR-IOV CNI    = attach / configure the network interface

Multus#

Multus is a meta-CNI. It lets a Pod have more than one network attachment.

flowchart TD P["Pod"] e1@--> E0["eth0"] P e2@--> N1["net1"] E0 e3@--> C["Cilium"] N1 e4@--> S["SR-IOV CNI"] C e5@--> K["Normal K8s networking
API, DNS, Services"] S e6@--> R["VF / RDMA
high-performance traffic"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } e6@{ animate: true } classDef k8s fill:#13264a,stroke:#38bdf8,color:#e6f6ff; classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class P,E0,C,K k8s; class N1,S,R net;
Multus orchestrates the attachments. It is not the fast datapath itself.

In this design, the traffic is separated like this:

eth0 = API / DNS / Services / management traffic
net1 = high-performance RDMA traffic

SR-IOV is not RDMA#

SR-IOV gives a Pod a direct-ish, hardware-backed virtual interface. RDMA needs a suitable NIC, firmware and driver, RDMA stack, and fabric. They often work together, but they are different things.

Troubleshooting#

SymptomThink about
Pod Pending: Insufficient <sriov-resource>No allocatable VF, a resource advertisement problem, device plugin issue, or scheduler constraints
Pod scheduled but no secondary interfaceMultus, NetworkAttachmentDefinition, SR-IOV CNI, configuration
net1 exists but RDMA failsVF driver, RDMA device exposure, RoCE or InfiniBand, NCCL, fabric
Device plugin unhealthyVF resources may vanish from Kubernetes accounting
PF downMany VFs can be affected at once

SmartNIC vs DPU#

  • SmartNIC: a programmable, high-performance NIC that can offload networking, security, or storage work.
  • DPU: a more capable infrastructure processor: NIC + onboard CPU cores + offload engines. NVIDIA BlueField is a common example.
ConnectX   = high-performance NIC
BlueField  = DPU with networking + onboard compute/offload

These are optional infrastructure components in this story. A DPU does not replace the GPU:

GPU = model compute
DPU = infrastructure / network / security / storage offload

Burn this into memory#

Multus         = multiple interfaces
SR-IOV         = hardware virtualization through PF/VFs
Device Plugin  = advertise/allocate VF
SR-IOV CNI     = attach/configure VF
ConnectX       = high-speed NIC
BlueField      = NIC + onboard compute/offload
RDMA           = communication capability

We now have GPU access, local topology, and a path between servers. NCCL brings those pieces together for communication between GPUs.

Large AI workloads need GPUs to swap data quickly: activations, partial results, gradients, tensor-parallel data.

PieceWhat it is
PCIeGeneral-purpose device interconnect
NVLinkNVIDIA’s high-bandwidth interconnect for supported GPUs, faster and more specialized than PCIe
NVSwitchA switching fabric that connects many NVLink GPUs. It is not an Ethernet switch
NCCLThe software library that does the actual GPU communication
GPU0 ─┐
GPU1 ─┼── NVSwitch fabric
GPU2 ─┼
GPU3 ─┘

NCCL collectives#

NCCL provides optimized group operations:

OperationWhat happens
AllReduceEveryone contributes values, everyone receives the reduced result
AllGatherEveryone gathers everyone’s pieces
ReduceScatterData is reduced, then split across participants
BroadcastOne participant sends data to all
Four GPUs in a ring holding 1, 2, 3 and 4. Dots circulate between them, then every GPU shows the result 10.

Sum AllReduce: each GPU contributes a value and every GPU gets 10. This illustrates the result, not the full ring algorithm.

Which path does NCCL use?#

NCCL sits above the physical paths and selects transports based on topology, support, and configuration:

  • Same server: NVLink, NVSwitch, or PCIe, depending on topology.
  • Across servers, with GPUDirect RDMA: GPU → RDMA NIC (such as ConnectX) → InfiniBand or RoCE → RDMA NIC → GPU.
  • Fallback paths: RDMA can use host-memory staging when direct GPU access is unavailable, and NCCL can use IP sockets when the RDMA transport is unavailable or disabled. A working job does not prove it is using the fast path.
flowchart TD N["NCCL"] e1@--> I["Inside one server"] N e2@--> X["Across servers"] I e3@--> L["NVLink / NVSwitch / PCIe"] X e4@--> G["GPUDirect RDMA"] G e5@--> C1["ConnectX"] C1 e6@--> F["InfiniBand / RoCE"] F e7@--> C2["ConnectX"] C2 e8@--> G2["Remote GPU"] e1@{ animate: true } e2@{ animate: true } e3@{ animate: true } e4@{ animate: true } e5@{ animate: true } e6@{ animate: true } e7@{ animate: true } e8@{ animate: true } classDef k8s fill:#13264a,stroke:#38bdf8,color:#e6f6ff; classDef gpu fill:#173f59,stroke:#22d3ee,color:#ecfeff; classDef net fill:#281d48,stroke:#a78bfa,color:#f1edff; classDef host fill:#302410,stroke:#fbbf24,color:#fff3cf; class N,I,X k8s; class L,G,G2 gpu; class C1,F,C2 net;
The intended fast paths. Verify the selected transport in NCCL logs; fallback paths can also work.

Set NCCL_DEBUG=INFO on a diagnostic run and inspect the selected network transport. Compare a collective benchmark such as nccl-tests on one node and across nodes with the same GPU count and message sizes. The NCCL environment variable reference describes transport controls and debug logging.

vLLM and tensor parallelism#

Tensor-parallel execution needs constant GPU-to-GPU communication. With a weak interconnect, the model becomes communication-bound: the GPUs have capacity, but they spend their time waiting on each other.

Narrow it down by scale#

What worksWhat failsThink about
NothingOne GPUGPU, driver, CUDA, app
One GPU8 GPUs, same nodeNCCL, NVLink/NVSwitch, PCIe/topology
8 GPUs, same nodeMulti-nodeNCCL, RDMA, ConnectX, RoCE or InfiniBand, fabric

This is the trap to remember:

Pod Running  ✓
GPU allocated ✓
AI performance still terrible

Kubernetes can look perfectly healthy while the bottleneck sits in the communication path below the application.

Burn this into memory#

PCIe            = general device interconnect
NVLink          = high-bandwidth NVIDIA GPU/device link
NVSwitch        = switching fabric for NVLink GPUs
NCCL            = optimized GPU collective communication library
ConnectX        = high-performance NIC
RDMA            = low-overhead remote memory communication
GPUDirect RDMA  = efficient GPU ↔ NIC path

The one-page version#

When a GPU workload misbehaves, walk up the layers and stop at the first one that fails.

LayerQuestionFirst tool
DriverDoes the host see the GPU?nvidia-smi
Device PluginDoes Kubernetes see it?Node allocatable: nvidia.com/gpu
Container ToolkitCan the container use it?nvidia-smi inside the Pod
TopologyAre GPU, CPU, and NIC close together?nvidia-smi topo -m, numactl --hardware
Multus + SR-IOVDoes the Pod have net1?ip a in the Pod
RDMADoes the fast path work?rdma link, ibv_devinfo, fabric counters
NCCLDo the GPUs talk fast?NCCL logs and tests, single node then multi-node

The GPU is only one part of the platform. Most of the hard problems live in the paths between the parts.

Related posts: to practice GPU scheduling without any hardware, try the laptop GPU simulation lab.