KVM: Intel iGPU passthrough to Windows Guest

This method achieves a headless host with the single gpu passed through to a Windows guest. The host will be inaccessible through video output and can only be accessed via SSH or serial console.

Tested on MSI Cubi 5 12M, with intel i3-1215U AlderLake CPU.
Host OS: Ubuntu server 24.04 (linux: 6.8.0-45-lowlatency)
Guest VM: QEMU 8.2.2 emulating a q35 machine with seabios in legacy bios mode.
Guest OS: Windows 11 24H2, Intel Graphics driver version: 32.0.101.6077

Preparing the host

  1. Install libvirtd and qemu, leaving unnecessary packages.
# apt install libvirt-daemon-system libvirt-clients qemu-system-modules-spice- qemu-system-gui- libdrm-amdgpu1- qemu-system-modules-opengl-
  1. Add necessary kernel parameters:
/etc/default/grub

GRUB_CMDLINE_LINUX_DEFAULT="video=efifb:off iommu=pt nofb nomodeset"
# update-grub
  1. Add vfio modules to initramfs and bind the gpu to vfio driver:
/etc/initramfs-tools/modules

# vfio modules must be put before drm
vfio
vfio_pci
vfio_iommu_type1
drm
/etc/modprobe.d/vfio.conf

# libvirt can handle automatic connection and disconnection of the GPU smoothly
# only uncomment if you have problems. run update-initramfs -u after making changes
# use lspci -nn to figure out the correct ids
#softdep drm pre: vfio-pci
#options vfio-pci ids=8086:46b3
  1. update initramfs:
# update-initramfs -u

At this point. ensure that the host is reachable via SSH or a serial console. Graphical interface will be unavailable after reboot.

Creating the VM

First, we create a basic VM with emulated vga graphics to install windows.
Because we are using virtio storage in our VM template, it is necessary to load virtio drivers from virtio-win.iso disk during Windows installation.
Download the latest virtio-win.iso from fedora website.

  1. Create a virtual disk to install windows on:
# qemu-img create -f raw /var/lib/libvirt/images/windows.img 120G
  1. Create the VM xml template:
    • Make sure to change the paths to the disk images to the correct locations in the following template
<domain type='kvm' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
    <name>Windows</name>
 <metadata xmlns:libosinfo="http://libosinfo.org/xmlns/libvirt/domain/1.0">
    <libosinfo:libosinfo>
      <libosinfo:os id="http://microsoft.com/win/11"/>
    </libosinfo:libosinfo>
  </metadata>
  <memory unit='KiB'>4194304</memory>
  <currentMemory unit='KiB'>4194304</currentMemory>
  <vcpu placement='static'>4</vcpu>
  <os>
    <type arch='x86_64' machine='pc-q35-8.2'>hvm</type>
    <boot dev='hd'/>
    <boot dev='cdrom'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <hyperv mode='custom'>
      <relaxed state='on'/>
      <vapic state='on'/>
      <spinlocks state='on' retries='8191'/>
      <vendor_id state='on' value='GenuineIntel'/>
    </hyperv>
  </features>
  <cpu mode='host-passthrough' check='none' migratable='on'/>
  <clock offset='localtime'>
    <timer name='rtc' tickpolicy='catchup'/>
    <timer name='pit' tickpolicy='delay'/>
    <timer name='hpet' present='no'/>
    <timer name='hypervclock' present='yes'/>
  </clock>
  <on_poweroff>destroy</on_poweroff>
  <on_reboot>restart</on_reboot>
  <on_crash>destroy</on_crash>
  <pm>
    <suspend-to-mem enabled='no'/>
    <suspend-to-disk enabled='no'/>
  </pm>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type="file" device="disk">
      <driver name="qemu" type="raw" discard="unmap"/>
      <source file="/var/lib/libvirt/images/windows.img"/>
      <target dev="vda" bus="virtio"/>
      <address type="pci" domain="0x0000" bus="0x05" slot="0x00" function="0x0"/>
    </disk>
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <backingStore/>
      <target dev='sda' bus='sata'/>
      <readonly/>
      <source file="/path/to/windows.iso"/>
      <address type='drive' controller='0' bus='0' target='0' unit='1'/>
    </disk>
    <disk type='file' device='cdrom'>
      <driver name='qemu' type='raw'/>
      <backingStore/>
      <target dev='sdb' bus='sata'/>
      <readonly/>
      <source file="/path/to/virtio-win.iso"/>
      <address type='drive' controller='0' bus='0' target='0' unit='2'/>
    </disk>
    <controller type='usb' index='0' model='qemu-xhci' ports='15'>
      <address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/>
    </controller>
    <controller type='pci' index='0' model='pcie-root'/>
    <controller type='pci' index='1' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='1' port='0x10'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0' multifunction='on'/>
    </controller>
    <controller type='pci' index='2' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='2' port='0x11'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x1'/>
    </controller>
    <controller type='pci' index='3' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='3' port='0x12'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x2'/>
    </controller>
    <controller type='pci' index='4' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='4' port='0x13'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x3'/>
    </controller>
    <controller type='pci' index='5' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='5' port='0x14'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x4'/>
    </controller>
    <controller type='pci' index='6' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='6' port='0x15'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x5'/>
    </controller>
    <controller type='pci' index='7' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='7' port='0x16'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x6'/>
    </controller>
    <controller type='pci' index='8' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='8' port='0x17'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x7'/>
    </controller>
    <controller type='pci' index='9' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='9' port='0x18'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x0' multifunction='on'/>
    </controller>
    <controller type='pci' index='10' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='10' port='0x19'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x1'/>
    </controller>
    <controller type='pci' index='11' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='11' port='0x1a'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x2'/>
    </controller>
    <controller type='pci' index='12' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='12' port='0x1b'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x3'/>
    </controller>
    <controller type='pci' index='13' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='13' port='0x1c'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x4'/>
    </controller>
    <controller type='pci' index='14' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='14' port='0x1d'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x03' function='0x5'/>
    </controller>
    <controller type='sata' index='0'>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/>
    </controller>
    <interface type='network'>
      <source network='default'/>
      <model type='virtio'/>
      <link state='down'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </interface>
    <sound model="ich9">
      <audio id="1"/>
      <alias name="sound0"/>
      <address type="pci" domain="0x0000" bus="0x00" slot="0x1b" function="0x0"/>
    </sound>
    <input type='tablet' bus='usb'>
      <address type='usb' bus='0' port='1'/>
    </input>
    <input type='mouse' bus='ps2'/>
    <input type='keyboard' bus='ps2'/>
    <graphics type='vnc' port='5900' autoport='no' listen='0.0.0.0'>
      <listen type='address' address='0.0.0.0'/>
    </graphics>
    <audio id='1' type='none'/>
    <video>
      <model type='vga' vram='16384' heads='1' primary='yes'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x0'/>
    </video>
    <watchdog model='itco' action='reset'/>
    <memballoon model='virtio'>
      <address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/>
    </memballoon>
    <tpm model="tpm-crb">
      <backend type="emulator" version="2.0"/>
     </tpm>
  </devices>
