From 8ccee9bca17dd839ef42bafd48451dd4ed1d17ca Mon Sep 17 00:00:00 2001 From: Farhan Saif Date: Sun, 2 Aug 2026 10:39:59 -0500 Subject: [PATCH] gh-155090: Fix ftscalingbench core selection on CPUs with favoured cores ftscalingbench keeps only the CPUs whose MAXMHZ equals the highest MAXMHZ on the machine, so that efficiency cores are left out. That assumes every performance core shares one clock ceiling. Intel's Turbo Boost Max 3.0 bins a couple of cores above their siblings, and on such a part only those few survive the filter. On an i7-14650HX (8 performance cores, two of them at 5200 MHz and the rest at 5000 MHz, plus 8 efficiency cores at 3700 MHz) the benchmark picked two CPUs and reported scaling for 2 threads instead of 8. Split performance and efficiency cores at the midpoint between the highest and lowest clock instead, which keeps all the performance cores however they are individually binned. Machines that report one clock for every core, or no clock at all, are unaffected. --- Lib/test/test_tools/test_ftscalingbench.py | 75 +++++++++++++++++++ ...-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst | 4 + Tools/ftscalingbench/ftscalingbench.py | 9 ++- 3 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 Lib/test/test_tools/test_ftscalingbench.py create mode 100644 Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst diff --git a/Lib/test/test_tools/test_ftscalingbench.py b/Lib/test/test_tools/test_ftscalingbench.py new file mode 100644 index 00000000000000..81fe925c8d64fe --- /dev/null +++ b/Lib/test/test_tools/test_ftscalingbench.py @@ -0,0 +1,75 @@ +"""Tests for Tools/ftscalingbench/ftscalingbench.py.""" + +import sys +import unittest +from unittest import mock + +from test.test_tools import skip_if_missing, imports_under_tool + +skip_if_missing('ftscalingbench') + +with imports_under_tool('ftscalingbench'): + import ftscalingbench + + +def lscpu(rows): + """Build `lscpu -p=cpu,node,core,MAXMHZ` output from (cpu, node, core, mhz).""" + lines = ['# cpu,node,core,MAXMHZ'] + lines += [f'{cpu},{node},{core},{mhz}' for cpu, node, core, mhz in rows] + return '\n'.join(lines) + '\n' + + +def smt_rows(count, mhz, first_cpu=0, first_core=0): + """Rows for `count` cores with two hardware threads each.""" + rows = [] + for i in range(count): + cpu = first_cpu + i * 2 + rows.append((cpu, 0, first_core + i, mhz)) + rows.append((cpu + 1, 0, first_core + i, mhz)) + return rows + + +class DetermineAffinityTests(unittest.TestCase): + + def select(self, output): + with (mock.patch('subprocess.check_output', return_value=output), + mock.patch.object(sys, 'platform', 'linux')): + return ftscalingbench.determine_num_threads_and_affinity() + + def test_performance_cores_binned_at_different_clocks(self): + # Two of the eight performance cores clock higher than the rest. + rows = smt_rows(4, '5000.0000') + rows += smt_rows(2, '5200.0000', first_cpu=8, first_core=4) + rows += smt_rows(2, '5000.0000', first_cpu=12, first_core=6) + rows += [(16 + i, 0, 8 + i, '3700.0000') for i in range(8)] + self.assertEqual(self.select(lscpu(rows)), + [0, 2, 4, 6, 8, 10, 12, 14]) + + def test_efficiency_cores_are_skipped(self): + rows = smt_rows(4, '4800.0000') + rows += [(8 + i, 0, 4 + i, '3600.0000') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 2, 4, 6]) + + def test_one_thread_per_physical_core(self): + self.assertEqual(self.select(lscpu(smt_rows(8, '3700.0000'))), + [0, 2, 4, 6, 8, 10, 12, 14]) + + def test_missing_max_clock(self): + # MAXMHZ is empty on some kernels and in many virtual machines. + rows = [(i, 0, i, '') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3]) + + def test_second_numa_node_is_ignored(self): + rows = [(i, 0, i, '3000.0000') for i in range(4)] + rows += [(4 + i, 1, 4 + i, '3000.0000') for i in range(4)] + self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3]) + + def test_lscpu_missing(self): + with (mock.patch('subprocess.check_output', side_effect=FileNotFoundError), + mock.patch.object(sys, 'platform', 'linux')): + cpus = ftscalingbench.determine_num_threads_and_affinity() + self.assertTrue(all(cpu is None for cpu in cpus)) + + +if __name__ == '__main__': + unittest.main() diff --git a/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst b/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst new file mode 100644 index 00000000000000..0c32dbc1a1b012 --- /dev/null +++ b/Misc/NEWS.d/next/Tools-Demos/2026-08-02-11-42-15.gh-issue-155090.Kq7mZt.rst @@ -0,0 +1,4 @@ +``Tools/ftscalingbench`` now uses every performance core on processors that +clock some of them higher than others, such as Intel parts with Turbo Boost +Max 3.0. It previously kept only the fastest cores and ran with fewer threads +than the machine offered. diff --git a/Tools/ftscalingbench/ftscalingbench.py b/Tools/ftscalingbench/ftscalingbench.py index a79242e740371b..f0e9e0574825aa 100644 --- a/Tools/ftscalingbench/ftscalingbench.py +++ b/Tools/ftscalingbench/ftscalingbench.py @@ -403,10 +403,17 @@ def determine_num_threads_and_affinity(): cpus = [] cores = set() max_mhz_all = max(row[3] for row in table) + min_mhz_all = min(row[3] for row in table) + # Performance cores are not always binned to the same clock, so split them + # from the efficiency cores at the midpoint rather than at the maximum. + if max_mhz_all != min_mhz_all: + min_mhz_wanted = (max_mhz_all + min_mhz_all) / 2 + else: + min_mhz_wanted = 0 for cpu, node, core, maxmhz in table: # Choose only CPUs on the same node, unique cores, and try to avoid # "efficiency" cores. - if node == 0 and core not in cores and maxmhz == max_mhz_all: + if node == 0 and core not in cores and maxmhz >= min_mhz_wanted: cpus.append(cpu) cores.add(core) return cpus