Skip to content

SR-IOV plumbing: device plugin, CNI and operator

S2·E2The NAD that set a priority and no VLAN · A 7 a.m. call, day two of acceptance week

S2·E2Apply~30 minsources checked todaylab mutates hardwareverified against sriov-network-device-plugin and sriov-cni repositories, sriov-network-operator repository, NVIDIA Network Operator 26.7.0 deployment guide, quick start and OpenShift guide - all re-fetched 2026-09-07

Builds on: How kubelet learns a NIC exists

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

  • Write an sriov-network-device-plugin resourceList that advertises ConnectX VFs under a chosen prefix.
  • Configure an SR-IOV CNI delegate with the fields the reference marks required and validate the optional ones against their allowed ranges.
  • Map each of the four SR-IOV Network Operator CRDs to the question it answers.
  • Predict whether a given ConfigMap and pod spec pair will schedule and name the evidence that proves it.

Episode 2 — The NAD that set a priority and no VLAN

The situation · A 7 a.m. call, day two of acceptance week

The prefix is fixed and the pods schedule now, which is progress you get to enjoy for about eleven hours. At seven the next morning every pod on the fast network is failing during attachment, and the customer’s platform engineer is back to suspecting the cards. They are not the problem: these pods reached CNI at all, which already proves the name matched and the scheduler placed them.

You read the delegate config back over the call. type: sriov, no vlan key, vlanQoS: 3. The reference is exact about that pair - vlanQoS must be in the range 0-7, and “This option requires vlan field to be set to a non-zero value. Otherwise, the error will be returned.”[3] A priority was set on a function with no VLAN to carry it, and the plugin refuses rather than guess.

The mistake sits on a seam, and the seam is why this plumbing exists at all. Counting hardware and attaching it are different jobs with different owners: the device plugin decides which function a pod gets, and the CNI only configures the one it is handed - “A metaplugin such as Multus gets the allocated VF’s deviceID (PCI address) and is responsible for invoking the SR-IOV CNI plugin with that deviceID.”[2] Two owners, two vocabularies, one hand-written file that mixed them.

Optional in the reference does not mean independent - check every field against the rule printed next to it.

Segment 1 starts where the name is minted: the device-plugin ConfigMap.

1Three programs, three jobs

SR-IOV in Kubernetes is not one component. It is three, and every escalation gets easier once you can say which one owns the symptom.

The device plugin counts. “The SR-IOV Network Device Plugin is Kubernetes device plugin for discovering and advertising networking resources”, covering SR-IOV VFs, PCI PFs and auxiliary network devices.[1] It turns hardware into a schedulable integer under a name it chooses.

The CNI attaches. “This plugin enables the configuration and usage of SR-IOV VF networks in containers and orchestrators like Kubernetes.”[2] Crucially it does not choose a VF: “A metaplugin such as Multus gets the allocated VF’s deviceID (PCI address) and is responsible for invoking the SR-IOV CNI plugin with that deviceID.”[2]

The operator configures the node. “The SR-IOV Network Operator simplifies the deployment and management of SR-IOV networking in Kubernetes and OpenShift clusters”, and it is what creates VFs and generates the network definitions rather than leaving you to hand-write them.[4]

The sequence is fixed: advertise, schedule, delegate, attach. Multus only reads the k8s.v1.cni.cncf.io/networks annotation after the pod is placed, and the delegate only runs after Multus resolves the NetworkAttachmentDefinition.[8] Everything downstream of scheduling is invisible while a pod is Pending - which is the whole reason the last segment of this lesson exists.

Without the operator the same path still works; you just write more YAML. NVIDIA’s older bare-metal Ethernet page shows exactly that shape: resource name nvidia.com/mlnx_sriov_netdevice with a hand-written NAD whose plugins array holds {"type":"sriov", ...}.[9] Keep that example in mind as the “what the operator is doing for you” reference.

primary
delegate
ipam
options
pod ns: default · securityContext IPC_LOCKeth0primary CNInet1sriov-netMultus CNI (meta-plugin)thick: multus-daemon + multus-shimCalicoowns eth0 + policysriovVFrdmanot chainednvidia.com/mlnx_sriov_netdevicerequested by the podwhereaboutsaddress assignmenthost: ens1f0 · VF 0000:03:02.3 · mlx5_core

Checks

