CPU Pinning Alder Lake

If you have an Alder Lake or newer Intel CPU, you probably want the option to load a qube on the P or E cores, and this might help someone who is new to xen.

https://dev.qubes-os.org/projects/core-admin/en/latest/libvirt.html
https://libvirt.org/formatdomain.html#cpu-allocation

Dom0

First, you need to add the following to GRUB
dom0_max_vcpus=4 dom0_vcpus_pin
If you want to use SMT read the notes at the end.

The CPU I’m using is the 12900K, which has 16 physical cores and 24 threads with SMT enabled.
The cores 0-15 are P cores and 16-23 are E cores.

If you want to move dom0 to the E cores, it can be done like this:

/usr/sbin/xl vcpu-pin Domain-0 0 20-23
/usr/sbin/xl vcpu-pin Domain-0 1 20-23
/usr/sbin/xl vcpu-pin Domain-0 2 20-23
/usr/sbin/xl vcpu-pin Domain-0 3 20-23

It will move dom0 from the first 4 cores to the last 4 cores, but it needs to be done every time the system boots.

xen-user.xml

It’s easy to make a xen-user.xml that uses jinja to automatically set the cpuset of a qube based on
the name, this is an example of how you can do it with prefixing the name with p-core or e-core.

You probably don’t want to just make two groups, this is just an example of how it can be done.

{% extends 'libvirt/xen.xml' %}
{% block basic %}
	<name>{{ vm.name }}</name>
        <uuid>{{ vm.uuid }}</uuid>
        {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list)
            or vm.maxmem == 0) -%}
            <memory unit="MiB">{{ vm.memory }}</memory>
        {% else -%}
            <memory unit="MiB">{{ vm.maxmem }}</memory>
        {% endif -%}
	<currentMemory unit="MiB">{{ vm.memory }}</currentMemory>
	{% if vm.name.startswith('p-core') -%}
	    <vcpu cpuset="0-15">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('e-core') -%}
	    <vcpu cpuset="16-23">{{ vm.vcpus }}</vcpu>
	{% else -%}
            <vcpu cpuset="0-15">{{ vm.vcpus }}</vcpu>
	{% endif -%}
{% endblock %}

If you want to see how your qubes are pinned, you can use the command:
/usr/sbin/xl vcpu-list

You can change the performance state of the individual cores:
/usr/sbin/xenpm set-scaling-governor 0 performance
This needs to be set every time the system boots, and the default setting is ondemand.

Qubes admin events

It’s possible to use qubes admin events to pin or migrate qubes, this method has the advantage it happens after the qube has started, and it’s possible to use on qubes that doesn’t use xen-user.xml

create a file: /usr/local/bin/cpu_pinning.py containing:

#!/usr/bin/env python3

import asyncio
import subprocess

import qubesadmin
import qubesadmin.events

# i5-13600k (smt=off)
P_CORES = '0-5'
E_CORES = '6-13'

tag = 'performance'

def _vcpu_pin(name, cores):
    cmd = ['xl', 'vcpu-pin', name, 'all', cores]   
    subprocess.run(cmd).check_returncode()

def pin_by_tag(vm, event, **kwargs):
    vm = app.domains[str(vm)]
    if tag in list(vm.tags):
        _vcpu_pin(vm.name, P_CORES)
        print(f'Pinned {vm.name} to P-cores')
    else:
        _vcpu_pin(vm.name, E_CORES)
        print(f'Pinned {vm.name} to E-cores')

app = qubesadmin.Qubes()
dispatcher = qubesadmin.events.EventsDispatcher(app)
dispatcher.add_handler('domain-start', pin_by_tag)
asyncio.run(dispatcher.listen_for_events())
alternative where VMs selected by name - CLICK

in @ymy s variant all VMs containing disp will be assigned to all cores, while other machines are bound to efficency cores (given with 4 p & 8 e)

#!/usr/bin/env python3

import asyncio
import subprocess

import qubesadmin
import qubesadmin.events

P_CORES = '0-11'
E_CORES = '4-11'


def _vcpu_pin(name, cores):
	cmd = [ 'xl', 'vcpu-pin', name, 'all', cores]
	subprocess.run(cmd).check_returncode()

def pin_by_tag(vm, event, **kwargs):
	vm = app.domains[str(vm)]
	if 'disp' in vm.name:
		_vcpu_pin(vm.name, P_CORES)
		print(f'Pinned {vm.name} to all Cores')
	else:
		_vcpu_pin(vm.name, E_CORES)
		print(f'Pinned {vm.name} to Efficency Cores')

app = qubesadmin.Qubes()
dispatcher = qubesadmin.events.EventsDispatcher(app)
dispatcher.add_handler('domain-start', pin_by_tag)
asyncio.run(dispatcher.listen_for_events())

create a file:/lib/systemd/system/cpu-pinning.service containing:

[Unit]
Description=Qubes CPU pinning
After=qubesd.service

[Service]
ExecStart=/usr/local/bin/cpu_pinning.py

[Install]
WantedBy=multi-user.target

follows by:
systemctl enable cpu-pinning.service
systemctl start cpu-pinning.service

Thanks to @noskb for shown how to use admin events.

CPU pools

You can use CPU pools as an alternative to pinning, which has the advantage of the pool configuration being defined in a single place.

If you are using CPU pools with SMT enabled, you probably need to use the credit scheduler, SMT doesn’t seem to work with credit2.

All cores start in the pool named Pool-0, dom0 needs to remain in Pool-0.

This is how you split the cores in a ecores and pcores pool, and leave 2 cores in Pool-0 for dom0.

/usr/sbin/xl cpupool-cpu-remove Pool-0 2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23
/usr/sbin/xl cpupool-create name="pcores" sched="credit2"
/usr/sbin/xl cpupool-cpu-add pcores 2,3,4,5,6,7,8,9,10,11,12,13,14,15
/usr/sbin/xl cpupool-create name="ecores" sched="credit2"
/usr/sbin/xl cpupool-cpu-add ecores 16,17,18,19,20,21,22,23

When the pools have been created, you can migrate domains to the pools with this command:

/usr/sbin/xl cpupool-migrate sys-net ecores

Using qrexec to migrate qubes

With CPU pools, it’s easy to make a qrexec rpc command to allow qubes to request to be moved to pcores.

Policy files /etc/qubes-rpc/policy/qubes.PCores

$anyvm dom0 allow

Qubes-rpc file /etc/qubes-rpc/qubes.PCores

#!/bin/bash

pool=$(/usr/sbin/xl list -c $QREXEC_REMOTE_DOMAIN | awk '{if(NR!=1) {print $7}}')

if [[ $pool == "ecores" ]]; then
	/usr/sbin/xl cpupool-migrate $QREXEC_REMOTE_DOMAIN pcores
fi

From any qube you can use the command qrexec-client-vm dom0 qubes.PCores and the qube will be moved to pcores, if the qube currently is placed on ecores.

This allows you to start qubes on E cores, and be moved to P cores when you start a program that needs to run on P cores.

The qrexec can be added to menu commands

Exec=bash -c ‘qrexec-client-vm dom0 qubes.PCores&&blender’

This is an example of how you can automatically move a qube to pcores when you start blender.

Notes on SMT.

Using sched-gran=core doesn’t seem to work with Alder Lake, xen dmesg has the following warning.

(XEN) ***************************************************
(XEN) Asymmetric cpu configuration.
(XEN) Falling back to sched-gran=cpu.
(XEN) ***************************************************

Using SMT can be dangerous, and not being able to use sched-gran=core makes it more dangerous.

Unless you understand and are okay with the consequences of enabling SMT, you should just leave it off.

If you are running your system with SMT enabled you can take advantage of the fact that E cores can’t hyper thread, even with SMT enabled the E cores are essentially running with SMT off.

You can also disable SMT for individual cores by changing the cpuset, where you don’t use the sibling core.

E.g. with SMT enabled, core 0 and 1 are running on the same physical core, but if you don’t use core 1 that core is no longer able to hyper thread.

Disabling SMT is that same as xen using the cpuset 0,2,4,6,8,10,12,14

17 Likes

Thanks for the guide. Would it be possible to do it more dynamically? If the qube has over x% cpu usage it should be on a P core, else on a E core.

Also would it be a bad idea to pin all my networking stuff on a single E core?

You could use xentop to gauge the load of the qubes and move them around, but I think it could be difficult to balance, and you might make xen work a lot harder to constantly move the VMs.

Dynamically changing the configuration is more of a bare metal approach, where you have a lot of processes in a single system. With qubes you pretty much know from the start what qubes you want on E and P cores, there really isn’t much point in switching them around.

I have everything that isn’t “user” qubes on E cores, dom0, sys, templates, updates, etc. I have pinned it all to the E cores.

1 Like

Good info! thanks

Thanks for a guide!
Can you please give some feedback how these actions improved your computer life?

Like:

  • Would you feel the difference in a blind test after that changes? Are those drastic and how would you feel the improvements?
  • Can it reduce fan noise in idle state due to the use of effective core instead of performance? E.g. on my Thinkpad T16 I have noisier Qubes OS compared to Kubuntu, I don’t know why.
  • Do you use something like youtube qube for playing videos? Maybe there are recommendations how to pin CPU for it to archive better performance or/and avoid effects on it from other qubes.
  • What are other possible advantages that I did not mention?

It’s not really something you can blind test, it’s not going to make your system x10 faster.

You can make a sysbench test qube on E cores, P cores, and P cores with the scaling governor set to performance, that will give you an idea of what the best case scenario gains are for your system.

I watch YouTube though Invidious in a disposable qube with Brave, I use the cores 0,2,4,8 which should be the same as running it without SMT, and the scaling for the cores are set to performance.

I have my P cores split into two groups, one without SMT and performance scaling enabled, and one with SMT running ondemand scaling.

2 Likes

I apologize in advance, english is not my native tongue. Thank you for this guide.
Is 8 e-core enough to run dom0, templates and qube system (net, firewall, usb, vpn)? Did qube startup speed decrease when using e-core or did you encounter any other limitations?
I’m going to take the msi z690-a pro ddr4 board and run an i7-13700k, but I’m assuming Qubes os is not optimized to use p and e cores. If not using e-core binding to qube, will the overall performance of all qube be degraded due to arbitrary assignment of e and p cores to qube?

8 E cores are more than enough, you could probably run it on 4 without any problems.

Pinning increase efficiency because it allows you to load more demanding qubes on the P cores, and it allows you to use the performance governor on specific cores.

It’s not going to make the CPU faster, you are just helping it by “manually” selecting what runs on P cores.

1 Like

In addition, pinning may reduce some of the churn in L1/L2 caches as each VM will be limited to a smaller subset of core caches. Useful if your workload is sensitive to that.

B

2 Likes

Is it possible for Qubes OS project somehow integrate the recommended approach out of the box for some future releases? I believe GNU/Linux does it nowadays (or not?).

And what is current behavior of Xen? Does it choose cores simply randomly as like they are all the same?

You need an input field in the settings menu to define the cpuset, similar to how you can define the number of vcpus.

<vcpu cpuset="0-15">{{ vm.vcpus }}</vcpu>

The vm.vcups value is assigned in the settings menu for the qube, you need a vm.cupset, but I don’t know if that is easy or hard to implement.

Without pinning, I think the cores are picked by least loaded core first. Even if xen know which is P and E cores, I don’t see how it would know where to place a given qube, not without you telling it what to do.

It would be interesting to know how GNU/Linux distros handle this new CPU-type separation, do they use it.

This article gives an overview of how the hybrid scheduling works.

1 Like

Can you check if I did it right.

  1. I added dom0_max_vcpus=4 dom0_vcpus_pin in /etc/default/grub between ucode=scan and smt=off.

  2. After logging in, I assign kernels to dom0 using:

/usr/sbin/xl vcpu-pin Domain-0 0 0 20-23
/usr/sbin/xl vcpu-pin Domain-0 1 20-23
/usr/sbin/xl vcpu-pin Domain-0 2 20-23
/usr/sbin/xl vcpu-pin Domain-0 3 20-23

Where should I add the autorun script in dom0 for these commands?

  1. In your example you used the full configuration from block basic and made changes there with xen-user.xml:
{% extends 'libvirt/xen.xml' %}
{% block basic %}
	<name>{{{vm.name }}</name>
        <uuid>{{{vm.uuid }}}</uuid>
        {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list)
            or vm.maxmem == 0) -%}
            <memory unit="MiB">{{ vm.memory }}</memory>
        {% else -%}
            <memory unit="MiB">{{{ vm.maxmem }}}</memory>
        {% endif -%}
	<currentMemory unit="MiB">{{{ vm.memory }}}</currentMemory>
	{% if vm.name.startswith('p-core') -%}
	    <vcpu cpuset="0-15">{{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('e-core') -%}
	    <vcpu cpuset="16-23">{{{vm.vcpus }}}</vcpu>
	{% else -%}
            <vcpu cpuset="0-15">{{{vm.vcpus }}}</vcpu>
	{% endif -%}
{% endblock %}

I created an xen-user.xml file in /etc/qubes/templates/libvirt/ with the following content:

{% extends 'libvirt/xen.xml' %}
{% block basic %}
        {% if vm.name.startswith('sys-') -%}
            <vcpu cpuset="16-23">{{ vm.vcpus }}</vcpu>
        {% elif vm.name.startswith('vault') -%}
            <vcpu cpuset="16-23">{{{vm.vcpus }}}</vcpu>
        {% else -%}
            <vcpu cpuset="0-15">{{{vm.vcpus }}}</vcpu>
        {% endif -%}
        {{ super() }}
{% endblock %}

Should I instead of the {{ super() }} expression, copy the entire configuration from block basic and modify it to suit me? It generally works, but I’m not sure if the template configuration is correct.

Output of the xl vcpus-list command:

Name                                ID  VCPU   CPU State   Time(s) Affinity (Hard / Soft)
Domain-0                             0     0   21   -b-     377.4  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     1   23   r--     419.8  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     2   20   -b-     380.2  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     3   22   -b-     361.3  20-23 / 0,2,4,6,8,10,12,14,16-23
sys-net                             10     0   16   -b-      67.0  16-23 / 0-23
sys-net                             10     1   18   -b-      60.0  16-23 / 0-23
sys-net-dm                          11     0    8   -b-       9.6  all / 0-23
sys-firewall                        12     0   19   -b-      88.9  16-23 / 0-23
sys-firewall                        12     1   17   -b-      77.6  16-23 / 0-23
disp2909                            13     0    6   -b-     383.5  0-15 / 0-23
disp2909                            13     1   12   -b-     387.2  0-15 / 0-23
vault                               18     0   18   -b-       5.9  16-23 / 0-23
vault                               18     1   16   -b-       3.7  16-23 / 0-23

How can I see the template of a running vm to see what configurations it uses? xl config is not working in dom0.
The xl vcpus-list duplicates vm names with the same id, is this normal? Also xen-user.xml does not change affinity stub-domain, xl vcpus-list sys-net-dm shows affinity “all”.

1 Like

Looks like it’s working.

xl vcpus-list adds one line for each VCPU used by the VM, the hard affinity tells you which cores the VM is allowed to use, and CPU which actual core the VCPU is using.

sys-net-dm you need to move like you did with dom0, that type of VM isn’t created using the xen.xml, you can see the affinity is all, because xen.xml isn’t used to modify the cpuset.

super() might work in xen.xml, but it was given me some issues. You don’t want to add to the current config, you wan’t to change the vcpu lines, but if it works it works. I was having issues with it just overwriting my changes, but I could have been using it wrong.

1 Like

Could you share your xen-user.xml so i can reproduce your problem with {{ super() }}?

Here is my xen-user.xml

{% extends 'libvirt/xen.xml' %}
{% block basic %}
	<name>{{ vm.name }}</name>
        <uuid>{{ vm.uuid }}</uuid>
        {% if ((vm.virt_mode == 'hvm' and vm.devices['pci'].persistent() | list)
            or vm.maxmem == 0) -%}
            <memory unit="MiB">{{ vm.memory }}</memory>
        {% else -%}
            <memory unit="MiB">{{ vm.maxmem }}</memory>
        {% endif -%}
	<currentMemory unit="MiB">{{ vm.memory }}</currentMemory>
	{% if vm.name.startswith('sys') -%}
	    <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('debian-') -%}
	    <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('kicksecure-') -%}
	    <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('whonix-') -%}
	    <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('disp-mgmt') -%}
       <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
	{% elif vm.name.startswith('user') -%}
	    <vcpu cpuset="8-15">{{ vm.vcpus }}</vcpu>
	{% else -%}
       <vcpu cpuset="0,2,4,6">{{ vm.vcpus }}</vcpu>
	{% endif -%}
{% endblock %}

