Geek Out: A Python Tool to Toggle Your CPU's Logical Cores (SMT Manager)

Hey everyone,

Today's post is for those who like to tinker a bit deeper with their system's performance, especially on Linux. I've put together a Python script called smt_manager.py that allows you to view and toggle the state of your CPU's logical processors (often known by Intel's term, Hyper-Threading, or AMD's Simultaneous Multi-Threading - SMT).

What's SMT/Hyper-Threading and Why Mess With It?

All 80 Threads Available

Most modern CPUs present more "cores" to the operating system than they physically have. For instance, an 8-core CPU might show up as 16 "processors." This is typically achieved by each physical core being able to handle two threads simultaneously – one primary thread and one logical (or SMT/hyper-thread).

For many everyday tasks and general multitasking, this is great! It can improve overall system responsiveness and throughput. However, there are specific situations where having these logical cores enabled can actually be detrimental to performance, or at least not beneficial:

  • Certain High-Performance Computing (HPC) Workloads: Some computationally intensive tasks, especially those that are heavily reliant on floating-point performance or specific cache behaviors, might see no improvement or even a slight degradation with SMT enabled. The two threads on a single physical core share resources, and this contention can sometimes outweigh the benefits.
  • Specific Benchmarks: You might find that some benchmarks give you "better" (or at least more consistent) scores with logical cores turned off, as it ensures each running process has dedicated access to a full physical core's resources without sharing.
  • Some Gaming Scenarios: While less common now, some older games or even specific modern titles might exhibit slightly more stable framerates or reduced micro-stuttering with SMT disabled, particularly if the game isn't optimized to effectively use a high number of logical cores and prefers fewer, more powerful physical cores.
  • Virtualization: Depending on the virtualization software and the workloads running in your VMs, you might find that assigning only physical cores to demanding VMs can lead to more predictable performance.

The ability to easily toggle these logical cores on or off without rebooting and going into the BIOS/UEFI can be handy for testing these scenarios or optimizing for specific tasks.

Introducing smt_manager.py

After Disabling 40 Logical Cores

This Python script is designed to do just that on Linux systems. It directly interacts with the /sys/devices/system/cpu/ interface to:

  1. Identify all available CPU processors.
  2. Determine which are "primary" (physical) cores and which are "logical" (SMT/hyper-threaded) cores.
  3. Display the current online/offline status of each processor.
  4. Allow you to set all logical cores to either "online" (enabled) or "offline" (disabled).

Important Note: Changing CPU states requires root privileges. So, you'll need to run the script with sudo.

The Code:

Here's the GitHub Gist and the script itself:

#!/usr/bin/env python3

# smt-manager.py - a script for managing logical cores
# Python implementation of the original Perl script by Steven Barrett
# https://github.com/damentz/smt-manager

import argparse
import os
import subprocess
import sys

# This is the top folder where CPUs can be enumerated and more detail retrieved
SYS_CPU = "/sys/devices/system/cpu"
DEBUG = False


def get_cpu_indexes() -> list[int]:
    """
    Get a list of CPU indexes by reading the system CPU directory

    Returns:
        list[int]: A sorted list of CPU indexes
    """
    try:
        cpu_dirs = [
            d for d in os.listdir(SYS_CPU) if d.startswith("cpu") and d[3:].isdigit()
        ]
        cpu_indexes = [int(d[3:]) for d in cpu_dirs]
        return sorted(cpu_indexes)
    except OSError as e:
        sys.exit(f"Cannot open folder: {SYS_CPU}. Error: {e}")