no forbidden combination
  • warnNo {"type":"rdma"} in the chain: the pod gets the VF netdev, but the RDMA device stays in the host namespace — no per-pod isolation. NVIDIA’s IB example chains it: [{"type":"ib-sriov",…},{"type":"rdma"}]. [rdma-cni is what moves the RDMA interface into the pod netns]
  • 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]
  • infoCalico owns eth0 only. Confirm it has not autodetected ens1f0 as its uplink: sharing one interface with the fast path works until Felix reconfigures it, and then RDMA disappears. [primary CNI must not claim the fast-path interface]
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",
      "type": "sriov",
      "vlan": 100,
      "spoofchk": "off",
      "trust": "on",
      "link_state": "enable",
      "ipam": {
        "type": "whereabouts",
        "range": "192.168.2.225/28",
        "exclude": [
          "192.168.2.229/30",
          "192.168.2.236/32"
        ]
      }
    }

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 plain SR-IOV attachment first: Calico keeps eth0, the sriov delegate owns net1. Read the three emitted artifacts - NAD, annotation, resources block - and note that the same string appears in two of them.

2The device-plugin ConfigMap, field by field

The config is a resourceList of pools. Each pool has resourceName (required), resourcePrefix (optional), deviceType (optional), selectors (an object or a list) and additionalInfo (which adds environment variables).[1]

resourcePrefix defaults to intel.com.[1] That single default is why NVIDIA’s own configuration always sets it explicitly: the operator’s sriovDevicePlugin block uses resourcePrefix: nvidia.com with resourceName: hostdev and selectors: {vendors: ["15b3"], isRdma: true}, producing nvidia.com/hostdev.[5]

deviceType takes netDevice (the default), accelerator or auxNetDevice.[1] The netDevice selectors are vendors, devices, drivers, pciAddresses, acpiIndexes, pfNames, rootDevices, linkTypes, ddpProfiles, pKeys, isRdma, needVhostNet and vdpaType.[1] An auxNetDevice pool takes a narrower set that adds auxTypes.[1]

Two of those selectors carry most of the meaning for an NVIDIA fleet. vendors: ["15b3"] is the NVIDIA/Mellanox PCI vendor ID, and isRdma: true restricts the pool to devices that can carry RDMA.[5]

What the pod sees is the other half of the contract. The plugin injects PCIDEVICE_<RESOURCE_NAME> with the comma-separated device IDs and PCIDEVICE_<RESOURCE_NAME>_INFO with JSON holding mount points and metadata; the resource name is upper-cased with . and / replaced by underscores.[1] That is your in-pod proof of which VF you got.

{ "resourceList": [{
    "resourcePrefix": "nvidia.com",
    "resourceName": "hostdev",
    "selectors": { "vendors": ["15b3"], "isRdma": true }
}]}

3The SR-IOV CNI delegate and its ranges

The delegate config is small and almost entirely optional. name and type ("sriov") are required, ipam is optional, and deviceID is required - “A valid pci address of an SRIOV NIC’s VF”, for example "0000:03:02.3".[3]

The optional fields are where a hand-written NAD goes wrong, so learn the ranges rather than the list:

Field Type Rule
vlan int “Value must be in the range 0-4094 (0 for disabled, 1-4094 for valid VLAN IDs)”[3]
vlanQoS int “Value must be in the range 0-7”; “This option requires vlan field to be set to a non-zero value. Otherwise, the error will be returned.”[3]
vlanProto string default "802.1q"; may be "802.1ad"[3]
mac string optional administrative MAC[3]
spoofchk string "on" or "off"[3]
trust string "on" or "off"[3]
link_state string auto, enable or disable[3]
min_tx_rate / max_tx_rate int in Mbps[3]
logLevel string default info; panic, error, warning, info, debug[3]
logFile string default stderr[3]

One documented incompatibility is worth memorising because it produces a pod with an interface and no address: “DHCP IPAM plugin can not be used for VF bound to a dpdk driver (uio/vfio).”[2] If the customer moved a VF to vfio-pci for a VM and left a DHCP IPAM block in the NAD, that is the bug, and lesson 5 covers why the VF was moved in the first place.

Who configured the device plugin?
scheduler comparesdevice plugin → node allocatablenvidia.com/hostdevkubectl describe node | grep -A20 AllocatableNetworkAttachmentDefinitionnvidia.com/hostdevannotation k8s.v1.cni.cncf.io/resourceNamepod specintel.com/hostdevresources.limits / resources.requests= names agree≠ nothing advertises intel.com/hostdev
step 4 / 4
pod/sriov-test
PendingPending — Insufficient intel.com/hostdev
  • The scheduler compares the pod request against node allocatable. Nothing on this node advertises intel.com/hostdev, so no node fits and the pod is never bound.
  • This is silent: there is no CNI error, no container log, no event from Multus — the pod simply stays Pending forever.
