Systems 314-833-6500

Hyper-V virtual processor scheduling internals

Core scheduler, SMT, NUMA, CPU groups, and vCPU sizing.
MGB Systems Engineering ·

Applies to Windows Server 2016, 2019, 2022, and 2025, and Azure Local

Most vCPU guidance stops at "a vCPU isn't a core" and "don't over-commit too much." This guide goes further. It covers what a Hyper-V virtual processor is inside the hypervisor, how each of the three hypervisor schedulers places it on silicon, the arithmetic behind reserve, limit and relative weight, and how CPU groups and minroot give you real isolation. It also shows how to measure CPU contention with the hypervisor's own counters, so you can set consolidation ratios from data instead of folklore.

Key takeaways
  • A Hyper-V vCPU is a virtual processor (VP): saved register state plus a scheduling entity owned by the hypervisor. It is never permanently bound to a core.
  • Windows Server 2019 and later use the core scheduler by default. It never lets two VMs share a physical core at the same time, which closes SMT side channels but makes odd vCPU counts and missing guest SMT more expensive.
  • Reserve and limit are percentages of the VM's own vCPUs, not of the host. Convert them before you reason about capacity.
  • Real isolation needs CPU groups plus minroot. Per-VM settings only shape fairness.
  • Root-partition Task Manager does not show host CPU load. Use Hyper-V Hypervisor Logical Processor\% Total Run Time and CPU Wait Time Per Dispatch.
  • Hyper-V enforces no vCPU:LP ratio. Size from measured 95th-percentile demand against physical cores.

1. Terminology: LP, VP, root VP, partition

Imprecise vocabulary causes most vCPU misunderstandings. These are the terms Microsoft's hypervisor documentation uses, and the ones used in the rest of this article.

TermMeaningWhere you see it
Physical coreAn execution core on the CPU package.Hardware spec, Win32_Processor.NumberOfCores
Logical processor (LP)A hardware thread. With SMT (Hyper-Threading) on, each core exposes two LPs.(Get-VMHost).LogicalProcessorCount, Hyper-V Hypervisor Logical Processor counters
PartitionAn isolation container managed by the hypervisor. The root (parent) partition runs the Windows Server host OS. Each VM is a child partition.Hyper-V architecture docs, TLFS
Virtual processor (VP)The hypervisor object that represents one vCPU of a partition: architectural register state plus scheduler bookkeeping.Hyper-V Hypervisor Virtual Processor(VM:Hv VP n)
Root VPA VP belonging to the root partition. The host OS is itself scheduled on VPs.Hyper-V Hypervisor Root Virtual Processor
NUMA nodeA group of cores with local memory. Usually one per socket, sometimes more (sub-NUMA clustering, AMD NPS settings).Get-VMHostNumaNode

2. Where a vCPU actually lives

Hyper-V is a type-1 hypervisor. When a host boots with the role installed, the hypervisor loads first and the Windows Server installation you log on to becomes the root partition, a privileged guest. Because of this, the host OS doesn't see the bare hardware either. Its "CPUs" are root VPs, and by default each root VP is pinned 1:1 to a logical processor, while guest VPs share those same LPs.

A guest VP has two states:

  • Dispatched: the hypervisor has loaded the VP's state into a hardware control structure (Intel VMCS or AMD VMCB) and performed a VM entry. Guest instructions now run natively on that LP.
  • Not running: the VP is idle (the guest executed HLT or an idle hypercall), blocked, or runnable but waiting for an LP. That last case is CPU contention, and it is what you measure in section 9.

Control returns to the hypervisor on a VM exit: a timer tick, an intercepted instruction, an interrupt, or a hypercall. At that point the scheduler can keep the VP running or switch to another VP. Hardware assists such as second-level address translation (Intel EPT, AMD NPT) and APIC virtualization exist mainly to reduce how often those exits happen.

Hyper-V scheduling layers: guest threads on guest VPs, hypervisor scheduler, logical processors on SMT cores Root partition (host OS) NT scheduler: host threads Root VP0 Root VP1 … 1:1 per LP VM A (child partition) Guest scheduler: app threads VP0 VP1 VM B (child partition) Guest scheduler: app threads VP0 VP1 Hypervisor scheduler: classic, core or root. Decides which VP runs on which LP, and when. Inputs: runnable VPs, reserve / limit / weight, CPU-group caps and affinity, NUMA placement, SMT sibling state Physical core 0 LP0 (SMT thread 0) LP1 (SMT thread 1) Physical core 1 LP2 (SMT thread 0) LP3 (SMT thread 1)
Figure 1. There are two schedulers in the path. The guest OS chooses which thread runs on its VP, and the hypervisor chooses which VP runs on which LP. Neither can see the other's queue.

This two-level design explains several things that confuse people:

  • A guest at 100% CPU means its VPs were busy whenever they were dispatched. It says nothing about how long they waited to be dispatched.
  • Guest-level processor affinity pins threads to virtual processors only. Hyper-V offers no per-VM pinning of guest VPs to particular LPs. The supported way to constrain placement is a CPU group with an LP affinity mask (section 6).
  • A VM can never have more VPs than the host has LPs, and Hyper-V refuses that configuration. Even at the limit, a very wide VM is a bad idea: the guest's spin locks, TLB shootdowns and barrier-style parallel code all assume that sibling VPs are running at the same time, and on a busy host they often aren't.

3. Classic, core and root schedulers

Since Windows Server 2016, the hypervisor has three scheduler implementations, chosen at boot.

SchedulerPlacement ruleDefault onPer-VM reserve / limit / weight
ClassicFair-share, preemptive round robin across all VPs, root VPs included. With SMT on, each sibling thread is scheduled independently, so VPs from two different VMs can share a core at the same moment.Windows Server 2016 and earlierHonored
CoreVPs are dispatched in groups that match the host's SMT topology. A physical core is only ever shared by VPs of the same VM. If a VM's sibling VP can't run, the other hardware thread on that core idles.Windows Server 2019, 2022, 2025Honored
RootThe hypervisor delegates scheduling to the root partition's NT scheduler, which treats guest VPs much like host threads.Windows 10 1803+ and Windows 11 client Hyper-VNot honored. Not supported on Windows Server.

Checking and changing the scheduler type

The hypervisor logs its scheduler type at every launch in the System log, provider Microsoft-Windows-Hyper-V-Hypervisor, event ID 2. The values are: 1 classic with SMT disabled, 2 classic, 3 core, 4 root.

# Most recent hypervisor launch event, decoded
$e = Get-WinEvent -FilterHashtable @{ LogName='System'; ProviderName='Microsoft-Windows-Hyper-V-Hypervisor'; Id=2 } -MaxEvents 1
$map = @{ 1='Classic (SMT disabled)'; 2='Classic'; 3='Core'; 4='Root' }
if ($e.Message -match '0x([0-9a-fA-F]+)') { $t = [Convert]::ToInt32($Matches[1],16) } else { $t = [int]$e.Properties[0].Value }
"{0}  Scheduler type {1} = {2}" -f $e.TimeCreated, $t, $map[$t]

To change it, run this from an elevated prompt and reboot. The setting is a boot configuration value, so it can't be changed while the hypervisor is running.

bcdedit /set hypervisorschedulertype Core     &:: Classic | Core | Root
Windows Server 2016 hosts: classic is still the default, and Microsoft recommends switching to the core scheduler. If a 2016 host has SMT enabled and runs VMs of different trust levels (multi-tenant, DMZ next to internal, and so on), the classic scheduler is a real side-channel exposure.

4. The core scheduler and guest SMT

Why the core scheduler exists

SMT siblings share a core's L1 data cache, store buffers, fill buffers and other microarchitectural state. The L1 Terminal Fault and MDS class of speculative-execution vulnerabilities showed that code on one hardware thread can infer data from its sibling. Under the classic scheduler, that sibling could belong to a different VM. The core scheduler makes the core the security boundary: two VMs never run on the same core at the same time.

The hypervisor tells guests about this guarantee. CPUID leaf 0x40000004, EAX bit 18 (NoNonArchitecturalCoreSharing) is set to 1 only when a VP will never share a core with anything except its own architectural SMT siblings. Windows guests read this bit to decide whether their own in-guest SMT mitigations are needed. Under the classic scheduler with SMT on, the bit is 0.

The cost: idle siblings

