Commit 74d8341c authored by Andrey Filippov's avatar Andrey Filippov

CLAUDE: elphel-perfd - CPU+GPU performance daemon, no sudo in the control path

Design ruled by Andrey 07/17/2026 (lives in imagej-elphel: the capability
follows the hardware the project runs on - 224/137; other programs use the
same localhost socket/CLI):
- perfd/elphel_perfd.py: root systemd service; line protocol on
  127.0.0.1:48890 (PING/STATUS/HOLD measure|max/RELEASE); a hold is TIED TO
  THE CONNECTION (client crash = auto-release), refcounted, strongest
  profile wins. 'measure' = CPU performance (power-profiles-daemon hold,
  same call as the old cpu_perf_hold.py it supersedes) + GPU persistence +
  clocks LOCKED (default 2550 MHz; 5060 Ti burst boost measured ~2595 @
  23 W/49 C - the lock is about run-to-run +-1% reproducibility for
  per-item margin evaluation, not speed). 'max' = performance, unlocked.
  ExecStopPost=nvidia-smi -rgc so a dead daemon never leaves clocks locked.
- perfd/install.sh (one-time sudo, idempotent) + perfctl (unprivileged CLI:
  'perfctl hold measure -- <cmd>') + README.
- Java: com.elphel.imagej.common.PerfDaemon (plain TCP, Java-8, zero deps;
  Hold implements AutoCloseable); Eyesis_Correction startup probe (one
  console line - uses the daemon when installed, prints the sudo install
  command when not); CuasPoseRT preloaded RT harness holds 'measure' for
  the timed run, releases after the RT summary.
