From 1ee8bb574010a269bd7b50dae29b425334b5cc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fabiano=20Fid=C3=AAncio?= Date: Tue, 14 Apr 2026 15:02:01 +0200 Subject: [PATCH] runtime: Add NUMA-aware SMP topology MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make cpuTopology() NUMA-aware by accepting a numNUMANodes parameter. When multiple NUMA nodes are configured, restructure the SMP topology so that Sockets=numNUMA and Cores=ceil(maxvcpus/numNUMA), grouping vCPUs by socket per NUMA node. Use ceiling division so that uneven vCPU counts (e.g. the +1 VMM overhead vCPU that Kata adds) produce a QEMU-valid SMP topology where MaxCPUs == Sockets * Cores * Threads. When numNUMANodes <= 1, the existing flat topology (Sockets=maxvcpus, Cores=1) is preserved. Signed-off-by: Fabiano FidĂȘncio Signed-off-by: Zvonko Kaiser --- .../virtcontainers/hypervisor_config_linux.go | 4 -- src/runtime/virtcontainers/qemu.go | 2 +- src/runtime/virtcontainers/qemu_arch_base.go | 26 +++++++++--- .../virtcontainers/qemu_arch_base_test.go | 41 ++++++++++++++++++- 4 files changed, 61 insertions(+), 12 deletions(-) diff --git a/src/runtime/virtcontainers/hypervisor_config_linux.go b/src/runtime/virtcontainers/hypervisor_config_linux.go index 381f3c8f07..1e21e4867c 100644 --- a/src/runtime/virtcontainers/hypervisor_config_linux.go +++ b/src/runtime/virtcontainers/hypervisor_config_linux.go @@ -63,10 +63,6 @@ func validateHypervisorConfig(conf *HypervisorConfig) error { conf.DefaultMaxVCPUs = defaultMaxVCPUs } - if numNUMA := conf.NumGuestNUMANodes(); numNUMA > 1 { - conf.DefaultMaxVCPUs -= conf.DefaultMaxVCPUs % numNUMA - } - if conf.Msize9p == 0 && conf.SharedFS != config.VirtioFS { conf.Msize9p = defaultMsize9p } diff --git a/src/runtime/virtcontainers/qemu.go b/src/runtime/virtcontainers/qemu.go index 4066c85e48..d13e1209c4 100644 --- a/src/runtime/virtcontainers/qemu.go +++ b/src/runtime/virtcontainers/qemu.go @@ -326,7 +326,7 @@ func (q *qemu) setup(ctx context.Context, id string, hypervisorConfig *Hyperviso } func (q *qemu) cpuTopology() govmmQemu.SMP { - return q.arch.cpuTopology(q.config.NumVCPUs(), q.config.DefaultMaxVCPUs) + return q.arch.cpuTopology(q.config.NumVCPUs(), q.config.DefaultMaxVCPUs, q.config.NumGuestNUMANodes()) } func (q *qemu) memoryTopology() (govmmQemu.Memory, error) { diff --git a/src/runtime/virtcontainers/qemu_arch_base.go b/src/runtime/virtcontainers/qemu_arch_base.go index aacb97b7cc..f3bba704ca 100644 --- a/src/runtime/virtcontainers/qemu_arch_base.go +++ b/src/runtime/virtcontainers/qemu_arch_base.go @@ -61,8 +61,9 @@ type qemuArch interface { // bridges sets the number bridges for the machine type bridges(number uint32) - // cpuTopology returns the CPU topology for the given amount of vcpus - cpuTopology(vcpus, maxvcpus uint32) govmmQemu.SMP + // cpuTopology returns the CPU topology for the given amount of vcpus. + // numNUMANodes > 1 restructures the topology so vCPUs are grouped by socket per NUMA node. + cpuTopology(vcpus, maxvcpus uint32, numNUMANodes uint32) govmmQemu.SMP // cpuModel returns the CPU model for the machine type cpuModel() string @@ -324,16 +325,29 @@ func (q *qemuArchBase) bridges(number uint32) { } } -func (q *qemuArchBase) cpuTopology(vcpus, maxvcpus uint32) govmmQemu.SMP { - smp := govmmQemu.SMP{ +func (q *qemuArchBase) cpuTopology(vcpus, maxvcpus uint32, numNUMANodes uint32) govmmQemu.SMP { + if numNUMANodes > 1 { + coresPerSocket := (maxvcpus + numNUMANodes - 1) / numNUMANodes + if coresPerSocket == 0 { + coresPerSocket = 1 + } + smpMaxCPUs := numNUMANodes * coresPerSocket * defaultThreads + return govmmQemu.SMP{ + CPUs: vcpus, + Sockets: numNUMANodes, + Cores: coresPerSocket, + Threads: defaultThreads, + MaxCPUs: smpMaxCPUs, + } + } + + return govmmQemu.SMP{ CPUs: vcpus, Sockets: maxvcpus, Cores: defaultCores, Threads: defaultThreads, MaxCPUs: maxvcpus, } - - return smp } func (q *qemuArchBase) cpuModel() string { diff --git a/src/runtime/virtcontainers/qemu_arch_base_test.go b/src/runtime/virtcontainers/qemu_arch_base_test.go index dfaebb8dab..c177ee44a8 100644 --- a/src/runtime/virtcontainers/qemu_arch_base_test.go +++ b/src/runtime/virtcontainers/qemu_arch_base_test.go @@ -189,7 +189,46 @@ func TestQemuArchBaseCPUTopology(t *testing.T) { MaxCPUs: defaultMaxVCPUs, } - smp := qemuArchBase.cpuTopology(vcpus, defaultMaxVCPUs) + smp := qemuArchBase.cpuTopology(vcpus, defaultMaxVCPUs, 0) + assert.Equal(expectedSMP, smp) +} + +func TestQemuArchBaseCPUTopologyNUMA(t *testing.T) { + assert := assert.New(t) + qemuArchBase := newQemuArchBase() + vcpus := uint32(2) + maxvcpus := uint32(8) + numNUMA := uint32(2) + + expectedSMP := govmmQemu.SMP{ + CPUs: vcpus, + Sockets: numNUMA, + Cores: maxvcpus / numNUMA, + Threads: defaultThreads, + MaxCPUs: maxvcpus, + } + + smp := qemuArchBase.cpuTopology(vcpus, maxvcpus, numNUMA) + assert.Equal(expectedSMP, smp) +} + +func TestQemuArchBaseCPUTopologyNUMAUneven(t *testing.T) { + assert := assert.New(t) + qemuArchBase := newQemuArchBase() + vcpus := uint32(2) + maxvcpus := uint32(5) + numNUMA := uint32(2) + + coresPerSocket := (maxvcpus + numNUMA - 1) / numNUMA + expectedSMP := govmmQemu.SMP{ + CPUs: vcpus, + Sockets: numNUMA, + Cores: coresPerSocket, + Threads: defaultThreads, + MaxCPUs: numNUMA * coresPerSocket * defaultThreads, + } + + smp := qemuArchBase.cpuTopology(vcpus, maxvcpus, numNUMA) assert.Equal(expectedSMP, smp) }