public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Dominik Csapak <d.csapak@proxmox.com>
To: Elias Huhsovitz <e.huhsovitz@proxmox.com>, pve-devel@lists.proxmox.com
Subject: Re: [PATCH qemu-server v3 3/3] cpu: add CPU binding
Date: Fri, 30 Oct 2026 14:46:52 +0100	[thread overview]
Message-ID: <447abbd2-ee7b-49b0-9ebc-d7c52c76b9d1@proxmox.com> (raw)
In-Reply-To: <20261022092227.58871-4-e.huhsovitz@proxmox.com>

some comments inline

On 10/22/26 11:22 AM, Elias Huhsovitz wrote:
> Add two new VM config options:
> * cpu-binding-mode  none | manual | one-to-one | numa | numa-manual
> * cpu-binding-cfg   mode-dependent syntax
> 
> When cpu-binding-mode is unset or "none", behavior is unchanged.
> Otherwise, vCPU threads are pinned to host CPUs after QEMU has spawned
> them, driven by the query-cpus-fast QMP response.
> 
> Five modes are supported:
> * manual - explicit vCPU:hostCPU mapping, e.g. "0:0;1:1;2:2;3:3"
> * one-to-one - each vCPU gets its own physical core from a user pool,
>    SMT stripped, e.g. "0-3"
> * numa - per-vNUMA-node set-to-set binding, sequential, e.g. "3;4"
> * numa-manual - explicit vNUMA->host NUMA mapping, e.g. "0:2;1:3"
> 
> Two entry points are provided in PVE::QemuServer::CPUBinding:
> * validate_binding() - called at VM start, before QEMU spawns. Rejects
> invalid configs against the current topology.
> 
> * bind_vcpus() - called after QEMU is up. Encodes the QMP cpu
> information as JSON and hands it to Rust together with the mode and cfg
> string.
> 
> Signed-off-by: Elias Huhsovitz <e.huhsovitz@proxmox.com>
> ---
>   src/PVE/QemuServer.pm            |  36 +++++++++
>   src/PVE/QemuServer/CPUBinding.pm | 133 +++++++++++++++++++++++++++++++
>   src/PVE/QemuServer/Makefile      |   1 +
>   3 files changed, 170 insertions(+)
>   create mode 100644 src/PVE/QemuServer/CPUBinding.pm
> 
> diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm
> index 1efeeb66..3ba266be 100644
> --- a/src/PVE/QemuServer.pm
> +++ b/src/PVE/QemuServer.pm
> @@ -60,6 +60,7 @@ use PVE::QemuServer::Helpers
>       qw(config_aware_timeout get_iscsi_initiator_name get_host_arch min_version kvm_user_version windows_version);
>   use PVE::QemuServer::Cloudinit;
>   use PVE::QemuServer::CGroup;
> +use PVE::QemuServer::CPUBinding;
>   use PVE::QemuServer::CPUConfig qw(
>       print_cpu_device
>       get_cpu_options
> @@ -739,6 +740,18 @@ EODESCR
>           optional => 1,
>           default => 1,
>       },
> +    'cpu-binding-mode' => {
> +        type => 'string',
> +        format => $PVE::QemuServer::Bind::bind_mode_fmt,
> +        description => "Set CPU binding mode for the guest",
> +        optional => 1,
> +    },
> +    'cpu-binding-cfg' => {
> +        type => 'string',
> +        format => $PVE::QemuServer::Bind::bind_cfg_fmt,
> +        description => "Configuration for CPU binding for the guest",
> +        optional => 1,
> +    },

i don't like that they are two different settings.
simplifying and having one property string would be better

