High VM-EXIT and Host CPU usage on idle with Windows Server 2025

According to the readme [0], stable should kinda match the most recent Red Hat Enterprise Linux release. Seems like November was the last one!?

On Fedora People, the last release was from November 2025.

On Rocky Linux, the last release was from March 24th 2026.

The `release-drivers-versions.txt` in the Rocky Linux RPM outlines the various versions of the drivers shipped in their version of the ISO.

I skimmed through it. The latest is driver build is 0.1.297 on Rocky Linux. This is newer than 0.1.285 on Fedora People. Additionally, the tools are newer -- The qemu guest agent for Windows is from Feb 19th 2026.
 
I narrowed it down to this new property (8):
AvailableSecurityProperties: 1,2,4,5,7,8
"8, If present, APIC virtualization is available."
see: Enable virtualization-based protection of code integrity

I'm not sure how you guys got APIC virtualization enabled. on my EPYC Milan, I only have 1, 2, 5, and 7. Is anyone seeing 8 on Zen3 or above or is that just showing up for Intel users?

I've been trying to trigger APIC virtualization so I can generate a BSOD dump on my end. I've been trying a bunch of stuff between settings in the bios, kvm_amd parameters, and vm conf file changes.

My motherboard is a bit unique in that I do have AVIC forced enabled "force_avic=Y" if that's somehow a factor in preventing APIC virtualization from showing up on the guest
 
I'm in a similar situation with Zen5 9955HX. I also had to go the force_avic=Y route. I did add the +hv-avic cpu flag, but propery 8 not showing in the Windows VM .
 
Can you post the output of:

dmesg | grep AMD-Vi
I'm on kernel 7.0.0-3-pve and stock pve-qemu (unpatched for GMET)

dmesg | grep AMD-Vi
AMD-Vi: Using global IVHD EFR:0x246577efa2254afa, EFR2:0x0
pci 0000:00:00.2: AMD-Vi: IOMMU performance counters supported
AMD-Vi: Extended features (0x246577efa2254afa, 0x0): PPR NX GT [5] IA GA PC GA_vAPIC
AMD-Vi: Interrupt remapping enabled
AMD-Vi: Virtual APIC enabled

dmesg | grep -i kvm_amd
kvm_amd: TSC scaling supported
kvm_amd: Nested Virtualization enabled
kvm_amd: Nested Paging enabled
kvm_amd: LBR virtualization supported
kvm_amd: AVIC unsupported in CPUID but force enabled, your system might crash and burn
kvm_amd: AVIC enabled
kvm_amd: x2AVIC enabled (max 512 vCPUs)
kvm_amd: Virtual VMLOAD VMSAVE supported
kvm_amd: Virtual GIF supported
kvm_amd: Virtual NMI enabled

cpuid -l 0x8000000a -1
CPU:
SVM Secure Virtual Machine (0x8000000a/eax):
SvmRev: SVM revision = 0x1 (1)
SVM Secure Virtual Machine (0x8000000a/edx):
nested paging = true
LBR virtualization = true
SVM lock = true
NRIP save = true
MSR based TSC rate control = true
VMCB clean bits support = true
flush by ASID = true
decode assists = true
PMC virtualization = true
SSSE3/SSE5 opcode set disable = false
pause intercept filter = true
pause filter threshold = true
AVIC: AMD virtual interrupt controller = false
virtualized VMLOAD/VMSAVE = true
virtualized global interrupt flag (GIF) = true
GMET: guest mode execute trap = true
X2AVIC: virtualized X2APIC = true
supervisor shadow stack = true
guest Spec_ctl support = true
ROGPT: read-only guest page table = true
host MCE override = true
INVLPGB/TLBSYNC hyperv interc enable = false
VNMI: NMI virtualization = true
IBS virtualization = true
extended LVT AVIC access changes = true
guest VMCB addr check = true
bus lock threshold = true
idlt HLT intercept = true
EXITINFO1 non-interceptible shutdown = true
NASID: number of address space identifiers = 0x8000 (32768)
 
Adding my findings from testing Windows Server 2025 / Windows 11 VBS guests on a mixed Intel Proxmox cluster.

Cluster CPUs involved in my testing:


  • Cascade Lake: Intel Xeon Gold 6244 @ 3.60 GHz
  • Ice Lake: Intel Xeon Gold 6348 @ 2.60 GHz
  • Emerald Rapids: Intel Xeon Gold 6548Y+ @ 2.50 GHz

Idea is to keep live migration working while reducing the high VM-exit / host CPU usage and visible guest lag seen with Windows Server 2025 (and 11 Pro), especially when VBS is enabled. With the wrong CPU flags, the VM is not just inefficient at idle; it is visibly laggy.

Testing was done on Proxmox 9.2.2 with Linux 7.x. vmx-mbec was recently added to the kernel and makes VBS in a VM feasible. Without MBEC, VBS/HVCI performance is significantly worse.

Custom CPU models are defined here:

Code:
nano /etc/pve/virtual-guest/cpu-models.conf

This file only needs to be edited on one cluster member, since it is under /etc/pve and is replicated automatically to the other cluster nodes.

