daemon/internal: select the maximum arm64 variant supported by the CPU

When an image has sevaral variants for ARM64, pull the maximum one
matching the current CPU. This is similar to what is done for AMD64.

Instead of relying on an external lib, we implement the detection
ourself using ARM64 ELF hwcaps. `golang.org/x/sys/cpu` does not expose
all the flags we need.

ARM v9.x is reported instead of ARM v8.(5+x) if the CPU supports SVE2
and FP16. ARM v9.4 requires an additional feature compared to ARM v8.9,
which is currently hardcoded as an exception. If we want to add support
for ARM v9.5 or later, we would need to add a separate requirement list
as there is no ARM v8.10.

There is a test checking the variants reported for the ARM64 machines
supported by QEMU.

The three copies of maximumSpec() are merged into
platform.MaximumSpec().

See also:
- https://docs.kernel.org/arch/arm64/elf_hwcaps.html
- https://github.com/llvm/llvm-project/blob/main/llvm/lib/Target/AArch64/AArch64Features.td

Signed-off-by: Vincent Bernat <vincent@bernat.ch>
This commit is contained in:
Vincent Bernat committed 2026-10-10 09:40:00 +02:00
1 parent a5a35d0b7d
commit f56e1d95c7
8 files changed
+288 -28

No files matched your search

+2 -6
View File
@@ -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()
}
+2 -11
View File
@@ -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
}
-10
View File
@@ -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
}
+2 -1
View File
@@ -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
}
@@ -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)
})
@@ -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)
})
}
}
@@ -0,0 +1,7 @@
//go:build !linux
package platform
func arm64Variant() string {
return ""
}
+22
View File
@@ -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
}