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.
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-qemuon Debian,qemuon Fedora). Runsudo chown qemu:qemu /data/vms/debian12.qcow2or setdynamic_ownership = 1in/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(oramdgpu) to/etc/modprobe.d/vfio.confand rebuild the initramfs again. - IOMMU groups contain unrelated devices: Some Intel/AMD consumer chipsets group PCIe slots poorly. The unofficial
ACS override patchkernel 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=0is 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.
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
AI and Artificial-Life Tools on Linux
Set up open-source AI/ML and artificial-life toolkits on Linux: PyTorch, JAX, DEAP, Avida, NetLogo, and RL environments with GPU driver guidance.
Assembly Language on Linux: A Starter Guide
Write x86-64 assembly on Linux from scratch: install NASM and GAS, learn syscalls, assemble and link a working program, then inspect and debug it.
How to Benchmark Disk Performance with fio
Learn to benchmark Linux disk performance with fio: writing job files, testing latency and throughput, and interpreting IOPS and percentile output correctly.
The Linux Boot Process Explained
Trace the full Linux boot sequence from UEFI firmware through GRUB2, the kernel, initramfs, and systemd to your login prompt — with diagnostics at each stage.