initial commit

This commit is contained in:
2026-04-27 11:35:05 +02:00
parent b8e85624b9
commit feda943270
19 changed files with 2381 additions and 2 deletions

209
ryzenPower.py Normal file
View File

@@ -0,0 +1,209 @@
#!/usr/bin/env python3
# ryzen-power: measure AMD Ryzen CPU power consumption.
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
#
# This program is a Python port of rapl-read-ryzen
# https://github.com/djselbeck/rapl-read-ryzen
import logging
import os.path
import argparse
from itertools import count
from struct import unpack
from time import sleep
from warnings import warn
from datetime import datetime
logger = logging.getLogger("ryzen-master")
class RyzenPower:
AMD_MSR_PWR_UNIT_OFFSET = 0xC0010299
AMD_MSR_CORE_ENERGY_OFFSET = 0xC001029A
AMD_MSR_PACKAGE_ENERGY_OFFSET = 0xC001029B
AMD_TIME_UNIT_MASK = 0xF0000
AMD_ENERGY_UNIT_MASK = 0x1F00
AMD_POWER_UNIT_MASK = 0xF
def __init__(self, duration=1.0):
self._energy_unit = self._get_energy_units()
self._is_smt = self._detect_smt()
self._package_topology = self._detect_physical_package_topology()
self._duration = duration
self._cores = list(self._package_topology.keys())
if self._is_smt:
self._cores = [c for c in self._cores if c % 2 == 0]
self._cores = sorted(self._cores)
self._msr_fd_cache = {}
self.timestamp = 0
self.package_energy = -1
self.core_energy = -1
@staticmethod
def _read(filename):
with open(filename, "r") as f:
return f.read()
def _detect_smt(self):
try:
smt_status = self._read("/sys/devices/system/cpu/smt/control").strip()
logger.debug("CPU smt status is {}".format(smt_status))
return smt_status == "on"
except FileNotFoundError:
warn("unable to detect CPU SMT status, assume SMT is on")
return True
@staticmethod
def _detect_physical_package_topology():
cpu_package_mapping = {}
for cpu_id in count():
filename = "/sys/devices/system/cpu/cpu{}/topology/physical_package_id".format(cpu_id)
if os.path.isfile(filename):
with open(filename, "r") as f:
package_id = int(f.read())
logger.debug("detected cpu {} in socket {}".format(cpu_id, package_id))
cpu_package_mapping[cpu_id] = package_id
else:
return cpu_package_mapping
def _read_msr(self, cpu_id, offset):
msr_file = "/dev/cpu/{}/msr".format(cpu_id)
try:
with open(msr_file, "rb", buffering=8192) as f:
f.seek(offset)
# MSR value is always 64 bits
# https://manpages.debian.org/buster/manpages/msr.4.en.html
return self._decode_int64(f.read(8))
except PermissionError:
raise PermissionError("root privilege is required to read model-specific registers")
except FileNotFoundError:
raise FileNotFoundError("msr driver is not loaded, try \"sudo modprobe msr\" to load msr module")
@staticmethod
def _decode_int64(buffer):
return unpack("q", buffer)[0]
def _read_all_units(self):
return self._read_msr(0, self.AMD_MSR_PWR_UNIT_OFFSET)
def _get_energy_units(self):
energy_unit = (self._read_all_units() & self.AMD_ENERGY_UNIT_MASK) >> 8
logger.debug("CPU energy unit is 1/2^{}".format(energy_unit))
energy_unit = 0.5 ** energy_unit
return energy_unit
def _read_package_energy(self, cpu_id):
energy = self._read_msr(cpu_id, self.AMD_MSR_PACKAGE_ENERGY_OFFSET)
logger.debug("CPU {} current package energy {} J".format(cpu_id, energy, self._energy_unit))
return energy
def _read_core_energy(self, cpu_id):
energy = self._read_msr(cpu_id, self.AMD_MSR_CORE_ENERGY_OFFSET)
logger.debug("CPU {} current core energy {} * {} J".format(cpu_id, energy, self._energy_unit))
return energy
def _calc_power(self, before, after):
return (after - before) * self._energy_unit / self._duration
def _calc_power_wtime(self, before, after, duration):
return (after - before) * self._energy_unit / duration
def measure(self):
package_energy_before = {c: self._read_package_energy(c) for c in self._cores}
core_energy_before = {c: self._read_core_energy(c) for c in self._cores}
logger.debug("sleep for {} seconds".format(self._duration))
sleep(self._duration)
package_energy_after = {c: self._read_package_energy(c) for c in self._cores}
core_energy_after = {c: self._read_core_energy(c) for c in self._cores}
package_power = {c: self._calc_power(package_energy_before[c], package_energy_after[c]) for c in self._cores}
core_power = {c: self._calc_power(core_energy_before[c], core_energy_after[c]) for c in self._cores}
return package_power , core_power
#print(self._format_result(package_power, core_power))
def measure_nonblocking(self):
timestamp = datetime.now()
package_energy = {c: self._read_package_energy(c) for c in self._cores}
core_energy = {c: self._read_core_energy(c) for c in self._cores}
if self.package_energy != -1:
time_delta = (timestamp - self.timestamp).total_seconds()
package_power = {c: self._calc_power_wtime(self.package_energy[c], package_energy[c], time_delta) for c in self._cores}
core_power = {c: self._calc_power_wtime(self.core_energy[c], core_energy[c], time_delta) for c in self._cores}
else:
for c in self._cores:
package_power = {c: 0 for c in self._cores}
core_power = {c: 0 for c in self._cores}
self.package_energy = package_energy
self.core_energy = core_energy
self.timestamp = timestamp
avg_pp = 0
for c in self._cores:
avg_pp += package_power[0]
package_power = avg_pp / len(self._cores)
return package_power, core_power
@staticmethod
def _format_table(table, widths, units):
buffer = []
for row in table:
row_buffer = []
for col, width, unit in zip(row, widths, units):
if isinstance(col, float):
row_buffer.append("{:.2f}{}".format(col, unit).ljust(width))
else:
row_buffer.append(str(col).ljust(width))
buffer.append("".join(row_buffer))
return "\n".join(buffer)
def _format_result(self, package_power, core_power):
sockets = sorted(set(self._package_topology.values()))
table = [["", "Cores Power", "Package Power"]]
for socket in sockets:
socket_total_cores_power = 0
socket_package_power = 0
socket_power_entry = ["SOCKET {: 2}:".format(socket)]
table.append(socket_power_entry)
for core in self._cores:
if self._package_topology[core] == socket:
socket_total_cores_power += core_power[core]
socket_package_power = package_power[core]
table.append([
" CORE {: 2}:".format(core // 2 if self._is_smt else core),
core_power[core],
""
])
socket_power_entry.append(socket_total_cores_power)
socket_power_entry.append(socket_package_power)
return self._format_table(table, (16, 16, 16), ("", "W", "W"))
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Measure power consumption for AMD Ryzen CPU')
parser.add_argument("--debug", action='store_true', help="show debug messages")
parser.add_argument("-d", "--duration", type=float, default=0.5,
help="the duration of measurement in seconds, default is 0.5 second")
args = parser.parse_args()
if args.debug:
stream_handler = logging.StreamHandler()
logger.addHandler(stream_handler)
logger.setLevel(logging.DEBUG)
package, core = RyzenPower(args.duration).measure()
print(RyzenPower(args.duration)._format_result(package, core))