Skip to content

Commit b75993a

Browse files
committed
GPU passthrough post
1 parent 05aa87f commit b75993a

1 file changed

Lines changed: 264 additions & 0 deletions

File tree

Lines changed: 264 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,264 @@
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

Comments
 (0)