Skip to content

Commit 8ccee9b

Browse files
committed
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.
1 parent 04a1fcf commit 8ccee9b

3 files changed

Lines changed: 87 additions & 1 deletion

File tree

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
"""Tests for Tools/ftscalingbench/ftscalingbench.py."""
2+
3+
import sys
4+
import unittest
5+
from unittest import mock
6+
7+
from test.test_tools import skip_if_missing, imports_under_tool
8+
9+
skip_if_missing('ftscalingbench')
10+
11+
with imports_under_tool('ftscalingbench'):
12+
import ftscalingbench
13+
14+
15+
def lscpu(rows):
16+
"""Build `lscpu -p=cpu,node,core,MAXMHZ` output from (cpu, node, core, mhz)."""
17+
lines = ['# cpu,node,core,MAXMHZ']
18+
lines += [f'{cpu},{node},{core},{mhz}' for cpu, node, core, mhz in rows]
19+
return '\n'.join(lines) + '\n'
20+
21+
22+
def smt_rows(count, mhz, first_cpu=0, first_core=0):
23+
"""Rows for `count` cores with two hardware threads each."""
24+
rows = []
25+
for i in range(count):
26+
cpu = first_cpu + i * 2
27+
rows.append((cpu, 0, first_core + i, mhz))
28+
rows.append((cpu + 1, 0, first_core + i, mhz))
29+
return rows
30+
31+
32+
class DetermineAffinityTests(unittest.TestCase):
33+
34+
def select(self, output):
35+
with (mock.patch('subprocess.check_output', return_value=output),
36+
mock.patch.object(sys, 'platform', 'linux')):
37+
return ftscalingbench.determine_num_threads_and_affinity()
38+
39+
def test_performance_cores_binned_at_different_clocks(self):
40+
# Two of the eight performance cores clock higher than the rest.
41+
rows = smt_rows(4, '5000.0000')
42+
rows += smt_rows(2, '5200.0000', first_cpu=8, first_core=4)
43+
rows += smt_rows(2, '5000.0000', first_cpu=12, first_core=6)
44+
rows += [(16 + i, 0, 8 + i, '3700.0000') for i in range(8)]
45+
self.assertEqual(self.select(lscpu(rows)),
46+
[0, 2, 4, 6, 8, 10, 12, 14])
47+
48+
def test_efficiency_cores_are_skipped(self):
49+
rows = smt_rows(4, '4800.0000')
50+
rows += [(8 + i, 0, 4 + i, '3600.0000') for i in range(4)]
51+
self.assertEqual(self.select(lscpu(rows)), [0, 2, 4, 6])
52+
53+
def test_one_thread_per_physical_core(self):
54+
self.assertEqual(self.select(lscpu(smt_rows(8, '3700.0000'))),
55+
[0, 2, 4, 6, 8, 10, 12, 14])
56+
57+
def test_missing_max_clock(self):
58+
# MAXMHZ is empty on some kernels and in many virtual machines.
59+
rows = [(i, 0, i, '') for i in range(4)]
60+
self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3])
61+
62+
def test_second_numa_node_is_ignored(self):
63+
rows = [(i, 0, i, '3000.0000') for i in range(4)]
64+
rows += [(4 + i, 1, 4 + i, '3000.0000') for i in range(4)]
65+
self.assertEqual(self.select(lscpu(rows)), [0, 1, 2, 3])
66+
67+
def test_lscpu_missing(self):
68+
with (mock.patch('subprocess.check_output', side_effect=FileNotFoundError),
69+
mock.patch.object(sys, 'platform', 'linux')):
70+
cpus = ftscalingbench.determine_num_threads_and_affinity()
71+
self.assertTrue(all(cpu is None for cpu in cpus))
72+
73+
74+
if __name__ == '__main__':
75+
unittest.main()
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
``Tools/ftscalingbench`` now uses every performance core on processors that
2+
clock some of them higher than others, such as Intel parts with Turbo Boost
3+
Max 3.0. It previously kept only the fastest cores and ran with fewer threads
4+
than the machine offered.

Tools/ftscalingbench/ftscalingbench.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -403,10 +403,17 @@ def determine_num_threads_and_affinity():
403403
cpus = []
404404
cores = set()
405405
max_mhz_all = max(row[3] for row in table)
406+
min_mhz_all = min(row[3] for row in table)
407+
# Performance cores are not always binned to the same clock, so split them
408+
# from the efficiency cores at the midpoint rather than at the maximum.
409+
if max_mhz_all != min_mhz_all:
410+
min_mhz_wanted = (max_mhz_all + min_mhz_all) / 2
411+
else:
412+
min_mhz_wanted = 0
406413
for cpu, node, core, maxmhz in table:
407414
# Choose only CPUs on the same node, unique cores, and try to avoid
408415
# "efficiency" cores.
409-
if node == 0 and core not in cores and maxmhz == max_mhz_all:
416+
if node == 0 and core not in cores and maxmhz >= min_mhz_wanted:
410417
cpus.append(cpu)
411418
cores.add(core)
412419
return cpus

0 commit comments

Comments
 (0)