RDMA in Kubernetes: the NVIDIA Network Operator
S4·E5Nobody proved the fabric, and acceptance is at nine · Hotel lobby, an hour before the acceptance meeting
Builds on: Triage in order: symptom to counter to cause, GPUDirect RDMA: proving it is actually on
Before you read: what do you already know?
3 quick questions. Wrong answers are fine and expected; trying first makes the lesson stick.
After this lesson you can
- Break the Network Operator into the components it installs and the CRDs that drive each one.
- Trace an RDMA pod from NicClusterPolicy through MacvlanNetwork and IPPool to the resource request in the pod spec.
- Differentiate the shared device plugin path from the SR-IOV path by the isolation and QoS each gives.
- Analyse a failing RDMA pod and decide whether the fault is in Kubernetes or in the fabric underneath it.
Episode 5 — Nobody proved the fabric, and acceptance is at nine
You are in a hotel lobby with a laptop, a coffee the SE bought two hours ago, and pod specs the customer’s platform team sent overnight. Their message says the NVIDIA Network Operator is broken: one pair of RDMA pods dies instantly with a memory registration error, another pair runs at a fifth of line rate, and the evidence for both is a failing training job. Acceptance is at nine, and the rented capacity they are leaving lapses this week.
The operator exists to do in Kubernetes what the last four episodes did by hand — it installs the DOCA-OFED driver, the RDMA shared device plugin, the SR-IOV device plugin, Multus, Whereabouts and the NVIDIA IPAM plugin, and it is driven by NicClusterPolicy and the network CRDs.[1] Driver install, resource advertisement, secondary interfaces, addresses. That is its whole job, and none of it is congestion control.
Which is why the two symptoms in the mail are not one bug. A registration failure happens before the first byte moves, so it cannot be congestion, and in a container the equivalent of unlimited memlock is the IPC_LOCK capability the sample pods carry.[5][3] A fifth of line rate between pods that do move data is the counter map and the switch policy, exactly as on bare metal.[10]
Kubernetes does not create a fabric; it schedules pods onto one that already works. You have an hour to turn one Kubernetes ticket into two honest ones.
1What the operator is, and what it is made of
The NVIDIA Network Operator is the Kubernetes side of everything the previous four lessons configured by hand. The current documented release is v26.7.0, and the version index runs from v26.7.0 back through v26.4.1, v26.1.x, v25.10.0, v25.7.0 and earlier to v23.5.0.[1][2] It is described as software for enabling AI networking in Kubernetes, and it is designed to work alongside the GPU Operator to give “high-throughput, low-latency networking for scale-out, GPU computing clusters”.[1]
It installs a set of components rather than one thing: the containerized NVIDIA DOCA-OFED networking driver, the Kubernetes RDMA shared device plugin, the SR-IOV Network Device Plugin, Multus, the container networking plugins, Whereabouts, the NVIDIA IPAM plugin, NVIDIA NIC Feature Discovery and the Maintenance Operator.[1] Between them they cover driver installation, resource advertisement, secondary interfaces and address management, which is exactly the list of things a bare-metal RDMA host solves with packages and static configuration.[1]
The control surface is custom resources. The primary CRDs are NicClusterPolicy for cluster-wide NIC configuration and MacvlanNetwork, HostDeviceNetwork and IPoIBNetwork for the networks themselves; newer API groups add NIC configuration and firmware templates under configuration.net.nvidia.com and a Spectrum-X rail pool configuration under spectrumx.nvidia.com.[1] One operational detail circulates widely and cannot be confirmed on the pages fetched: that the operator acts only on a NicClusterPolicy named nic-cluster-policy and ignores instances with other names. Treat that as unverified and simply use the documented name, which costs nothing.[1]
This lesson is also the whole of NCP-AIN Domain 4, worth 5 percent of the exam, whose two objectives are deploying the Network Operator to manage RDMA interfaces in Kubernetes clusters and verifying its functionality.[9] The study guide’s reading list for that domain still names Network Operator v25.7.0 while v26.7.0 ships, which is a useful thing to know before the exam and a useful thing to say to a customer comparing documents.[9][2]
Diagram source (Mermaid)
flowchart LR; A["NicClusterPolicy: rdmaSharedDevicePlugin"] --> B["Device plugin advertises rdma/rdma_shared_device_a"]; C["IPPool: subnet + perNodeBlockSize + gateway"] --> D["MacvlanNetwork: master = PF netdev - ipam nv-ipam"]; D --> E["Pod annotation k8s.v1.cni.cncf.io/networks"]; B --> F["Pod requests + limits rdma/rdma_shared_device_a: 1"]; E --> G["Pod: second interface with an address"]; F --> G; H["securityContext capabilities add IPC_LOCK"] --> G; G --> I["kubectl exec -- ibv_devinfo"]
2The shared-device path, object by object
NVIDIA’s quick start gives a working NicClusterPolicy for the shared path.[3]
apiVersion: mellanox.com/v1alpha1
kind: NicClusterPolicy
metadata:
name: nic-cluster-policy
spec:
rdmaSharedDevicePlugin:
image: k8s-rdma-shared-dev-plugin
repository: nvcr.io/nvidia/mellanox
version: network-operator-v25.7.0
config: |
{
"configList": [
{
"resourceName": "rdma_shared_device_a",
"rdmaHcaMax": 63,
"selectors": { "ifNames": ["ens1f0"] }
}
]
}Two fields carry all the meaning. rdmaHcaMax: 63 is the sharing factor — the plugin README defines it as the “Maximum number of RDMA resources that can be provided by the device plugin resource”, and each pod requesting the resource consumes one unit, so 63 pods may share the HCA. The quick start prints the field without defining it.[3][12] selectors.ifNames is the netdev, not the RDMA device name.[3] That second point is a real operational hazard on a RoCE host: bonding into RoCE LAG changes the RDMA device name to mlx5_bond_0 while the selector keeps naming Ethernet interfaces, and any renaming scheme that moves ens1f0 silently stops the resource from being advertised.[3][7]
The network side is a MacvlanNetwork whose master is the PF netdev, with mode: "bridge", an mtu, a networkNamespace, and an ipam block of {"type":"nv-ipam","poolName":"macvlan-pool"}; the pool itself is an IPPool in nv-ipam.nvidia.com/v1alpha1 with subnet, perNodeBlockSize and gateway.[3]
The pod then has to ask for three things at once: the annotation k8s.v1.cni.cncf.io/networks: macvlan-network, the capability IPC_LOCK in securityContext.capabilities.add, and both requests and limits of rdma/rdma_shared_device_a: 1.[3] Missing the annotation gives a pod with no second interface; missing IPC_LOCK gives a pod that has the device and cannot register memory; missing the resource gives a pod that may be scheduled where the device does not exist.[3][5]
Verification is two commands, both documented: kubectl exec -it macvlan-test-pod -- ip addr show and kubectl exec -it macvlan-test-pod -- ibv_devinfo.[3]
3Shared or SR-IOV, and what GPUDirect adds
The design question a customer asks is which path to use, and it decomposes cleanly.[1][3]
The shared device plugin puts many pods on one physical function. Everything shares the PF’s queues, its GID table, and its QoS context, and the sharing factor is rdmaHcaMax. That is the right shape for storage clients and inference services, where the pods are numerous, individually modest, and not adversarial.[3]
SR-IOV gives each pod a virtual function through the SR-IOV Network Device Plugin.[1] A VF per pod is what buys isolation and a per-tenant QoS story, which is what a training job with a service-level expectation needs. It also inherits the bare-metal rule that NVIDIA states directly — “Setting the Trust State mode shall be done before enabling SR-IOV in order to propagate the Trust State to the VFs” — so a trust state set after the VFs exist does not reach them.[11]
GPUDirect is a separate axis that the GPU Operator owns. Its helm surface is small: current releases enable DMA-BUF by default, --set driver.rdma.useHostMofed=true covers host-installed drivers, --set driver.rdma.enabled=true selects the legacy nvidia-peermem path, and --set gds.enabled=true adds GPUDirect Storage.[4] The legacy path carries its bare-metal ordering trap into the cluster: if the GPU driver was installed before the network drivers, it must be uninstalled and installed again so nvidia-peermem builds against the right RDMA APIs.[6] Inspecting the rollout is kubectl describe ds -n gpu-operator nvidia-driver-daemonset, where the mofed-validation init container should be present.[4]
The cross-pod proof NVIDIA documents is a perftest pair: on the server kubectl exec -it demo-pod-1 -- ib_write_bw --use_cuda=0 --use_cuda_dmabuf -d mlx5_0 -a -F --report_gbits -q 1, with the client adding -n 5000 and the server pod’s IP.[4]
1. BFB CR created
The BFB controller downloads spec.url ("Download the BFB from a remote server") and records status.versions {bsp, doca, uefi, atf} read from the image. v26.4.1 matrix: BFB 3.4 fully supported, 3.2 LTS allowed, non-LTS BFBs (3.3, 3.5) only with their matching DPF release.
DPU phases: BFB.status.phase: Initializing → Downloading → Ready
DOCA Platform Framework (DPF) v26.4.1
DPF "is a system that provisions and orchestrates NVIDIA BlueField DPUs" from Kubernetes: the operator installs controllers in the host cluster, CRs describe the desired image (BFB), sizing (DPUFlavor) and node set (DPUSet), each card becomes a DPU object, and the DPUs join a separate tenant control plane where DPUServices (Helm charts) run.
Why it matters. GA in DOCA 3.5.0. Latest tag v26.4.1 (2026-08-31); v26.4.0 shipped 2026-06-29. Requirements (v26.4.1): Kubernetes 1.33–1.36, Helm 3.5+, DPU firmware ≥ 32.38.1002, MFT 4.33.0-169, Ubuntu 24.04 + containerd; B3240/B3220/B3210; Socket Direct unsupported.
FAE note. Click any box for a YAML skeleton and what it owns. Switch trust mode to see who provisions, then walk the Reconcile stepper.
Source: networking-docs.nvidia.com · fields marked UNVERIFIED were not confirmed against the Go types.
4Kubernetes does not create a fabric
The single most useful sentence to say on a Dell AI Factory call: Kubernetes only plumbs a RoCE fabric that already works.[10][7] The node still needs the same GID selection, trust state, PFC and ECN configuration as bare metal, and the switches still need their half of the policy — RoCE data on priority 3, CNP on priority 6, an ECN threshold, and PFC on every hop if the design is lossless.[10]
So the failures split into two piles, and telling them apart is the analysis skill this lesson is for.[3][5]
| Symptom | Kubernetes-side or fabric-side | First check |
|---|---|---|
| Pod stuck Pending on a node that has the NIC | Kubernetes | Is the resource advertised? kubectl describe node for rdma/..., then the device plugin pod’s logs[3] |
| Pod runs but has no second interface | Kubernetes | The k8s.v1.cni.cncf.io/networks annotation and the MacvlanNetwork name[3] |
ibv_devinfo inside the pod is empty |
Kubernetes | Driver rollout and the selectors.ifNames value against the actual netdev name[3] |
| Memory registration fails immediately | Kubernetes | IPC_LOCK in the pod securityContext[5][3] |
| Pods talk but bandwidth is a fraction of line rate | Fabric | The counter map: per-priority bytes and pause on the node, then the switch[10] |
| Two pods fine and 32 pods collapse | Fabric | PFC on every hop and ECN marking, exactly as on bare metal[10] |
| GPU-to-GPU far slower than host-to-host | Node | GPUDirect: driver order, nvidia-peermem or DMA-BUF, NIC-to-GPU distance[4][6] |
The practical consequence is a sequencing rule for any cluster bring-up: prove the fabric between two plain perftest pods before anyone schedules a training job.[4] A failing all-reduce is an expensive way to discover that priority 3 was never trusted on the host.[7]
Report. “The Network Operator is installed. Our pod comes up, ip addr show inside it shows the macvlan interface with an address, and ibv_devinfo shows mlx5_0 with state: PORT_ACTIVE and link_layer: Ethernet. But ib_write_bw between two of these pods dies instantly with a memory registration error, and on a different pair it runs but only reaches a fifth of line rate.”
Split the report into two problems first. They are not the same fault; one pair fails before any data moves and the other pair moves data slowly. A fault that occurs before the first byte cannot be congestion.[5]
Problem A, the instant failure. Registration happens at start-up, so the candidates are memory locking and the device itself. ibv_devinfo already proved the device is present and active, which eliminates the driver and the selector.[3] That leaves locking: in a container the equivalent of unlimited memlock is the IPC_LOCK capability, which NVIDIA’s own sample pods carry for this reason.[5][3] Check: kubectl get pod <name> -o jsonpath='{.spec.containers[0].securityContext.capabilities}'. Fix: add IPC_LOCK to securityContext.capabilities.add.[3]
Problem B, the slow pair. Data moves, so the plumbing is right and the question is which queue it is in. This is the bare-metal counter map applied inside a cluster: on the node, per-priority bytes and pause with ethtool -S; on the switch, its RoCE and PFC counters.[10] If the node shows the bytes on priority 0 rather than the RoCE priority, the trust state or the application marking is the fault and nothing in Kubernetes will fix it.[7] If the bytes are on the right priority and pause is climbing, walk the hops.[10]
What to tell the customer. Problem A is a pod-spec fix. Problem B is a fabric configuration finding that would have hit them identically on bare metal, and the evidence is the node and switch counters, not the pod.[5][10]
Same report. Answer each in one line.
- Why can Problem A not be congestion? ________
- What does a successful in-pod
ibv_devinfoalready eliminate? ________ - The container equivalent of unlimited memlock is ________.
- For Problem B the first counters to read are ________ on the ________ and ________ on the ________.
- Bytes on priority 0 instead of priority 3 means the fault is ________ and lives ________ (in Kubernetes / on the node / on the switch).
New report. “We upgraded the node. Now every RDMA pod on it is Pending. kubectl describe node no longer lists the rdma/rdma_shared_device_a resource. Nothing in the NicClusterPolicy changed. The NIC is up and ibv_devinfo on the node itself is healthy, and it now shows mlx5_bond_0.”
Produce the analysis. Acceptance criteria:
- You identify the object whose configuration no longer matches reality and quote the field by name.[3]
- You explain why a healthy
ibv_devinfoon the node is consistent with a resource that is no longer advertised.[3] - You name the node-side change that caused it and how you would confirm it in one command.
- You give two fixes, one that changes the node and one that changes the CRD, and say which you would recommend to a Dell customer with a large fleet and why.
- You state one thing you would check on the fabric before declaring the incident closed, and why it is not optional.[10]
Two piles, before nine o'clock
IPC_LOCK goes into the pod securityContext and the instant failure disappears; the slow pair stays slow, which is the point.[3][5] Two perftest pods and the per-priority counters put that half back where it belongs: host trust state and switch policy.[10][7]
What you say to the customer: “Kubernetes only plumbs a fabric that already works. Let us prove it with two perftest pods before we schedule another training job against it.”[4][10]
Acceptance signs at 09:40. The network lead’s notebook is full to the back cover, procurement wants to know when the next sixty-four ship, and the SE closes the spreadsheet over a cold coffee. On top of the rack the night operator has left one more label: NOT THE FABRIC — this time everyone can prove it.
Lab
Dell-lab node with a real ConnectX or BlueField-3 PF. All steps that create cluster objects are mutating and are removed in reverse order at the end.
- Pre-flight, read-only. On the node:
ibdev2netdev;ibv_devinfo | grep -E "hca_id|state|link_layer";mlnx_qos -i <netdev> | tee ~/qos-before.txt;ofed_info -s. In the cluster:kubectl get nodes -o wideandkubectl describe node <node> | grep -A5 Capacity.[7] - Install or confirm the Network Operator and apply the
NicClusterPolicywith the real PF netdev inselectors.ifNames.[3] Expected:kubectl describe nodelistsrdma/rdma_shared_device_awith capacityrdmaHcaMax. Rollback: delete the policy and confirm the resource disappears. - Apply the
IPPoolandMacvlanNetwork, launch two pods on different nodes, and verify withip addr showandibv_devinfoin each.[3] Rollback: delete pods, network, pool. - Prove the fabric before anything GPU-related: run
ib_write_bwpod to pod with--tclassset to the RoCE DSCP times four, and readethtool -S <netdev> | grep prio3on both nodes.[10] Expected: priority-3 bytes on both nodes. If they land on priority 0, stop and fix the host trust state; the cluster is not the problem.[7] - GPUDirect check. Confirm the GPU Operator’s RDMA path:
kubectl describe ds -n gpu-operator nvidia-driver-daemonsetand confirm themofed-validationinit container ran; on the nodelsmod | grep -E "nvidia_peermem|nvidia_fs".[4][6] Then run NVIDIA’s documented cross-pod check: serverkubectl exec -it demo-pod-1 -- ib_write_bw --use_cuda=0 --use_cuda_dmabuf -d mlx5_0 -a -F --report_gbits -q 1, client the same with-n 5000and the server pod IP.[4] Expected: GPU numbers close to the host-memory numbers; a third of them means the transfer is bouncing through host memory. - Break it on purpose, once. Remove
IPC_LOCKfrom one pod, re-run, observe the registration failure, restore it.[5] Then, if the node is safe to change, rename or bond the PF netdev and watch the resource disappear fromkubectl describe nodewhileibv_devinfoon the node stays healthy. Rollback: restore the interface configuration captured in step 1 and confirm the resource returns. - Restore and verify. Delete the cluster objects in reverse order, re-apply
~/qos-before.txt, and diffmlnx_qos -i <netdev>against the capture. Deliverable: a one-page runbook separating the Kubernetes-side checks from the fabric-side checks, with the exact command for each.
A kind or k3s cluster on the containerlab host, with Soft-RoCE providing the RDMA device. The honest goal is to find out which steps need real hardware.
- Create the cluster and confirm it is healthy:
kubectl get nodesshows Ready. Record the Kubernetes version. - Create the Soft-RoCE device on the node’s netdev (
sudo rdma link add rxe0 type rxe netdev <if>) and confirmibv_devinfoworks on the host before involving Kubernetes. Rollback:sudo rdma link delete rxe0. - Install the Network Operator via helm. Expected: operator pods Running. If Node Feature Discovery is already deployed you may need
--set nfd.enabled=false; that guidance is unverified on the pages cited here, so treat a NFD conflict as a diagnosis rather than a prediction.[1] - Apply a
NicClusterPolicywithrdmaSharedDevicePluginwhoseselectors.ifNamesnames the Soft-RoCE netdev, then check advertisement:kubectl describe node | grep rdma.[3] Expected: a resource with the capacity you set inrdmaHcaMax. If it does not appear, read the device plugin pod’s logs and compare the selector withip -br link. Rollback: delete the policy. - Apply an
IPPooland aMacvlanNetworkover the same netdev, then launch two pods with thek8s.v1.cni.cncf.io/networksannotation,IPC_LOCK, and matchingrequestsandlimitsfor the resource.[3] Rollback: delete the pods, network and pool. - Verify inside the pods with the documented commands:
kubectl exec -it <pod> -- ip addr showandkubectl exec -it <pod> -- ibv_devinfo.[3] Expected: an address on the second interface and an RDMA device. - Run
ib_write_bwpod to pod. Expected on Soft-RoCE: it works but at software speeds. - Deliberately remove
IPC_LOCKfrom one pod and re-run. Expected: a memory-registration failure at start.[5] Restore it. This is the fault you will diagnose most often. - Write down every step that failed or was meaningless without a real HCA — GPUDirect, per-priority queueing, SR-IOV VFs — and why. That list is the deliverable.
Retrieval check
10 questions from memory. Answer before looking anything up; misses become flashcards.
Explain it to a Dell SE
Explain to a Dell platform engineer in five sentences what the NVIDIA Network Operator does and does not do for their RoCE cluster.
Sources
Facts in this lesson were checked against NVIDIA Network Operator v26.7.0 index re-fetched 2026-09-07 (version and CRD groups confirmed), v25.7.0 MacVLAN RDMA-shared quick start, GPU Operator GPUDirect RDMA page, NCCL 2.31.2 troubleshooting, NCP-AIN study guide doc 4417000 MAY26. Dates are when each page was fetched.
- NVIDIA Network Operator v26.7.0 · fetched 2026-09-07
- NVIDIA Cloud Orchestration documentation index (Network Operator versions) · fetched 2026-09-07
- MacVLAN Network with RDMA Shared Device: Network Operator v25.7.0 quick start · fetched 2026-09-07
- GPUDirect RDMA and GPUDirect Storage: NVIDIA GPU Operator · fetched 2026-09-07
- Networking Troubleshooting: NCCL 2.31.2 documentation · fetched 2026-09-07
- GPUDirect RDMA Peer Memory Client: NVIDIA Linux driver README · fetched 2026-09-07
- RDMA over Converged Ethernet (RoCE): MLNX_OFED 24.10-5.1.6.1 LTS · fetched 2026-09-07
- QoS Configuration (DOCA SDK) · fetched 2026-09-07 · DOCA 3.5.0
- NVIDIA-Certified Professional: AI Networking Exam Study Guide (doc 4417000 MAY26) · fetched 2026-09-07
- RDMA over Converged Ethernet (RoCE): Cumulus Linux 5.18 · fetched 2026-09-07
- Ethernet QoS (DOCA SDK 3.5.0) · fetched 2026-09-07 · DOCA 3.5.0
- k8s-rdma-shared-dev-plugin README (Mellanox) · fetched 2026-09-07
The same idea elsewhere
Other lessons that cover this ground, sometimes from another course's angle.
- Network Operator and the NicClusterPolicyK8sNet course · Same ground: NicClusterPolicy, crd and gpudirect
- The Kubernetes network model and where a second CNI fitsK8sNet course · Same ground: certification, nv-ipam and bluefield
- RDMA in pods: shared plugin vs exclusive netnsK8sNet course · Same ground: verification, pod and shared