-
Notifications
You must be signed in to change notification settings - Fork 167
Expand file tree
/
Copy paththrottled.py
More file actions
executable file
·1615 lines (1398 loc) · 63.4 KB
/
Copy paththrottled.py
File metadata and controls
executable file
·1615 lines (1398 loc) · 63.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
import argparse
import asyncio
import configparser
import glob
import gzip
import math
import os
import re
import struct
import subprocess
import sys
import traceback
from collections import defaultdict
from datetime import datetime
from errno import EACCES, EIO, ENOENT, EPERM
from platform import uname
from subprocess import check_output, CalledProcessError, PIPE
from threading import Event, Lock, Thread, current_thread, main_thread
from time import time
from mmio import MMIO, MMIOError
from throttled_version import __version__
DEFAULT_SYSFS_POWER_PATH = '/sys/class/power_supply/AC*/online'
UPOWER_SERVICE = 'org.freedesktop.UPower'
UPOWER_PATH = '/org/freedesktop/UPower'
LOGIN1_SERVICE = 'org.freedesktop.login1'
LOGIN1_PATH = '/org/freedesktop/login1'
LOGIN1_MANAGER_INTERFACE = 'org.freedesktop.login1.Manager'
DBUS_PROPERTIES_INTERFACE = 'org.freedesktop.DBus.Properties'
VOLTAGE_PLANES = {'CORE': 0, 'GPU': 1, 'CACHE': 2, 'UNCORE': 3, 'ANALOGIO': 4}
CURRENT_PLANES = {'CORE': 0, 'GPU': 1, 'CACHE': 2}
TRIP_TEMP_RANGE = [40, 97]
PKG_POWER_LIMIT_POWER_MASK = (1 << 15) - 1
PKG_POWER_LIMIT_TIME_WINDOW_MASK = (1 << 7) - 1
POWER_PROFILES = ('AC', 'BATTERY')
UNDERVOLT_KEYS = ('UNDERVOLT', 'UNDERVOLT.AC', 'UNDERVOLT.BATTERY')
ICCMAX_KEYS = ('ICCMAX', 'ICCMAX.AC', 'ICCMAX.BATTERY')
power = {'source': None, 'method': 'polling'}
# serializes config publication with the resume callback's read-then-write
config_lock = Lock()
MSR_DICT = {
'MSR_PLATFORM_INFO': 0xCE,
'MSR_OC_MAILBOX': 0x150,
'IA32_PERF_STATUS': 0x198,
'IA32_THERM_STATUS': 0x19C,
'MSR_TEMPERATURE_TARGET': 0x1A2,
'MSR_POWER_CTL': 0x1FC,
'MSR_RAPL_POWER_UNIT': 0x606,
'MSR_PKG_POWER_LIMIT': 0x610,
'MSR_INTEL_PKG_ENERGY_STATUS': 0x611,
'MSR_DRAM_ENERGY_STATUS': 0x619,
'MSR_PP1_ENERGY_STATUS': 0x641,
'MSR_CONFIG_TDP_CONTROL': 0x64B,
'IA32_HWP_REQUEST': 0x774,
}
HWP_PERFORMANCE_VALUE = 0x20
HWP_DEFAULT_VALUE = 0x80
HWP_INTERVAL = 60
UNDERVOLT_TICKS_PER_MV = 1.024
UNDERVOLT_MIN_TICKS = -(1 << 10)
UNDERVOLT_MAX_TICKS = 0
ICCMAX_STEPS_PER_A = 4
ICCMAX_MAX_FIELD = 0x3FF
MCHBAR_ENABLE_BIT = 1
MCHBAR_PACKAGE_POWER_LIMIT_OFFSET = 0x59A0
MCHBAR_PACKAGE_POWER_LIMIT_SIZE = 8
PCI_HOST_BRIDGE_SYSFS_PATH = '/sys/bus/pci/devices/0000:00:00.0'
PCI_VENDOR_ID_INTEL = 0x8086
# The masked address bits also encode the per-generation window alignment, so
# rejecting any bit outside the mask fully validates the BAR before /dev/mem
# is ever opened.
MCHBAR_ADDRESS_MASK_39_15 = ((1 << 39) - 1) & ~((1 << 15) - 1)
MCHBAR_ADDRESS_MASK_39_17 = ((1 << 39) - 1) & ~((1 << 17) - 1)
MCHBAR_ADDRESS_MASK_42_17 = ((1 << 42) - 1) & ~((1 << 17) - 1)
platform_info_bits = {
'maximum_non_turbo_ratio': [8, 15],
'maximum_efficiency_ratio': [40, 47],
'minimum_operating_ratio': [48, 55],
'feature_ppin_cap': [23, 23],
'feature_programmable_turbo_ratio': [28, 28],
'feature_programmable_tdp_limit': [29, 29],
'number_of_additional_tdp_profiles': [33, 34],
'feature_programmable_temperature_target': [30, 30],
'feature_low_power_mode': [32, 32],
}
thermal_status_bits = {
'thermal_limit_status': [0, 0],
'thermal_limit_log': [1, 1],
'prochot_or_forcepr_status': [2, 2],
'prochot_or_forcepr_log': [3, 3],
'crit_temp_status': [4, 4],
'crit_temp_log': [5, 5],
'thermal_threshold1_status': [6, 6],
'thermal_threshold1_log': [7, 7],
'thermal_threshold2_status': [8, 8],
'thermal_threshold2_log': [9, 9],
'power_limit_status': [10, 10],
'power_limit_log': [11, 11],
'current_limit_status': [12, 12],
'current_limit_log': [13, 13],
'cross_domain_limit_status': [14, 14],
'cross_domain_limit_log': [15, 15],
'cpu_temp': [16, 22],
'temp_resolution': [27, 30],
'reading_valid': [31, 31],
}
supported_cpus = {
(6, 26, 1): 'Nehalem',
(6, 26, 2): 'Nehalem-EP',
(6, 26, 4): 'Bloomfield',
(6, 28, 2): 'Silverthorne',
(6, 28, 10): 'PineView',
(6, 29, 0): 'Dunnington-6C',
(6, 29, 1): 'Dunnington',
(6, 30, 0): 'Lynnfield',
(6, 30, 5): 'Lynnfield_CPUID',
(6, 31, 1): 'Auburndale',
(6, 37, 2): 'Clarkdale',
(6, 37, 5): 'Arrandale',
(6, 38, 1): 'TunnelCreek',
(6, 39, 2): 'Medfield',
(6, 42, 2): 'SandyBridge',
(6, 42, 6): 'SandyBridge',
(6, 42, 7): 'Sandy Bridge-DT',
(6, 44, 1): 'Westmere-EP',
(6, 44, 2): 'Gulftown',
(6, 45, 5): 'Sandy Bridge-EP',
(6, 45, 6): 'Sandy Bridge-E',
(6, 46, 4): 'Beckton',
(6, 46, 5): 'Beckton',
(6, 46, 6): 'Beckton',
(6, 47, 2): 'Eagleton',
(6, 53, 1): 'Cloverview',
(6, 54, 1): 'Cedarview-D',
(6, 54, 9): 'Centerton',
(6, 55, 3): 'Bay Trail-D',
(6, 55, 8): 'Silvermont',
(6, 58, 9): 'Ivy Bridge-DT',
(6, 60, 3): 'Haswell-DT',
(6, 61, 4): 'Broadwell-U',
(6, 62, 3): 'IvyBridgeEP',
(6, 62, 4): 'Ivy Bridge-E',
(6, 63, 2): 'Haswell-EP',
(6, 69, 1): 'HaswellULT',
(6, 70, 1): 'Crystal Well-DT',
(6, 71, 1): 'Broadwell-H',
(6, 76, 3): 'Braswell',
(6, 77, 8): 'Avoton',
(6, 78, 3): 'Skylake',
(6, 79, 1): 'BroadwellE',
(6, 85, 4): 'SkylakeXeon',
(6, 85, 6): 'CascadeLakeSP',
(6, 85, 7): 'CascadeLakeXeon2',
(6, 86, 2): 'BroadwellDE',
(6, 86, 4): 'BroadwellDE',
(6, 87, 0): 'KnightsLanding',
(6, 87, 1): 'KnightsLanding',
(6, 90, 0): 'Moorefield',
(6, 92, 9): 'Apollo Lake',
(6, 93, 1): 'SoFIA',
(6, 94, 0): 'Skylake',
(6, 94, 3): 'Skylake-S',
(6, 95, 1): 'Denverton',
(6, 102, 3): 'Cannon Lake-U',
(6, 117, 10): 'Spreadtrum',
(6, 122, 1): 'Gemini Lake-D',
(6, 122, 8): 'GoldmontPlus',
(6, 126, 5): 'IceLakeY',
(6, 138, 1): 'Lakefield',
(6, 140, 1): 'TigerLake-U',
(6, 140, 2): 'TigerLake-U',
(6, 141, 1): 'TigerLake-H',
(6, 142, 9): 'KabyLake',
(6, 142, 10): 'KabyLake',
(6, 142, 11): 'WhiskeyLake',
(6, 142, 12): 'CometLake-U',
(6, 151, 2): 'AlderLake-S/HX',
(6, 151, 5): 'AlderLake-S',
(6, 154, 3): 'AlderLake-P/H',
(6, 154, 4): 'AlderLake-U',
(6, 156, 0): 'JasperLake',
(6, 158, 9): 'KabyLakeG',
(6, 158, 10): 'CoffeeLake',
(6, 158, 11): 'CoffeeLake',
(6, 158, 12): 'CoffeeLake',
(6, 158, 13): 'CoffeeLake',
(6, 165, 2): 'CometLake',
(6, 165, 4): 'CometLake',
(6, 165, 5): 'CometLake-S',
(6, 166, 0): 'CometLake',
(6, 167, 1): 'RocketLake',
(6, 170, 4): 'MeteorLake',
(6, 181, 0): 'ArrowLake-U',
(6, 183, 1): 'RaptorLake-HX',
(6, 186, 2): 'RaptorLake',
(6, 186, 3): 'RaptorLake-U',
(6, 189, 1): 'LunarLake',
(6, 190, 0): 'AlderLake-N',
(6, 198, 2): 'ArrowLake-HX',
(6, 204, 2): 'PantherLake',
}
# MCHBAR belongs to the PCI host bridge, not to a CPUID signature. This is a
# deliberately narrow allowlist: an unlisted device remains MSR-only.
#
# TGL/ADL/RPL/Core Ultra datasheets document PACKAGE_RAPL_LIMIT_0_0_0_MCHBAR_PCU
# at MCHBAR + 0x59a0 (Intel docs 631122, 767625/767626, 764981/767624, 795258,
# 819323, 844345). Kaby and Coffee Lake do not publish the offset: those two
# rest on coreboot's MCH_PKG_POWER_LIMIT_LO (the MSR 0x610 mirror) and a live
# MMIO == MSR equality check on 0x3ec4. Groups follow Linux igen6/ie31200 EDAC.
MCHBAR_ADDRESS_MASKS_BY_PCI_DEVICE = {
# Kaby Lake-U/R and Coffee Lake-H target systems (T480/T480s/X1C6, P53).
0x5914: MCHBAR_ADDRESS_MASK_39_15,
0x3EC4: MCHBAR_ADDRESS_MASK_39_15,
# Tiger Lake (Linux igen6 tgl_cfg).
0x9A14: MCHBAR_ADDRESS_MASK_39_17,
# Alder Lake (Linux igen6 adl_cfg).
0x4601: MCHBAR_ADDRESS_MASK_42_17,
0x4602: MCHBAR_ADDRESS_MASK_42_17,
0x4621: MCHBAR_ADDRESS_MASK_42_17,
0x4641: MCHBAR_ADDRESS_MASK_42_17,
# Alder/Raptor Lake S/HX (Linux ie31200 rpl_s_cfg).
0x4660: MCHBAR_ADDRESS_MASK_42_17,
0x4668: MCHBAR_ADDRESS_MASK_42_17,
0x4648: MCHBAR_ADDRESS_MASK_42_17,
0xA703: MCHBAR_ADDRESS_MASK_42_17,
0x4640: MCHBAR_ADDRESS_MASK_42_17,
0x4630: MCHBAR_ADDRESS_MASK_42_17,
0xA700: MCHBAR_ADDRESS_MASK_42_17,
0xA740: MCHBAR_ADDRESS_MASK_42_17,
0xA704: MCHBAR_ADDRESS_MASK_42_17,
0xA702: MCHBAR_ADDRESS_MASK_42_17,
# Raptor Lake-P (Linux igen6 rpl_p_cfg).
0xA706: MCHBAR_ADDRESS_MASK_42_17,
0xA707: MCHBAR_ADDRESS_MASK_42_17,
0xA708: MCHBAR_ADDRESS_MASK_42_17,
0xA716: MCHBAR_ADDRESS_MASK_42_17,
0xA718: MCHBAR_ADDRESS_MASK_42_17,
# Meteor Lake and Arrow Lake-U/H (Linux igen6 mtl_ps/mtl_p_cfg).
0x7D21: MCHBAR_ADDRESS_MASK_42_17,
0x7D22: MCHBAR_ADDRESS_MASK_42_17,
0x7D23: MCHBAR_ADDRESS_MASK_42_17,
0x7D24: MCHBAR_ADDRESS_MASK_42_17,
0x7D01: MCHBAR_ADDRESS_MASK_42_17,
0x7D02: MCHBAR_ADDRESS_MASK_42_17,
0x7D14: MCHBAR_ADDRESS_MASK_42_17,
0x7D06: MCHBAR_ADDRESS_MASK_42_17,
0x7D20: MCHBAR_ADDRESS_MASK_42_17,
0x7D30: MCHBAR_ADDRESS_MASK_42_17,
}
TESTMSR = False
UNSUPPORTED_FEATURES = []
MONITOR_POWER_DOMAINS = {
'Package': 'MSR_INTEL_PKG_ENERGY_STATUS',
'Graphics': 'MSR_PP1_ENERGY_STATUS',
'DRAM': 'MSR_DRAM_ENERGY_STATUS',
}
class bcolors:
YELLOW = '\033[93m'
GREEN = '\033[92m'
RED = '\033[91m'
RESET = '\033[0m'
BOLD = '\033[1m'
OK = bcolors.GREEN + bcolors.BOLD + 'OK' + bcolors.RESET
ERR = bcolors.RED + bcolors.BOLD + 'ERR' + bcolors.RESET
LIM = bcolors.YELLOW + bcolors.BOLD + 'LIM' + bcolors.RESET
log_history = set()
ANSI_ESCAPE_RE = re.compile(r'\x1b\[[0-9;]*m')
def _format(prefix, msg):
if args.log:
tstamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')[:-3]
return f'{tstamp}: {prefix}{ANSI_ESCAPE_RE.sub("", msg)}'
return f'{prefix}{msg}'
def log(msg, oneshot=False, end='\n'):
outfile = args.log if args.log else sys.stdout
if not oneshot or msg.strip() not in log_history:
print(_format('', msg), file=outfile, end=end)
if oneshot:
log_history.add(msg.strip())
def fatal(msg, code=1, end='\n'):
outfile = args.log if args.log else sys.stderr
print(_format('[E] ', msg), file=outfile, end=end)
if current_thread() is not main_thread():
# sys.exit() would only kill the calling thread, leaving a zombie
# daemon that looks healthy to systemd but no longer touches MSRs
outfile.flush()
os._exit(code)
sys.exit(code)
def warning(msg, oneshot=True, end='\n'):
outfile = args.log if args.log else sys.stderr
if not oneshot or msg.strip() not in log_history:
print(_format('[W] ', msg), file=outfile, end=end)
if oneshot:
log_history.add(msg.strip())
def get_cpu_list():
"""Return sorted CPU indices that expose a /dev/cpu/N entry."""
try:
entries = os.listdir('/dev/cpu')
except FileNotFoundError:
return []
return sorted(int(x) for x in entries if x.isdigit())
def get_msr_list():
"""Return the per-CPU MSR device paths in CPU-index order."""
return [f'/dev/cpu/{cpu:d}/msr' for cpu in get_cpu_list()]
def _ensure_msr_module(cpu=None):
"""Return all MSR devices, or the MSR device for CPU N, loading the module if needed."""
if cpu is not None:
target = f'/dev/cpu/{cpu:d}/msr'
if not os.path.exists(target) and not os.path.exists('/sys/module/msr'):
try:
subprocess.check_call(('modprobe', 'msr'))
except subprocess.CalledProcessError:
fatal('Unable to load the msr module.')
if not os.path.exists(target):
fatal(f'CPU {cpu:d} has no MSR device under /dev/cpu; it may have gone offline.')
return target
msr_list = get_msr_list()
if not msr_list or not os.path.exists(msr_list[0]):
try:
subprocess.check_call(('modprobe', 'msr'))
except subprocess.CalledProcessError:
fatal('Unable to load the msr module.')
msr_list = get_msr_list()
if not msr_list or not os.path.exists(msr_list[0]):
fatal('No MSR devices found under /dev/cpu after loading the msr module.')
return msr_list
def writemsr(msr, val, cpu=None):
"""Write a 64-bit value to the named MSR on every online CPU or CPU N."""
if cpu is not None and cpu < 0:
fatal('Wrong writemsr cpu param')
try:
msr_list = [_ensure_msr_module(cpu)] if cpu is not None else _ensure_msr_module()
for addr in msr_list:
f = os.open(addr, os.O_WRONLY)
try:
os.lseek(f, MSR_DICT[msr], os.SEEK_SET)
os.write(f, struct.pack('Q', val))
finally:
os.close(f)
except (IOError, OSError) as e:
if TESTMSR:
raise e
if cpu is not None and e.errno == ENOENT:
fatal(f'CPU {cpu:d} went offline while writing MSR {msr} ({MSR_DICT[msr]:x}); aborting.')
if e.errno == EPERM or e.errno == EACCES:
fatal(
f'Unable to write to MSR {msr} ({MSR_DICT[msr]:x}). Check that the msr kernel module '
'is loaded with allow_writes=on and that kernel lockdown is disabled (many kernels '
'enable lockdown automatically when Secure Boot is on).'
)
elif e.errno == EIO:
fatal(f'Unable to write to MSR {msr} ({MSR_DICT[msr]:x}). Unknown error.')
else:
raise e
def readmsr(msr, from_bit=0, to_bit=63, cpu=None, flatten=False):
"""Read the named MSR and return the [from_bit, to_bit] field as
an unsigned integer. By default returns one value per CPU; with
cpu=N returns just CPU N, with flatten=True returns the shared value
(warning if CPUs disagree).
"""
if cpu is not None and cpu < 0:
fatal('Wrong readmsr cpu param')
if from_bit > to_bit:
fatal('Wrong readmsr bit params')
try:
msr_list = [_ensure_msr_module(cpu)] if cpu is not None else _ensure_msr_module()
output = []
for addr in msr_list:
f = os.open(addr, os.O_RDONLY)
try:
os.lseek(f, MSR_DICT[msr], os.SEEK_SET)
val = struct.unpack('Q', os.read(f, 8))[0]
finally:
os.close(f)
output.append(get_value_for_bits(val, from_bit, to_bit))
if flatten:
if len(set(output)) > 1:
warning(f'Found multiple values for {msr:s} ({MSR_DICT[msr]:x}). This should never happen.')
return output[0]
if cpu is not None:
return output[0]
return output
except (IOError, OSError) as e:
if TESTMSR:
raise e
if cpu is not None and e.errno == ENOENT:
fatal(f'CPU {cpu:d} went offline while reading MSR {msr} ({MSR_DICT[msr]:x}); aborting.')
if e.errno == EPERM or e.errno == EACCES:
fatal(
f'Unable to read from MSR {msr} ({MSR_DICT[msr]:x}). Check that the msr kernel module '
'is loaded and not restricted by kernel lockdown.'
)
elif e.errno == EIO:
fatal(f'Unable to read to MSR {msr} ({MSR_DICT[msr]:x}). Unknown error.')
else:
raise e
def get_value_for_bits(val, from_bit=0, to_bit=63):
"""Extract bits [from_bit, to_bit] (inclusive) from val."""
mask = sum(2**x for x in range(from_bit, to_bit + 1))
return (val & mask) >> from_bit
def set_msr_allow_writes():
"""Try to enable msr.allow_writes; tolerate kernels that don't expose it."""
log('[I] Trying to unlock MSR allow_writes.')
if not os.path.exists('/sys/module/msr'):
try:
subprocess.check_call(('modprobe', 'msr'))
except subprocess.CalledProcessError:
return
if os.path.exists('/sys/module/msr/parameters/allow_writes'):
try:
with open('/sys/module/msr/parameters/allow_writes', 'w') as f:
f.write('on')
except OSError:
warning('Unable to set MSR allow_writes to on. You might experience warnings in kernel logs.')
def get_dbus_fast():
"""Import dbus-fast lazily so tests and --help do not need a live DBus stack."""
from dbus_fast.aio import MessageBus
from dbus_fast.constants import BusType
return MessageBus, BusType
def unwrap_dbus_value(value):
return value.value if hasattr(value, 'value') else value
async def get_upower_on_battery_async():
MessageBus, BusType = get_dbus_fast()
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
try:
introspection = await bus.introspect(UPOWER_SERVICE, UPOWER_PATH)
upower = bus.get_proxy_object(UPOWER_SERVICE, UPOWER_PATH, introspection)
properties = upower.get_interface(DBUS_PROPERTIES_INTERFACE)
return bool(unwrap_dbus_value(await properties.call_get(UPOWER_SERVICE, 'OnBattery')))
finally:
bus.disconnect()
def get_upower_on_battery():
return asyncio.run(get_upower_on_battery_async())
def is_on_battery(config):
"""Return True if the system is on battery power.
Every adapter matched by Sysfs_Power_Path is checked and any one online
means AC; falls back to UPower over D-Bus on unreadable paths.
"""
paths = sorted(glob.glob(config.get('GENERAL', 'Sysfs_Power_Path', fallback=DEFAULT_SYSFS_POWER_PATH)))
values = []
errors = []
for path in paths:
try:
with open(path) as f:
value = int(f.read())
if value not in (0, 1):
raise ValueError(f'expected 0 or 1, got {value!r}')
values.append(value)
except (IOError, OSError, ValueError) as e:
errors.append(f'{path}: {e}')
if values and any(value == 1 for value in values):
if errors:
warning(f'Sysfs_Power_Path read failed for {len(errors)} path(s): {"; ".join(errors)}')
return False
if values and not errors:
return True
if errors:
warning(f'Sysfs_Power_Path read failed for {len(errors)} path(s): {"; ".join(errors)}. Trying upower method.')
else:
warning('No valid Sysfs_Power_Path found! Trying upower method')
try:
return get_upower_on_battery()
except Exception:
pass
warning('No valid power detection methods found. Assuming that the system is running on battery power.')
return True
def _current_config(config_or_state):
return config_or_state['config'] if isinstance(config_or_state, dict) else config_or_state
def config_is_enabled(config):
"""Return whether hardware changes are enabled in the loaded config."""
return config.getboolean('GENERAL', 'Enabled', fallback=False)
def handle_sleep_prepare(sleeping, config_or_state):
if not sleeping:
with config_lock:
config = _current_config(config_or_state)
if config_is_enabled(config):
undervolt(config)
set_icc_max(config)
def handle_ac_properties_changed(if_name, changed, invalidated):
if "OnBattery" in changed:
power['method'] = 'dbus'
power['source'] = 'BATTERY' if bool(unwrap_dbus_value(changed['OnBattery'])) else 'AC'
def should_listen_for_resume(config):
return config_is_enabled(config) and any(
config.getfloat(key, plane, fallback=0) != 0
for keys, planes in ((UNDERVOLT_KEYS, VOLTAGE_PLANES), (ICCMAX_KEYS, CURRENT_PLANES))
for key in keys
for plane in planes
)
async def setup_dbus_signal_handlers(config_or_state):
from dbus_fast import DBusError, ErrorType
config = _current_config(config_or_state)
MessageBus, BusType = get_dbus_fast()
bus = await MessageBus(bus_type=BusType.SYSTEM).connect()
context = {'bus': bus}
try:
upower_introspection = await bus.introspect(UPOWER_SERVICE, UPOWER_PATH)
upower = bus.get_proxy_object(UPOWER_SERVICE, UPOWER_PATH, upower_introspection)
upower_properties = upower.get_interface(DBUS_PROPERTIES_INTERFACE)
upower_properties.on_properties_changed(handle_ac_properties_changed)
context['upower'] = upower
context['upower_properties'] = upower_properties
resume_required = should_listen_for_resume(config)
if resume_required or (
config_is_enabled(config) and config.getboolean('GENERAL', 'Autoreload', fallback=False)
):
try:
login1_introspection = await bus.introspect(LOGIN1_SERVICE, LOGIN1_PATH)
except DBusError as e:
if e.type != ErrorType.SERVICE_UNKNOWN.value or resume_required:
raise
warning('login1 is unavailable; resume-time reapplication is disabled.')
return context
login1 = bus.get_proxy_object(LOGIN1_SERVICE, LOGIN1_PATH, login1_introspection)
login1_manager = login1.get_interface(LOGIN1_MANAGER_INTERFACE)
login1_manager.on_prepare_for_sleep(lambda sleeping: handle_sleep_prepare(sleeping, config_or_state))
context['login1'] = login1
context['login1_manager'] = login1_manager
return context
except Exception:
bus.disconnect()
raise
async def run_dbus_loop(config_or_state):
context = await setup_dbus_signal_handlers(config_or_state)
try:
await context['bus'].wait_for_disconnect()
finally:
context['bus'].disconnect()
def get_cpu_platform_info():
"""Decode MSR_PLATFORM_INFO into a dict of named feature bits."""
features_msr_value = readmsr('MSR_PLATFORM_INFO', cpu=0)
cpu_platform_info = {}
for key, value in platform_info_bits.items():
cpu_platform_info[key] = int(get_value_for_bits(features_msr_value, value[0], value[1]))
return cpu_platform_info
def get_reset_thermal_status():
"""Read IA32_THERM_STATUS for every CPU, then clear the sticky log bits."""
thermal_status_msr_value = readmsr('IA32_THERM_STATUS')
thermal_status = []
for msr_value in thermal_status_msr_value:
thermal_status_core = {}
for key, value in thermal_status_bits.items():
thermal_status_core[key] = int(get_value_for_bits(msr_value, value[0], value[1]))
thermal_status.append(thermal_status_core)
# reset log bits
writemsr('IA32_THERM_STATUS', 0)
return thermal_status
def get_time_unit():
"""Return the RAPL time unit in seconds (Intel SDM Vol. 4, MSR 0x606)."""
return 1.0 / 2 ** readmsr('MSR_RAPL_POWER_UNIT', 16, 19, cpu=0)
def get_power_unit():
"""Return the RAPL power unit in watts (Intel SDM Vol. 4, MSR 0x606)."""
return 1.0 / 2 ** readmsr('MSR_RAPL_POWER_UNIT', 0, 3, cpu=0)
def get_critical_temp():
"""Return the package critical temperature offset in degrees Celsius."""
return readmsr('MSR_TEMPERATURE_TARGET', 16, 23, cpu=0)
def get_cur_pkg_power_limits():
"""Return the current PL1/PL2 power and time-window fields from
MSR_PKG_POWER_LIMIT."""
value = readmsr('MSR_PKG_POWER_LIMIT', 0, 55, flatten=True)
return {
'PL1': get_value_for_bits(value, 0, 14),
'TW1': get_value_for_bits(value, 17, 23),
'PL2': get_value_for_bits(value, 32, 46),
'TW2': get_value_for_bits(value, 49, 55),
}
def calc_time_window_vars(t):
"""Encode a time-window duration (s) as the (Y, Z) pair used by
MSR_PKG_POWER_LIMIT."""
time_unit = get_time_unit()
for Y in range(2**5):
for Z in range(2**2):
if t <= (2**Y) * (1.0 + Z / 4.0) * time_unit:
return (Y, Z)
raise ValueError('Unable to find a good combination!')
def _encode_pkg_power_limit(pl1, tw1, pl2, tw2):
"""Encode MSR_PKG_POWER_LIMIT fields without allowing adjacent-bit spill."""
fields = (
('PL1', pl1, PKG_POWER_LIMIT_POWER_MASK),
('TW1', tw1, PKG_POWER_LIMIT_TIME_WINDOW_MASK),
('PL2', pl2, PKG_POWER_LIMIT_POWER_MASK),
('TW2', tw2, PKG_POWER_LIMIT_TIME_WINDOW_MASK),
)
encoded = {}
for name, value, mask in fields:
if not isinstance(value, int) or not 0 <= value <= mask:
raise ValueError(f'{name:s} value {value!r} does not fit its {mask.bit_length():d}-bit field.')
if name in ('PL1', 'PL2') and value == 0:
raise ValueError(f'{name:s} must be at least one power-unit tick, got {value!r}.')
encoded[name] = value
return (
encoded['PL1']
| (1 << 15)
| (1 << 16)
| (encoded['TW1'] << 17)
| (encoded['PL2'] << 32)
| (1 << 47)
| (encoded['TW2'] << 49)
)
def _undervolt_offset_to_ticks(offset):
"""Convert an undervolt in mV to the signed 11-bit mailbox field."""
try:
offset = float(offset)
except (TypeError, ValueError) as e:
raise ValueError(f'Undervolt offset must be a number, got {offset!r}.') from e
minimum_mv = UNDERVOLT_MIN_TICKS / UNDERVOLT_TICKS_PER_MV
if not math.isfinite(offset) or not minimum_mv <= offset <= 0:
raise ValueError(f'Undervolt offset must be between {minimum_mv:g} and 0 mV, got {offset!r}.')
ticks = int(round(offset * UNDERVOLT_TICKS_PER_MV))
if not UNDERVOLT_MIN_TICKS <= ticks <= UNDERVOLT_MAX_TICKS:
raise ValueError(f'Undervolt offset {offset!r} mV does not fit the signed 11-bit mailbox field.')
return ticks
def calc_undervolt_msr(plane, offset):
"""Return the value to be written in the MSR 150h for setting the given
offset voltage (in mV) to the given voltage plane.
"""
if plane not in VOLTAGE_PLANES:
raise ValueError(f'Unknown voltage plane: {plane!r}.')
ticks = _undervolt_offset_to_ticks(offset)
encoded_offset = (ticks & 0x7FF) << 21
return 0x8000001100000000 | (VOLTAGE_PLANES[plane] << 40) | encoded_offset
def calc_undervolt_mv(msr_value):
"""Return the offset voltage (in mV) from the given raw MSR 150h value."""
offset = (msr_value & 0xFFE00000) >> 21
# 11-bit two's complement: values >= 0x400 are negative
offset = offset if offset < 0x400 else -(0x800 - offset)
return int(round(offset / UNDERVOLT_TICKS_PER_MV))
def get_undervolt(plane=None, convert=False):
"""Read the current undervolt offset from one or all voltage planes."""
if 'UNDERVOLT' in UNSUPPORTED_FEATURES:
return 0
planes = [plane] if plane in VOLTAGE_PLANES else VOLTAGE_PLANES
out = {}
for plane in planes:
writemsr('MSR_OC_MAILBOX', 0x8000001000000000 | (VOLTAGE_PLANES[plane] << 40))
read_value = readmsr('MSR_OC_MAILBOX', flatten=True) & 0xFFFFFFFF
out[plane] = calc_undervolt_mv(read_value) if convert else read_value
return out
def undervolt(config, source=None):
"""Apply the undervolt offsets from the config to all voltage planes."""
source = source or power['source']
section = f'UNDERVOLT.{source}'
if (section not in config and 'UNDERVOLT' not in config) or 'UNDERVOLT' in UNSUPPORTED_FEATURES:
return
for plane in VOLTAGE_PLANES:
write_offset_mv = config.getfloat(section, plane, fallback=config.getfloat('UNDERVOLT', plane, fallback=0.0))
write_value = calc_undervolt_msr(plane, write_offset_mv)
writemsr('MSR_OC_MAILBOX', write_value)
if args.debug:
write_value &= 0xFFFFFFFF
read_value = get_undervolt(plane)[plane]
read_offset_mv = calc_undervolt_mv(read_value)
match = OK if write_value == read_value else ERR
log(
f'[D] Undervolt plane {plane:s} - write {write_offset_mv:.0f} mV ({write_value:#x}) - read {read_offset_mv:.0f} mV ({read_value:#x}) - match {match}'
)
def _icc_max_to_field(current):
"""Convert an IccMax in A to the unsigned 10-bit quarter-ampere field."""
try:
current = float(current)
except (TypeError, ValueError) as e:
raise ValueError(f'IccMax must be a number, got {current!r}.') from e
maximum_a = ICCMAX_MAX_FIELD / ICCMAX_STEPS_PER_A
if not math.isfinite(current) or not 0 < current <= maximum_a:
raise ValueError(f'IccMax must be between 0 (exclusive) and {maximum_a:g} A, got {current!r}.')
# floor: quantisation must never enforce a ceiling above the configured one
field = int(current * ICCMAX_STEPS_PER_A)
if not 1 <= field <= ICCMAX_MAX_FIELD:
raise ValueError(f'IccMax {current!r} A quantises outside the unsigned 10-bit field.')
return field
def calc_icc_max_msr(plane, current):
"""Return the value to be written in the MSR 150h for setting the given
IccMax (in A) to the given current plane.
"""
if plane not in CURRENT_PLANES:
raise ValueError(f'Unknown current plane: {plane!r}.')
return 0x8000001700000000 | (CURRENT_PLANES[plane] << 40) | _icc_max_to_field(current)
def calc_icc_max_amp(msr_value):
"""Return the max current (in A) from the given raw MSR 150h value."""
return (msr_value & 0x3FF) / 4.0
def get_configured_power_profiles(config):
"""Return the AC/BATTERY power profiles present in the config file."""
return [profile for profile in POWER_PROFILES if profile in config]
def get_update_rate(config, power_source):
"""Return the update rate for power_source, or any configured profile."""
update_rate = config.getfloat(power_source, 'Update_Rate_s', fallback=None)
if update_rate is not None:
return update_rate
for fallback_power_source in get_configured_power_profiles(config):
update_rate = config.getfloat(fallback_power_source, 'Update_Rate_s', fallback=None)
if update_rate is not None:
return update_rate
fatal('At least one configured power profile must define "Update_Rate_s".')
def get_icc_max(plane=None, convert=False):
"""Read the IccMax setting from one or all current planes."""
planes = [plane] if plane in CURRENT_PLANES else CURRENT_PLANES
out = {}
for plane in planes:
writemsr('MSR_OC_MAILBOX', 0x8000001600000000 | (CURRENT_PLANES[plane] << 40))
read_value = readmsr('MSR_OC_MAILBOX', flatten=True) & 0x3FF
out[plane] = calc_icc_max_amp(read_value) if convert else read_value
return out
def set_icc_max(config, source=None):
"""Apply the IccMax limits from the config to all current planes."""
if 'ICCMAX' in UNSUPPORTED_FEATURES:
return
source = source or power['source']
section = f'ICCMAX.{source}'
for plane in CURRENT_PLANES:
try:
write_current_amp = config.getfloat(
section, plane, fallback=config.getfloat('ICCMAX', plane, fallback=-1.0)
)
if write_current_amp <= 0 and any(
config.getfloat(key, plane, fallback=-1.0) > 0 for key in ICCMAX_KEYS
):
warning(f'IccMax {plane:s} is not configured for the {source:s} profile: leaving it untouched.')
if write_current_amp > 0:
write_value = calc_icc_max_msr(plane, write_current_amp)
writemsr('MSR_OC_MAILBOX', write_value)
if args.debug:
write_value &= 0x3FF
read_value = get_icc_max(plane)[plane]
read_current_A = calc_icc_max_amp(read_value)
match = OK if write_value == read_value else ERR
log(
f'[D] IccMax plane {plane:s} - write {calc_icc_max_amp(write_value):.2f} A ({write_value:#x}) - read {read_current_A:.2f} A ({read_value:#x}) - match {match}'
)
except (configparser.NoSectionError, configparser.NoOptionError):
pass
def _remove_config_option(config, section, option):
"""Drop option from the layer holding it and return that layer's name."""
if config.remove_option(section, option):
return section
config.remove_option(config.default_section, option)
return config.default_section
def load_config():
"""Parse the config file, validating and clamping out-of-range values."""
config = configparser.ConfigParser()
config.read(args.config)
power_profiles = get_configured_power_profiles(config)
if not power_profiles:
fatal('At least one power profile ([AC] or [BATTERY]) is required.')
boolean_options = [('GENERAL', 'Enabled'), ('GENERAL', 'Autoreload'), ('AC', 'HWP_Mode')]
boolean_options.extend((profile, 'Disable_BDPROCHOT') for profile in power_profiles)
for section, option in boolean_options:
try:
config.getboolean(section, option, fallback=None)
except (ValueError, configparser.InterpolationError):
fatal(f'The "{option:s}" parameter in [{section:s}] must be a boolean.')
# config values sanity check
for power_source in power_profiles:
for option in ('Update_Rate_s', 'PL1_Tdp_W', 'PL1_Duration_s', 'PL2_Tdp_W', 'PL2_Duration_S'):
value = None
# a malformed profile value may mask a second malformed value inherited from [DEFAULT]
for _ in range(2):
try:
value = config.getfloat(power_source, option, fallback=None)
if value is None or math.isfinite(value):
break
raise ValueError(value)
except (ValueError, configparser.InterpolationError):
value = None
section = _remove_config_option(config, power_source, option)
if option == 'Update_Rate_s':
fatal(f'The mandatory "Update_Rate_s" parameter in [{section:s}] must be a finite number.')
warning(f'Invalid "{option:s}" value in [{section:s}]: ignoring it.', oneshot=False)
if value is not None:
config.set(power_source, option, str(max(0.001, value)))
elif option == 'Update_Rate_s':
fatal(f'The mandatory "Update_Rate_s" parameter is missing in the [{power_source:s}] profile.')
trip_temp = None
for _ in range(2):
try:
trip_temp = config.getfloat(power_source, 'Trip_Temp_C', fallback=None)
if trip_temp is None or math.isfinite(trip_temp):
break
raise ValueError(trip_temp)
except (ValueError, configparser.InterpolationError):
trip_temp = None
section = _remove_config_option(config, power_source, 'Trip_Temp_C')
warning(f'Invalid "Trip_Temp_C" value in [{section:s}]: ignoring it.', oneshot=False)
if trip_temp is not None:
valid_trip_temp = min(TRIP_TEMP_RANGE[1], max(TRIP_TEMP_RANGE[0], trip_temp))
if trip_temp != valid_trip_temp:
config.set(power_source, 'Trip_Temp_C', str(valid_trip_temp))
log(
f'[!] Overriding invalid "Trip_Temp_C" value in "{power_source:s}": {trip_temp:.1f} -> {valid_trip_temp:.1f}'
)
# handle the case where only one of UNDERVOLT.AC, UNDERVOLT.BATTERY keys exists
# by forcing the other key to all zeros (ie. no undervolt); synthesizing it
# before the vetting below puts its [DEFAULT]-inherited planes through it too
if any(key in config for key in UNDERVOLT_KEYS[1:]):
for key in UNDERVOLT_KEYS[1:]:
if key not in config:
config.add_section(key)
# the mailbox field is signed 11-bit: reject anything below -1000 mV instead of wrapping it positive
for key in UNDERVOLT_KEYS:
for plane in VOLTAGE_PLANES:
if key in config:
for _ in range(2):
try:
value = config.getfloat(key, plane, fallback=0.0)
if not math.isfinite(value):
raise ValueError(f'Undervolt offset must be finite, got {value!r}.')
if value > 0:
config.set(key, plane, '0')
log(
f'[!] Overriding invalid "{key:s}" value in "{plane:s}" voltage plane: {value:.0f} -> 0'
)
else:
_undervolt_offset_to_ticks(value)
break
except (ValueError, configparser.InterpolationError) as e:
section = key if config.remove_option(key, plane) else config.default_section
warning(f'Invalid value for {plane:s} in [{section:s}]: {e}', oneshot=False)
if section == config.default_section:
# the plane names are shared with ICCMAX: shadow the inherited value, never touch [DEFAULT]
config.set(key, plane, '0')
break
for key in UNDERVOLT_KEYS[1:]:
if key in config:
for plane in VOLTAGE_PLANES:
config.set(key, plane, str(config.getfloat(key, plane, fallback=0.0)))
# Check for CORE/CACHE values mismatch
for key in UNDERVOLT_KEYS:
if key in config:
if config.getfloat(key, 'CORE', fallback=0) != config.getfloat(key, 'CACHE', fallback=0):
warning('On Skylake and newer CPUs CORE and CACHE values should match!')
break
iccmax_enabled = False
# check for invalid values (ie. <= 0 or > 0x3FF) in the IccMax settings
for key in ICCMAX_KEYS:
if key not in config:
continue
for option in config[key]:
if option in config.defaults():
continue
if option.upper() not in CURRENT_PLANES:
warning(f'Unknown IccMax plane "{option:s}" in [{key:s}]: ignoring it.', oneshot=False)
for plane in CURRENT_PLANES:
if key in config:
for _ in range(2):
try:
value = config.getfloat(key, plane)
_icc_max_to_field(value)
iccmax_enabled = True
break
except (ValueError, configparser.InterpolationError) as e:
section = key if config.remove_option(key, plane) else config.default_section
warning(f'Invalid value for {plane:s} in [{section:s}]: {e}', oneshot=False)
if section == config.default_section:
# the plane names are shared with UNDERVOLT: shadow the inherited value, never touch [DEFAULT]
config.set(key, plane, '0')
break
except configparser.NoOptionError:
break
if iccmax_enabled:
warning('Warning! Raising IccMax above design limits can damage your system!')
return config
def calc_reg_values(platform_info, config):
"""Compute the MSR values to apply for each power source from the config."""
regs = defaultdict(dict)
for power_source in get_configured_power_profiles(config):
if platform_info['feature_programmable_temperature_target'] != 1:
warning("Setting temperature target is not supported by this CPU")
else:
critical_temp = get_critical_temp()
# update the allowed temp range to keep at least 3 'C from the CPU critical temperature
global TRIP_TEMP_RANGE
TRIP_TEMP_RANGE[1] = min(TRIP_TEMP_RANGE[1], critical_temp - 3)