vmm: raise the (v)CPU limit on kvm/x86_64

Raise the max number of supported (v)CPUs on kvm x86_64 hosts
to 8192 (the max allowed value of CONFIG_NR_CPUS in the Linux kernel).

Other platfroms keep their existing CPU limits pending further
development and testing.

The change has been tested on Intel and AMD hosts.

Signed-off-by: Barret Rhoden <brho@google.com>
Signed-off-by: Neel Natu <neelnatu@google.com>
Signed-off-by: Ofir Weisse <oweisse@google.com>
Signed-off-by: Peter Oskolkov <posk@google.com>
This commit is contained in:
Peter Oskolkov
2025-09-05 20:36:25 +00:00
committed by Bo Chen
parent 57bc78da4f
commit 05d222f0eb
6 changed files with 97 additions and 45 deletions
+25 -5
View File
@@ -27,6 +27,11 @@ use crate::vm_config::*;
const MAX_NUM_PCI_SEGMENTS: u16 = 96; const MAX_NUM_PCI_SEGMENTS: u16 = 96;
const MAX_IOMMU_ADDRESS_WIDTH_BITS: u8 = 64; const MAX_IOMMU_ADDRESS_WIDTH_BITS: u8 = 64;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
const MAX_SUPPORTED_CPUS: u32 = 8192;
#[cfg(not(all(feature = "kvm", target_arch = "x86_64")))]
const MAX_SUPPORTED_CPUS: u32 = 255;
/// Errors associated with VM configuration parameters. /// Errors associated with VM configuration parameters.
#[derive(Debug, Error)] #[derive(Debug, Error)]
pub enum Error { pub enum Error {
@@ -182,6 +187,9 @@ pub enum ValidationError {
/// Max is less than boot /// Max is less than boot
#[error("Max CPUs lower than boot CPUs")] #[error("Max CPUs lower than boot CPUs")]
CpusMaxLowerThanBoot, CpusMaxLowerThanBoot,
/// Too many CPUs.
#[error("Too many CPUs: specified {0} but {MAX_SUPPORTED_CPUS} is the limit")]
TooManyCpus(u32 /* specified CPUs */),
/// Missing file value for debug-console /// Missing file value for debug-console
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
#[error("Path missing when using file mode for debug console")] #[error("Path missing when using file mode for debug console")]
@@ -586,11 +594,11 @@ impl CpusConfig {
.add("features"); .add("features");
parser.parse(cpus).map_err(Error::ParseCpus)?; parser.parse(cpus).map_err(Error::ParseCpus)?;
let boot_vcpus: u8 = parser let boot_vcpus: u32 = parser
.convert("boot") .convert("boot")
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.unwrap_or(DEFAULT_VCPUS); .unwrap_or(DEFAULT_VCPUS);
let max_vcpus: u8 = parser let max_vcpus: u32 = parser
.convert("max") .convert("max")
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.unwrap_or(boot_vcpus); .unwrap_or(boot_vcpus);
@@ -605,7 +613,7 @@ impl CpusConfig {
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.unwrap_or(DEFAULT_MAX_PHYS_BITS); .unwrap_or(DEFAULT_MAX_PHYS_BITS);
let affinity = parser let affinity = parser
.convert::<Tuple<u8, Vec<usize>>>("affinity") .convert::<Tuple<u32, Vec<usize>>>("affinity")
.map_err(Error::ParseCpus)? .map_err(Error::ParseCpus)?
.map(|v| { .map(|v| {
v.0.iter() v.0.iter()
@@ -2147,7 +2155,7 @@ impl NumaConfig {
let cpus = parser let cpus = parser
.convert::<IntegerList>("cpus") .convert::<IntegerList>("cpus")
.map_err(Error::ParseNuma)? .map_err(Error::ParseNuma)?
.map(|v| v.0.iter().map(|e| *e as u8).collect()); .map(|v| v.0.iter().map(|e| *e as u32).collect());
let distances = parser let distances = parser
.convert::<Tuple<u64, u64>>("distances") .convert::<Tuple<u64, u64>>("distances")
.map_err(Error::ParseNuma)? .map_err(Error::ParseNuma)?
@@ -2523,6 +2531,15 @@ impl VmConfig {
return Err(ValidationError::CpusMaxLowerThanBoot); return Err(ValidationError::CpusMaxLowerThanBoot);
} }
if self.cpus.max_vcpus > MAX_SUPPORTED_CPUS {
// Note: historically, Cloud Hypervisor did not support more than 255(254 on x64)
// vCPUs: self.cpus.max_vcpus was of type u8, so 255 was the maximum;
// on x86_64, the legacy mptable/apic was limited to 254 CPUs.
//
// Now the limit is lifted on x86_64 targets. Other targests/archs: TBD.
return Err(ValidationError::TooManyCpus(self.cpus.max_vcpus));
}
if let Some(rate_limit_groups) = &self.rate_limit_groups { if let Some(rate_limit_groups) = &self.rate_limit_groups {
for rate_limit_group in rate_limit_groups { for rate_limit_group in rate_limit_groups {
rate_limit_group.validate(self)?; rate_limit_group.validate(self)?;
@@ -2614,7 +2631,10 @@ impl VmConfig {
return Err(ValidationError::CpuTopologyDiesPerPackage); return Err(ValidationError::CpuTopologyDiesPerPackage);
} }
let total = t.threads_per_core * t.cores_per_die * t.dies_per_package * t.packages; let total: u32 = (t.threads_per_core as u32)
* (t.cores_per_die as u32)
* (t.dies_per_package as u32)
* (t.packages as u32);
if total != self.cpus.max_vcpus { if total != self.cpus.max_vcpus {
return Err(ValidationError::CpuTopologyCount); return Err(ValidationError::CpuTopologyCount);
} }
+30 -26
View File
@@ -196,8 +196,8 @@ pub enum Error {
#[error("Error setting up AMX")] #[error("Error setting up AMX")]
AmxEnable(#[source] anyhow::Error), AmxEnable(#[source] anyhow::Error),
#[error("Maximum number of vCPUs exceeds host limit")] #[error("Maximum number of vCPUs {0} exceeds host limit {1}")]
MaximumVcpusExceeded, MaximumVcpusExceeded(u32, u32),
#[cfg(feature = "sev_snp")] #[cfg(feature = "sev_snp")]
#[error("Failed to set sev control register")] #[error("Failed to set sev control register")]
@@ -698,12 +698,16 @@ impl CpuManager {
numa_nodes: &NumaNodes, numa_nodes: &NumaNodes,
#[cfg(feature = "sev_snp")] sev_snp_enabled: bool, #[cfg(feature = "sev_snp")] sev_snp_enabled: bool,
) -> Result<Arc<Mutex<CpuManager>>> { ) -> Result<Arc<Mutex<CpuManager>>> {
if u32::from(config.max_vcpus) > hypervisor.get_max_vcpus() { if config.max_vcpus > hypervisor.get_max_vcpus() {
return Err(Error::MaximumVcpusExceeded); return Err(Error::MaximumVcpusExceeded(
config.max_vcpus,
hypervisor.get_max_vcpus(),
));
} }
let mut vcpu_states = Vec::with_capacity(usize::from(config.max_vcpus)); let max_vcpus = usize::try_from(config.max_vcpus).unwrap();
vcpu_states.resize_with(usize::from(config.max_vcpus), VcpuState::default); let mut vcpu_states = Vec::with_capacity(max_vcpus);
vcpu_states.resize_with(max_vcpus, VcpuState::default);
let hypervisor_type = hypervisor.hypervisor_type(); let hypervisor_type = hypervisor.hypervisor_type();
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
let cpu_vendor = hypervisor.get_cpu_vendor(); let cpu_vendor = hypervisor.get_cpu_vendor();
@@ -755,7 +759,7 @@ impl CpuManager {
let affinity = if let Some(cpu_affinity) = config.affinity.as_ref() { let affinity = if let Some(cpu_affinity) = config.affinity.as_ref() {
cpu_affinity cpu_affinity
.iter() .iter()
.map(|a| (a.vcpu as u32, a.host_cpus.clone())) .map(|a| (a.vcpu, a.host_cpus.clone()))
.collect() .collect()
} else { } else {
BTreeMap::new() BTreeMap::new()
@@ -781,7 +785,7 @@ impl CpuManager {
#[cfg(feature = "guest_debug")] #[cfg(feature = "guest_debug")]
vm_debug_evt, vm_debug_evt,
selected_cpu: 0, selected_cpu: 0,
vcpus: Vec::with_capacity(usize::from(config.max_vcpus)), vcpus: Vec::with_capacity(max_vcpus),
seccomp_action, seccomp_action,
vm_ops, vm_ops,
acpi_address: None, acpi_address: None,
@@ -895,10 +899,10 @@ impl CpuManager {
}, },
|t| { |t| {
( (
t.threads_per_core.into(), t.threads_per_core,
t.cores_per_die.into(), t.cores_per_die,
t.dies_per_package.into(), t.dies_per_package,
t.packages.into(), t.packages,
) )
}, },
); );
@@ -934,7 +938,7 @@ impl CpuManager {
self.present_vcpus() self.present_vcpus()
); );
if desired_vcpus > self.config.max_vcpus as u32 { if desired_vcpus > self.config.max_vcpus {
return Err(Error::DesiredVCpuCountExceedsMax); return Err(Error::DesiredVCpuCountExceedsMax);
} }
@@ -1245,7 +1249,7 @@ impl CpuManager {
inserting: bool, inserting: bool,
paused: Option<bool>, paused: Option<bool>,
) -> Result<()> { ) -> Result<()> {
if desired_vcpus > self.config.max_vcpus as u32 { if desired_vcpus > self.config.max_vcpus {
return Err(Error::DesiredVCpuCountExceedsMax); return Err(Error::DesiredVCpuCountExceedsMax);
} }
@@ -1418,11 +1422,11 @@ impl CpuManager {
} }
pub fn boot_vcpus(&self) -> u32 { pub fn boot_vcpus(&self) -> u32 {
self.config.boot_vcpus as u32 self.config.boot_vcpus
} }
pub fn max_vcpus(&self) -> u32 { pub fn max_vcpus(&self) -> u32 {
self.config.max_vcpus as u32 self.config.max_vcpus
} }
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
@@ -1456,10 +1460,10 @@ impl CpuManager {
pub fn get_vcpu_topology(&self) -> Option<(u16, u16, u16, u16)> { pub fn get_vcpu_topology(&self) -> Option<(u16, u16, u16, u16)> {
self.config.topology.clone().map(|t| { self.config.topology.clone().map(|t| {
( (
t.threads_per_core.into(), t.threads_per_core,
t.cores_per_die.into(), t.cores_per_die,
t.dies_per_package.into(), t.dies_per_package,
t.packages.into(), t.packages,
) )
}) })
} }
@@ -1475,7 +1479,7 @@ impl CpuManager {
{ {
madt.write(36, arch::layout::APIC_START.0); madt.write(36, arch::layout::APIC_START.0);
for cpu in 0..self.config.max_vcpus as u32 { for cpu in 0..self.config.max_vcpus {
let x2apic_id = get_x2apic_id(cpu, self.get_vcpu_topology()); let x2apic_id = get_x2apic_id(cpu, self.get_vcpu_topology());
let lapic = LocalX2Apic { let lapic = LocalX2Apic {
@@ -1483,7 +1487,7 @@ impl CpuManager {
length: 16, length: 16,
processor_id: cpu, processor_id: cpu,
apic_id: x2apic_id, apic_id: x2apic_id,
flags: if cpu < self.config.boot_vcpus as u32 { flags: if cpu < self.config.boot_vcpus {
1 << MADT_CPU_ENABLE_FLAG 1 << MADT_CPU_ENABLE_FLAG
} else { } else {
0 0
@@ -1535,8 +1539,8 @@ impl CpuManager {
r#type: acpi::ACPI_APIC_GENERIC_CPU_INTERFACE, r#type: acpi::ACPI_APIC_GENERIC_CPU_INTERFACE,
length: 80, length: 80,
reserved0: 0, reserved0: 0,
cpu_interface_number: cpu as u32, cpu_interface_number: cpu,
uid: cpu as u32, uid: cpu,
flags: 1, flags: 1,
parking_version: 0, parking_version: 0,
performance_interrupt: 0, performance_interrupt: 0,
@@ -2274,7 +2278,7 @@ impl Aml for CpuManager {
let uid = aml::Name::new("_CID".into(), &aml::EISAName::new("PNP0A05")); let uid = aml::Name::new("_CID".into(), &aml::EISAName::new("PNP0A05"));
// Bundle methods together under a common object // Bundle methods together under a common object
let methods = CpuMethods { let methods = CpuMethods {
max_vcpus: self.config.max_vcpus as u32, max_vcpus: self.config.max_vcpus,
dynamic: self.dynamic, dynamic: self.dynamic,
}; };
let mut cpu_data_inner: Vec<&dyn Aml> = vec![&hid, &uid, &methods]; let mut cpu_data_inner: Vec<&dyn Aml> = vec![&hid, &uid, &methods];
@@ -2282,7 +2286,7 @@ impl Aml for CpuManager {
#[cfg(target_arch = "x86_64")] #[cfg(target_arch = "x86_64")]
let topology = self.get_vcpu_topology(); let topology = self.get_vcpu_topology();
let mut cpu_devices = Vec::new(); let mut cpu_devices = Vec::new();
for cpu_id in 0..(self.config.max_vcpus as u32) { for cpu_id in 0..self.config.max_vcpus {
let proximity_domain = *self.proximity_domain_per_cpu.get(&cpu_id).unwrap_or(&0); let proximity_domain = *self.proximity_domain_per_cpu.get(&cpu_id).unwrap_or(&0);
let cpu_device = Cpu { let cpu_device = Cpu {
cpu_id, cpu_id,
+1 -1
View File
@@ -1694,7 +1694,7 @@ impl DeviceManager {
) -> DeviceManagerResult<Arc<Mutex<dyn InterruptController>>> { ) -> DeviceManagerResult<Arc<Mutex<dyn InterruptController>>> {
let interrupt_controller: Arc<Mutex<gic::Gic>> = Arc::new(Mutex::new( let interrupt_controller: Arc<Mutex<gic::Gic>> = Arc::new(Mutex::new(
gic::Gic::new( gic::Gic::new(
self.config.lock().unwrap().cpus.boot_vcpus as u32, self.config.lock().unwrap().cpus.boot_vcpus,
Arc::clone(&self.msi_interrupt_manager), Arc::clone(&self.msi_interrupt_manager),
self.address_manager.vm.clone(), self.address_manager.vm.clone(),
) )
+8 -1
View File
@@ -27,6 +27,8 @@ use anyhow::anyhow;
#[cfg(feature = "dbus_api")] #[cfg(feature = "dbus_api")]
use api::dbus::{DBusApiOptions, DBusApiShutdownChannels}; use api::dbus::{DBusApiOptions, DBusApiShutdownChannels};
use api::http::HttpApiHandle; use api::http::HttpApiHandle;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
use arch::x86_64::MAX_SUPPORTED_CPUS_LEGACY;
use console_devices::{pre_create_console_devices, ConsoleInfo}; use console_devices::{pre_create_console_devices, ConsoleInfo};
use landlock::LandlockError; use landlock::LandlockError;
use libc::{tcsetattr, termios, EFD_NONBLOCK, SIGINT, SIGTERM, TCSANOW}; use libc::{tcsetattr, termios, EFD_NONBLOCK, SIGINT, SIGTERM, TCSANOW};
@@ -888,6 +890,11 @@ impl Vmm {
)) ))
})?; })?;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
if config.lock().unwrap().max_apic_id() > MAX_SUPPORTED_CPUS_LEGACY {
vm.enable_x2apic_api().unwrap();
}
let phys_bits = let phys_bits =
vm::physical_bits(&self.hypervisor, config.lock().unwrap().cpus.max_phys_bits); vm::physical_bits(&self.hypervisor, config.lock().unwrap().cpus.max_phys_bits);
@@ -1822,7 +1829,7 @@ impl RequestHandler for Vmm {
} else { } else {
let mut config = self.vm_config.as_ref().unwrap().lock().unwrap(); let mut config = self.vm_config.as_ref().unwrap().lock().unwrap();
if let Some(desired_vcpus) = desired_vcpus { if let Some(desired_vcpus) = desired_vcpus {
config.cpus.boot_vcpus = desired_vcpus.try_into().unwrap(); config.cpus.boot_vcpus = desired_vcpus;
} }
if let Some(desired_ram) = desired_ram { if let Some(desired_ram) = desired_ram {
config.memory.size = desired_ram; config.memory.size = desired_ram;
+10 -3
View File
@@ -29,6 +29,8 @@ use anyhow::anyhow;
use arch::layout::{KVM_IDENTITY_MAP_START, KVM_TSS_START}; use arch::layout::{KVM_IDENTITY_MAP_START, KVM_TSS_START};
#[cfg(feature = "tdx")] #[cfg(feature = "tdx")]
use arch::x86_64::tdx::TdvfSection; use arch::x86_64::tdx::TdvfSection;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
use arch::x86_64::MAX_SUPPORTED_CPUS_LEGACY;
#[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))] #[cfg(any(target_arch = "aarch64", target_arch = "riscv64"))]
use arch::PciSpaceInfo; use arch::PciSpaceInfo;
use arch::{get_host_cpu_phys_bits, EntryPoint, NumaNode, NumaNodes}; use arch::{get_host_cpu_phys_bits, EntryPoint, NumaNode, NumaNodes};
@@ -944,7 +946,7 @@ impl Vm {
} }
if let Some(cpus) = &config.cpus { if let Some(cpus) = &config.cpus {
node.cpus.extend(cpus.iter().map(|cpu| *cpu as u32)); node.cpus.extend(cpus);
} }
if let Some(pci_segments) = &config.pci_segments { if let Some(pci_segments) = &config.pci_segments {
@@ -1022,6 +1024,11 @@ impl Vm {
vm_config.lock().unwrap().memory.total_size(), vm_config.lock().unwrap().memory.total_size(),
)?; )?;
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
if vm_config.lock().unwrap().max_apic_id() > MAX_SUPPORTED_CPUS_LEGACY {
vm.enable_x2apic_api().unwrap();
}
let phys_bits = physical_bits(&hypervisor, vm_config.lock().unwrap().cpus.max_phys_bits); let phys_bits = physical_bits(&hypervisor, vm_config.lock().unwrap().cpus.max_phys_bits);
let memory_manager = if let Some(snapshot) = let memory_manager = if let Some(snapshot) =
@@ -1655,7 +1662,7 @@ impl Vm {
.notify_hotplug(AcpiNotificationFlags::CPU_DEVICES_CHANGED) .notify_hotplug(AcpiNotificationFlags::CPU_DEVICES_CHANGED)
.map_err(Error::DeviceManager)?; .map_err(Error::DeviceManager)?;
} }
self.config.lock().unwrap().cpus.boot_vcpus = desired_vcpus.try_into().unwrap(); self.config.lock().unwrap().cpus.boot_vcpus = desired_vcpus;
} }
if let Some(desired_memory) = desired_memory { if let Some(desired_memory) = desired_memory {
@@ -2709,7 +2716,7 @@ impl Vm {
&mut self, &mut self,
destination_url: &str, destination_url: &str,
) -> std::result::Result<DumpState, GuestDebuggableError> { ) -> std::result::Result<DumpState, GuestDebuggableError> {
let nr_cpus = self.config.lock().unwrap().cpus.boot_vcpus as u32; let nr_cpus = self.config.lock().unwrap().cpus.boot_vcpus;
let elf_note_size = self.get_note_size(NoteDescType::ElfAndVmm, nr_cpus) as isize; let elf_note_size = self.get_note_size(NoteDescType::ElfAndVmm, nr_cpus) as isize;
let mut elf_phdr_num = 1; let mut elf_phdr_num = 1;
let elf_sh_info = 0; let elf_sh_info = 0;
+23 -9
View File
@@ -26,7 +26,7 @@ pub(crate) trait ApplyLandlock {
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpuAffinity { pub struct CpuAffinity {
pub vcpu: u8, pub vcpu: u32,
pub host_cpus: Vec<usize>, pub host_cpus: Vec<usize>,
} }
@@ -39,10 +39,10 @@ pub struct CpuFeatures {
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpuTopology { pub struct CpuTopology {
pub threads_per_core: u8, pub threads_per_core: u16,
pub cores_per_die: u8, pub cores_per_die: u16,
pub dies_per_package: u8, pub dies_per_package: u16,
pub packages: u8, pub packages: u16,
} }
// When booting with PVH boot the maximum physical addressable size // When booting with PVH boot the maximum physical addressable size
@@ -56,8 +56,8 @@ pub fn default_cpuconfig_max_phys_bits() -> u8 {
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
pub struct CpusConfig { pub struct CpusConfig {
pub boot_vcpus: u8, pub boot_vcpus: u32,
pub max_vcpus: u8, pub max_vcpus: u32,
#[serde(default)] #[serde(default)]
pub topology: Option<CpuTopology>, pub topology: Option<CpuTopology>,
#[serde(default)] #[serde(default)]
@@ -70,7 +70,7 @@ pub struct CpusConfig {
pub features: CpuFeatures, pub features: CpuFeatures,
} }
pub const DEFAULT_VCPUS: u8 = 1; pub const DEFAULT_VCPUS: u32 = 1;
impl Default for CpusConfig { impl Default for CpusConfig {
fn default() -> Self { fn default() -> Self {
@@ -684,7 +684,7 @@ pub struct NumaConfig {
#[serde(default)] #[serde(default)]
pub guest_numa_id: u32, pub guest_numa_id: u32,
#[serde(default)] #[serde(default)]
pub cpus: Option<Vec<u8>>, pub cpus: Option<Vec<u32>>,
#[serde(default)] #[serde(default)]
pub distances: Option<Vec<NumaDistance>>, pub distances: Option<Vec<NumaDistance>>,
#[serde(default)] #[serde(default)]
@@ -1035,4 +1035,18 @@ impl VmConfig {
Ok(()) Ok(())
} }
#[cfg(all(feature = "kvm", target_arch = "x86_64"))]
pub(crate) fn max_apic_id(&self) -> u32 {
if let Some(topology) = &self.cpus.topology {
arch::x86_64::get_max_x2apic_id((
topology.threads_per_core,
topology.cores_per_die,
topology.dies_per_package,
topology.packages,
))
} else {
self.cpus.max_vcpus
}
}
} }