Because scheduling is per core, a VM whose VPs are not presented as SMT pairs consumes a whole core for each running VP, and the sibling thread stays empty. The same happens when only one VP in a sibling pair is runnable. On a busy host, this lowers the effective throughput of SMT, so two settings matter much more than they did under the classic scheduler:

  1. Guest SMT exposure, controlled by HwThreadCountPerCore.
  2. vCPU count parity, meaning even versus odd vCPU counts.

HwThreadCountPerCore

ValueGuest seesNotes
0The same threads-per-core as the hostDefault for new VMs on Windows Server 2019+ (config version 9.0+). Not supported on 2016.
1No SMT: every VP is its own coreDefault on Windows Server 2016 and for VMs with config version 8.x or earlier. Updating the config version does not change it.
2Two threads per coreCan't exceed the host's physical threads per core.

The trap is upgrades. VMs created on 2016 and later moved to 2019, 2022 or 2025 (live migration, then Update-VMVersion) keep HwThreadCountPerCore = 1. They run on a core-scheduler host without SMT awareness and burn a full core per VP. When a non-SMT VM has more VPs than the host has physical cores, the Hyper-V worker logs event 3498 (Microsoft-Windows-Hyper-V-Worker), flags the configuration as non-optimal, and recommends enabling SMT for the VM. Search for it, and also audit the setting directly:

Get-WinEvent -FilterHashtable @{ ProviderName='Microsoft-Windows-Hyper-V-Worker'; Id=3498 } -ErrorAction SilentlyContinue |
  Select-Object TimeCreated, Message -First 20

# Find VMs not inheriting host SMT, then fix them (VM must be off)
Get-VM | Get-VMProcessor | Where-Object HwThreadCountPerCore -ne 0 |
  Select-Object VMName, Count, HwThreadCountPerCore
# Set-VMProcessor -VMName 'APP01' -HwThreadCountPerCore 0

Even vCPU counts

With guest SMT exposed on a two-thread-per-core host, a 3-vCPU VM is presented as one full core plus one half core. When the lone VP on that half core runs, the core scheduler can't put another VM's VP on its sibling, so half a core idles. On core-scheduler hosts with SMT, use even vCPU counts (2, 4, 6, 8…). The waste is small for a single VM but adds up across a dense cluster.

How much is SMT worth?

SMT adds throughput only when one thread stalls (cache misses, branch mispredictions, memory latency) and the other can use the idle execution units. Mixed server workloads usually gain somewhere in the 10–30% range per core, and some highly vectorized or cache-bound workloads gain nothing or lose. For capacity planning, count physical cores as capacity and treat SMT as a modest uplift, not a doubling. A 2×24-core host has 96 LPs but roughly 53–62 cores' worth of real throughput, not 96.

5. Reserve, limit and relative weight: the math

Each VM has three processor resource controls, exposed in Hyper-V Manager under Processor and in PowerShell through Set-VMProcessor. Their units cause most of the confusion.

ControlPowerShellRangeUnitBehavior
Virtual machine reserve-Reserve0–100% of the VM's own vCPUsGuaranteed minimum. Enforced at VM start through admission control.
Virtual machine limit-Maximum0–100% of the VM's own vCPUsHard cap, applied even when the host is idle.
Relative weight-RelativeWeight1–10000 (default 100)UnitlessProportional share, used only when VPs compete for LPs.

Converting to host terms

Let V be the VM's vCPU count, L the host's logical processor count, r the reserve, and m the limit (both as percentages):

Reserved capacity (LP-equivalents) = V × r / 100
Reserved share of host (%)         = V × r / L
Capped capacity (LP-equivalents)   = V × m / 100
Admission rule for running VMs:    Σ (Vᵢ × rᵢ) / L  ≤  100 %

Worked example. On a 64-LP host, an 8-vCPU SQL Server VM has -Reserve 50 -Maximum 75. It is guaranteed 8 × 50% = 4 LPs' worth of time, which is 6.25% of the host and the value Hyper-V Manager shows in its grayed-out "percent of total system resources" box. It can never use more than 8 × 75% = 6 LPs' worth, even at 3 a.m. on an idle host.

Reserves and failover clusters: admission control runs whenever a VM starts, and that includes failover. Reserves that fit comfortably across a four-node cluster can exceed 100% on the surviving nodes after a node is lost, and the VMs that don't fit won't start. If you use reserves, calculate the admission sum for the N−1 case, not the steady state.

Relative weight

Weight matters only when runnable VPs outnumber available LPs. Contended time is then split roughly in proportion to weight: a VM at 200 competing with a VM at 100 gets about twice as much of the contended time. Weight never stops a VM from starting and has no effect on an uncontended host, which makes it the safest of the three controls. Use it to protect tier-1 VMs during contention bursts, and keep reserves for the rare vendor-mandated guarantee.

All three controls are ignored under the root scheduler. If you build lab or dev environments on Windows 10/11 client Hyper-V, don't expect reserves or caps to reproduce production behavior.

# Audit every VM's processor controls on a host
Get-VM | Get-VMProcessor |
  Select-Object VMName, Count, Reserve, Maximum, RelativeWeight, HwThreadCountPerCore |
  Sort-Object Reserve -Descending | Format-Table -AutoSize

# Example: protect a tier-1 VM by weight rather than reserve
Set-VMProcessor -VMName 'SQL01' -RelativeWeight 300

6. CPU groups and minroot for real isolation

Per-VM controls shape fairness, but they can't keep a VM off specific LPs or keep the host OS from sharing LPs with guests. Two lesser-known features do: CPU groups and minroot. Both have been available since Windows Server 2016.

CPU groups

A CPU group is a set of VMs that share one CPU cap and, optionally, an LP affinity mask. Groups are managed only through the Host Compute Service (HCS), not through Hyper-V Manager, WMI or the Hyper-V PowerShell module. Microsoft provides cpugroups.exe on the Download Center as the management tool. The group cap is:

G = n × C
  G = LPs' worth of CPU time available to the whole group
  n = number of LPs in the group's affinity set
  C = group cap (cpugroups.exe scale: 0–65536, where 32768 = 50 %)

A group affinitized to 8 LPs with a 50% cap gets 4 LPs of time in total, shared by every VM bound to it. The cap doesn't grow when you add VMs, so per-VM share shrinks as the group fills. That makes groups good for enforcing a service tier ("bronze VMs together never exceed 4 cores") and something to plan carefully for per-VM guarantees.

CpuGroups.exe GetCpuTopology
CpuGroups.exe CreateGroup /GroupId:6f1e4b2c-0000-4000-8000-000000000001 /GroupAffinity:16,17,18,19,20,21,22,23
CpuGroups.exe SetGroupProperty /GroupId:6f1e4b2c-0000-4000-8000-000000000001 /CpuCap:65536
CpuGroups.exe SetVmGroup /VmName:SQL01 /GroupId:6f1e4b2c-0000-4000-8000-000000000001
CpuGroups.exe GetGroupVms

Minroot

By default, every LP has a root VP pinned to it, so host work (VMBus servicing, storage and network stacks, management agents, backup and antivirus) can land on any LP that guests also use. Minroot limits the root partition to a subset of LPs:

bcdedit /set hypervisorrootproc 8      &:: root gets 8 LPs, split evenly across NUMA nodes; reboot required

Combine minroot with affinitized CPU groups on the remaining LPs and you get the closest thing Hyper-V offers to dedicated cores. The host runs on its own LPs, a premium group gets exclusive LPs at a 100% cap, and general-purpose VMs share the rest. Notes from the field:

  • Size the root from measured Hyper-V Hypervisor Root Virtual Processor load, including peaks during backups and live migrations. A starved root partition shows up as poor storage and network latency for every VM.
  • On multi-NUMA hosts, the root LPs are divided across nodes. Make sure each node still has enough free LPs for the VMs placed on it.
  • Minroot is fixed for the life of the boot. Validate it in a non-production environment first.
  • CPU groups and bindings are host configuration. Build the same groups on every node a VM can fail over to, and check the binding after any migration.

7. NUMA, virtual NUMA and spanning

On multi-socket servers, and on single-socket parts split into several NUMA domains (AMD EPYC NPS2/NPS4, Intel sub-NUMA clustering), memory access from a remote node costs noticeably more latency and bandwidth than local access. Hyper-V handles this in three ways.

Virtual NUMA

When a VM's vCPU or memory size exceeds what a host NUMA node can provide, Hyper-V presents a virtual NUMA topology to the guest, so NUMA-aware software (SQL Server, Exchange, JVMs, HPC code) can keep threads near their memory. The topology is calculated from the host when the VM starts and controlled by:

  • Set-VMProcessor -MaximumCountPerNumaNode and -MaximumCountPerNumaSocket
  • Set-VMMemory -MaximumAmountPerNumaNodeBytes

If you live-migrate a VM to a host with a different NUMA layout, it keeps its original virtual topology until it restarts. Standardize host hardware within a cluster, or reset the VM to the hardware topology ("Use Hardware Topology" in Hyper-V Manager) after hardware changes.

Dynamic Memory flattens vNUMA. A VM with Dynamic Memory enabled is presented as a single NUMA node regardless of its vNUMA settings. For large NUMA-aware workloads, use static memory.

NUMA spanning

Host-level NUMA spanning (on by default) lets a VM's memory be allocated from more than one node when its home node runs short. Disabling it (Set-VMHost -NumaSpanningEnabled $false, then restart the Virtual Machine Management service) guarantees node-local memory, but a VM that can't fit in one node will fail to start or restore. Leave it on in general-purpose clusters and consider turning it off only on dedicated, carefully sized hosts.

Sizing rule

Keep each VM's vCPU count at or below the physical cores in one NUMA node whenever you can. When a VM must be wider, size it in whole-node multiples so the virtual topology maps cleanly onto the physical one. A 26-vCPU VM on 24-core nodes gets the cross-node penalty without the benefit of a full second node.

8. Guest enlightenments that reduce scheduler cost

An over-committed host behaves better when guests know they are virtualized. Hyper-V advertises itself through CPUID leaves starting at 0x40000000, and enlightened guests (current Windows, and Linux kernels with the in-tree Hyper-V drivers) use hypercalls defined in Microsoft's Hypervisor Top-Level Functional Specification instead of operations that would cause expensive exits. Three of these matter most for CPU scheduling:

  • Long spin-wait notification. If a guest VP spins on a lock whose holder VP has been descheduled (lock-holder preemption), it wastes its whole time slice. The hypervisor publishes a recommended spin count. After that many spins, an enlightened guest notifies the hypervisor, which can run something useful instead, often the lock holder.
  • Hypercall-based remote TLB flush. Without enlightenment, invalidating mappings across VPs means sending inter-processor interrupts to every VP and waiting, including VPs that aren't running. The flush hypercalls let the hypervisor handle it without waking every VP.
  • Synthetic timers and the reference TSC page. These give cheap, exit-free timekeeping and reduce timer-interrupt overhead on idle VPs.

The practical consequences: keep guest operating systems current, prefer Generation 2 VMs, and treat legacy unenlightened guests as expensive tenants on dense hosts.

9. Measuring CPU contention correctly

In the root partition, Task Manager and \Processor(*)\% Processor Time report root VP activity only. A host can show 5% in Task Manager while its LPs are saturated by guests. Inside a guest, CPU % shows how busy the VPs were while dispatched, not how long they waited. Contention can only be seen from the hypervisor's own counters.

CounterWhat it tells you
\Hyper-V Hypervisor Logical Processor(_Total)\% Total Run TimeTrue physical CPU load: guest plus hypervisor plus root.
\Hyper-V Hypervisor Logical Processor(*)\% Hypervisor Run TimeTime spent in the hypervisor itself. Rises with high exit rates (interrupt storms, unenlightened guests).
\Hyper-V Hypervisor Root Virtual Processor(*)\% Total Run TimeHost OS overhead: I/O path, vSwitch, backup agents. Use it to size minroot.
\Hyper-V Hypervisor Virtual Processor(*)\% Guest Run TimePer-VP consumption. Instance names look like VMName:Hv VP 0.
\Hyper-V Hypervisor Virtual Processor(*)\CPU Wait Time Per DispatchAverage time a runnable VP waited for an LP: the Hyper-V equivalent of VMware's "CPU ready." Monitoring vendors document the raw value in 100-ns units. Whatever the unit, trend it against your own baseline.

Starting thresholds for % Total Run Time: below 70% sustained is healthy, 70–85% deserves attention, and above 85% sustained means latency-sensitive VMs are already queuing. Queueing delay grows non-linearly as utilization approaches 100%, so the jump from 80% to 90% hurts much more than the jump from 50% to 60%.

