Refactor VM config to support different CPU topologies

This is preliminary work to support crosvm's --host-cpu-topology (WIP),
which will make it possible to mirror host's CPU topology in the guest.
As a first step, we refactor AVF's system API to stop accepting number
of vCPUs as an argument, but instead only expose two topology configs:
1 vCPU (default) and matching the host's CPU topology.

For the time being, the latter results in crosvm started with `--cpu
<nproc>`.

Bug: 266664564
Test: atest -p packages/modules/Virtualization:avf-presubmit
Change-Id: I03a37be0b68b93dc0fa6e84fd51ca3bdefbe6dde
diff --git a/compos/common/compos_client.rs b/compos/common/compos_client.rs
index f6811cb..59fdd9f 100644
--- a/compos/common/compos_client.rs
+++ b/compos/common/compos_client.rs
@@ -22,6 +22,7 @@
     COMPOS_APEX_ROOT, COMPOS_DATA_ROOT, COMPOS_VSOCK_PORT,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
@@ -32,20 +33,29 @@
 use log::{info, warn};
 use rustutils::system_properties;
 use std::fs::{self, File};
-use std::num::NonZeroU32;
 use std::path::{Path, PathBuf};
 use vmclient::{DeathReason, ErrorCode, VmInstance, VmWaitError};
 
 /// This owns an instance of the CompOS VM.
 pub struct ComposClient(VmInstance);
 
+/// CPU topology configuration for a virtual machine.
+#[derive(Default, Debug, Clone)]
+pub enum VmCpuTopology {
+    /// Run VM with 1 vCPU only.
+    #[default]
+    OneCpu,
+    /// Run VM vCPU topology matching that of the host.
+    MatchHost,
+}
+
 /// Parameters to be used when creating a virtual machine instance.
 #[derive(Default, Debug, Clone)]
 pub struct VmParameters {
     /// Whether the VM should be debuggable.
     pub debug_mode: bool,
-    /// Number of vCPUs to have in the VM. If None, defaults to 1.
-    pub cpus: Option<NonZeroU32>,
+    /// CPU topology of the VM. Defaults to 1 vCPU.
+    pub cpu_topology: VmCpuTopology,
     /// List of task profiles to apply to the VM
     pub task_profiles: Vec<String>,
     /// If present, overrides the amount of RAM to give the VM
@@ -111,6 +121,11 @@
             (Some(console_fd), Some(log_fd))
         };
 
+        let cpu_topology = match parameters.cpu_topology {
+            VmCpuTopology::OneCpu => CpuTopology::ONE_CPU,
+            VmCpuTopology::MatchHost => CpuTopology::MATCH_HOST,
+        };
+
         let config = VirtualMachineConfig::AppConfig(VirtualMachineAppConfig {
             name: String::from("Compos"),
             apk: Some(apk_fd),
@@ -122,7 +137,7 @@
             extraIdsigs: extra_idsigs,
             protectedVm: protected_vm,
             memoryMib: parameters.memory_mib.unwrap_or(0), // 0 means use the default
-            numCpus: parameters.cpus.map_or(1, NonZeroU32::get) as i32,
+            cpuTopology: cpu_topology,
             taskProfiles: parameters.task_profiles.clone(),
         });
 
diff --git a/compos/composd/Android.bp b/compos/composd/Android.bp
index 07a9be3..cee4b01 100644
--- a/compos/composd/Android.bp
+++ b/compos/composd/Android.bp
@@ -17,7 +17,6 @@
         "libcompos_common",
         "libcomposd_native_rust",
         "libminijail_rust",
-        "libnum_cpus",
         "libnix",
         "liblibc",
         "liblog_rust",
diff --git a/compos/composd/src/instance_manager.rs b/compos/composd/src/instance_manager.rs
index 0a6c3d6..2db13c7 100644
--- a/compos/composd/src/instance_manager.rs
+++ b/compos/composd/src/instance_manager.rs
@@ -21,9 +21,8 @@
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice;
 use anyhow::{bail, Result};
 use binder::Strong;
-use compos_common::compos_client::VmParameters;
+use compos_common::compos_client::{VmCpuTopology, VmParameters};
 use compos_common::{CURRENT_INSTANCE_DIR, TEST_INSTANCE_DIR};
-use std::num::NonZeroU32;
 use std::sync::{Arc, Mutex, Weak};
 use virtualizationservice::IVirtualizationService::IVirtualizationService;
 
@@ -80,9 +79,14 @@
     // By default, dex2oat starts as many threads as there are CPUs. This can be overridden with
     // a system property. Start the VM with all CPUs and assume the guest will start a suitable
     // number of dex2oat threads.
-    let cpus = NonZeroU32::new(num_cpus::get() as u32);
+    let cpu_topology = VmCpuTopology::MatchHost;
     let task_profiles = vec!["SCHED_SP_COMPUTE".to_string()];
-    Ok(VmParameters { cpus, task_profiles, memory_mib: Some(VM_MEMORY_MIB), ..Default::default() })
+    Ok(VmParameters {
+        cpu_topology,
+        task_profiles,
+        memory_mib: Some(VM_MEMORY_MIB),
+        ..Default::default()
+    })
 }
 
 // Ensures we only run one instance at a time.
