├── requirements.txt ├── runit └── throttled │ └── run ├── openrc └── throttled ├── pyproject.toml ├── systemd └── throttled.service ├── LICENSE ├── package └── PKGBUILD ├── .gitignore ├── install.sh ├── etc └── throttled.conf ├── mmio.py ├── README.md └── throttled.py /requirements.txt: -------------------------------------------------------------------------------- 1 | configparser==7.0.0 2 | dbus-python==1.3.2 3 | PyGObject==3.48.2 4 | -------------------------------------------------------------------------------- /runit/throttled/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | PYTHONUNBUFFERED=1 exec /opt/throttled/venv/bin/python3 /opt/throttled/throttled.py 3 | -------------------------------------------------------------------------------- /openrc/throttled: -------------------------------------------------------------------------------- 1 | #!/sbin/openrc-run 2 | command="/opt/throttled/venv/bin/python3 /opt/throttled/throttled.py" 3 | pidfile=${pidfile-/var/run/throttled.pid} 4 | description="Stop Intel throttling" 5 | command_background="yes" 6 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.black] 2 | line-length = 120 3 | skip-string-normalization = true 4 | include = '\.pyi?$' 5 | exclude = ''' 6 | /( 7 | \.git 8 | | \.hg 9 | | \.mypy_cache 10 | | \.tox 11 | | \.venv 12 | | venv 13 | | _build 14 | | buck-out 15 | | build 16 | | dist 17 | )/ 18 | ''' 19 | -------------------------------------------------------------------------------- /systemd/throttled.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Stop Intel throttling 3 | 4 | [Service] 5 | Type=simple 6 | ExecStart=/opt/throttled/venv/bin/python3 /opt/throttled/throttled.py 7 | # Setting PYTHONUNBUFFERED is necessary to see the output of this service in the journal 8 | Environment=PYTHONUNBUFFERED=1 9 | 10 | [Install] 11 | WantedBy=multi-user.target 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Francesco Palmarini 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package/PKGBUILD: -------------------------------------------------------------------------------- 1 | 2 | pkgname=throttled 3 | pkgver=0.7 4 | pkgrel=4 5 | pkgdesc="Workaround for Intel throttling issues in Linux." 6 | arch=('any') 7 | url="https://github.com/erpalma/throttled" 8 | license=('MIT') 9 | depends=('python-dbus' 'python-psutil' 'python-gobject') 10 | conflicts=('lenovo-throttling-fix-git' 'lenovo-throttling-fix') 11 | replaces=('lenovo-throttling-fix') 12 | backup=('etc/throttled.conf') 13 | source=("git+https://github.com/Hyper-KVM/throttled.git#branch=openrc") 14 | sha256sums=('SKIP') 15 | 16 | 17 | build() { 18 | cd "${srcdir}/throttled/" 19 | python -m compileall *.py 20 | } 21 | 22 | package() { 23 | cd "${srcdir}/throttled/" 24 | install -Dm644 etc/throttled.conf "$pkgdir"/etc/throttled.conf 25 | install -Dm644 systemd/throttled.service "$pkgdir"/usr/lib/systemd/system/throttled.service 26 | install -Dm755 throttled.py "$pkgdir"/usr/lib/$pkgname/throttled.py 27 | install -Dm755 openrc/throttled "$pkgdir/etc/init.d/throttled" 28 | install -Dm755 mmio.py "$pkgdir"/usr/lib/$pkgname/mmio.py 29 | cp -a __pycache__ "$pkgdir"/usr/lib/$pkgname/ 30 | install -Dm644 LICENSE "$pkgdir"/usr/share/licenses/$pkgname/LICENSE 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | -------------------------------------------------------------------------------- /install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | LEGACY_INSTALL_DIR="/opt/lenovo_fix" 4 | INSTALL_DIR="/opt/throttled" 5 | 6 | INIT="$(ps --no-headers -o comm 1)" 7 | 8 | if [ "$INIT" = "systemd" ]; then 9 | systemctl stop lenovo_fix.service >/dev/null 2>&1 10 | systemctl stop throttled.service >/dev/null 2>&1 11 | elif [ "$INIT" = "runit" ]; then 12 | sv down lenovo_fix >/dev/null 2>&1 13 | sv down throttled >/dev/null 2>&1 14 | elif [ "$INIT" = "init" ]; then 15 | rc-service lenovo_fix stop >/dev/null 2>&1 16 | rc-service throttled stop >/dev/null 2>&1 17 | fi 18 | 19 | mv "$LEGACY_INSTALL_DIR" "$INSTALL_DIR" >/dev/null 2>&1 20 | rm -f "$INSTALL_DIR/lenovo_fix.py" >/dev/null 2>&1 21 | mkdir -p "$INSTALL_DIR" >/dev/null 2>&1 22 | set -e 23 | 24 | cd "$(dirname "$0")" 25 | 26 | if [ -f /etc/lenovo_fix.conf ]; then 27 | echo "Updating config filename" 28 | mv /etc/lenovo_fix.conf /etc/throttled.conf 29 | fi 30 | echo "Copying config file" 31 | if [ ! -f /etc/throttled.conf ]; then 32 | cp etc/throttled.conf /etc 33 | else 34 | echo "Config file already exists, skipping" 35 | fi 36 | 37 | if [ "$INIT" = "systemd" ]; then 38 | echo "Copying systemd service file" 39 | cp systemd/throttled.service /etc/systemd/system 40 | rm -f /etc/systemd/system/lenovo_fix.service >/dev/null 2>&1 41 | elif [ "$INIT" = "runit" ]; then 42 | echo "Copying runit service file" 43 | cp -R runit/throttled /etc/sv/ 44 | rm -r /etc/sv/lenovo_fix >/dev/null 2>&1 45 | elif [ "$INIT" = "init" ]; then 46 | echo "Copying OpenRC service file" 47 | cp -R openrc/throttled /etc/init.d/throttled 48 | rm -f /etc/init.d/lenovo_fix >/dev/null 2>&1 49 | chmod 755 /etc/init.d/throttled 50 | fi 51 | 52 | echo "Copying core files" 53 | cp requirements.txt throttled.py mmio.py "$INSTALL_DIR" 54 | echo "Building virtualenv" 55 | cd "$INSTALL_DIR" 56 | /usr/bin/python3 -m venv venv 57 | . venv/bin/activate 58 | pip install wheel 59 | pip install -r requirements.txt 60 | 61 | if [ "$INIT" = "systemd" ]; then 62 | echo "Enabling and starting systemd service" 63 | systemctl daemon-reload 64 | systemctl enable throttled.service 65 | systemctl restart throttled.service 66 | elif [ "$INIT" = "runit" ]; then 67 | echo "Enabling and starting runit service" 68 | ln -sv /etc/sv/throttled /var/service/ 69 | sv up throttled 70 | elif [ "$INIT" = "init" ]; then 71 | echo "Enabling and starting OpenRC service" 72 | rc-update add throttled default 73 | rc-service throttled start 74 | fi 75 | 76 | echo "All done." 77 | -------------------------------------------------------------------------------- /etc/throttled.conf: -------------------------------------------------------------------------------- 1 | [GENERAL] 2 | # Enable or disable the script execution 3 | Enabled: True 4 | # SYSFS path for checking if the system is running on AC power 5 | Sysfs_Power_Path: /sys/class/power_supply/AC*/online 6 | # Auto reload config on changes 7 | Autoreload: True 8 | 9 | ## Settings to apply while connected to Battery power 10 | [BATTERY] 11 | # Update the registers every this many seconds 12 | Update_Rate_s: 30 13 | # Max package power for time window #1 14 | PL1_Tdp_W: 29 15 | # Time window #1 duration 16 | PL1_Duration_s: 28 17 | # Max package power for time window #2 18 | PL2_Tdp_W: 44 19 | # Time window #2 duration 20 | PL2_Duration_S: 0.002 21 | # Max allowed temperature before throttling 22 | Trip_Temp_C: 85 23 | # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL) 24 | cTDP: 0 25 | # Disable BDPROCHOT (EXPERIMENTAL) 26 | Disable_BDPROCHOT: False 27 | 28 | ## Settings to apply while connected to AC power 29 | [AC] 30 | # Update the registers every this many seconds 31 | Update_Rate_s: 5 32 | # Max package power for time window #1 33 | PL1_Tdp_W: 44 34 | # Time window #1 duration 35 | PL1_Duration_s: 28 36 | # Max package power for time window #2 37 | PL2_Tdp_W: 44 38 | # Time window #2 duration 39 | PL2_Duration_S: 0.002 40 | # Max allowed temperature before throttling 41 | Trip_Temp_C: 95 42 | # Set HWP energy performance hints to 'performance' on high load (EXPERIMENTAL) 43 | # Uncomment only if you really want to use it 44 | # HWP_Mode: False 45 | # Set cTDP to normal=0, down=1 or up=2 (EXPERIMENTAL) 46 | cTDP: 0 47 | # Disable BDPROCHOT (EXPERIMENTAL) 48 | Disable_BDPROCHOT: False 49 | 50 | # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)! 51 | [UNDERVOLT.BATTERY] 52 | # CPU core voltage offset (mV) 53 | CORE: 0 54 | # Integrated GPU voltage offset (mV) 55 | GPU: 0 56 | # CPU cache voltage offset (mV) 57 | CACHE: 0 58 | # System Agent voltage offset (mV) 59 | UNCORE: 0 60 | # Analog I/O voltage offset (mV) 61 | ANALOGIO: 0 62 | 63 | # All voltage values are expressed in mV and *MUST* be negative (i.e. undervolt)! 64 | [UNDERVOLT.AC] 65 | # CPU core voltage offset (mV) 66 | CORE: 0 67 | # Integrated GPU voltage offset (mV) 68 | GPU: 0 69 | # CPU cache voltage offset (mV) 70 | CACHE: 0 71 | # System Agent voltage offset (mV) 72 | UNCORE: 0 73 | # Analog I/O voltage offset (mV) 74 | ANALOGIO: 0 75 | 76 | # [ICCMAX.AC] 77 | # # CPU core max current (A) 78 | # CORE: 79 | # # Integrated GPU max current (A) 80 | # GPU: 81 | # # CPU cache max current (A) 82 | # CACHE: 83 | 84 | # [ICCMAX.BATTERY] 85 | # # CPU core max current (A) 86 | # CORE: 87 | # # Integrated GPU max current (A) 88 | # GPU: 89 | # # CPU cache max current (A) 90 | # CACHE: 91 | -------------------------------------------------------------------------------- /mmio.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Stripped down version from https://github.com/vsergeev/python-periphery/blob/master/periphery/mmio.py 3 | ''' 4 | import mmap 5 | import os 6 | import struct 7 | import sys 8 | 9 | # Alias long to int on Python 3 10 | if sys.version_info[0] >= 3: 11 | long = int 12 | 13 | 14 | class MMIOError(IOError): 15 | """Base class for MMIO errors.""" 16 | pass 17 | 18 | 19 | class MMIO(object): 20 | def __init__(self, physaddr, size): 21 | """Instantiate an MMIO object and map the region of physical memory 22 | specified by the address base `physaddr` and size `size` in bytes. 23 | Args: 24 | physaddr (int, long): base physical address of memory region. 25 | size (int, long): size of memory region. 26 | Returns: 27 | MMIO: MMIO object. 28 | Raises: 29 | MMIOError: if an I/O or OS error occurs. 30 | TypeError: if `physaddr` or `size` types are invalid. 31 | """ 32 | self.mapping = None 33 | self._open(physaddr, size) 34 | 35 | def __del__(self): 36 | self.close() 37 | 38 | def __enter__(self): 39 | pass 40 | 41 | def __exit__(self, t, value, traceback): 42 | self.close() 43 | 44 | def _open(self, physaddr, size): 45 | if not isinstance(physaddr, (int, long)): 46 | raise TypeError("Invalid physaddr type, should be integer.") 47 | if not isinstance(size, (int, long)): 48 | raise TypeError("Invalid size type, should be integer.") 49 | 50 | pagesize = os.sysconf(os.sysconf_names['SC_PAGESIZE']) 51 | 52 | self._physaddr = physaddr 53 | self._size = size 54 | self._aligned_physaddr = physaddr - (physaddr % pagesize) 55 | self._aligned_size = size + (physaddr - self._aligned_physaddr) 56 | 57 | try: 58 | fd = os.open("/dev/mem", os.O_RDWR | os.O_SYNC) 59 | except OSError as e: 60 | raise MMIOError(e.errno, "Opening /dev/mem: " + e.strerror) 61 | 62 | try: 63 | self.mapping = mmap.mmap( 64 | fd, self._aligned_size, flags=mmap.MAP_SHARED, prot=mmap.PROT_WRITE, offset=self._aligned_physaddr) 65 | except OSError as e: 66 | raise MMIOError(e.errno, "Mapping /dev/mem: " + e.strerror) 67 | 68 | try: 69 | os.close(fd) 70 | except OSError as e: 71 | raise MMIOError(e.errno, "Closing /dev/mem: " + e.strerror) 72 | 73 | # Methods 74 | 75 | def _adjust_offset(self, offset): 76 | return offset + (self._physaddr - self._aligned_physaddr) 77 | 78 | def _validate_offset(self, offset, length): 79 | if (offset + length) > self._aligned_size: 80 | raise ValueError("Offset out of bounds.") 81 | 82 | def read32(self, offset): 83 | """Read 32-bits from the specified `offset` in bytes, relative to the 84 | base physical address of the MMIO region. 85 | Args: 86 | offset (int, long): offset from base physical address, in bytes. 87 | Returns: 88 | int: 32-bit value read. 89 | Raises: 90 | TypeError: if `offset` type is invalid. 91 | ValueError: if `offset` is out of bounds. 92 | """ 93 | if not isinstance(offset, (int, long)): 94 | raise TypeError("Invalid offset type, should be integer.") 95 | 96 | offset = self._adjust_offset(offset) 97 | self._validate_offset(offset, 4) 98 | return struct.unpack("=L", self.mapping[offset:offset + 4])[0] 99 | 100 | def write32(self, offset, value): 101 | """Write 32-bits to the specified `offset` in bytes, relative to the 102 | base physical address of the MMIO region. 103 | Args: 104 | offset (int, long): offset from base physical address, in bytes. 105 | value (int, long): 32-bit value to write. 106 | Raises: 107 | TypeError: if `offset` or `value` type are invalid. 108 | ValueError: if `offset` or `value` are out of bounds. 109 | """ 110 | if not isinstance(offset, (int, long)): 111 | raise TypeError("Invalid offset type, should be integer.") 112 | if not isinstance(value, (int, long)): 113 | raise TypeError("Invalid value type, should be integer.") 114 | if value < 0 or value > 0xffffffff: 115 | raise ValueError("Value out of bounds.") 116 | 117 | offset = self._adjust_offset(offset) 118 | self._validate_offset(offset, 4) 119 | self.mapping[offset:offset + 4] = struct.pack("=L", value) 120 | 121 | def close(self): 122 | """Unmap the MMIO object's mapped physical memory.""" 123 | if self.mapping is None: 124 | return 125 | 126 | self.mapping.close() 127 | self.mapping = None 128 | 129 | self._fd = None 130 | 131 | # String representation 132 | 133 | def __str__(self): 134 | return "MMIO 0x%08x (size=%d)" % (self.base, self.size) 135 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Fix Intel CPU Throttling on Linux 2 | This tool was originally developed to fix Linux CPU throttling issues affecting Lenovo T480 / T480s / X1C6 as described [here](https://www.reddit.com/r/thinkpad/comments/870u0a/t480s_linux_throttling_bug/). 3 | 4 | The CPU package power limit (PL1/2) is forced to a value of **44 W** (29 W on battery) and the temperature trip point to **95 'C** (85 'C on battery) by overriding default values in MSR and MCHBAR every 5 seconds (30 on battery) to block the Embedded Controller from resetting these values to default. 5 | 6 | On systems where the EC doesn't reset the values (ex: ASUS Zenbook UX430UNR), the power limit can be altered by using the official intel_rapl driver (see [Static fix](#static-fix) for more information) 7 | 8 | ## Warning! 9 | The latest commit (30 Oct 2021) switched from the legacy name `lenovo_fix` for the tool/config/system to a more uniform `throttled`. The install script was updated, but please report back if anything breaks. 10 | 11 | ### Tested hardware 12 | Other users have confirmed that the tool is also working for these laptops: 13 | - Lenovo T470s, T480, T480s, X1C5, X1C6, X1C8, T580, L590, L490, L480, T470, X280, X390, ThinkPad Anniversary Edition 25, E590 w/ RX 550X, P43s, E480, E580, T14 Gen 1, P14s Gen 1, T15 Gen 1, P15s Gen 1, E14 Gen 2, X1 Extreme Gen 4 14 | - Dell XPS 9365, 9370, 9550, 7390 2-in-1, Latitude 7390 2-in-1, Inspiron 16 Plus 7620 15 | - Microsoft Surface Book 2 16 | - HP Probook 470 G5, Probook 450 G5, ZBook Firefly 15 G7 17 | 18 | 19 | I will keep this list updated. 20 | 21 | ### Is this tool really doing something on my PC?? 22 | I suggest you to use the excellent **[s-tui](https://github.com/amanusk/s-tui)** tool to check and monitor the CPU usage, frequency, power and temperature under load! 23 | 24 | ### Undervolt 25 | The tool supports **undervolting** the CPU by configuring voltage offsets for CPU, cache, GPU, System Agent and Analog I/O planes. The tool will re-apply undervolt on resume from standby and hibernate by listening to DBus signals. You can now either use the `UNDERVOLT` key in config to set global values or the `UNDERVOLT.AC` and `UNDERVOLT.BATTERY` keys to selectively set undervolt values for the two power profiles. 26 | 27 | **===== Notice that undervolt is typically locked from 10th gen onwards! =====** 28 | 29 | ### IccMax (EXPERTS ONLY) 30 | The tool now supports overriding the **IccMax** by configuring the maximum allowed current for CPU, cache and GPU planes. The tool will re-apply IccMax on resume from standby and hibernate. You can now either use the `ICCMAX` key in config to set global values or the `ICCMAX.AC` and `ICCMAX.BATTERY` keys to selectively set current values for the two power profiles. **NOTE:** the values specified in the config file are the actual current limit of your system, so those are not a offset from the default values as for the undervolt. As such, you should first find your system default values with the `--monitor` command. 31 | 32 | ### HWP override (EXPERIMENTAL) 33 | I have found that under load my CPU was not always hitting max turbo frequency, in particular when using one/two cores only. For instance, when running [prime95](https://www.mersenne.org/download/) (1 core, test #1) my CPU is limited to about 3500 MHz over the theoretical 4000 MHz maximum. The reason is the value for the HWP energy performance [hints](http://manpages.ubuntu.com/manpages/artful/man8/x86_energy_perf_policy.8.html). By default TLP sets this value to `balance_performance` on AC in order to reduce the power consumption/heat in idle. By setting this value to `performance` I was able to reach 3900 MHz in the prime95 single core test, achieving a +400 MHz boost. Since this value forces the CPU to full speed even during idle, a new experimental feature allows to automatically set HWP to performance under load and revert it to balanced when idle. This feature can be enabled (in AC mode *only*) by setting to `True` the `HWP_Mode` parameter in the throttled config file : https://github.com/erpalma/throttled/blob/master/etc/throttled.conf#L41 . 34 | 35 | I have run **[Geekbench 4](https://browser.geekbench.com/v4/cpu/8656840)** and now I can get a score of 5391/17265! On `balance_performance` I can reach only 4672/16129, so **15% improvement** in single core and 7% in multicore, not bad ;) 36 | 37 | ### setting cTDP (EXPERIMENTAL) 38 | On a lot of modern CPUs from Intel one can configure the TDP up or down based on predefined profiles. This is what this option does. For a i7-8650U normal would be 15W, up profile is setting it to 25W and down to 10W. You can lookup the values of your CPU at the Intel product website. 39 | 40 | ## Requirements 41 | A stripped down version of the python module `python-periphery` is now built-in and it is used for accessing the MCHBAR register by memory mapped I/O. You also need `dbus` and `gobject` python bindings for listening to dbus signals on resume from sleep/hibernate. 42 | 43 | ### Writing to MSR and PCI BAR 44 | Some time ago a feature called [Kernel Lockdown](https://lwn.net/Articles/706637/) was added to Linux. Kernel Lockdown automatically enables some security measures when Secure Boot is enabled, among them restricted access to MSR and PCI BAR via /dev/mem, which this tool requires. There are two ways to get around this: You can either disable Secure Boot in your firmware settings, or disable the Kernel Lockdown LSM. 45 | 46 | The LSM can be disabled this way: Check the contents of the file `/sys/kernel/security/lsm` (example contents: `capability,lockdown,yama`). Take the contents of the file, remove `lockdown` and add the rest as a kernel parameter, like this: `lsm=capability,yama`. Reboot and Kernel Lockdown will be disabled! 47 | 48 | As of Linux 5.9, kernel messages will be logged whenever the script writes to MSR registers. These aren't a problem for now, but there's some indication that future kernels may restrict MSR writes from userspace by default. This is being tracked by issue #215. The messages will look something like: 49 | ``` 50 | [ 324.833543] msr: Write to unrecognized MSR 0x1a2 by python3 51 | Please report to x86@kernel.org 52 | ``` 53 | 54 | Note that some kernels (e.g. [linux-hardened](https://www.archlinux.org/packages/extra/x86_64/linux-hardened/)) will prevent from writing to `/dev/mem` too. Specifically, you need a kernel with `CONFIG_DEVMEM` and `CONFIG_X86_MSR` set. 55 | 56 | ### Thermald 57 | As discovered by *DEvil0000* the Linux Thermal Monitor ([thermald](https://github.com/intel/thermal_daemon)) can conflict with the purpose of this tool. In particular, thermald might be pre-installed (e.g. on Ubuntu) and configured in such a way to keep the CPU temperature below a certain threshold (~80 'C) by applying throttling or messing up with RAPL or other CPU-specific registers. I strongly suggest to either disable/uninstall it or to review its default configuration. 58 | 59 | Note that on some platforms thermald seems to be required. E.g. Dell Latitude 7320 i7-1185G7, Linux 6.6.48, Void Linux musl, still runs into throttling issues unless `thermald --adaptive` is running. If you are running throttled but still seeing throttling issues, try testing with `thermald --adaptive` running as well. 60 | 61 | ### Update 62 | The tool is now running with Python3 by default (tested w/ 3.6) and a virtualenv is automatically created in `/opt/throttled`. Python2 should probably still work. 63 | 64 | ## Installation 65 | 66 | ### Arch Linux [community package](https://www.archlinux.org/packages/community/x86_64/throttled/): 67 | ``` 68 | pacman -S throttled 69 | sudo systemctl enable --now throttled.service 70 | ``` 71 | Thanks to *felixonmars* for creating and maintaining this package. 72 | 73 | ### Artix Linux 74 | ``` 75 | makepkg -si 76 | sudo rc-update add throttled default 77 | sudo rc-service throttled start 78 | ``` 79 | 80 | ### Alpine Linux 81 | You need to be on `edge` and have the `testing` repository enabled. 82 | ``` 83 | doas apk add throttled 84 | doas rc-update add throttled 85 | doas rc-service throttled start 86 | ``` 87 | 88 | ### Debian/Ubuntu 89 | ``` 90 | sudo apt install git build-essential python3-dev libdbus-glib-1-dev libgirepository1.0-dev libcairo2-dev python3-cairo-dev python3-venv python3-wheel 91 | git clone https://github.com/erpalma/throttled.git 92 | sudo ./throttled/install.sh 93 | ``` 94 | If you own a X1C6 you can also check a tutorial for Ubuntu 18.04 [here](https://mensfeld.pl/2018/05/lenovo-thinkpad-x1-carbon-6th-gen-2018-ubuntu-18-04-tweaks/). 95 | 96 | You should make sure that **_thermald_** is not setting it back down. Stopping/disabling it will do the trick: 97 | ``` 98 | sudo systemctl stop thermald.service 99 | sudo systemctl disable thermald.service 100 | ``` 101 | If you want to keep it disabled even after a package update you should also run: 102 | ``` 103 | sudo systemctl mask thermald.service 104 | ``` 105 | 106 | In order to check if the service is well started, you could run: 107 | ``` 108 | systemctl status throttled 109 | ``` 110 | 111 | ### Fedora 112 | A [copr repository](https://copr.fedorainfracloud.org/coprs/abn/throttled/) is available and can be used as detailed below. You can find the configuration installed at `/etc/throttled.conf`. The issue tracker for this packaging is available [here](https://github.com/abn/throttled-rpm/issues). 113 | ``` 114 | sudo dnf copr enable abn/throttled 115 | sudo dnf install -y throttled 116 | 117 | sudo systemctl enable --now throttled 118 | ``` 119 | 120 | If you prefer to install from source, you can use the following commands. 121 | ``` 122 | sudo dnf install python3-cairo-devel cairo-gobject-devel gobject-introspection-devel dbus-glib-devel python3-devel make libX11-devel 123 | git clone https://github.com/erpalma/throttled.git 124 | sudo ./throttled/install.sh 125 | ``` 126 | Feedback about Fedora installation is welcome. 127 | 128 | ### Fedora Silverblue 129 | 130 | 131 | Download the `.repo` file matching your Fedora on the [copr repository page](https://copr.fedorainfracloud.org/coprs/abn/throttled/) then copy it to `/etc/yum.repos.d/`. 132 | 133 | You can then install the package: 134 | 135 | ```console 136 | rpm-ostree override remove thermald 137 | rpm-ostree install throttled 138 | systemctl reboot 139 | 140 | sudo systemctl enable --now throttled 141 | ``` 142 | 143 | ### openSUSE 144 | User *brycecordill* reported that the following dependencies are required for installing in openSUSE, tested on openSUSE 15.0 Leap. 145 | ``` 146 | sudo zypper install gcc make python3-devel dbus-1-glib-devel python3-cairo-devel cairo-devel python3-gobject-cairo gobject-introspection-devel 147 | git clone https://github.com/erpalma/throttled.git 148 | sudo ./throttled/install.sh 149 | ``` 150 | 151 | ### Gentoo 152 | The ebuild is now in the official tree! 153 | ``` 154 | sudo emerge -av sys-power/throttled 155 | # when using OpenRC: 156 | rc-update add throttled default 157 | /etc/init.d/throttled start 158 | # when using systemd: 159 | systemctl enable --now throttled.service 160 | ``` 161 | 162 | ### Solus 163 | ``` 164 | sudo eopkg it -c system.devel 165 | sudo eopkg it git python3-devel dbus-glib-devel python3-cairo-devel libcairo-devel python3-gobject-devel 166 | git clone https://github.com/erpalma/throttled.git 167 | sudo ./throttled/install.sh 168 | ``` 169 | 170 | ### Void 171 | 172 | The installation itself will create a runit service as throttled, enable it and start it. Before installation, make sure dbus is running `sv up dbus`. 173 | 174 | ``` 175 | sudo xbps-install -Sy gcc git python3-devel dbus-glib-devel libgirepository-devel cairo-devel python3-wheel pkg-config make 176 | 177 | git clone https://github.com/erpalma/throttled.git 178 | 179 | sudo ./throttled/install.sh 180 | ``` 181 | 182 | ### Uninstall 183 | To permanently stop and disable the execution just issue: 184 | ``` 185 | systemctl stop throttled.service 186 | systemctl disable throttled.service 187 | ``` 188 | 189 | If you're running runit instead of systemd: 190 | ``` 191 | sv down throttled 192 | rm /var/service/throttled 193 | ``` 194 | 195 | If you're using OpenRC instead of systemd: 196 | ``` 197 | rc-service throttled stop 198 | rc-update del throttled default 199 | ``` 200 | 201 | If you also need to remove the tool from the system: 202 | ``` 203 | rm -rf /opt/throttled /etc/systemd/system/throttled.service 204 | # to purge also the config file 205 | rm /etc/throttled.conf 206 | ``` 207 | On Arch you should probably use `pacman -R lenovo-throttling-fix-git` instead. 208 | 209 | ### Update 210 | If you update the tool you should manually check your config file for changes or additional features and modify it accordingly. The update process is then as simple as: 211 | ``` 212 | cd throttled 213 | git pull 214 | sudo ./install.sh 215 | sudo systemctl restart throttled.service 216 | OpenRC: sudo rc-service throttled restart 217 | ``` 218 | 219 | ## Configuration 220 | The configuration has moved to `/etc/throttled.conf`. Makefile does not overwrite your previous config file, so you need to manually check for differences in config file structure when updating the tool. If you want to overwrite the config with new defaults just issue `sudo cp etc/throttled.conf /etc`. There exist two profiles `AC` and `BATTERY` and the tool can be totally disabled by setting `Enabled: False` in the `GENERAL` section. Undervolt is applied if any voltage plane in the config file (section UNDERVOLT) was set. Notice that the offset is in *mV* and only undervolting (*i.e.* negative values) is supported. 221 | All fields accept floating point values as well as integers. 222 | 223 | My T480s with i7-8550u is stable with: 224 | ``` 225 | [UNDERVOLT] 226 | # CPU core voltage offset (mV) 227 | CORE: -105 228 | # Integrated GPU voltage offset (mV) 229 | GPU: -85 230 | # CPU cache voltage offset (mV) 231 | CACHE: -105 232 | # System Agent voltage offset (mV) 233 | UNCORE: -85 234 | # Analog I/O voltage offset (mV) 235 | ANALOGIO: 0 236 | ``` 237 | **IMPORTANT:** Please notice that *my* system is stable with these values. Your notebook might crash even with slight undervolting! You should test your system and slowly incresing undervolt to find the maximum stable value for your CPU. You can check [this](https://www.notebookcheck.net/Intel-Extreme-Tuning-Utility-XTU-Undervolting-Guide.272120.0.html) tutorial if you don't know where to start. 238 | 239 | ## Monitoring 240 | With the flag `--monitor` the tool *constantly* monitors the throttling status, indicating the cause among thermal limit, power limit, current limit or cross-origin. The last cause is often related to an external event (e.g. by the GPU). The update rate can be adjusted and defaults to 1 second. Example output: 241 | ``` 242 | ./throttled.py --monitor 243 | [I] Detected CPU architecture: Intel Kaby Lake (R) 244 | [I] Loading config file. 245 | [I] Starting main loop. 246 | [D] Undervolt offsets: CORE: -105.00 mV | GPU: -85.00 mV | CACHE: -105.00 mV | UNCORE: -85.00 mV | ANALOGIO: 0.00 mV 247 | [D] IccMax: CORE: 64.00 A | GPU: 31.00 A | CACHE: 6.00 A 248 | [D] Realtime monitoring of throttling causes: 249 | 250 | [AC] Thermal: OK - Power: OK - Current: OK - Cross-domain (e.g. GPU): OK || VCore: 549 mV - Package: 2.6 W - Graphics: 0.4 W - DRAM: 1.2 W 251 | ``` 252 | 253 | ## Static Fix 254 | You can alternatively set the power limits using intel_rapl driver (modifying MCHBAR values requires [Linux 5.3+](https://git.kernel.org/pub/scm/linux/kernel/git/rzhang/linux.git/commit/drivers/thermal/intel/int340x_thermal/processor_thermal_device.c?h=for-5.4&id=555c45fe0d04bd817e245a125d242b6a86af4593)). Bear in mind, some embedded controllers (EC) control the power limit values and will reset them from time to time): 255 | ``` 256 | # MSR 257 | # PL1 258 | echo 44000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw # 44 watt 259 | echo 28000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_0_time_window_us # 28 sec 260 | # PL2 261 | echo 44000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_1_power_limit_uw # 44 watt 262 | echo 2440 | sudo tee /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_1_time_window_us # 0.00244 sec 263 | 264 | # MCHBAR 265 | # PL1 266 | echo 44000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_0_power_limit_uw # 44 watt 267 | # ^ Only required change on a ASUS Zenbook UX430UNR 268 | echo 28000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_0_time_window_us # 28 sec 269 | # PL2 270 | echo 44000000 | sudo tee /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_1_power_limit_uw # 44 watt 271 | echo 2440 | sudo tee /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_1_time_window_us # 0.00244 sec 272 | ``` 273 | If you want to change the values automatic on boot you can use [systemd-tmpfiles](https://www.freedesktop.org/software/systemd/man/tmpfiles.d.html): 274 | ``` 275 | # /etc/tmpfiles.d/power_limit.conf 276 | # MSR 277 | # PL1 278 | w /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_0_power_limit_uw - - - - 44000000 279 | w /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_0_time_window_us - - - - 28000000 280 | # PL2 281 | w /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_1_power_limit_uw - - - - 44000000 282 | w /sys/devices/virtual/powercap/intel-rapl/intel-rapl:0/constraint_1_time_window_us - - - - 2440 283 | 284 | # MCHBAR 285 | # PL1 286 | w /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_0_power_limit_uw - - - - 44000000 287 | # ^ Only required change on a ASUS Zenbook UX430UNR 288 | w /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_0_time_window_us - - - - 28000000 289 | # PL2 290 | w /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_1_power_limit_uw - - - - 44000000 291 | w /sys/devices/virtual/powercap/intel-rapl-mmio/intel-rapl-mmio:0/constraint_1_time_window_us - - - - 2440 292 | ``` 293 | 294 | ## Debug 295 | You can enable the `--debug` option to read back written values and check if the tool is working properly. At the statup it will also show the CPUs platform info which contains information about multiplier values and features present for this CPU. Additionally the tool will print the thermal status per core which is handy when it comes to figuring out the reason for CPU throttle. Status fields stands for the current throttle reason or condition and log shows if this was a throttle reason since the last interval. 296 | This is an example output: 297 | ``` 298 | ./throttled.py --debug 299 | [D] cpu platform info: maximum non turbo ratio = 20 300 | [D] cpu platform info: maximum efficiency ratio = 4 301 | [D] cpu platform info: minimum operating ratio = 4 302 | [D] cpu platform info: feature ppin cap = 0 303 | [D] cpu platform info: feature programmable turbo ratio = 1 304 | [D] cpu platform info: feature programmable tdp limit = 1 305 | [D] cpu platform info: number of additional tdp profiles = 2 306 | [D] cpu platform info: feature programmable temperature target = 1 307 | [D] cpu platform info: feature low power mode = 1 308 | [D] TEMPERATURE_TARGET - write 0xf - read 0xf 309 | [D] Undervolt plane CORE - write 0xf2800000 - read 0xf2800000 310 | [D] Undervolt plane GPU - write 0xf5200000 - read 0xf5200000 311 | [D] Undervolt plane CACHE - write 0xf2800000 - read 0xf2800000 312 | [D] Undervolt plane UNCORE - write 0xf5200000 - read 0xf5200000 313 | [D] Undervolt plane ANALOGIO - write 0x0 - read 0x0 314 | [D] MSR PACKAGE_POWER_LIMIT - write 0xcc816000dc80e8 - read 0xcc816000dc80e8 315 | [D] MCHBAR PACKAGE_POWER_LIMIT - write 0xcc816000dc80e8 - read 0xcc816000dc80e8 316 | [D] TEMPERATURE_TARGET - write 0xf - read 0xf 317 | [D] core 0 thermal status: thermal throttle status = 0 318 | [D] core 0 thermal status: thermal throttle log = 1 319 | [D] core 0 thermal status: prochot or forcepr event = 0 320 | [D] core 0 thermal status: prochot or forcepr log = 0 321 | [D] core 0 thermal status: crit temp status = 0 322 | [D] core 0 thermal status: crit temp log = 0 323 | [D] core 0 thermal status: thermal threshold1 status = 0 324 | [D] core 0 thermal status: thermal threshold1 log = 1 325 | [D] core 0 thermal status: thermal threshold2 status = 0 326 | [D] core 0 thermal status: thermal threshold2 log = 1 327 | [D] core 0 thermal status: power limit status = 0 328 | [D] core 0 thermal status: power limit log = 1 329 | [D] core 0 thermal status: current limit status = 0 330 | [D] core 0 thermal status: current limit log = 0 331 | [D] core 0 thermal status: cross domain limit status = 0 332 | [D] core 0 thermal status: cross domain limit log = 0 333 | [D] core 0 thermal status: cpu temp = 44 334 | [D] core 0 thermal status: temp resolution = 1 335 | [D] core 0 thermal status: reading valid = 1 336 | ..... 337 | ``` 338 | 339 | ## Autoreload 340 | Auto reload config on changes (unless it's deleted) can be enabled/disabled in the config 341 | 342 | ``` 343 | [General] 344 | Autoreload = True 345 | ``` 346 | 347 | ## A word about manufacturer provided tooling 348 | Tools provided by your notebook manufacturer like [Dell Power Manager](https://www.dell.com/support/contents/us/en/04/article/product-support/self-support-knowledgebase/software-and-downloads/dell-power-manager) tend to persist their settings to the system board. If you ever had it running under Windows and activated a cool/quiet/silent/saving profile, this setting will still be active when running linux, throttling your system. 349 | 350 | > On my Dell Latitude 5591, not even a BIOS reset to manufacturar default killed the active `Quiet` profile 351 | 352 | ## Disclaimer 353 | This script overrides the default values set by Lenovo. I'm using it without any problem, but it is still experimental so use it at your own risk. 354 | -------------------------------------------------------------------------------- /throttled.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from __future__ import print_function 3 | 4 | import argparse 5 | import configparser 6 | import glob 7 | import gzip 8 | import os 9 | import re 10 | import struct 11 | import subprocess 12 | import sys 13 | from collections import defaultdict 14 | from datetime import datetime 15 | from errno import EACCES, EIO, EPERM 16 | from multiprocessing import cpu_count 17 | from platform import uname 18 | from subprocess import check_output, CalledProcessError 19 | from threading import Event, Thread 20 | from time import time 21 | 22 | import dbus 23 | from dbus.mainloop.glib import DBusGMainLoop 24 | from gi.repository import GLib 25 | from mmio import MMIO, MMIOError 26 | 27 | DEFAULT_SYSFS_POWER_PATH = '/sys/class/power_supply/AC*/online' 28 | VOLTAGE_PLANES = {'CORE': 0, 'GPU': 1, 'CACHE': 2, 'UNCORE': 3, 'ANALOGIO': 4} 29 | CURRENT_PLANES = {'CORE': 0, 'GPU': 1, 'CACHE': 2} 30 | TRIP_TEMP_RANGE = [40, 97] 31 | UNDERVOLT_KEYS = ('UNDERVOLT', 'UNDERVOLT.AC', 'UNDERVOLT.BATTERY') 32 | ICCMAX_KEYS = ('ICCMAX', 'ICCMAX.AC', 'ICCMAX.BATTERY') 33 | power = {'source': None, 'method': 'polling'} 34 | MSR_DICT = { 35 | 'MSR_PLATFORM_INFO': 0xCE, 36 | 'MSR_OC_MAILBOX': 0x150, 37 | 'IA32_PERF_STATUS': 0x198, 38 | 'IA32_THERM_STATUS': 0x19C, 39 | 'MSR_TEMPERATURE_TARGET': 0x1A2, 40 | 'MSR_POWER_CTL': 0x1FC, 41 | 'MSR_RAPL_POWER_UNIT': 0x606, 42 | 'MSR_PKG_POWER_LIMIT': 0x610, 43 | 'MSR_INTEL_PKG_ENERGY_STATUS': 0x611, 44 | 'MSR_DRAM_ENERGY_STATUS': 0x619, 45 | 'MSR_PP1_ENERGY_STATUS': 0x641, 46 | 'MSR_CONFIG_TDP_CONTROL': 0x64B, 47 | 'IA32_HWP_REQUEST': 0x774, 48 | } 49 | 50 | HWP_PERFORMANCE_VALUE = 0x20 51 | HWP_DEFAULT_VALUE = 0x80 52 | HWP_INTERVAL = 60 53 | 54 | 55 | platform_info_bits = { 56 | 'maximum_non_turbo_ratio': [8, 15], 57 | 'maximum_efficiency_ratio': [40, 47], 58 | 'minimum_operating_ratio': [48, 55], 59 | 'feature_ppin_cap': [23, 23], 60 | 'feature_programmable_turbo_ratio': [28, 28], 61 | 'feature_programmable_tdp_limit': [29, 29], 62 | 'number_of_additional_tdp_profiles': [33, 34], 63 | 'feature_programmable_temperature_target': [30, 30], 64 | 'feature_low_power_mode': [32, 32], 65 | } 66 | 67 | thermal_status_bits = { 68 | 'thermal_limit_status': [0, 0], 69 | 'thermal_limit_log': [1, 1], 70 | 'prochot_or_forcepr_status': [2, 2], 71 | 'prochot_or_forcepr_log': [3, 3], 72 | 'crit_temp_status': [4, 4], 73 | 'crit_temp_log': [5, 5], 74 | 'thermal_threshold1_status': [6, 6], 75 | 'thermal_threshold1_log': [7, 7], 76 | 'thermal_threshold2_status': [8, 8], 77 | 'thermal_threshold2_log': [9, 9], 78 | 'power_limit_status': [10, 10], 79 | 'power_limit_log': [11, 11], 80 | 'current_limit_status': [12, 12], 81 | 'current_limit_log': [13, 13], 82 | 'cross_domain_limit_status': [14, 14], 83 | 'cross_domain_limit_log': [15, 15], 84 | 'cpu_temp': [16, 22], 85 | 'temp_resolution': [27, 30], 86 | 'reading_valid': [31, 31], 87 | } 88 | 89 | supported_cpus = { 90 | (6, 26, 1): 'Nehalem', 91 | (6, 26, 2): 'Nehalem-EP', 92 | (6, 26, 4): 'Bloomfield', 93 | (6, 28, 2): 'Silverthorne', 94 | (6, 28, 10): 'PineView', 95 | (6, 29, 0): 'Dunnington-6C', 96 | (6, 29, 1): 'Dunnington', 97 | (6, 30, 0): 'Lynnfield', 98 | (6, 30, 5): 'Lynnfield_CPUID', 99 | (6, 31, 1): 'Auburndale', 100 | (6, 37, 2): 'Clarkdale', 101 | (6, 38, 1): 'TunnelCreek', 102 | (6, 39, 2): 'Medfield', 103 | (6, 42, 2): 'SandyBridge', 104 | (6, 42, 6): 'SandyBridge', 105 | (6, 42, 7): 'Sandy Bridge-DT', 106 | (6, 44, 1): 'Westmere-EP', 107 | (6, 44, 2): 'Gulftown', 108 | (6, 45, 5): 'Sandy Bridge-EP', 109 | (6, 45, 6): 'Sandy Bridge-E', 110 | (6, 46, 4): 'Beckton', 111 | (6, 46, 5): 'Beckton', 112 | (6, 46, 6): 'Beckton', 113 | (6, 47, 2): 'Eagleton', 114 | (6, 53, 1): 'Cloverview', 115 | (6, 54, 1): 'Cedarview-D', 116 | (6, 54, 9): 'Centerton', 117 | (6, 55, 3): 'Bay Trail-D', 118 | (6, 55, 8): 'Silvermont', 119 | (6, 58, 9): 'Ivy Bridge-DT', 120 | (6, 60, 3): 'Haswell-DT', 121 | (6, 61, 4): 'Broadwell-U', 122 | (6, 62, 3): 'IvyBridgeEP', 123 | (6, 62, 4): 'Ivy Bridge-E', 124 | (6, 63, 2): 'Haswell-EP', 125 | (6, 69, 1): 'HaswellULT', 126 | (6, 70, 1): 'Crystal Well-DT', 127 | (6, 71, 1): 'Broadwell-H', 128 | (6, 76, 3): 'Braswell', 129 | (6, 77, 8): 'Avoton', 130 | (6, 78, 3): 'Skylake', 131 | (6, 79, 1): 'BroadwellE', 132 | (6, 85, 4): 'SkylakeXeon', 133 | (6, 85, 6): 'CascadeLakeSP', 134 | (6, 85, 7): 'CascadeLakeXeon2', 135 | (6, 86, 2): 'BroadwellDE', 136 | (6, 86, 4): 'BroadwellDE', 137 | (6, 87, 0): 'KnightsLanding', 138 | (6, 87, 1): 'KnightsLanding', 139 | (6, 90, 0): 'Moorefield', 140 | (6, 92, 9): 'Apollo Lake', 141 | (6, 93, 1): 'SoFIA', 142 | (6, 94, 0): 'Skylake', 143 | (6, 94, 3): 'Skylake-S', 144 | (6, 95, 1): 'Denverton', 145 | (6, 102, 3): 'Cannon Lake-U', 146 | (6, 117, 10): 'Spreadtrum', 147 | (6, 122, 1): 'Gemini Lake-D', 148 | (6, 122, 8): 'GoldmontPlus', 149 | (6, 126, 5): 'IceLakeY', 150 | (6, 138, 1): 'Lakefield', 151 | (6, 140, 1): 'TigerLake-U', 152 | (6, 140, 2): 'TigerLake-U', 153 | (6, 141, 1): 'TigerLake-H', 154 | (6, 142, 9): 'KabyLake', 155 | (6, 142, 10): 'KabyLake', 156 | (6, 142, 11): 'WhiskeyLake', 157 | (6, 142, 12): 'CometLake-U', 158 | (6, 151, 2): 'AlderLake-S/HX', 159 | (6, 151, 5): 'AlderLake-S', 160 | (6, 154, 3): 'AlderLake-P/H', 161 | (6, 154, 4): 'AlderLake-U', 162 | (6, 156, 0): 'JasperLake', 163 | (6, 158, 9): 'KabyLakeG', 164 | (6, 158, 10): 'CoffeeLake', 165 | (6, 158, 11): 'CoffeeLake', 166 | (6, 158, 12): 'CoffeeLake', 167 | (6, 158, 13): 'CoffeeLake', 168 | (6, 165, 2): 'CometLake', 169 | (6, 165, 4): 'CometLake', 170 | (6, 165, 5): 'CometLake-S', 171 | (6, 166, 0): 'CometLake', 172 | (6, 167, 1): 'RocketLake', 173 | (6, 170, 4): 'MeteorLake', 174 | (6, 183, 1): 'RaptorLake-HX', 175 | (6, 186, 2): 'RaptorLake', 176 | (6, 186, 3): 'RaptorLake-U', 177 | (6, 189, 1): 'LunarLake', 178 | } 179 | 180 | TESTMSR = False 181 | UNSUPPORTED_FEATURES = [] 182 | 183 | 184 | class bcolors: 185 | YELLOW = '\033[93m' 186 | GREEN = '\033[92m' 187 | RED = '\033[91m' 188 | RESET = '\033[0m' 189 | BOLD = '\033[1m' 190 | 191 | 192 | OK = bcolors.GREEN + bcolors.BOLD + 'OK' + bcolors.RESET 193 | ERR = bcolors.RED + bcolors.BOLD + 'ERR' + bcolors.RESET 194 | LIM = bcolors.YELLOW + bcolors.BOLD + 'LIM' + bcolors.RESET 195 | 196 | log_history = set() 197 | 198 | 199 | def log(msg, oneshot=False, end='\n'): 200 | outfile = args.log if args.log else sys.stdout 201 | if msg.strip() not in log_history or oneshot is False: 202 | tstamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] 203 | full_msg = '{:s}: {:s}'.format(tstamp, msg) if args.log else msg 204 | print(full_msg, file=outfile, end=end) 205 | log_history.add(msg.strip()) 206 | 207 | 208 | def fatal(msg, code=1, end='\n'): 209 | outfile = args.log if args.log else sys.stderr 210 | tstamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] 211 | full_msg = '{:s}: [E] {:s}'.format(tstamp, msg) if args.log else '[E] {:s}'.format(msg) 212 | print(full_msg, file=outfile, end=end) 213 | sys.exit(code) 214 | 215 | 216 | def warning(msg, oneshot=True, end='\n'): 217 | outfile = args.log if args.log else sys.stderr 218 | if msg.strip() not in log_history or oneshot is False: 219 | tstamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3] 220 | full_msg = '{:s}: [W] {:s}'.format(tstamp, msg) if args.log else '[W] {:s}'.format(msg) 221 | print(full_msg, file=outfile, end=end) 222 | log_history.add(msg.strip()) 223 | 224 | 225 | def writemsr(msr, val): 226 | msr_list = ['/dev/cpu/{:d}/msr'.format(x) for x in range(cpu_count())] 227 | if not os.path.exists(msr_list[0]): 228 | try: 229 | subprocess.check_call(('modprobe', 'msr')) 230 | except subprocess.CalledProcessError: 231 | fatal('Unable to load the msr module.') 232 | try: 233 | for addr in msr_list: 234 | f = os.open(addr, os.O_WRONLY) 235 | os.lseek(f, MSR_DICT[msr], os.SEEK_SET) 236 | os.write(f, struct.pack('Q', val)) 237 | os.close(f) 238 | except (IOError, OSError) as e: 239 | if TESTMSR: 240 | raise e 241 | if e.errno == EPERM or e.errno == EACCES: 242 | fatal( 243 | 'Unable to write to MSR {} ({:x}). Try to disable Secure Boot ' 244 | 'and check if your kernel does not restrict access to MSR.'.format(msr, MSR_DICT[msr]) 245 | ) 246 | elif e.errno == EIO: 247 | fatal('Unable to write to MSR {} ({:x}). Unknown error.'.format(msr, MSR_DICT[msr])) 248 | else: 249 | raise e 250 | 251 | 252 | # returns the value between from_bit and to_bit as unsigned long 253 | def readmsr(msr, from_bit=0, to_bit=63, cpu=None, flatten=False): 254 | assert cpu is None or cpu in range(cpu_count()) 255 | if from_bit > to_bit: 256 | fatal('Wrong readmsr bit params') 257 | msr_list = ['/dev/cpu/{:d}/msr'.format(x) for x in range(cpu_count())] 258 | if not os.path.exists(msr_list[0]): 259 | try: 260 | subprocess.check_call(('modprobe', 'msr')) 261 | except subprocess.CalledProcessError: 262 | fatal('Unable to load the msr module.') 263 | try: 264 | output = [] 265 | for addr in msr_list: 266 | f = os.open(addr, os.O_RDONLY) 267 | os.lseek(f, MSR_DICT[msr], os.SEEK_SET) 268 | val = struct.unpack('Q', os.read(f, 8))[0] 269 | os.close(f) 270 | output.append(get_value_for_bits(val, from_bit, to_bit)) 271 | if flatten: 272 | if len(set(output)) > 1: 273 | warning('Found multiple values for {:s} ({:x}). This should never happen.'.format(msr, MSR_DICT[msr])) 274 | return output[0] 275 | return output[cpu] if cpu is not None else output 276 | except (IOError, OSError) as e: 277 | if TESTMSR: 278 | raise e 279 | if e.errno == EPERM or e.errno == EACCES: 280 | fatal('Unable to read from MSR {} ({:x}). Try to disable Secure Boot.'.format(msr, MSR_DICT[msr])) 281 | elif e.errno == EIO: 282 | fatal('Unable to read to MSR {} ({:x}). Unknown error.'.format(msr, MSR_DICT[msr])) 283 | else: 284 | raise e 285 | 286 | 287 | def get_value_for_bits(val, from_bit=0, to_bit=63): 288 | mask = sum(2 ** x for x in range(from_bit, to_bit + 1)) 289 | return (val & mask) >> from_bit 290 | 291 | 292 | def set_msr_allow_writes(): 293 | log('[I] Trying to unlock MSR allow_writes.') 294 | if not os.path.exists('/sys/module/msr'): 295 | try: 296 | subprocess.check_call(('modprobe', 'msr')) 297 | except subprocess.CalledProcessError: 298 | return 299 | if os.path.exists('/sys/module/msr/parameters/allow_writes'): 300 | try: 301 | with open('/sys/module/msr/parameters/allow_writes', 'w') as f: 302 | f.write('on') 303 | except: 304 | warning('Unable to set MSR allow_writes to on. You might experience warnings in kernel logs.') 305 | 306 | 307 | def is_on_battery(config): 308 | try: 309 | for path in glob.glob(config.get('GENERAL', 'Sysfs_Power_Path', fallback=DEFAULT_SYSFS_POWER_PATH)): 310 | with open(path) as f: 311 | return not bool(int(f.read())) 312 | raise 313 | except: 314 | warning('No valid Sysfs_Power_Path found! Trying upower method') 315 | try: 316 | bus = dbus.SystemBus() 317 | proxy = bus.get_object('org.freedesktop.UPower', '/org/freedesktop/UPower') 318 | iface = dbus.Interface(proxy, 'org.freedesktop.DBus.Properties') 319 | return iface.Get('org.freedesktop.UPower', 'OnBattery') 320 | except: 321 | pass 322 | 323 | warning('No valid power detection methods found. Assuming that the system is running on battery power.') 324 | return True 325 | 326 | 327 | def get_cpu_platform_info(): 328 | features_msr_value = readmsr('MSR_PLATFORM_INFO', cpu=0) 329 | cpu_platform_info = {} 330 | for key, value in platform_info_bits.items(): 331 | cpu_platform_info[key] = int(get_value_for_bits(features_msr_value, value[0], value[1])) 332 | return cpu_platform_info 333 | 334 | 335 | def get_reset_thermal_status(): 336 | # read thermal status 337 | thermal_status_msr_value = readmsr('IA32_THERM_STATUS') 338 | thermal_status = [] 339 | for core in range(cpu_count()): 340 | thermal_status_core = {} 341 | for key, value in thermal_status_bits.items(): 342 | thermal_status_core[key] = int(get_value_for_bits(thermal_status_msr_value[core], value[0], value[1])) 343 | thermal_status.append(thermal_status_core) 344 | # reset log bits 345 | writemsr('IA32_THERM_STATUS', 0) 346 | return thermal_status 347 | 348 | 349 | def get_time_unit(): 350 | # 0.000977 is the time unit of my CPU 351 | # TODO formula might be different for other CPUs 352 | return 1.0 / 2 ** readmsr('MSR_RAPL_POWER_UNIT', 16, 19, cpu=0) 353 | 354 | 355 | def get_power_unit(): 356 | # 0.125 is the power unit of my CPU 357 | # TODO formula might be different for other CPUs 358 | return 1.0 / 2 ** readmsr('MSR_RAPL_POWER_UNIT', 0, 3, cpu=0) 359 | 360 | 361 | def get_critical_temp(): 362 | # the critical temperature for my CPU is 100 'C 363 | return readmsr('MSR_TEMPERATURE_TARGET', 16, 23, cpu=0) 364 | 365 | 366 | def get_cur_pkg_power_limits(): 367 | value = readmsr('MSR_PKG_POWER_LIMIT', 0, 55, flatten=True) 368 | return { 369 | 'PL1': get_value_for_bits(value, 0, 14), 370 | 'TW1': get_value_for_bits(value, 17, 23), 371 | 'PL2': get_value_for_bits(value, 32, 46), 372 | 'TW2': get_value_for_bits(value, 49, 55), 373 | } 374 | 375 | 376 | def calc_time_window_vars(t): 377 | time_unit = get_time_unit() 378 | for Y in range(2 ** 5): 379 | for Z in range(2 ** 2): 380 | if t <= (2 ** Y) * (1.0 + Z / 4.0) * time_unit: 381 | return (Y, Z) 382 | raise ValueError('Unable to find a good combination!') 383 | 384 | 385 | def calc_undervolt_msr(plane, offset): 386 | """Return the value to be written in the MSR 150h for setting the given 387 | offset voltage (in mV) to the given voltage plane. 388 | """ 389 | assert offset <= 0 390 | assert plane in VOLTAGE_PLANES 391 | offset = int(round(offset * 1.024)) 392 | offset = 0xFFE00000 & ((offset & 0xFFF) << 21) 393 | return 0x8000001100000000 | (VOLTAGE_PLANES[plane] << 40) | offset 394 | 395 | 396 | def calc_undervolt_mv(msr_value): 397 | """Return the offset voltage (in mV) from the given raw MSR 150h value.""" 398 | offset = (msr_value & 0xFFE00000) >> 21 399 | offset = offset if offset <= 0x400 else -(0x800 - offset) 400 | return int(round(offset / 1.024)) 401 | 402 | 403 | def get_undervolt(plane=None, convert=False): 404 | if 'UNDERVOLT' in UNSUPPORTED_FEATURES: 405 | return 0 406 | planes = [plane] if plane in VOLTAGE_PLANES else VOLTAGE_PLANES 407 | out = {} 408 | for plane in planes: 409 | writemsr('MSR_OC_MAILBOX', 0x8000001000000000 | (VOLTAGE_PLANES[plane] << 40)) 410 | read_value = readmsr('MSR_OC_MAILBOX', flatten=True) & 0xFFFFFFFF 411 | out[plane] = calc_undervolt_mv(read_value) if convert else read_value 412 | 413 | return out 414 | 415 | 416 | def undervolt(config): 417 | if ('UNDERVOLT.{:s}'.format(power['source']) not in config and 'UNDERVOLT' not in config) or ( 418 | 'UNDERVOLT' in UNSUPPORTED_FEATURES 419 | ): 420 | return 421 | for plane in VOLTAGE_PLANES: 422 | write_offset_mv = config.getfloat( 423 | 'UNDERVOLT.{:s}'.format(power['source']), plane, fallback=config.getfloat('UNDERVOLT', plane, fallback=0.0) 424 | ) 425 | write_value = calc_undervolt_msr(plane, write_offset_mv) 426 | writemsr('MSR_OC_MAILBOX', write_value) 427 | if args.debug: 428 | write_value &= 0xFFFFFFFF 429 | read_value = get_undervolt(plane)[plane] 430 | read_offset_mv = calc_undervolt_mv(read_value) 431 | match = OK if write_value == read_value else ERR 432 | log( 433 | '[D] Undervolt plane {:s} - write {:.0f} mV ({:#x}) - read {:.0f} mV ({:#x}) - match {}'.format( 434 | plane, write_offset_mv, write_value, read_offset_mv, read_value, match 435 | ) 436 | ) 437 | 438 | 439 | def calc_icc_max_msr(plane, current): 440 | """Return the value to be written in the MSR 150h for setting the given 441 | IccMax (in A) to the given current plane. 442 | """ 443 | assert 0 < current <= 0x3FF 444 | assert plane in CURRENT_PLANES 445 | current = int(round(current * 4)) 446 | return 0x8000001700000000 | (CURRENT_PLANES[plane] << 40) | current 447 | 448 | 449 | def calc_icc_max_amp(msr_value): 450 | """Return the max current (in A) from the given raw MSR 150h value.""" 451 | return (msr_value & 0x3FF) / 4.0 452 | 453 | 454 | def get_icc_max(plane=None, convert=False): 455 | planes = [plane] if plane in CURRENT_PLANES else CURRENT_PLANES 456 | out = {} 457 | for plane in planes: 458 | writemsr('MSR_OC_MAILBOX', 0x8000001600000000 | (CURRENT_PLANES[plane] << 40)) 459 | read_value = readmsr('MSR_OC_MAILBOX', flatten=True) & 0x3FF 460 | out[plane] = calc_icc_max_amp(read_value) if convert else read_value 461 | 462 | return out 463 | 464 | 465 | def set_icc_max(config): 466 | for plane in CURRENT_PLANES: 467 | try: 468 | write_current_amp = config.getfloat( 469 | 'ICCMAX.{:s}'.format(power['source']), plane, fallback=config.getfloat('ICCMAX', plane, fallback=-1.0) 470 | ) 471 | if write_current_amp > 0: 472 | write_value = calc_icc_max_msr(plane, write_current_amp) 473 | writemsr('MSR_OC_MAILBOX', write_value) 474 | if args.debug: 475 | write_value &= 0x3FF 476 | read_value = get_icc_max(plane)[plane] 477 | read_current_A = calc_icc_max_amp(read_value) 478 | match = OK if write_value == read_value else ERR 479 | log( 480 | '[D] IccMax plane {:s} - write {:.2f} A ({:#x}) - read {:.2f} A ({:#x}) - match {}'.format( 481 | plane, write_current_amp, write_value, read_current_A, read_value, match 482 | ) 483 | ) 484 | except (configparser.NoSectionError, configparser.NoOptionError): 485 | pass 486 | 487 | 488 | def load_config(): 489 | config = configparser.ConfigParser() 490 | config.read(args.config) 491 | 492 | # config values sanity check 493 | for power_source in ('AC', 'BATTERY'): 494 | for option in ('Update_Rate_s', 'PL1_Tdp_W', 'PL1_Duration_s', 'PL2_Tdp_W', 'PL2_Duration_S'): 495 | value = config.getfloat(power_source, option, fallback=None) 496 | if value is not None: 497 | value = config.set(power_source, option, str(max(0.001, value))) 498 | elif option == 'Update_Rate_s': 499 | fatal('The mandatory "Update_Rate_s" parameter is missing.') 500 | 501 | trip_temp = config.getfloat(power_source, 'Trip_Temp_C', fallback=None) 502 | if trip_temp is not None: 503 | valid_trip_temp = min(TRIP_TEMP_RANGE[1], max(TRIP_TEMP_RANGE[0], trip_temp)) 504 | if trip_temp != valid_trip_temp: 505 | config.set(power_source, 'Trip_Temp_C', str(valid_trip_temp)) 506 | log( 507 | '[!] Overriding invalid "Trip_Temp_C" value in "{:s}": {:.1f} -> {:.1f}'.format( 508 | power_source, trip_temp, valid_trip_temp 509 | ) 510 | ) 511 | 512 | # fix any invalid value (ie. > 0) in the undervolt settings 513 | for key in UNDERVOLT_KEYS: 514 | for plane in VOLTAGE_PLANES: 515 | if key in config: 516 | value = config.getfloat(key, plane) 517 | valid_value = min(0, value) 518 | if value != valid_value: 519 | config.set(key, plane, str(valid_value)) 520 | log( 521 | '[!] Overriding invalid "{:s}" value in "{:s}" voltage plane: {:.0f} -> {:.0f}'.format( 522 | key, plane, value, valid_value 523 | ) 524 | ) 525 | 526 | # handle the case where only one of UNDERVOLT.AC, UNDERVOLT.BATTERY keys exists 527 | # by forcing the other key to all zeros (ie. no undervolt) 528 | if any(key in config for key in UNDERVOLT_KEYS[1:]): 529 | for key in UNDERVOLT_KEYS[1:]: 530 | if key not in config: 531 | config.add_section(key) 532 | for plane in VOLTAGE_PLANES: 533 | value = config.getfloat(key, plane, fallback=0.0) 534 | config.set(key, plane, str(value)) 535 | 536 | # Check for CORE/CACHE values mismatch 537 | for key in UNDERVOLT_KEYS: 538 | if key in config: 539 | if config.getfloat(key, 'CORE', fallback=0) != config.getfloat(key, 'CACHE', fallback=0): 540 | warning('On Skylake and newer CPUs CORE and CACHE values should match!') 541 | break 542 | 543 | iccmax_enabled = False 544 | # check for invalid values (ie. <= 0 or > 0x3FF) in the IccMax settings 545 | for key in ICCMAX_KEYS: 546 | for plane in CURRENT_PLANES: 547 | if key in config: 548 | try: 549 | value = config.getfloat(key, plane) 550 | if value <= 0 or value >= 0x3FF: 551 | raise ValueError 552 | iccmax_enabled = True 553 | except ValueError: 554 | warning('Invalid value for {:s} in {:s}'.format(plane, key), oneshot=False) 555 | config.remove_option(key, plane) 556 | except configparser.NoOptionError: 557 | pass 558 | if iccmax_enabled: 559 | warning('Warning! Raising IccMax above design limits can damage your system!') 560 | 561 | return config 562 | 563 | 564 | def calc_reg_values(platform_info, config): 565 | regs = defaultdict(dict) 566 | for power_source in ('AC', 'BATTERY'): 567 | if platform_info['feature_programmable_temperature_target'] != 1: 568 | warning("Setting temperature target is not supported by this CPU") 569 | else: 570 | # the critical temperature for my CPU is 100 'C 571 | critical_temp = get_critical_temp() 572 | # update the allowed temp range to keep at least 3 'C from the CPU critical temperature 573 | global TRIP_TEMP_RANGE 574 | TRIP_TEMP_RANGE[1] = min(TRIP_TEMP_RANGE[1], critical_temp - 3) 575 | 576 | Trip_Temp_C = config.getfloat(power_source, 'Trip_Temp_C', fallback=None) 577 | if Trip_Temp_C is not None: 578 | trip_offset = int(round(critical_temp - Trip_Temp_C)) 579 | regs[power_source]['MSR_TEMPERATURE_TARGET'] = trip_offset << 24 580 | else: 581 | log('[I] {:s} trip temperature is disabled in config.'.format(power_source)) 582 | 583 | power_unit = get_power_unit() 584 | 585 | PL1_Tdp_W = config.getfloat(power_source, 'PL1_Tdp_W', fallback=None) 586 | PL1_Duration_s = config.getfloat(power_source, 'PL1_Duration_s', fallback=None) 587 | PL2_Tdp_W = config.getfloat(power_source, 'PL2_Tdp_W', fallback=None) 588 | PL2_Duration_s = config.getfloat(power_source, 'PL2_Duration_s', fallback=None) 589 | 590 | if (PL1_Tdp_W, PL1_Duration_s, PL2_Tdp_W, PL2_Duration_s).count(None) < 4: 591 | cur_pkg_power_limits = get_cur_pkg_power_limits() 592 | if PL1_Tdp_W is None: 593 | PL1 = cur_pkg_power_limits['PL1'] 594 | log('[I] {:s} PL1_Tdp_W disabled in config.'.format(power_source)) 595 | else: 596 | PL1 = int(round(PL1_Tdp_W / power_unit)) 597 | 598 | if PL1_Duration_s is None: 599 | TW1 = cur_pkg_power_limits['TW1'] 600 | log('[I] {:s} PL1_Duration_s disabled in config.'.format(power_source)) 601 | else: 602 | Y, Z = calc_time_window_vars(PL1_Duration_s) 603 | TW1 = Y | (Z << 5) 604 | 605 | if PL2_Tdp_W is None: 606 | PL2 = cur_pkg_power_limits['PL2'] 607 | log('[I] {:s} PL2_Tdp_W disabled in config.'.format(power_source)) 608 | else: 609 | PL2 = int(round(PL2_Tdp_W / power_unit)) 610 | 611 | if PL2_Duration_s is None: 612 | TW2 = cur_pkg_power_limits['TW2'] 613 | log('[I] {:s} PL2_Duration_s disabled in config.'.format(power_source)) 614 | else: 615 | Y, Z = calc_time_window_vars(PL2_Duration_s) 616 | TW2 = Y | (Z << 5) 617 | 618 | regs[power_source]['MSR_PKG_POWER_LIMIT'] = ( 619 | PL1 | (1 << 15) | (1 << 16) | (TW1 << 17) | (PL2 << 32) | (1 << 47) | (TW2 << 49) 620 | ) 621 | else: 622 | log('[I] {:s} package power limits are disabled in config.'.format(power_source)) 623 | 624 | # cTDP 625 | c_tdp_target_value = config.getint(power_source, 'cTDP', fallback=None) 626 | if c_tdp_target_value is not None: 627 | if platform_info['feature_programmable_tdp_limit'] != 1: 628 | log("[W] cTDP setting not supported by this CPU") 629 | elif platform_info['number_of_additional_tdp_profiles'] < c_tdp_target_value: 630 | log("[W] the configured cTDP profile is not supported by this CPU") 631 | else: 632 | valid_c_tdp_target_value = max(0, c_tdp_target_value) 633 | regs[power_source]['MSR_CONFIG_TDP_CONTROL'] = valid_c_tdp_target_value 634 | return regs 635 | 636 | 637 | def set_hwp(performance_mode): 638 | if performance_mode not in (True, False) or 'HWP' in UNSUPPORTED_FEATURES: 639 | return 640 | # set HWP energy performance preference 641 | cur_val = readmsr('IA32_HWP_REQUEST', cpu=0) 642 | hwp_mode = HWP_PERFORMANCE_VALUE if performance_mode is True else HWP_DEFAULT_VALUE 643 | new_val = (cur_val & 0xFFFFFFFF00FFFFFF) | (hwp_mode << 24) 644 | 645 | writemsr('IA32_HWP_REQUEST', new_val) 646 | if args.debug: 647 | read_value = readmsr('IA32_HWP_REQUEST', from_bit=24, to_bit=31)[0] 648 | match = OK if hwp_mode == read_value else ERR 649 | log('[D] HWP - write "{:#02x}" - read "{:#02x}" - match {}'.format(hwp_mode, read_value, match)) 650 | 651 | 652 | def set_disable_bdprochot(): 653 | # Disable BDPROCHOT 654 | cur_val = readmsr('MSR_POWER_CTL', flatten=True) 655 | new_val = cur_val & 0xFFFFFFFFFFFFFFFE 656 | 657 | writemsr('MSR_POWER_CTL', new_val) 658 | if args.debug: 659 | read_value = readmsr('MSR_POWER_CTL', from_bit=0, to_bit=0)[0] 660 | match = OK if ~read_value else ERR 661 | log('[D] BDPROCHOT - write "{:#02x}" - read "{:#02x}" - match {}'.format(0, read_value, match)) 662 | 663 | 664 | def get_config_write_time(): 665 | try: 666 | return os.stat(args.config).st_mtime 667 | except FileNotFoundError: 668 | return None 669 | 670 | 671 | def reload_config(): 672 | config = load_config() 673 | regs = calc_reg_values(get_cpu_platform_info(), config) 674 | undervolt(config) 675 | set_icc_max(config) 676 | set_hwp(config.getboolean('AC', 'HWP_Mode', fallback=None)) 677 | log('[I] Reloading changes.') 678 | return config, regs 679 | 680 | 681 | def power_thread(config, regs, exit_event, cpuid): 682 | try: 683 | MCHBAR_BASE = int(check_output(('setpci', '-s', '0:0.0', '48.l')), 16) 684 | except CalledProcessError: 685 | warning('Please ensure that "setpci" is in path. This is typically provided by the "pciutils" package.') 686 | warning('Trying to guess the MCHBAR address from the CPUID. This MIGHT NOT WORK!') 687 | if cpuid in ((6, 140, 1),(6, 140, 2),(6, 141, 1),(6, 151, 2),(6, 151, 5), (6, 154, 3),(6, 154, 4)): 688 | MCHBAR_BASE = 0xFEDC0001 689 | else: 690 | MCHBAR_BASE = 0xFED10001 691 | try: 692 | mchbar_mmio = MMIO(MCHBAR_BASE + 0x599F, 8) 693 | except MMIOError: 694 | warning('Unable to open /dev/mem. TDP override might not work correctly.') 695 | warning('Try to disable Secure Boot and/or enable CONFIG_DEVMEM in kernel config.') 696 | mchbar_mmio = None 697 | 698 | next_hwp_write = 0 699 | last_config_write_time = ( 700 | get_config_write_time() if config.getboolean('GENERAL', 'Autoreload', fallback=False) else None 701 | ) 702 | while not exit_event.is_set(): 703 | # log thermal status 704 | if args.debug: 705 | thermal_status = get_reset_thermal_status() 706 | for index, core_thermal_status in enumerate(thermal_status): 707 | for key, value in core_thermal_status.items(): 708 | log('[D] core {} thermal status: {} = {}'.format(index, key.replace("_", " "), value)) 709 | 710 | # Reload config on changes (unless it's deleted) 711 | if config.getboolean('GENERAL', 'Autoreload', fallback=False): 712 | config_write_time = get_config_write_time() 713 | if config_write_time and last_config_write_time != config_write_time: 714 | last_config_write_time = config_write_time 715 | config, regs = reload_config() 716 | 717 | # switch back to sysfs polling 718 | if power['method'] == 'polling': 719 | power['source'] = 'BATTERY' if is_on_battery(config) else 'AC' 720 | 721 | # set temperature trip point 722 | if 'MSR_TEMPERATURE_TARGET' in regs[power['source']]: 723 | write_value = regs[power['source']]['MSR_TEMPERATURE_TARGET'] 724 | writemsr('MSR_TEMPERATURE_TARGET', write_value) 725 | if args.debug: 726 | read_value = readmsr('MSR_TEMPERATURE_TARGET', 24, 29, flatten=True) 727 | match = OK if write_value >> 24 == read_value else ERR 728 | log( 729 | '[D] TEMPERATURE_TARGET - write {:#x} - read {:#x} - match {}'.format( 730 | write_value >> 24, read_value, match 731 | ) 732 | ) 733 | 734 | # set cTDP 735 | if 'MSR_CONFIG_TDP_CONTROL' in regs[power['source']]: 736 | write_value = regs[power['source']]['MSR_CONFIG_TDP_CONTROL'] 737 | writemsr('MSR_CONFIG_TDP_CONTROL', write_value) 738 | if args.debug: 739 | read_value = readmsr('MSR_CONFIG_TDP_CONTROL', 0, 1, flatten=True) 740 | match = OK if write_value == read_value else ERR 741 | log( 742 | '[D] CONFIG_TDP_CONTROL - write {:#x} - read {:#x} - match {}'.format( 743 | write_value, read_value, match 744 | ) 745 | ) 746 | 747 | # set PL1/2 on MSR 748 | write_value = regs[power['source']]['MSR_PKG_POWER_LIMIT'] 749 | writemsr('MSR_PKG_POWER_LIMIT', write_value) 750 | if args.debug: 751 | read_value = readmsr('MSR_PKG_POWER_LIMIT', 0, 55, flatten=True) 752 | match = OK if write_value == read_value else ERR 753 | log( 754 | '[D] MSR PACKAGE_POWER_LIMIT - write {:#x} - read {:#x} - match {}'.format( 755 | write_value, read_value, match 756 | ) 757 | ) 758 | if mchbar_mmio is not None: 759 | # set MCHBAR register to the same PL1/2 values 760 | mchbar_mmio.write32(0, write_value & 0xFFFFFFFF) 761 | mchbar_mmio.write32(4, write_value >> 32) 762 | if args.debug: 763 | read_value = mchbar_mmio.read32(0) | (mchbar_mmio.read32(4) << 32) 764 | match = OK if write_value == read_value else ERR 765 | log( 766 | '[D] MCHBAR PACKAGE_POWER_LIMIT - write {:#x} - read {:#x} - match {}'.format( 767 | write_value, read_value, match 768 | ) 769 | ) 770 | 771 | # Disable BDPROCHOT 772 | disable_bdprochot = config.getboolean(power['source'], 'Disable_BDPROCHOT', fallback=None) 773 | if disable_bdprochot: 774 | set_disable_bdprochot() 775 | 776 | wait_t = config.getfloat(power['source'], 'Update_Rate_s') 777 | enable_hwp_mode = config.getboolean('AC', 'HWP_Mode', fallback=None) 778 | # set HWP less frequently. Just to be safe since (e.g.) TLP might reset this value 779 | if ( 780 | enable_hwp_mode 781 | and next_hwp_write <= time() 782 | and ( 783 | (power['method'] == 'dbus' and power['source'] == 'AC') 784 | or (power['method'] == 'polling' and not is_on_battery(config)) 785 | ) 786 | ): 787 | set_hwp(enable_hwp_mode) 788 | next_hwp_write = time() + HWP_INTERVAL 789 | 790 | else: 791 | exit_event.wait(wait_t) 792 | 793 | 794 | def check_kernel(): 795 | if os.geteuid() != 0: 796 | fatal('No root no party. Try again with sudo.') 797 | 798 | kernel_config = None 799 | try: 800 | with open(os.path.join('/boot', 'config-{:s}'.format(uname()[2]))) as f: 801 | kernel_config = f.read() 802 | except IOError: 803 | config_gz_path = os.path.join('/proc', 'config.gz') 804 | try: 805 | if not os.path.isfile(config_gz_path): 806 | subprocess.check_call(('modprobe', 'configs')) 807 | with gzip.open(config_gz_path) as f: 808 | kernel_config = f.read().decode() 809 | except (subprocess.CalledProcessError, IOError): 810 | pass 811 | if kernel_config is None: 812 | log('[W] Unable to obtain and validate kernel config.') 813 | return 814 | elif not re.search('CONFIG_DEVMEM=y', kernel_config): 815 | warning('Bad kernel config: you need CONFIG_DEVMEM=y.') 816 | if not re.search('CONFIG_X86_MSR=(y|m)', kernel_config): 817 | fatal('Bad kernel config: you need CONFIG_X86_MSR builtin or as module.') 818 | 819 | 820 | def check_cpu(): 821 | try: 822 | with open('/proc/cpuinfo') as f: 823 | cpuinfo = {} 824 | for row in f.readlines(): 825 | try: 826 | key, value = map(lambda x: x.strip(), row.split(':')) 827 | if key == 'processor' and value == '1': 828 | break 829 | try: 830 | cpuinfo[key] = int(value, 0) 831 | except ValueError: 832 | cpuinfo[key] = value 833 | except ValueError: 834 | pass 835 | if cpuinfo['vendor_id'] != 'GenuineIntel': 836 | fatal('This tool is designed for Intel CPUs only.') 837 | 838 | cpuid = (cpuinfo['cpu family'], cpuinfo['model'], cpuinfo['stepping']) 839 | if cpuid not in supported_cpus: 840 | fatal( 841 | 'Your CPU model is not supported.\n\n' 842 | 'Please open a new issue (https://github.com/erpalma/throttled/issues) specifying:\n' 843 | ' - model name\n' 844 | ' - cpu family\n' 845 | ' - model\n' 846 | ' - stepping\n' 847 | 'from /proc/cpuinfo.' 848 | ) 849 | 850 | log('[I] Detected CPU architecture: Intel {:s}'.format(supported_cpus[cpuid])) 851 | return cpuid 852 | except SystemExit: 853 | sys.exit(1) 854 | except: 855 | fatal('Unable to identify CPU model.') 856 | 857 | 858 | def test_msr_rw_capabilities(): 859 | TESTMSR = True 860 | 861 | try: 862 | log('[I] Testing if undervolt is supported...') 863 | get_undervolt() 864 | except: 865 | warning('Undervolt seems not to be supported by your system, disabling.') 866 | UNSUPPORTED_FEATURES.append('UNDERVOLT') 867 | 868 | try: 869 | log('[I] Testing if HWP is supported...') 870 | cur_val = readmsr('IA32_HWP_REQUEST', cpu=0) 871 | writemsr('IA32_HWP_REQUEST', cur_val) 872 | except: 873 | warning('HWP seems not to be supported by your system, disabling.') 874 | UNSUPPORTED_FEATURES.append('HWP') 875 | 876 | TESTMSR = False 877 | 878 | 879 | def monitor(exit_event, wait): 880 | wait = max(0.1, wait) 881 | rapl_power_unit = 0.5 ** readmsr('MSR_RAPL_POWER_UNIT', from_bit=8, to_bit=12, cpu=0) 882 | power_plane_msr = { 883 | 'Package': 'MSR_INTEL_PKG_ENERGY_STATUS', 884 | 'Graphics': 'MSR_PP1_ENERGY_STATUS', 885 | 'DRAM': 'MSR_DRAM_ENERGY_STATUS', 886 | } 887 | prev_energy = { 888 | 'Package': (readmsr('MSR_INTEL_PKG_ENERGY_STATUS', cpu=0) * rapl_power_unit, time()), 889 | 'Graphics': (readmsr('MSR_PP1_ENERGY_STATUS', cpu=0) * rapl_power_unit, time()), 890 | 'DRAM': (readmsr('MSR_DRAM_ENERGY_STATUS', cpu=0) * rapl_power_unit, time()), 891 | } 892 | 893 | undervolt_values = get_undervolt(convert=True) 894 | undervolt_output = ' | '.join('{:s}: {:.2f} mV'.format(plane, undervolt_values[plane]) for plane in VOLTAGE_PLANES) 895 | log('[D] Undervolt offsets: {:s}'.format(undervolt_output)) 896 | 897 | iccmax_values = get_icc_max(convert=True) 898 | iccmax_output = ' | '.join('{:s}: {:.2f} A'.format(plane, iccmax_values[plane]) for plane in CURRENT_PLANES) 899 | log('[D] IccMax: {:s}'.format(iccmax_output)) 900 | 901 | log('[D] Realtime monitoring of throttling causes:\n') 902 | while not exit_event.is_set(): 903 | value = readmsr('IA32_THERM_STATUS', from_bit=0, to_bit=15, cpu=0) 904 | offsets = {'Thermal': 0, 'Power': 10, 'Current': 12, 'Cross-domain (e.g. GPU)': 14} 905 | output = ('{:s}: {:s}'.format(cause, LIM if bool((value >> offsets[cause]) & 1) else OK) for cause in offsets) 906 | 907 | # ugly code, just testing... 908 | vcore = readmsr('IA32_PERF_STATUS', from_bit=32, to_bit=47, cpu=0) / (2.0 ** 13) * 1000 909 | stats2 = {'VCore': '{:.0f} mV'.format(vcore)} 910 | total = 0.0 911 | for power_plane in ('Package', 'Graphics', 'DRAM'): 912 | energy_j = readmsr(power_plane_msr[power_plane], cpu=0) * rapl_power_unit 913 | now = time() 914 | prev_energy[power_plane], energy_w = ( 915 | (energy_j, now), 916 | (energy_j - prev_energy[power_plane][0]) / (now - prev_energy[power_plane][1]), 917 | ) 918 | stats2[power_plane] = '{:.1f} W'.format(energy_w) 919 | total += energy_w 920 | 921 | stats2['Total'] = '{:.1f} W'.format(total) 922 | 923 | output2 = ('{:s}: {:s}'.format(label, stats2[label]) for label in stats2) 924 | terminator = '\n' if args.log else '\r' 925 | log( 926 | '[{}] {} || {}{}'.format(power['source'], ' - '.join(output), ' - '.join(output2), ' ' * 10), 927 | end=terminator, 928 | ) 929 | exit_event.wait(wait) 930 | 931 | 932 | def main(): 933 | global args 934 | 935 | parser = argparse.ArgumentParser() 936 | exclusive_group = parser.add_mutually_exclusive_group() 937 | exclusive_group.add_argument('--debug', action='store_true', help='add some debug info and additional checks') 938 | exclusive_group.add_argument( 939 | '--monitor', 940 | metavar='update_rate', 941 | const=1.0, 942 | type=float, 943 | nargs='?', 944 | help='realtime monitoring of throttling causes (default 1s)', 945 | ) 946 | parser.add_argument('--config', default='/etc/throttled.conf', help='override default config file path') 947 | parser.add_argument('--force', action='store_true', help='bypass compatibility checks (EXPERTS only)') 948 | parser.add_argument('--log', metavar='/path/to/file', help='log to file instead of stdout') 949 | args = parser.parse_args() 950 | 951 | if args.log: 952 | try: 953 | args.log = open(args.log, 'w') 954 | except: 955 | args.log = None 956 | fatal('Unable to write to the log file!') 957 | 958 | if not args.force: 959 | check_kernel() 960 | cpuid = check_cpu() 961 | 962 | set_msr_allow_writes() 963 | 964 | test_msr_rw_capabilities() 965 | 966 | DBusGMainLoop(set_as_default=True) 967 | bus = dbus.SystemBus() 968 | 969 | log('[I] Loading config file.') 970 | config = load_config() 971 | power['source'] = 'BATTERY' if is_on_battery(config) else 'AC' 972 | 973 | platform_info = get_cpu_platform_info() 974 | if args.debug: 975 | for key, value in platform_info.items(): 976 | log('[D] cpu platform info: {} = {}'.format(key.replace("_", " "), value)) 977 | regs = calc_reg_values(platform_info, config) 978 | 979 | if not config.getboolean('GENERAL', 'Enabled'): 980 | log('[I] Throttled is disabled in config file... Quitting. :(') 981 | return 982 | 983 | undervolt(config) 984 | set_icc_max(config) 985 | set_hwp(config.getboolean('AC', 'HWP_Mode', fallback=None)) 986 | 987 | exit_event = Event() 988 | thread = Thread(target=power_thread, args=(config, regs, exit_event, cpuid)) 989 | thread.daemon = True 990 | thread.start() 991 | 992 | # handle dbus events for applying undervolt/IccMax on resume from sleep/hibernate 993 | def handle_sleep_callback(sleeping): 994 | if not sleeping: 995 | undervolt(config) 996 | set_icc_max(config) 997 | 998 | def handle_ac_callback(if_name, changed, invalidated): 999 | if "OnBattery" in changed: 1000 | power['method'] = 'dbus' 1001 | power['source'] = 'BATTERY' if bool(changed['OnBattery']) else 'AC' 1002 | 1003 | # add dbus receiver only if undervolt/IccMax is enabled in config 1004 | if any( 1005 | config.getfloat(key, plane, fallback=0) != 0 for plane in VOLTAGE_PLANES for key in UNDERVOLT_KEYS + ICCMAX_KEYS 1006 | ): 1007 | bus.add_signal_receiver( 1008 | handle_sleep_callback, 'PrepareForSleep', 'org.freedesktop.login1.Manager', 'org.freedesktop.login1' 1009 | ) 1010 | bus.add_signal_receiver( 1011 | handle_ac_callback, 1012 | signal_name="PropertiesChanged", 1013 | dbus_interface="org.freedesktop.DBus.Properties", 1014 | path="/org/freedesktop/UPower", 1015 | ) 1016 | 1017 | log('[I] Starting main loop.') 1018 | 1019 | if args.monitor is not None: 1020 | monitor_thread = Thread(target=monitor, args=(exit_event, args.monitor)) 1021 | monitor_thread.daemon = True 1022 | monitor_thread.start() 1023 | 1024 | try: 1025 | loop = GLib.MainLoop() 1026 | loop.run() 1027 | except (KeyboardInterrupt, SystemExit): 1028 | pass 1029 | 1030 | exit_event.set() 1031 | loop.quit() 1032 | thread.join(timeout=1) 1033 | if args.monitor is not None: 1034 | monitor_thread.join(timeout=0.1) 1035 | 1036 | 1037 | if __name__ == '__main__': 1038 | main() 1039 | --------------------------------------------------------------------------------