Tested end-to-end user-mode (daemon on a test port): protocol, Java client,
CPU hold engaged, GPU lock correctly permission-refused as non-root,
auto-release on disconnect. GPU lock path activates once installed under
root. mvn package + test PASS.
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 a30784a5
# elphel-perfd — CPU+GPU performance management without sudo
Lets Eyesis/imagej-elphel (and any other local program) request deterministic
or maximum-performance clock states with **no sudo in the control path**.
Design ruled by Andrey 07/17/2026: the capability ships with imagej-elphel
because it follows the hardware the project runs on (224/137), while staying
usable by any program through the same local socket.
## Install (once per machine, the only sudo ever needed)
sudo <imagej-elphel>/perfd/install.sh
Installs `/usr/local/lib/elphel-perfd/elphel_perfd.py`, the systemd unit
`elphel-perfd.service` (enabled, running as root), and the unprivileged
client `/usr/local/bin/perfctl`. Idempotent — re-run to update.
Eyesis checks for the daemon at startup: uses it when present, prints the
install command above when absent (and continues normally).
## Use
perfctl status # clocks/holds one-liner
perfctl hold measure -- <command> # deterministic clocks for the command
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.
A hold is tied to the client's connection — process exit or crash releases
it automatically (refcounted across concurrent holders; `measure` outranks
`max`). Daemon stop resets GPU clocks (`ExecStopPost=nvidia-smi -rgc`).
Protocol (127.0.0.1:48890, line-based): `PING`, `STATUS`, `HOLD measure|max`,
`RELEASE`, `QUIT`. Java client: `com.elphel.imagej.common.PerfDaemon`.
Supersedes `attic/elphel-agent-tools/bin/cpu_perf_hold.py` (CPU-only,
process-watch based) once installed.
# elphel-perfd - CPU+GPU performance-management daemon (see elphel_perfd.py).
# Installed by perfd/install.sh from the imagej-elphel repository.
# By Claude on 07/17/2026, design ruled by Andrey same day.
[Unit]
Description=Elphel performance-management daemon (CPU + GPU clock holds without sudo)
After=network.target nvidia-persistenced.service
[Service]
Type=simple
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_GPU_INDEX=0
Restart=on-failure
RestartSec=2
# Safety: never leave GPU clocks locked after the daemon is gone
ExecStopPost=/usr/bin/nvidia-smi -rgc
# Hardening (root is needed for nvidia-smi clock control; contain the rest)
NoNewPrivileges=yes
ProtectHome=yes
PrivateTmp=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
[Install]
WantedBy=multi-user.target
#!/usr/bin/env python3
"""
elphel-perfd - fleet performance-management daemon (CPU + GPU), so that
unprivileged programs (Eyesis/imagej-elphel, agents, scripts) can request
deterministic or maximum-performance clock states WITHOUT sudo.
Design ruled by Andrey 07/17/2026 (lives in the imagej-elphel codebase: the
capability follows the hardware the project runs on - 224/137 - while other
programs use it through the same socket/CLI). Implemented by Claude.
- Runs as a root systemd service (one-time `sudo perfd/install.sh`).
- Control = line protocol on 127.0.0.1:48890 (localhost only; TCP so that
Java 8/11 clients need no extra libraries):
PING -> OK pong
STATUS -> OK {json}
HOLD measure -> OK held measure ... (until connection closes)
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).
- 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
(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).
Config via environment (set in the systemd unit or /etc/default/elphel-perfd):
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_INDEX GPU index (default 0)
"""
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"))
GPU_INDEX = os.environ.get("PERFD_GPU_INDEX", "0")
NVIDIA_SMI = "/usr/bin/nvidia-smi"
PROFILES = ("measure", "max") # strongest first
def log(msg):
print("[elphel-perfd] " + msg, flush=True)
# ---------------------------------------------------------------- GPU control
def _smi(*args):
"""Run nvidia-smi with a FIXED argument list; returns (rc, output)."""
try:
r = subprocess.run((NVIDIA_SMI, "-i", GPU_INDEX) + args,
capture_output=True, text=True, timeout=20)
out = (r.stdout + r.stderr).strip()
return r.returncode, out
except Exception as e: # missing binary, timeout
return -1, str(e)
class GpuControl:
def __init__(self):
self.locked = False
def apply(self, profile):
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))
if rc != 0:
errs.append("lgc: " + out)
else:
self.locked = True
else: # max - make sure no stale lock survives a profile downgrade
self.release_lock()
return errs
def release_lock(self):
if self.locked:
rc, out = _smi("-rgc")
if rc != 0:
log("WARNING: -rgc failed: " + out)
self.locked = False
def release_all(self):
# persistence mode is left ON (harmless, avoids driver re-init cost)
self.release_lock()
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,
"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."""
def __init__(self):
self.cookie = None
self.iface = None
try:
import dbus
bus = dbus.SystemBus()
obj = bus.get_object("org.freedesktop.UPower.PowerProfiles",
"/org/freedesktop/UPower/PowerProfiles")
self.iface = dbus.Interface(obj,
"org.freedesktop.UPower.PowerProfiles")
except Exception as e:
log("WARNING: power-profiles-daemon unavailable (" + str(e) +
") - CPU holds will be no-ops")
def hold(self):
if (self.iface is not None) and (self.cookie is None):
try:
self.cookie = self.iface.HoldProfile(
"performance", "elphel-perfd client hold",
"com.elphel.perfd")
except Exception as e:
log("WARNING: CPU HoldProfile failed: " + str(e))
def release(self):
if (self.iface is not None) and (self.cookie is not None):
try:
self.iface.ReleaseProfile(self.cookie)
except Exception as e:
log("WARNING: CPU ReleaseProfile failed: " + str(e))
self.cookie = None
def status(self):
return {"held": self.cookie is not None}
# ------------------------------------------------------------------ hold book
class HoldBook:
"""Refcounted holds; the strongest requested profile is applied."""
def __init__(self):
self.lock = threading.Lock()
self.counts = {p: 0 for p in PROFILES}
self.applied = None
self.gpu = GpuControl()
self.cpu = CpuControl()
def _target(self):
for p in PROFILES: # strongest first
if self.counts[p] > 0:
return p
return None
def _apply_target(self):
target = self._target()
if target == self.applied:
return []
errs = []
if target is None:
self.cpu.release()
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"))
self.applied = target
return errs
def acquire(self, profile):
with self.lock:
self.counts[profile] += 1
return self._apply_target()
def release(self, profile):
with self.lock:
if self.counts[profile] > 0:
self.counts[profile] -= 1
self._apply_target()
def release_everything(self):
with self.lock:
for p in PROFILES:
self.counts[p] = 0
self._apply_target()
def status(self):
with self.lock:
return {"applied": self.applied,
"holds": dict(self.counts),
"cpu": self.cpu.status(),
"gpu": self.gpu.status(),
"gpu_lock_mhz_configured": GPU_LOCK_MHZ}
BOOK = HoldBook()
# ------------------------------------------------------------------ TCP layer
class Handler(socketserver.StreamRequestHandler):
def handle(self):
held = None # this connection's profile
try:
for raw in self.rfile:
parts = raw.decode("utf-8", "replace").strip().split()
if not parts:
continue
cmd = parts[0].upper()
if cmd == "PING":
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 == "RELEASE":
if held is not None:
BOOK.release(held)
held = None
self._reply("OK released")
elif cmd == "QUIT":
break
else:
self._reply("ERR unknown command")
except Exception:
pass
finally:
if held is not None: # crash-safe auto-release
BOOK.release(held)
def _reply(self, line):
self.wfile.write((line + "\n").encode("utf-8"))
self.wfile.flush()
class Server(socketserver.ThreadingTCPServer):
allow_reuse_address = True
daemon_threads = True
def main():
def shutdown(signum, frame):
log("signal %d - releasing everything and exiting" % signum)
BOOK.release_everything()
sys.exit(0)
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))
try:
srv.serve_forever()
finally:
BOOK.release_everything()
if __name__ == "__main__":
main()
#!/usr/bin/env bash
# elphel-perfd installer - one-time, requires sudo; after this NO program
# ever needs sudo for clock control (they talk to 127.0.0.1:48890).
# Idempotent: re-running updates the script/unit and restarts the service.
# Usage: sudo ./install.sh (from the imagej-elphel/perfd directory)
# By Claude on 07/17/2026.
set -eu
cd "$(dirname "$0")"
if [ "$(id -u)" != 0 ]; then
echo "elphel-perfd install needs root: sudo $0" >&2
exit 1
fi
echo "== installing /usr/local/lib/elphel-perfd/elphel_perfd.py"
install -D -m 0755 elphel_perfd.py /usr/local/lib/elphel-perfd/elphel_perfd.py
echo "== installing /etc/systemd/system/elphel-perfd.service"
install -m 0644 elphel-perfd.service /etc/systemd/system/elphel-perfd.service
echo "== installing /usr/local/bin/perfctl (unprivileged client)"
install -m 0755 perfctl /usr/local/bin/perfctl
echo "== systemctl daemon-reload + enable --now"
systemctl daemon-reload
systemctl enable --now elphel-perfd.service
systemctl restart elphel-perfd.service
sleep 1
echo "== status probe (unprivileged from now on):"
perfctl status || { echo "PROBE FAILED - check: journalctl -u elphel-perfd" >&2; exit 1; }
echo "== elphel-perfd installed. Try: perfctl hold measure -- <command>"
#!/usr/bin/env python3
"""
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...]
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.
"""
import os
import signal
import socket
import subprocess
import sys
PORT = int(os.environ.get("PERFD_PORT", "48890"))
def connect():
s = socket.create_connection(("127.0.0.1", PORT), timeout=2.0)
return s, s.makefile("rw", encoding="utf-8")
def ask(f, line):
f.write(line + "\n")
f.flush()
return f.readline().strip()
def main():
args = sys.argv[1:]
if not args or args[0] not in ("status", "ping", "hold"):
print(__doc__.strip())
return 2
try:
s, f = connect()
except OSError as e:
print("perfctl: elphel-perfd not reachable on 127.0.0.1:%d (%s)" % (PORT, e))
print("perfctl: install once with: sudo <imagej-elphel>/perfd/install.sh")
return 3
if args[0] == "ping":
print(ask(f, "PING"))
return 0
if args[0] == "status":
print(ask(f, "STATUS"))
return 0
# hold
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])
print("perfctl: " + reply)
if not reply.startswith("OK"):
return 1
if len(args) > 2 and args[2] == "--":
rc = subprocess.call(args[3:])
ask(f, "RELEASE")
return rc
print("perfctl: holding '%s' until Ctrl-C ..." % args[1])
try:
signal.pause()
except KeyboardInterrupt:
pass
ask(f, "RELEASE")
print("perfctl: released")
return 0
if __name__ == "__main__":
sys.exit(main())
/**
** PerfDaemon.java - unprivileged client for the elphel-perfd performance-
** management daemon (CPU + GPU clock holds without sudo; see perfd/README.md).
**
** Design ruled by Andrey 07/17/2026: the daemon ships with the imagej-elphel
** codebase (the capability follows the hardware the project runs on), Eyesis
** checks for it at startup - uses it when installed, suggests the one-time
** "sudo .../perfd/install.sh" when not.
**
** Copyright (C) 2026 Elphel, Inc.
**
** -----------------------------------------------------------------------------**
**
** PerfDaemon.java 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 3 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, see <http://www.gnu.org/licenses/>.
** -----------------------------------------------------------------------------**
*/
package com.elphel.imagej.common;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
/**
* Plain-TCP (127.0.0.1:48890, Java-8 compatible, zero dependencies) client
* for elphel-perfd. A hold is tied to the open socket: closing it (or the
* JVM exiting for any reason) releases the hold in the daemon - crash-safe
* by construction. By Claude on 07/17/2026.
*/
public class PerfDaemon {
public static final String HOST = "127.0.0.1";
public static final int PORT = Integer.parseInt(
System.getProperty("elphel.perfd.port", "48890"));
private static final int CONNECT_TIMEOUT_MS = 300;
private static boolean startup_reported = false;
/** A live profile hold; close() (or JVM exit) releases it. */
public static class Hold implements AutoCloseable {
private final Socket socket;
private final Writer out;
private final String profile;
private Hold(Socket socket, Writer out, String profile) {
this.socket = socket;
this.out = out;
this.profile = profile;
}
public String getProfile() { return profile; }
@Override public void close() {
try {
out.write("RELEASE\n");
out.flush();
} catch (IOException e) { /* daemon releases on disconnect anyway */ }
try { socket.close(); } catch (IOException e) { }
}
}
private static Socket connect() throws IOException {
final Socket s = new Socket();
s.connect(new InetSocketAddress(HOST, PORT), CONNECT_TIMEOUT_MS);
s.setSoTimeout(2000);
return s;
}
/** One-shot command on a throwaway connection; null if the daemon is absent. */
private static String query(String command) {
try (Socket s = connect()) {
final Writer w = new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8);
final BufferedReader r = new BufferedReader(
new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
w.write(command + "\n");
w.flush();
return r.readLine();
} catch (IOException e) {
return null;
}
}
/** true when elphel-perfd is installed and answering. */
public static boolean isAvailable() {
final String reply = query("PING");
return (reply != null) && reply.startsWith("OK");
}
/** Daemon status line (JSON after "OK "), or null when absent. */
public static String status() {
return query("STATUS");
}
/**
* Acquire a profile hold ("measure" = deterministic locked clocks,
* "max" = production performance). Returns null (with one console line,
* never an exception) when the daemon is not installed/reachable -
* callers proceed normally without it.
*/
public static Hold hold(String profile) {
try {
final Socket s = connect();
final Writer w = new OutputStreamWriter(s.getOutputStream(), StandardCharsets.UTF_8);
final BufferedReader r = new BufferedReader(
new InputStreamReader(s.getInputStream(), StandardCharsets.UTF_8));
w.write("HOLD " + profile + "\n");
w.flush();
final String reply = r.readLine();
if ((reply != null) && reply.startsWith("OK")) {
System.out.println("PerfDaemon: " + reply);
return new Hold(s, w, profile);
}
System.out.println("PerfDaemon: hold '" + profile + "' refused: " + reply);
s.close();
return null;
} catch (IOException e) {
System.out.println("PerfDaemon: not available (" + e.getMessage() +
") - continuing with default power management");
return null;
}
}
/**
* Startup probe (once per program): one console line either way; when the
* daemon is absent, prints the one-time install command and continues.
*/
public static void startupCheck() {
if (startup_reported) return;
startup_reported = true;
if (isAvailable()) {
System.out.println("PerfDaemon: elphel-perfd available on " + HOST + ":" + PORT +
" (CPU+GPU clock holds without sudo; profiles: measure/max)");
} else {
System.out.println("PerfDaemon: elphel-perfd NOT installed - runs use default power "+
"management (timing varies with clock boost). Install once with:");
System.out.println("PerfDaemon: sudo <imagej-elphel>/perfd/install.sh");
}
}
}
......@@ -522,6 +522,10 @@ public class Eyesis_Correction implements PlugIn, ActionListener {
}
if (IJ.versionLessThan("1.43q"))
return;
// elphel-perfd startup probe (one console line): use the CPU+GPU clock
// daemon when installed, suggest the one-time sudo install when not.
// Design ruled by Andrey 07/17/2026. By Claude on 07/17/2026.
com.elphel.imagej.common.PerfDaemon.startupCheck();
/*
if (instance != null) {
instance.toFront();
......
......@@ -2911,6 +2911,7 @@ public class CuasPoseRT {
}
RtPoseTiming rtTiming = null;
RtPoseProfile rtProfile = null;
com.elphel.imagej.common.PerfDaemon.Hold rtPerfHold = null; // 'measure' clock hold for the timed run // By Claude on 07/17/2026
RT_POSE_PROFILE.remove();
if (activePreload != null) {
int rtInputs = 0;
......@@ -2924,6 +2925,12 @@ public class CuasPoseRT {
clt_parameters.curt.pose_cycles : clt_parameters.imp.max_cycles;
rtProfile = new RtPoseProfile(rtInputs, profileCycles);
RT_POSE_PROFILE.set(rtProfile);
// elphel-perfd 'measure' hold for the timed run: deterministic CPU/GPU
// clocks (per-item A/B evaluation needs +-1%, free-running boost gives
// +-10%). Null when the daemon is not installed (one console line, run
// proceeds normally); released after the RT summary; an abnormal exit
// releases via the JVM's socket close. By Claude on 07/17/2026.
rtPerfHold = com.elphel.imagej.common.PerfDaemon.hold("measure");
}
for (int nscene = earliest; nscene < quadCLTs.length; nscene++) {
if (quadCLTs[nscene] == null) continue;
......@@ -3333,6 +3340,10 @@ public class CuasPoseRT {
if (rtTiming != null) rtTiming.printSummary(num_fail);
if (rtProfile != null) rtProfile.printSummary();
RT_POSE_PROFILE.remove();
if (rtPerfHold != null) { // release the 'measure' clock hold (perfd) // By Claude on 07/17/2026
rtPerfHold.close();
rtPerfHold = null;
}
// ---- Output ----
final String csv_path = center_CLT.getX3dDirectory(true) + Prefs.getFileSeparator() +
......
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