Diagnosis matrix

Guest CPUWait time / dispatchHost LP total run timeDiagnosisAction
HighLow, stableAnyVM is undersizedAdd vCPUs (in even steps), or optimize the workload
High or moderateHigh, risingHighHost saturatedRebalance or migrate VMs, add hosts. Don't add vCPUs, which makes it worse.
ModerateHighModerateArtificial constraintCheck -Maximum limits, CPU-group caps, NUMA placement, HwThreadCountPerCore = 1 on SMT hosts
LowLowHighOverhead outside the guestsInspect root VP and hypervisor run time: vSwitch and VMQ/RSS configuration, backup jobs, host agents

10. vCPU sizing and consolidation ratios

Hyper-V imposes no vCPU-per-LP ratio. Microsoft's scale-limits documentation says so directly. Ratios like 4:1 or 8:1 are outputs of a measurement, not inputs. Here is a method that holds up.

  1. Measure demand per VM over at least one full business cycle (including month-end, patch nights and backups): the 95th-percentile CPU % inside the guest, or the VM's % Guest Run Time.
  2. Convert to core-equivalents: Dᵢ = vCPUᵢ × p95ᵢ.
  3. Calculate usable host capacity: Cap = physical cores × SMT uplift (≈1.1–1.3) − root reservation.
  4. Plan to a ceiling: Σ Dᵢ ≤ 0.70 × Cap for the N−1 cluster state, not N.
  5. Validate in production with CPU Wait Time Per Dispatch and adjust.

Worked example. A dual-socket host has 2 × 24 cores with SMT (96 LPs). Using an SMT uplift of 1.2 and 4 cores' worth set aside for the root: Cap = 48 × 1.2 − 4 ≈ 53.6 core-equivalents. The 70% planning ceiling is about 37.5. Sixty VMs averaging 4 vCPUs at a p95 of 15% demand 60 × 4 × 0.15 = 36 core-equivalents, so they fit. The resulting ratio is 240 vCPUs on 48 cores, 5:1 vCPU:core (2.5:1 vCPU:LP). That ratio came from the data. A different workload mix on the same hardware could justify 2:1 or 10:1.

Right-sizing rules

  • Extra vCPUs aren't free. Idle VPs still take timer interrupts and TLB-flush work. Wide VMs suffer more from lock-holder preemption. Every VP adds scheduling work.
  • Use even counts on SMT hosts under the core scheduler (section 4).
  • Stay within a NUMA node unless the workload is NUMA-aware and sized in whole nodes (section 7).
  • Add vCPUs only for the right symptom: high guest CPU and low wait time per dispatch.
  • Remember licensing. Per-core licensed software can price vCPUs directly. For example, SQL Server licensed per virtual core requires at least four core licenses per VM. Oversized VMs carry a real licensing cost as well as a performance one.

11. Processor compatibility, nested virtualization and PMU

Processor compatibility mode

Live migration and saved-state restore require the destination CPU to support every instruction-set feature the guest has already detected. Set-VMProcessor -CompatibilityForMigrationEnabled $true (VM powered off) hides newer instruction sets, so a VM can move between CPU generations of the same vendor. Intel-to-AMD moves are never supported. The trade-off is performance: encryption (AES-NI, VAES), compression and vector-heavy workloads lose their newest instructions.

Windows Server 2025 adds dynamic processor compatibility mode for clusters. VMs at configuration version 10.0 or later get the largest feature set common to all cluster nodes, recalculated as nodes change, instead of a fixed lowest-common-denominator mask. To migrate from a newer-CPU cluster to an older one, Microsoft documents setting the VM's CompatibilityForMigrationMode to MinimumFeatureSet. Enable compatibility only when you need it, and turn it off once the hardware is uniform again.

Nested virtualization and performance monitoring

# Both require the VM to be off
Set-VMProcessor -VMName 'LAB-HV01' -ExposeVirtualizationExtensions $true   # nested Hyper-V / WSL2 / VBS in guest
Set-VMProcessor -VMName 'PERF01'   -Perfmon pmu,pebs,lbr                   # expose Intel PMU to guest profilers

