9 Commits

8 changed files with 110 additions and 83 deletions

View File

@@ -75,12 +75,6 @@ Enable-NetFirewallRule -DisplayGroup "Remote Desktop"
Put your public key in `C:\ProgramData\ssh\administrators_authorized_keys` for an Administrator Put your public key in `C:\ProgramData\ssh\administrators_authorized_keys` for an Administrator
account. `vm-native-verify` uses that key. account. `vm-native-verify` uses that key.
SSH into the guest fails with `Corrupted MAC on input` until the host has the NIC offload rule
from the `vfio-native` package. The emulated NIC's TX offloads corrupt integrity-checked traffic on
the host side of the tap; SMB tolerates it, SSH does not. This holds for both `igb` and `e1000e`.
The package installs a udev rule that turns the offloads off on every libvirt tap as it appears,
and `vm-native-setup` says so if it is missing.
## 3. Make the NVMe driver boot-critical ## 3. Make the NVMe driver boot-critical
The disk is about to move from virtio to emulated NVMe, and Windows only loads boot-start drivers The disk is about to move from virtio to emulated NVMe, and Windows only loads boot-start drivers
@@ -127,35 +121,14 @@ Two things it asks or warns about:
Before the first boot, if the host has less free memory than the guest's RAM, free and compact Before the first boot, if the host has less free memory than the guest's RAM, free and compact
it so the guest lands on transparent hugepages; `vm-native-setup` prints the two commands when it it so the guest lands on transparent hugepages; `vm-native-setup` prints the two commands when it
applies. The NIC stays `igb`, so the network survives the driver removal in the next step: Windows applies. The NIC stays `igb`, so the network survives the driver removal in the next step. Do not use
has an in-box driver for the Intel 82576 it emulates. Do not use virtiofs for host files: it is a virtiofs for host files: it is a virtio device the scanner names, and its shared memory backing
virtio device the scanner names, and its shared memory backing blocks transparent hugepages for the blocks transparent hugepages for the whole guest. Share over SMB on the `igb` link instead.
whole guest. Share over SMB on the `igb` link instead.
### Jumbo frames The interface asks for MTU 9000, and the package's hook turns GSO and GRO on for the tap. Together
they take the inbound link from 2.9 to 14 Gbit/s; neither does anything alone. The libvirt network
`vm-native-setup` puts `<mtu size='9000'/>` on the guest interface. The emulated NIC is packet-rate needs `<mtu size='9000'/>` too, and the guest needs *Jumbo Packet* 9014 with its interface MTU at
bound rather than bandwidth bound, so larger frames cut the per-packet cost the guest pays on 9000 - setting the adapter property alone leaves the IP MTU at 1500 and gains nothing.
receive - the direction that matters when you read files off an SMB share. Measured on igb, host to
guest: 2922 Mbit/s at 1500, 14232 Mbit/s at 9000, byte-for-byte identical either way.
Both ends have to agree or the frames are simply dropped. The libvirt network needs it too:
```
virsh net-edit default # add <mtu size='9000'/>
virsh net-destroy default && virsh net-start default
```
and in the guest, set the adapter's *Jumbo Packet* to 9014 and the interface MTU to 9000:
```powershell
$n = (Get-NetAdapter -Physical | Where-Object Status -eq 'Up').Name
Set-NetAdapterAdvancedProperty -Name $n -RegistryKeyword "*JumboPacket" -RegistryValue 9014
Set-NetIPInterface -InterfaceAlias $n -NlMtu 9000
```
Check `Get-NetIPInterface` afterwards: setting *Jumbo Packet* alone leaves the IP MTU at 1500 and
you get none of the benefit.
## 5. Remove the virtio drivers and the agents ## 5. Remove the virtio drivers and the agents

View File

@@ -1,6 +1,6 @@
pkgbase = vfio-native pkgbase = vfio-native
pkgdesc = Present a libvirt guest as a self-consistent physical machine, and tune it pkgdesc = Present a libvirt guest as a self-consistent physical machine, and tune it
pkgver = 1.1.1 pkgver = 1.3.3
pkgrel = 1 pkgrel = 1
url = https://git.archworks.co/sandwich/vfio-native url = https://git.archworks.co/sandwich/vfio-native
install = vfio-native.install install = vfio-native.install
@@ -17,7 +17,7 @@ pkgbase = vfio-native
optdepends = cpupower: set the host CPU governor optdepends = cpupower: set the host CPU governor
optdepends = vfio-native-kvm-dkms: patched KVM modules for the full level optdepends = vfio-native-kvm-dkms: patched KVM modules for the full level
optdepends = vfio-native-qemu: patched QEMU for the full level optdepends = vfio-native-qemu: patched QEMU for the full level
source = git+https://git.archworks.co/sandwich/vfio-native.git#tag=v1.1.1 source = git+https://git.archworks.co/sandwich/vfio-native.git#tag=v1.3.3
sha256sums = SKIP sha256sums = SKIP
pkgname = vfio-native pkgname = vfio-native

