Commit b48ee82a authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: perfd - 'measure' now PINS the CPU frequency (min=max) + per-hold overrides

Andrey's observation on the first perfd-held run: CPU boosting to 4.9 GHz,
100 C. Calibration (openssl all-core): the i9-11900K spikes 4.8 GHz @ 85 C,
hits 100 C within seconds and throttles to a wobbling 3.2-3.5 GHz - the
'performance' boost bias is itself a nondeterminism source AND rides Tjmax.
- measure profile: CPU pinned via sysfs scaling_min=max (default 3.2 GHz =
  the measured all-core throttle equilibrium, so the pin holds even in the
  worst phase and runs cooler; PERFD_CPU_LOCK_KHZ) + the existing GPU lock.
  PPD 'performance' hold now only backs the max profile.
- HOLD accepts bounds-checked per-hold overrides (HOLD measure cpu=KHZ
  gpu=MHZ; perfctl passes them through) so lock-value calibration needs no
  daemon reconfiguration/sudo.
- Holds now per-connection with values from the newest holder of the
  winning profile; STATUS extended.
User-mode tested: override parsing/apply, bounds rejection, permission
errors surfaced, auto-release. Root paths activate on reinstall.
Co-authored-by: 's avatarClaude Opus 4.8 <claude-opus-4-8@anthropic.com>
Co-authored-by: 's avatarClaude Fable 5 <claude-fable-5@anthropic.com>
parent 74d8341c
......@@ -24,11 +24,17 @@ install command above when absent (and continues normally).
perfctl hold max # production profile until Ctrl-C
Profiles:
- `measure` — CPU `performance` + GPU persistence + clocks **locked**
(default 2550 MHz, `PERFD_GPU_LOCK_MHZ` in the unit): run-to-run timing
reproducibility (±1% instead of ±10% with free-running boost) for per-item
performance evaluation.
- `max` — CPU `performance` + GPU persistence, clocks unlocked: throughput.
- `measure` — CPU frequency **pinned** (`scaling_min=max`, default 3.2 GHz,
`PERFD_CPU_LOCK_KHZ`) + GPU persistence + GPU clocks **locked** (default
2550 MHz, `PERFD_GPU_LOCK_MHZ`): run-to-run timing reproducibility (±1%
instead of ±10% with free-running boost) for per-item performance
evaluation. Pinned, not `performance`: the i9-11900K at free boost hits
4.9 GHz → 100 °C → throttle wobble within seconds of an all-core phase
(measured 07/17/2026) — boost bias is itself a nondeterminism source.
Per-hold overrides: `perfctl hold measure cpu=3400000 gpu=2400 -- <cmd>`
(bounds-checked; for calibration without reconfiguration).
- `max` — CPU `performance` profile (free boost) + GPU persistence, clocks
unlocked: throughput.
A hold is tied to the client's connection — process exit or crash releases
it automatically (refcounted across concurrent holders; `measure` outranks
......
......@@ -11,6 +11,7 @@ ExecStart=/usr/bin/python3 /usr/local/lib/elphel-perfd/elphel_perfd.py
# Config overrides (defaults live in the script):
#Environment=PERFD_PORT=48890
#Environment=PERFD_GPU_LOCK_MHZ=2550
#Environment=PERFD_CPU_LOCK_KHZ=3200000
#Environment=PERFD_GPU_INDEX=0
Restart=on-failure
RestartSec=2
......
......@@ -13,49 +13,60 @@ programs use it through the same socket/CLI). Implemented by Claude.
Java 8/11 clients need no extra libraries):
PING -> OK pong
STATUS -> OK {json}
HOLD measure -> OK held measure ... (until connection closes)
HOLD measure [cpu=KHZ] [gpu=MHZ] -> OK held measure ...
HOLD max -> OK held max ...
RELEASE -> OK released
A hold is TIED TO THE CONNECTION: client exit/crash closes the socket and
the hold is released automatically (crash-safe, like a media player's
sleep inhibitor). Multiple concurrent holders are refcounted; the
strongest profile wins (measure > max).
strongest profile wins (measure > max); lock values come from the newest
holder of the winning profile. cpu=/gpu= overrides are bounds-checked and
exist so lock-value calibration needs no daemon reconfiguration.
- Profiles:
measure : CPU 'performance' (power-profiles-daemon hold) +
GPU persistence ON + clocks LOCKED at GPU_LOCK_MHZ
-> run-to-run deterministic timing (per-item A/B evaluation)
max : CPU 'performance' + GPU persistence ON, clocks unlocked
-> production throughput
measure : CPU frequency PINNED (intel_pstate scaling_min=max=KHZ) +
GPU persistence ON + GPU clocks LOCKED at MHZ
-> run-to-run deterministic timing for per-item A/B
evaluation. PINNED, not 'performance': the i9-11900K at
free boost hits 4.9 GHz -> 100 C -> throttle wobble
(3.2-3.5 GHz) within seconds of an all-core phase - the
boost bias itself is a nondeterminism source (measured
07/17/2026).
max : CPU 'performance' profile (power-profiles-daemon hold,
free boost) + GPU persistence ON, clocks unlocked
-> production throughput.
(idle) : everything released; also enforced on daemon stop.
- GPU ops shell out to nvidia-smi with fixed argument lists (no client
input ever reaches a command line); CPU hold uses the same D-Bus call the
old attic/elphel-agent-tools/bin/cpu_perf_hold.py used (which this daemon
supersedes).
- GPU ops shell out to nvidia-smi with fixed argument lists; CPU pinning
writes sysfs cpufreq directly (root). No client input ever reaches a
command line. Supersedes attic/elphel-agent-tools/bin/cpu_perf_hold.py.
Config via environment (set in the systemd unit or /etc/default/elphel-perfd):
Config via environment (systemd unit):
PERFD_PORT control port (default 48890)
PERFD_GPU_LOCK_MHZ 'measure' lock (default 2550; RTX 5060 Ti burst
boost measured ~2595 @ 23 W / 49 C on 07/17/2026 -
thermally trivial, so the value is about
reproducibility, not speed)
PERFD_GPU_LOCK_MHZ 'measure' GPU lock (default 2550; RTX 5060 Ti burst
boost measured ~2595 @ 23 W / 49 C - thermally trivial)
PERFD_CPU_LOCK_KHZ 'measure' CPU pin (default 3200000; i9-11900K
all-core THROTTLE equilibrium measured 3.2-3.5 GHz
@ 100 C - the pin must sit at/below it to stay
deterministic AND cooler)
PERFD_GPU_INDEX GPU index (default 0)
"""
import glob
import json
import os
import signal
import socket
import socketserver
import subprocess
import sys
import threading
import time
PORT = int(os.environ.get("PERFD_PORT", "48890"))
GPU_LOCK_MHZ = int(os.environ.get("PERFD_GPU_LOCK_MHZ", "2550"))
CPU_LOCK_KHZ = int(os.environ.get("PERFD_CPU_LOCK_KHZ", "3200000"))
GPU_INDEX = os.environ.get("PERFD_GPU_INDEX", "0")
NVIDIA_SMI = "/usr/bin/nvidia-smi"
PROFILES = ("measure", "max") # strongest first
GPU_MHZ_BOUNDS = (180, 3090)
CPU_KHZ_BOUNDS = (800000, 5300000)
def log(msg):
......@@ -76,29 +87,30 @@ def _smi(*args):
class GpuControl:
def __init__(self):
self.locked = False
self.locked_mhz = None
def apply(self, profile):
def apply(self, profile, gpu_mhz):
errs = []
rc, out = _smi("-pm", "1")
if rc != 0:
errs.append("persistence: " + out)
if profile == "measure":
rc, out = _smi("-lgc", "{0},{0}".format(GPU_LOCK_MHZ))
rc, out = _smi("-lgc", "{0},{0}".format(gpu_mhz))
if rc != 0:
errs.append("lgc: " + out)
self.locked_mhz = None
else:
self.locked = True
else: # max - make sure no stale lock survives a profile downgrade
self.locked_mhz = gpu_mhz
else: # max - no stale lock may survive a profile downgrade
self.release_lock()
return errs
def release_lock(self):
if self.locked:
if self.locked_mhz is not None:
rc, out = _smi("-rgc")
if rc != 0:
log("WARNING: -rgc failed: " + out)
self.locked = False
self.locked_mhz = None
def release_all(self):
# persistence mode is left ON (harmless, avoids driver re-init cost)
......@@ -107,17 +119,28 @@ class GpuControl:
def status(self):
rc, out = _smi("--query-gpu=clocks.sm,temperature.gpu,power.draw",
"--format=csv,noheader")
return {"locked": self.locked,
"lock_mhz": GPU_LOCK_MHZ if self.locked else None,
return {"locked_mhz": self.locked_mhz,
"telemetry": out if rc == 0 else "unavailable"}
# ---------------------------------------------------------------- CPU control
class CpuControl:
"""Hold the 'performance' profile via power-profiles-daemon (D-Bus) -
the same mechanism cpu_perf_hold.py used; falls back to a no-op with a
warning if the D-Bus service is unavailable."""
"""measure: PIN the frequency (scaling_min=max=KHZ on every cpufreq
policy) - deterministic and below the thermal wall. max: hold the
'performance' profile via power-profiles-daemon (free boost), the same
call the retired cpu_perf_hold.py used."""
def __init__(self):
self.policies = sorted(glob.glob(
"/sys/devices/system/cpu/cpufreq/policy*"))
self.hw_bounds = {}
for p in self.policies:
try:
lo = int(open(p + "/cpuinfo_min_freq").read())
hi = int(open(p + "/cpuinfo_max_freq").read())
self.hw_bounds[p] = (lo, hi)
except OSError:
pass
self.pinned_khz = None
self.cookie = None
self.iface = None
try:
......@@ -129,9 +152,40 @@ class CpuControl:
"org.freedesktop.UPower.PowerProfiles")
except Exception as e:
log("WARNING: power-profiles-daemon unavailable (" + str(e) +
") - CPU holds will be no-ops")
") - 'max' CPU holds will be no-ops")
@staticmethod
def _write(path, value):
with open(path, "w") as f:
f.write(str(value))
def pin(self, khz):
errs = []
for p, (lo, hi) in self.hw_bounds.items():
k = max(lo, min(hi, khz))
try:
# safe order: open the floor, set the ceiling, raise the floor
self._write(p + "/scaling_min_freq", lo)
self._write(p + "/scaling_max_freq", k)
self._write(p + "/scaling_min_freq", k)
except OSError as e:
errs.append(os.path.basename(p) + ": " + str(e))
if not errs:
self.pinned_khz = khz
return errs
def unpin(self):
if self.pinned_khz is None:
return
for p, (lo, hi) in self.hw_bounds.items():
try:
self._write(p + "/scaling_min_freq", lo)
self._write(p + "/scaling_max_freq", hi)
except OSError as e:
log("WARNING: unpin " + os.path.basename(p) + ": " + str(e))
self.pinned_khz = None
def hold(self):
def hold_ppd(self):
if (self.iface is not None) and (self.cookie is None):
try:
self.cookie = self.iface.HoldProfile(
......@@ -140,7 +194,7 @@ class CpuControl:
except Exception as e:
log("WARNING: CPU HoldProfile failed: " + str(e))
def release(self):
def release_ppd(self):
if (self.iface is not None) and (self.cookie is not None):
try:
self.iface.ReleaseProfile(self.cookie)
......@@ -148,24 +202,45 @@ class CpuControl:
log("WARNING: CPU ReleaseProfile failed: " + str(e))
self.cookie = None
def apply(self, profile, cpu_khz):
if profile == "measure":
self.release_ppd()
return self.pin(cpu_khz)
else: # max
self.unpin()
self.hold_ppd()
return []
def release_all(self):
self.unpin()
self.release_ppd()
def status(self):
return {"held": self.cookie is not None}
return {"pinned_khz": self.pinned_khz,
"ppd_held": self.cookie is not None,
"policies": len(self.hw_bounds)}
# ------------------------------------------------------------------ hold book
class HoldBook:
"""Refcounted holds; the strongest requested profile is applied."""
"""Per-connection holds; the strongest profile wins; lock values come
from the NEWEST holder of the winning profile."""
def __init__(self):
self.lock = threading.Lock()
self.counts = {p: 0 for p in PROFILES}
self.applied = None
self.holds = {} # conn_id -> (seq, profile, cpu_khz, gpu_mhz)
self.seq = 0
self.applied = None # (profile, cpu_khz, gpu_mhz) or None
self.gpu = GpuControl()
self.cpu = CpuControl()
def _target(self):
for p in PROFILES: # strongest first
if self.counts[p] > 0:
return p
for prof in PROFILES: # strongest first
best = None
for (seq, p, ck, gm) in self.holds.values():
if (p == prof) and ((best is None) or (seq > best[0])):
best = (seq, p, ck, gm)
if best is not None:
return best[1:]
return None
def _apply_target(self):
......@@ -174,50 +249,77 @@ class HoldBook:
return []
errs = []
if target is None:
self.cpu.release()
self.cpu.release_all()
self.gpu.release_all()
log("all holds released - default power management")
else:
self.cpu.hold()
errs = self.gpu.apply(target)
log("profile '%s' applied (gpu errors: %s)" %
(target, errs if errs else "none"))
profile, cpu_khz, gpu_mhz = target
errs = self.cpu.apply(profile, cpu_khz) + \
self.gpu.apply(profile, gpu_mhz)
log("profile '%s' applied (cpu=%d kHz gpu=%d MHz; errors: %s)" %
(profile, cpu_khz, gpu_mhz, errs if errs else "none"))
self.applied = target
return errs
def acquire(self, profile):
def acquire(self, conn_id, profile, cpu_khz, gpu_mhz):
with self.lock:
self.counts[profile] += 1
self.seq += 1
self.holds[conn_id] = (self.seq, profile, cpu_khz, gpu_mhz)
return self._apply_target()
def release(self, profile):
def release(self, conn_id):
with self.lock:
if self.counts[profile] > 0:
self.counts[profile] -= 1
self.holds.pop(conn_id, None)
self._apply_target()
def release_everything(self):
with self.lock:
for p in PROFILES:
self.counts[p] = 0
self.holds.clear()
self._apply_target()
def status(self):
with self.lock:
return {"applied": self.applied,
"holds": dict(self.counts),
return {"applied": (None if self.applied is None else {
"profile": self.applied[0],
"cpu_khz": self.applied[1],
"gpu_mhz": self.applied[2]}),
"holders": len(self.holds),
"cpu": self.cpu.status(),
"gpu": self.gpu.status(),
"gpu_lock_mhz_configured": GPU_LOCK_MHZ}
"defaults": {"cpu_khz": CPU_LOCK_KHZ,
"gpu_mhz": GPU_LOCK_MHZ}}
BOOK = HoldBook()
# ------------------------------------------------------------------ TCP layer
def _parse_hold(parts):
"""HOLD measure [cpu=KHZ] [gpu=MHZ] -> (profile, cpu_khz, gpu_mhz) or None."""
if (len(parts) < 2) or (parts[1].lower() not in PROFILES):
return None
profile, cpu_khz, gpu_mhz = parts[1].lower(), CPU_LOCK_KHZ, GPU_LOCK_MHZ
for tok in parts[2:]:
if "=" not in tok:
return None
key, _, val = tok.partition("=")
try:
v = int(val)
except ValueError:
return None
if key == "cpu" and CPU_KHZ_BOUNDS[0] <= v <= CPU_KHZ_BOUNDS[1]:
cpu_khz = v
elif key == "gpu" and GPU_MHZ_BOUNDS[0] <= v <= GPU_MHZ_BOUNDS[1]:
gpu_mhz = v
else:
return None
return profile, cpu_khz, gpu_mhz
class Handler(socketserver.StreamRequestHandler):
def handle(self):
held = None # this connection's profile
conn_id = id(self)
holding = False
try:
for raw in self.rfile:
parts = raw.decode("utf-8", "replace").strip().split()
......@@ -228,19 +330,20 @@ class Handler(socketserver.StreamRequestHandler):
self._reply("OK pong")
elif cmd == "STATUS":
self._reply("OK " + json.dumps(BOOK.status()))
elif cmd == "HOLD" and (len(parts) == 2) and \
(parts[1].lower() in PROFILES):
prof = parts[1].lower()
if held is not None: # one hold per connection
BOOK.release(held)
errs = BOOK.acquire(prof)
held = prof
self._reply("OK held %s%s" % (prof,
(" gpu_errors=" + ";".join(errs)) if errs else ""))
elif cmd == "HOLD":
parsed = _parse_hold(parts)
if parsed is None:
self._reply("ERR usage: HOLD measure|max [cpu=KHZ] [gpu=MHZ] (bounds cpu %d..%d, gpu %d..%d)" %
(CPU_KHZ_BOUNDS + GPU_MHZ_BOUNDS))
continue
errs = BOOK.acquire(conn_id, *parsed)
holding = True
self._reply("OK held %s cpu=%d gpu=%d%s" %
(parsed[0], parsed[1], parsed[2],
(" errors=" + ";".join(errs)) if errs else ""))
elif cmd == "RELEASE":
if held is not None:
BOOK.release(held)
held = None
BOOK.release(conn_id)
holding = False
self._reply("OK released")
elif cmd == "QUIT":
break
......@@ -249,8 +352,8 @@ class Handler(socketserver.StreamRequestHandler):
except Exception:
pass
finally:
if held is not None: # crash-safe auto-release
BOOK.release(held)
if holding: # crash-safe auto-release
BOOK.release(conn_id)
def _reply(self, line):
self.wfile.write((line + "\n").encode("utf-8"))
......@@ -270,8 +373,8 @@ def main():
signal.signal(signal.SIGTERM, shutdown)
signal.signal(signal.SIGINT, shutdown)
srv = Server(("127.0.0.1", PORT), Handler) # localhost ONLY
log("listening on 127.0.0.1:%d (gpu lock for 'measure' = %d MHz)" %
(PORT, GPU_LOCK_MHZ))
log("listening on 127.0.0.1:%d (measure defaults: cpu=%d kHz, gpu=%d MHz)" %
(PORT, CPU_LOCK_KHZ, GPU_LOCK_MHZ))
try:
srv.serve_forever()
finally:
......
......@@ -4,11 +4,14 @@ perfctl - unprivileged client for elphel-perfd (127.0.0.1:48890).
perfctl status one-line daemon/clock status
perfctl ping probe (exit 0 = daemon installed)
perfctl hold <measure|max> hold until Ctrl-C
perfctl hold <measure|max> -- CMD [ARGS...]
perfctl hold <measure|max> [cpu=KHZ] [gpu=MHZ]
hold until Ctrl-C (overrides optional)
perfctl hold <measure|max> [cpu=KHZ] [gpu=MHZ] -- CMD [ARGS...]
hold for the duration of CMD
The hold is tied to this process's connection: if perfctl dies, the daemon
releases automatically. By Claude on 07/17/2026.
releases automatically. cpu=/gpu= override the daemon's 'measure' lock
defaults (bounds-checked server-side) - for calibration without
reconfiguration. By Claude on 07/17/2026.
"""
import os
import signal
......@@ -47,16 +50,20 @@ def main():
if args[0] == "status":
print(ask(f, "STATUS"))
return 0
# hold
# hold [cpu=KHZ] [gpu=MHZ] [-- CMD...]
if len(args) < 2 or args[1] not in ("measure", "max"):
print("perfctl: hold needs a profile: measure | max")
return 2
reply = ask(f, "HOLD " + args[1])
overrides = []
rest = args[2:]
while rest and ("=" in rest[0]) and (rest[0] != "--"):
overrides.append(rest.pop(0))
reply = ask(f, " ".join(["HOLD", args[1]] + overrides))
print("perfctl: " + reply)
if not reply.startswith("OK"):
return 1
if len(args) > 2 and args[2] == "--":
rc = subprocess.call(args[3:])
if rest and rest[0] == "--":
rc = subprocess.call(rest[1:])
ask(f, "RELEASE")
return rc
print("perfctl: holding '%s' until Ctrl-C ..." % args[1])
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment