diff --git a/daemon/containerd/platform_matchers.go b/daemon/containerd/platform_matchers.go index fb9cff3a5d..f8692923cd 100644 --- a/daemon/containerd/platform_matchers.go +++ b/daemon/containerd/platform_matchers.go @@ -2,8 +2,8 @@ package containerd import ( "github.com/containerd/platforms" + "github.com/moby/moby/v2/daemon/internal/platform" ocispec "github.com/opencontainers/image-spec/specs-go/v1" - archvariant "github.com/tonistiigi/go-archvariant" ) // platformsWithPreferenceMatcher is a platform matcher that matches any of the @@ -75,9 +75,5 @@ func (i *ImageService) hostPlatformSpec() ocispec.Platform { if i.defaultPlatformOverride != nil { return *i.defaultPlatformOverride } - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p + return platform.MaximumSpec() } diff --git a/daemon/create.go b/daemon/create.go index a14a28d5f2..69ee518480 100644 --- a/daemon/create.go +++ b/daemon/create.go @@ -22,13 +22,13 @@ import ( "github.com/moby/moby/v2/daemon/internal/metrics" "github.com/moby/moby/v2/daemon/internal/multierror" "github.com/moby/moby/v2/daemon/internal/otelutil" + "github.com/moby/moby/v2/daemon/internal/platform" "github.com/moby/moby/v2/daemon/server/backend" "github.com/moby/moby/v2/daemon/server/imagebackend" "github.com/moby/moby/v2/errdefs" "github.com/moby/sys/user" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/opencontainers/selinux/go-selinux" - "github.com/tonistiigi/go-archvariant" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/trace" @@ -99,7 +99,7 @@ func (daemon *Daemon) containerCreate(ctx context.Context, daemonCfg *configStor return containertypes.CreateResponse{}, err } if img != nil { - p := maximumSpec() + p := platform.MaximumSpec() imgPlat := ocispec.Platform{ OS: img.OS, Architecture: img.Architecture, @@ -396,12 +396,3 @@ func (daemon *Daemon) validateNetworkingConfig(nwConfig *networktypes.Networking return nil } - -// maximumSpec returns the distribution platform with maximum compatibility for the current node. -func maximumSpec() ocispec.Platform { - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p -} diff --git a/daemon/internal/distribution/pull_v2.go b/daemon/internal/distribution/pull_v2.go index 4156183d62..651ca73db8 100644 --- a/daemon/internal/distribution/pull_v2.go +++ b/daemon/internal/distribution/pull_v2.go @@ -30,7 +30,6 @@ import ( "github.com/opencontainers/go-digest" ocispec "github.com/opencontainers/image-spec/specs-go/v1" "github.com/pkg/errors" - "github.com/tonistiigi/go-archvariant" ) var ( @@ -886,12 +885,3 @@ func toOCIPlatform(p manifestlist.PlatformSpec) *ocispec.Platform { OSVersion: p.OSVersion, } } - -// maximumSpec returns the distribution platform with maximum compatibility for the current node. -func maximumSpec() ocispec.Platform { - p := platforms.DefaultSpec() - if p.Architecture == "amd64" { - p.Variant = archvariant.AMD64Variant() - } - return p -} diff --git a/daemon/internal/distribution/pull_v2_unix.go b/daemon/internal/distribution/pull_v2_unix.go index bd74a3e68c..1d82cfb4ad 100644 --- a/daemon/internal/distribution/pull_v2_unix.go +++ b/daemon/internal/distribution/pull_v2_unix.go @@ -10,6 +10,7 @@ import ( "github.com/containerd/platforms" "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" + "github.com/moby/moby/v2/daemon/internal/platform" ocispec "github.com/opencontainers/image-spec/specs-go/v1" ) @@ -53,7 +54,7 @@ func checkImageCompatibility(imageOS, imageOSVersion string) error { } func withDefault(p ocispec.Platform) ocispec.Platform { - def := maximumSpec() + def := platform.MaximumSpec() if p.OS == "" { p.OS = def.OS } diff --git a/daemon/internal/platform/arm64variant_linux.go b/daemon/internal/platform/arm64variant_linux.go new file mode 100644 index 0000000000..c1bd5eaaba --- /dev/null +++ b/daemon/internal/platform/arm64variant_linux.go @@ -0,0 +1,133 @@ +package platform + +import ( + "fmt" + "runtime" + "sync" + + "golang.org/x/sys/unix" +) + +const ( + atHWCAP = 16 + atHWCAP2 = 26 +) + +// HWCAP bits from arch/arm64/include/uapi/asm/hwcap.h. +const ( + hwcapCRC32 = 1 << 7 + hwcapATOMICS = 1 << 8 + hwcapFPHP = 1 << 9 + hwcapASIMDHP = 1 << 10 + hwcapASIMDRDM = 1 << 12 + hwcapJSCVT = 1 << 13 + hwcapFCMA = 1 << 14 + hwcapLRCPC = 1 << 15 + hwcapDCPOP = 1 << 16 + hwcapASIMDDP = 1 << 20 + hwcapDIT = 1 << 24 + hwcapUSCAT = 1 << 25 + hwcapILRCPC = 1 << 26 + hwcapFLAGM = 1 << 27 + hwcapSB = 1 << 29 + hwcapPACA = 1 << 30 + hwcapPACG = 1 << 31 + + hwcap2DCPODP = 1 << 0 + hwcap2SVE2 = 1 << 1 + hwcap2FLAGM2 = 1 << 7 + hwcap2FRINT = 1 << 8 + hwcap2I8MM = 1 << 13 + hwcap2BF16 = 1 << 14 + hwcap2BTI = 1 << 17 + hwcap2ECV = 1 << 19 + hwcap2WFXT = 1 << 31 + hwcap2CSSC = 1 << 34 + hwcap2MOPS = 1 << 43 + hwcap2SVE2P1 = 1 << 36 + hwcap2HBC = 1 << 44 +) + +// arm64Levels lists the features required by each v8 minor version, in addition +// to the ones required by the previous versions. Features implied or enabled by +// default for each architecture version in LLVM's AArch64Features.td, limited +// to the ones user space can use and the kernel reports in HWCAP. +var arm64Levels = []struct { + hwcap, hwcap2 uint64 +}{ + // v8.1: LLVM: FeatureCRC, FeatureLSE, FeatureRDM, FeaturePAN, FeatureLOR, + // FeatureVH + {hwcap: hwcapCRC32 | hwcapATOMICS | hwcapASIMDRDM}, + // v8.2: LLVM: FeaturePsUAO, FeaturePAN_RWV, FeatureRAS, FeatureCCPP + {hwcap: hwcapDCPOP}, + // v8.3: LLVM: FeatureRCPC, FeaturePAuth, FeatureJS, FeatureComplxNum + {hwcap: hwcapLRCPC | hwcapPACA | hwcapPACG | hwcapJSCVT | hwcapFCMA}, + // v8.4: LLVM: FeatureDotProd, FeatureNV, FeatureMPAM, FeatureDIT, + // FeatureTRACEV8_4, FeatureAM, FeatureSEL2, FeatureTLB_RMI, FeatureFlagM, + // FeatureRCPC_IMMO, FeatureLSE2 + {hwcap: hwcapASIMDDP | hwcapDIT | hwcapFLAGM | hwcapILRCPC | hwcapUSCAT}, + // v8.5: LLVM: FeatureAltFPCmp, FeatureFRInt3264, FeatureSpecRestrict, + // FeatureSB, FeaturePredRes, FeatureCacheDeepPersist, FeatureBranchTargetId + {hwcap: hwcapSB, hwcap2: hwcap2FLAGM2 | hwcap2FRINT | hwcap2DCPODP | hwcap2BTI}, + // v8.6: LLVM: FeatureAMVS, FeatureBF16, FeatureFineGrainedTraps, + // FeatureEnhancedCounterVirtualization, FeatureMatMulInt8 + {hwcap2: hwcap2BF16 | hwcap2ECV | hwcap2I8MM}, + // v8.7: LLVM: FeatureXS, FeatureWFxT + {hwcap2: hwcap2WFXT}, + // v8.8: LLVM: FeatureHBC, FeatureMOPS, FeatureNMI + {hwcap2: hwcap2HBC | hwcap2MOPS}, + // v8.9: LLVM: FeatureCLRBHB, FeaturePRFM_SLC, FeatureSPECRES2, FeatureCSSC, + // FeatureRASv2, FeatureCHK + {hwcap2: hwcap2CSSC}, +} + +// arm64VariantFromHWCAP returns the highest arm64 variant whose features are +// all present in hwcap and hwcap2. +func arm64VariantFromHWCAP(hwcap, hwcap2 uint64) string { + minor := 0 + for _, l := range arm64Levels { + if hwcap&l.hwcap != l.hwcap || hwcap2&l.hwcap2 != l.hwcap2 { + break + } + minor++ + } + + // Armv9.x requires SVE2, FP16 and the features of Armv8.(x+5). + const v9hwcap = hwcapFPHP | hwcapASIMDHP + const v9hwcap2 = hwcap2SVE2 + if minor >= 5 && hwcap&v9hwcap == v9hwcap && hwcap2&v9hwcap2 == v9hwcap2 { + minor -= 5 + // Armv9.4 also requires SVE2.1. + if minor >= 4 && hwcap2&hwcap2SVE2P1 == 0 { + minor = 3 + } + if minor == 0 { + return "v9" + } + return fmt.Sprintf("v9.%d", minor) + } + if minor == 0 { + return "v8" + } + return fmt.Sprintf("v8.%d", minor) +} + +var arm64Variant = sync.OnceValue(func() string { + if runtime.GOARCH != "arm64" { + return "" + } + auxv, err := unix.Auxv() + if err != nil { + return "" + } + var hwcap, hwcap2 uint64 + for _, kv := range auxv { + switch kv[0] { + case atHWCAP: + hwcap = uint64(kv[1]) + case atHWCAP2: + hwcap2 = uint64(kv[1]) + } + } + return arm64VariantFromHWCAP(hwcap, hwcap2) +}) diff --git a/daemon/internal/platform/arm64variant_linux_test.go b/daemon/internal/platform/arm64variant_linux_test.go new file mode 100644 index 0000000000..a3da3396e3 --- /dev/null +++ b/daemon/internal/platform/arm64variant_linux_test.go @@ -0,0 +1,120 @@ +package platform + +import ( + "strings" + "testing" + + "gotest.tools/v3/assert" +) + +// upTo returns the features of the first n levels of arm64Levels. +func upTo(n int) (hwcap, hwcap2 uint64) { + for _, l := range arm64Levels[:n] { + hwcap |= l.hwcap + hwcap2 |= l.hwcap2 + } + return hwcap, hwcap2 +} + +func TestARM64VariantFromHWCAP(t *testing.T) { + const fp16 = hwcapFPHP | hwcapASIMDHP + v81, v81hwcap2 := upTo(1) + v82, v82hwcap2 := upTo(2) + v84, v84hwcap2 := upTo(4) + v85, v85hwcap2 := upTo(5) + v89, v89hwcap2 := upTo(len(arm64Levels)) + v83 := arm64Levels[2] + + tests := []struct { + name string + hwcap, hwcap2 uint64 + expected string + }{ + {name: "none", expected: "v8"}, + {name: "v8.1", hwcap: v81, hwcap2: v81hwcap2, expected: "v8.1"}, + {name: "v8.9", hwcap: v89, hwcap2: v89hwcap2, expected: "v8.9"}, + {name: "v8.3 partial", hwcap: v82 | v83.hwcap&(v83.hwcap-1), hwcap2: v82hwcap2 | v83.hwcap2, expected: "v8.2"}, + {name: "v8.3 without v8.2", hwcap: v81 | v83.hwcap, hwcap2: v81hwcap2 | v83.hwcap2, expected: "v8.1"}, + {name: "v9.0", hwcap: v85 | fp16, hwcap2: v85hwcap2 | hwcap2SVE2, expected: "v9"}, + {name: "v9.4", hwcap: v89 | fp16, hwcap2: v89hwcap2 | hwcap2SVE2 | hwcap2SVE2P1, expected: "v9.4"}, + {name: "v9.4 without SVE2.1", hwcap: v89 | fp16, hwcap2: v89hwcap2 | hwcap2SVE2, expected: "v9.3"}, + {name: "SVE2 before v8.5", hwcap: v84 | fp16, hwcap2: v84hwcap2 | hwcap2SVE2, expected: "v8.4"}, + {name: "SVE2 without FP16", hwcap: v85, hwcap2: v85hwcap2 | hwcap2SVE2, expected: "v8.5"}, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + assert.Equal(t, arm64VariantFromHWCAP(tc.hwcap, tc.hwcap2), tc.expected) + }) + } +} + +// Features from /proc/cpuinfo, using names from the kernel. Unknown names +// are not used for the detection. +var cpuinfoHWCAP = map[string]uint64{ + "crc32": hwcapCRC32, + "atomics": hwcapATOMICS, + "fphp": hwcapFPHP, + "asimdhp": hwcapASIMDHP, + "asimdrdm": hwcapASIMDRDM, + "jscvt": hwcapJSCVT, + "fcma": hwcapFCMA, + "lrcpc": hwcapLRCPC, + "dcpop": hwcapDCPOP, + "asimddp": hwcapASIMDDP, + "dit": hwcapDIT, + "uscat": hwcapUSCAT, + "ilrcpc": hwcapILRCPC, + "flagm": hwcapFLAGM, + "sb": hwcapSB, + "paca": hwcapPACA, + "pacg": hwcapPACG, +} + +var cpuinfoHWCAP2 = map[string]uint64{ + "dcpodp": hwcap2DCPODP, + "sve2": hwcap2SVE2, + "sve2p1": hwcap2SVE2P1, + "flagm2": hwcap2FLAGM2, + "frint": hwcap2FRINT, + "i8mm": hwcap2I8MM, + "bf16": hwcap2BF16, + "bti": hwcap2BTI, + "ecv": hwcap2ECV, + "wfxt": hwcap2WFXT, + "cssc": hwcap2CSSC, + "mops": hwcap2MOPS, + "hbc": hwcap2HBC, +} + +// TestARM64VariantFromCPUInfo uses the features reported by QEMU 11.1 for +// each CPU model. +func TestARM64VariantFromCPUInfo(t *testing.T) { + tests := []struct { + cpu string + features string + expected string + }{ + {cpu: "cortex-a35", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a53", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a57", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a72", features: "fp asimd aes pmull sha1 sha2 crc32 cpuid", expected: "v8"}, + {cpu: "cortex-a55", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "cortex-a76", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "cortex-a78ae", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp uscat ilrcpc flagm", expected: "v8.2"}, + {cpu: "neoverse-n1", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm lrcpc dcpop asimddp", expected: "v8.2"}, + {cpu: "a64fx", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm fcma dcpop sve", expected: "v8.2"}, + {cpu: "neoverse-v1", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm paca pacg dcpodp svei8mm svebf16 i8mm rng", expected: "v8.4"}, + {cpu: "cortex-a710", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 bti mte", expected: "v9"}, + {cpu: "neoverse-n2", features: "fp asimd aes pmull sha1 sha2 crc32 atomics fphp asimdhp cpuid asimdrdm jscvt fcma lrcpc dcpop sha3 sm3 sm4 asimddp sha512 sve asimdfhm dit uscat ilrcpc flagm sb paca pacg dcpodp sve2 sveaes svepmull svebitperm svesha3 svesm4 flagm2 frint svei8mm svebf16 i8mm bf16 rng bti mte", expected: "v9"}, + } + for _, tc := range tests { + t.Run(tc.cpu, func(t *testing.T) { + var hwcap, hwcap2 uint64 + for f := range strings.FieldsSeq(tc.features) { + hwcap |= cpuinfoHWCAP[f] + hwcap2 |= cpuinfoHWCAP2[f] + } + assert.Equal(t, arm64VariantFromHWCAP(hwcap, hwcap2), tc.expected) + }) + } +} diff --git a/daemon/internal/platform/arm64variant_other.go b/daemon/internal/platform/arm64variant_other.go new file mode 100644 index 0000000000..45d1bd02bc --- /dev/null +++ b/daemon/internal/platform/arm64variant_other.go @@ -0,0 +1,7 @@ +//go:build !linux + +package platform + +func arm64Variant() string { + return "" +} diff --git a/daemon/internal/platform/spec.go b/daemon/internal/platform/spec.go new file mode 100644 index 0000000000..f56399e241 --- /dev/null +++ b/daemon/internal/platform/spec.go @@ -0,0 +1,22 @@ +package platform + +import ( + "github.com/containerd/platforms" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" + "github.com/tonistiigi/go-archvariant" +) + +// MaximumSpec returns the host platform with the highest CPU variant +// supported by the host. +func MaximumSpec() ocispec.Platform { + p := platforms.DefaultSpec() + switch p.Architecture { + case "amd64": + p.Variant = archvariant.AMD64Variant() + case "arm64": + if v := arm64Variant(); v != "" { + p.Variant = v + } + } + return p +}