diff --git a/compos/verify/verify.rs b/compos/verify/verify.rs
index 528719f..13e9292 100644
--- a/compos/verify/verify.rs
+++ b/compos/verify/verify.rs
@@ -21,7 +21,7 @@
 use anyhow::{bail, Context, Result};
 use binder::ProcessState;
 use clap::{Parser, ValueEnum};
-use compos_common::compos_client::{ComposClient, VmParameters};
+use compos_common::compos_client::{ComposClient, VmCpuTopology, VmParameters};
 use compos_common::odrefresh::{
     CURRENT_ARTIFACTS_SUBDIR, ODREFRESH_OUTPUT_ROOT_DIR, PENDING_ARTIFACTS_SUBDIR,
     TEST_ARTIFACTS_SUBDIR,
@@ -33,7 +33,6 @@
 use log::error;
 use std::fs::File;
 use std::io::Read;
-use std::num::NonZeroU32;
 use std::panic;
 use std::path::Path;
 
@@ -116,7 +115,7 @@
         &idsig_manifest_apk,
         &idsig_manifest_ext_apk,
         &VmParameters {
-            cpus: Some(NonZeroU32::new(1).unwrap()), // This VM runs very little work at boot
+            cpu_topology: VmCpuTopology::OneCpu, // This VM runs very little work at boot
             debug_mode: args.debug,
             ..Default::default()
         },
diff --git a/javalib/api/system-current.txt b/javalib/api/system-current.txt
index b455c85..1ba479f 100644
--- a/javalib/api/system-current.txt
+++ b/javalib/api/system-current.txt
@@ -57,15 +57,17 @@
 
   public final class VirtualMachineConfig {
     method @Nullable public String getApkPath();
-    method @NonNull public int getDebugLevel();
+    method public int getCpuTopology();
+    method public int getDebugLevel();
     method @IntRange(from=0) public long getEncryptedStorageBytes();
     method @IntRange(from=0) public long getMemoryBytes();
-    method @IntRange(from=1) public int getNumCpus();
     method @Nullable public String getPayloadBinaryName();
     method public boolean isCompatibleWith(@NonNull android.system.virtualmachine.VirtualMachineConfig);
     method public boolean isEncryptedStorageEnabled();
     method public boolean isProtectedVm();
     method public boolean isVmOutputCaptured();
+    field public static final int CPU_TOPOLOGY_MATCH_HOST = 1; // 0x1
+    field public static final int CPU_TOPOLOGY_ONE_CPU = 0; // 0x0
     field public static final int DEBUG_LEVEL_FULL = 1; // 0x1
     field public static final int DEBUG_LEVEL_NONE = 0; // 0x0
   }
@@ -74,10 +76,10 @@
     ctor public VirtualMachineConfig.Builder(@NonNull android.content.Context);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig build();
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setApkPath(@NonNull String);
+    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setCpuTopology(int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setDebugLevel(int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setEncryptedStorageBytes(@IntRange(from=1) long);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setMemoryBytes(@IntRange(from=1) long);
-    method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setNumCpus(@IntRange(from=1) int);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setPayloadBinaryName(@NonNull String);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setProtectedVm(boolean);
     method @NonNull public android.system.virtualmachine.VirtualMachineConfig.Builder setVmOutputCaptured(boolean);
diff --git a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
index cb9bad0..5842d01 100644
--- a/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
+++ b/javalib/src/android/system/virtualmachine/VirtualMachineConfig.java
@@ -60,7 +60,7 @@
     private static final String[] EMPTY_STRING_ARRAY = {};
 
     // These define the schema of the config file persisted on disk.
-    private static final int VERSION = 5;
+    private static final int VERSION = 6;
     private static final String KEY_VERSION = "version";
     private static final String KEY_PACKAGENAME = "packageName";
     private static final String KEY_APKPATH = "apkPath";
@@ -70,6 +70,7 @@
     private static final String KEY_PROTECTED_VM = "protectedVm";
     private static final String KEY_MEMORY_BYTES = "memoryBytes";
     private static final String KEY_NUM_CPUS = "numCpus";
+    private static final String KEY_CPU_TOPOLOGY = "cpuTopology";
     private static final String KEY_ENCRYPTED_STORAGE_BYTES = "encryptedStorageBytes";
     private static final String KEY_VM_OUTPUT_CAPTURED = "vmOutputCaptured";
 
@@ -97,6 +98,33 @@
      */
     @SystemApi public static final int DEBUG_LEVEL_FULL = 1;
 
+    /** @hide */
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef(
+            prefix = "CPU_TOPOLOGY_",
+            value = {
+                CPU_TOPOLOGY_ONE_CPU,
+                CPU_TOPOLOGY_MATCH_HOST,
+            })
+    public @interface CpuTopology {}
+
+    /**
+     * Run VM with 1 vCPU. This is the default option, usually the fastest to boot and consuming the
+     * least amount of resources. Typically the best option for small or ephemeral workloads.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_ONE_CPU = 0;
+
+    /**
+     * Run VM with vCPU topology matching the physical CPU topology of the host. Usually takes
+     * longer to boot and cosumes more resources compared to a single vCPU. Typically a good option
+     * for long-running workloads that benefit from parallel execution.
+     *
+     * @hide
+     */
+    @SystemApi public static final int CPU_TOPOLOGY_MATCH_HOST = 1;
+
     /** Name of a package whose primary APK contains the VM payload. */
     @Nullable private final String mPackageName;
 
@@ -116,10 +144,8 @@
      */
     private final long mMemoryBytes;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1 when not specified.
-     */
-    private final int mNumCpus;
+    /** CPU topology configuration of the VM. */
+    @CpuTopology private final int mCpuTopology;
 
     /**
      * Path within the APK to the payload config file that defines software aspects of the VM.
@@ -143,7 +169,7 @@
             @DebugLevel int debugLevel,
             boolean protectedVm,
             long memoryBytes,
-            int numCpus,
+            @CpuTopology int cpuTopology,
             long encryptedStorageBytes,
             boolean vmOutputCaptured) {
         // This is only called from Builder.build(); the builder handles parameter validation.
@@ -154,7 +180,7 @@
         mDebugLevel = debugLevel;
         mProtectedVm = protectedVm;
         mMemoryBytes = memoryBytes;
-        mNumCpus = numCpus;
+        mCpuTopology = cpuTopology;
         mEncryptedStorageBytes = encryptedStorageBytes;
         mVmOutputCaptured = vmOutputCaptured;
     }
@@ -225,7 +251,7 @@
         if (memoryBytes != 0) {
             builder.setMemoryBytes(memoryBytes);
         }
-        builder.setNumCpus(b.getInt(KEY_NUM_CPUS));
+        builder.setCpuTopology(b.getInt(KEY_CPU_TOPOLOGY));
         long encryptedStorageBytes = b.getLong(KEY_ENCRYPTED_STORAGE_BYTES);
         if (encryptedStorageBytes != 0) {
             builder.setEncryptedStorageBytes(encryptedStorageBytes);
@@ -258,7 +284,7 @@
         b.putString(KEY_PAYLOADBINARYNAME, mPayloadBinaryName);
         b.putInt(KEY_DEBUGLEVEL, mDebugLevel);
         b.putBoolean(KEY_PROTECTED_VM, mProtectedVm);
-        b.putInt(KEY_NUM_CPUS, mNumCpus);
+        b.putInt(KEY_CPU_TOPOLOGY, mCpuTopology);
         if (mMemoryBytes > 0) {
             b.putLong(KEY_MEMORY_BYTES, mMemoryBytes);
         }
@@ -312,7 +338,6 @@
      * @hide
      */
     @SystemApi
-    @NonNull
     @DebugLevel
     public int getDebugLevel() {
         return mDebugLevel;
@@ -341,14 +366,14 @@
     }
 
     /**
-     * Returns the number of vCPUs that the VM will have.
+     * Returns the CPU topology configuration of the VM.
      *
      * @hide
      */
     @SystemApi
-    @IntRange(from = 1)
-    public int getNumCpus() {
-        return mNumCpus;
+    @CpuTopology
+    public int getCpuTopology() {
+        return mCpuTopology;
     }
 
     /**
@@ -455,7 +480,14 @@
         }
         vsConfig.protectedVm = mProtectedVm;
         vsConfig.memoryMib = bytesToMebiBytes(mMemoryBytes);
-        vsConfig.numCpus = mNumCpus;
+        switch (mCpuTopology) {
+            case CPU_TOPOLOGY_MATCH_HOST:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.MATCH_HOST;
+                break;
+            default:
+                vsConfig.cpuTopology = android.system.virtualizationservice.CpuTopology.ONE_CPU;
+                break;
+        }
         // Don't allow apps to set task profiles ... at least for now.
         vsConfig.taskProfiles = EMPTY_STRING_ARRAY;
         return vsConfig;
@@ -486,7 +518,7 @@
         private boolean mProtectedVm;
         private boolean mProtectedVmSet;
         private long mMemoryBytes;
-        private int mNumCpus = 1;
+        @CpuTopology private int mCpuTopology = CPU_TOPOLOGY_ONE_CPU;
         private long mEncryptedStorageBytes;
         private boolean mVmOutputCaptured = false;
 
@@ -555,7 +587,7 @@
                     mDebugLevel,
                     mProtectedVm,
                     mMemoryBytes,
-                    mNumCpus,
+                    mCpuTopology,
                     mEncryptedStorageBytes,
                     mVmOutputCaptured);
         }
@@ -687,25 +719,21 @@
         }
 
         /**
-         * Sets the number of vCPUs in the VM. Defaults to 1. Cannot be more than the number of real
-         * CPUs (as returned by {@link Runtime#availableProcessors}).
+         * Sets the CPU topology configuration of the VM. Defaults to {@link #CPU_TOPOLOGY_ONE_CPU}.
+         *
+         * <p>This determines how many virtual CPUs will be created, and their performance and
+         * scheduling characteristics, such as affinity masks. Topology also has an effect on memory
+         * usage as each vCPU requires additional memory to keep its state.
          *
          * @hide
          */
         @SystemApi
         @NonNull
-        public Builder setNumCpus(@IntRange(from = 1) int numCpus) {
-            int availableCpus = Runtime.getRuntime().availableProcessors();
-            if (numCpus < 1 || numCpus > availableCpus) {
-                throw new IllegalArgumentException(
-                        "Number of vCPUs ("
-                                + numCpus
-                                + ") is out of "
-                                + "range [1, "
-                                + availableCpus
-                                + "]");
+        public Builder setCpuTopology(@CpuTopology int cpuTopology) {
+            if (cpuTopology != CPU_TOPOLOGY_ONE_CPU && cpuTopology != CPU_TOPOLOGY_MATCH_HOST) {
+                throw new IllegalArgumentException("Invalid cpuTopology: " + cpuTopology);
             }
-            mNumCpus = numCpus;
+            mCpuTopology = cpuTopology;
             return this;
         }
 
diff --git a/rialto/tests/test.rs b/rialto/tests/test.rs
index b25034f..04a87f3 100644
--- a/rialto/tests/test.rs
+++ b/rialto/tests/test.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::{
-        VirtualMachineConfig::VirtualMachineConfig,
+        CpuTopology::CpuTopology, VirtualMachineConfig::VirtualMachineConfig,
         VirtualMachineRawConfig::VirtualMachineRawConfig,
     },
     binder::{ParcelFileDescriptor, ProcessState},
@@ -65,7 +65,7 @@
         disks: vec![],
         protectedVm: false,
         memoryMib: 300,
-        numCpus: 1,
+        cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
     });
diff --git a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
index db87126..e979f30 100644
--- a/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
+++ b/tests/benchmark/src/java/com/android/microdroid/benchmark/MicrodroidBenchmarks.java
@@ -16,6 +16,8 @@
 
 package com.android.microdroid.benchmark;
 
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 
@@ -190,21 +192,21 @@
     @Test
     public void testMicrodroidBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
-        BootTimeStats stats = runBootTimeTest("test_vm_boot_time", (builder) -> builder);
+        BootTimeStats stats =
+                runBootTimeTest(
+                        "test_vm_boot_time",
+                        (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_ONE_CPU));
         reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
     }
 
     @Test
-    public void testMicrodroidMulticoreBootTime()
+    public void testMicrodroidHostCpuTopologyBootTime()
             throws VirtualMachineException, InterruptedException, IOException {
-        for (int numCpus : new int[] {2, 4, 8}) {
-            BootTimeStats stats =
-                    runBootTimeTest(
-                            "test_vm_boot_time_multicore",
-                            (builder) -> builder.setNumCpus(numCpus));
-            String metricName = "boot_time_" + numCpus + "cpus";
-            reportMetrics(stats.get(BootTimeMetric.TOTAL), metricName, "ms");
-        }
+        BootTimeStats stats =
+                runBootTimeTest(
+                        "test_vm_boot_time_host_topology",
+                        (builder) -> builder.setCpuTopology(CPU_TOPOLOGY_MATCH_HOST));
+        reportMetrics(stats.get(BootTimeMetric.TOTAL), "boot_time", "ms");
     }
 
     @Test
diff --git a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
index c47e915..2591fce 100644
--- a/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
+++ b/tests/benchmark_hostside/java/android/avf/test/AVFHostTestCase.java
@@ -79,7 +79,6 @@
     private static final int ROUND_IGNORE_STARTUP_TIME = 3;
     private static final String APK_NAME = "MicrodroidTestApp.apk";
     private static final String PACKAGE_NAME = "com.android.microdroid.test";
-    private static final int NUM_VCPUS = 3;
 
     private MetricsProcessor mMetricsProcessor;
     @Rule public TestMetrics mMetrics = new TestMetrics();
@@ -247,7 +246,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(vm_mem_mb)
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(device);
         microdroidDevice.waitForBootComplete(30000);
         microdroidDevice.enableAdbRoot();
diff --git a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
index d40dcba..a780a8b 100644
--- a/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
+++ b/tests/hostside/java/com/android/microdroid/test/MicrodroidHostTests.java
@@ -92,9 +92,6 @@
     private static final int MIN_MEM_ARM64 = 145;
     private static final int MIN_MEM_X86_64 = 196;
 
-    // Number of vCPUs for testing purpose
-    private static final int NUM_VCPUS = 3;
-
     private static final int BOOT_COMPLETE_TIMEOUT = 30000; // 30 seconds
 
     private static final Pattern sCIDPattern = Pattern.compile("with CID (\\d+)");
@@ -222,6 +219,14 @@
         assertThat(callable.call(), matcher);
     }
 
+    private int getDeviceNumCpus(CommandRunner runner) throws DeviceNotAvailableException {
+        return Integer.parseInt(runner.run("nproc --all").trim());
+    }
+
+    private int getDeviceNumCpus(ITestDevice device) throws DeviceNotAvailableException {
+        return getDeviceNumCpus(new CommandRunner(device));
+    }
+
     static class ActiveApexInfo {
         public String name;
         public String path;
@@ -443,7 +448,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .protectedVm(protectedVm)
                         .build(getAndroidDevice());
 
@@ -554,7 +559,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
@@ -657,7 +662,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(device);
         microdroid.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         device.shutdownMicrodroid(microdroid);
@@ -685,7 +690,7 @@
         assertThat(atomVmCreationRequested.getVmIdentifier()).isEqualTo("VmRunApp");
         assertThat(atomVmCreationRequested.getConfigType())
                 .isEqualTo(AtomsProto.VmCreationRequested.ConfigType.VIRTUAL_MACHINE_APP_CONFIG);
-        assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(NUM_VCPUS);
+        assertThat(atomVmCreationRequested.getNumCpus()).isEqualTo(getDeviceNumCpus(device));
         assertThat(atomVmCreationRequested.getMemoryMib()).isEqualTo(minMemorySize());
         assertThat(atomVmCreationRequested.getApexes())
                 .isEqualTo("com.android.art:com.android.compos:com.android.sdkext");
@@ -720,7 +725,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         CommandRunner microdroid = new CommandRunner(mMicrodroidDevice);
@@ -749,8 +754,7 @@
         assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", LOG_PATH)).isNull();
         assertThat(android.tryRun("egrep", "'avc:[[:space:]]{1,2}denied'", CONSOLE_PATH)).isNull();
 
-        assertThat(microdroid.run("cat /proc/cpuinfo | grep processor | wc -l"))
-                .isEqualTo(Integer.toString(NUM_VCPUS));
+        assertThat(getDeviceNumCpus(microdroid)).isEqualTo(getDeviceNumCpus(android));
 
         // Check that selinux is enabled
         assertThat(microdroid.run("getenforce")).isEqualTo("Enforcing");
@@ -786,7 +790,7 @@
                 MicrodroidBuilder.fromDevicePath(getPathForPackage(PACKAGE_NAME), configPath)
                         .debugLevel("full")
                         .memoryMib(minMemorySize())
-                        .numCpus(NUM_VCPUS)
+                        .cpuTopology("match_host")
                         .build(getAndroidDevice());
         mMicrodroidDevice.waitForBootComplete(BOOT_COMPLETE_TIMEOUT);
         mMicrodroidDevice.enableAdbRoot();
diff --git a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
index 984b10b..c31bf52 100644
--- a/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
+++ b/tests/testapk/src/java/com/android/microdroid/test/MicrodroidTests.java
@@ -18,6 +18,8 @@
 import static android.system.virtualmachine.VirtualMachine.STATUS_DELETED;
 import static android.system.virtualmachine.VirtualMachine.STATUS_RUNNING;
 import static android.system.virtualmachine.VirtualMachine.STATUS_STOPPED;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_ONE_CPU;
+import static android.system.virtualmachine.VirtualMachineConfig.CPU_TOPOLOGY_MATCH_HOST;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_FULL;
 import static android.system.virtualmachine.VirtualMachineConfig.DEBUG_LEVEL_NONE;
 import static android.system.virtualmachine.VirtualMachineManager.CAPABILITY_NON_PROTECTED_VM;
@@ -348,7 +350,7 @@
         assertThat(minimal.getApkPath()).isNull();
         assertThat(minimal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_NONE);
         assertThat(minimal.getMemoryBytes()).isEqualTo(0);
-        assertThat(minimal.getNumCpus()).isEqualTo(1);
+        assertThat(minimal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_ONE_CPU);
         assertThat(minimal.getPayloadBinaryName()).isEqualTo("binary.so");
         assertThat(minimal.getPayloadConfigPath()).isNull();
         assertThat(minimal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -364,9 +366,9 @@
                         .setProtectedVm(mProtectedVm)
                         .setPayloadConfigPath("config/path")
                         .setApkPath("/apk/path")
-                        .setNumCpus(maxCpus)
                         .setDebugLevel(DEBUG_LEVEL_FULL)
                         .setMemoryBytes(42)
+                        .setCpuTopology(CPU_TOPOLOGY_MATCH_HOST)
                         .setEncryptedStorageBytes(1_000_000)
                         .setVmOutputCaptured(true);
         VirtualMachineConfig maximal = maximalBuilder.build();
@@ -374,7 +376,7 @@
         assertThat(maximal.getApkPath()).isEqualTo("/apk/path");
         assertThat(maximal.getDebugLevel()).isEqualTo(DEBUG_LEVEL_FULL);
         assertThat(maximal.getMemoryBytes()).isEqualTo(42);
-        assertThat(maximal.getNumCpus()).isEqualTo(maxCpus);
+        assertThat(maximal.getCpuTopology()).isEqualTo(CPU_TOPOLOGY_MATCH_HOST);
         assertThat(maximal.getPayloadBinaryName()).isNull();
         assertThat(maximal.getPayloadConfigPath()).isEqualTo("config/path");
         assertThat(maximal.isProtectedVm()).isEqualTo(isProtectedVm());
@@ -406,7 +408,7 @@
                 IllegalArgumentException.class, () -> builder.setPayloadBinaryName("dir/file.so"));
         assertThrows(IllegalArgumentException.class, () -> builder.setDebugLevel(-1));
         assertThrows(IllegalArgumentException.class, () -> builder.setMemoryBytes(0));
-        assertThrows(IllegalArgumentException.class, () -> builder.setNumCpus(0));
+        assertThrows(IllegalArgumentException.class, () -> builder.setCpuTopology(-1));
         assertThrows(IllegalArgumentException.class, () -> builder.setEncryptedStorageBytes(0));
 
         // Consistency checks enforced at build time.
@@ -431,8 +433,6 @@
     @Test
     @CddTest(requirements = {"9.17/C-1-1"})
     public void compatibleConfigTests() {
-        int maxCpus = Runtime.getRuntime().availableProcessors();
-
         VirtualMachineConfig baseline = newBaselineBuilder().build();
 
         // A config must be compatible with itself
@@ -440,9 +440,9 @@
 
         // Changes that must always be compatible
         assertConfigCompatible(baseline, newBaselineBuilder().setMemoryBytes(99)).isTrue();
-        if (maxCpus > 1) {
-            assertConfigCompatible(baseline, newBaselineBuilder().setNumCpus(2)).isTrue();
-        }
+        assertConfigCompatible(
+                        baseline, newBaselineBuilder().setCpuTopology(CPU_TOPOLOGY_MATCH_HOST))
+                .isTrue();
 
         // Changes that must be incompatible, since they must change the VM identity.
         assertConfigCompatible(baseline, newBaselineBuilder().setDebugLevel(DEBUG_LEVEL_FULL))
diff --git a/virtualizationmanager/src/aidl.rs b/virtualizationmanager/src/aidl.rs
index 678c91f..89f74d6 100644
--- a/virtualizationmanager/src/aidl.rs
+++ b/virtualizationmanager/src/aidl.rs
@@ -27,6 +27,7 @@
     ErrorCode::ErrorCode,
 };
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     DiskImage::DiskImage,
     IVirtualMachine::{BnVirtualMachine, IVirtualMachine},
     IVirtualMachineCallback::IVirtualMachineCallback,
@@ -384,6 +385,18 @@
             })
             .collect::<Result<Vec<DiskFile>, _>>()?;
 
+        let (cpus, host_cpu_topology) = match config.cpuTopology {
+            CpuTopology::MATCH_HOST => (None, true),
+            CpuTopology::ONE_CPU => (NonZeroU32::new(1), false),
+            val => {
+                error!("Unexpected value of CPU topology: {:?}", val);
+                return Err(Status::new_service_specific_error_str(
+                    -1,
+                    Some(format!("Failed to parse CPU topology value: {:?}", val)),
+                ));
+            }
+        };
+
         // Creating this ramdump file unconditionally is not harmful as ramdump will be created
         // only when the VM is configured as such. `ramdump_write` is sent to crosvm and will
         // be the backing store for the /dev/hvc1 where VM will emit ramdump to. `ramdump_read`
@@ -408,7 +421,8 @@
             params: config.params.to_owned(),
             protected: *is_protected,
             memory_mib: config.memoryMib.try_into().ok().and_then(NonZeroU32::new),
-            cpus: config.numCpus.try_into().ok().and_then(NonZeroU32::new),
+            cpus,
+            host_cpu_topology,
             task_profiles: config.taskProfiles.clone(),
             console_fd,
             log_fd,
@@ -567,7 +581,7 @@
 
     vm_config.name = config.name.clone();
     vm_config.protectedVm = config.protectedVm;
-    vm_config.numCpus = config.numCpus;
+    vm_config.cpuTopology = config.cpuTopology;
     vm_config.taskProfiles = config.taskProfiles.clone();
 
     // Microdroid takes additional init ramdisk & (optionally) storage image
diff --git a/virtualizationmanager/src/atom.rs b/virtualizationmanager/src/atom.rs
index c33f262..567fce9 100644
--- a/virtualizationmanager/src/atom.rs
+++ b/virtualizationmanager/src/atom.rs
@@ -19,6 +19,7 @@
 use crate::get_calling_uid;
 use android_system_virtualizationcommon::aidl::android::system::virtualizationcommon::DeathReason::DeathReason;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualMachine::IVirtualMachine,
     VirtualMachineAppConfig::{Payload::Payload, VirtualMachineAppConfig},
     VirtualMachineConfig::VirtualMachineConfig,
@@ -38,6 +39,8 @@
 use std::time::{Duration, SystemTime};
 use zip::ZipArchive;
 
+const INVALID_NUM_CPUS: i32 = -1;
+
 fn get_apex_list(config: &VirtualMachineAppConfig) -> String {
     match &config.payload {
         Payload::PayloadConfig(_) => String::new(),
@@ -76,6 +79,19 @@
     }
 }
 
+// Returns the number of CPUs configured in the host system.
+// This matches how crosvm determines the number of logical cores.
+// For telemetry purposes only.
+pub(crate) fn get_num_cpus() -> Option<usize> {
+    // SAFETY - Only integer constants passed back and forth.
+    let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_CONF) };
+    if ret > 0 {
+        ret.try_into().ok()
+    } else {
+        None
+    }
+}
+
 /// Write the stats of VMCreation to statsd
 pub fn write_vm_creation_stats(
     config: &VirtualMachineConfig,
@@ -94,23 +110,33 @@
             binder_exception_code = e.exception_code() as i32;
         }
     }