>   };
>   
>   my $cicustom_fmt = {
> @@ -3218,6 +3231,15 @@ sub config_to_command {
>           push @$cmd, '/usr/bin/taskset', '--cpu-list', '--all-tasks', $conf->{affinity};
>       }
>   
> +    if ($conf->{'cpu-binding-mode'}) {
> +        PVE::QemuServer::CPUBinding::validate_binding(
> +            $conf->{'cpu-binding-mode'},
> +            $conf->{'sockets'},
> +            $conf->{'cores'},
> +            $conf->{'cpu-binding-cfg'},
> +        );
> +    }
> +
>       push @$cmd, $kvm_binary;
>   
>       push @$cmd, '-id', $vmid;
> @@ -5715,6 +5737,20 @@ sub vm_start_nolock {
>       my $qemu_pkg_string = PVE::QemuServer::QMPHelpers::get_qemu_package_string($vmid);
>       syslog("info", "VM $vmid started with PID $pid using $qemu_pkg_string.");
>   
> +    if ($conf->{'cpu-binding-mode'}) {
> +        eval {
> +            PVE::QemuServer::CPUBinding::bind_vcpus(
> +                $vmid,
> +                $conf->{'cpu-binding-mode'},
> +                $conf->{'cpu-binding-cfg'},
> +            );
> +            1;
> +        } or do {
> +            my $error = $@ || 'Unknown failure';
> +            log_warn("unable to bind vCPU threads - $error");
> +        };
> +    }
> +
>       PVE::QemuServer::RunState::create_cleanup_flag($vmid);
>   
>       if (defined(my $migrate = $res->{migrate})) {
> diff --git a/src/PVE/QemuServer/CPUBinding.pm b/src/PVE/QemuServer/CPUBinding.pm
> new file mode 100644
> index 00000000..3ce3653b
> --- /dev/null
> +++ b/src/PVE/QemuServer/CPUBinding.pm
> @@ -0,0 +1,133 @@
> +package PVE::QemuServer::CPUBinding;
> +
> +use v5.36;
> +
> +use JSON;
> +
> +use PVE::QemuServer::Monitor;
> +use PVE::RS::CPU::Bind;
> +
> +our $bind_mode_fmt = {
> +    mode => {
> +        type => 'string',
> +        enum => [qw(none manual one-to-one numa numa-manual)],
> +        description => "Set the CPU binding mode for the guest.",
> +        verbose_description => <<'EODESCR',
> +Selects how guest vCPUs are pinned to host CPUs. The mode determines
> +how cpu-binding-cfg is interpreted.
> +
> +* none (default): no binding.
> +
> +* manual: explicit vCPU-to-hostCPU mapping. The config string is a
> +  semicolon-separated list of `<vcpu-set>:<hostcpu-set>` entries.
> +  Example: `0:0;1:1;2:2;3:3` pins vCPU 0 to CPU 0, vCPU 1 to CPU 1,
> +  etc. Multi-value sets use `a-b` ranges or `{a,b,c}` unions.
> +
> +* one-to-one: pin each vCPU to its own physical core, chosen from a
> +  user-supplied pool. The config string is an upper bound: the list of
> +  host cores the vCPUs may be assigned to. SMT siblings are stripped
> +  from that pool before assignment, so each vCPU gets its own distinct
> +  physical core, not a hyperthread.
> +
> +  If the pool has N cores and the VM has M vCPUs, each vCPU is pinned
> +  to exactly one of the N cores (N must be >= M, otherwise validation
> +  fails). For example, with `0-3` on a 4-vCPU VM, vCPU 0 gets core 0,
> +  vCPU 1 gets core 1, and so on. Each vCPU is fixed to a single core,
> +  it does not float within the pool. With `0;1;4;5` on the same VM,
> +  vCPU 2 is pinned to core 4 and vCPU 3 to core 5, skipping cores 2-3.
> +
> +  An empty string means "use all physical cores on the host" as the
> +  upper bound. On a host with 16 cores, that pins an N-vCPU VM to
> +  cores 0, 1, ..., N-1 — one core per vCPU, SMT stripped.
> +
> +  Unlike `manual`, you do not specify which core each vCPU gets; that
> +  assignment is up to hwloc. Use `manual` if you need a specific
> +  vCPU-to-core mapping.
> +
> +* numa: bind the set of vCPUs of each guest vNUMA node to the set of
> +  CPUs of a host NUMA node. The config string is a `;`-separated list
> +  of host NUMA node indices, consumed in order: guest vNUMA 0 gets the
> +  first host node, guest vNUMA 1 the second, and so on.
> +
> +  Each guest vNUMA node's vCPUs form a group that may run on any CPU
> +  of the assigned host NUMA node — including its cores and SMT threads.
> +  The host scheduler is free to migrate vCPUs within the node, but not
> +  outside it. This is the intended trade-off: memory allocated by a
> +  vCPU stays local to the host node it runs on (preserving NUMA
> +  memory locality), while the scheduler still has room to balance
> +  load across the node's cores and hyperthreads.
> +
> +  This is a set-to-set binding, not a per-vCPU assignment. All vCPUs
> +  of a guest vNUMA node share the same host CPU pool. If you need
> +  each vCPU pinned to its own distinct core, use one-to-one or manual
> +  instead.
> +
> +  Example: `3;4` lets all vCPUs on guest vNUMA 0 run on any CPU of
> +  host NUMA node 3, and all vCPUs on guest vNUMA 1 run on any CPU of
> +  host NUMA node 4.
> +
> +  An empty string uses host NUMA nodes in ascending index order:
> +  guest vNUMA 0 → host node 0, guest vNUMA 1 → host node 1, etc.
> +
> +* numa-manual: explicit variant of numa. The config string is a
> +  `;`-separated list of `<guest-vnuma>:<host-numa>` pairs. Each guest
> +  vNUMA node's set of vCPUs is bound to the set of CPUs of the
> +  specified host NUMA node.
> +
> +  Semantics are identical to numa — vCPUs of a guest vNUMA node may
> +  be scheduled on any CPU of the assigned host node, but cannot leave
> +  it. Only the node mapping is explicit here.
> +
> +  Example: `0:2;1:3` binds all vCPUs on guest vNUMA 0 to host NUMA
> +  node 2's CPUs, and all vCPUs on guest vNUMA 1 to host NUMA node 3's
> +  CPUs.
> +EODESCR
> +        default => 'none',
> +        default_key => 1,
> +    },
> +};
> +
> +our $bind_cfg_fmt = {
> +    cfg => {
> +        type => 'string',
> +        description => "CPU binding configuration (syntax depends on cpu-binding-mode).",
> +        verbose_description => <<'EODESCR',
> +The exact syntax depends on cpu-binding-mode:
> +
> +* manual, one-to-one:
> +    `;`-separated list of `<vcpu-set>:<hostcpu-set>` entries (manual)
> +    or a plain host CPU list (one-to-one). CPU sets accept `a-b` ranges
> +    and `{a,b,c}` unions.
> +
> +* numa:
> +    `;`-separated list of host NUMA node indices, e.g. `3;4`.
> +
> +* numa-manual:
> +    `;`-separated list of `<guest-vnuma>:<host-numa>` pairs, e.g. `0:2;1:3`.
> +
> +Host CPU and NUMA node indices are OS indices as reported by `lstopo -p`.
> +EODESCR
> +        default => '',
> +        default_key => 1,
> +    },
> +};
> +
> +sub validate_binding($mode, $sockets, $cores, $binding_cfg) {
> +    return if $mode eq 'none';
> +
> +    $binding_cfg //= '';
> +
> +    PVE::RS::CPU::Bind::validate_binding($mode, $sockets, $cores, $binding_cfg);


mhmm the config does not require a sockets or cores entry
but the rust binding take a u32, we should read the default
from the schema here

> +}
> +
> +sub bind_vcpus($vmid, $mode, $binding_cfg) {
> +    return if $mode eq 'none';
> +
> +    $binding_cfg //= '';
> +
> +    my $cpuinfo = PVE::QemuServer::Monitor::mon_cmd($vmid, 'query-cpus-fast');
> +    my $cpuinfo_json = JSON::encode_json($cpuinfo);
> +    PVE::RS::CPU::Bind::bind_vcpus($mode, $binding_cfg, $cpuinfo_json);
> +}
> +
> +1;
> diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile
> index 61ed1a4e..ee123cf5 100644
> --- a/src/PVE/QemuServer/Makefile
> +++ b/src/PVE/QemuServer/Makefile
> @@ -8,6 +8,7 @@ SOURCES=Agent.pm	\
>   	Cfg2Cmd.pm	\
>   	CGroup.pm	\
>   	Cloudinit.pm	\
> +	CPUBinding.pm	\
>   	CPUConfig.pm	\
>   	CPUFlags.pm	\
>   	DBusVMState.pm	\





  reply	other threads:[~2026-10-30 13:47 UTC|newest]

Thread overview: 7+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-22  9:22 [PATCH proxmox-perl-rs/qemu-server/qemu-server-rs v3 0/3] vCPU binding for qemu guests Elias Huhsovitz
2026-10-22  9:22 ` [PATCH qemu-server-rs v3 1/3] cpu: add pve-qemu-server-cpu crate for vCPU binding Elias Huhsovitz
2026-10-30 13:46   ` Dominik Csapak
2026-10-22  9:22 ` [PATCH proxmox-perl-rs v3 2/3] pve: cpu: add PVE::RS::CPU::Bind perl bindings Elias Huhsovitz
2026-10-22  9:22 ` [PATCH qemu-server v3 3/3] cpu: add CPU binding Elias Huhsovitz
2026-10-30 13:46   ` Dominik Csapak [this message]
2026-10-30 13:46 ` [PATCH proxmox-perl-rs/qemu-server/qemu-server-rs v3 0/3] vCPU binding for qemu guests Dominik Csapak

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=447abbd2-ee7b-49b0-9ebc-d7c52c76b9d1@proxmox.com \
    --to=d.csapak@proxmox.com \
    --cc=e.huhsovitz@proxmox.com \
    --cc=pve-devel@lists.proxmox.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal