From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [45.144.208.40]) by lore.proxmox.com (Postfix) with ESMTPS id 10A7F1FF0AF for ; Thu, 10 Sep 2026 16:37:23 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id DACC821571; Thu, 10 Sep 2026 16:37:19 +0200 (CEST) Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable Content-Type: text/plain; charset=UTF-8 Date: Thu, 10 Sep 2026 16:37:12 +0200 Message-Id: Subject: Re: [RFC PATCH qemu-server] fix #7282: allow (NUMA aware) vCPU pinning From: "Elias Huhsovitz" To: "Dominik Csapak" , X-Mailer: aerc 0.20.0 References: <20260217114813.2063770-1-d.csapak@proxmox.com> In-Reply-To: <20260217114813.2063770-1-d.csapak@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1789051023477 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.645 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: 27QJASWE5TJR5QLIIR4AZRS4PM6CNZH2 X-Message-ID-Hash: 27QJASWE5TJR5QLIIR4AZRS4PM6CNZH2 X-MailFrom: e.huhsovitz@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: On Tue Feb 17, 2026 at 12:01 PM CET, Dominik Csapak wrote: > Introduce a new 'pinning' property, which (for now) has two methods for > vCPU pinning: IMO very important patch for specific use cases. * I noticed 1 potential cause for bugs when using one-to-one pinning (See choose_single_cpu) * numa setting might be working correctly. I tried different approach * but benchmark results are unchanged (stream benchmark [0] still benefits * greatly from the numam setting) Also see other comments inline. [...] > > src/PVE/QemuServer.pm | 12 ++ > src/PVE/QemuServer/Makefile | 1 + > src/PVE/QemuServer/Pinning.pm | 388 ++++++++++++++++++++++++++++++++++ > 3 files changed, 401 insertions(+) > create mode 100644 src/PVE/QemuServer/Pinning.pm > > diff --git a/src/PVE/QemuServer.pm b/src/PVE/QemuServer.pm > index 5d2dbe03..f055f33f 100644 > --- a/src/PVE/QemuServer.pm > +++ b/src/PVE/QemuServer.pm > @@ -85,6 +85,7 @@ use PVE::QemuServer::Memory qw(get_current_memory); > use PVE::QemuServer::MetaInfo; > use PVE::QemuServer::Monitor qw(mon_cmd); > use PVE::QemuServer::Network; > +use PVE::QemuServer::Pinning; > use PVE::QemuServer::OVMF; > use PVE::QemuServer::PCI qw(print_pci_addr print_pcie_addr print_pcie_ro= ot_port parse_hostpci); > use PVE::QemuServer::QemuImage; > @@ -735,6 +736,12 @@ EODESCR > optional =3D> 1, > default =3D> 1, > }, > + pinning =3D> { > + type =3D> 'string', > + format =3D> $PVE::QemuServer::Pinning::pinning_fmt, > + description =3D> "Set pinning options for the guest", > + optional =3D> 1, > + }, > }; > =20 > my $cicustom_fmt =3D { > @@ -3174,6 +3181,8 @@ sub config_to_command { > push @$cmd, '/usr/bin/taskset', '--cpu-list', '--all-tasks', $co= nf->{affinity}; > } > =20 > + PVE::QemuServer::Pinning::assert_pinning_constraints($conf); > + > push @$cmd, $kvm_binary; > =20 > push @$cmd, '-id', $vmid; > @@ -5816,6 +5825,9 @@ sub vm_start_nolock { > =20 > syslog("info", "VM $vmid started with PID $pid."); > =20 > + eval { PVE::QemuServer::Pinning::pin_threads_to_cpus($conf, $vmid) }= ; > + log_warn("could not pin vCPU threads - $@") if $@; > + > if (defined(my $migrate =3D $res->{migrate})) { > if ($migrate->{proto} eq 'tcp') { > my $nodename =3D nodename(); > diff --git a/src/PVE/QemuServer/Makefile b/src/PVE/QemuServer/Makefile > index d599ca91..9edcbf6b 100644 > --- a/src/PVE/QemuServer/Makefile > +++ b/src/PVE/QemuServer/Makefile > @@ -21,6 +21,7 @@ SOURCES=3DAgent.pm \ > Network.pm \ > OVMF.pm \ > PCI.pm \ > + Pinning.pm \ > QemuImage.pm \ > QMPHelpers.pm \ > QSD.pm \ > diff --git a/src/PVE/QemuServer/Pinning.pm b/src/PVE/QemuServer/Pinning.p= m > new file mode 100644 > index 00000000..c3bc870f > --- /dev/null > +++ b/src/PVE/QemuServer/Pinning.pm > @@ -0,0 +1,388 @@ > +package PVE::QemuServer::Pinning; > + > +use v5.36; > + > +use PVE::QemuServer::Memory; > +use PVE::QemuServer::Monitor; > + > +use PVE::Tools qw(dir_glob_foreach run_command); > + > +=3Dhead1 NAME > + > +C - Functions for pinning vCPUs of QEMU guests > + > +=3Dhead1 DESCRIPTION > + > +This module contains functions for helping with pinning QEMU guest vCPUS= to > +host CPUs, considering other parts of the config like NUMA nodes and aff= inity. > + > +Before the guest is started C should be call= ed to > +check the pinning constraints. > + > +After the guest is started, C is called to actually= pin > +the vCPU threads to the host CPUs according to the config. > + > +=3Dcut > + > +my $DEFAULT_VCPU_PINNING =3D "none"; > + > +our $pinning_fmt =3D { > + 'vcpus' =3D> { > + type =3D> 'string', > + enum =3D> [qw(one-to-one numa $DEFAULT_VCPU_PINNING)], > + description =3D> "Set the type of vCPU pinning modes.", > + verbose_description =3D> < +There are multiple ways to pin vcpus to host cores: > + > +* $DEFAULT_VCPU_PINNING (default): either no pinning at all, or when 'af= finity' > + is set, pins all vcpus to the range of given cores by 'affinity'. > + > +* numa: pins the sets of vcpus of each virtual NUMA node, to a correspon= ding > + host numa node. This makes memory access consistent for each vcpu, sin= ce it > + won't be rescheduled to a different NUMA node. Takes into account the = 'numaX' > + setting when binding vcpus to host nodes. If 'affinity' is set, only c= onsider > + those cores. > + > +* one-to-one: tries to pin each vcpu to a specific host core. This preve= nts > + vcpus to be rescheduled on other vcpus entirely and can thus reach the= most > + performance, it is the least flexible for the host scheduler. Takes in= to > + account the virtual and physical NUMA layout. > + > +This is only supported when there is at least one host NUMA node per vir= tual > +one, and each manual 'numaX' node is assigned to at most one host NUMA n= ode. > + > +Any of these options won't take into account pinning settings from diffe= rent > +virtual machines or containers, so to achieve the best and most consiste= nt > +performance, use the combination of 'numaX' and 'affinity' options to ma= ke sure > +host cores are not crowded with vcpu assignments. > + > +EODESCR > + default =3D> 'none', > + default_key =3D> 1, > + }, > +}; > + > +=3Dhead2 Restrictions and Constraints > + > +To keep it simple, pinning is limited to a subset of possible NUMA > +configurations. For example, with 'numaX' configs, it's possible to crea= te > +overlapping virtual NUMA nodes, e.g. assigning vCPUs 0-3 to host NUMA no= des 0-1 > +and vCPUs 4-7 to host NUMA nodes 1-2. To keep the pinning logic simple, = such > +configurations are not supported. Instead the user should assign virtual= NUMA > +nodes only to a single host NUMA node. > + > +=3Dcut > + > +=3Dhead2 host_numa_node_cpu_list > + > +returns a map of host numa nodes to cpus > + > +=3Dcut > + > +my sub host_numa_node_cpu_list { > + my $base_path =3D "/sys/devices/system/node/"; > + > + my $map =3D {}; > + my $count =3D 0; > + > + dir_glob_foreach( > + $base_path, > + 'node(\d+)', > + sub { > + my ($fullnode, $nodeid) =3D @_; > + opendir(my $dirfd, "$base_path/$fullnode") > + || die "cannot open numa node dir $fullnode\n"; > + # this is a hash so we can use the key randomness for select= ing random selecting might cause issues for the one-to-one pinning option. See comments in choose_single_cpu for my take on this. > + my $cpus =3D {}; > + for my $cpu (readdir($dirfd)) { > + if ($cpu =3D~ m/^cpu(\d+)$/) { > + $cpus->{$1} =3D 1; > + $count++; > + } > + } > + closedir($dirfd); > + > + $map->{$nodeid} =3D $cpus; > + }, > + ); > + > + return ($count, $map); > +} > + > +=3Dhead2 limit_by_affinity > + > +limits a list of map of numa nodes to cpu by a the given affinity map > + > +=3Dcut > + nit: IMO: filter_by_affinity would a more fitting name > +my sub limit_by_affinity($host_cpus, $affinity_members) { > + return $host_cpus if !$affinity_members || scalar($affinity_members-= >%*) =3D=3D 0; > + > + for my $node_id (keys $host_cpus->%*) { > + my $node =3D $host_cpus->{$node_id}; > + for my $cpu_id (keys $node->%*) { > + delete $node->{$cpu_id} if !defined($affinity_members->{$cpu= _id}); > + } > + } > + > + return $host_cpus; > +} > + > +=3Dhead2 get_vnuma_vcpu_map > + > +Returns a hash from virtual NUMA nodes to vCPUs and an optional host NUM= A node > + > +=3Dcut IMO the nesting in this subroutine is a bit overkill. I would: 1. invert the if statement, so we have an early return in the Non-NUMA case 2. Introduce my $node =3D $map->{$i} =3D { vcpus =3D> {} }; 3. Remove redundant parathesis, e.g.,=20 for my $socket ((0 .. ($sockets - 1))) becomes for my $socket (0 .. ($sockets - 1)) 4. Extract the deep for-loops into their own subroutines. > +my sub get_vnuma_vcpu_map($conf) { > + my $map =3D {}; > + my $sockets =3D $conf->{sockets} // 1; > + my $cores =3D $conf->{cores} // 1; > + my $vcpu_count =3D 0; > + > + if ($conf->{numa}) { > + for (my $i =3D 0; $i < $PVE::QemuServer::Memory::MAX_NUMA; $i++)= { > + my $entry =3D $conf->{"numa$i"} or next; > + my $numa =3D PVE::QemuServer::Memory::parse_numa($entry) or = next; > + > + $map->{$i} =3D { vcpus =3D> {} }; > + for my $cpurange ($numa->{cpus}->@*) { > + my ($start, $end) =3D $cpurange->@*; > + for my $cpu (($start .. ($end // $start))) { > + $map->{$i}->{vcpus}->{$cpu} =3D 1; > + $vcpu_count++; > + } > + } > + > + if (my $hostnodes =3D $numa->{hostnodes}) { > + die "Pinning only available for 1-to-1 NUMA node mapping= \n" > + if (scalar($hostnodes->@*) > 1 || defined($hostnodes= ->[0]->[1])); > + $map->{$i}->{hostnode} =3D $hostnodes->[0]->[0]; > + } > + } > + > + my $vcpu_maps =3D scalar(keys $map->%*); > + if ($vcpu_maps =3D=3D 0) { > + for my $socket ((0 .. ($sockets - 1))) { > + $map->{$socket} =3D { vcpus =3D> {} }; > + for my $cpu ((0 .. ($cores - 1))) { > + my $vcpu =3D $socket * $cores + $cpu; > + $map->{$socket}->{vcpus}->{$vcpu} =3D 1; > + $vcpu_count++; > + } > + } > + } > + > + die "Invalid NUMA configuration for pinning, some vCPUs missing = in numa binding\n" > + if $vcpu_count !=3D $sockets * $cores; > + } else { > + # numa not enabled so all are on the same node > + $map->{0} =3D { vcpus =3D> {} }; > + for my $i ((0 .. ($sockets * $cores - 1))) { > + $map->{0}->{vcpus}->{$i} =3D 1; > + } > + } > + > + return $map; > +} > + > +=3Dhead2 get_filtered_host_cpus > + > +Returns the map of host numa nodes to CPUs after applying the optional a= ffinity from the config. > + > +=3Dcut > + > +sub get_filtered_host_cpus($conf) { > + my ($host_cpu_count, $host_cpus) =3D host_numa_node_cpu_list(); > + > + if (my $affinity =3D $conf->{affinity}) { > + my ($_affinity_count, $affinity_members) =3D PVE::CpuSet::parse_= cpuset($affinity); > + $host_cpus =3D limit_by_affinity($host_cpus, $affinity_members); > + } > + > + return $host_cpus; > +} > + > +=3Dhead2 get_vcpu_to_host_numa_map > + > +Returns a map from vcpus to host numa nodes, and also checks the size co= nstraints currently supported > + > +=3Dcut > + > +sub get_vcpu_to_host_numa_map($conf, $host_cpus) { > + my $numa_vcpu_map =3D get_vnuma_vcpu_map($conf); > + > + my $map =3D {}; > + > + if ($conf->{numa}) { > + my $used_host_nodes =3D {}; > + > + # fill the map of requested host numa nodes with their vcpu coun= t > + for my $numa_node (sort keys $numa_vcpu_map->%*) { > + my $vcpus =3D $numa_vcpu_map->{$numa_node}->{vcpus}; > + my $hostnode =3D $numa_vcpu_map->{$numa_node}->{hostnode}; > + if (defined($hostnode)) { > + my $vcpu_count =3D scalar(keys $vcpus->%*); > + > + $used_host_nodes->{$hostnode} //=3D 0; > + $used_host_nodes->{$hostnode} +=3D $vcpu_count; > + } > + } > + > + # check if there are enough host cpus for the vcpus > + for my $hostnode (keys $used_host_nodes->%*) { > + my $vcpu_count =3D $used_host_nodes->{$hostnode}; > + my $host_cpu_count =3D scalar(keys $host_cpus->{$hostnode}->= %*); > + > + die "Not enough CPUs available on NUMA node $hostnode\n" > + if $host_cpu_count < $vcpu_count; > + } > + > + # try to fit remaining virtual numa nodes to real ones > + for my $numa_node (sort keys $numa_vcpu_map->%*) { > + my $vcpus =3D $numa_vcpu_map->{$numa_node}->{vcpus}; > + my $hostnode =3D $numa_vcpu_map->{$numa_node}->{hostnode}; > + if (!defined($hostnode)) { > + # try real nodes in ascending order until we find one th= at fits > + # NOTE: this is not the optimal solution as this would p= robably be NP hard, > + # as it's similar to the Bin Packing problem > + for my $node (sort keys $host_cpus->%*) { > + next if $used_host_nodes->{$node}; > + my $host_cpu_count =3D scalar(keys $host_cpus->{$nod= e}->%*); > + my $vcpu_count =3D scalar(keys $numa_vcpu_map->{$num= a_node}->{vcpus}->%*); > + next if $host_cpu_count < $vcpu_count; > + > + $hostnode =3D $node; > + $used_host_nodes->{$hostnode} //=3D 0; > + $used_host_nodes->{$hostnode} +=3D $vcpu_count; > + last; > + } > + > + die "Could not find a fitting host NUMA node for guest N= UMA node $numa_node\n" > + if !defined($hostnode); > + $numa_vcpu_map->{$numa_node}->{hostnode} =3D $hostnode; > + } > + } > + > + # now every virtual numa node has a fitting host numa node and w= e can map from vcpu -> numa node > + for my $numa_node (keys $numa_vcpu_map->%*) { > + my $vcpus =3D $numa_vcpu_map->{$numa_node}->{vcpus}; > + my $hostnode =3D $numa_vcpu_map->{$numa_node}->{hostnode}; > + > + for my $vcpu (keys $vcpus->%*) { > + $map->{$vcpu} =3D $hostnode; > + } > + } > + } else { > + my $vcpus =3D ($conf->{sockets} // 1) * ($conf->{cores} // 1); > + my $host_cpu_count =3D 0; > + for my $node (keys $host_cpus->%*) { > + $host_cpu_count +=3D scalar(keys $host_cpus->{$node}->%*); > + } > + die "not enough available CPUs (limited by affinity) to pin\n" i= f $vcpus > $host_cpu_count; > + # returning empty list means the code can choose any cpu from th= e available ones > + } > + return $map; > +} > + > +=3Dhead2 choose_single_cpu > + > +Selects a CPU from the available C<$host_cpus> with the help of the give= n index > +C<$vcpu> and the vCPU to NUMA node map C<$vcpu_map>. > + > +It modifies the C<$host_cpus> hash to reserve the chosen CPU. > + > +=3Dcut > + > +sub choose_single_cpu($vcpu_map, $host_cpus, $vcpu) { > + my $hostnode =3D $vcpu_map->{$vcpu}; > + if (!defined($hostnode)) { > + # choose a numa node at random > + $hostnode =3D (keys $host_cpus->%*)[0]; > + } > + > + # choose one at random I think choosing a real CPU at random here might lead to random=20 performance issues. If my understanding is correct, this could lead to SMT sibling collision. In our case with the AMD EPYC 7351P 16-Core Processor: CPU NODE SOCKET CORE L1d:L1i:L2:L3 0 0 0 0 0:0:0:0 1 0 0 1 1:1:1:0 2 0 0 2 4:4:4:2 3 0 0 3 5:5:5:2 4 1 0 4 8:8:8:4 5 1 0 5 9:9:9:4 6 1 0 6 12:12:12:6 7 1 0 7 13:13:13:6 8 2 0 8 16:16:16:8 9 2 0 9 17:17:17:8 10 2 0 10 20:20:20:10 11 2 0 11 21:21:21:10 12 3 0 12 24:24:24:12 13 3 0 13 25:25:25:12 14 3 0 14 28:28:28:14 15 3 0 15 29:29:29:14 16 0 0 0 0:0:0:0 17 0 0 1 1:1:1:0 18 0 0 2 4:4:4:2 19 0 0 3 5:5:5:2 20 1 0 4 8:8:8:4 21 1 0 5 9:9:9:4 22 1 0 6 12:12:12:6 23 1 0 7 13:13:13:6 24 2 0 8 16:16:16:8 25 2 0 9 17:17:17:8 26 2 0 10 20:20:20:10 27 2 0 11 21:21:21:10 28 3 0 12 24:24:24:12 29 3 0 13 25:25:25:12 30 3 0 14 28:28:28:14 31 3 0 15 29:29:29:14 CPU 0 and 16 are siblings. They run on the same core, sharing the same L1 &= L2 cache. If we use random selection process it might assign: =E2=94=8C=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=90 =E2=94=82 vCPU 0 =E2=86=92 CPU 0 =E2=94=82 =E2=86=90 Same physical co= re =E2=94=82 vCPU 1 =E2=86=92 CPU 16 =E2=94=82 =E2=86=90 SMT sibling =E2=94=94=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94= =80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80=E2=94=80= =E2=94=80=E2=94=80=E2=94=98 So we have different programms vCPU0 and vCPU1 but the utilize the same har= dware core. This would degrade performance as both of these CPUs share the same cache. We should have some kind of safe guard to avoid SMT collisions. e.g., we could create a cpu selection policy: 1. Read core_id for every logical CPU in the node. (Read it from /sys/devices/system/cpu/cpu{N}/topology/core_id since the har= dware core id should be the most reliable) 2. Build a map with: key=3Dcore-id, value=3Dlist of cpus that reside on tha= t core In order to avoid any randomness, we should sort these entries, e.g. core-0 =3D [cpu0, cpu16] core-1 =3D [cpu1, cpu17] core-2 =3D [cpu2, cpu18] core-3 =3D [cpu3, cpu19] core-4 =3D [cpu4, cpu20] ... 3. Return the cpus in a round-robin fashion. So calling choose_single_cpu w= ould yield: 1. cpu0 2. cpu1 3. cpu2 4. cpu3 5. cpu16 6. cpu17 <-- here we could give a notice that we are overcomitting ... Let me know what you think of this approach! > + my $real_cpu =3D (keys $host_cpus->{$hostnode}->%*)[0]; > + delete $host_cpus->{$hostnode}->{$real_cpu}; > + if (scalar($host_cpus->{$hostnode}->%*) =3D=3D 0) { > + delete $host_cpus->{$hostnode}; > + } > + > + return $real_cpu; > +} > + > +=3Dhead2 get_numa_cpulist > + > +Returns the list of usable CPUs for the given C<$vcpu> index with the he= lp of > +the vCPU to NUMA node map C<$vcpu_map> and the available C<$host_cpus> a= s a > +string usable by the 'taskset' command. > + > +=3Dcut > + > +sub get_numa_cpulist($vcpu_map, $host_cpus, $vcpu) { > + my $hostnode =3D $vcpu_map->{$vcpu}; > + if (!defined($hostnode)) { > + # if there is not explicit mapping, simply don't pin at all > + return undef; > + } > + > + return join(',', keys $host_cpus->{$hostnode}->%*); > +} > + > +=3Dhead2 assert_pinning_constraints > + > +Used to verify the constraints from pinnning by trying to construct > +the pinning configuration. Useful to check the config before actually st= arting > +the guest. > + > +=3Dcut > + > +sub assert_pinning_constraints($conf) { > + > + my $pinning =3D $conf->{pinning} // $DEFAULT_VCPU_PINNING; > + if ($pinning ne $DEFAULT_VCPU_PINNING) { > + my $host_cpus =3D get_filtered_host_cpus($conf); > + get_vcpu_to_host_numa_map($conf, $host_cpus); > + } > +} > + > +=3Dhead2 pin_threads_to_cpus > + > +Pins the vCPU threads of a running guest to host CPUs according to the > +pinning, affinity and NUMA configuraton. > + > +Needs the guest to be running, since it querys QMP for the vCPU thread l= ist. > + > +=3Dcut > + > +sub pin_threads_to_cpus($conf, $vmid) { > + my $pinning =3D $conf->{pinning} // $DEFAULT_VCPU_PINNING; > + if ($pinning ne $DEFAULT_VCPU_PINNING) { > + my $host_cpus =3D get_filtered_host_cpus($conf); > + my $vcpu_map =3D get_vcpu_to_host_numa_map($conf, $host_cpus); > + > + my $cpuinfo =3D PVE::QemuServer::Monitor::mon_cmd($vmid, 'query-= cpus-fast'); > + for my $vcpu ($cpuinfo->@*) { > + > + my $vcpu_index =3D $vcpu->{'cpu-index'}; > + > + my $cpus; > + if ($pinning eq 'one-to-one') { > + $cpus =3D choose_single_cpu($vcpu_map, $host_cpus, $vcpu= _index); > + } elsif ($pinning eq 'numa') { > + $cpus =3D get_numa_cpulist($vcpu_map, $host_cpus, $vcpu_= index); I am still not sure what causes the sysbench performance degradation when using the numa setting. Altough the stream benchmark seems to benefit greatly, so perhaps the config is working correctly? I will check if there are other benchmarks to measure the performance implications. > + } > + > + die "no cpus selected for pinning vcpu $vcpu_index\n" > + if !defined($cpus); > + > + my $tid =3D $vcpu->{'thread-id'}; > + print "pinning vcpu $vcpu_index (thread $tid) to cpu(s) $cpu= s\n"; nit: The affinity settings uses the absolute path: /usr/bin/taskset IMO we either always use relative or absolute call for consistency > + run_command( > + ['taskset', '-c', '-p', $cpus, $vcpu->{'thread-id'}], lo= gfunc =3D> sub { }, > + ); Calling taskset for every cpu might create some overhead. If we have 32 cpus, we are creating 32 threads that call taskset. Instead we could create a big string containing all taskset commands and execute it in a single run_command call. e.g. my @taskset_cmd; push @taskset_cmd, "taskset -c -p $cpus $tid"; my $cmd_str =3D join(' && ', @taskset_cmds); run_command($cmd_str, logfunc =3D> sub { print "$_[0]\n" }); > + } > + } > +} > + > +1; [0] https://www.cs.virginia.edu/stream/ref.html