View File

@@ -8,7 +8,7 @@
# vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt # vfio-native-qemu QEMU 11.1.1 with the platform-identity patches, in /opt
pkgname=vfio-native pkgname=vfio-native
pkgver=1.3.0 pkgver=1.3.3
pkgrel=1 pkgrel=1
pkgdesc="Present a libvirt guest as a self-consistent physical machine, and tune it" pkgdesc="Present a libvirt guest as a self-consistent physical machine, and tune it"
arch=('any') arch=('any')
@@ -48,7 +48,6 @@ package() {
# this coexists with whatever hook the host already has. # this coexists with whatever hook the host already has.
install -Dm755 scripts/libvirt-hook-cpu-isolation.sh \ install -Dm755 scripts/libvirt-hook-cpu-isolation.sh \
"${pkgdir}/etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh" "${pkgdir}/etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh"
# emulated NIC offloads corrupt integrity-checked traffic on libvirt taps; host-wide by nature install -Dm755 scripts/libvirt-hook-vnet-offload.sh \
install -Dm644 scripts/99-vfio-native-vnet-offload.rules \ "${pkgdir}/etc/libvirt/hooks/qemu.d/20-vnet-offload.sh"
"${pkgdir}/usr/lib/udev/rules.d/99-vfio-native-vnet-offload.rules"
} }

View File