The custom model names below are used in VM configs with the custom- prefix, for example:

Code:
cpu: custom-icelake-win-vbs

Current test models:

Code:
cpu-model: cascadelake-lnx
reported-model Cascadelake-Server-v5
phys-bits 46
flags +md-clear;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss

cpu-model: cascadelake-win
reported-model Cascadelake-Server-v5
phys-bits 46
flags +md-clear;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss;+hv-apicv;+hv-frequencies;+hv-tlbflush;+hv-tlbflush-ext;+hv-xmm-input

cpu-model: cascadelake-win-vbs
reported-model Cascadelake-Server-v5
phys-bits 46
flags +md-clear;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss;+hv-apicv;+hv-frequencies;+hv-tlbflush;+hv-tlbflush-ext;+hv-xmm-input;+hv-evmcs;+hv-emsr-bitmap;+hv-tlbflush-direct;+vmx;+vmx-mbec;+hv-reenlightenment;+hv-stimer-direct

cpu-model: icelake-lnx
reported-model Icelake-Server-v6
phys-bits 46
flags -taa-no;+sbdr-ssdp-no;+psdp-no;+md-clear;+rdctl-no;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss;+dtes64;+pdcm;+pmu

cpu-model: icelake-win
reported-model Icelake-Server-v6
phys-bits 46
flags -taa-no;+sbdr-ssdp-no;+psdp-no;+md-clear;+rdctl-no;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss;+dtes64;+pdcm;+pmu;+hv-apicv;+hv-frequencies;+hv-tlbflush;+hv-tlbflush-ext;+hv-xmm-input

cpu-model: icelake-win-vbs
reported-model Icelake-Server-v6
phys-bits 46
flags -taa-no;+sbdr-ssdp-no;+psdp-no;+md-clear;+rdctl-no;+rfds-no;+flush-l1d;+stibp;+tsc-adjust;+ss;+dtes64;+pdcm;+pmu;+hv-apicv;+hv-frequencies;+hv-tlbflush;+hv-tlbflush-ext;+hv-xmm-input;+hv-evmcs;+hv-emsr-bitmap;+hv-tlbflush-direct;+vmx;+vmx-mbec;+hv-reenlightenment;+hv-stimer-direct

A few notes from testing:

  1. For Windows VBS guests, +vmx-mbec is the most important accelerator. With VBS/HVCI enabled, MBEC makes a major difference to guest responsiveness.
  2. For Windows VBS guests, +hv-evmcs also helped in my environment. Removing it made the VM slower.
  3. +hv-reenlightenment requires pinning a virtual TSC frequency for live migration. The hosts in this migration pool have nominal TSC/base frequencies of 2.5 GHz, 2.6 GHz, and 3.6 GHz. I pinned the VM TSC to 2.5 GHz, the lowest value in the pool:

    Code:
    qm set <vmid> --args "-global cpu.tsc-frequency=2500000000"

    This is not a CPU performance cap. It is the virtual invariant TSC rate exposed to the guest for migration compatibility.
  4. +invtsc and +hv-stimer-direct appear to be mutually exclusive for VBS guests. When both are set, the VM hangs as the Windows bootloader loads if VBS is enabled. In my testing, +invtsc and +hv-stimer-direct were similar in performance, but +hv-stimer-direct felt subjectively faster, so I stopped using +invtsc for these VBS guests. Both require TSC pinning.
  5. Migration from Emerald Rapids 6548Y+ to Cascade Lake 6244 failed when +pmu/+pdcm were exposed in the Cascade Lake ABI. The failure looked like this:

    Code:
    Failed to set MSR 0x38f to 0x7000000ff

    My conclusion: do not expose +pmu/+pdcm in a Cascade Lake cpu model. PMU MSR state can break live migration.
  6. Emerald Rapids 6548Y+ does not support +fb-clear in my testing, so I do not add +fb-clear to any custom CPU model that needs to migrate there.
  7. For Ice Lake Server models, I had to keep -taa-no. Removing it caused some VMs to fail to boot. My understanding is that Intel partially disabledTSX behavior on Ice Lake, and exposing taa-no incorrectly can break things.
  8. Proxmox still adds the normal Windows Hyper-V enlightenments automatically for Windows guests. For example, on one of my Windows VBS VMs, qm showcmd showed the expected base flags such as:

    Code:
    hv_relaxed
    hv_reset
    hv_runtime
    hv_spinlocks=0x1fff
    hv_stimer
    hv_synic
    hv_time
    hv_vapic
    hv_vpindex
    hv_ipi

    The custom model above adds the extra VBS/nested-Hyper-V related pieces on top, such as:

    Code:
    +hv-evmcs
    +hv-emsr-bitmap
    +hv-tlbflush-direct
    +hv-reenlightenment
    +hv-stimer-direct
    +vmx
    +vmx-mbec