With this configuration I run all qube without errors, assigning the correct kernels from xen-user.xml.

{% extends 'libvirt/xen.xml' %}
{% block basic %}
        {% if vm.name.startswith('sys') -%}
            <vcpu cpuset="16-19">{{ vm.vcpus }}</vcpu>
        {% elif vm.name.startswith('debian-') -%}
            <vcpu cpuset="16-19">{{{ vm.vcpus }}}</vcpu>
        {% elif vm.name.startswith('kicksecure-') -%}
            <vcpu cpuset="16-19">{{{ vm.vcpus }}}</vcpu>
        {% elif vm.name.startswith('whonix-') -%}
            <vcpu cpuset="16-19">{{{ vm.vcpus }}}</vcpu>
        {% elif vm.name.startswith('disp-mgmt') -%}
       <vcpu cpuset="16-19">{{{ vm.vcpus }}}</vcpu>
        {% elif vm.name.startswith('user') -%}
            <vcpu cpuset="8-15">{{{ vm.vcpus }}}</vcpu>
        {% else -%}
       <vcpu cpuset="0,2,4,6">{{{vm.vcpus }}}</vcpu>
        {% endif -%}
        {{ super() }}
{% endblock %}

Output xl vcpu-list:

Name                                ID  VCPU   CPU State   Time(s) Affinity (Hard / Soft)
Domain-0                             0     0   23   r--     628.3  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     1   22   r--     696.7  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     2   21   r--     628.5  20-23 / 0,2,4,6,8,10,12,14,16-23
Domain-0                             0     3   20   -b-     606.8  20-23 / 0,2,4,6,8,10,12,14,16-23
disp4431                            20     0    2   -b-       4.0  0,2,4,6 / 0-23
disp4431                            20     1    6   -b-       2.3  0,2,4,6 / 0-23
sys-whonix                          21     0   19   -b-      15.2  16-19 / 0-23
sys-whonix                          21     1   17   -b-      14.3  16-19 / 0-23
sys-net                             22     0   17   -b-       7.3  16-19 / 0-23
sys-net                             22     1   19   -b-       6.1  16-19 / 0-23
sys-net-dm                          23     0   10   -b-       2.9  all / 0-23
sys-firewall                        24     0   18   -b-       8.4  16-19 / 0-23
sys-firewall                        24     1   16   -b-       4.5  16-19 / 0-23
vault                               25     0    0   -b-       3.7  0,2,4,6 / 0-23
vault                               25     1    4   -b-       1.6  0,2,4,6 / 0-23
2 Likes

It also works for me now, I probably used super at the start, which is why it was overwriting my changes.

Yes, I also tried to call it before the vcpus changes, then I did some research and realized that calling {{ super() }} must be after all the changes related to the xen.xml block from xen-user.xml. If {{ super() }} is called before changes, it will probably break the parent template configuration, in my case the wrong {{vm.vcpus }} was assigned and qube would not start.

1 Like