Status: Draft (2026-10-17), Source + Read-only Measurement. Based on KubeVirt
v1.9.0(commit79d34c17). The GPU-attached VM is powered off, so its domain XML is an expecteddrawing based on the code (unmeasured). The measurements consist of reading the KubeVirt CR, node allocatables, and virt-handler logs.
Reading KubeVirt Source Code 7: Device Pass-through
Table of Contents
- 1. Intuition: Three Places Must Know About the Same Device
- 2. Diagram: Seven Steps
- 3. Code Paths
- 4. Measurement: Allowlist Enabled but Resources are 0
- 5. Limitations
- References
1. Intuition: Three Places Must Know About the Same Device
To pass a PCI device to a VM, three entities must be aware of the same fact.
- Host Kernel: The device must be detached from its original driver and bound to
vfio-pci. This allows user space (QEMU) to directly manipulate the device under IOMMU protection. - Kubernetes: The device must appear as a "node resource" so that the scheduler can place pods on that node and kubelet can inject device files into the pods. This bridge is the device plugin.
- libvirt: The domain XML must contain
<hostdev>so that QEMU attaches that PCI address to the guest.
KubeVirt does not handle step 1 (that is the administrator's responsibility). Four components share the work of connecting steps 2 and 3.
2. Diagram: Seven Steps
3. Code Paths
① Allowlist — KubeVirt CR
In the KubeVirt CR spec.configuration.permittedHostDevices.pciHostDevices[] , you write the pciVendorSelector(벤더:장치 (hexadecimal) and resourceName . The feature gate HostDevices must also be enabled. Devices not listed here cannot be used even if specified by name in the VMI. virt-controller verifies this using validatePermittedHostDevices()
(renderresources.go:493) before drawing the pod.
② Device Manager Scans sysfs — virt-handler
Inside virt-handler, DeviceController.Run()(device_controller.go:481) sets up a callback so that whenever the KubeVirt configuration changes, refreshPermittedDevices() is called (:497). This function calls
updatePermittedHostDevicePlugins()(:208via discoverPermittedHostPCIDevices()(pci_device.go:258). What this function does while iterating through /sys/bus/pci/devices consists of three things:
- Whether the device's
벤더:장치is in the allowlist. - Whether the driver is
vfio-pci((:278). Otherwise,shouldPermitNvidia()(nvidia.go:23) checks once more. Even if bound to the NVIDIA driver, if it is a Virtual Function (VF) with a vGPU profile, it accepts it; otherwise, it logs and discards it. - Reads the device's IOMMU group number and NUMA node.
③ Device Plugin — Registering Resources with kubelet
A PCIDevicePlugin starts for each resource name (Start, pci_device.go:62) and registers with kubelet's device plugin socket. The device ID presented to kubelet is not the PCI address but the IOMMU group number(constructDPIdevices, :132). This is because VFIO passes devices by group, meaning devices in the same group cannot be given to different VMs. Once registered, it appears in the node's status.allocatable as
nvidia.com/<GPU 이름>: 1 . The same device manager exposes generic devices like devices.kubevirt.io/kvm·tun·vhost-net in the same manner.
④ Pod Requests Resources — virt-controller
If the VMI has spec.domain.devices.gpus[].deviceName: nvidia.com/<GPU 이름> , the template from Part 3 uses the resource renderer option
WithGPUsDevicePlugins()(renderresources.go:149) to add 1 of those resources to the
compute container's limits. An additional 1Gi is added to the memory overhead (hypervisorbackend.go:130). Comments note this is because VFIO locks the entire guest RAM for DMA. The judgment criterion is vmitrait.HasVFIO()
(vmitrait.go:33) — if any of host devices, GPUs, or SR-IOV are present.
⑤ Allocate — Pass the device file and address
When the scheduler places the pod on a node with remaining resources, the kubelet calls the device plugin's Allocate()
(pci_device.go:152). There are two answers.
- device file:
/dev/vfio/vfioand/dev/vfio/<그룹 번호>(formatVFIODeviceSpecs, common.go:275), and if it's a kernel using iommufd, it also adds device-specific cdevs. - environment variable:
PCI_RESOURCE_prefix (types.go:3672, with the resource name capitalized and/·.replaced with_(ত্ত্বResourceNameToEnvVar, util.go:89), and the values are comma-separated PCI addresses. Example:PCI_RESOURCE_NVIDIA_COM_<GPU>=0000:01:00.0.
This environment variable is the sole link connecting the node (handler) and the pod (launcher). The launcher does not look at sysfs and relies solely on this value.
⑥ The launcher <hostdev> creates a
5 generateConverterContext() of getGPUDevices()(manager.go:1138) and gpu.CreateHostDevices()(gpu/hostdev.go:39) creates the address pool. The pool is populated by reading the environment variable from step 5 (AddressPool.load,
addresspool.go:46). It takes one address per GPU, and createPCIHostDevice()(hostdev.go:141) creates
api.HostDevice , and the converter's HostDeviceDomainConfigurator
(compute/host_device.go:42) attaches it to the domain. If the same list of GPUs consists of vGPUs (mediated devices), it goes toward the MDEV_PCI_RESOURCE_… variable and the CreateMDEVHostDevices side. In v1.9.0, a path to receive them via DRA (Dynamic Resource Allocation) instead of the device plugin (hostdevice/dra/) is also included behind a feature gate.
⑦ Expected XML (Unmeasured)
According to the code, the domain XML for a single GPU (named gpu1) looks like this.
<hostdev mode='subsystem' type='pci' managed='no'>
<source>
<address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
</source>
<alias name='ua-gpu-gpu1'/>
</hostdev>
managed='no' is important. If it is managed='yes' , libvirt detaches the device from the host driver and binds it to vfio-pci when turning on the VM, but the non-root libvirt inside the pod does not have the permission to do that. Therefore, the binding must be done in advance on the host (Section 1, item 1), and KubeVirt only counts already bound devices (②). The alias uses the user alias prefix ua- are given the GPU prefix gpu- . Also, a VMI with such devices is marked as unmigratable by virt-handler (vmiContainsNonMigratablePCIHostDevices, vm.go:1304).
4. Measurement: Allowlist is enabled but resources are 0
I inspected a setup where a single GPU node in a small cluster was configured to be dedicated to VMs (2026-10-17).
-
① feature gate in the KubeVirt CR
HostDevices, andpciHostDevicesone (pciVendorSelector: 10DE:xxxx,resourceName: nvidia.com/<GPU 이름>) existed. One VM requesting this GPU wasrunStrategy: Haltedturned off as -
③ However, that node's allocatable was
nvidia.com/<GPU 이름>: 0. On the same node, the GPU resources for containers(nvidia.com/gpu, exposed by another device plugin) appeared as 1. On the same node, KubeVirt's generic devices(devices.kubevirt.io/kvm·tun·vhost-net) were 1k each. -
② This line was repeating in the virt-handler logs on that node(
pos: nvidia.go:33).Skipping non-vfio-pci device without vGPU profile 0000:01:00.0 (10de:xxxx, driver: nvidia)The device's driver was
vfio-pciinstead of the host'snvidia, and there was no vGPU profile, so it was filtered out at step ②. Therefore, the device count in ③ is 0. If you start a VM in this state, the pod will remain in Pending due to insufficient resources (I didn't actually try it). The solution is not in KubeVirt, but on the host by binding that GPU tovfio-pci(via kernel arguments or driver binding), while also ensuring that the GPU device plugin for containers does not expose the same GPU. -
This line was printed 641 times on a single day (UTC) on 2026-10-16, and 7,889 times since that virt-handler restarted. This means it scans the devices again every 2 minutes on average. What calls ② so frequently was not pursued this time.
-
Traces of ⑤. In the same virt-handler log, the moment another VM's pod was created (2026-10-14T14:38:34Z), the generic device plugin's
Generic Allocate: resourceName: kvm·tun·vhost-netthree lines (generic_device.go:240) were logged at 1ms intervals. This is how the moment looks when kubelet places a pod and calls the device plugin's Allocate.
5. Limitations
- Since we did not turn on a VM with a GPU attached, ⑤'s environment variables, ⑥ and ⑦'s XML, and inside the guest
lspciare all unmeasured. Moving devices to vfio-pci on the host requires changing node configurations, which is beyond the scope of this post (reading). - How to handle cases where there are multiple devices in an IOMMU group (such as audio functions in the same group) was only examined through code reading.
- The paths of vGPU, SR-IOV, and DRA were only briefly mentioned by name.
Titi's note. If you are wondering, "I added it to the allowlist, why isn't it showing up?", look for
Skippingin the virt-handler logs. KubeVirt only counts devices already bound to vfio-pci. Binding them is the host's job!
References
All accessed on October 17, 2026.
device-manager: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-handler/device-managerhostdevice: https://github.com/kubevirt/kubevirt/tree/v1.9.0/pkg/virt-launcher/virtwrap/device/hostdevice- KubeVirt User Guide, Host Device Assignment: https://kubevirt.io/user-guide/compute/host-devices/
- Kubernetes Device Plugins: https://kubernetes.io/docs/concepts/extend-kubernetes/compute-storage-net/device-plugins/
- Linux Kernel VFIO Documentation: https://docs.kernel.org/driver-api/vfio.html
- libvirt Domain XML, Host Device: https://libvirt.org/formatdomain.html#host-device-assignment