diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml index 9f19e4f3..11fd5805 100644 --- a/.github/workflows/copilot-setup-steps.yml +++ b/.github/workflows/copilot-setup-steps.yml @@ -350,6 +350,18 @@ jobs: if [[ -r /dev/kvm && -w /dev/kvm ]]; then kvm=yes fi + # The CPU as NVX names it (vendor family/model/stepping), which + # decides the CPU profile that a cold boot's --cpu-profile auto + # selects (doc/usage.md, "CPU profiles"). + cpu=$(awk -F': *' ' + /^vendor_id/ { vendor = $2 } + /^cpu family/ { family = $2 } + /^model[ \t]*:/ { model = $2 } + /^stepping/ { stepping = $2 } + /^model name/ { name = $2 } + /^$/ { exit } + END { printf "%s %s/%s/%s (%s)", vendor, family, model, stepping, name } + ' /proc/cpuinfo) { echo "### NVX Copilot environment" echo @@ -357,6 +369,7 @@ jobs: echo "| --- | --- |" echo "| Setup status | ${status} |" echo "| Failed steps | ${failed_list:-none} |" + echo "| CPU | ${cpu} |" echo "| CPUs / memory | $(nproc) / $(free -g | awk '/^Mem:/ { print $2 }') GiB |" echo "| Workspace free space | $(df -h --output=avail . | tail -n 1 | tr -d ' ') |" echo "| KVM access | ${kvm} |" diff --git a/doc/benchmarks.md b/doc/benchmarks.md index e167c134..f912150a 100644 --- a/doc/benchmarks.md +++ b/doc/benchmarks.md @@ -44,6 +44,17 @@ coordinator fills the `.sh.in` templates before use. | `linux-mshv-virtual-machine` | MSHV | Virtual machine | | `windows-whp-virtual-machine` | WHP | Virtual machine | +The series names carry no CPU vendor: every CI microVM runner is an Azure virtual machine with an +Intel Xeon CPU (Ice Lake-SP or Emerald Rapids), so each series has one vendor's samples. No CI +microVM runner has an AMD CPU, so no AMD series exists yet. An AMD runner needs a built-in AMD +[CPU profile](usage.md#cpu-profiles), because CI never uses host profiles, and only Milan CPUs have +one so far, `amd.milan.v1`. Such a runner boots its guests on that profile through AMD-V, so its +timings would skew an Intel series' baseline. Before an AMD runner joins CI, give it series of its +own, named with an `-amd` suffix such as `linux-kvm-virtual-machine-amd`, in `PLATFORM_NAMES` and +`OPENVMM_BACKENDS` in `scripts/nvx_tools/performance.py` and in the CI matrix, so their history +lives in separate `data/` files. Local measurements on AMD hosts can use the names above, because +only CI records history. + CI runs the complete acceptance and performance suites and the five device metrics at one vCPU under the canonical microVM, and only the 512 MiB shell snapshot restore at `2`, `4`, and `8` vCPUs. This produces 37 p50 values per series, 34 at one vCPU and one at each higher diff --git a/doc/ci.md b/doc/ci.md index e68ddbc7..bffd376f 100644 --- a/doc/ci.md +++ b/doc/ci.md @@ -337,7 +337,7 @@ microcode and the invariant-TSC flags, may read `unknown`. | Check | Implementation | | --- | --- | | H1 | `/dev/kvm` or `/dev/mshv` is readable and writable, and a KVM host has no `/dev/mshv`; on Windows, `WHvGetCapability` reports a hypervisor | -| H2 | Vendor, family, model, stepping, microcode, and OS build from `/proc/cpuinfo` or the Windows registry. Then `openvmm --hypervisor --cpu-fingerprint ` writes the host's CPU fingerprint (by default `nvx-cpu-fingerprint-.json` in the probe directory; `--cpu-fingerprint` overrides it), checks it against the profile that `auto` selects from OpenVMM's catalog, which shares one profile per generation across backends, and prints one `NVX-CPU-PROFILE:` line. H2 reports the generation and the profile from that line, so a profile that an OpenVMM pin adds qualifies its hosts without an NVX change. H2 requires exit status 0, `status=pass`, the same backend, and a catalog profile of the reported generation, reports the profile and surface digests, and otherwise fails with OpenVMM's code, for example `[E_PROFILE_HOST_UNKNOWN]` on a CPU that no profile serves or `[E_PROFILE_UNSUPPORTED]` naming every unsupported CPUID bit. On WHP, OpenVMM checks the entries outside the profile on a probe partition configured from the profile, as a cold boot does. With `--no-openvmm`, H2 maps the host with NVX's copy of the catalog instead: `skylake-sp` (6/85, steppings 0 to 4, `intel.skylake-sp.v1`), `icelake-sp` (6/106, `intel.icelake-sp.v1`), `emeraldrapids` (6/207, `intel.emeraldrapids.v1`), or `alderlake` (6/151 and 6/154, `intel.alderlake.v1`). Any other CPU, including Cascade Lake and Cooper Lake, fails with `E_PROFILE_HOST_UNKNOWN`. A unit test keeps the copy equal to the profiles of the OpenVMM submodule's pinned revision, which it reads from the gitlink's commit, so the CI jobs validate the NVX CLI after they check out OpenVMM. The host OS's invariant-TSC flags (`constant_tsc nonstop_tsc`, or the CPUID bit on Windows) are recorded as evidence and never fail the check, because they don't decide what a guest observes: Azure WHP hosts show the CPUID bit but cannot offer invariant TSC to partitions, and their guests measure tens of nanoseconds of skew | +| H2 | Vendor, family, model, stepping, microcode, and OS build from `/proc/cpuinfo` or the Windows registry. Then `openvmm --hypervisor --cpu-fingerprint ` writes the host's CPU fingerprint (by default `nvx-cpu-fingerprint-.json` in the probe directory; `--cpu-fingerprint` overrides it), checks it against the profile that `auto` selects from OpenVMM's catalog, which shares one profile per generation across backends, and prints one `NVX-CPU-PROFILE:` line. H2 reports the generation and the profile from that line, so a profile that an OpenVMM pin adds qualifies its hosts without an NVX change. H2 requires exit status 0, `status=pass`, the same backend, and a catalog profile of the reported generation, reports the profile and surface digests, and otherwise fails with OpenVMM's code, for example `[E_PROFILE_HOST_UNKNOWN]` on a CPU that no profile serves or `[E_PROFILE_UNSUPPORTED]` naming every unsupported CPUID bit. On WHP, OpenVMM checks the entries outside the profile on a probe partition configured from the profile, as a cold boot does. With `--no-openvmm`, H2 maps the host with NVX's copy of the catalog instead: `skylake-sp` (6/85, steppings 0 to 4, `intel.skylake-sp.v1`), `icelake-sp` (6/106, `intel.icelake-sp.v1`), `emeraldrapids` (6/207, `intel.emeraldrapids.v1`), `alderlake` (6/151 and 6/154, `intel.alderlake.v1`), or `milan` (AMD 25/1, `amd.milan.v1`). Any other CPU, including Cascade Lake and Cooper Lake, fails with `E_PROFILE_HOST_UNKNOWN`. A unit test keeps the copy equal to the profiles of the OpenVMM submodule's pinned revision, which it reads from the gitlink's commit, so the CI jobs validate the NVX CLI after they check out OpenVMM. The host OS's invariant-TSC flags (`constant_tsc nonstop_tsc`, or the CPUID bit on Windows) are recorded as evidence and never fail the check, because they don't decide what a guest observes: Azure WHP hosts show the CPUID bit but cannot offer invariant TSC to partitions, and their guests measure tens of nanoseconds of skew | | H3 | `openvmm --x-time-abi-verify` builds the partition and runs the time ABI preflight without running the guest. Its `NVX-TIME-ABI-VERIFY:` line must report `status=ok` for the backend, plausible declared and native TSC rates, the backend's LAPIC rate, and a `cpu_profile` that is a catalog profile, `..v` of any generation but a host profile's `host`, and, when H2 runs too, a revision of the profile H2 names. A failed preflight reports OpenVMM's code, for example `[E_TSC_SYNC_UNSUPPORTED]` | | H4 | Samples of the TSC against the host's monotonic clocks, with sleeps between them so the host's CPUs idle: 13 samples 10 s apart, or 3 samples 1 s apart with `--ci-schedule`. Each sample reads the TSC between two reads of a clock, keeping the tightest of 64 brackets; its uncertainty is half the bracket plus half the clock's resolution. A clock that returns the same value to consecutive reads is coarser than one read, so the probe takes its smallest step as its resolution: Hyper-V's reference TSC page advances the Linux clocks in 100 ns steps although `clock_getres` reports 1 ns. The interval stability is judged against a clock that time synchronization never steers, `CLOCK_MONOTONIC_RAW` on Linux and `QueryPerformanceCounter` on Windows: every interval between consecutive samples must be conclusive within 0.25 ppm, and the interval rates must agree within 1 ppm. chrony's frequency updates move `CLOCK_MONOTONIC`'s rate by up to several ppm between seconds on the Azure runners, which says nothing about the TSC. The rate over the whole window is measured against the disciplined clock, `CLOCK_MONOTONIC` on Linux, and must lie within 100 ppm of the rate H3 reports when H3 runs in the same invocation. A Linux host's clocksource is recorded as evidence | | H5 | Pinned-thread ping-pong rounds over every pair of host CPUs; `max_abs_offset_ns` is at most 1,000, the measurement is conclusive, and no pair stalls | diff --git a/doc/design/time-abi.md b/doc/design/time-abi.md index 6be9b108..e66f69f4 100644 --- a/doc/design/time-abi.md +++ b/doc/design/time-abi.md @@ -53,7 +53,8 @@ Non-goals: - TSC scaling and RDTSC trapping. - Paravirtual clocks: kvmclock, the Hyper-V reference TSC page and reference counter, synthetic timers, VMGenID, and VMClock. -- A virtual PMU. AMD profiles are deferred until an AMD host is registered. +- A virtual PMU. +- CPUs of vendors other than Intel and AMD, such as Hygon's. - Live migration, standard-machine guests, and guests other than the NVX Linux 6.18 kernel. - Snapshots older than manifest version 6. They are rejected with a recapture @@ -146,6 +147,27 @@ stubs. Linux 6.18 never accesses them. A fault on an MSR that the guest kernel accesses unchecked logs `unchecked MSR access error`, which the boot check (`C4`) and the violation watcher (`G_UNCHECKED_MSR`) reject. +Under an AMD CPU profile, OpenVMM also serves two of AMD's configuration +MSRs on every backend, with fixed values +(`virt::time_abi::msr::AMD_CONFIG_MSRS`): + +| MSR | Name | Read | Write | +| --- | --- | --- | --- | +| `0xc0010015` | `HWCR` | `0x1000000`: only `TscFreqSel` (bit 24) | The value read changes nothing; any other value #GP | +| `0xc0011029` | `DE_CFG` | `0x2`: only `LFENCE` serialization (bit 1) | The value read changes nothing; any other value #GP | + +Linux reads `HWCR` with an unchecked `rdmsr` on every AMD CPU with an +invariant TSC, which the time ABI's CPUID always presents, and logs +`[Firmware Bug]: TSC doesn't count with P0 frequency!` unless `TscFreqSel` +is set. It sets `DE_CFG`'s `LFENCE` serialization bit, with safe accessors, +on every CPU whose CPUID does not enumerate it, and relies on a serializing +`LFENCE` to order `RDTSC`. It writes neither MSR on a profile's CPU: only +the instructions-retired counter and CPUID faulting would make it, and the +profiles clear both. KVM's MSR filter denies both MSRs, MSHV intercepts +them, and WHP delivers them as unhandled-MSR exits. Booting Linux 6.18 on 4 +vCPUs on WHP reads `HWCR` once, on the BSP, and `DE_CFG` once per vCPU. +Under an Intel profile, both keep the backend's handling. + ### CPU time bits Every CPU profile fixes these bits. A profile that violates them is rejected. @@ -275,9 +297,9 @@ settled cell on the registered hosts. | Obligation | KVM | MSHV | WHP | | --- | --- | --- | --- | -| Identity CPUID (exact leaves, out-of-range rule) | `KVM_SET_CPUID2` with the identity and explicit zero leaves; every KVM `0x4xxxxxxx` entry removed. Other leaves in the range return KVM's Intel out-of-range result | No synthetic processor features, so the hypervisor reports no `0x400000xx` leaves; CPUID intercept results (`always_override`) for the identity leaves, while the explicit zero leaves already read zero in VP 0's view and get none (see the next row); preflight reads every entry back and requires VP 0 to read zero at six sentinel leaves (`0x40000006`, `0x40000081`, `0x400000ff`, `0x40000100`, `0x40000200`, and `0x4000ff00`) | CPUID exits for the six identity leaves, served from OpenVMM's table; with synthetic features off, WHP returns zero natively for `0x40000006..=0x400000ff` and the bases from `0x40000100`, which preflight asserts | -| Profile CPUID and time bits | `KVM_SET_CPUID2` with the effective CPUID. KVM 6.3 and newer hide invariant TSC while KVM's own `HV_X64_MSR_TSC_INVARIANT_CONTROL` is 0, so the backend mirrors the guest-visible value, which core saves with the VM, into it with a host-initiated `KVM_SET_MSRS` after every accepted guest write and before a VP runs after a restore or reset; without it the guest loses `constant_tsc` and `nonstop_tsc` and boots about 160 ms slower | Processor feature banks derived from the profile (`hv_banks`), plus a CPUID intercept result (`always_override`, subleaf-specific where the entry has a subleaf) for every partition-wide entry of the effective CPUID that VP 0's own view does not already present under its mask. Once VP 0 exists, one bulk `HvCallGetVpCpuidValues` reads every partition-wide entry before any result is registered, and a failed bulk read registers every entry. That leaves 11 of 61 entries on the bare-metal host (the hypervisor bit, one leaf-2 descriptor byte, ARAT, the six identity leaves, and two brand leaves) and 12 of 65 on the nested Azure host (the same plus invariant TSC), at every vCPU count, because VP 0's own view already presents leaf 4's shared L3; registering them, the read included, takes 63 to 71 µs and about 90 µs, against 200 and about 300 µs for every entry. Profile masks never cover runtime-owned or VM-owned bits, so a pinned value that VP 0 presents at reset it presents always. Reserved entries pass through from the hypervisor's guest view, which reads zero at every one with no result registered, and verification requires every candidate to read zero (`E_CPU_UNLISTED`). Per-VP fields cannot be left to the hypervisor: it does not implement `0xB` for these partitions and reads `EDX` as 0 on every VP, so Linux would log `APIC ID mismatch` (`G_APIC_ID_MISMATCH`). The `0xB` and `0x1F` entries are therefore per-VP intercept results with the VP's x2APIC ID in `EDX`, registered as each VP is created: the hypervisor refuses a per-VP result for a VP that does not exist yet (`InvalidVpIndex`). Leaf 1's initial APIC ID is the hypervisor's own, which is correct | Processor feature banks derived from the profile (`hv_banks`), and every effective-CPUID entry outside the hypervisor range as a `CpuidResultList2` result (the banks cannot express the hypervisor bit, ARAT, or, on Azure, invariant TSC); reserved entries pass through from WHP's guest view, which verification requires to be zero (`E_CPU_UNLISTED`). CPUID exits only for the identity leaves and the leaves with per-VP APIC fields (`1`, `0xB`, and, where listed, `0x1F` and `0x8000001E`), 8 exits for every v1 profile: each exit-list entry adds about 25 µs to partition setup, and the legacy 269-entry list cost about 7 ms per restore. The exit handler presents the effective CPUID's `0xB` terminator, with the VP's x2APIC ID, instead of zeroing subleaves from 2 | -| Identity MSRs routed to OpenVMM | `KVM_CAP_X86_USER_SPACE_MSR` (`UNKNOWN`, `FILTER`) and `KVM_X86_SET_MSR_FILTER` denying reads and writes of `0x40000000..=0x400001ff`; the filter takes precedence over KVM's in-kernel Hyper-V MSRs. A Linux boot takes four MSR exits, all on the BSP, at any vCPU count | MSR-index intercepts (`HV_INTERCEPT_TYPE_X64_MSR_INDEX`, `READ_WRITE`) for `0x40000002`, `0x40000022`, `0x40000023`, and `0x40000118`. The hypervisor itself raises #GP for writes to the three read-only MSRs and for every other MSR in the range. Native synthetic MSRs are forbidden: they pre-empt the intercepts, and the native `HV_X64_MSR_TSC_FREQUENCY` returns the destination's rate instead of `F`. Verified on hypervisor builds 26100.30000 (bare metal) and 26100.9444 (Azure) | `X64MsrExitBitmap` with `UnhandledMsrs` (capability `0x3f` on every host) and the offloaded APIC, no synthetic features and no `hv1_emulator`: the identity MSRs exit to OpenVMM, which raises #GP for every other MSR in the range | +| Identity CPUID (exact leaves, out-of-range rule) | `KVM_SET_CPUID2` with the identity and explicit zero leaves; every KVM `0x4xxxxxxx` entry removed. Other leaves in the range return KVM's out-of-range result: Intel's under an Intel profile, and zero under an AMD one, as KVM answers a guest whose CPUID vendor is AMD | No synthetic processor features, so the hypervisor reports no `0x400000xx` leaves; CPUID intercept results (`always_override`) for the identity leaves, while the explicit zero leaves already read zero in VP 0's view and get none (see the next row); preflight reads every entry back and requires VP 0 to read zero at six sentinel leaves (`0x40000006`, `0x40000081`, `0x400000ff`, `0x40000100`, `0x40000200`, and `0x4000ff00`) | CPUID exits for the six identity leaves, served from OpenVMM's table; with synthetic features off, WHP returns zero natively for `0x40000006..=0x400000ff` and the bases from `0x40000100`, which preflight asserts | +| Profile CPUID and time bits | `KVM_SET_CPUID2` with the effective CPUID. KVM 6.3 and newer hide invariant TSC while KVM's own `HV_X64_MSR_TSC_INVARIANT_CONTROL` is 0, so the backend mirrors the guest-visible value, which core saves with the VM, into it with a host-initiated `KVM_SET_MSRS` after every accepted guest write and before a VP runs after a restore or reset; without it the guest loses `constant_tsc` and `nonstop_tsc` and boots about 160 ms slower | Processor feature banks derived from the profile (`hv_banks`), plus a CPUID intercept result (`always_override`, subleaf-specific where the entry has a subleaf) for every partition-wide entry of the effective CPUID that VP 0's own view does not already present under its mask. Once VP 0 exists, one bulk `HvCallGetVpCpuidValues` reads every partition-wide entry before any result is registered, and a failed bulk read registers every entry. That leaves 11 of 61 entries on the bare-metal host (the hypervisor bit, one leaf-2 descriptor byte, ARAT, the six identity leaves, and two brand leaves) and 12 of 65 on the nested Azure host (the same plus invariant TSC), at every vCPU count, because VP 0's own view already presents leaf 4's shared L3; registering them, the read included, takes 63 to 71 µs and about 90 µs, against 200 and about 300 µs for every entry. Profile masks never cover runtime-owned or VM-owned bits, so a pinned value that VP 0 presents at reset it presents always. Reserved entries pass through from the hypervisor's guest view, which reads zero at every one with no result registered, and verification requires every candidate to read zero (`E_CPU_UNLISTED`). Per-VP fields cannot be left to the hypervisor: it does not implement `0xB` for these partitions and reads `EDX` as 0 on every VP, so Linux would log `APIC ID mismatch` (`G_APIC_ID_MISMATCH`). The `0xB` and `0x1F` entries are therefore per-VP intercept results with the VP's x2APIC ID in `EDX`, registered as each VP is created: the hypervisor refuses a per-VP result for a VP that does not exist yet (`InvalidVpIndex`). Under an AMD profile, so is `0x8000001E`, with the VP's extended APIC, compute unit, and node IDs (no AMD MSHV host has run it yet). Leaf 1's initial APIC ID is the hypervisor's own, which is correct | Processor feature banks derived from the profile (`hv_banks`), and every effective-CPUID entry outside the hypervisor range as a `CpuidResultList2` result (the banks cannot express the hypervisor bit, ARAT, or, on Azure, invariant TSC); reserved entries pass through from WHP's guest view, which verification requires to be zero (`E_CPU_UNLISTED`). CPUID exits only for the identity leaves and the leaves with per-VP APIC fields (`1`, `0xB`, and, where listed, `0x1F` and `0x8000001E`), 8 exits for every Intel v1 profile and 9 for `amd.milan.v1`: each exit-list entry adds about 25 µs to partition setup, and the legacy 269-entry list cost about 7 ms per restore. The exit handler presents the effective CPUID's `0xB` terminator, with the VP's x2APIC ID, instead of zeroing subleaves from 2 | +| Identity MSRs routed to OpenVMM | `KVM_CAP_X86_USER_SPACE_MSR` (`UNKNOWN`, `FILTER`) and `KVM_X86_SET_MSR_FILTER` denying reads and writes of `0x40000000..=0x400001ff`, and under an AMD profile of `HWCR` and `DE_CFG` (see [MSRs](#msrs)); the filter takes precedence over KVM's in-kernel Hyper-V MSRs. Under an Intel profile, a Linux boot takes four MSR exits, all on the BSP, at any vCPU count; under an AMD profile, it also reads `HWCR` once, on the BSP, and `DE_CFG` once on each vCPU, so it takes 4 + 1 + N exits on N vCPUs (counted on WHP; no AMD KVM host has run it) | MSR-index intercepts (`HV_INTERCEPT_TYPE_X64_MSR_INDEX`, `READ_WRITE`) for `0x40000002`, `0x40000022`, `0x40000023`, and `0x40000118`, and under an AMD profile for `HWCR` and `DE_CFG`. The hypervisor itself raises #GP for writes to the three read-only MSRs and for every other MSR in the range. Native synthetic MSRs are forbidden: they pre-empt the intercepts, and the native `HV_X64_MSR_TSC_FREQUENCY` returns the destination's rate instead of `F`. Verified on hypervisor builds 26100.30000 (bare metal) and 26100.9444 (Azure) | `X64MsrExitBitmap` with `UnhandledMsrs` (capability `0x3f` on every host) and the offloaded APIC, no synthetic features and no `hv1_emulator`: the identity MSRs exit to OpenVMM, which raises #GP for every other MSR in the range; `HWCR` and `DE_CFG` exit too, and OpenVMM serves them under an AMD profile | | Native rate `F_d` | `KVM_GET_TSC_KHZ` × 1000 on VP 0 (1 kHz granularity) | `ProcessorClockFrequency` partition property | `WHvCapabilityCodeProcessorClockFrequency` | | No TSC scaling | Never `KVM_SET_TSC_KHZ`; every vCPU reports the host rate | No frequency override | No `ProcessorClockFrequency` partition property; the 1 GHz request is removed | | LAPIC rate `L` | In-kernel LAPIC at 1 GHz; `KVM_CAP_X86_APIC_BUS_CYCLES_NS` never set | The hypervisor's LAPIC at 200 MHz | Offloaded APIC at its fixed 200 MHz, verified at preflight (setting `InterruptClockFrequency` is not supported); the emulated APIC is not used | @@ -329,6 +351,9 @@ immutable once released. A normative change creates a new revision; released revisions are never deleted, so old snapshots stay restorable. One surface serves all three backends: MSHV and WHP derive their processor feature banks from the profile's CPUID (`cpu_profile::hv_banks`) instead of from the host's, +reading each feature where the profile's vendor enumerates it (AMD CPUs +enumerate the speculation controls in `0x80000008` EBX and SVM in +`0x80000001` ECX, and Intel's `PREFETCHW` feature controls no AMD bit), and each backend verifies at partition creation that it supports the profile. Every backend programs the whole surface explicitly and never relies on hypervisor defaults; WHP's default processor features, for example, omit @@ -361,7 +386,7 @@ document in pretty form. | `id` | `..v`, each component `[a-z0-9-]+`, for example `intel.icelake-sp.v1` | | `description` | Free text | | `vendor` | The 12-byte CPUID vendor string | -| `generation` | The generation `name` (`skylake-sp`, `icelake-sp`, `emeraldrapids`, or `alderlake`; `host` for a host profile, see **Host profiles**) and its `cpus`: `(family, model, stepping range)` display signatures; stepping ranges separate model 85's Skylake-SP (0 to 4), Cascade Lake, and Cooper Lake | +| `generation` | The generation `name` (`skylake-sp`, `icelake-sp`, `emeraldrapids`, `alderlake`, or `milan`; `host` for a host profile, see **Host profiles**) and its `cpus`: `(family, model, stepping range)` display signatures; stepping ranges separate model 85's Skylake-SP (0 to 4), Cascade Lake, and Cooper Lake | | `cpuid` | A dense table of every leaf and subleaf in `[0, max basic]` and `[0x80000000, max extended]` with a value and a mask per register; mask bit 1 pins the value, mask bit 0 marks a VMM-owned or runtime-owned bit | | `xcr0`, `xss`, `xsave_components` | The XSAVE features the guest may enable, and the size, offset, and flags of every enabled component | | `physical_address_width` | The guest physical address width | @@ -376,7 +401,15 @@ core per vCPU ([configuration boundary](configuration-boundary.md)), so in `CPUID.4` every cache reports the vCPU count less one as the cores per socket (`EAX[31:26]`), and the L3 cache reports it as the logical processors sharing the cache (`EAX[25:14]`) too; the L1 and L2 caches report 0 there, -one vCPU each. Runtime-owned bits mirror control state: `OSXSAVE`, `OSPKE`, +one vCPU each. AMD CPUs have no leaf 4, so the table differs by vendor: in +AMD profiles, OpenVMM owns instead the core count and APIC ID size of +`CPUID.80000008:ECX` (bits 7:0 and 15:12), the cache-sharing counts of +`CPUID.8000001D:EAX[25:14]` (the L3 cache shared by every vCPU, the L1 and +L2 caches by one), and the processor topology of `CPUID.8000001E` (each +vCPU's extended APIC, compute unit, and node IDs, the threads per compute +unit, and the nodes per processor). Intel profiles pin those AMD fields as +before, so adding the AMD rows changed no Intel profile's encoding or +digest. Runtime-owned bits mirror control state: `OSXSAVE`, `OSPKE`, and the XSAVE sizes for the current XCR0 and XSS. The time ABI owns `0x40000000..=0x4fffffff`, `0x15`, and `0x16`, and every profile pins the [CPU time bits](#cpu-time-bits). Invariant TSC and ARAT are @@ -386,7 +419,12 @@ banks cannot express ARAT, and on Azure, WHP and nested MSHV cannot offer invariant TSC to guests through their feature banks although the host OS sees it. Host qualification measures them instead. Profiles also pin policy zeros (VMX and SVM, SGX, PT, RDT, PCONFIG, and the other features listed by the -profiles' derivation policy). The effective guest CPUID is a pure function of +profiles' derivation policy; in AMD profiles also IBS, LWP, SKINIT, the +performance counter extensions, MONITORX, RDPRU, PPIN, CPPC, INVLPGB, the +instructions-retired counter, SME and SEV, whose leaf `0x8000001F` is +zero, `BTC_NO`, which KVM never enumerates, and PSFD unless a `SPEC_CTRL` +control stays, without which KVM denies a guest `SPEC_CTRL`). The +effective guest CPUID is a pure function of the profile, the VM topology, and the time ABI's identity leaves. **Effective CPUID.** The effective CPUID @@ -439,8 +477,10 @@ on them. They are: - a `0xD` subleaf of a component that is not enabled. KVM answers them from the effective CPUID: zero, Intel's out-of-range result -(the highest basic leaf's) above the maximum basic leaf, or, past the last -topology level, an invalid level with the x2APIC ID. MSHV and WHP pass them +(the highest basic leaf's) above the maximum basic leaf under an Intel +profile, or, past the last topology level, an invalid level with the x2APIC +ID. Under an AMD profile, KVM answers every out-of-range leaf with zero, as +it answers any guest whose CPUID vendor is AMD. MSHV and WHP pass them through from their own guest views. Both read zero at every reserved entry their sweeps reach (below), so neither programs zero results there, which would cost about 5 µs per entry at partition setup: about 35 µs on WHP for @@ -471,14 +511,19 @@ Informational fields: - The brand string (`0x80000002..=0x80000004`) is generic per generation: `Intel(R) Xeon(R) Processor ()` with the generation's display name - (`Skylake-SP`, `Ice Lake-SP`, or `Emerald Rapids`), or - `Intel(R) Core(TM) Processor (Alder Lake)`, zero-padded, without a + (`Skylake-SP`, `Ice Lake-SP`, or `Emerald Rapids`), + `Intel(R) Core(TM) Processor (Alder Lake)`, or + `AMD EPYC Processor (Milan)`, zero-padded, without a frequency. Every host of a generation presents it whatever its SKU, and derivation needs no common brand across the source hosts. Host profiles - present `Intel(R) Processor (host profile)`. -- The cache leaves (`0x2` and `0x4`, except their VMM-owned counts) come from - the reference hosts; other SKUs of the generation differ, and verification - does not compare them. Address widths are checked as limits. + present `Intel(R) Processor (host profile)` or + `AMD Processor (host profile)`. +- The cache leaves (`0x2` and `0x4`, and AMD's `0x80000005`, `0x80000006`, + and `0x8000001D`, except their VMM-owned counts) come from the reference + hosts; other SKUs of the generation differ, and verification does not + compare them. Neither does it compare the copy of the CPU's signature in + AMD's `0x80000001` EAX, which describes the CPU as leaf 1 does. Address + widths are checked as limits. - `IA32_UCODE_REV` is not pinned: Linux disables its microcode loader and skips its microcode checks under a hypervisor, so the value only reaches `/proc/cpuinfo`, and it is outside the effective-CPUID record and every @@ -502,11 +547,14 @@ highest revision of the single pinned profile whose generation covers the host's vendor, family, model, and stepping as the VMM's host OS sees them (the L1 view on Azure). No match, or matches in more than one generation, is `E_PROFILE_HOST_UNKNOWN`, naming the host's signature and the available IDs, -and, on an Intel CPU, pointing to `--cpu-profile host`; `auto` never falls -back to a host profile, and host CPUID passthrough does not exist. A restore -always uses the profile recorded in the snapshot; an explicit `--cpu-profile` -must name the same profile, or be `host` for a host profile -(`E_PROFILE_UNKNOWN`). +and, on an Intel or AMD CPU, pointing to `--cpu-profile host`; `auto` never +falls back to a host profile, and host CPUID passthrough does not exist. A +cold boot with `auto` whose profile the backend does not support fails with +`E_PROFILE_UNSUPPORTED` (verification step 3), which on an Intel or AMD CPU +points to `--cpu-profile host` too, because a host profile derives from what +the backend supports. A restore always uses the profile recorded in the +snapshot; an explicit `--cpu-profile` must name the same profile, or be +`host` for a host profile (`E_PROFILE_UNKNOWN`). **Host profiles.** `--cpu-profile host` is an opt-in for development hosts that no pinned profile serves. Before it creates @@ -523,8 +571,9 @@ partition's configuration (`cpu_profile::PartitionProfile`), so each VM of a process holds its own host profile. A host profile is neither reviewed nor immutable: a microcode, firmware, or hypervisor update can change it. Its fingerprint costs a probe partition, 2.6 to 5.5 ms on WHP, and the host -identity at every cold boot. Only Intel CPUs have host profiles; another -vendor fails with `E_PROFILE_HOST_UNKNOWN`, and a backend that lacks a CPU +identity at every cold boot. Only Intel and AMD CPUs have host profiles, +`intel.host.v1` and `amd.host.v1`; another vendor, such as Hygon, fails +with `E_PROFILE_HOST_UNKNOWN`, and a backend that lacks a CPU feature that the time ABI requires fails with `E_PROFILE_UNSUPPORTED`. Host qualification never accepts a host profile, so CI and benchmark hosts need a pinned one. @@ -580,7 +629,8 @@ reports every violation at once, naming the leaf, subleaf, register, and bit: (`0xF.1`, `0x10.1` to `0x10.3`, `0x12.1`, `0x12.2`, and `0x14.1`), 6 on the Azure 8370C and 8573C runners, whose roots report no Intel PT, and 12 on the Core i9-12900H, among them `0xD.11`, `0xD.12`, `0x14.1`, - and `0x20`, which its root reads as non-zero. + and `0x20`, which its root reads as non-zero, and 2 on the EPYC 7763 + WHP host, `0xD.11` and `0xD.12`. The supported surface of step 3 cannot serve: it derives from the root's CPUID, which shows values that a guest does not see there, so it would fail sound hosts. @@ -590,8 +640,8 @@ reports every violation at once, naming the leaf, subleaf, register, and bit: - KVM answers reserved entries from the effective CPUID itself, so it needs no check. -The catalog has four profiles, derived from the fingerprints of three -bare-metal hosts (one per backend), fifteen Azure hosts, and one laptop: +The catalog has five profiles, derived from the fingerprints of three +bare-metal hosts (one per backend), sixteen Azure hosts, and one laptop: | ID | Generation | Hosts | Source backends | | --- | --- | --- | --- | @@ -599,6 +649,7 @@ bare-metal hosts (one per backend), fifteen Azure hosts, and one laptop: | `intel.icelake-sp.v1` | 6/106 | Xeon Platinum 8370C runners | KVM, MSHV, WHP | | `intel.emeraldrapids.v1` | 6/207 | Xeon Platinum 8573C runners | MSHV, WHP (no KVM host exists) | | `intel.alderlake.v1` | 6/151 and 6/154 | One Core i9-12900H laptop (6/154, stepping 3) | WHP | +| `amd.milan.v1` | 25/1 | One Azure Standard_D16as_v5 VM with an EPYC 7763 (25/1, stepping 1) and nested Hyper-V | WHP | The Alder Lake profile serves development hosts, not CI. Derived from one WHP fingerprint, it pins XCR0 `0x7`, because WHP offers that host only x87, @@ -608,6 +659,36 @@ with `E_PROFILE_UNSUPPORTED` until a later revision intersects their fingerprints. The policy needs nothing for the hybrid cores: it clears the hybrid flag, `CPUID.(7,0):EDX[15]`, and keeps no data in leaf `0x1A`. +The Milan profile, the only AMD one, also serves development hosts, not CI; +no CI microVM runner has an AMD CPU. Its generation takes every stepping of +family 25 model 1, Milan-X's 2 included, which differs only in descriptive +fields. +Derived from one nested WHP fingerprint, it presents what that WHP offers +its guests: XCR0 `0x7`, TOPOEXT with AMD's cache and topology leaves, and +CLZERO, but none of the speculation controls of `0x80000008` EBX and +nothing in `0x80000021`, which that WHP does not offer. Linux guests of the +profile therefore mitigate Spectre v2 with retpolines and report SSB, SRSO, +and TSA as vulnerable on every host. The policy's AMD rules keep data in +`0x80000005`, `0x8000001D`, `0x8000001E`, and `0x80000021`, keep AMD's +speculation controls and immunities where every source host offers them, +except what KVM cannot present, and clear 3DNowPrefetch, which the root +partition of a nested Azure host does not see although WHP presents it to +guests, so that host's cold-boot support check would fail a profile that +pinned it; Linux infers `PREFETCHW` from long mode. The WHP fingerprint +offers PSFD and `BTC_NO` too, which the profile drops: KVM never enumerates +`BTC_NO`, and Linux marks no CPU of family `0x19` or later as affected by +RETBLEED, which is all the bit decides; PSFD is a bit of `SPEC_CTRL`, +which KVM lets a guest access only beside IBRS, STIBP, or SSBD, and stays +only with one of them, as OpenVMM's KVM backend keeps it in KVM's +supported surface. A Milan host on KVM therefore fails the profile on +neither bit. Milan hosts on KVM and MSHV, bare-metal ones, and other SKUs +are still unverified, and so are the AMD paths of the KVM and MSHV +backends: the AMD MSR contract and MSHV's per-VP `0x8000001E`. A host that +lacks one of the profile's features fails with `E_PROFILE_UNSUPPORTED`, +which names `--cpu-profile host`, until a later revision intersects their +fingerprints, which could also add the speculation controls, and PSFD +beside them, that bare-metal hosts offer. + Sharing costs nothing on Ice Lake-SP and Emerald Rapids, where every backend offers the same features. On Skylake-SP, used only by the bare-metal development hosts, the shared profile drops what one backend alone offers: @@ -1625,7 +1706,7 @@ code. | `E_BACKEND_MISMATCH` | Snapshot taken on another backend | Restore | | `E_PROFILE_UNKNOWN` | Profile ID neither pinned in this OpenVMM nor a host profile, or a restore's explicit `--cpu-profile` names another profile than the snapshot's | Cold boot, restore | | `E_PROFILE_DIGEST` | The recorded profile digest or embedded document is not the pinned profile's of the same ID, or a host profile's embedded document is not a canonical host profile of the recorded ID and digest | Restore | -| `E_PROFILE_HOST_UNKNOWN` | `--cpu-profile auto` maps the host to no profile, or to profiles of more than one generation, or `--cpu-profile host` runs on a CPU of another vendor than Intel | Cold boot | +| `E_PROFILE_HOST_UNKNOWN` | `--cpu-profile auto` maps the host to no profile, or to profiles of more than one generation, or `--cpu-profile host` runs on a CPU of another vendor than Intel and AMD | Cold boot | | `E_PROFILE_TIME_BITS` | The profile is invalid or violates the CPU time bits | Cold boot, restore | | `E_CPU_GENERATION` | Host CPU vendor, family, model, or stepping not in the profile | Cold boot, restore | | `E_PROFILE_UNSUPPORTED` | Backend lacks a feature, limit, XSAVE layout, MSR value, or feature-bank bit of the profile, or the host is not qualified | Cold boot, restore | @@ -1821,8 +1902,9 @@ most 2.1 ns. ### Generations and runner placement Generation names used in logs, reports, and job summaries are `skylake-sp` -(family 6, model 85), `icelake-sp` (6/106), `emeraldrapids` (6/207), and -`alderlake` (6/151 and 6/154), which no CI runner has. +(family 6, model 85), `icelake-sp` (6/106), `emeraldrapids` (6/207), +`alderlake` (6/151 and 6/154), and `milan` (25/1); no CI microVM runner has +the last two. `validate-runner` and `nvx.py doctor` detect the generation at run time and report it, together with the selected profile, `F_d`, `L`, the `H4` rate metrics, and the skew metrics, in their log and the job summary; an unknown diff --git a/doc/setup.md b/doc/setup.md index 5c734989..74ad75cd 100644 --- a/doc/setup.md +++ b/doc/setup.md @@ -31,12 +31,15 @@ Python 3.10 or newer and Git are required on every platform. By default, a microVM boots on the built-in [CPU profile](usage.md#cpu-profiles) of its host's CPU, so the host needs a CPU -that one of OpenVMM's built-in profiles serves. On another Intel CPU, +that one of OpenVMM's built-in profiles serves, four Intel generations and +AMD's EPYC Milan, on a hypervisor that supports that profile. On another +Intel or AMD CPU, or where the hypervisor does not support the built-in +profile (`E_PROFILE_UNSUPPORTED`, whose message names this option), `nvx.py run --cpu-profile host` opts in to a development profile derived from the host, which OpenVMM verifies as it verifies a built-in profile: the cold boot still fails with `E_PROFILE_UNSUPPORTED` if the hypervisor lacks a CPU -feature that the time ABI requires. AMD CPUs have no CPU profiles yet -([#396](https://github.com/microsoft/nvx/issues/396)). +feature that the time ABI requires. CPUs of other vendors have no CPU +profiles. For Debian/Ubuntu hosts: diff --git a/doc/usage.md b/doc/usage.md index 578bbdb2..bc213926 100644 --- a/doc/usage.md +++ b/doc/usage.md @@ -487,8 +487,11 @@ If no built-in CPU profile serves the host's CPU, OpenVMM exits with `E_PROFILE_HOST_UNKNOWN` before it creates the VM. After a failed cold boot on such a CPU, `run` names the CPU, lists the CPUs that the built-in profiles cover, and gives the next steps described in [CPU profiles](#cpu-profiles), -suggesting `--cpu-profile host` only on an Intel CPU. OpenVMM's own error, -above the guidance, names the cause of the failure. +suggesting `--cpu-profile host` only on an Intel or AMD CPU. OpenVMM's own +error, above the guidance, names the cause of the failure. A host whose +hypervisor does not support its built-in profile fails with +`E_PROFILE_UNSUPPORTED` instead, and on an Intel or AMD CPU OpenVMM's error +itself names `--cpu-profile host`. ### `sandbox` @@ -586,16 +589,24 @@ built-in profile of the host's CPU generation: | `intel.icelake-sp.v1` | `icelake-sp` | Intel Xeon Scalable, third generation (6/106) | | `intel.emeraldrapids.v1` | `emeraldrapids` | Intel Xeon Scalable, fifth generation (6/207) | | `intel.alderlake.v1` | `alderlake` | Intel Core, twelfth generation (6/151 and 6/154) | +| `amd.milan.v1` | `milan` | AMD EPYC, third generation (25/1, Milan-X included) | Any other CPU, including Cascade Lake, Sapphire Rapids, Tiger Lake, Raptor -Lake, and every AMD CPU, fails with `E_PROFILE_HOST_UNKNOWN`. A host of a -listed generation can still fail with `E_PROFILE_UNSUPPORTED` if its SKU or -hypervisor lacks a feature of the profile: `intel.alderlake.v1` derives from -one Core i9-12900H on WHP, so Alder Lake hosts on KVM, and SKUs without its -features, are unverified. - -On an Intel development host that no built-in profile serves, `run ---cpu-profile host` opts in to a host profile, `intel.host.v1`. OpenVMM +Lake, and AMD's Genoa and Ryzen CPUs, fails with `E_PROFILE_HOST_UNKNOWN`. A +host of a listed generation can still fail with `E_PROFILE_UNSUPPORTED` if its +SKU or hypervisor lacks a feature of the profile, and on an Intel or AMD CPU +OpenVMM's error then names `--cpu-profile host`: `intel.alderlake.v1` derives +from one Core i9-12900H on WHP, and `amd.milan.v1` from one EPYC 7763 Azure +VM on WHP, so Alder Lake and Milan hosts on KVM and MSHV, and SKUs without +their features, are unverified. The Milan profile leaves out what KVM cannot +present, `BTC_NO` and PSFD without a `SPEC_CTRL` control, and presents none +of the speculation controls that the Azure VM's WHP withholds, so its Linux +guests use retpolines and report SSB, SRSO, and TSA as vulnerable on every +host. + +On an Intel or AMD development host that no built-in profile serves, or whose +hypervisor does not support its built-in profile, `run --cpu-profile host` +opts in to a host profile, `intel.host.v1` or `amd.host.v1`. OpenVMM fingerprints the hypervisor on this host and applies the built-in profiles' derivation policy to it, so the guest sees the same kind of filtered CPU surface, and verifies it as it verifies a built-in profile: a cold boot still @@ -612,10 +623,10 @@ the time ABI requires. A host profile is for development only: profile. [#390](https://github.com/microsoft/nvx/issues/390) tracks built-in profiles -for more CPUs, and [#396](https://github.com/microsoft/nvx/issues/396) -profiles for AMD CPUs, which host profiles do not serve either. A profile -derives from fingerprints of its generation's hosts on every backend that it -serves; see `vmm_core/cpu_profile` in the OpenVMM submodule. +for more CPUs, and [#396](https://github.com/microsoft/nvx/issues/396) for +more AMD CPUs; host profiles serve no CPU of another vendor than Intel and +AMD. A profile derives from fingerprints of its generation's hosts on every +backend that it serves; see `vmm_core/cpu_profile` in the OpenVMM submodule. ## Benchmarking diff --git a/openvmm b/openvmm index a41cf735..07850d27 160000 --- a/openvmm +++ b/openvmm @@ -1 +1 @@ -Subproject commit a41cf7351807b2ce7dcfbd52bdd868c5df5d6a71 +Subproject commit 07850d27af783ea8765111741763cb6d326228df diff --git a/scripts/nvx_tools/time_abi.py b/scripts/nvx_tools/time_abi.py index 5e53e505..12e65bf4 100644 --- a/scripts/nvx_tools/time_abi.py +++ b/scripts/nvx_tools/time_abi.py @@ -68,7 +68,7 @@ WARP_PROBE_ROUNDS - 1 ) QUALIFICATION_WARP_GAPS: tuple[str, ...] = ("0.1", "1", "5", "1") -PROFILE_VENDORS: Mapping[str, str] = {"GenuineIntel": "intel"} +PROFILE_VENDORS: Mapping[str, str] = {"GenuineIntel": "intel", "AuthenticAMD": "amd"} # The generation of every host profile, which `openvmm --cpu-profile host` # derives from the host it runs on (doc/design/time-abi.md, "Selection"). No # pinned profile uses it, and host qualification never accepts one. @@ -123,12 +123,14 @@ def describe(self) -> str: # the submodule. Doctor uses it only without OpenVMM (--no-openvmm), and run # to explain a failed cold boot on a CPU that it lacks; otherwise OpenVMM # reports the generation and the profile itself. Model 85 also covers Cascade -# Lake (steppings 5-7) and Cooper Lake (10-11), which have no profile. +# Lake (steppings 5-7) and Cooper Lake (10-11), which have no profile; AMD's +# family 25 model 1 is Milan's in every stepping, Milan-X's 2 included. CPU_GENERATIONS: tuple[CpuGeneration, ...] = ( CpuGeneration("skylake-sp", "GenuineIntel", (CpuModel(6, 85, range(5)),)), CpuGeneration("icelake-sp", "GenuineIntel", (CpuModel(6, 106),)), CpuGeneration("emeraldrapids", "GenuineIntel", (CpuModel(6, 207),)), CpuGeneration("alderlake", "GenuineIntel", (CpuModel(6, 151), CpuModel(6, 154))), + CpuGeneration("milan", "AuthenticAMD", (CpuModel(25, 1),)), ) @@ -143,8 +145,8 @@ def describe_cpu_generations() -> str: PROFILE_HOST_UNKNOWN = "E_PROFILE_HOST_UNKNOWN" # The CPU vendors whose CPUs host profiles serve, as OpenVMM's # `cpu_profile::supports_host_profiles` decides. -HOST_PROFILE_VENDORS = frozenset({"GenuineIntel"}) -# The issues that track CPU profiles for more CPUs, and for AMD CPUs. +HOST_PROFILE_VENDORS = frozenset({"GenuineIntel", "AuthenticAMD"}) +# The issues that track CPU profiles for more CPUs, and for more AMD CPUs. CPU_SUPPORT_ISSUE = "https://github.com/microsoft/nvx/issues/390" AMD_SUPPORT_ISSUE = "https://github.com/microsoft/nvx/issues/396" @@ -199,7 +201,7 @@ def host_cpu_unsupported_guidance( if not host_profiles: lines.append( f"nvx: --cpu-profile {HOST_PROFILE_GENERATION} cannot boot on it " - "either: host CPU profiles serve only Intel CPUs." + "either: host CPU profiles serve only Intel and AMD CPUs." ) elif auto: lines.append( @@ -208,7 +210,7 @@ def host_cpu_unsupported_guidance( 'this host; doc/usage.md ("CPU profiles") explains its limits.' ) if host.vendor == "AuthenticAMD": - lines.append(f"nvx: {AMD_SUPPORT_ISSUE} tracks CPU profiles for AMD CPUs.") + lines.append(f"nvx: {AMD_SUPPORT_ISSUE} tracks CPU profiles for more AMD CPUs.") else: lines.append(f"nvx: {CPU_SUPPORT_ISSUE} tracks CPU profiles for more CPUs.") return "\n".join(lines) diff --git a/scripts/test_nvx_tools.py b/scripts/test_nvx_tools.py index 2a41e033..f759ae6b 100644 --- a/scripts/test_nvx_tools.py +++ b/scripts/test_nvx_tools.py @@ -1738,14 +1738,24 @@ def run( self.assertIn("rerun with --cpu-profile host", output) self.assertIn("https://github.com/microsoft/nvx/issues/390", output) self.assertEqual(run(1, "auto"), (1, output)) - # Host profiles serve only Intel CPUs, so an AMD host gets no such - # suggestion, with either request, but the issue that tracks AMD. + # Host profiles serve AMD CPUs too, so an AMD host that no built-in + # profile serves gets the same suggestion, with the issue that tracks + # profiles for more AMD CPUs, and a host profile that fails on it + # fails for another reason, which OpenVMM explains. status, output = run(1, None, host=zen3) self.assertIn("AuthenticAMD 25/33/0", output) - self.assertNotIn("rerun with --cpu-profile host", output) - self.assertIn("host CPU profiles serve only Intel CPUs", output) + self.assertIn("rerun with --cpu-profile host", output) self.assertIn("https://github.com/microsoft/nvx/issues/396", output) - self.assertEqual(run(1, "host", host=zen3), (1, output)) + self.assertNotIn("issues/390", output) + self.assertEqual(run(1, "host", host=zen3), (1, "")) + # Host profiles serve only Intel and AMD CPUs, so another vendor's + # host gets no such suggestion, with either request. + hygon = HostCpu("HygonGenuine", 24, 0, 1) + status, output = run(1, None, host=hygon) + self.assertIn("HygonGenuine 24/0/1", output) + self.assertNotIn("rerun with --cpu-profile host", output) + self.assertIn("host CPU profiles serve only Intel and AMD CPUs", output) + self.assertEqual(run(1, "host", host=hygon), (1, output)) # Successful runs, restores, other requests and CPUs, and an unknown # CPU get no guidance. for returncode, cpu_profile, cold_boot, host in ( @@ -1754,6 +1764,7 @@ def run( (1, "host", True, tiger_lake), (1, "intel.alderlake.v1", True, tiger_lake), (1, None, True, HostCpu("GenuineIntel", 6, 154, 3)), + (1, None, True, HostCpu("AuthenticAMD", 25, 1, 1)), (1, None, True, None), ): with self.subTest( diff --git a/scripts/test_time_abi.py b/scripts/test_time_abi.py index 8ed644ca..90fb84fc 100644 --- a/scripts/test_time_abi.py +++ b/scripts/test_time_abi.py @@ -135,6 +135,20 @@ def name(model: int, stepping: int, vendor: str = "GenuineIntel"): self.assertIsNone(name(140, 1)) self.assertIsNone(name(183, 1)) self.assertIsNone(name(1, 1, "AuthenticAMD")) + # Milan's profile serves every stepping of AMD's family 25 model 1, + # Milan-X's 2 included, and no other vendor's CPU with its signature. + for stepping in (0, 1, 2, 15): + generation = time_abi.cpu_generation("AuthenticAMD", 25, 1, stepping) + assert generation is not None + self.assertEqual(generation.name, "milan") + for vendor, family, model in ( + ("AuthenticAMD", 25, 17), + ("AuthenticAMD", 25, 33), + ("GenuineIntel", 25, 1), + ("HygonGenuine", 25, 1), + ): + with self.subTest(vendor=vendor, family=family, model=model): + self.assertIsNone(time_abi.cpu_generation(vendor, family, model, 1)) def test_names_the_catalog_profiles(self): # One profile per generation serves every backend. @@ -145,12 +159,13 @@ def test_names_the_catalog_profiles(self): "intel.icelake-sp.v1", "intel.emeraldrapids.v1", "intel.alderlake.v1", + "amd.milan.v1", ], ) self.assertEqual( time_abi.describe_cpu_generations(), "skylake-sp 6/85 steppings 0-4, icelake-sp 6/106, emeraldrapids 6/207, " - "alderlake 6/151 and 6/154", + "alderlake 6/151 and 6/154, milan 25/1", ) def test_the_catalog_copy_matches_openvmm_pinned_profiles(self): @@ -226,7 +241,9 @@ def test_tells_catalog_profiles_from_host_profiles(self): for profile_id, catalog, host in ( ("intel.icelake-sp.v1", True, False), ("intel.raptorlake.v2", True, False), + ("amd.milan.v1", True, False), ("intel.host.v1", False, True), + ("amd.host.v1", False, True), # Host profile IDs take the revision syntax of every profile ID. ("intel.host.v0", False, False), ("interim.host.kvm.v1", False, False), @@ -261,10 +278,11 @@ def test_guides_hosts_that_no_built_in_profile_serves(self): # for another reason, which OpenVMM's own error explains. self.assertIsNone(time_abi.host_cpu_unsupported_guidance("host", tiger_lake)) - # Host profiles cannot serve an AMD CPU either: no suggestion, with - # either request, and the issue that tracks AMD CPUs. - zen4 = time_abi.HostCpu("AuthenticAMD", 25, 17, 1) - guidance = time_abi.host_cpu_unsupported_guidance(None, zen4) + # Host profiles serve AMD CPUs too: an AMD CPU without a built-in + # profile, such as Genoa, gets the same suggestion as an Intel one, + # and the issue that tracks profiles for more AMD CPUs. + genoa = time_abi.HostCpu("AuthenticAMD", 25, 17, 1) + guidance = time_abi.host_cpu_unsupported_guidance(None, genoa) assert guidance is not None self.assertIn( "this host's CPU, AuthenticAMD 25/17/1, so a cold boot with " @@ -273,27 +291,33 @@ def test_guides_hosts_that_no_built_in_profile_serves(self): guidance, ) self.assertIn(time_abi.describe_cpu_generations(), guidance) - self.assertNotIn("rerun with", guidance) - self.assertIn("host CPU profiles serve only Intel CPUs", guidance) + self.assertIn("rerun with --cpu-profile host", guidance) self.assertIn("https://github.com/microsoft/nvx/issues/396", guidance) self.assertNotIn("issues/390", guidance) - self.assertEqual(guidance, time_abi.host_cpu_unsupported_guidance("host", zen4)) - # Another vendor gets the general issue. - guidance = time_abi.host_cpu_unsupported_guidance( - "auto", time_abi.HostCpu("HygonGenuine", 24, 0, 1) - ) + self.assertIsNone(time_abi.host_cpu_unsupported_guidance("host", genoa)) + # Another vendor gets no suggestion, with either request. + hygon = time_abi.HostCpu("HygonGenuine", 24, 0, 1) + guidance = time_abi.host_cpu_unsupported_guidance("auto", hygon) assert guidance is not None - self.assertIn("host CPU profiles serve only Intel CPUs", guidance) + self.assertNotIn("rerun with", guidance) + self.assertIn("host CPU profiles serve only Intel and AMD CPUs", guidance) self.assertIn("https://github.com/microsoft/nvx/issues/390", guidance) + self.assertEqual( + guidance, time_abi.host_cpu_unsupported_guidance("host", hygon) + ) # A CPU that a built-in profile serves, an explicit profile, and an # unknown CPU get none. alder_lake = time_abi.HostCpu("GenuineIntel", 6, 154, 3) + milan = time_abi.HostCpu("AuthenticAMD", 25, 1, 1) for cpu_profile, host in ( (None, alder_lake), ("host", alder_lake), + (None, milan), + ("host", milan), ("intel.alderlake.v1", tiger_lake), - ("intel.alderlake.v1", zen4), + ("intel.alderlake.v1", genoa), + ("intel.skylake-sp.v1", milan), (None, None), ): with self.subTest(cpu_profile=cpu_profile, host=host):