basic-benchmark: portable Fedora benchmark kit
Headless + headed suites with power/thermal logging and efficiency reporting. Includes PassMark, llama.cpp, app workloads (LibreOffice/GEGL/Inkscape/GIMP), a GOVERNOR selector, compare-report.py, and captured results.
This commit is contained in:
commit
0cd63d0233
533 changed files with 43679 additions and 0 deletions
211
powerlog.py
Executable file
211
powerlog.py
Executable file
|
|
@ -0,0 +1,211 @@
|
|||
#!/usr/bin/env python3
|
||||
"""basic-benchmark: background power/thermal sampler.
|
||||
|
||||
RAPL energy counters are root-only, so run with sudo:
|
||||
|
||||
sudo python3 powerlog.py --tag before --interval 1
|
||||
|
||||
Writes results/power-<tag>.csv at --interval seconds until SIGINT/SIGTERM.
|
||||
Columns: epoch,iso,pkg_w,core_w,cpu_temp_c,cpu_mhz,gpu_w,gpu_temp_c,gpu_busy,fan1,fan2
|
||||
"""
|
||||
import argparse
|
||||
import csv
|
||||
import glob
|
||||
import os
|
||||
import signal
|
||||
import sys
|
||||
import time
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
def rd(path):
|
||||
try:
|
||||
with open(path) as fh:
|
||||
return fh.read().strip()
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
|
||||
def rdi(path):
|
||||
try:
|
||||
return int(rd(path))
|
||||
except (TypeError, ValueError):
|
||||
return None
|
||||
|
||||
|
||||
def rapl(name):
|
||||
for d in sorted(glob.glob("/sys/class/powercap/intel-rapl:*/")):
|
||||
if rd(os.path.join(d, "name")) == name:
|
||||
energy = os.path.join(d, "energy_uj")
|
||||
if os.path.exists(energy):
|
||||
return energy, rdi(os.path.join(d, "max_energy_range_uj"))
|
||||
return None, None
|
||||
|
||||
|
||||
def hwmon(name):
|
||||
for h in sorted(glob.glob("/sys/class/hwmon/hwmon*")):
|
||||
if rd(os.path.join(h, "name")) == name:
|
||||
return h
|
||||
return None
|
||||
|
||||
|
||||
def first(paths):
|
||||
for p in paths:
|
||||
if p and os.path.exists(p):
|
||||
return p
|
||||
return None
|
||||
|
||||
|
||||
def amdgpu_hwmons():
|
||||
out = []
|
||||
for h in sorted(glob.glob("/sys/class/hwmon/hwmon*")):
|
||||
if rd(os.path.join(h, "name")) == "amdgpu":
|
||||
out.append(h)
|
||||
return out
|
||||
|
||||
|
||||
def pick_gpu_hwmon():
|
||||
"""Prefer the discrete GPU.
|
||||
|
||||
On hybrid AMD systems both the iGPU and the dGPU expose a hwmon named
|
||||
"amdgpu"; the dGPU is the one that reports power1_average. Fall back to the
|
||||
first amdgpu hwmon (and to GPU_DEVPATH, if set) so this stays stable.
|
||||
"""
|
||||
hs = amdgpu_hwmons()
|
||||
want = os.environ.get("GPU_DEVPATH")
|
||||
if want:
|
||||
for h in hs:
|
||||
if os.path.realpath(os.path.join(h, "device")) == os.path.realpath(want):
|
||||
return h
|
||||
for h in hs:
|
||||
if os.path.exists(os.path.join(h, "power1_average")):
|
||||
return h
|
||||
return hs[0] if hs else None
|
||||
|
||||
|
||||
def card_for_device(devpath):
|
||||
"""Match a hwmon device to its /sys/class/drm/cardN node."""
|
||||
if not devpath:
|
||||
return None
|
||||
real = os.path.realpath(devpath)
|
||||
for c in sorted(glob.glob("/sys/class/drm/card[0-9]*")):
|
||||
name = os.path.basename(c)
|
||||
if not (name.startswith("card") and name[4:].isdigit()):
|
||||
continue
|
||||
dev = os.path.join(c, "device")
|
||||
if os.path.exists(dev) and os.path.realpath(dev) == real:
|
||||
return c
|
||||
return None
|
||||
|
||||
|
||||
def cpu_temp():
|
||||
for name in ("k10temp", "coretemp", "zenpower"):
|
||||
h = hwmon(name)
|
||||
if not h:
|
||||
continue
|
||||
hit = first(sorted(glob.glob(os.path.join(h, "temp*_input"))))
|
||||
if hit:
|
||||
return hit
|
||||
return None
|
||||
|
||||
|
||||
def avg_mhz():
|
||||
freq = [rdi(f) for f in glob.glob("/sys/devices/system/cpu/cpu*/cpufreq/scaling_cur_freq")]
|
||||
freq = [f for f in freq if f]
|
||||
return int(sum(freq) / len(freq) / 1000) if freq else None
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--tag", required=True)
|
||||
ap.add_argument("--interval", type=float, default=1.0)
|
||||
ap.add_argument("--out", default=None)
|
||||
ap.add_argument("--count", type=int, default=0, help="0 = run until signalled")
|
||||
args = ap.parse_args()
|
||||
|
||||
here = os.path.dirname(os.path.abspath(__file__))
|
||||
out = args.out or os.path.join(here, "results", f"power-{args.tag}.csv")
|
||||
os.makedirs(os.path.dirname(out), exist_ok=True)
|
||||
|
||||
pkg_e, pkg_max = rapl("package-0")
|
||||
core_e, core_max = rapl("core")
|
||||
cpu_t = cpu_temp()
|
||||
gpu_h = pick_gpu_hwmon()
|
||||
gpu_p = os.path.join(gpu_h, "power1_average") if gpu_h else None
|
||||
gpu_t = os.path.join(gpu_h, "temp1_input") if gpu_h else None
|
||||
fan_h = hwmon("nct6798") or hwmon("nct6775")
|
||||
fan1 = os.path.join(fan_h, "fan1_input") if fan_h else None
|
||||
fan2 = os.path.join(fan_h, "fan2_input") if fan_h else None
|
||||
gpu_busy = None
|
||||
gpu_card = card_for_device(os.path.join(gpu_h, "device")) if gpu_h else None
|
||||
if gpu_card:
|
||||
gpu_busy = first([os.path.join(gpu_card, "device", "gpu_busy_percent")])
|
||||
|
||||
if not pkg_e and not core_e:
|
||||
sys.stderr.write("powerlog: no RAPL counters found; run as root\n")
|
||||
|
||||
state = {"stop": False}
|
||||
|
||||
def on_signal(*_):
|
||||
state["stop"] = True
|
||||
|
||||
signal.signal(signal.SIGINT, on_signal)
|
||||
signal.signal(signal.SIGTERM, on_signal)
|
||||
|
||||
prev = {"pkg": None, "core": None}
|
||||
last = time.time()
|
||||
rows = 0
|
||||
|
||||
with open(out, "w", newline="") as fh:
|
||||
w = csv.writer(fh)
|
||||
w.writerow(["epoch", "iso", "pkg_w", "core_w", "cpu_temp_c", "cpu_mhz",
|
||||
"gpu_w", "gpu_temp_c", "gpu_busy", "fan1", "fan2"])
|
||||
fh.flush()
|
||||
|
||||
def watts(key, path, mx):
|
||||
cur = rdi(path) if path else None
|
||||
if cur is None:
|
||||
return None
|
||||
old = prev[key]
|
||||
prev[key] = cur
|
||||
if old is None:
|
||||
return None
|
||||
delta = cur - old
|
||||
if delta < 0 and mx:
|
||||
delta += mx
|
||||
return round(delta / 1e6 / max(now - last, 1e-3), 2)
|
||||
|
||||
while not state["stop"]:
|
||||
now = time.time()
|
||||
pkg_w = watts("pkg", pkg_e, pkg_max)
|
||||
core_w = watts("core", core_e, core_max)
|
||||
gpu_w = None
|
||||
gw = rdi(gpu_p) if gpu_p else None
|
||||
if gw is not None:
|
||||
gpu_w = round(gw / 1e6, 2)
|
||||
temp = rdi(cpu_t) if cpu_t else None
|
||||
gt = rdi(gpu_t) if gpu_t else None
|
||||
w.writerow([
|
||||
int(now),
|
||||
datetime.fromtimestamp(now).isoformat(timespec="seconds"),
|
||||
pkg_w, core_w,
|
||||
round(temp / 1000, 1) if temp is not None else None,
|
||||
avg_mhz(),
|
||||
gpu_w,
|
||||
round(gt / 1000, 1) if gt is not None else None,
|
||||
rdi(gpu_busy) if gpu_busy else None,
|
||||
rdi(fan1) if fan1 else None,
|
||||
rdi(fan2) if fan2 else None,
|
||||
])
|
||||
last = now
|
||||
rows += 1
|
||||
fh.flush()
|
||||
if args.count and rows >= args.count:
|
||||
break
|
||||
time.sleep(max(0.05, args.interval - (time.time() - now)))
|
||||
|
||||
sys.stderr.write(f"powerlog: wrote {rows} samples to {out}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in a new issue