Nested hosts add a second scheduling layer: the L1 guest hypervisor schedules L2 VPs onto VPs that the L0 host is itself time-slicing. Keep nested hosts out of contended production clusters.

12. Windows Server 2025 processor scale limits

ItemMaximum
Virtual processors per Generation 2 VM2,048 (configured through PowerShell)
Virtual processors per Generation 1 VM64
Logical processors per host2,048
Virtual processors in use per host2,048
Running VMs per host1,024
Virtual processors per logical processorNo ratio imposed by Hyper-V

13. PowerShell audit toolkit

Run this on a Hyper-V host from an elevated session. It reports the scheduler type, density ratios, the reserve admission sum, sampled contention per VM, and configuration findings (odd vCPU counts, SMT not inherited, limits, VMs wider than a NUMA node, Dynamic Memory on wide VMs).

#Requires -Modules Hyper-V
#Requires -RunAsAdministrator
param([int]$SampleSeconds = 60)

# --- Host topology ---------------------------------------------------------
$lp        = (Get-VMHost).LogicalProcessorCount
$cores     = (Get-CimInstance Win32_Processor | Measure-Object NumberOfCores -Sum).Sum
$numaNodes = @(Get-VMHostNumaNode).Count
$coresNode = [math]::Floor($cores / [math]::Max(1, $numaNodes))
$smtHost   = $lp -gt $cores

# --- Scheduler type (Hypervisor event ID 2) --------------------------------
$map = @{ 1='Classic (SMT off)'; 2='Classic'; 3='Core'; 4='Root' }
$ev  = Get-WinEvent -FilterHashtable @{ LogName='System'; ProviderName='Microsoft-Windows-Hyper-V-Hypervisor'; Id=2 } -MaxEvents 1 -ErrorAction SilentlyContinue
$sched = 'Unknown'
if ($ev -and $ev.Message -match '0x([0-9a-fA-F]+)') { $sched = $map[[Convert]::ToInt32($Matches[1],16)] }

# --- Density and admission -------------------------------------------------
$vms      = Get-VM
$running  = $vms | Where-Object State -eq 'Running'
$vpAll    = ($vms     | Get-VMProcessor | Measure-Object Count -Sum).Sum
$vpRun    = ($running | Get-VMProcessor | Measure-Object Count -Sum).Sum
$resSum   = ($running | Get-VMProcessor | ForEach-Object { $_.Count * $_.Reserve } | Measure-Object -Sum).Sum / $lp

[pscustomobject]@{
  Host                  = $env:COMPUTERNAME
  Scheduler             = $sched
  PhysicalCores         = $cores
  LogicalProcessors     = $lp
  NumaNodes             = $numaNodes
  vCPU_Configured       = $vpAll
  vCPU_Running          = $vpRun
  'vCPU:Core (running)' = '{0:N1}:1' -f ($vpRun / $cores)
  'vCPU:LP (running)'   = '{0:N1}:1' -f ($vpRun / $lp)
  'ReserveAdmission %'  = '{0:N1}' -f $resSum
} | Format-List

# --- Configuration findings ------------------------------------------------
$findings = foreach ($vm in $vms) {
  $p = Get-VMProcessor -VMName $vm.Name
  $m = Get-VMMemory   -VMName $vm.Name
  $issues = @()
  if ($smtHost -and $sched -eq 'Core' -and ($p.Count % 2) -and $p.HwThreadCountPerCore -ne 1) { $issues += 'Odd vCPU count with guest SMT' }
  if ($smtHost -and $p.HwThreadCountPerCore -eq 1) { $issues += 'Guest SMT disabled (HwThreadCountPerCore=1)' }
  if ($p.Maximum -lt 100)    { $issues += "CPU limit $($p.Maximum)%" }
  if ($p.Reserve -gt 0)      { $issues += "CPU reserve $($p.Reserve)%" }
  if ($p.Count -gt $coresNode) {
    $issues += 'Wider than one NUMA node'
    if ($m.DynamicMemoryEnabled) { $issues += 'Dynamic Memory flattens vNUMA' }
  }
  if ($p.CompatibilityForMigrationEnabled) { $issues += 'Processor compatibility on' }
  if ($issues) { [pscustomobject]@{ VM = $vm.Name; vCPU = $p.Count; Findings = $issues -join '; ' } }
}
$findings | Format-Table -AutoSize -Wrap