NVIDIA Network Operator

4. Verdict — what kubectl shows

FAE angle: the fastest split is which side failed. Pending means the scheduler never found the name — a device-plugin or request problem. ContainerCreating means the name was allocatable and the NAD is wrong. Collect kubectl netop-sosreport --verbose --log-lines 10000 before you change anything: it captures the previous-container logs you are about to destroy.

Evidence that proves it
kubectl describe pod sriov-test | tail -n 5
Warning  FailedScheduling  …  0/3 nodes are available: 3 Insufficient intel.com/hostdev.

The quoted fragment to look for is "Insufficient <resource name>".

⚠ Wording reconstructed — the exact event sentence is not captured verbatim in this course's sources.

kubectl describe node dell-r760-01 | grep -A20 Allocatable
  nvidia.com/hostdev:  8      # intel.com/hostdev is not in this list

Extended resources are integer-only, not overcommittable, and cannot be shared between containers.

ls -l /var/lib/kubelet/device-plugins/
kubelet.sock   sriovdp.sock

The registration socket path /var/lib/kubelet/device-plugins/kubelet.sock is hardcoded. No plugin socket means the plugin never registered and nothing is advertised at all.

What this source writes
# NicClusterPolicy .spec.sriovDevicePlugin.config
{ "resourceList": [{
    "resourcePrefix": "nvidia.com",
    "resourceName": "hostdev",
    "selectors": { "vendors": ["15b3"], "isRdma": true }
}]}
Advertised: nvidia.com/hostdev · allocatable 8
  • NVIDIA pod examples request nvidia.com/hostdev: 1 in both requests and limits, with securityContext.capabilities.add: ["IPC_LOCK"].
  • The GPUDirect RDMA example requests nvidia.com/hostdev: 1 and nvidia.com/gpu: 1 in the same container, so both hint providers vote for NUMA alignment.
  • HostDeviceNetwork carries the same name in its own resourceName field and generates the NAD for you.

FAE angle. Most escalations are a runbook written for one of these four sources being applied to a cluster configured by another. Ask who wrote the ConfigMap before you read any YAML.

Network Operator — Deployment Guide (Kubernetes) · Kubernetes device plugins · Multus how-to · netop-sosreport
Same name, two other routes

vfio-pci for VMs. A SriovNetworkNodePolicy with deviceType: vfio-pci, numVfs: 8, nicSelector.vendor "15b3", pfNames [ens1f0] and deliberately isRdma: false. The VF is passed into the guest through the VFIO userspace interface, so the guest needs mlx5_core and the host needs intel_iommu=on iommu=pt (or amd_iommu=on iommu=pt). The resource-name mechanics are identical: the SriovNetwork references the same resourceName and generates the NAD.

DRA SR-IOV driver (Tech Preview). Dynamic Resource Allocation replaces the plain name match with a ResourceClaimTemplate: deviceClassName sriovnetwork.k8snetworkplumbingwg.io plus a CEL expression device.attributes["k8s.cni.cncf.io"].resourceName == "nvidia.com/sriov_resource". Devices are published as ResourceSlice objects carrying PCIe bus ID, NUMA node and vendor. Needs the dynamicResourceAllocation: true feature gate, is vanilla Kubernetes only (not OpenShift), and is not recommended for production.

KubeVirt SR-IOV integration · DRA SR-IOV driver

The operator advertised nvidia.com but the pod was copied from an upstream example that used the default prefix. Work out which of the three places to change - and which two you must not touch.

4The operator: four CRDs and one privileged namespace

The SR-IOV Network Operator exposes four primary CRDs, and each answers a different question.[4]

  • SriovNetworkNodePolicy - what hardware should this node group present? Its example fields are deviceType (for example netdevice), nicSelector.pfName, nodeSelector, numVfs and resourceName.[4]
  • SriovNetwork - what network do workloads attach to? It generates the NetworkAttachmentDefinition for you.[4]
  • SriovNetworkNodeState - what did the operator actually find? Read-only discovered hardware state.[4]
  • SriovOperatorConfig - global settings.[4]

OVSNetwork and SriovIBNetwork exist alongside them for OVS-offloaded and InfiniBand attachments.[4]

Install is a Helm chart from oci://ghcr.io/k8snetworkplumbingwg/sriov-network-operator-chart, and the namespace must carry pod-security.kubernetes.io/enforce=privileged.[4] The repo states a base requirement of “Kubernetes 1.30+ or OpenShift 4.16+”.[4] In an NVIDIA deployment you normally do not install it separately: the Network Operator chart enables it with --set sriovNetworkOperator.enabled=true, and you verify with kubectl -n nvidia-network-operator get pods.[7]

Two platform-specific adjustments are documented. On OpenShift the config daemon must be kept out of the DOCA driver’s way with a configDaemonNodeSelector patch that includes network.nvidia.com/operator.mofed.wait: "false", and SR-IOV resources use the openshift.io prefix rather than a custom one.[6] In Spectrum-X deployments the operator is passed disablePlugins: [mellanox] plus featureGates: {manageSoftwareBridges: true}, because otherwise its built-in mellanox plugin and the NIC Configuration Operator fight over the same firmware settings.[11]

One honest gap: the repo’s landing page does not state the operator’s node-draining default, and it defers its feature-gate list to separate documents.[4] In Network Operator deployments the Maintenance Operator is the documented drain path, so do not promise a customer standalone drain behaviour you have not read.

5Where numVfs really lives

numVfs appears in two different places and they are not the same knob. In SriovNetworkNodePolicy it is a request the operator applies to a node group.[4] In the NIC Configuration Operator’s NicConfigurationTemplate it is a firmware setting, alongside linkType ("Ethernet" or "Infiniband"), pciPerformanceOptimized, roceOptimized and gpuDirectOptimized.[10] The documentation is blunt about the cost: “A configuration reset triggers a node reboot. Ensure that workloads are drained or that the Maintenance Operator is configured to handle the node maintenance automatically.”[10]

So when a customer says “we set numVfs: 8 and nothing happened”, the answer is a two-part check: what does SriovNetworkNodeState say was discovered, and has the firmware level actually been applied and rebooted.[4][10]

From a bare ConnectX PF to a scheduled pod

A Dell R760 with one ConnectX-7, PF ens1f0, and four VFs already exposed. You want one pod on an RDMA-capable VF, named nvidia.com/sriov_netdevice.

Step 1 - device plugin ConfigMap. resourceName is required, resourcePrefix is not but must be set or you inherit intel.com.[1]

{ "resourceList": [{
    "resourcePrefix": "nvidia.com",
    "resourceName": "sriov_netdevice",
    "deviceType": "netDevice",
    "selectors": { "vendors": ["15b3"], "pfNames": ["ens1f0"], "isRdma": true }
}]}

Reasoning: vendors narrows to NVIDIA silicon, pfNames narrows to this PF so the Calico uplink on another PF is never captured, isRdma keeps non-RDMA functions out.[1]

Step 2 - confirm the advertisement. kubectl describe node r760-01 | grep -A20 Allocatable must show nvidia.com/sriov_netdevice: 4. A zero here means the selectors matched nothing and nothing downstream can work.[12]

Step 3 - the NAD. The delegate is sriov; deviceID is supplied at runtime by Multus, so it is not written in the NAD.[2][3]

{"cniVersion":"0.3.1","name":"sriov-network","plugins":[
  {"type":"sriov","vlan":101,"ipam":{"type":"whereabouts","range":"192.168.2.225/28"}}]}

Reasoning: vlan is inside 0-4094; whereabouts is cluster-wide so two nodes cannot hand out the same address.[3]

Step 4 - the pod. The annotation names the NAD, the resource block names the advertised string, and they must both be the same name the plugin published.[8][5]

metadata:
  annotations:
    k8s.v1.cni.cncf.io/networks: sriov-network
spec:
  containers:
  - name: test
    resources:
      requests: { nvidia.com/sriov_netdevice: "1" }
      limits:   { nvidia.com/sriov_netdevice: "1" }

Step 5 - prove it from inside. kubectl exec -it test -- env | grep PCIDEVICE returns PCIDEVICE_NVIDIA_COM_SRIOV_NETDEVICE with the PCI address of the VF you were given.[1]

Back on the call

How it ended

Adding "vlan": 101 next to the QoS value clears the attachment error, and the engineer stops blaming the cards. Before hanging up you walk the four CRDs once - the policy is the request, SriovNetworkNodeState is what the operator actually found on the node - so the next VLAN change happens in one place instead of in a copied file.[4]