def get_cpu_settings() -> dict[int, dict[str, str]]:
    """
    Get settings for all CPUs including core type and power state

    Returns:
        dict[int, dict[str, str]]: A dictionary mapping CPU indexes to their settings,
            where each setting is a dictionary with 'core_type' and 'power' keys
    """
    cpu_indexes = get_cpu_indexes()
    cpus = {}

    for cpu in cpu_indexes:
        siblings_file = f"{SYS_CPU}/cpu{cpu}/topology/thread_siblings_list"
        power_file = f"{SYS_CPU}/cpu{cpu}/online"

        cpu_settings = {"core_type": "unknown", "power": "offline"}

        # Populate core topology, primary / logical
        try:
            with open(siblings_file, "r") as f:
                siblings_line = f.readline().strip()
                # Handle both comma-separated and hyphen-separated formats
                if "," in siblings_line:
                    siblings = [int(s) for s in siblings_line.split(",")]
                elif "-" in siblings_line:
                    start, end = map(int, siblings_line.split("-"))
                    siblings = list(range(start, end + 1))
                else:
                    siblings = [int(siblings_line)]

                if cpu == siblings[0]:
                    cpu_settings["core_type"] = "primary"
                else:
                    cpu_settings["core_type"] = "logical"
        except (OSError, IOError):
            if DEBUG:
                print(f"[ERROR] Could not open: {siblings_file}")

        # Populate core status, online / offline
        try:
            # CPU0 is always online and doesn't have an 'online' file
            if cpu == 0:
                cpu_settings["power"] = "online"
            else:
                with open(power_file, "r") as f:
                    cpu_power = f.readline().strip()
                    if cpu_power == "1":
                        cpu_settings["power"] = "online"
        except (OSError, IOError):
            if DEBUG:
                print(f"[ERROR] Could not open: {power_file}, assuming online")
            cpu_settings["power"] = "online"

        cpus[cpu] = cpu_settings

    return cpus


def set_logical_cpus(power_state: str) -> bool:
    """
    Set all logical CPUs to the specified power state (online/offline)

    Args:
        power_state (str): The desired power state ('online' or 'offline')

    Returns:
        bool: True if any CPU state was changed, False otherwise
    """
    cpus = get_cpu_settings()
    state_changed = False
    changed_cpus = []

    for cpu in sorted(cpus.keys()):
        # Skip CPU0 as it can't be disabled
        if cpu == 0:
            continue

        if (
            cpus[cpu]["core_type"] == "logical" or cpus[cpu]["core_type"] == "unknown"
        ) and cpus[cpu]["power"] != power_state:
            power_file = f"{SYS_CPU}/cpu{cpu}/online"

            try:
                with open(power_file, "w") as f:
                    state_changed = True
                    print(f"Setting CPU {cpu} to {power_state} ... ", end="")

                    if power_state == "online":
                        f.write("1")
                    elif power_state == "offline":
                        f.write("0")

                    changed_cpus.append(cpu)
                    print("done!")
            except (OSError, IOError):
                print(
                    f"[ERROR] failed to open file for writing: {power_file}. Are you root?"
                )

    if state_changed:
        # Rebalance the interrupts after power state changes
        try:
            subprocess.run(["irqbalance", "--oneshot"], check=True)
        except (subprocess.SubprocessError, FileNotFoundError):
            print(
                "[ERROR] Failed to balance interrupts with 'irqbalance --oneshot', "
                "you may experience strange behavior.",
                file=sys.stderr,
            )

        print()

    return state_changed


def pretty_print_topology() -> None:
    """
    Print the current CPU topology in a readable format using only standard library
    """
    cpus = get_cpu_settings()

    # Get the maximum width needed for each column
    cpu_width = max(len(str(cpu)) for cpu in cpus.keys())
    type_width = max(len(cpus[cpu]["core_type"]) for cpu in cpus.keys())
    power_width = max(len(cpus[cpu]["power"]) for cpu in cpus.keys())

    # Add header width to the calculation
    cpu_width = max(cpu_width, len("CPU"))
    type_width = max(type_width, len("Core Type"))
    power_width = max(power_width, len("Power State"))

    # Calculate total table width for the title
    total_width = cpu_width + type_width + power_width + 8  # 8 for borders and padding

    # Print table title
    print("CPU Topology".center(total_width))
    print("-" * total_width)

    # Print header
    print(
        f" {'CPU':<{cpu_width}} | {'Core Type':<{type_width}} | {'Power State':<{power_width}} "
    )
    print(f" {'-' * cpu_width} | {'-' * type_width} | {'-' * power_width} ")

    # Print rows
    for cpu in sorted(cpus.keys()):
        print(
            f" {cpu:<{cpu_width}} | {cpus[cpu]['core_type']:<{type_width}} | {cpus[cpu]['power']:<{power_width}} "
        )

    print()


def main() -> None:
    parser = argparse.ArgumentParser(
        description="View current status of CPU topology or set logical cores to offline or online.",
        epilog="This script provides details about whether each CPU is physical or logical. "
        "When provided an optional parameter, the logical CPUs can be enabled or disabled.",
    )

    group = parser.add_mutually_exclusive_group()
    group.add_argument(
        "--online", action="store_true", help="Enables all logical CPU cores"
    )
    group.add_argument(
        "--offline", action="store_true", help="Disables all logical CPU cores"
    )
    parser.add_argument("--debug", action="store_true", help="Enable debug output")

    args = parser.parse_args()

    global DEBUG
    DEBUG = args.debug

    power_state = None
    if args.online:
        power_state = "online"
    elif args.offline:
        power_state = "offline"

    pretty_print_topology()

    if power_state and set_logical_cpus(power_state):
        # If there was a change, print the new state
        pretty_print_topology()


if __name__ == "__main__":
    # Check if running as root, which is required for changing CPU states
    if os.geteuid() != 0 and (
        len(sys.argv) > 1 and ("--online" in sys.argv or "--offline" in sys.argv)
    ):
        print("You need to have root privileges to change CPU states.")
        print("Please run the script with sudo or as root.")
        sys.exit(1)

    main()

How to Use It:

  1. Save the code above as a Python file (e.g., smt_manager.py).
  2. Make it executable: chmod +x smt_manager.py
  3. Run it:
    • To view current topology: sudo ./smt_manager.py
    • To disable logical cores: sudo ./smt_manager.py --offline
    • To enable logical cores: sudo ./smt_manager.py --online
    • For debug output: sudo ./smt_manager.py --debug (or add it to the online/offline commands)

The script will print the CPU topology before, and if changes are made, it will print the new topology afterward. It also attempts to run irqbalance --oneshot after changing CPU states to help rebalance system interrupts, which is generally a good idea.

This is definitely a more advanced tool, but for those who need this kind of control for specific performance tuning or testing on Linux, it can be quite handy.

Let me know if you've found other interesting use cases for toggling SMT!

As always,
Michael Garcia a.k.a. TheCrazyGM



0
0
0.000
7 comments
avatar
(Edited)

Super cool! I've been tweaking my Arch Linux systems for years, so I love tools like this. I'm going to give it a try. I could also make BASH alias for the toggle commands, like smton and smtoff, to make to even simpler and quicker. Thank you for your awesome tools! 😁 🙏 💚 ✨ 🤙

0
0
0.000
avatar

bash alias is a smart move if you plan on using it often.

Give it a try, I don't know what kind of CPU you are using, but I have found that even as counter intuitive as it sounds, some things build from source faster with the logical cores turned off.

0
0
0.000
avatar

I tried it this morning, and it works perfectly well on my 10th generation i7 (skylake), and I did end up creating aliases for the commands. Now I just have to test assorted things to see if I notice a difference. I used to build from source all the time, and I'd like to get back to it, though I need a bit more time...lol! I dig it mucho! 😁 🙏 💚 ✨ 🤙

0
0
0.000
avatar

Congratulations @thecrazygm! You received a personal badge!

You powered-up at least 10 HIVE on Hive Power Up Day!
Wait until the end of Power Up Day to find out the size of your Power-Bee.
May the Hive Power be with you!

You can view your badges on your board and compare yourself to others in the Ranking

Check out our last posts:

Hive Power Up Month Challenge - May 2025 Winners List
Be ready for the June edition of the Hive Power Up Month!
Hive Power Up Day - June 1st 2025
0
0
0.000