So far, the best-performing VBS setup for my Intel hosts has been:
  • keep +vmx-mbec
  • keep +hv-evmcs
  • Keep +hv-stimer-direct
  • Keep +hv-reenlightenment
  • Pin cpu.tsc-frequency
  • Avoid using +invtsc together with +hv-stimer-direct
  • Avoid +fb-clear for Emerald Rapids compatibility
  • Avoid +pmu/+pdcm in Cascade Lake migration ABIs
  • Keep -taa-no on the Ice Lake Server model

This does not eliminate all VBS overhead, but at least the VM is usable and still supports live migration. CPU vulnerabilities reported by Windows and Linux closely match the CPU generation being exposed.

Could the Proxmox team add CPU frequency pinning to the custom CPU config itself. As setting the pinned frequency per VM through the console is not great.
 
I've retested using:
- 25H2 (26200.8524)
- QEMU 11.0.0
- kernel 7.0.10
- vmx-mbec

But unfortunately no change. Idle CPU load is still 1.5-2x higher than on 23H2 and Intel C6/C7 CPU states are still being disabled on VM start.

As for vmx-mbec, neither me nor the OP had VBS enabled in the first place, so I guess it's not related to the issue of this topic.
 
My write up about issues with Windows 11, high CPU used, VBS, WSL2, nested virt.

https://opensourcetechblog.hashnode...ce-on-proxmox-and-keep-live-migration-working

My main takeaway is, that with "older" hardware, in my case 3rd generation Xeon CPUs(dual socket), CPU frequency needs to be manually defined, while for newer hardware the kernel trusts the clock and it does not have to be.

As @ltctech mentioned, it would be great if CPU frequency could be defined as part of the custom cpu. Maybe by using some "I know what I am doing" check box somewhere. For now Win 11 with VBS, WSL2 on older hardware require manual "args:" setting in vm configuration file. It works, but it is quite impractical.

I am using Win 11 25H2. Qemu 11.0.x, kernel 7.0.10(+) with vmx-mbec.
 
Since the recommended settings vary by CPU, I understand they might not be able to provide specific recommendations, but it’s a bit of a hassle that they don’t at least list the minimum requirements for custom CPUs and machine types.

*I’d like to see all flags and properties (such as `migratable`, `hv_` and all other CPU-related configuration options) made available for custom CPUs. If these settings aren’t provided, we’ll end up having to use `args` anyway, and this new feature will be rendered useless.

Also, while it’s one thing for a major update like from PVE 8 to 9, having to change the args, CPU type, and machine type settings every time there’s a minor update is a bit of a problem…

My only complaint about PVE right now is this CPU issue related to VBS.
 
Last edited:
Hi guys,

I spent a quite some time trying everything mentioned here and in other threads related to this. For me, the benchmark was being able to lower PkgWatt and reach Pkg%pc2 and Pkg%pc6 residency in my VM. This VM I run on my laptop (ThinkPad P1 Gen 8, i7 265H, 64GB RAM, no NVIDIA GPU) onto which I passthrough the GPU (Intel Arc 140T), NVMe controller (990 Pro), and WiFi card (BE210).

On Server 2022, I am able to reach Pkg%pc6 residency of around 60%, even 70% when idle with this setup, that it is with a few tweaks (echo 0 > /sys/module/kvm/parameters/halt_poll_ns, echo 8 > /sys/kernel/debug/pmc_core/ltr_ignore - this is because something disables LTR on this device when the VM is booted and then it is never reverted back until the entire host is rebooted; anyway, not related).

On Windows 11 24H2, as mentioned here, both states are basically unreachable and PkgWatt is pinned at minimum 8W. I observed the high write MSR counts and everything else mentioned here.

I am running on stock Debian, but it doesn't really matter. For these tests on Windows 11 24H2, I have enabled MBEC/GMEC support by using a very recent Ubuntu Linux 7.2 kernel from here which has the necessary patches. I can't say I observed much, but I am sure it doesn't hurt. I am using QEMU v11.0.3-3-gc3ea634 with the "igd-lpc support for q35" patch [1], ACPI CPPC patches and ACPI battery, AC adapter and lid patches.

After much "debating" with Claude, I finally succumbed and accepted to take a look on the NT kernel side of things. Among a few (hallucinated) dead ends, the idea of increasing KeMinimumIncrement came. For some reason, in this VM it is set to 0.5ms, and by just increasing it to 15.6ms, the problem goes away. First, launch this on host: turbostat --interval 5 --show Pkg%pc2,Pkg%pc6,PkgWatt, and then, to adjust the minimum in Windows, do so from WinDbg kernel debugger, on a fresh boot, when you have reached the desktop:

Code:
ed nt!KeMinimumIncrement 000186a0
g

This gets me from numbers like these:

Code:
                                 VM-EXIT    Samples  Samples%     Time%    Min Time    Max Time         Avg time
                               MSR_WRITE       3304    36.49%    27.39%      1.29us  58660.68us    780.72us ( +-   9.33% )
                                     HLT       2933    32.39%    70.88%      0.99us  59354.54us   2275.92us ( +-   3.09% )
                             EOI_INDUCED       2486    27.46%     1.02%      0.82us  36253.94us     38.75us ( +-  47.78% )
                                  VMCALL        220     2.43%     0.39%      1.62us  23145.27us    168.79us ( +-  71.21% )
                      EXTERNAL_INTERRUPT         86     0.95%     0.24%      2.49us  21929.36us    265.32us ( +-  96.06% )
                          IO_INSTRUCTION         21     0.23%     0.07%     17.28us   6245.37us    326.76us ( +-  90.57% )
                           EPT_MISCONFIG          4     0.04%     0.00%      7.89us     19.04us     11.70us ( +-  21.71% )