</domain>
  1. Create and start the VM:
# virsh define /path/to/vm.xml
# virsh start Windows
  1. Connect to the VM by using a VNC client on port 5900
    • The VNC server is not secured as it is just a temporary measure.
  2. Install windows normally.
    • Load the storage driver (viostor) during the installation steps.
    • You might need to disable Windows 11 system requirement checks.

Passing through the iGPU

Once windows is installed, we can add the gpu passthrough options to the VM.

# virsh shutdown Windows
# virsh edit Windows
  1. Add xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0' to <domain> at the top.
  2. Add GPU passthrough inside the <devices> block:
    • Important: domain, bus and function values MUST be 0x0. slot value MUST be 0x02.
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
      </source>
      <alias name='hostdev0'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
    </hostdev>
  1. Enable OpRegion settings by putting the following block inside the <domain> block, but outside the <devices> block:
  <qemu:override>
    <qemu:device alias='hostdev0'>
      <qemu:frontend>
        <qemu:property name='x-igd-opregion' type='bool' value='true'/>
      </qemu:frontend>
    </qemu:device>
  </qemu:override>
  1. Start the VM again and install intel GPU drivers normally. The connected monitor may flicker after install but it should output the display normally after reboot.
    • During boot up, the external monitor will not display anything until after Windows has initialized the GPU drivers which is around the same time as the login screen appears.

At this point, the iGPU pass through should be working. you can remove the emulated vga graphics from the VM if you wish.

Extra tuning

CPU pinning to only use P-Cores inside the guest. use lscpu -e to determine cpusets:

  <vcpu placement='static'>4</vcpu>
  <cputune>
    <vcpupin vcpu='0' cpuset='0'/>
    <vcpupin vcpu='1' cpuset='1'/>
    <vcpupin vcpu='2' cpuset='2'/>
    <vcpupin vcpu='3' cpuset='3'/>
  </cputune>

Using static hugepages for memory.

  • Kernel parameters (for 16GB):
default_hugepagesz=1G hugepagesz=1G hugepages=16
  • Tell libvirt to use hugepages:
  <memoryBacking>
    <hugepages/>
  </memoryBacking>

Extra resources


Posted

in

, ,

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *