Skip to content

RDMA in pods: shared plugin vs exclusive netns

S2·E3The node that was hardened at 02:10 · A phone call from the night shift, day three of acceptance week

S2·E3Analyze~30 minsources checked todaylab mutates hardwareverified against k8s-rdma-shared-dev-plugin README and issue 87, rdma-cni README, rdma-system(8), NVIDIA Network Operator 26.7.0 deployment guide and Spectrum-X verify page - all re-fetched 2026-09-07

Builds on: How kubelet learns a NIC exists, SR-IOV plumbing: device plugin, CNI and operator

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

  • Contrast the shared RDMA device plugin and SR-IOV plus rdma-cni by what each one actually isolates.
  • Predict the outcome of every combination of rdma system netns mode and deployed design including the two failure strings.
  • Order the prerequisites and the config edits that put an isolated RDMA device inside a pod.
  • Justify a design choice for a given node and name the evidence that would falsify it.

Episode 3 — The node that was hardened at 02:10

The situation · A phone call from the night shift, day three of acceptance week

The operator is standing at the rack with the label maker still in one hand. One node out of twelve has rdma-shared-dp-ds in CrashLoopBackOff. The DaemonSet is identical on all twelve, the image is identical, the ConfigMap is identical, and Friday’s run needs all twelve.

You ask for the container log rather than the DaemonSet: Exiting.. can not change : incorrect RDMA subsystem network namespace.[2] Then one command on the node explains the whole cluster - rdma system show reports exclusive. This node was rebuilt for per-tenant isolation by somebody who was told to harden it, and nobody told the platform team.

Neither side is wrong; they are opposite designs, and the fork is in the kernel rather than in Kubernetes. In the default shared mode the “RDMA device is accessible in all network namespaces”, which is exactly the property the shared device plugin sells - it advertises rdmaHcaMax units of one physical HCA and lets every scheduled pod reach it.[4][1] Exclusive mode withdraws that property so a device can be moved into one namespace and seen nowhere else, and it is close to a boot-time decision: switching a live node with namespaces already up returns EBUSY.[4]

rdma system show before you deploy anything RDMA - the mode is a property of the node, and the plugin has to match it.

Segment 1 takes the two designs apart by what each one actually isolates.

1A fork in the road, not a setting

There are two supported ways to get RDMA inside a pod, and a node can only be in one of them. This is the single most useful thing in the module, so state it the way you would to a customer: it is a design fork, not a bug.

Shared. k8s-rdma-shared-dev-plugin “provisions RDMA (InfiniBand and RoCE) HCA resources” and runs as a DaemonSet.[1] Every pod that lands on the node sees the same HCA. The kernel is in shared netns mode, where “RDMA device is accessible in all network namespaces”.[4]

Exclusive. rdma-cni is a “CNI compliant plugin for network namespace aware RDMA interfaces” that provides “network namespace isolation for RDMA workloads in a containerized environment”.[3] It is chained after an SR-IOV delegate and moves “the associated RDMA interfaces of the provided network interface to the container’s network namespace path”.[3] The kernel is in exclusive mode, where the manual page says exclusive “should be set before creating any network namespace” so that a dedicated RDMA device can be assigned to a particular namespace.[4]

The two are incompatible because they need opposite kernel states. Shared needs devices visible everywhere; exclusive needs them visible in exactly one place.[4] Deploy the shared plugin on an exclusive node and its pod rdma-shared-dp-ds sits in CrashLoopBackOff.[2]

Choosing is a workload question. Several small jobs that trust each other on one card, or an IPoIB/macvlan secondary path? Shared. Multi-tenant, one VF per pod, per-pod isolation and Spectrum-X style rails? Exclusive.[11]

rdma system set netns
deployed design
node state when you set it
0 / 8 combinations revealed
Kubernetes node — click a box to explain itpod A netnsrdma link → mlx5_0 (same device as the other pod)pod B netnsrdma link → mlx5_0 (same device as the other pod)host netnsibstat · ibv_devinfo · rdma system showrdma-shared-dp-ds (device plugin)rdma/rdma_shared_device_a × rdmaHcaMax 63kernel RDMA subsystem (ib_core)netns sharedrdma system show → netns sharedmlx5_0 — physical HCA (ens1f0)RoCEv2 = IP + UDP 4791SR-IOV VFs — mlx5_2, mlx5_3 …unused by the shared plugin
Predict before you look

shared mode · Shared device plugin · pods already running. Does the node come up ok, does a pod CrashLoopBackOff, or does the command itself fail?

