diff --git a/docs/denoise.md b/docs/denoise.md index 8df1b6e3..6e78cb6c 100755 --- a/docs/denoise.md +++ b/docs/denoise.md @@ -5,7 +5,7 @@ It adjusts the system settings to reduce interference by: - raising the process priority to minimize the impact of the process scheduler - using CPU core shielding to minimize interference from other processes - - requesting the system to avoid tubo-boosting to keep the CPU at a constant frequency + - requesting the system to avoid turbo-boosting to keep the CPU at a constant frequency - requesting the system to use a performance governor, which tries to keep the CPU at a high frequency - reducing the frequency with which perf events can be collected, except when in profiling mode diff --git a/rebench/denoise.py b/rebench/denoise.py index ed51899c..1169c602 100755 --- a/rebench/denoise.py +++ b/rebench/denoise.py @@ -4,20 +4,29 @@ import sys from argparse import ArgumentParser +from glob import glob from math import log, floor from multiprocessing import Pool +from os.path import isfile, join as path_join, abspath, dirname, realpath from subprocess import check_output, CalledProcessError, DEVNULL, STDOUT -from typing import Optional, Union, Literal, TypedDict, TYPE_CHECKING +from typing import ( + Literal, + Optional, + Tuple, + TypedDict, + Union, + TYPE_CHECKING, +) if TYPE_CHECKING: from typing import NotRequired -denoise_py = os.path.abspath(__file__) +denoise_py = abspath(__file__) if __name__ == "__main__": # ensure that the rebench module is available - rebench_module = os.path.dirname(denoise_py) - sys.path.append(os.path.dirname(rebench_module)) + rebench_module = dirname(denoise_py) + sys.path.append(dirname(rebench_module)) # pylint: disable-next=import-error from output import output_as_str, UIError # type: ignore @@ -65,7 +74,7 @@ def __init__(self): def get_which(self): if not self._which_path: - if os.path.isfile("/usr/bin/which"): + if isfile("/usr/bin/which"): self._which_path = "/usr/bin/which" else: raise UIError( @@ -87,7 +96,7 @@ def absolute_path_for_command(self, command, arguments_for_successful_exe): selected_cmd = output_as_str( check_output([self.get_which(), command], shell=False, stderr=DEVNULL) ).strip() - result_cmd = os.path.realpath(selected_cmd) + result_cmd = realpath(selected_cmd) except CalledProcessError: result_cmd = command @@ -115,7 +124,7 @@ def ensure_denoise(self): if self._denoise_path is None: if os.access(denoise_py, os.X_OK): self._denoise_path = denoise_py - elif not os.path.isfile(denoise_py): + elif not isfile(denoise_py): raise UIError( f"{denoise_py} not found. " "Could it be that the user has no access to the file? " @@ -269,32 +278,73 @@ def _set_scaling_governor(governor, num_cores) -> str: return governor +_INTEL_NO_TURBO = "/sys/devices/system/cpu/intel_pstate/no_turbo" +_CPUFREQ_BOOST = "/sys/devices/system/cpu/cpufreq/boost" +_CPUFREQ_POLICY_DIR = "/sys/devices/system/cpu/cpufreq" + + +def _find_turbo_boost_control() -> Union[Tuple[list[str], bool], str]: + """ + Find the paths that allow us to disable turbo boost. + """ + if isfile(_INTEL_NO_TURBO): + return [_INTEL_NO_TURBO], True + + if isfile(_CPUFREQ_BOOST): + return [_CPUFREQ_BOOST], False + + # e.g. amd-pstate in active mode only provides per-policy boost files + policy_boost_files = sorted( + glob(path_join(_CPUFREQ_POLICY_DIR, "policy*", "boost")) + ) + if policy_boost_files: + return policy_boost_files, False + + return ( + "failed: No turbo boost control found. Checked " + + _INTEL_NO_TURBO + + ", " + + _CPUFREQ_BOOST + + ", and " + + path_join(_CPUFREQ_POLICY_DIR, "policy*", "boost") + ) + + def _read_no_turbo() -> Optional[Union[bool, str]]: + control = _find_turbo_boost_control() + if isinstance(control, str): + return control + + files, indicates_disabled = control try: - with open( - "/sys/devices/system/cpu/intel_pstate/no_turbo", "r", encoding="utf-8" - ) as nt_file: - return nt_file.read().strip() == "1" + values = [] + for filename in files: + with open(filename, "r", encoding="utf-8") as nt_file: + values.append(nt_file.read().strip() == "1") except IOError as e: - if e.errno == 2 and "No such file" in str(e): - return "failed: No such file: /sys/devices/system/cpu/intel_pstate/no_turbo" return "failed: " + str(e) + if indicates_disabled: + return all(values) + return not any(values) + def _set_no_turbo(no_turbo_value: bool) -> Union[Literal[True], str]: - if no_turbo_value: - value = "1" + control = _find_turbo_boost_control() + if isinstance(control, str): + return control + + files, indicates_disabled = control + if indicates_disabled: + value = "1" if no_turbo_value else "0" else: - value = "0" + value = "0" if no_turbo_value else "1" try: - with open( - "/sys/devices/system/cpu/intel_pstate/no_turbo", "w", encoding="utf-8" - ) as nt_file: - nt_file.write(value + "\n") + for filename in files: + with open(filename, "w", encoding="utf-8") as nt_file: + nt_file.write(value + "\n") except IOError as e: - if e.errno == 2 and "No such file" in str(e): - return "failed: No such file: /sys/devices/system/cpu/intel_pstate/no_turbo" return "failed: " + str(e) return True diff --git a/rebench/tests/integration_test.py b/rebench/tests/integration_test.py index 0cb48e2a..8e00cea8 100644 --- a/rebench/tests/integration_test.py +++ b/rebench/tests/integration_test.py @@ -65,9 +65,24 @@ def test_rebench(rebench_cmd, rebench_conf, rebench_module_path, test_data_file) "can_set_scaling_governor", "can_minimize_perf_sampling", ], - "cassius": ["can_set_nice", "can_set_scaling_governor"], - "laertes": ["can_set_nice", "can_set_scaling_governor"], - "ophelia": ["can_set_nice", "can_set_scaling_governor"], + "cassius": [ + "can_set_nice", + "can_set_scaling_governor", + "initial_no_turbo", + "can_minimize_perf_sampling", + ], + "laertes": [ + "can_set_nice", + "can_set_scaling_governor", + "initial_no_turbo", + "can_minimize_perf_sampling", + ], + "ophelia": [ + "can_set_nice", + "can_set_scaling_governor", + "initial_no_turbo", + "can_minimize_perf_sampling", + ], "zullie1": ["can_set_nice"], } _default_capabilities = [ @@ -111,11 +126,21 @@ def test_machine_denoise_capabilities(): assert isinstance( capabilities, dict ), f"Expected capabilities to be a dict, but got {capabilities}" + + bool_expectations = {} + for k, v in capabilities.items(): + if v is True: + bool_expectations[k] = v + for cap in expectations: - assert cap in capabilities, ( + assert cap in bool_expectations, ( f"Expected {cap} to be in capabilities, but got {capabilities}," f" got_json: {got_json}, raw_output: {raw_output} on {hostname}" ) assert ( - capabilities[cap] is True + bool_expectations[cap] is True ), f"Expected {cap} to be True, but got {capabilities} on {hostname}" + del bool_expectations[cap] + assert ( + not bool_expectations + ), f"Expected no more capabilities, but got {bool_expectations}"