public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
From: Elias Huhsovitz <e.huhsovitz@proxmox.com>
To: pve-devel@lists.proxmox.com
Cc: Elias Huhsovitz <e.huhsovitz@proxmox.com>
Subject: [RFC qemu-server v2 2/4] pinning: add NUMA allocator and reservation tracking
Date: Mon, 21 Sep 2026 11:54:01 +0200	[thread overview]
Message-ID: <20260921095404.61552-3-e.huhsovitz@proxmox.com> (raw)
In-Reply-To: <20260921095404.61552-1-e.huhsovitz@proxmox.com>

Introduce Allocator.pm to handle NUMA node placement and CPU thread
allocation.

The allocator assigns guest NUMA nodes to host NUMA nodes based on
uncommitted memory and CPU load. To prevent placement collisions during
concurrent VM starts, it uses a file lock and a JSON tracker in /run to
record pending reservations.

Three allocation strategies are supported:
1. assign_numa_nodes: explicit or auto-placed 1:1 guest-to-host mapping.

2. balance_numa_nodes: spreads vCPUs across multiple nodes proportional
to their capacity (used for 'balanced' mode).

3. allocate_one_to_one: spreads vCPUs across distinct physical cores to
avoid SMT collisions.

Signed-off-by: Elias Huhsovitz <e.huhsovitz@proxmox.com>
---
 src/PVE/QemuServer/Pinning/Allocator.pm | 590 ++++++++++++++++++++++++
 1 file changed, 590 insertions(+)
 create mode 100644 src/PVE/QemuServer/Pinning/Allocator.pm