Kernel RDMA subsystem (ib_core) — netns mode

Current: “RDMA devices are shared among network namespaces.”

One switch, node-wide, two values. `rdma system show` prints the netns mode, privileged-qkey state and monitoring support.

“If there are active network namespaces and if one or more RDMA devices exist, changing mode from shared to exclusive returns error code EBUSY.”

Persist it with `options ib_core netns_mode=0` in /etc/modprobe.d/ib_core.conf rather than setting it at runtime; the runtime path is the one that hits EBUSY.

rdma system show
rdma system set netns exclusive
rdma system set netns shared
echo "options ib_core netns_mode=0" >> /etc/modprobe.d/ib_core.conf

FAE angle: ask for this one line of output before any other question about RDMA in pods. It tells you which half of the fork the customer is standing on.

rdma-system(8) · rdma-cni

Start from the working shared design. Flip only the kernel mode and predict the outcome before revealing it; then flip the design too and find the one combination that returns EBUSY rather than crash-looping.

2Shared: a counter with a required number

The shared config is one JSON document with a configList of pools:

{"periodicUpdateInterval": 300,
 "configList": [{
    "resourceName": "hca_shared_devices_a",
    "resourcePrefix": "example_prefix",
    "rdmaHcaMax": 1000,
    "devices": ["ib0","ib1"]
 }]}
[1]

rdmaHcaMax is the “maximum number of RDMA resources that can be provided by the device plugin resource”; it is required and has no default.[1] resourcePrefix defaults to rdma, which is where the familiar rdma/rdma_shared_device_a comes from.[1][5] periodicUpdateInterval defaults to 60 seconds if unset and is disabled at 0.[1]

Selectors are vendors (hex, such as "15b3"), deviceIDs (hex), drivers ("mlx5_core"), ifNames ("ib0") and linkTypes ("ether").[1] Matching is “logical OR between elements of a specific selector and logical AND … between selectors”, and unspecified selectors are ignored.[1] That last clause is why a config carrying only ifNames can capture more devices than the author expected.

In the Network Operator flow this is a block of NicClusterPolicy, with rdmaHcaMax: 63 and selectors: {ifNames: ["ens1f0"]} producing rdma/rdma_shared_device_a.[5] The pod then requests rdma/rdma_shared_device_a: 1 in both requests and limits and adds IPC_LOCK.[5]

Now the honesty note. The README does not document how devices are exposed to the container - whether /dev/infiniband is mounted, and whether rdmaHcaMax maps to an rdma cgroup limit. Treat the internal mechanism as unverified: confirm it on the node rather than describing it to a customer.[1]

Because extended resources are integer and non-shareable, rdmaHcaMax is the only lever you have, and what it limits is how many pods land here, not what any of them may do.[9]

3Exclusive: chain rdma after sriov

Exclusive mode is set one of two ways. Persistently, echo "options ib_core netns_mode=0" >> /etc/modprobe.d/ib_core.conf and reboot.[3] At runtime, rdma system set netns exclusive.[4]

The ordering rule is the part people learn the hard way: “If there are active network namespaces and if one or more RDMA devices exist, changing mode from shared to exclusive returns error code EBUSY.”[4] The manual page also advises against changing mode while RDMA traffic is running, even though it is supported.[4] On a node that has already joined a cluster, that makes the modprobe-plus-reboot path the realistic one.

Prerequisites are explicit: kernel 5.3.0 or newer (or an out-of-tree driver from MLNX_OFED 4.7+), an iproute2 built against 5.3.0 or newer, Multus CNI v3.4.1 or newer, SR-IOV capable hardware and an SR-IOV supporting CNI; Kubernetes 1.16+ for the DaemonSet deployment.[3]

The CNI config itself is trivially small:

{"cniVersion":"0.3.1","type":"rdma","args":{"cni":{"debug":true}}}
[3]

In a NAD it is simply the second plugin in the array:

{"cniVersion":"0.3.1","name":"sriov-network","plugins":[
  {"type":"sriov","ipam":{"type":"host-local","subnet":"10.56.217.0/24"}},
  {"type":"rdma"}]}
[7]

Order is not stylistic. rdma moves the RDMA interfaces associated with a network interface that already exists, so the delegate that creates that interface has to run first.[3] The InfiniBand variant is the same shape with ib-sriov and a PKey: {"type":"ib-sriov","pkey":"0x6","link_state":"enable","ibKubernetesEnabled":true,...} followed by {"type":"rdma"}.[5]