@@ -49,11 +49,11 @@ post_install() {
'native' is a good default if you would rather not maintain anything. 'native' is a good default if you would rather not maintain anything.
A udev rule (99-vfio-native-vnet-offload.rules) turns TX offloads off on every A hook at /etc/libvirt/hooks/qemu.d/20-vnet-offload.sh turns GSO and GRO on
libvirt tap as it appears. The emulated e1000e NIC corrupts integrity-checked for the guest's tap. With the interface's MTU 9000 that takes the inbound link
traffic with them on; SSH to the guest fails with "Corrupted MAC on input". from 2.9 to 14 Gbit/s; neither does anything alone. The libvirt network needs
It applies to every VM on the host; the throughput cost on a host<->guest <mtu size='9000'/> too, and the guest needs Jumbo Packet 9014 with its
link is not measurable. interface MTU at 9000.
A libvirt hook is installed at /etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh. A libvirt hook is installed at /etc/libvirt/hooks/qemu.d/10-cpu-isolation.sh.
It keeps host processes off the cores the guest is pinned to, automatically, It keeps host processes off the cores the guest is pinned to, automatically,

View File

@@ -1,8 +0,0 @@
# vfio-native: the emulated e1000e NIC's TX checksum and segmentation offloads
# corrupt packets on the host side of a libvirt tap. SMB tolerates it; SSH fails
# with "Corrupted MAC on input" and any integrity-checked protocol breaks.
# Measured on a Zen 4 host with QEMU 11.1.1. Disabling the offloads on every
# libvirt tap as it appears fixes it; on a host<->guest link the throughput
# cost is not measurable. libvirt's <driver><host .../> attributes are ignored
# for e1000e, and a libvirt hook must not call virsh, hence udev.
ACTION=="add", SUBSYSTEM=="net", KERNEL=="vnet*", RUN+="/usr/bin/ethtool -K %k tx off gso off gro off tso off"

View File

@@ -1,12 +1,18 @@
#!/bin/bash #!/bin/bash
# Waits for a just-started guest's network to actually come up, then turns # Keeps the kvm_amd cpuid_passthrough switch correct across a guest's whole life,
# kvm_amd cpuid_passthrough on. # in-guest reboots included.
# #
# The readiness signal is the guest's own tap RX counter: it is fresh every boot, # The switch must be OFF (N) while the guest enumerates CPUID at boot or Windows
# it needs nothing enabled inside the guest (no SSH, no RDP, no agent), and it only # hangs, and ON (Y) once it is up, where it clears the timer detection. The libvirt
# moves once the guest's NIC driver has really loaded - which is well past the CPU # hook only fires at VM start and stop, so a guest-initiated reboot would otherwise
# enumeration that the switch must not change under. A DHCP lease left over from a # re-enumerate with the switch still Y and hang. This watcher drops it to N on every
# previous boot cannot trip it early. # QEMU RESET and raises it again once the guest's NIC is back up.
#
# Readiness signal: the guest tap's rx_packets counter growing past a baseline. It
# only moves once the guest NIC driver has loaded, well past CPU enumeration, and it
# needs nothing enabled inside the guest. The counter is cumulative and does NOT
# reset on an in-guest reboot, so readiness is growth past the value captured at the
# reset, not an absolute threshold.
# #
# Launched as a transient systemd unit by the cpuid-passthrough hook, so it is free # Launched as a transient systemd unit by the cpuid-passthrough hook, so it is free
# to call virsh (the hook itself must not - that deadlocks libvirtd). # to call virsh (the hook itself must not - that deadlocks libvirtd).
@@ -17,25 +23,49 @@ PARAM=/sys/module/kvm_amd/parameters
V="virsh -c qemu:///system" V="virsh -c qemu:///system"
running() { [ "$($V domstate "$DOMAIN" 2>/dev/null)" = running ]; } running() { [ "$($V domstate "$DOMAIN" 2>/dev/null)" = running ]; }
rx() { cat "$RX" 2>/dev/null || echo 0; }
tap="" set_N() { echo N > "$PARAM/cpuid_passthrough"; }
i=0 set_Y() { printf '%s' "$BRAND" > "$PARAM/brand_string"; echo Y > "$PARAM/cpuid_passthrough"; }
for _ in $(seq 1 65); do # ~195 s cap, then flip anyway if still up
i=$((i + 1))
running || { sleep 3; continue; } # not "running" yet at prepare time - wait
[ -z "$tap" ] && tap=$($V domiflist "$DOMAIN" 2>/dev/null |
awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}')
rx="/sys/class/net/$tap/statistics/rx_packets"
# the iteration floor keeps a stray pre-OS packet (a UEFI netboot attempt) from
# tripping the flip before the guest is even past its interrupt and timer setup
if [ "$i" -ge 4 ] && [ -n "$tap" ] && [ -r "$rx" ] &&
[ "$(cat "$rx" 2>/dev/null || echo 0)" -ge 4 ]; then
break
fi
sleep 3
done
running || exit 0 # guest went away before it came up # Wait until the tap rx counter grows at least 4 past $1 (guest NIC driver up again).
printf '%s' "$BRAND" > "$PARAM/brand_string" # The iteration floor keeps a stray pre-OS packet (a UEFI netboot attempt) from
echo Y > "$PARAM/cpuid_passthrough" # tripping the flip before the guest is even past its interrupt and timer setup.
wait_net_up() {
local base=$1 i=0
for _ in $(seq 1 100); do # ~300 s cap, then raise anyway if still up
i=$((i + 1))
running || return 1
[ "$i" -ge 4 ] && [ -n "$tap" ] && [ -r "$RX" ] &&
[ "$(rx)" -ge "$((base + 4))" ] && return 0
sleep 3
done
return 0
}
# The hook arms this watcher at prepare, before QEMU starts, so the domain is not
# yet running and its tap does not exist. Wait for the domain, then read the tap;
# otherwise the first running check in wait_net_up bails and the flip never happens.
for _ in $(seq 1 120); do running && break; sleep 1; done
running || exit 0
tap=$($V domiflist "$DOMAIN" 2>/dev/null | awk '$1 ~ /^(vnet|tap|macvtap)/ {print $1; exit}')
RX="/sys/class/net/$tap/statistics/rx_packets"
# initial cold boot: wait for the network, then harden
wait_net_up 0
running || exit 0
set_Y
logger -t vfio-cpuid "$DOMAIN network up: cpuid_passthrough=Y" logger -t vfio-cpuid "$DOMAIN network up: cpuid_passthrough=Y"
# every in-guest reboot fires a QEMU RESET: drop to N for the re-enumeration, then
# raise it again once the guest's NIC is back. --loop streams one line per reset.
$V qemu-monitor-event --domain "$DOMAIN" --event RESET --loop 2>/dev/null | while read -r _; do
running || continue
base=$(rx)
set_N
logger -t vfio-cpuid "$DOMAIN reset: cpuid_passthrough=N for re-enumeration"
wait_net_up "$base" || continue
running || continue
set_Y
logger -t vfio-cpuid "$DOMAIN back up: cpuid_passthrough=Y"
done

View File

@@ -57,6 +57,24 @@ group_members() {
done done
} }
# The functions to hand to the guest with GPU $1: every device in its IOMMU group
# (mandatory for vfio), plus any sibling function of the same PCI device that is
# itself a display or HDMI/DP audio controller. An APU parks its PSP and USB
# controllers on the same PCI device in separate IOMMU groups - those are the
# host's, so siblings are filtered by class and never pulled in blindly.
passthrough_devs() {
local d="$1" m cls
{
group_members "$d"
for m in "/sys/bus/pci/devices/${d%.*}".*; do
[ -e "$m" ] || continue
m=$(basename "$m")
cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)
case "$cls" in 0x03*|0x0403*) echo "$m";; esac
done
} | sort -u
}
driver_of() { driver_of() {
local l local l
l=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null) l=$(readlink -f "/sys/bus/pci/devices/$1/driver" 2>/dev/null)
@@ -79,7 +97,7 @@ drives_display() {
ids_of() { # vendor:device for vfio-pci binding ids_of() { # vendor:device for vfio-pci binding
local d local d
for d in $(group_members "$1"); do for d in $(passthrough_devs "$1"); do
local cls; cls=$(cat "/sys/bus/pci/devices/$d/class" 2>/dev/null) local cls; cls=$(cat "/sys/bus/pci/devices/$d/class" 2>/dev/null)
# only real functions of the card: skip bridges (class 0x0604xx) # only real functions of the card: skip bridges (class 0x0604xx)
case "$cls" in 0x0604*) continue;; esac case "$cls" in 0x0604*) continue;; esac
@@ -219,7 +237,7 @@ prepare)
# 1. stop whatever is holding the DRM device # 1. stop whatever is holding the DRM device
dm=$(systemctl list-units --type=service --state=running --no-legend 2>/dev/null | dm=$(systemctl list-units --type=service --state=running --no-legend 2>/dev/null |
awk '{print $1}' | grep -xE '(gdm|sddm|lightdm|lxdm|greetd|display-manager)\.service' | head -1) awk '{print $1}' | grep -xE '((gdm|sddm|lightdm|lxdm|greetd|ly|emptty|lemurs)(@[a-z0-9-]+)?|display-manager)\.service' | head -1)
if [ -n "$dm" ]; then if [ -n "$dm" ]; then
echo "$dm" > "$STATE/dm" echo "$dm" > "$STATE/dm"
log "stopping $dm" log "stopping $dm"
@@ -303,7 +321,7 @@ apply() {
"$v" -c qemu:///system dumpxml --inactive "$dom" > "$backup/$dom.gpu-current.xml" "$v" -c qemu:///system dumpxml --inactive "$dom" > "$backup/$dom.gpu-current.xml"
grep -q 'ua-vfionative-gpu' "$backup/$dom.gpu-current.xml" || cp "$backup/$dom.gpu-current.xml" "$backup/$dom.before-gpu.xml" grep -q 'ua-vfionative-gpu' "$backup/$dom.gpu-current.xml" || cp "$backup/$dom.gpu-current.xml" "$backup/$dom.before-gpu.xml"
local devs="" m local devs="" m
for m in $( { group_members "$d"; ls -d "/sys/bus/pci/devices/${d%.*}".* | xargs -n1 basename; } | sort -u); do for m in $(passthrough_devs "$d"); do
case "$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)" in 0x0604*) continue;; esac case "$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)" in 0x0604*) continue;; esac
IFS=':.' read -r dm bs sl fn <<< "$m" IFS=':.' read -r dm bs sl fn <<< "$m"
devs+=" <hostdev mode='subsystem' type='pci' managed='yes'>\n <source>\n" devs+=" <hostdev mode='subsystem' type='pci' managed='yes'>\n <source>\n"
@@ -330,7 +348,7 @@ xml() {
echo "Add this to the domain, inside <devices>. Every device in the GPU's" echo "Add this to the domain, inside <devices>. Every device in the GPU's"
echo "IOMMU group has to go together:" echo "IOMMU group has to go together:"
echo echo
for m in $(group_members "$d"); do for m in $(passthrough_devs "$d"); do
local cls; cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null) local cls; cls=$(cat "/sys/bus/pci/devices/$m/class" 2>/dev/null)
case "$cls" in 0x0604*) continue;; esac case "$cls" in 0x0604*) continue;; esac
IFS=':. ' read -r dom bus slot fn <<< "$(echo "$m" | tr ':.' ' ')" IFS=':. ' read -r dom bus slot fn <<< "$(echo "$m" | tr ':.' ' ')"

View File

@@ -0,0 +1,15 @@
#!/bin/bash
# libvirt qemu hook: turn GSO and GRO on for the guest's tap.
#
# QEMU leaves them off and sets the tap up after udev has run, so it has to
# happen here. Paired with MTU 9000 they are the difference between 2.5 and
# 14 Gbit/s into the guest; neither helps alone. Tap names come from the domain
# XML on stdin, so the hook never calls virsh, which would deadlock libvirtd.
[ "$2" = started ] || exit 0
for tap in $(grep -oE "<target dev='(vnet|tap|macvtap)[^']*'" | sed "s/.*dev='//; s/'$//"); do
ethtool -K "$tap" gso on gro on 2>/dev/null
done
exit 0