$linuxjunkies
>

Virtualisation with QEMU/KVM (deep dive)

Full guide to QEMU/KVM with libvirt: storage pools, virtual networks, virsh management, CPU pinning, and VFIO GPU passthrough on Linux.

AdvancedUbuntuDebianFedoraArch14 min readUpdated June 7, 2026

Before you start

  • ▸A CPU with Intel VT-x or AMD-V enabled in UEFI/BIOS
  • ▸Root or sudo access on the host machine
  • ▸For GPU passthrough: Intel VT-d or AMD-Vi (IOMMU) support and a secondary display adapter for the host
  • ▸A guest OS ISO image downloaded locally

QEMU/KVM is the backbone of most production Linux virtualisation stacks. KVM turns the kernel into a Type-1 hypervisor; QEMU provides the emulated hardware; libvirt wraps both with a stable management API. Together they run OpenStack, oVirt, and countless bare-metal server deployments. This guide walks through a full working setup: installing the stack, configuring storage pools and virtual networks, managing VMs with virsh, and setting up VFIO GPU passthrough for workloads that need direct hardware access.

Install the QEMU/KVM/libvirt Stack

Verify hardware virtualisation support

Before installing anything, confirm your CPU exposes virtualisation extensions and the kernel can use them.

grep -Ec '(vmx|svm)' /proc/cpuinfo
lsmod | grep kvm

A non-zero count means the CPU is capable. vmx is Intel VT-x; svm is AMD-V. If lsmod shows no kvm module, check your UEFI/BIOS settings — virtualisation is often disabled by default on consumer boards.

Install on Debian/Ubuntu

sudo apt update
sudo apt install -y qemu-system-x86 libvirt-daemon-system libvirt-clients \
  virtinst bridge-utils virt-manager ovmf

Install on Fedora / RHEL 9 / Rocky 9

sudo dnf install -y @virtualization
sudo dnf install -y virt-manager ovmf

Install on Arch

sudo pacman -S qemu-desktop libvirt virt-manager virt-viewer \
  dnsmasq bridge-utils ebtables ovmf

Enable and start libvirt

sudo systemctl enable --now libvirtd

Add your user to the libvirt group (and kvm on some distros) to manage VMs without sudo.

sudo usermod -aG libvirt,kvm $USER
newgrp libvirt

Storage Pools

A storage pool is a libvirt-managed source of disk images. The default pool points to /var/lib/libvirt/images, which is fine for getting started. For production, define a dedicated pool — either a directory, an LVM volume group, or a ZFS dataset.

Define a directory pool

sudo virsh pool-define-as vmpool dir --target /data/vms
sudo virsh pool-build vmpool
sudo virsh pool-start vmpool
sudo virsh pool-autostart vmpool

Create a disk image (volume) inside the pool

sudo virsh vol-create-as vmpool debian12.qcow2 40G --format qcow2

qcow2 supports thin provisioning, snapshots, and copy-on-write — use it unless you have a specific reason to use raw. Raw is marginally faster at scale and easier to snapshot at the block device level with LVM.

Virtual Networks

libvirt ships a NAT-based default network out of the box. For most VMs that only need outbound internet access, it works immediately. For VMs that need to be reachable from the LAN, use a bridged network.

Inspect and start the default NAT network

sudo virsh net-list --all
sudo virsh net-start default
sudo virsh net-autostart default

Create a bridged network

First create a bridge on the host. With systemd-networkd, drop a pair of files:

sudo tee /etc/systemd/network/10-br0.netdev <<'EOF'
[NetDev]
Name=br0
Kind=bridge
EOF

sudo tee /etc/systemd/network/11-br0.network <<'EOF'
[Match]
Name=br0

[Network]
DHCP=yes
EOF

sudo tee /etc/systemd/network/12-eth0.network <<'EOF'
[Match]
Name=eth0

[Network]
Bridge=br0
EOF

sudo systemctl restart systemd-networkd

Replace eth0 with your actual interface name (ip link). On NetworkManager systems (Fedora desktop, Ubuntu desktop) use nmcli instead:

sudo nmcli con add type bridge con-name br0 ifname br0
sudo nmcli con add type bridge-slave con-name br0-slave ifname eth0 master br0
sudo nmcli con up br0

Then define the libvirt network that uses it:

cat > /tmp/br0-net.xml <<'EOF'
<network>
  <name>br0-net</name>
  <forward mode="bridge"/>
  <bridge name="br0"/>