-    let (vm_identifier, config_type, num_cpus, memory_mib, apexes) = match config {
+    let (vm_identifier, config_type, cpu_topology, memory_mib, apexes) = match config {
         VirtualMachineConfig::AppConfig(config) => (
             config.name.clone(),
             vm_creation_requested::ConfigType::VirtualMachineAppConfig,
-            config.numCpus,
+            config.cpuTopology,
             config.memoryMib,
             get_apex_list(config),
         ),
         VirtualMachineConfig::RawConfig(config) => (
             config.name.clone(),
             vm_creation_requested::ConfigType::VirtualMachineRawConfig,
-            config.numCpus,
+            config.cpuTopology,
             config.memoryMib,
             String::new(),
         ),
     };
 
+    let num_cpus: i32 = match cpu_topology {
+        CpuTopology::MATCH_HOST => {
+            get_num_cpus().and_then(|v| v.try_into().ok()).unwrap_or_else(|| {
+                warn!("Failed to determine the number of CPUs in the host");
+                INVALID_NUM_CPUS
+            })
+        }
+        _ => 1,
+    };
+
     let atom = AtomVmCreationRequested {
         uid: get_calling_uid() as i32,
         vmIdentifier: vm_identifier,
diff --git a/virtualizationmanager/src/crosvm.rs b/virtualizationmanager/src/crosvm.rs
index 19d862a..ea1146e 100644
--- a/virtualizationmanager/src/crosvm.rs
+++ b/virtualizationmanager/src/crosvm.rs
@@ -15,7 +15,7 @@
 //! Functions for running instances of `crosvm`.
 
 use crate::aidl::{remove_temporary_files, Cid, VirtualMachineCallbacks};
-use crate::atom::write_vm_exited_stats;
+use crate::atom::{get_num_cpus, write_vm_exited_stats};
 use anyhow::{anyhow, bail, Context, Error, Result};
 use command_fds::CommandFdExt;
 use lazy_static::lazy_static;
@@ -97,6 +97,7 @@
     pub protected: bool,
     pub memory_mib: Option<NonZeroU32>,
     pub cpus: Option<NonZeroU32>,
+    pub host_cpu_topology: bool,
     pub task_profiles: Vec<String>,
     pub console_fd: Option<File>,
     pub log_fd: Option<File>,
@@ -732,6 +733,15 @@
         command.arg("--cpus").arg(cpus.to_string());
     }
 
+    if config.host_cpu_topology {
+        // TODO(b/266664564): replace with --host-cpu-topology once available
+        if let Some(cpus) = get_num_cpus() {
+            command.arg("--cpus").arg(cpus.to_string());
+        } else {
+            bail!("Could not determine the number of CPUs in the system");
+        }
+    }
+
     if !config.task_profiles.is_empty() {
         command.arg("--task-profiles").arg(config.task_profiles.join(","));
     }
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
new file mode 100644
index 0000000..8a8e3d0
--- /dev/null
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/CpuTopology.aidl
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package android.system.virtualizationservice;
+
+/** The vCPU topology that will be generated for the VM. */
+@Backing(type="byte")
+enum CpuTopology {
+    /** One vCPU */
+    ONE_CPU = 0,
+    /** Match physical CPU topology of the host. */
+    MATCH_HOST = 1,
+}
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
index 884561d..7f90ed6 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineAppConfig.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualizationservice;
 
+import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.VirtualMachinePayloadConfig;
 
 /** Configuration for running an App in a VM */
@@ -78,10 +79,8 @@
      */
     int memoryMib;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1.
-     */
-    int numCpus = 1;
+    /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+    CpuTopology cpuTopology = CpuTopology.ONE_CPU;
 
     /**
      * List of task profile names to apply for the VM
diff --git a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
index 993bbb0..1cda163 100644
--- a/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
+++ b/virtualizationservice/aidl/android/system/virtualizationservice/VirtualMachineRawConfig.aidl
@@ -15,6 +15,7 @@
  */
 package android.system.virtualizationservice;
 
+import android.system.virtualizationservice.CpuTopology;
 import android.system.virtualizationservice.DiskImage;
 
 /** Raw configuration for running a VM. */
@@ -49,10 +50,8 @@
     /** The amount of RAM to give the VM, in MiB. 0 or negative to use the default. */
     int memoryMib;
 
-    /**
-     * Number of vCPUs in the VM. Defaults to 1.
-     */
-    int numCpus = 1;
+    /** The vCPU topology that will be generated for the VM. Default to 1 vCPU. */
+    CpuTopology cpuTopology = CpuTopology.ONE_CPU;
 
     /**
      * A version or range of versions of the virtual platform that this config is compatible with.
diff --git a/vm/src/main.rs b/vm/src/main.rs
index ea744f7..e1c3413 100644
--- a/vm/src/main.rs
+++ b/vm/src/main.rs
@@ -19,8 +19,8 @@
 mod run;
 
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
-    IVirtualizationService::IVirtualizationService, PartitionType::PartitionType,
-    VirtualMachineAppConfig::DebugLevel::DebugLevel,
+    CpuTopology::CpuTopology, IVirtualizationService::IVirtualizationService,
+    PartitionType::PartitionType, VirtualMachineAppConfig::DebugLevel::DebugLevel,
 };
 use anyhow::{Context, Error};
 use binder::ProcessState;
@@ -91,9 +91,9 @@
         #[clap(short, long)]
         mem: Option<u32>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
@@ -146,9 +146,9 @@
         #[clap(short, long)]
         mem: Option<u32>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
@@ -163,9 +163,9 @@
         #[clap(long)]
         name: Option<String>,
 
-        /// Number of vCPUs in the VM. If unspecified, defaults to 1.
-        #[clap(long)]
-        cpus: Option<u32>,
+        /// Run VM with vCPU topology matching that of the host. If unspecified, defaults to 1 vCPU.
+        #[clap(long, default_value = "one_cpu", value_parser = parse_cpu_topology)]
+        cpu_topology: CpuTopology,
 
         /// Comma separated list of task profile names to apply to the VM
         #[clap(long)]
@@ -222,6 +222,14 @@
     }
 }
 
+fn parse_cpu_topology(s: &str) -> Result<CpuTopology, String> {
+    match s {
+        "one_cpu" => Ok(CpuTopology::ONE_CPU),
+        "match_host" => Ok(CpuTopology::MATCH_HOST),
+        _ => Err(format!("Invalid cpu topology {}", s)),
+    }
+}
+
 fn main() -> Result<(), Error> {
     env_logger::init();
     let opt = Opt::parse();
@@ -248,7 +256,7 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
             extra_idsigs,
         } => command_run_app(
@@ -266,7 +274,7 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
             &extra_idsigs,
         ),
@@ -280,7 +288,7 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
         } => command_run_microdroid(
             name,
@@ -293,10 +301,10 @@
             debug,
             protected,
             mem,
-            cpus,
+            cpu_topology,
             task_profiles,
         ),
-        Opt::Run { name, config, cpus, task_profiles, console, log } => {
+        Opt::Run { name, config, cpu_topology, task_profiles, console, log } => {
             command_run(
                 name,
                 service.as_ref(),
@@ -304,7 +312,7 @@
                 console.as_deref(),
                 log.as_deref(),
                 /* mem */ None,
-                cpus,
+                cpu_topology,
                 task_profiles,
             )
         }
diff --git a/vm/src/run.rs b/vm/src/run.rs
index e229933..fa84591 100644
--- a/vm/src/run.rs
+++ b/vm/src/run.rs
@@ -16,6 +16,7 @@
 
 use crate::create_partition::command_create_partition;
 use android_system_virtualizationservice::aidl::android::system::virtualizationservice::{
+    CpuTopology::CpuTopology,
     IVirtualizationService::IVirtualizationService,
     PartitionType::PartitionType,
     VirtualMachineAppConfig::{DebugLevel::DebugLevel, Payload::Payload, VirtualMachineAppConfig},
@@ -54,7 +55,7 @@
     debug_level: DebugLevel,
     protected: bool,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
     extra_idsigs: &[PathBuf],
 ) -> Result<(), Error> {
@@ -141,7 +142,7 @@
         debugLevel: debug_level,
         protectedVm: protected,
         memoryMib: mem.unwrap_or(0) as i32, // 0 means use the VM default
-        numCpus: cpus.unwrap_or(1) as i32,
+        cpuTopology: cpu_topology,
         taskProfiles: task_profiles,
     });
     run(service, &config, &payload_config_str, console_path, log_path)
@@ -182,7 +183,7 @@
     debug_level: DebugLevel,
     protected: bool,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
 ) -> Result<(), Error> {
     let apk = find_empty_payload_apk_path()?;
@@ -211,7 +212,7 @@
         debug_level,
         protected,
         mem,
-        cpus,
+        cpu_topology,
         task_profiles,
         &extra_sig,
     )
@@ -226,7 +227,7 @@
     console_path: Option<&Path>,
     log_path: Option<&Path>,
     mem: Option<u32>,
-    cpus: Option<u32>,
+    cpu_topology: CpuTopology,
     task_profiles: Vec<String>,
 ) -> Result<(), Error> {
     let config_file = File::open(config_path).context("Failed to open config file")?;
@@ -235,14 +236,12 @@
     if let Some(mem) = mem {
         config.memoryMib = mem as i32;
     }
-    if let Some(cpus) = cpus {
-        config.numCpus = cpus as i32;
-    }
     if let Some(name) = name {
         config.name = name;
     } else {
         config.name = String::from("VmRun");
     }
+    config.cpuTopology = cpu_topology;
     config.taskProfiles = task_profiles;
     run(
         service,
diff --git a/vmbase/example/tests/test.rs b/vmbase/example/tests/test.rs
index c6aea8c..cfb0225 100644
--- a/vmbase/example/tests/test.rs
+++ b/vmbase/example/tests/test.rs
@@ -16,7 +16,7 @@
 
 use android_system_virtualizationservice::{
     aidl::android::system::virtualizationservice::{
-        DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
+        CpuTopology::CpuTopology, DiskImage::DiskImage, VirtualMachineConfig::VirtualMachineConfig,
         VirtualMachineRawConfig::VirtualMachineRawConfig,
     },
     binder::{ParcelFileDescriptor, ProcessState},
@@ -84,7 +84,7 @@
         disks: vec![disk_image],
         protectedVm: false,
         memoryMib: 300,
-        numCpus: 1,
+        cpuTopology: CpuTopology::ONE_CPU,
         platformVersion: "~1.0".to_string(),
         taskProfiles: vec![],
     });