At 02:10 the night-shift operator calls. One node out of the twelve has an RDMA plugin in CrashLoopBackOff, and it is the node wearing the newest label on its cable.

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 a ConnectX host in the Dell lab. Steps 2 and 6 change node state - read the rollback before you start.

  1. Pre-flight inventory, recorded to a file. uname -r; lspci -nn | grep -i mellanox; ip -br link show; cat /sys/class/net/<pf>/device/sriov_totalvfs; cat /sys/class/net/<pf>/device/sriov_numvfs; kubectl describe node <node> | grep -A20 Allocatable; and confirm which PF the primary CNI is using so you never select it. Keep this file - it is your rollback target.

  2. Confirm the PF is not the cluster uplink. If ens1f0 is both the Calico uplink and your intended pfNames selector, stop and pick another PF. A working cluster that silently loses RDMA when the CNI reconfigures the interface is the failure this prevents.

  3. Create VFs. echo 4 > /sys/class/net/<pf>/device/sriov_numvfs Expected: lspci -nn | grep -i mellanox gains four Virtual Function lines; ip link show <pf> lists vf 0 through vf 3. If not: the card may not have SR-IOV enabled in firmware, or sriov_totalvfs is 0 - that is a firmware-level numVfs question and needs the NIC Configuration Operator and a reboot, not a sysfs write.[10] Rollback: echo 0 > /sys/class/net/<pf>/device/sriov_numvfs. Do this before any driver or firmware work, and note that VFs in use by pods will block it - delete the pods first.

  4. Deploy the device plugin with resourcePrefix: nvidia.com, resourceName: sriov_netdevice, and selectors {"vendors":["15b3"],"pfNames":["<pf>"],"isRdma":true}.[1][5] Expected: a new socket in /var/lib/kubelet/device-plugins/ and nvidia.com/sriov_netdevice: 4 in Allocatable. If not: socket present with count 0 means the selectors matched nothing - check vendors and the exact PF name. Rollback: delete the DaemonSet and its ConfigMap; the resource disappears from Allocatable within one ListAndWatch update.

  5. Create the NAD and one pod requesting nvidia.com/sriov_netdevice: 1, with the annotation k8s.v1.cni.cncf.io/networks naming the NAD in the same namespace.[8] Expected: the pod reaches Running and kubectl exec -- ip -br link shows net1. If not: Pending means the name; a CNI ADD error means the delegate. Rollback: delete the pod and the NAD.

  6. Prove which VF you got. kubectl exec -it <pod> -- env | grep PCIDEVICE Expected: PCIDEVICE_NVIDIA_COM_SRIOV_NETDEVICE=0000:xx:xx.x matching one of the VFs from step 2.[1]

  7. Optional, and only with a maintenance window: repeat with resourcePrefix omitted and confirm the pod from step 4 goes Pending with no CNI error. Rollback: restore the ConfigMap from your pre-flight file and delete the plugin pods so they reload it.

  8. Full teardown, in order: delete pods, delete NAD, delete device plugin DaemonSet and ConfigMap, then echo 0 > /sys/class/net/<pf>/device/sriov_numvfs. Re-run the step 0 inventory and diff it against the file you saved.

Retrieval check

10 questions from memory. Answer before looking anything up; misses become flashcards.

Explain it to a Dell SE

Explain to a Dell SE, in five sentences, what has to line up for a pod to get a real ConnectX VF, and which single string is responsible for most of the tickets.

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

Sources

Facts in this lesson were checked against sriov-network-device-plugin and sriov-cni repositories, sriov-network-operator repository, NVIDIA Network Operator 26.7.0 deployment guide, quick start and OpenShift guide - all re-fetched 2026-09-07. Dates are when each page was fetched.

  1. GitHub - k8snetworkplumbingwg/sriov-network-device-plugin · fetched 2026-09-07
  2. GitHub - k8snetworkplumbingwg/sriov-cni · fetched 2026-09-07
  3. sriov-cni - Configuration Reference · fetched 2026-09-07
  4. GitHub - k8snetworkplumbingwg/sriov-network-operator · 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 - Deployment Guide with OpenShift · fetched 2026-09-07
  7. NVIDIA Network Operator v26.7.0 - Quick Start Guide for Kubernetes · fetched 2026-09-07
  8. Multus CNI - How to use · fetched 2026-09-07
  9. NVIDIA Network Operator v23.7.0 - K8s on Bare Metal - Ethernet · fetched 2026-09-07
  10. NVIDIA Network Operator v26.7.0 - NIC Firmware Configuration · fetched 2026-09-07
  11. NVIDIA Network Operator v26.7.0 - Spectrum-X Quick Start · fetched 2026-09-07
  12. Kubernetes - Device Plugins · fetched 2026-09-07

The same idea elsewhere

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