# --- Sampled contention ----------------------------------------------------
$n = [math]::Max(1, [int]($SampleSeconds / 5))
$counters = @(
  '\Hyper-V Hypervisor Logical Processor(_Total)\% Total Run Time',
  '\Hyper-V Hypervisor Root Virtual Processor(_Total)\% Total Run Time',
  '\Hyper-V Hypervisor Virtual Processor(*)\% Guest Run Time',
  '\Hyper-V Hypervisor Virtual Processor(*)\CPU Wait Time Per Dispatch'
)
$s = (Get-Counter -Counter $counters -SampleInterval 5 -MaxSamples $n).CounterSamples

'Host LP % Total Run Time (avg): {0:N1}' -f ($s | Where-Object Path -like '*logical processor(_total)\% total run time' | Measure-Object CookedValue -Average).Average
'Root VP % Total Run Time (avg): {0:N1}' -f ($s | Where-Object Path -like '*root virtual processor(_total)\% total run time' | Measure-Object CookedValue -Average).Average

$s | Where-Object { $_.Path -like '*\hyper-v hypervisor virtual processor(*' -and $_.InstanceName -ne '_total' } |
  ForEach-Object {
    [pscustomobject]@{
      VM     = ($_.InstanceName -split ':')[0]
      Metric = if ($_.Path -like '*guest run time') { 'Run' } else { 'Wait' }
      Value  = $_.CookedValue
    }
  } | Group-Object VM | ForEach-Object {
    $g = $_.Group
    [pscustomobject]@{
      VM                 = $_.Name
      AvgVpGuestRunPct   = [math]::Round(($g | Where-Object Metric -eq 'Run'  | Measure-Object Value -Average).Average, 1)
      AvgWaitPerDispatch = [math]::Round(($g | Where-Object Metric -eq 'Wait' | Measure-Object Value -Average).Average, 0)
    }
  } | Sort-Object AvgWaitPerDispatch -Descending | Format-Table -AutoSize

Run it at the same time every week (or as a scheduled RMM script) and keep the output. Trends in wait time per dispatch tell you about upcoming capacity problems before users do.

14. Frequently asked questions

Does Hyper-V dedicate a physical core to each virtual CPU?

No. A virtual processor is a schedulable hypervisor object, not a reserved core. The closest you can get to dedication is a 100% reserve, or better, an affinitized CPU group combined with minroot so those LPs aren't shared with the root or with other VMs.

Which Hyper-V scheduler is the default?

Classic on Windows Server 2016, core on Windows Server 2019 and later, and root on Windows 10 1803+ and Windows 11 client Hyper-V. Check event ID 2 from Microsoft-Windows-Hyper-V-Hypervisor: 1 = classic with SMT off, 2 = classic, 3 = core, 4 = root.

What is a safe vCPU to physical core ratio for Hyper-V?

There isn't a universal one, and Hyper-V doesn't enforce any. Sum each VM's vCPUs × p95 utilization, compare that with physical cores plus a modest SMT uplift, plan to about 70% in the N−1 cluster state, and validate with CPU Wait Time Per Dispatch.

Should VMs have an even number of vCPUs on a core-scheduler host?

Yes, when the guest sees SMT. The core scheduler dispatches sibling VPs together on one core, so an odd count leaves one virtual core with a single thread, and the physical sibling idles while it runs.

How do I measure CPU contention on a Hyper-V host?

Ignore root-partition Task Manager. Use the Hyper-V Hypervisor Logical Processor, Root Virtual Processor and Virtual Processor counter sets, especially % Total Run Time and CPU Wait Time Per Dispatch.

Does Dynamic Memory affect virtual NUMA?

Yes. A VM with Dynamic Memory enabled is presented as a single NUMA node. Use static memory for large NUMA-aware workloads such as SQL Server.

References

MGB Systems designs, builds, and manages Hyper-V and Windows Server infrastructure. Need help right-sizing a Hyper-V cluster or tracking down CPU contention? Call 314-833-6500 or email sales@mgbsys.com.

Validate every configuration change in a test environment before you apply it in production. Commands were written for Windows Server 2016–2025 with the Hyper-V PowerShell module.