diff --git a/include/hw/cpu/cpus.h b/include/hw/cpu/cpus.h
new file mode 100644
index 0000000000..c65f568ef8
--- /dev/null
+++ b/include/hw/cpu/cpus.h
@@ -0,0 +1,71 @@
+/*
+ * QEMU CPUs type
+ *
+ * Copyright (c) 2022 GreenSocs
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#ifndef HW_CPU_CPUS_H
+#define HW_CPU_CPUS_H
+
+#include "qemu/typedefs.h"
+#include "hw/qdev-core.h"
+#include "qom/object.h"
+
+/*
+ * This object represent several CPUs which are all identical.
+ *
+ * If CPUs are not identical (for example, Cortex-A53 and Cortex-A57 CPUs in an
+ * Arm big.LITTLE system) they should be in different groups. If the CPUs do
+ * not have the same view of memory (for example the main CPU and a management
+ * controller processor) they should be in different groups.
+ *
+ * This is an abstract class, subclasses are supposed to be created on
+ * per-architecture basis to handle the specifics of the cpu architecture.
+ * Subclasses are meant to be user-creatable (for cold-plug).
+ */
+
+#define TYPE_CPUS "cpus"
+OBJECT_DECLARE_TYPE(CpusState, CpusClass, CPUS)
+
+/**
+ * CpusState:
+ * @cpu_type: The type of cpu.
+ * @topology.cpus: The number of cpus in this group.
+ *      Explicity put this field into a topology structure in
+ *      order to eventually update this smoothly with a full
+ *      CpuTopology structure in the future.
+ * @cpus: Array of pointer to cpu objects.
+ */
+struct CpusState {
+    /*< private >*/
+    DeviceState parent_obj;
+
+    /*< public >*/
+    char *cpu_type;
+    struct {
+        uint16_t cpus;
+    } topology;
+    CPUState **cpus;
+};
+
+typedef void (*CpusConfigureCpu)(CpusState *s, CPUState *cpu, unsigned idx);
+
+/**
+ * CpusClass:
+ * @base_cpu_type: base cpu type accepted by this cpu group
+ *     (the state cpu_type will be tested against it).
+ * @configure_cpu: method to configure a cpu (called between
+ *     cpu init and realize)
+ * @skip_cpus_creation: CPUCLuster do not rely on creating
+ *     cpus internally. This flag disables this feature.
+ */
+struct CpusClass {
+    DeviceClass parent_class;
+    const char *base_cpu_type;
+    CpusConfigureCpu configure_cpu;
+    bool skip_cpus_creation;
+};
+
+#endif /* HW_CPU_CPUS_H */
diff --git a/hw/cpu/cpus.c b/hw/cpu/cpus.c
new file mode 100644
index 0000000000..5fad1de2c7
--- /dev/null
+++ b/hw/cpu/cpus.c
@@ -0,0 +1,127 @@
+/*
+ * QEMU CPUs type
+ *
+ * Copyright (c) 2022 GreenSocs
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "qemu/osdep.h"
+#include "hw/qdev-properties.h"
+#include "qapi/error.h"
+#include "qemu/module.h"
+#include "qemu/cutils.h"
+#include "hw/cpu/cpus.h"
+#include "hw/core/cpu.h"
+#include "hw/resettable.h"
+#include "sysemu/reset.h"
+
+static Property cpus_properties[] = {
+    DEFINE_PROP_STRING("cpu-type", CpusState, cpu_type),
+    DEFINE_PROP_UINT16("num-cpus", CpusState, topology.cpus, 0),
+    DEFINE_PROP_END_OF_LIST()
+};
+
+static void cpus_reset(Object *obj)
+{
+    CpusState *s = CPUS(obj);
+    for (unsigned i = 0; i < s->topology.cpus; i++) {
+        cpu_reset(s->cpus[i]);
+    }
+}
+
+static void cpus_create_cpus(CpusState *s, Error **errp)
+{
+    Error *err = NULL;
+    CpusClass *cgc = CPUS_GET_CLASS(s);
+    s->cpus = g_new0(CPUState *, s->topology.cpus);
+
+    for (unsigned i = 0; i < s->topology.cpus; i++) {
+        CPUState *cpu = CPU(object_new(s->cpu_type));
+        s->cpus[i] = cpu;
+
+        /* set child property and release the initial ref */
+        object_property_add_child(OBJECT(s), "cpu[*]", OBJECT(cpu));
+        object_unref(OBJECT(cpu));
+
+        /* let subclass configure the cpu */
+        if (cgc->configure_cpu) {
+            cgc->configure_cpu(s, cpu, i);
+        }
+
+        /* finally realize the cpu */
+        qdev_realize(DEVICE(cpu), NULL, &err);
+        if (err) {
+            error_propagate(errp, err);
+            return;
+        }
+    }
+}
+
+static void cpus_realize(DeviceState *dev, Error **errp)
+{
+    CpusState *s = CPUS(dev);
+    CpusClass *cgc = CPUS_GET_CLASS(s);
+
+    /* if subclass defined a base type, let's check it */
+    if (cgc->base_cpu_type &&
+        !object_class_dynamic_cast(object_class_by_name(s->cpu_type),
+                                   cgc->base_cpu_type)) {
+        error_setg(errp, "bad cpu-type '%s' (expected '%s')", s->cpu_type,
+                   cgc->base_cpu_type);
+        return;
+    }
+
+    if (s->topology.cpus == 0) {
+        error_setg(errp, "num-cpus is zero");
+        return;
+    }
+
+    /* create the cpus if needed */
+    if (!cgc->skip_cpus_creation) {
+        cpus_create_cpus(s, errp);
+        qemu_register_reset(resettable_cold_reset_fn, s);
+    }
+}
+
+static void cpus_finalize(Object *obj)
+{
+    CpusState *s = CPUS(obj);
+
+    g_free(s->cpus);
+}
+
+static void cpus_class_init(ObjectClass *klass, void *data)
+{
+    DeviceClass *dc = DEVICE_CLASS(klass);
+    ResettableClass *rc = RESETTABLE_CLASS(klass);
+
+    device_class_set_props(dc, cpus_properties);
+    dc->realize = cpus_realize;
+
+    rc->phases.exit = cpus_reset;
+
+    /*
+     * Subclasses are expected to be user-creatable.
+     * They may provide support to hotplug cpus, but they are
+     * not expected to be hotpluggable themselves.
+     */
+    dc->hotpluggable = false;
+}
+
+static const TypeInfo cpus_type_info = {
+    .name              = TYPE_CPUS,
+    .parent            = TYPE_DEVICE,
+    .abstract          = true,
+    .instance_size     = sizeof(CpusState),
+    .instance_finalize = cpus_finalize,
+    .class_size        = sizeof(CpusClass),
+    .class_init        = cpus_class_init,
+};
+
+static void cpus_register_types(void)
+{
+    type_register_static(&cpus_type_info);
+}
+
+type_init(cpus_register_types)
diff --git a/hw/cpu/meson.build b/hw/cpu/meson.build
index 9e52fee9e7..ca4dda4f88 100644
--- a/hw/cpu/meson.build
+++ b/hw/cpu/meson.build
@@ -1,4 +1,4 @@
-softmmu_ss.add(files('core.c', 'cluster.c'))
+softmmu_ss.add(files('core.c', 'cluster.c', 'cpus.c'))
 
 specific_ss.add(when: 'CONFIG_ARM11MPCORE', if_true: files('arm11mpcore.c'))
 specific_ss.add(when: 'CONFIG_REALVIEW', if_true: files('realview_mpcore.c'))