</network>
EOF
sudo virsh net-define /tmp/br0-net.xml
sudo virsh net-start br0-net
sudo virsh net-autostart br0-net

Launching and Managing VMs with virsh

Install a VM from ISO

sudo virt-install \
  --name debian12 \
  --ram 4096 \
  --vcpus 4 \
  --disk vol=vmpool/debian12.qcow2,bus=virtio \
  --cdrom /data/iso/debian-12.5.0-amd64-netinst.iso \
  --network network=default,model=virtio \
  --os-variant debian12 \
  --graphics spice \
  --video qxl \
  --boot uefi \
  --noautoconsole

Use osinfo-query os to find the correct --os-variant string for your guest OS. Specifying it correctly lets libvirt apply optimised defaults.

Common virsh commands

virsh list --all              # list all VMs
virsh start debian12          # start a stopped VM
virsh shutdown debian12       # ACPI shutdown (graceful)
virsh destroy debian12        # force-off (like pulling power)
virsh suspend debian12        # pause execution
virsh resume debian12
virsh snapshot-create-as debian12 clean-install --atomic
virsh snapshot-list debian12
virsh snapshot-revert debian12 clean-install

Edit VM XML directly

sudo virsh edit debian12

This opens the VM definition in $EDITOR. Changes take effect at next boot. This is how you attach additional disks, tweak CPU pinning, or add PCI devices for passthrough.

CPU pinning for latency-sensitive workloads

Pin vCPUs to specific physical cores to eliminate scheduler jitter. First identify your CPU topology:

lscpu --extended

Then set pinning inside the VM XML's <vcpu> and <cputune> sections, or via virsh:

sudo virsh vcpupin debian12 0 4
sudo virsh vcpupin debian12 1 5

This pins guest vCPU 0 to host CPU 4, vCPU 1 to host CPU 5, and so on.

VFIO / GPU Passthrough

VFIO lets you assign a physical PCIe device — typically a GPU, NVMe controller, or USB card — exclusively to a VM. The host OS loses access to that device. This is required for gaming VMs, ML training on a dedicated card, or any workload that needs native GPU performance.

Warning: GPU passthrough requires IOMMU support (Intel VT-d or AMD-Vi), a CPU and motherboard that properly isolate IOMMU groups, and a secondary GPU or iGPU for the host. On some consumer boards, IOMMU grouping is poor and unrelated devices share groups — you may be forced to pass through more hardware than intended.

Step 1 — Enable IOMMU in the bootloader

Add kernel parameters. For systemd-boot:

sudo kernelstub -a "intel_iommu=on iommu=pt"   # Intel
# or
sudo kernelstub -a "amd_iommu=on iommu=pt"    # AMD

For GRUB, edit GRUB_CMDLINE_LINUX_DEFAULT in /etc/default/grub and run sudo update-grub (Debian/Ubuntu) or sudo grub2-mkconfig -o /boot/grub2/grub.cfg (Fedora/RHEL). Reboot, then verify:

sudo dmesg | grep -e IOMMU -e DMAR -e AMD-Vi | head -20

Step 2 — Identify the GPU's PCI IDs and IOMMU group

lspci -nn | grep -i nvidia   # or amd, radeon
# Example output (yours will differ):
# 01:00.0 VGA compatible controller [0300]: NVIDIA ... [10de:2684]
# 01:00.1 Audio device [0403]: NVIDIA ... [10de:22ba]
for d in /sys/kernel/iommu_groups/*/devices/*; do
  echo "Group $(basename $(dirname $d)): $(lspci -nns ${d##*/})"
done | sort -V | grep -i nvidia

All devices in the same IOMMU group must be passed through together.

Step 3 — Bind the GPU to vfio-pci

echo "options vfio-pci ids=10de:2684,10de:22ba" | \
  sudo tee /etc/modprobe.d/vfio.conf

echo -e "vfio\nvfio_iommu_type1\nvfio_pci" | \
  sudo tee /etc/modules-load.d/vfio.conf

On Fedora/RHEL also add vfio_virqfd if your kernel is older than 6.2 (it was merged into vfio core after that). Then rebuild the initramfs:

# Debian/Ubuntu
sudo update-initramfs -u
# Fedora/RHEL
sudo dracut --force
# Arch
sudo mkinitcpio -P

Reboot. Verify the GPU is bound to vfio-pci, not the vendor driver:

lspci -ks 01:00.0

The Kernel driver in use line should read vfio-pci.

Step 4 — Add the GPU to a VM

Attach the PCI device to your VM XML using virsh edit, or via command line:

sudo virsh attach-device debian12 /tmp/gpu.xml --config

Where /tmp/gpu.xml contains:

cat > /tmp/gpu.xml <<'EOF'
<hostdev mode='subsystem' type='pci' managed='yes'>
  <source>
    <address domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
  </source>
</hostdev>
EOF

Use the actual bus/slot/function values from lspci. Repeat for the audio function (function='0x1'). For NVIDIA cards, also set <vendor_id state='on' value='randomid'/> in the <hyperv> section and hide the KVM signature to avoid error 43 in older drivers.

Verification

virsh list --all           # VM appears and shows 'running'
virsh dominfo debian12     # shows RAM, vCPUs, state
virsh domblklist debian12  # lists attached disks
virsh domiflist debian12   # lists network interfaces

Connect to the console with virsh console debian12 (requires a serial console configured in the guest) or open virt-manager for a graphical view.

Troubleshooting

  • VM fails to start with "permission denied" on image: Check that the image is owned by the user/group libvirt runs as (libvirt-qemu on Debian, qemu on Fedora). Run sudo chown qemu:qemu /data/vms/debian12.qcow2 or set dynamic_ownership = 1 in /etc/libvirt/qemu.conf.
  • GPU still bound to nvidia/amdgpu after reboot: The vendor driver may be loading before vfio-pci. Add softdep nvidia pre: vfio-pci (or amdgpu) to /etc/modprobe.d/vfio.conf and rebuild the initramfs again.
  • IOMMU groups contain unrelated devices: Some Intel/AMD consumer chipsets group PCIe slots poorly. The unofficial ACS override patch kernel splits groups but is not upstream and carries security implications — weigh the trade-off carefully.
  • VM network unreachable from LAN with bridge: Ensure net.bridge.bridge-nf-call-iptables=0 is set via sysctl, or your firewall rules are explicitly allowing bridge traffic. Bridged frames can be filtered by netfilter unexpectedly.
  • SPICE/VNC console blank after GPU passthrough: Expected — the GPU is no longer driving a virtual display. Connect a physical monitor to the passed-through GPU or install a VNC server inside the guest.
tested on:Ubuntu 24.04Fedora 40Arch rolling-2024Rocky 9.3

Frequently asked questions

What is the difference between QEMU and KVM?
KVM (Kernel-based Virtual Machine) is a kernel module that exposes hardware virtualisation extensions to userspace, making the Linux kernel itself act as a hypervisor. QEMU is the userspace process that emulates CPU, RAM, storage, and other devices. When KVM is available, QEMU offloads CPU execution to it for near-native speed; without KVM, QEMU falls back to pure software emulation which is orders of magnitude slower.
Do I need a second GPU to do GPU passthrough?
Typically yes. Once a GPU is bound to vfio-pci, the host OS has no driver for it and cannot use it for display output. Most setups use an integrated GPU or a cheap secondary card for the host desktop while passing the primary discrete GPU to the guest. Some AMD systems support SR-IOV or MxGPU which can share one card, but that requires enterprise GPU models.
When should I use raw disk images instead of qcow2?
Use raw when maximum disk I/O throughput matters and you do not need libvirt-level snapshots — for example, database servers in production. qcow2 thin-provisioning and snapshot features are convenient but add a small overhead per I/O, especially with random writes. If you use LVM volumes as storage pool backends, snapshots can be done at the LVM layer and raw becomes more attractive.
How do I live-migrate a VM to another host?
Both hosts must share storage accessible at the same path (NFS, Ceph, or shared SAN) and have compatible CPU features. Run: virsh migrate --live debian12 qemu+ssh://otherhost/system. For non-shared storage, add --copy-storage-all, but this suspends the VM briefly during disk sync.
Why does my NVIDIA GPU show error 43 inside the VM?
Older NVIDIA drivers detect they are running inside a hypervisor and refuse to load, returning error code 43. The fix is to hide the KVM signature and provide a fake vendor ID in the VM's XML under the <hyperv> and <kvm> feature sections. Set <vendor_id state='on' value='anyrandom'/> and <hidden state='on'/> in the kvm element. Modern NVIDIA drivers (since R465) are much less aggressive about this check.

Related guides