|
| 1 | +--- |
| 2 | +title: GPU passthrough with libvirt on Fedora Kinoite |
| 3 | +--- |
| 4 | + |
| 5 | +I spent the last few days getting GPU passthrough working for my VMs. The goal was to run VMs with full GPU acceleration for game streaming with [Sunshine](https://github.com/LizardByte/Sunshine). I ran into kernel panics, hanging virsh commands, and display manager crashes along the way. |
| 6 | + |
| 7 | +Here's what I learned. If you have integrated graphics available, enable it in BIOS first. It makes things much simpler. |
| 8 | + |
| 9 | +## My setup |
| 10 | + |
| 11 | +- Fedora Kinoite (ostree/bootc-based system) |
| 12 | +- NVIDIA RTX 4070 Ti SUPER (discrete GPU) |
| 13 | +- Intel UHD Graphics 770 (integrated GPU - initially disabled in BIOS) |
| 14 | +- libvirt + QEMU for virtualization |
| 15 | + |
| 16 | +## Finding your GPU details |
| 17 | + |
| 18 | +First, find the PCI address of your GPU: |
| 19 | + |
| 20 | +``` |
| 21 | +$ lspci | grep -i nvidia |
| 22 | +01:00.0 VGA compatible controller: NVIDIA Corporation AD103 [GeForce RTX 4070 Ti SUPER] (rev a1) |
| 23 | +01:00.1 Audio device: NVIDIA Corporation AD103 High Definition Audio Controller (rev a1) |
| 24 | +``` |
| 25 | + |
| 26 | +The PCI address is `01:00.0` for the GPU and `01:00.1` for the audio controller. In sysfs format, these become `0000:01:00.0` and `0000:01:00.1` (add the domain prefix `0000:`). |
| 27 | + |
| 28 | +Next, find the vendor and device IDs: |
| 29 | + |
| 30 | +``` |
| 31 | +$ lspci -n -s 01:00.0 |
| 32 | +01:00.0 0300: 10de:2705 (rev a1) |
| 33 | +``` |
| 34 | + |
| 35 | +The format is `class: vendor:device`. The vendor ID is `10de` (NVIDIA) and the device ID is `2705` (this specific GPU model). You'll use this as `10de 2705` (space-separated) when binding drivers. |
| 36 | + |
| 37 | +## First attempt: Single-GPU passthrough |
| 38 | + |
| 39 | +Single-GPU passthrough works like this: |
| 40 | +1. VM starts → unbind GPU from host → pass to VM |
| 41 | +2. VM stops → rebind GPU to host |
| 42 | + |
| 43 | +The host display goes completely black while the VM runs because the GPU is gone. You need SSH access to manage anything on the host. |
| 44 | + |
| 45 | +### Problem: virsh nodedev commands hang |
| 46 | + |
| 47 | +My initial libvirt hooks used `virsh nodedev-detach` and `virsh nodedev-reattach`: |
| 48 | + |
| 49 | +```bash |
| 50 | +# Using virsh naming format (underscores instead of colons) |
| 51 | +virsh nodedev-detach pci_0000_01_00_0 |
| 52 | +virsh nodedev-detach pci_0000_01_00_1 |
| 53 | +``` |
| 54 | + |
| 55 | +These commands would hang indefinitely when the system was in certain states. No error messages, just infinite waiting. |
| 56 | + |
| 57 | +### Problem: Kernel panics |
| 58 | + |
| 59 | +Trying to unbind the NVIDIA driver while it was still in use caused kernel panics: |
| 60 | + |
| 61 | +``` |
| 62 | +nvidia 0000:01:00.0: [drm] drm_WARN_ON(!list_empty(&fb->filp_head)) |
| 63 | +list_del corruption, ffff8b3442b5d310->next is LIST_POISON1 |
| 64 | +``` |
| 65 | + |
| 66 | +The system would become completely unresponsive. SSH would hang. `systemctl restart` commands would freeze. |
| 67 | + |
| 68 | +The display manager needs to be stopped cleanly before unbinding the GPU driver. |
| 69 | + |
| 70 | +## Dual-GPU setup |
| 71 | + |
| 72 | +I had integrated graphics available but disabled in BIOS. After enabling it (look for "Primary Display", "iGPU Multi-Monitor", or "Integrated Graphics"), I had two GPUs: |
| 73 | + |
| 74 | +```bash |
| 75 | +$ lspci | grep -i VGA |
| 76 | +00:02.0 VGA compatible controller: Intel Corporation Raptor Lake-S GT1 [UHD Graphics 770] |
| 77 | +01:00.0 VGA compatible controller: NVIDIA Corporation AD103 [GeForce RTX 4070 Ti SUPER] |
| 78 | +``` |
| 79 | + |
| 80 | +With two GPUs: |
| 81 | +- Host uses Intel iGPU for display |
| 82 | +- NVIDIA GPU passes to VM |
| 83 | +- No host display blackout |
| 84 | +- Simpler driver management |
| 85 | + |
| 86 | +The hooks needed to detect dual-GPU mode automatically. |
| 87 | + |
| 88 | +## Problem: Display manager crashes |
| 89 | + |
| 90 | +Even with dual-GPU, SDDM would crash. NVIDIA kernel modules were still loaded while the GPU was passed through to the VM. |
| 91 | + |
| 92 | +When logging in via the iGPU: |
| 93 | +1. SDDM starts |
| 94 | +2. KWin/Plasma tries to initialize all GPUs |
| 95 | +3. Finds `nvidia_drm` module loaded |
| 96 | +4. Tries to use the NVIDIA GPU |
| 97 | +5. GPU is bound to vfio-pci (in the VM) |
| 98 | +6. Session exits with code 4 |
| 99 | + |
| 100 | +The fix is to stop SDDM before unbinding the GPU, even with dual-GPU. This cleanly unloads NVIDIA modules, then restart SDDM on the iGPU only. |
| 101 | + |
| 102 | +### Problem: GPU doesn't rebind on VM shutdown |
| 103 | + |
| 104 | +When the VM stopped, the GPU wouldn't automatically rebind to the nvidia driver. Using `new_id` wasn't enough: |
| 105 | + |
| 106 | +```bash |
| 107 | +# Registers the ID but doesn't bind the device |
| 108 | +echo "10de 2705" > /sys/bus/pci/drivers/nvidia/new_id |
| 109 | +``` |
| 110 | + |
| 111 | +The `new_id` file tells the driver "you can claim devices with this vendor:device ID", but it doesn't actually bind any specific device to the driver. Explicitly binding the specific PCI device is required: |
| 112 | + |
| 113 | +```bash |
| 114 | +echo "0000:01:00.0" > /sys/bus/pci/drivers/nvidia/bind |
| 115 | +``` |
| 116 | + |
| 117 | +## Using sysfs directly |
| 118 | + |
| 119 | +Since `virsh nodedev-*` commands kept hanging, I switched to direct sysfs manipulation (which is what virsh uses anyway): |
| 120 | + |
| 121 | +```bash |
| 122 | +# Unbind from current driver (use your PCI address from lspci) |
| 123 | +echo "0000:01:00.0" > /sys/bus/pci/devices/0000:01:00.0/driver/unbind |
| 124 | + |
| 125 | +# Bind to vfio-pci |
| 126 | +modprobe vfio-pci |
| 127 | +# Tell vfio-pci it can claim this vendor:device ID (use your ID from lspci -n) |
| 128 | +echo "10de 2705" > /sys/bus/pci/drivers/vfio-pci/new_id |
| 129 | +# The device should automatically bind since it matches the ID |
| 130 | + |
| 131 | +# Later, bind back to nvidia |
| 132 | +modprobe nvidia nvidia_modeset nvidia_drm |
| 133 | +# Explicitly bind the device (new_id alone doesn't do this reliably) |
| 134 | +echo "0000:01:00.0" > /sys/bus/pci/drivers/nvidia/bind |
| 135 | +``` |
| 136 | + |
| 137 | +This is more reliable - no hanging commands, immediate errors, and works even when libvirt is in a weird state. |
| 138 | + |
| 139 | +## The working solution |
| 140 | + |
| 141 | +### Prerequisites |
| 142 | + |
| 143 | +1. **IOMMU enabled** in kernel (added to my kickstart template): |
| 144 | + ``` |
| 145 | + intel_iommu=on iommu=pt |
| 146 | + ``` |
| 147 | + |
| 148 | +2. **Secure Boot disabled** in the VM (NVIDIA drivers aren't signed for secure boot) |
| 149 | + |
| 150 | +3. **Both GPUs visible:** |
| 151 | + ```bash |
| 152 | + $ lspci | grep -i VGA # Should show 2 devices |
| 153 | + ``` |
| 154 | + |
| 155 | +### The hook scripts |
| 156 | + |
| 157 | +I created libvirt hooks that automatically handle GPU switching when the VM starts/stops. You'll need to replace the PCI addresses and vendor:device IDs with your own values from the commands above. |
| 158 | + |
| 159 | +**vfio-startup.sh** - When VM starts: |
| 160 | +```bash |
| 161 | +# Always stop SDDM to cleanly unload nvidia |
| 162 | +systemctl stop sddm.service |
| 163 | + |
| 164 | +# Unbind GPU from nvidia via sysfs (replace with your PCI address) |
| 165 | +echo "0000:01:00.0" > /sys/bus/pci/devices/0000:01:00.0/driver/unbind |
| 166 | + |
| 167 | +# Unload NVIDIA modules |
| 168 | +modprobe -r nvidia_drm nvidia_modeset nvidia_uvm nvidia |
| 169 | + |
| 170 | +# Bind to vfio-pci (replace with your vendor:device ID) |
| 171 | +modprobe vfio-pci |
| 172 | +echo "10de 2705" > /sys/bus/pci/drivers/vfio-pci/new_id |
| 173 | + |
| 174 | +# If dual-GPU, restart SDDM on iGPU |
| 175 | +if [ "$(lspci | grep -c 'VGA')" -gt 1 ]; then |
| 176 | + systemctl start sddm.service |
| 177 | +fi |
| 178 | +``` |
| 179 | + |
| 180 | +**vfio-teardown.sh** - When VM stops: |
| 181 | +```bash |
| 182 | +# Unbind from vfio-pci (replace with your PCI address) |
| 183 | +echo "0000:01:00.0" > /sys/bus/pci/drivers/vfio-pci/unbind |
| 184 | +modprobe -r vfio-pci |
| 185 | + |
| 186 | +# Load nvidia modules |
| 187 | +modprobe nvidia nvidia_modeset nvidia_drm nvidia_uvm |
| 188 | + |
| 189 | +# Explicitly bind to nvidia driver (replace with your PCI address) |
| 190 | +echo "0000:01:00.0" > /sys/bus/pci/drivers/nvidia/bind |
| 191 | +``` |
| 192 | + |
| 193 | +## Making it optional |
| 194 | + |
| 195 | +Once the hooks were working, every VM with the configured name would trigger GPU passthrough. I couldn't test VMs without it or have multiple VMs running. |
| 196 | + |
| 197 | +I considered using a flag file in `/tmp`, but that seemed like a terrible hack. |
| 198 | + |
| 199 | +Using the VM name as a signal works better. Libvirt hooks receive the VM name as an argument, so I can check for a suffix: |
| 200 | + |
| 201 | +```bash |
| 202 | +GUEST_NAME="$1" |
| 203 | + |
| 204 | +# Only run GPU passthrough hooks for VMs with "-gpu" suffix |
| 205 | +if [[ "$GUEST_NAME" != *-gpu ]]; then |
| 206 | + exit 0 |
| 207 | +fi |
| 208 | +``` |
| 209 | + |
| 210 | +Then in my Makefile: |
| 211 | + |
| 212 | +```makefile |
| 213 | +GPU_PASSTHROUGH ?= no |
| 214 | + |
| 215 | +# Append -gpu suffix to VM name if GPU passthrough is enabled |
| 216 | +ifeq ($(GPU_PASSTHROUGH),yes) |
| 217 | + VM_NAME_FULL := $(VM_NAME)-gpu |
| 218 | +else |
| 219 | + VM_NAME_FULL := $(VM_NAME) |
| 220 | +endif |
| 221 | +``` |
| 222 | + |
| 223 | +Now I can choose at VM creation time: |
| 224 | + |
| 225 | +```bash |
| 226 | +# Without GPU passthrough |
| 227 | +make virt-install |
| 228 | +# Creates VM: "fedora-mybox" |
| 229 | + |
| 230 | +# With GPU passthrough |
| 231 | +GPU_PASSTHROUGH=yes make virt-install |
| 232 | +# Creates VM: "fedora-mybox-gpu" |
| 233 | +``` |
| 234 | + |
| 235 | +The VM name is self-documenting, there's no state files to manage, and I can have both VMs exist simultaneously. |
| 236 | + |
| 237 | +## What happens now |
| 238 | + |
| 239 | +**Starting a VM with GPU passthrough:** |
| 240 | +1. Hook detects dual-GPU mode |
| 241 | +2. Stops SDDM |
| 242 | +3. Unbinds NVIDIA GPU from nvidia driver |
| 243 | +4. Unloads all NVIDIA modules |
| 244 | +5. Loads vfio-pci and binds GPU to it |
| 245 | +6. Restarts SDDM on iGPU |
| 246 | +7. VM starts with full GPU access |
| 247 | + |
| 248 | +**Stopping the VM:** |
| 249 | +1. VM releases GPU |
| 250 | +2. Hook unbinds from vfio-pci |
| 251 | +3. Loads nvidia modules |
| 252 | +4. Explicitly binds GPU to nvidia driver |
| 253 | +5. NVIDIA GPU is available on host again |
| 254 | + |
| 255 | +## Things to remember |
| 256 | + |
| 257 | +- Dual-GPU setup avoids the host display going black |
| 258 | +- Direct sysfs manipulation is more reliable than virsh commands when things go wrong |
| 259 | +- Module loading order matters - you can't unload nvidia_drm if nvidia is loaded |
| 260 | +- Display manager needs to be stopped to cleanly unload NVIDIA modules |
| 261 | +- `new_id` registers a device ID but doesn't bind it - explicit bind is required |
| 262 | +- Kernel parameters `intel_iommu=on iommu=pt` are required |
| 263 | + |
| 264 | +The hooks are in my [toolbox repo](https://github.com/shanemcd/toolbox/tree/main/libvirt-hooks) for future reference. |
0 commit comments