diff --git a/src/PVE/QemuServer/Pinning/Allocator.pm b/src/PVE/QemuServer/Pinning/Allocator.pm
new file mode 100644
index 00000000..813c5aca
--- /dev/null
+++ b/src/PVE/QemuServer/Pinning/Allocator.pm
@@ -0,0 +1,590 @@
+package PVE::QemuServer::Pinning::Allocator;
+
+use v5.36;
+
+use JSON;
+use PVE::Tools qw(lock_file file_get_contents file_set_contents file_read_firstline);
+use PVE::QemuConfig;
+use PVE::QemuServer::Pinning::Config;
+use PVE::QemuServer::Memory;
+
+use constant RESERVATIONS_FILE => '/run/qemu-server/numa-reservations.json';
+use constant LOCK_FILE => '/var/lock/pve-numa-pin.lck';
+
+my sub host_cpu_capacity($topology) {
+    return {
+        map { $_ => scalar keys $topology->{numa_nodes}->{$_}->{cpus}->%* }
+        keys $topology->{numa_nodes}->%*
+    };
+}
+
+# Return MemTotal per NUMA node from
+# /sys/devices/system/node/nodeN/meminfo (bytes).
+# out: { 0 => 16907931648, 1 => 16907931648, 2 => 16907931648, 3 => 16907931648 }
+my sub host_memory_capacity() {
+    my $mem = {};
+    my $base = "/sys/devices/system/node";
+    opendir(my $dh, $base) or return $mem;
+    while (my $entry = readdir($dh)) {
+        next if $entry !~ /^node(\d+)$/;
+        my $node_id = $1;
+        my $meminfo_path = "$base/$entry/meminfo";
+        my $line = file_read_firstline($meminfo_path);
+        next if !defined($line);
+        if ($line =~ /MemTotal:\s+(\d+)\s+kB/) {
+            $mem->{$node_id} = $1 * 1024;
+        } else {
+            $mem->{$node_id} = 0;
+        }
+    }
+    closedir($dh);
+    return $mem;
+}
+
+# Read persisted reservations from /run, empty hash if missing/corrupt.
+# in: (no args)
+# out: { 100 => {nodes => {0 => 4}, memory => {0 => 4294967296}} }
+my sub load_reservations() {
+    return {} if !-e RESERVATIONS_FILE;
+    my $data = eval { file_get_contents(RESERVATIONS_FILE) };
+    return {} if !defined($data) || $data eq '';
+    my $reservations = eval { decode_json($data) };
+    return $reservations // {};
+}
+
+my sub save_reservations($reservations) {
+    file_set_contents(RESERVATIONS_FILE, encode_json($reservations));
+}
+
+# Drop reservation entries where VM has no running pid file.
+my sub prune_stale_reservations($reservations, $vm_id) {
+    for my $other_vmid (keys %$reservations) {
+        next if $other_vmid == $vm_id;
+        my $pid_file = "/run/qemu-server/$other_vmid.pid";
+        delete $reservations->{$other_vmid} if !-e $pid_file;
+    }
+}
+
+sub clear_reservation($vmid) {
+    lock_file(LOCK_FILE, 10, sub {
+        my $reservations = load_reservations();
+        delete $reservations->{$vmid};
+        save_reservations($reservations);
+    });
+}
+
+# Sum vCPU counts and memory per host node across all running pinned VMs.
+# in: 100
+# out: ({ 0 => 8, 1 => 4 }, { 0 => 8589934592, 1 => 4294967296 }
+my sub calculate_running_load($current_vmid) {
+    my $cpu_load = {};
+    my $mem_load = {};
+
+    my $conf_dir = "/etc/pve/qemu-server";
+    opendir(my $dh, $conf_dir) or return ($cpu_load, $mem_load);
+
+    while (my $file = readdir($dh)) {
+        next if $file !~ /^(\d+)\.conf$/;
+        my $other_vmid = $1;
+        next if $other_vmid == $current_vmid;
+
+        my $pid_file = "/run/qemu-server/$other_vmid.pid";
+        next if !-e $pid_file;
+
+        eval {
+            my $other_conf = PVE::QemuConfig->load_config($other_vmid);
+            my $pinning = PVE::QemuServer::Pinning::Config::parse_pinning(
+                $other_conf->{pinning},
+            );
+            return if $pinning->{mode} eq PVE::QemuServer::Pinning::Config::DEFAULT_VCPU_PINNING;
+
+            my $numa_map = PVE::QemuServer::Pinning::Config::build_vnuma_vcpu_map($other_conf);
+            my $vm_memory_mib = PVE::QemuServer::Memory::get_current_memory($other_conf->{memory});
+            my $vm_memory_bytes = $vm_memory_mib * 1024 * 1024;
+            my $node_count = scalar keys %$numa_map;
+            my $memory_per_node = $node_count > 0
+                ? int($vm_memory_bytes / $node_count)
+                : 0;
+
+            for my $numa_node (keys %$numa_map) {
+                my $hostnode = $numa_map->{$numa_node}->{hostnode};
+                next if !defined($hostnode);
+
+                my $vcpu_count = scalar keys $numa_map->{$numa_node}->{vcpus}->%*;
+                $cpu_load->{$hostnode} = ($cpu_load->{$hostnode} // 0) + $vcpu_count;
+                $mem_load->{$hostnode} = ($mem_load->{$hostnode} // 0) + $memory_per_node;
+            }
+        } or do {
+            my $error = $@ || 'Unknown failure';
+            warn "Failed to read NUMA load from running VM $other_vmid: $error\n";
+        }
+    }
+    closedir($dh);
+
+    return ($cpu_load, $mem_load);
+}
+
+# Fold other VMs' pending reservations into load maps.
+# in: ({0=>4}, {0=>4294967296}, {101=>{nodes=>{0=>4},memory=>{0=>4294967296}}}, 100)
+# mutated: cpu_load={0=>8}, mem_load={0=>8589934592}
+my sub apply_pending_load($cpu_load, $mem_load, $reservations, $current_vmid) {
+    for my $other_vmid (keys %$reservations) {
+        next if $other_vmid == $current_vmid;
+        my $entry = $reservations->{$other_vmid};
+        for my $node (keys %{$entry->{nodes} // {}}) {
+            $cpu_load->{$node} = ($cpu_load->{$node} // 0) + $entry->{nodes}->{$node};
+        }
+        for my $node (keys %{$entry->{memory} // {}}) {
+            $mem_load->{$node} = ($mem_load->{$node} // 0) + $entry->{memory}->{$node};
+        }
+    }
+}
+
+my sub run_vm_with_load_data($vmid, $function) {
+    my $worker = sub {
+        my $reservations = load_reservations();
+        prune_stale_reservations($reservations, $vmid);
+
+        my ($cpu_used, $mem_used) = calculate_running_load($vmid);
+        apply_pending_load($cpu_used, $mem_used, $reservations, $vmid);
+
+        $function->($cpu_used, $mem_used, $reservations);
+
+        save_reservations($reservations);
+    };
+    eval { lock_file(LOCK_FILE, 10, $worker); };
+    die $@ if $@;
+}
+
+# Restrict node list to pinning=balanced,hostnodes=... if set.
+# in: {pinning => "balanced,hostnodes=0;1"}, (0, 1, 2, 3)
+# out: (0, 1)
+my sub filter_by_hostnodes($conf, @nodes) {
+    my $pinning_conf = PVE::QemuServer::Pinning::Config::parse_pinning($conf->{pinning});
+    my $allowed = $pinning_conf->{hostnodes};
+    return @nodes if !defined($allowed);
+
+    my %allowed_set = map { $_ => 1 } @$allowed;
+    my @filtered = grep { $allowed_set{$_} } @nodes;
+    die "None of the host-nodes specified in pinning exist on"
+        . " this host (requested: " . join(';', @$allowed) . ")\n"
+        if !@filtered;
+    return @filtered;
+}
+
+# Pick host node with
+# 1. most free memory
+# 2. then least CPU load
+# that fits $vcpu_count.
+my sub pick_host_node($cpu_capacity, $mem_capacity, $cpu_used, $mem_used, $order, $vcpu_count) {
+    my @candidates;
+
+    for my $node (@$order) {
+        my $cpu_load = $cpu_used->{$node} // 0;
+        next if ($cpu_capacity->{$node} // 0) - $cpu_load < $vcpu_count;
+        my $free_mem = ($mem_capacity->{$node} // 0) - ($mem_used->{$node} // 0);
+        push @candidates, { node => $node, cpu_load => $cpu_load, free_mem => $free_mem };
+    }
+
+    return undef if !@candidates;
+
+    @candidates = sort {
+        $b->{free_mem} <=> $a->{free_mem}
+            || $a->{cpu_load} <=> $b->{cpu_load}
+    } @candidates;
+
+    return $candidates[0]->{node};
+}
+
+# Bump vCPU and memory used counters for a node
+my sub reserve_host_node($cpu_used, $mem_used, $cpu_capacity, $node, $vcpu_count, $memory_bytes) {
+    my $free = ($cpu_capacity->{$node} // 0) - ($cpu_used->{$node} // 0);
+    die "Not enough CPUs available on NUMA node $node\n" if $free < $vcpu_count;
+    $cpu_used->{$node} = ($cpu_used->{$node} // 0) + $vcpu_count;
+    $mem_used->{$node} = ($mem_used->{$node} // 0) + $memory_bytes;
+}
+
+# Rotate a node list by $seed so different VMs start at different nodes.
+# in: [0, 1, 2, 3], 2
+# out: (2, 3, 0, 1)
+my sub rotation_order($nodes, $seed) {
+    return @$nodes if !@$nodes;
+    my $start = $seed % @$nodes;
+    return (
+        @$nodes[$start .. $#$nodes],
+        @$nodes[0 .. $start - 1],
+    );
+}
+
+my sub compute_memory_per_node($conf, $guest_node_count) {
+    my $vm_memory_bytes =
+        PVE::QemuServer::Memory::get_current_memory($conf->{memory}) * 1024 * 1024;
+    return $guest_node_count > 0
+        ? int($vm_memory_bytes / $guest_node_count)
+        : 0;
+}
+
+my sub count_vcpus($vcpu_to_numa_map) {
+    return scalar keys %$vcpu_to_numa_map;
+}
+
+# Host node search order: affinity-restricted, rotated by vmid.
+# in: {pinning => "balanced,hostnodes=0;1"}, $topology, 100
+# out: (1, 0)     # 100 % 2 = 0, so rotated for vmid 101 → (1, 0)
+my sub build_search_order($conf, $topology, $vmid) {
+    my @available = filter_by_hostnodes($conf,
+        sort keys $topology->{numa_nodes}->%*);
+    return rotation_order(\@available, $vmid);
+}
+
+# Reserve host nodes that are explicitly pinned using: numaX hostnodes=)
+my sub reserve_explicit_nodes($numa_vcpu_map, $cpu_used, $mem_used, $cpu_capacity, $memory_per_node) {
+    for my $numa_node (sort keys $numa_vcpu_map->%*) {
+        my $hostnode = $numa_vcpu_map->{$numa_node}->{hostnode};
+        next if !defined($hostnode);
+
+        my $vcpu_count = scalar keys $numa_vcpu_map->{$numa_node}->{vcpus}->%*;
+        reserve_host_node(
+            $cpu_used, $mem_used, $cpu_capacity,
+            $hostnode, $vcpu_count, $memory_per_node,
+        );
+    }
+}
+
+=head2 assign_unassigned_nodes
+
+Assigns a host NUMA node to every guest NUMA node in C<$numa_vcpu_map>
+that does not already have a C<hostnode> set. Guest nodes with an explicit
+binding (from C<numaX=...,hostnodes=N>) are skipped; they are reserved
+separately by C<reserve_explicit_nodes()>.
+
+For each unbound guest node, C<pick_host_node()> selects the best host node
+that can fit the guest node's vCPU count. The order is defined by C<$search_order>.
+The chosen node's CPU and memory usage are then recorded.
+
+B<Mutates:> C<$numa_vcpu_map> (sets C<hostnode>), C<$cpu_used>, C<$mem_used>.
+
+B<Dies> if no host node has enough free CPUs for a guest node.
+
+=over
+
+=item C<$numa_vcpu_map> - C<< { $guest_node => { vcpus => { $v => 1, ... }, hostnode => $host_node } } >>
+
+=item C<$cpu_capacity> - C<< { $host_node => $total_cpus } >>
+
+=item C<$mem_capacity> - C<< { $host_node => $total_bytes } >>
+
+=item C<$cpu_used>, C<$mem_used> - current per-host-node usage, updated in place.
+
+=item C<$search_order> - arrayref of host node ids, tried in this order.
+
+=item C<$memory_per_node> - bytes charged to the host node for this guest node.
+
+=back
+
+=cut
+my sub assign_unassigned_nodes(
+    $numa_vcpu_map, $cpu_capacity, $mem_capacity,
+    $cpu_used, $mem_used, $search_order, $memory_per_node
+) {
+    for my $numa_node (sort keys $numa_vcpu_map->%*) {
+        next if defined($numa_vcpu_map->{$numa_node}->{hostnode});
+
+        my $vcpu_count = scalar keys $numa_vcpu_map->{$numa_node}->{vcpus}->%*;
+        my $hostnode = pick_host_node(
+            $cpu_capacity, $mem_capacity,
+            $cpu_used, $mem_used,
+            $search_order, $vcpu_count,
+        ) // die "Could not find a fitting host NUMA node for guest"
+            . " NUMA node $numa_node\n";
+
+        reserve_host_node(
+            $cpu_used, $mem_used, $cpu_capacity,
+            $hostnode, $vcpu_count, $memory_per_node,
+        );
+        $numa_vcpu_map->{$numa_node}->{hostnode} = $hostnode;
+    }
+}
+
+my sub build_reservation_from_guest_map($numa_vcpu_map, $memory_per_node) {
+    my ($nodes, $memory) = ({}, {});
+    for my $numa_node (keys %$numa_vcpu_map) {
+        my $hostnode = $numa_vcpu_map->{$numa_node}->{hostnode};
+        my $vcpu_count = scalar keys $numa_vcpu_map->{$numa_node}->{vcpus}->%*;
+        $nodes->{$hostnode}  = ($nodes->{$hostnode}  // 0) + $vcpu_count;
+        $memory->{$hostnode} = ($memory->{$hostnode} // 0) + $memory_per_node;
+    }
+    return { nodes => $nodes, memory => $memory };
+}
+
+my sub flatten_vcpu_to_host_map($numa_vcpu_map) {
+    my %vcpu_to_host;
+    for my $numa_node (keys %$numa_vcpu_map) {
+        my $hostnode = $numa_vcpu_map->{$numa_node}->{hostnode};
+        $vcpu_to_host{$_} = $hostnode
+            for keys $numa_vcpu_map->{$numa_node}->{vcpus}->%*;
+    }
+    return \%vcpu_to_host;
+}
+
+# Distribute vCPUs across nodes proportionally to CPU capacity.
+# in: [0, 1], {0=>8, 1=>8}, 6
+# out: {0=>0, 1=>0, 2=>0, 3=>1, 4=>1, 5=>1}    # remainder given to first node
+# with key: vcpu, val: hostcpu
+# in: [0, 1, 2, 3], {0=>8, 1=>8, 2=>4, 3=>4}, 5
+# out: {0=>0, 1=>0, 2=>0, 3=>1, 4=>1}          # floor + remainder to node 0
+my sub distribute_vcpus_proportional($selected_nodes, $cpu_capacity, $total_vcpus) {
+    my $total_capacity = 0;
+    $total_capacity += $cpu_capacity->{$_} // 0 for @$selected_nodes;
+
+    my @per_node;
+    my $assigned = 0;
+    for my $node (@$selected_nodes) {
+        my $count = int($total_vcpus * ($cpu_capacity->{$node} // 0) / $total_capacity);
+        push @per_node, $count;
+        $assigned += $count;
+    }
+    $per_node[0] += $total_vcpus - $assigned;
+
+    my %vcpu_to_host;
+    my $vcpu_id = 0;
+    for my $i (0 .. $#$selected_nodes) {
+        for (1 .. $per_node[$i]) {
+            $vcpu_to_host{$vcpu_id++} = $selected_nodes->[$i];
+        }
+    }
+    return \%vcpu_to_host;
+}
+
+my sub build_reservation_from_vcpu_map($vcpu_to_host, $selected_nodes, $memory_per_node) {
+    my ($nodes, $memory) = ({}, {});
+    for my $node (@$selected_nodes) {
+        my $node_vcpus = scalar grep { $vcpu_to_host->{$_} == $node }
+            keys %$vcpu_to_host;
+        $nodes->{$node}  = $node_vcpus;
+        $memory->{$node} = $memory_per_node;
+    }
+    return { nodes => $nodes, memory => $memory };
+}
+
+# Assign each guest NUMA node to a host NUMA node.
+#
+# Guest nodes with an explicit "hostnodes=N" in their numaX entry are
+# reserved first. Guest nodes without one are auto-placed by
+# assign_unassigned_nodes().
+#
+# in:  $conf = {
+#          numa  => 1,
+#          numa0 => "cpus=0-1,hostnodes=0",
+#          numa1 => "cpus=2-3,hostnodes=1",
+#      },
+#      $topology, 100
+# out: {0=>0, 1=>0, 2=>1, 3=>1} with vcpu id => host node id
+sub assign_numa_nodes($conf, $topology, $vmid) {
+    my $numa_vcpu_map = PVE::QemuServer::Pinning::Config::build_vnuma_vcpu_map($conf);
+    my $cpu_capacity = host_cpu_capacity($topology);
+    my $mem_capacity = host_memory_capacity();
+    my $memory_per_node = compute_memory_per_node($conf, scalar keys %$numa_vcpu_map);
+    my @search_order = build_search_order($conf, $topology, $vmid);
+
+    run_vm_with_load_data($vmid, sub ($cpu_used, $mem_used, $reservations) {
+        reserve_explicit_nodes(
+            $numa_vcpu_map, $cpu_used, $mem_used, $cpu_capacity, $memory_per_node);
+
+        assign_unassigned_nodes(
+            $numa_vcpu_map, $cpu_capacity, $mem_capacity,
+            $cpu_used, $mem_used, \@search_order, $memory_per_node);
+
+        $reservations->{$vmid} = build_reservation_from_guest_map(
+            $numa_vcpu_map, $memory_per_node);
+    });
+
+    return flatten_vcpu_to_host_map($numa_vcpu_map);
+}
+
+=head2 balance_numa_nodes
+
+    my $vcpu_to_host = balance_numa_nodes($conf, $topology, $vmid);
+
+Selects the smallest set of host NUMA nodes that fits the VM's vCPUs and
+memory, then spreads vCPUs across them proportionally to each node's CPU
+capacity. Used for C<pinning=balanced>. Inspired by Incus's
+C<balanceNUMANodes()> in F<incus/internal/server/instance/drivers/driver_common.go>.
+
+=over
+
+=item 1. Filter by C<hostnodes> restriction.
+
+=item 2. Sort by uncommitted memory (desc), then CPU load (asc).
+
+=item 3. Use as many nodes as needed to fit CPU + memory.
+
+=item 4. Distribute vCPUs proportionally.
+
+=item 5. Persist reservation.
+
+=back
+
+Returns C<< { $vcpu_id => $host_node_id } >>.
+
+B<Mutates:> writes the VM's reservation to the reservations file under the
+global NUMA lock; stale entries are pruned in the same critical section.
+
+B<Dies> if the pinning restriction names non-existent host nodes, or if no
+set of nodes can fit the VM.
+
+Example (2 equal nodes, 4 vCPUs, 4 GiB):
+
+    balance_numa_nodes({vcpus=>4, memory=>4096, pinning=>'balanced'}, $topo, 100);
+    # => { 0 => 0, 1 => 0, 2 => 1, 3 => 1 }
+
+=cut
+sub balance_numa_nodes($conf, $topology, $vmid) {
+    my $cpu_capacity = host_cpu_capacity($topology);
+    my $mem_capacity = host_memory_capacity();
+
+    my $vcpus = $conf->{vcpus}
+        // (($conf->{sockets} // 1) * ($conf->{cores} // 1));
+    my $vm_memory_mib = PVE::QemuServer::Memory::get_current_memory($conf->{memory});
+    my $vm_memory_bytes = $vm_memory_mib * 1024 * 1024;
+
+    my @nodes = filter_by_hostnodes($conf,
+        sort keys $topology->{numa_nodes}->%*);
+
+    my $vcpu_to_numa_map;
+
+    run_vm_with_load_data($vmid, sub ($cpu_used, $mem_used, $reservations) {
+        # Sort: most uncommitted memory first, then least CPU load.
+        my @sorted = sort {
+            (($mem_capacity->{$b} // 0) - ($mem_used->{$b} // 0))
+            <=>
+            (($mem_capacity->{$a} // 0) - ($mem_used->{$a} // 0))
+            ||
+            ($cpu_used->{$a} // 0) <=> ($cpu_used->{$b} // 0)
+        } @nodes;
+
+        # Use as many nodes as needed to fit CPU and memory.
+        my @selected;
+        my $cpu_total = 0;
+        my $mem_free  = 0;
+        for my $node (@sorted) {
+            $cpu_total += $cpu_capacity->{$node} // 0;
+            $mem_free  += ($mem_capacity->{$node} // 0) - ($mem_used->{$node} // 0);
+            push @selected, $node;
+            last if $vcpus <= $cpu_total && $vm_memory_bytes <= $mem_free;
+        }
+
+        die "Could not fit VM ($vcpus vCPUs, $vm_memory_mib MiB) across"
+            . " available host NUMA nodes\n"
+            if $vcpus > $cpu_total || $vm_memory_bytes > $mem_free;
+
+        $vcpu_to_numa_map = distribute_vcpus_proportional(
+            \@selected, $cpu_capacity, $vcpus);
+
+        my $memory_per_node = int($vm_memory_bytes / scalar(@selected));
+        $reservations->{$vmid} = build_reservation_from_vcpu_map(
+            $vcpu_to_numa_map, \@selected, $memory_per_node);
+    });
+
+    return $vcpu_to_numa_map;
+}
+
+# Returns: {node => {core_key => [cpu_ids]}}
+my sub build_numa_node_core_pool($topology) {
+    my $pool = {};
+    for my $node (keys $topology->{numa_nodes}->%*) {
+        for my $core_key (keys $topology->{numa_nodes}->{$node}->{cores}->%*) {
+            my @cpus = $topology->{cores}->{$core_key}->{cpus}->@*;
+            $pool->{$node}->{$core_key} = [@cpus];
+        }
+    }
+    return $pool;
+}
+
+# Pop one CPU from the core with the most remaining CPUs (SMT spread).
+# in: {"0_0" => [0, 4], "0_1" => [1, 5]}, 0
+# out: 0    # pool now {"0_0" => [4], "0_1" => [1, 5]}
+my sub take_cpu_from_core_pool($core_pool, $vcpu) {
+    my @cores = sort {
+        $core_pool->{$b}->@* <=> $core_pool->{$a}->@*
+            || $a cmp $b
+    } keys %$core_pool;
+
+    die "No available cores for vcpu $vcpu\n" if !@cores;
+
+    my $core_key = $cores[0];
+    my $cpus = $core_pool->{$core_key};
+    my $cpu = shift $cpus->@*;
+    delete $core_pool->{$core_key} if !$cpus->@*;
+    return $cpu;
+}
+
+my sub take_cpu_from_node($pool, $node, $vcpu) {
+    die "No available cores on NUMA node $node for vcpu $vcpu\n"
+        if !keys %{$pool->{$node} // {}};
+    return take_cpu_from_core_pool($pool->{$node}, $vcpu);
+}
+
+# Flat per-vCPU pinning: spread across all cores, ignoring NUMA boundaries.
+# take_cpu_from_core_pool() sees all 16 cores in one pool, so the spread
+# is over physical cores first (lowest core key wins each tie).
+# in:  $topology (above), $vcpu_count = 4
+# out: $vcpu_to_cpu_map = { vcpu_id => host_cpu_id } = {0=>0, 1=>1, 2=>2, 3=>3}
+sub allocate_one_to_one($topology, $vcpu_to_numa_map, $vcpu_count) {
+    my $node_cores_pool = build_numa_node_core_pool($topology);
+    my $final_map = {};
+
+    for my $vcpu (0 .. $vcpu_count - 1) {
+        my $node = $vcpu_to_numa_map->{$vcpu};
+        if (!defined($node) || !defined($node_cores_pool->{$node})) {
+            die "No available CPUs for pinning\n" if !keys %$node_cores_pool;
+            ($node) = sort keys %$node_cores_pool;
+        }
+        $final_map->{$vcpu} = take_cpu_from_node($node_cores_pool, $node, $vcpu);
+    }
+
+    return $final_map;
+}
+
+# Flat per-vCPU pinning: spread across all cores, ignoring NUMA.
+# in: $topology, 4
+# out: {0=>0, 1=>1, 2=>2, 3=>3}
+sub allocate_one_to_one_flat($topology, $vcpu_count) {
+    my $node_cores_pool = build_numa_node_core_pool($topology);
+
+    my %flat_pool;
+    for my $node (keys %$node_cores_pool) {
+        for my $core_key (keys %{$node_cores_pool->{$node}}) {
+            $flat_pool{$core_key} = $node_cores_pool->{$node}->{$core_key};
+        }
+    }
+
+    my $final_map = {};
+    for my $vcpu (0 .. $vcpu_count - 1) {
+        die "No available CPUs for pinning\n" if !keys %flat_pool;
+        $final_map->{$vcpu} = take_cpu_from_core_pool(\%flat_pool, $vcpu);
+    }
+
+    return $final_map;
+}
+
+# Each vCPU gets ALL CPUs of its assigned host node as a comma-joined set.
+# in:  $topology,
+#      $vcpu_to_numa_map = { vcpu_id => host_node_id } = {0=>0, 1=>0, 2=>1, 3=>1}
+# out: $vcpu_to_cpuset_map = { vcpu_id => "cpu,cpu,..." } = {
+#          0 => "0,1,2,3,16,17,18,19",
+#          1 => "0,1,2,3,16,17,18,19",
+#          2 => "4,5,6,7,20,21,22,23",
+#          3 => "4,5,6,7,20,21,22,23",
+#      }
+sub allocate_numa($topology, $vcpu_to_numa_map) {
+    my $final_map = {};
+    for my $vcpu (keys $vcpu_to_numa_map->%*) {
+        my $node = $vcpu_to_numa_map->{$vcpu};
+        my @cpus = sort keys $topology->{numa_nodes}->{$node}->{cpus}->%*;
+        $final_map->{$vcpu} = join(',', @cpus);
+    }
+    return $final_map;
+}
+
+1;
-- 
2.47.3





  parent reply	other threads:[~2026-09-21  9:54 UTC|newest]

Thread overview: 5+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-09-21  9:53 [RFC qemu-server v2 0/4] fix #7282: allow (NUMA aware) vCPU pinning Elias Huhsovitz
2026-09-21  9:54 ` [RFC qemu-server v2 1/4] pinning: add topology discovery and config parsing Elias Huhsovitz
2026-09-21  9:54 ` Elias Huhsovitz [this message]
2026-09-21  9:54 ` [RFC qemu-server v2 3/4] memory: integrate pinning-aware NUMA memory binding Elias Huhsovitz
2026-09-21  9:54 ` [RFC qemu-server v2 4/4] pinning: integrate cpu pinning into vm lifecycle Elias Huhsovitz

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=20260921095404.61552-3-e.huhsovitz@proxmox.com \
    --to=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