One documented limitation to carry into a design review: “RDMA workloads utilizing RDMA Connection Manager on Mellanox hardware require pre-allocated MAC addresses for all VFs due to kernel constraints.”[3]

primary
delegate
ipam
options
pod ns: default · securityContext IPC_LOCKeth0primary CNInet1sriov-netMultus CNI (meta-plugin)thick: multus-daemon + multus-shimCiliumowns eth0 + policysriovVFrdmachained, after sriovnvidia.com/mlnx_sriov_netdevicerequested by the podnv-ipamaddress assignmenthost: ens1f0 · VF 0000:03:02.3 · mlx5_core

Checks

no forbidden combination
  • warnNeeds kernel 5.3.0+, iproute2 5.3.0+, Multus v3.4.1+ and exclusive namespaces (rdma system set netns exclusive, or options ib_core netns_mode=0) — set before namespaces exist, or it returns EBUSY. This node can then no longer run k8s-rdma-shared-dev-plugin: rdma-shared-dp-ds crash-loops with "Exiting.. can not change : incorrect RDMA subsystem network namespace". A design fork, not a setting. [rdma-cni prerequisites + netns modes]
  • warnresourceName must match a name the device plugin advertised, prefix included: sriov-network-device-plugin defaults to intel.com, the Network Operator writes nvidia.com, OpenShift writes openshift.io. A mismatch keeps the pod Pending with no CNI error at all (kubectl describe node | grep -A20 Allocatable). deviceID is never hand-written — Multus passes the allocated VF. [resourcePrefix default is intel.com]
  • infonv-ipam needs the matching CR: poolType "ippool" reads an IPPool (subnet, gateway, perNodeBlockSize), "cidrpool" a CIDRPool (cidr, gatewayIndex, perNodeNetworkPrefix). Deployed by NicClusterPolicy.spec.nvIpam. [nv-ipam poolType must match an existing CR]
  • infoCilium’s CNI-chaining docs never mention Multus. If secondary interfaces vanish after an upgrade, ask whether generic-veth chaining is in use — and whether L7 policy went with it. [Cilium + Multus is untested in Cilium’s docs]
selected block

Delegate — sriov

Slices a PF into VFs. Multus gets the allocated VF’s deviceID from the device plugin and invokes sriov with it.

  • deviceID is required — a valid PCI address of an SR-IOV NIC’s VF, e.g. "0000:03:02.3". Do not hand-write it: Multus passes the allocated VF.
  • vlan 0–4094, vlanQoS 0–7, vlanProto default "802.1q" (or "802.1ad"), mac, spoofchk on/off, trust on/off, link_state auto|enable|disable, min_tx_rate / max_tx_rate in Mbps.
  • The DHCP IPAM plugin cannot be used for a VF bound to a DPDK driver (uio/vfio).
  • Device-plugin side: resourceList entries take resourceName (required), resourcePrefix, deviceType (netDevice default, accelerator, auxNetDevice) and selectors (vendors, devices, drivers, pciAddresses, pfNames, rootDevices, linkTypes, isRdma).
  • Operator route: SriovNetwork (SR-IOV Network Operator) generates the NAD from a resourceName; SriovNetworkNodePolicy creates the VFs.

FAE angle: the pod also gets PCIDEVICE_<RESOURCE_NAME> and PCIDEVICE_<RESOURCE_NAME>_INFO env vars (upper-cased, "." and "/" → "_") — the fastest in-pod proof that the VF was really allocated.

source
apiVersion: k8s.cni.cncf.io/v1
kind: NetworkAttachmentDefinition
metadata:
  name: sriov-net
  namespace: default
  annotations:
    k8s.v1.cni.cncf.io/resourceName: nvidia.com/mlnx_sriov_netdevice
spec:
  config: |
    {
      "cniVersion": "0.3.1",
      "name": "sriov-net",
      "plugins": [
        {
          "type": "sriov",
          "vlan": 100,
          "spoofchk": "off",
          "trust": "on",
          "link_state": "enable",
          "ipam": {
            "type": "nv-ipam",
            "poolName": "pool1",
            "poolType": "ippool"
          }
        },
        {
          "type": "rdma"
        }
      ]
    }

The annotation key spelling k8s.v1.cni.cncf.io/resourceName is not quoted on the pages fetched for this course; the rule that a NAD resourceName must match an advertised device-plugin resource is.

Documented bare-metal Ethernet example name; the real name is whatever your device plugin resourceList advertised.

Operator route: SriovNetwork (SR-IOV Network Operator) generates the NAD from a resourceName; SriovNetworkNodePolicy creates the VFs. Network Operator deployment guide · Multus how-to-use · quick start
Build the exclusive design and read the emitted NAD. Then try to place the rdma plugin first and see which documented rule the validator quotes back at you.

4The collision, in the customer's words

Two error signatures resolve most of this lesson’s field cases in under ten minutes.

Signature one - wrong pairing. The shared plugin started on a node whose RDMA subsystem is exclusive. The pod rdma-shared-dp-ds is in CrashLoopBackOff and the log line is verbatim:

Exiting.. can not change : incorrect RDMA subsystem network namespace
[2]

The issue has been open since 2023 with no maintainer resolution captured on the page, so present it as an observed incompatibility rather than as vendor guidance.[2]

Signature two - too late. Someone tried to fix signature one by flipping the mode on a running node and got EBUSY from rdma system set netns exclusive.[4] Active namespaces plus RDMA devices means the window has closed; the fix is the modprobe line and a reboot.[3]

The inverse case appears in NVIDIA’s own Spectrum-X troubleshooting: the symptom “RDMA visibility across Pods” is fixed by enabling RDMA subsystem namespace awareness with options ib_core netns_mode=0 and a reboot.[6] Read those two pages together and the picture is consistent: NVIDIA’s isolated designs assume exclusive, the shared plugin assumes shared, and neither document is wrong.

Spectrum-X shows what exclusive looks like at scale. Each rail gets the RDMA CNI with rdmaQoS: {tos: 96, tc: 96}, and the pod’s RDMA device is named rdma_<rail topology name>.[11]

5Choosing, and defending the choice

Two nodes, two answers

Node A - a shared inference node. Four small serving pods, one ConnectX-7 on ens1f0, macvlan secondary interfaces, one tenant, no VF budget worth managing.

Design: shared. Kernel stays in the default shared mode, where “RDMA device is accessible in all network namespaces”.[4]

NicClusterPolicy carries an rdmaSharedDevicePlugin block with rdmaHcaMax: 63 and selectors: {ifNames: ["ens1f0"]}, advertising rdma/rdma_shared_device_a.[5]

Pod spec: annotation k8s.v1.cni.cncf.io/networks: example-macvlannetwork, securityContext.capabilities.add: ["IPC_LOCK"], and rdma/rdma_shared_device_a: 1 in both requests and limits.[5]

Verification: kubectl exec -it <pod> -- rdma link lists a device.[6]

Why not exclusive: macvlan pods share the parent’s function, so there is no per-pod RDMA device for rdma-cni to move.[3] NVIDIA’s own quick start splits its use cases along the same line, pairing “IP over InfiniBand with RDMA Shared Device” and “MacVLAN Network with RDMA Shared Device” against “SR-IOV Network with RDMA” and “Host Device Network with RDMA”.[13]

Node B - a training node, multi-tenant, eight VFs, per-pod isolation required.

Design: exclusive. Set it before the node joins: echo "options ib_core netns_mode=0" >> /etc/modprobe.d/ib_core.conf and reboot, which sidesteps the EBUSY rule entirely.[3][4]

Device plugin: SR-IOV, resourcePrefix: nvidia.com, selectors: {vendors: ["15b3"], isRdma: true}.[8]

NAD: plugins: [{"type":"sriov", ...},{"type":"rdma"}], in that order.[7][3]

Pod: requests the SR-IOV resource, adds IPC_LOCK.[5]

Falsification: if rdma system show on node B says shared, the isolation you sold does not exist, whatever the NAD says.[4]

One node, twelve nodes

How it ended

The node is not broken; it was built for the other design. You either return it to shared mode and let the shared plugin run, or you finish the exclusive path it was started on - VFs advertised with isRdma: true, a NAD chaining sriov then rdma, pods that request the VF resource.[8][3] Either is defensible. Running one node differently from eleven others, two days before an acceptance run, is not.

Twelve nodes agree by morning. The run still lands at a third of the number on the contract, and the network lead is already drafting a case against the switch vendor.

Lab

Mutating steps ahead. Needs a maintenance window, out-of-band access (BMC/iDRAC/rshim console) and a rollback path. Record the pre-flight inventory before changing anything. Never on a production host.

On the Dell-lab BlueField-3 host in NIC mode with a single-node Kubernetes cluster. Steps 4 and 5 change node-wide state; read both rollbacks first.

  1. Pre-flight inventory, saved to a file. rdma system show; lsmod | grep -E 'mlx5|ib_core'; ibstat; ibv_devinfo; mst status; ip -br link show; kubectl describe node <node> | grep -A20 Allocatable; kubectl get pods -A -o wide. Those first five are the commands NVIDIA’s own sos-report collector runs on the node.[12]

  2. Collect a support bundle before you touch anything. kubectl netop-sosreport --verbose --log-lines 10000 Expected: a tarball. It captures previous-container logs that the next steps will destroy.[12]

  3. Deploy the shared plugin. Configure rdmaHcaMax: 63 with selectors: {ifNames: ["<pf>"]} and let it advertise rdma/rdma_shared_device_a.[5] Expected: kubectl describe node <node> | grep -A20 Allocatable shows rdma/rdma_shared_device_a: 63. If not: check the socket in /var/lib/kubelet/device-plugins/ first, then the selectors. Rollback: delete the DaemonSet and its ConfigMap.

  4. Run a pod that uses it. Request rdma/rdma_shared_device_a: 1 in requests and limits and add securityContext.capabilities.add: ["IPC_LOCK"].[5] Expected: kubectl exec -it <pod> -- rdma link lists a device and kubectl exec -it <pod> -- ibv_devinfo shows the port state.[6] If not: check that IPC_LOCK was added - every NVIDIA RDMA pod example includes it.[5] Rollback: delete the pod.

  5. Flip the node to exclusive and observe the collision. rdma system set netns exclusive Expected: either EBUSY (namespaces are already active - which is itself the documented result and a valid observation) or success followed by rdma-shared-dp-ds entering CrashLoopBackOff with Exiting.. can not change : incorrect RDMA subsystem network namespace.[4][2] Rollback: rdma system set netns shared, then delete the crash-looping plugin pod so it restarts cleanly. Confirm with rdma system show before moving on.

  6. Optional, needs a reboot window - the persistent form. echo "options ib_core netns_mode=0" | sudo tee -a /etc/modprobe.d/ib_core.conf then reboot.[3] Expected: after reboot rdma system show reports exclusive with no EBUSY, because the mode was set before namespaces existed. Rollback: remove that line from /etc/modprobe.d/ib_core.conf and reboot again. Do not attempt to undo it at runtime on a node with workloads - that is the EBUSY case.

  7. Build the exclusive path. SR-IOV device plugin with isRdma: true, a NAD chaining {"type":"sriov"} then {"type":"rdma"}, a pod requesting the SR-IOV resource with IPC_LOCK.[8][7][5] Expected: kubectl exec -it <pod> -- rdma link lists a device that the host no longer shows in the default namespace. Rollback: delete pod, NAD and device plugin, then return the node to shared mode per step 4.

  8. Diff against step 0 and confirm the node is back in its original mode and resource set.

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, the two ways a pod can end up with RDMA, why a node has to pick one, and what the wrong pairing looks like in kubectl.

14 flashcards for this lesson — 0 in deck. Spaced review lives at /review.

Sources

Facts in this lesson were checked against k8s-rdma-shared-dev-plugin README and issue 87, rdma-cni README, rdma-system(8), NVIDIA Network Operator 26.7.0 deployment guide and Spectrum-X verify page - all re-fetched 2026-09-07. Dates are when each page was fetched.

  1. GitHub - Mellanox/k8s-rdma-shared-dev-plugin · fetched 2026-09-07
  2. Issue 87 - When netns is set to exclusive mode rdma-shared-dp-ds will not start · fetched 2026-09-07
  3. GitHub - k8snetworkplumbingwg/rdma-cni · fetched 2026-09-07
  4. rdma-system(8) - Linux manual page · fetched 2026-09-07
  5. NVIDIA Network Operator v26.7.0 - Deployment Guide with Kubernetes · fetched 2026-09-07
  6. NVIDIA Network Operator v26.7.0 - Spectrum-X Verify and Troubleshoot · fetched 2026-09-07
  7. NVIDIA Network Operator v23.7.0 - K8s on Bare Metal - Ethernet · fetched 2026-09-07
  8. GitHub - k8snetworkplumbingwg/sriov-network-device-plugin · fetched 2026-09-07
  9. Kubernetes - Device Plugins · fetched 2026-09-07
  10. Multus CNI - How to use · fetched 2026-09-07
  11. NVIDIA Network Operator v26.7.0 - Spectrum-X Architecture and Components · fetched 2026-09-07
  12. NVIDIA Network Operator v26.7.0 - SOS-Report Collection Script · fetched 2026-09-07
  13. NVIDIA Network Operator v26.7.0 - Quick Start Guide for Kubernetes · fetched 2026-09-09

The same idea elsewhere

Other lessons that cover this ground, sometimes from another course's angle.