From ffa9c0096cd104c8cae56731a4777eb809882e62 Mon Sep 17 00:00:00 2001 From: Joongi Kim Date: Sun, 9 Aug 2026 18:32:19 +0000 Subject: [PATCH] feat: align CPU providers with the WheelNext reference plugins MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit aarch64: replace the hardcoded arch=v8 property with real detection — the Armv8.x/v9.x version ladder is computed from HWCAP flags (golang.org/x/sys/cpu), emitted as version = 9.0a/8.5a/…/8a following the WheelNext provider-variant-aarch64 value scheme. Both CPU providers additionally emit one property per supported CPU flag (feature = "on"), with the feature vocabulary taken verbatim from the WheelNext reference plugins' all_features lists and detected by parsing /proc/cpuinfo (Linux; omitted elsewhere). Upstream's "btiecv" and "asimdrdmatomics" entries are concatenation typos and are carried as their separated archspec features. Breaking: the aarch64 namespace no longer emits arch=v8; images labeled with the old schema will not match. Claude-Session: https://claude.ai/code/session_01EPVqgoDSoJ5Xx3jZ6d1AbX --- docs/DESIGN.md | 4 +- pkg/providers/aarch64.go | 124 +++++++++++++++++++++++++++++++- pkg/providers/cpuinfo.go | 62 ++++++++++++++++ pkg/providers/providers_test.go | 76 ++++++++++++++++++++ pkg/providers/x86_64.go | 85 +++++++++++++++++++++- 5 files changed, 344 insertions(+), 7 deletions(-) create mode 100644 pkg/providers/cpuinfo.go diff --git a/docs/DESIGN.md b/docs/DESIGN.md index 70683c4..7d9c4b5 100644 --- a/docs/DESIGN.md +++ b/docs/DESIGN.md @@ -182,8 +182,8 @@ interaction uses `oras-go` v2 with Docker's credential store (ADR-4). | Namespace | Features | Source | |---|---|---| -| `x86_64` | `level` = `v4`/`v3`/`v2`/`v1` (preference-ordered, all supported levels emitted) | CPUID via `golang.org/x/sys/cpu` | -| `aarch64` | `arch` = `v8` | runtime GOARCH | +| `x86_64` | `level` = `v4`/`v3`/`v2`/`v1` (preference-ordered, all supported levels emitted); plus `` = `on` per supported flag in the WheelNext x86_64 provider feature list (`avx2`, `sha_ni`, …) | CPUID via `golang.org/x/sys/cpu`; `/proc/cpuinfo` flags (Linux) | +| `aarch64` | `version` = `9.0a`/`8.5a`/`8.4a`/…/`8a` (preference-ordered, all supported versions emitted; WheelNext aarch64 provider value scheme); plus `` = `on` per supported flag in the WheelNext aarch64 provider feature list (`sve2`, `i8mm`, …) | HWCAP via `golang.org/x/sys/cpu`; `/proc/cpuinfo` Features (Linux) | | `nvidia` | `cuda_version_lower_bound` = all known CUDA versions ≤ detected, descending | `nvidia-smi` header parse | | (mock) | anything | `--properties-file` / `DOCKER_VARIANT_PROPERTIES_FILE` | diff --git a/pkg/providers/aarch64.go b/pkg/providers/aarch64.go index 3868863..b059435 100644 --- a/pkg/providers/aarch64.go +++ b/pkg/providers/aarch64.go @@ -3,10 +3,16 @@ package providers import ( "context" "runtime" + + "golang.org/x/sys/cpu" ) -// aarch64Provider reports a minimal ARM64 architecture property. Finer -// detection (v9, SVE, …) is future work; "v8" is the baseline every Go +// aarch64Provider reports the Arm architecture versions supported by the +// host CPU, as the feature "version" with values "9.0a" > "8.4a" > … > "8a" +// (all supported versions are emitted, preference-ordered), plus one +// property per supported CPU flag ("sve2" = "on", …) from the WheelNext +// reference feature list. The value scheme follows the WheelNext aarch64 +// provider plugin: "armv9.0a" → "9.0a", with "8a" as the baseline every Go // arm64 binary can assume. type aarch64Provider struct{} @@ -16,5 +22,117 @@ func (aarch64Provider) Detect(ctx context.Context) (map[string][]string, error) if runtime.GOARCH != "arm64" { return nil, nil } - return map[string][]string{"arch": {"v8"}}, nil + features := onFeatures(aarch64RefFeatures, hostCPUFlags()) + features["version"] = supportedVersions(hostARMFeatures()) + return features, nil +} + +// aarch64RefFeatures mirrors all_features of the WheelNext aarch64 provider +// plugin (wheelnext/provider-variant-aarch64), preference-ordered; the names +// are the Linux hwcap vocabulary used by archspec. Upstream's "btiecv" and +// "asimdrdmatomics" entries are concatenations of the archspec features +// listed separately here. +var aarch64RefFeatures = []string{ + // neoverse_n2/v2 (armv9.0) features + "sve2", + "flagm2", + "frint", + "sb", + // m2 (armv8.5) features + "bti", + "ecv", + // m1 (armv8.4) features + "paca", + "pacg", + "ssbs", + // neoverse_v1 (armv8.4) features + "asimdfhm", + "bf16", + "dcpodp", + "dgh", + "dit", + "flagm", + "i8mm", + "ilrcpc", + "jscvt", + "rng", + "sha3", + "sha512", + "svebf16", + "svei8mm", + "uscat", + // neoverse_n1 (armv8.2) features + "asimddp", + "lrcpc", + // a64fx (armv8.2) features + "asimdhp", + "dcpop", + "fcma", + "fphp", + "sve", + // thunderx2 (armv8.1) features + "asimdrdm", + "atomics", + // cortex_a72 (armv8.0) features + "aes", + "asimd", + "cpuid", + "crc32", + "evtstrm", + "fp", + "pmull", + "sha1", + "sha2", +} + +// armFeatures is the subset of HWCAP flags consulted by supportedVersions, +// extracted as a struct so tests can construct arbitrary CPUs. +type armFeatures struct { + ATOMICS, ASIMDRDM, CRC32 bool + DCPOP bool + JSCVT, FCMA, LRCPC bool + DIT, ASIMDFHM bool + SVE2 bool +} + +func hostARMFeatures() armFeatures { + return armFeatures{ + ATOMICS: cpu.ARM64.HasATOMICS, ASIMDRDM: cpu.ARM64.HasASIMDRDM, + CRC32: cpu.ARM64.HasCRC32, + DCPOP: cpu.ARM64.HasDCPOP, + JSCVT: cpu.ARM64.HasJSCVT, FCMA: cpu.ARM64.HasFCMA, LRCPC: cpu.ARM64.HasLRCPC, + DIT: cpu.ARM64.HasDIT, ASIMDFHM: cpu.ARM64.HasASIMDFHM, + SVE2: cpu.ARM64.HasSVE2, + } +} + +// supportedVersions computes the Armv8.x/v9.x version ladder from the +// mandatory features of each revision, limited to the flags golang.org/x/sys +// exposes: v8.1 FEAT_LSE/RDM/CRC32, v8.2 FEAT_DPB, v8.3 +// FEAT_JSCVT/FCMA/LRCPC, v8.4 FEAT_DIT/FHM, v9.0 FEAT_SVE2. Armv8.5–v8.6 +// have no detectable mandatory flags here, but Armv9.0 compliance implies +// Armv8.5, so "8.5a" is emitted alongside "9.0a". +func supportedVersions(f armFeatures) []string { + v81 := f.ATOMICS && f.ASIMDRDM && f.CRC32 + v82 := v81 && f.DCPOP + v83 := v82 && f.JSCVT && f.FCMA && f.LRCPC + v84 := v83 && f.DIT && f.ASIMDFHM + v90 := v84 && f.SVE2 + versions := []string{"8a"} + if v81 { + versions = append([]string{"8.1a"}, versions...) + } + if v82 { + versions = append([]string{"8.2a"}, versions...) + } + if v83 { + versions = append([]string{"8.3a"}, versions...) + } + if v84 { + versions = append([]string{"8.4a"}, versions...) + } + if v90 { + versions = append([]string{"9.0a", "8.5a"}, versions...) + } + return versions } diff --git a/pkg/providers/cpuinfo.go b/pkg/providers/cpuinfo.go new file mode 100644 index 0000000..073af7a --- /dev/null +++ b/pkg/providers/cpuinfo.go @@ -0,0 +1,62 @@ +package providers + +import ( + "bufio" + "io" + "os" + "strings" +) + +// Per-feature detection shared by the CPU providers. The WheelNext reference +// providers (provider-variant-x86-64, provider-variant-aarch64) emit each +// supported CPU feature as its own property (feature name -> "on"), with +// names taken from the Linux /proc/cpuinfo flag vocabulary via archspec. We +// read the same source directly. + +// hostCPUFlags returns the feature tokens the kernel reports in +// /proc/cpuinfo ("flags" on x86, "Features" on arm64), or nil on hosts +// without it (non-Linux) — there, per-feature properties are simply omitted +// and only the level/version ladder is detected. +func hostCPUFlags() map[string]bool { + f, err := os.Open("/proc/cpuinfo") + if err != nil { + return nil + } + defer f.Close() + return parseCPUFlags(f) +} + +// parseCPUFlags extracts the tokens of the first "flags" (x86) or +// "Features" (arm64) line of a /proc/cpuinfo stream. +func parseCPUFlags(r io.Reader) map[string]bool { + sc := bufio.NewScanner(r) + for sc.Scan() { + key, rest, ok := strings.Cut(sc.Text(), ":") + if !ok { + continue + } + switch strings.TrimSpace(key) { + case "flags", "Features": + flags := map[string]bool{} + for _, tok := range strings.Fields(rest) { + flags[tok] = true + } + return flags + } + } + return nil +} + +// onFeatures maps every reference feature present on the host to the single +// value "on", mirroring the reference providers' per-feature properties. +// Host flags outside the reference list are ignored so the emitted feature +// vocabulary stays identical to the reference providers'. +func onFeatures(ref []string, host map[string]bool) map[string][]string { + features := map[string][]string{} + for _, f := range ref { + if host[f] { + features[f] = []string{"on"} + } + } + return features +} diff --git a/pkg/providers/providers_test.go b/pkg/providers/providers_test.go index 4b39c32..fda815b 100644 --- a/pkg/providers/providers_test.go +++ b/pkg/providers/providers_test.go @@ -3,9 +3,11 @@ package providers import ( "context" "errors" + "maps" "os" "path/filepath" "slices" + "strings" "testing" ) @@ -35,6 +37,80 @@ func TestSupportedLevels(t *testing.T) { } } +func TestSupportedVersions(t *testing.T) { + v81CPU := armFeatures{ATOMICS: true, ASIMDRDM: true, CRC32: true} + v82CPU := v81CPU + v82CPU.DCPOP = true + v83CPU := v82CPU + v83CPU.JSCVT, v83CPU.FCMA, v83CPU.LRCPC = true, true, true + v84CPU := v83CPU + v84CPU.DIT, v84CPU.ASIMDFHM = true, true + v90CPU := v84CPU + v90CPU.SVE2 = true + sve2Only := armFeatures{SVE2: true} // missing v8.1–v8.4 prerequisites + + cases := []struct { + name string + cpu armFeatures + want []string + }{ + {"bare", armFeatures{}, []string{"8a"}}, + {"v8.1", v81CPU, []string{"8.1a", "8a"}}, + {"v8.2", v82CPU, []string{"8.2a", "8.1a", "8a"}}, + {"v8.3", v83CPU, []string{"8.3a", "8.2a", "8.1a", "8a"}}, + {"v8.4", v84CPU, []string{"8.4a", "8.3a", "8.2a", "8.1a", "8a"}}, + {"v9.0", v90CPU, []string{"9.0a", "8.5a", "8.4a", "8.3a", "8.2a", "8.1a", "8a"}}, + {"sve2 without v8.4 base", sve2Only, []string{"8a"}}, + } + for _, c := range cases { + if got := supportedVersions(c.cpu); !slices.Equal(got, c.want) { + t.Errorf("%s: supportedVersions = %v, want %v", c.name, got, c.want) + } + } +} + +func TestParseCPUFlags(t *testing.T) { + x86 := `processor : 0 +vendor_id : GenuineIntel +flags : fpu vme sse sse2 ssse3 sse4_1 sse4_2 avx avx2 sha_ni +bugs : spectre_v1 spectre_v2 +` + arm := `processor : 0 +Features : fp asimd evtstrm aes sve sve2 i8mm bf16 +CPU implementer : 0x41 +` + got := parseCPUFlags(strings.NewReader(x86)) + for _, f := range []string{"sse4_2", "sha_ni", "avx2"} { + if !got[f] { + t.Errorf("x86 flags: %q missing from %v", f, got) + } + } + if got["spectre_v1"] { + t.Error("x86 flags: bugs line must not be parsed as flags") + } + got = parseCPUFlags(strings.NewReader(arm)) + for _, f := range []string{"sve2", "bf16", "asimd"} { + if !got[f] { + t.Errorf("arm Features: %q missing from %v", f, got) + } + } + if parseCPUFlags(strings.NewReader("no flag line here")) != nil { + t.Error("no flags line: want nil") + } +} + +func TestOnFeatures(t *testing.T) { + host := map[string]bool{"sve2": true, "aes": true, "weird_vendor_flag": true} + got := onFeatures(aarch64RefFeatures, host) + want := map[string][]string{"sve2": {"on"}, "aes": {"on"}} + if !maps.EqualFunc(got, want, slices.Equal) { + t.Errorf("onFeatures = %v, want %v", got, want) + } + if got := onFeatures(x86RefFeatures, nil); len(got) != 0 { + t.Errorf("nil host flags: want empty map, got %v", got) + } +} + func TestParseNvidiaSMI(t *testing.T) { banner := `Mon Jul 27 10:00:00 2026 +-----------------------------------------------------------------------------------------+ diff --git a/pkg/providers/x86_64.go b/pkg/providers/x86_64.go index e1880ba..80dde64 100644 --- a/pkg/providers/x86_64.go +++ b/pkg/providers/x86_64.go @@ -9,7 +9,9 @@ import ( // x86Provider reports the x86-64 microarchitecture levels supported by the // host CPU, as the feature "level" with values "v4" > "v3" > "v2" > "v1" -// (all supported levels are emitted, preference-ordered). +// (all supported levels are emitted, preference-ordered), plus one property +// per supported CPU flag ("avx2" = "on", …) from the WheelNext reference +// feature list. type x86Provider struct{} func (x86Provider) Namespace() string { return "x86_64" } @@ -18,7 +20,86 @@ func (x86Provider) Detect(ctx context.Context) (map[string][]string, error) { if runtime.GOARCH != "amd64" { return nil, nil } - return map[string][]string{"level": supportedLevels(hostX86Features())}, nil + features := onFeatures(x86RefFeatures, hostCPUFlags()) + features["level"] = supportedLevels(hostX86Features()) + return features, nil +} + +// x86RefFeatures mirrors all_features of the WheelNext x86_64 provider +// plugin (wheelnext/provider-variant-x86-64), preference-ordered; the names +// are the Linux /proc/cpuinfo flag vocabulary used by archspec. +var x86RefFeatures = []string{ + // zen5 features + "avx_vnni", + "cppc", + "ibrs_enhanced", + "tsc_adjust", + // zen4 features + "flush_l1d", + // sapphirerapids features + "movdir64b", + "movdiri", + // icelake features + "avx512_bf16", + "avx512_bitalg", + "avx512_vbmi2", + "avx512_vnni", + "avx512_vp2intersect", + "avx512_vpopcntdq", + "avx512ifma", + "avx512vbmi", + "rdpid", + "sha_ni", + "vaes", + "vpclmulqdq", + // skylake_avx512 features + "clwb", + "clzero", + // x86-64-v4 features + "avx512bw", + "avx512cd", + "avx512dq", + "avx512f", + "avx512vl", + // skylake features + "clflushopt", + "gfni", + "rdseed", + "xsavec", + "xsaveopt", + // broadwell features + "adx", + // x86-64-v3 features + "avx2", + "avx", + "bmi2", + "bmi1", + "abm", + "f16c", + "fma", + "movbe", + "xsave", + // sandybridge features + "rdrand", + // westmere features + "aes", + // nehalem features + "pclmulqdq", + // steamroller features + "sse4a", + "fsgsbase", + // x86-64-v2 features + "sse4_2", + "sse4_1", + "ssse3", + "sse3", + "cx16", + "lahf_lm", + "popcnt", + // x86-64-v1 features + "sse2", + "sse", + "mmx", } // x86Features is the subset of CPUID flags consulted by supportedLevels,