To numbers like these:


Code:
                                 VM-EXIT    Samples  Samples%     Time%    Min Time    Max Time         Avg time

                             EOI_INDUCED        950    46.64%     0.03%      1.01us     39.53us      5.14us ( +-   2.49% )
                                     HLT        940    46.15%    97.62%    186.04us  62462.74us  15069.58us ( +-   0.61% )
                      EXTERNAL_INTERRUPT         65     3.19%     0.01%      6.88us     51.68us     23.80us ( +-   5.04% )
                                MSR_READ         44     2.16%     1.51%      1.51us  62507.30us   4965.37us ( +-  45.38% )
                           EPT_MISCONFIG         21     1.03%     0.64%      6.95us  92747.53us   4427.60us ( +-  99.74% )
                          IO_INSTRUCTION         15     0.74%     0.19%      4.93us  27776.22us   1869.92us ( +-  98.96% )
                                   CPUID          1     0.05%     0.00%      2.43us      2.43us      2.43us ( +-   0.00% )
                       PAUSE_INSTRUCTION          1     0.05%     0.00%      5.24us      5.24us      5.24us ( +-   0.00% )

From turbostat showing this:
Code:
Pkg%pc2 Pkg%pc6 PkgWatt
0.00    0.00    8.48
0.00    0.00    8.48

To numbers like these:
Code:
Pkg%pc2 Pkg%pc6 PkgWatt
9.65    55.13   4.19
9.65    55.13   4.19

Does it break anything? To be honest, I don't really know. I just sketched this up, tested it a bit and posted it here. Claude also wrote a script that I spawn after the VM is started (via a libvirtd hook, or manually), which scans the memory and identifies the value of KeMinimumIncrement and patches like we did from WinDbg. It works, doesn't seem to break other stuff, but yeah, has to be tested in the field so to say. I had it include plenty of debugging info into it, so you can see what it is doing. Call it like python3 fix-tick.py console, where fix-tick.py is its filename, and console the domain for which you want to apply this. It uses that and gets the PID of the respective qemu process, you can hardcode PID for quick testing.

Code:
#!/usr/bin/env python3
import os, re, struct, subprocess, sys, time

WANT = 0x2625a
EXPECT = 0x1388
SYM = b"KeQueryTimeIncrement"
DELTA = 0x9c        # KeMinimumIncrement - KeMaximumIncrement
WINDOW = 0x400


def log(msg):
    print(f"[{time.strftime('%H:%M:%S')}] {msg}", flush=True)


def qemu_pid(dom):
    pid = subprocess.check_output(["pgrep", "-f", f"guest={dom},"]).decode().split()[0]
    log(f"domain {dom!r} -> qemu pid {pid}")
    return pid


def ram_regions(pid):
    out = []
    for line in open(f"/proc/{pid}/maps"):
        m = re.match(r"([0-9a-f]+)-([0-9a-f]+) rw-[sp]", line)
        if not m:
            continue
        a, b = int(m.group(1), 16), int(m.group(2), 16)
        if b - a >= 512 << 20:
            out.append((a, b - a))
            log(f"  candidate RAM region {a:#014x} size {(b-a)>>20} MiB")
    if not out:
        log("  NO RAM REGIONS FOUND (>=512 MiB rw mapping) -- wrong pid?")
    return out


def rd(fd, off, n):
    os.lseek(fd, off, os.SEEK_SET)
    return os.read(fd, n)


def parse_pe(fd, base):
    try:
        hdr = rd(fd, base, 0x1000)
        if len(hdr) < 0x1000 or hdr[:2] != b"MZ":
            return None
        e = struct.unpack_from("<I", hdr, 0x3c)[0]
        if e > 0xf00 or hdr[e:e + 4] != b"PE\0\0":
            return None
        if struct.unpack_from("<H", hdr, e + 24)[0] != 0x20b:
            return None
        edir = struct.unpack_from("<I", hdr, e + 24 + 112)[0]
        if not edir:
            return None
        ed = rd(fd, base + edir, 40)
        nrva = struct.unpack_from("<I", ed, 12)[0]
        name = rd(fd, base + nrva, 16).split(b"\0")[0]
        if not name.lower().startswith(b"ntoskrnl.exe"):
            return None
        log(f"  PE64 at {base:#014x} export name {name!r}")
        log("  -> ntoskrnl.exe MATCH")
        return ed
    except OSError:
        return None


def find_sym(fd, base, ed):
    nfun, nnam = struct.unpack_from("<II", ed, 20)
    afun, anam, aord = struct.unpack_from("<III", ed, 28)
    log(f"  export dir: {nfun} functions, {nnam} names")
    names = struct.unpack("<%dI" % nnam, rd(fd, base + anam, 4 * nnam))
    for i, nrva in enumerate(names):
        if rd(fd, base + nrva, len(SYM) + 1) == SYM + b"\0":
            o = struct.unpack_from("<H", rd(fd, base + aord + 2 * i, 2))[0]
            rva = struct.unpack_from("<I", rd(fd, base + afun + 4 * o, 4))[0]
            log(f"  {SYM.decode()} name index {i} ordinal {o} rva {rva:#x}")
            return rva
    log(f"  {SYM.decode()} NOT in export table")
    return None


def attempt(fd, pid, n):
    log(f"--- pass {n} ---")
    scanned = 0
    for start, size in ram_regions(pid):
        for off in range(start, start + size, 0x1000):
            scanned += 1
            ed = parse_pe(fd, off)
            if not ed:
                continue
            rva = find_sym(fd, off, ed)
            if not rva:
                continue
            code = rd(fd, off + rva, 16)
            log(f"  KeQueryTimeIncrement bytes {code[:8].hex()}")
            if code[0:2] != b"\x8b\x05":
                log("  unexpected prologue, cannot derive KeMaximumIncrement")
                continue
            disp = struct.unpack_from("<i", code, 2)[0]
            maxr = rva + 6 + disp
            log(f"  KeMaximumIncrement rva {maxr:#x} = "
                f"{struct.unpack('<I', rd(fd, off + maxr, 4))[0]:#x}")
            minr = maxr + DELTA
            if struct.unpack("<I", rd(fd, off + minr, 4))[0] != EXPECT:
                log(f"  +{DELTA:#x} not {EXPECT:#x}, scanning +/-{WINDOW:#x}")
                blob = rd(fd, off + maxr - WINDOW, 2 * WINDOW)
                hits = [i for i in range(0, len(blob) - 3, 4)
                        if struct.unpack_from("<I", blob, i)[0] == EXPECT]
                if len(hits) != 1:
                    log(f"  {len(hits)} candidates, ambiguous -- aborting")
                    continue
                minr = maxr - WINDOW + hits[0]
                log(f"  unique match at rva {minr:#x}")
            addr = off + minr
            cur = struct.unpack("<I", rd(fd, addr, 4))[0]
            log(f"  host addr {addr:#014x} currently {cur:#x} ({cur/10000:.2f} ms)")
            if cur != EXPECT:
                log(f"  MISMATCH: expected {EXPECT:#x}; not writing (image may not be "
                    f"physically contiguous, or already patched)")
                continue
            os.lseek(fd, addr, os.SEEK_SET)
            os.write(fd, struct.pack("<I", WANT))
            back = struct.unpack("<I", rd(fd, addr, 4))[0]
            if back != WANT:
                log(f"  WRITE FAILED: reads back {back:#x}")
                return False
            log(f"  PATCHED {cur:#x} -> {WANT:#x} ({WANT/10000:.2f} ms) verified")
            return True
    log(f"  scanned {scanned} pages, no ntoskrnl yet (guest still booting?)")
    return False


dom = sys.argv[1]
log(f"start: target {SYM.decode()} {EXPECT:#x} -> {WANT:#x}")
pid = qemu_pid(dom)
fd = os.open(f"/proc/{pid}/mem", os.O_RDWR)
try:
    for n in range(1, 61):
        if attempt(fd, pid, n):
            log("done")
            sys.exit(0)
        time.sleep(10)
finally:
    os.close(fd)
log("TIMED OUT after 10 minutes")
sys.exit(1)

(continued in next message due to 16384 char limit)
 
Last edited:
For completeness, here is also my full virsh XML for this VM:
Code:
<domain type='kvm' id='2' xmlns:qemu='http://libvirt.org/schemas/domain/qemu/1.0'>
  <name>console</name>
  <uuid>837730e7-258a-4357-865b-375fe422825f</uuid>
  <memory unit='KiB'>33554432</memory>
  <currentMemory unit='KiB'>33554432</currentMemory>
  <vcpu placement='static'>16</vcpu>
  <iothreads>1</iothreads>
  <cputune>
    <vcpupin vcpu='0' cpuset='0'/>
    <vcpupin vcpu='1' cpuset='1'/>
    <vcpupin vcpu='2' cpuset='2'/>
    <vcpupin vcpu='3' cpuset='3'/>
    <vcpupin vcpu='4' cpuset='4'/>
    <vcpupin vcpu='5' cpuset='5'/>
    <vcpupin vcpu='6' cpuset='6'/>
    <vcpupin vcpu='7' cpuset='7'/>
    <vcpupin vcpu='8' cpuset='8'/>
    <vcpupin vcpu='9' cpuset='9'/>
    <vcpupin vcpu='10' cpuset='10'/>
    <vcpupin vcpu='11' cpuset='11'/>
    <vcpupin vcpu='12' cpuset='12'/>
    <vcpupin vcpu='13' cpuset='13'/>
    <vcpupin vcpu='14' cpuset='14'/>
    <vcpupin vcpu='15' cpuset='15'/>
    <emulatorpin cpuset='14-15'/>
  </cputune>
  <resource>
    <partition>/machine</partition>
  </resource>
  <os firmware='efi'>
    <type arch='x86_64' machine='pc-q35-10.0'>hvm</type>
    <firmware>
      <feature enabled='no' name='enrolled-keys'/>
      <feature enabled='no' name='secure-boot'/>
    </firmware>
    <loader readonly='yes' type='pflash' format='raw'>/usr/share/OVMF/OVMF_CODE_4M.fd</loader>
    <nvram template='/usr/share/OVMF/OVMF_VARS_4M.fd' templateFormat='raw' format='raw'>/var/lib/libvirt/qemu/nvram/console_VARS.fd</nvram>
    <boot dev='hd'/>
    <smbios mode='sysinfo'/>
  </os>
  <features>
    <acpi/>
    <apic/>
    <hyperv mode='custom'>
    </hyperv>
    <smm state='on'/>
  </features>
  <cpu mode='host-passthrough' check='none' migratable='on'>
    <topology sockets='1' dies='1' clusters='1' cores='16' threads='1'/>
    <maxphysaddr mode='passthrough' limit='42'/>
  </cpu>
  <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>
  <devices>
    <emulator>/usr/bin/qemu-system-x86_64</emulator>
    <disk type='file' device='cdrom'>
      <driver name='qemu' discard='unmap'/>
      <target dev='sda' bus='sata'/>
      <readonly/>
      <alias name='sata0-0-0'/>
      <address type='drive' controller='0' bus='0' target='0' unit='0'/>
    </disk>
    <controller type='pci' index='0' model='pcie-root'>
      <alias name='pcie.0'/>
    </controller>
    <controller type='pci' index='1' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='1' port='0x10'/>
      <alias name='pci.1'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x04' function='0x0'/>
    </controller>
    <controller type='pci' index='2' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='2' port='0x8'/>
      <alias name='pci.2'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x0' multifunction='on'/>
    </controller>
    <controller type='pci' index='3' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='3' port='0x9'/>
      <alias name='pci.3'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x1'/>
    </controller>
    <controller type='pci' index='4' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='4' port='0xa'/>
      <alias name='pci.4'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x2'/>
    </controller>
    <controller type='pci' index='5' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='5' port='0xb'/>
      <alias name='pci.5'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x3'/>
    </controller>
    <controller type='pci' index='6' model='pcie-to-pci-bridge'>
      <model name='pcie-pci-bridge'/>
      <alias name='pci.6'/>
      <address type='pci' domain='0x0000' bus='0x01' slot='0x00' function='0x0'/>
    </controller>
    <controller type='pci' index='7' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='7' port='0xc'/>
      <alias name='pci.7'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x4'/>
    </controller>
    <controller type='pci' index='8' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='8' port='0xd'/>
      <alias name='pci.8'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x5'/>
    </controller>
    <controller type='pci' index='9' model='pcie-root-port'>
      <model name='pcie-root-port'/>
      <target chassis='9' port='0xe'/>
      <alias name='pci.9'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x01' function='0x6'/>
    </controller>
    <controller type='sata' index='0'>
      <alias name='ide'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x1f' function='0x2'/>
    </controller>
    <controller type='usb' index='0' model='qemu-xhci'>
      <alias name='usb'/>
      <address type='pci' domain='0x0000' bus='0x02' slot='0x00' function='0x0'/>
    </controller>
    <interface type='network'>
      <mac address='52:54:00:dd:6d:9c'/>
      <source network='default' portid='66704f91-cef2-4eff-b2b4-b3939822f4aa' bridge='virbr0'/>
      <target dev='vnet1'/>
      <model type='virtio'/>
      <alias name='net0'/>
      <address type='pci' domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
    </interface>
    <serial type='pty'>
      <source path='/dev/pts/1'/>
      <target type='isa-serial' port='0'>
        <model name='isa-serial'/>
      </target>
      <alias name='serial0'/>
    </serial>
    <serial type='tcp'>
      <source mode='bind' host='0.0.0.0' service='4445' tls='no'/>
      <protocol type='raw'/>
      <target type='isa-serial' port='1'>
        <model name='isa-serial'/>
      </target>
      <alias name='serial1'/>
    </serial>
    <console type='pty' tty='/dev/pts/1'>
      <source path='/dev/pts/1'/>
      <target type='serial' port='0'/>
      <alias name='serial0'/>
    </console>
    <input type='evdev'>
      <source dev='/dev/input/vmpointer'/>
      <alias name='input0'/>
    </input>
    <input type='evdev'>
      <source dev='/dev/input/vmkeys'/>
      <alias name='input1'/>
    </input>
    <input type='evdev'>
      <source dev='/dev/input/by-path/platform-i8042-serio-0-event-kbd' grab='all' grabToggle='ctrl-ctrl' repeat='on'/>
      <alias name='input2'/>
    </input>
    <input type='evdev'>
      <source dev='/dev/input/by-path/platform-i8042-serio-1-event-mouse'/>
      <alias name='input3'/>
    </input>
    <input type='mouse' bus='ps2'>
      <alias name='input4'/>
    </input>
    <input type='keyboard' bus='ps2'>
      <alias name='input5'/>
    </input>
    <audio id='1' type='none'/>
    <video>
      <model type='none'/>
      <alias name='video0'/>
    </video>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
      </source>
      <alias name='ua-igd'/>
      <rom bar='on' file='/igd2.rom'/>
      <address type='pci' domain='0x0000' bus='0x00' slot='0x02' function='0x0'/>
    </hostdev>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x04' slot='0x00' function='0x0'/>
      </source>
      <alias name='hostdev0'/>
      <address type='pci' domain='0x0000' bus='0x03' slot='0x00' function='0x0'/>
    </hostdev>
    <hostdev mode='subsystem' type='pci' managed='yes'>
      <driver name='vfio'/>
      <source>
        <address domain='0x0000' bus='0x00' slot='0x14' function='0x3'/>
      </source>
      <alias name='hostdev1'/>
      <address type='pci' domain='0x0000' bus='0x07' slot='0x00' function='0x0'/>
    </hostdev>
    <watchdog model='itco' action='reset'>
      <alias name='watchdog0'/>
    </watchdog>
    <memballoon model='virtio'>
      <alias name='balloon0'/>
      <address type='pci' domain='0x0000' bus='0x05' slot='0x00' function='0x0'/>
    </memballoon>
  </devices>
  <seclabel type='dynamic' model='dac' relabel='yes'>
    <label>+0:+0</label>
    <imagelabel>+0:+0</imagelabel>
  </seclabel>
  <qemu:commandline>
    <qemu:arg value='-cpu'/>
    <qemu:arg value='+vmx,+kvm_pv_eoi,+kvm_pv_unhalt,hv-relaxed,hv-vapic,hv-time,hv-synic,hv-stimer,hv-vpindex,hv-ipi,hv-runtime,hv-reset,hv-crash,hv-spinlocks=0xfff,hv-tlbflush,hv-tlbflush-ext,hv-tlbflush-direct,hv-stimer-direct,hv-apicv,hv-avic,hv-evmcs,hv-xmm-input,hv-emsr-bitmap,host-cache-info=on,l3-cache=on,hv-frequencies,hv-reenlightenment'/>
    <qemu:arg value='-device'/>
    <qemu:arg value='{&apos;driver&apos;:&apos;acpi-cppc&apos;,&apos;processors&apos;:[&apos;0:0:85:35:29:1&apos;,&apos;1:1:85:35:29:1&apos;,&apos;2:2:85:35:29:1&apos;,&apos;3:3:85:35:29:1&apos;,&apos;4:4:85:35:29:1&apos;,&apos;5:5:85:35:29:1&apos;,&apos;6:6:63:23:19:1&apos;,&apos;7:7:63:23:19:1&apos;,&apos;8:8:63:23:19:1&apos;,&apos;9:9:63:23:19:1&apos;,&apos;10:10:63:23:19:1&apos;,&apos;11:11:63:23:19:1&apos;,&apos;12:12:63:23:19:1&apos;,&apos;13:13:63:23:19:1&apos;,&apos;14:14:25:7:700:1&apos;,&apos;15:15:25:7:700:1&apos;]}'/>
    <qemu:arg value='-overcommit'/>
    <qemu:arg value='x-cpu-perf=on'/>
    <qemu:arg value='-cpu'/>
    <qemu:arg value='hv-no-nonarch-coresharing=on'/>
    <qemu:arg value='-device'/>
    <qemu:arg value='battery'/>
    <qemu:arg value='-qmp'/>
    <qemu:arg value='unix:/tmp/qmp.sock,server=on,wait=off'/>
    <qemu:arg value='-device'/>
    <qemu:arg value='acad'/>
    <qemu:arg value='-qmp'/>
    <qemu:arg value='unix:/tmp/qmp.sock,server=on,wait=off'/>
    <qemu:arg value='-device'/>
    <qemu:arg value='button'/>
    <qemu:arg value='-qmp'/>
    <qemu:arg value='unix:/tmp/qmp.sock,server=on,wait=off'/>
  </qemu:commandline>
  <qemu:override>
    <qemu:device alias='ua-igd'>
      <qemu:frontend>
        <qemu:property name='x-igd-opregion' type='bool' value='true'/>
        <qemu:property name='x-igd-lpc' type='bool' value='true'/>
      </qemu:frontend>
    </qemu:device>
  </qemu:override>
</domain>

PLEASE flag any obvious and not so obvious mistakes I have made, as I am being honest, I do not understand everything presented 100%, but I am learning. And feel free to ask for any additional resources that you need from my setup, even unrelated.

[1]
Code:
From d4e88b70f045cc26d7698c200c238839e355cba0 Mon Sep 17 00:00:00 2001
From: Valentin Radu <valentin.radu@valinet.ro>
Date: Sun, 23 Aug 2026 09:41:40 +0300
Subject: [PATCH] igd-lpc support for q35

---
 hw/vfio/igd.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/hw/vfio/igd.c b/hw/vfio/igd.c
index 6aacf27..d92fc24 100644
--- a/hw/vfio/igd.c
+++ b/hw/vfio/igd.c
@@ -371,7 +371,7 @@ static bool vfio_pci_igd_setup_lpc_bridge(VFIOPCIDevice *vdev, Error **errp)
      */
     lpc_bridge = pci_find_device(pci_device_root_bus(pdev),
                                  0, PCI_DEVFN(0x1f, 0));
-    if (lpc_bridge && !object_dynamic_cast(OBJECT(lpc_bridge),
+    if (lpc_bridge && object_dynamic_cast(OBJECT(lpc_bridge),
                                            "vfio-pci-igd-lpc-bridge")) {
         error_setg(errp,
                    "Cannot create LPC bridge due to existing device at 1f.0");
--
2.47.3
 
Something's wrong with this OS. You can bump that number in the Python script, I tested with 500ms. The pkg pc6 residency then reaches 60-70% when idle, as was the case with previous versions. Effects can be easily seen, for example, the cursor takes a moment to move after it's been stationary for a while, for example. Expected. But what this shows is that there are either too many timers scheduled, or that they or someone does too much work in the servicing routines for this, thus killing residency. When you make them happen less often, that long processing becomes less noticeable, since it happens more rarely.

But to break this basic feature is another level even for Microslop, really...

This is more of an experiment. Software sometimes wants a more frequent timer, in order to meet user's demands, that the user knows about. But why the fuck would you infect the kernel with such an unexplained random behavior... My theory is that they are simply doing too much work in one of those handlers. 500ms is a long time, some things need that more frequent 15.6ms servicing frequency, and that did not use to cause issues previously,as it is fine to do crap every 15.6 so long it doesn't take milliseconds.
 
Last edited:
No, it's off. 24H2 indeed introduced per core clock ticks, and apparently that is what kills residency in VMs. I don't really understand why, but that is one difference in the KiSetTime/KiGetTime functions in 24H2 compared to Server 2022, for example. Fortunately, this is gated behind a kernel registry flag:

Code:
Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\kernel]
"EnablePerCpuClockTickScheduling"=dword:00000002

0 means default, 1 means enabled, anything else, like 2, means disabled

This makes the previous "console-fix-ticks" not necessary, of course. It was a huge hack in the first place.

I now get this residency at idle in Windows 11 25H2:

Code:
Pkg%pc2 Pkg%pc3 Pkg%pc6 Pk%pc10 PkgWatt
3.44    0.00    80.54   0.00    2.53
3.44    0.00    80.54   0.00    2.53

I don’t really understand where this change is documented. Something in the newer code path doesn’t cope well with what qemu presents, so maybe it could be fixed at hypervisor level somehow. Has to be studied, but at least now I can get proper idling on this laptop with the VM on.

Edit: I mean, it is an issue with the machine qemu emulates. I say this because, if it were a problem necessarily only with this new clock model, then physical hosts would not reach deeper states as well, yet that is not the case, you can still get that under 24h2 physically. So there is a thing the hypervisor presents that the Windows guest, in this new model, doesn’t cooperate well with. It should be investigated from a qemu/kvm perspective, I mean, it is easy to blame Microslop, they could be more transparent and step up here, for example, since they continuously brag about how they <3 Linux every time they leech off some open infrastructure, but if their model works on most machines but qemu’s, maybe there is something to look into here as well.

Thanks to everyone involved for the tips and support!
 
Last edited:
  • Like
Reactions: ksb and SchorschSK
Absolutely great work !!! After applying the registry hack and rebooting our Idle times are now the same as on pre 24H2.

CPU idle less then 1%.

1787683616303.png
 
  • Like
Reactions: vali20
Machine left for 2 hours idling, virtio-nic connected, WiFi turned off from action center in Windows, screen off, average residency:
Code:
Pkg%pc2 Pkg%pc3 Pkg%pc6 Pk%pc10 PkgWatt
4.77    0.00    71.89   0.00    2.63
4.77    0.00    71.89   0.00    2.63

Measuring with: upower -i /org/freedesktop/UPower/devices/battery_BAT0

It went from:
Code:
updated: Tue Aug 25 20:34:30 2026
energy: 84.04 Wh
energy-rate: 3.055 W
percentage: 94%

To:
Code:
updated: Tue Aug 25 22:38:35 2026
energy: 78.5 Wh
energy-rate: 2.854 W
percentage: 87%

So 7% in 2 hours on a 90 Wh battery, that is very adequate in my opinion. It is approaching Connected Standby level figures (which yet again shows how much crap that is and how much better S3 sleep was, but. again, Microslop).

I am going to daily drive this, and hopefully prepare a write-up to document all of this, plus all my patches to qemu and custom scripts/utilities to get this laptop usable like this: Linux underneath, Windows as a shell on top (fixed brightness keys, brightness slider, touchpad, horizontal scrolling on touchpad, battery, lid and AC adapter reporting and much more).

Edit: Useful reading: https://noverse.dev/docs/win-config/system/timer-expiration, https://github.com/nohuto/win-config/blob/main/system/desc.md
 
Last edited: