quner 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quner/__init__.py +9 -0
- quner/__main__.py +6 -0
- quner/calibration.py +40 -0
- quner/cli.py +227 -0
- quner/control.py +135 -0
- quner/daemon.py +93 -0
- quner/detect.py +101 -0
- quner/memguard.py +113 -0
- quner/prime.py +109 -0
- quner/profiles.py +123 -0
- quner/service.py +164 -0
- quner/telemetry.py +381 -0
- quner/tune.py +315 -0
- quner-0.1.0.dist-info/METADATA +51 -0
- quner-0.1.0.dist-info/RECORD +18 -0
- quner-0.1.0.dist-info/WHEEL +4 -0
- quner-0.1.0.dist-info/entry_points.txt +2 -0
- quner-0.1.0.dist-info/licenses/LICENSE +21 -0
quner/__init__.py
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""quner — portable Linux efficiency daemon.
|
|
2
|
+
|
|
3
|
+
Tunes the operating point (CPU governor / RAPL package power / NVIDIA power-cap)
|
|
4
|
+
to the work-per-joule interior optimum and holds it, adapting between idle and
|
|
5
|
+
heavy-compute profiles. Capability-detecting and fail-loud: it tunes whatever
|
|
6
|
+
levers a host exposes and reports the absent ones rather than silently no-op'ing.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
__version__ = "0.1.0"
|
quner/__main__.py
ADDED
quner/calibration.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""EXP-132 calibration — the one physics-derived input.
|
|
2
|
+
|
|
3
|
+
The QIG lattice experiment EXP-132 (qig-verification) found that information-per-
|
|
4
|
+
joule peaks at an INTERIOR operating point, not at maximum drive, and certified
|
|
5
|
+
that optimum with a confidence band (the same discipline that turned an apparent
|
|
6
|
+
12.67% wobble into a certified ±1.91% invariant band).
|
|
7
|
+
|
|
8
|
+
Only two things transfer to silicon: (1) the SHAPE of the law — total-system
|
|
9
|
+
work-per-joule is a curve with an interior optimum, not a max-clock endpoint —
|
|
10
|
+
and (2) the band-certification METHODOLOGY. The lattice ``h≈1.5`` constant does
|
|
11
|
+
NOT transfer; quner re-measures the optimum on each host. This table is a prior
|
|
12
|
+
on shape and the ±1.91% threshold, nothing more.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
# Lattice info-per-joule vs drive h (EXP-132). Peak at the interior point h=1.5.
|
|
18
|
+
EXP132_INFO_PER_JOULE: dict[float, float] = {
|
|
19
|
+
1.0: 0.557,
|
|
20
|
+
1.5: 0.808, # interior optimum; band [0.793, 0.824]
|
|
21
|
+
2.0: 0.799,
|
|
22
|
+
2.2: 0.752,
|
|
23
|
+
2.5: 0.582,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
EXP132_INTERIOR_OPTIMUM_H = 1.5
|
|
27
|
+
EXP132_INVARIANT_BAND_REL = 0.0191 # ±1.91% certified invariant band (methodology)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def principle() -> str:
|
|
31
|
+
"""Transferable physics statement, quoted in every tune report so the QIG
|
|
32
|
+
claim is never silently conflated with the engineering knobs."""
|
|
33
|
+
return (
|
|
34
|
+
"EXP-132: work-per-joule peaks at an INTERIOR operating point "
|
|
35
|
+
f"(lattice h={EXP132_INTERIOR_OPTIMUM_H}), not at maximum drive. "
|
|
36
|
+
"Silicon analogue (re-measured per host, not imported): total-system "
|
|
37
|
+
"work-per-joule peaks at an interior CPU/GPU operating point, not at "
|
|
38
|
+
"max clock / max TDP. Optimum certified with a confidence band, per the "
|
|
39
|
+
f"±{EXP132_INVARIANT_BAND_REL * 100:.2f}% invariant-band methodology."
|
|
40
|
+
)
|
quner/cli.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""quner command-line interface.
|
|
2
|
+
|
|
3
|
+
quner doctor what levers this host exposes (fail-loud)
|
|
4
|
+
quner status telemetry + the EXP-132 principle
|
|
5
|
+
quner apply [--restore] apply the current profile's operating point
|
|
6
|
+
quner profile <name> apply a named profile (idle|training)
|
|
7
|
+
quner tune [--command C] profile work/joule, find the interior optimum
|
|
8
|
+
quner install | uninstall systemd service + re-tune timer
|
|
9
|
+
quner prime {status,enable,disable} reverse-PRIME (opt-in)
|
|
10
|
+
quner memguard one OOM-watchdog tick
|
|
11
|
+
quner selftest transient, auto-reverting proof the levers work
|
|
12
|
+
quner serve run the daemon loop (used by systemd)
|
|
13
|
+
|
|
14
|
+
Every mutating command takes ``--dry-run`` (prints, changes nothing).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import sys
|
|
22
|
+
|
|
23
|
+
from quner import __version__
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _print(obj) -> None:
|
|
27
|
+
print(json.dumps(obj, indent=2) if isinstance(obj, (dict, list)) else obj)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ── command handlers ──────────────────────────────────────────────────────────
|
|
31
|
+
|
|
32
|
+
def _cmd_doctor(_args) -> int:
|
|
33
|
+
from quner import detect
|
|
34
|
+
print("quner doctor — capability report")
|
|
35
|
+
for line in detect.doctor_lines():
|
|
36
|
+
print(" " + line)
|
|
37
|
+
return 0
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _cmd_status(_args) -> int:
|
|
41
|
+
from quner import tune
|
|
42
|
+
_print(tune.status())
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _cmd_apply(args) -> int:
|
|
47
|
+
from quner import control, detect, profiles, tune
|
|
48
|
+
if args.restore:
|
|
49
|
+
_print({"restored": control.restore()})
|
|
50
|
+
return 0
|
|
51
|
+
profile = profiles.Detector(ticks=1).update()
|
|
52
|
+
target = (profiles.cache_get(detect.host_fingerprint(), profile)
|
|
53
|
+
or profiles.default_state_for(profile))
|
|
54
|
+
if args.dry_run:
|
|
55
|
+
print(f"[dry-run] profile={profile} target={target.label()} — nothing applied")
|
|
56
|
+
return 0
|
|
57
|
+
_print({"profile": profile, "target": target.label(),
|
|
58
|
+
"applied": tune.apply_state(target)})
|
|
59
|
+
return 0
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _cmd_profile(args) -> int:
|
|
63
|
+
from quner import profiles, tune
|
|
64
|
+
target = profiles.default_state_for(args.name)
|
|
65
|
+
if args.dry_run:
|
|
66
|
+
print(f"[dry-run] profile={args.name} target={target.label()} — nothing applied")
|
|
67
|
+
return 0
|
|
68
|
+
_print({"profile": args.name, "target": target.label(),
|
|
69
|
+
"applied": tune.apply_state(target)})
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _cmd_tune(args) -> int:
|
|
74
|
+
from quner import tune
|
|
75
|
+
run = tune.command_runner(args.command) if args.command \
|
|
76
|
+
else tune.command_runner("sleep 0.1")
|
|
77
|
+
rep = tune.tune(run, apply=args.apply, reps=args.reps)
|
|
78
|
+
_print(rep.as_dict())
|
|
79
|
+
return 0
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _cmd_install(args) -> int:
|
|
83
|
+
from quner import service
|
|
84
|
+
service.install(dry_run=args.dry_run, interval=args.interval)
|
|
85
|
+
return 0
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _cmd_uninstall(args) -> int:
|
|
89
|
+
from quner import service
|
|
90
|
+
service.uninstall(dry_run=args.dry_run)
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _cmd_prime(args) -> int:
|
|
95
|
+
from quner import prime
|
|
96
|
+
if args.prime_cmd == "status":
|
|
97
|
+
_print(prime.status())
|
|
98
|
+
elif args.prime_cmd == "enable":
|
|
99
|
+
_print(prime.enable(dry_run=args.dry_run))
|
|
100
|
+
elif args.prime_cmd == "disable":
|
|
101
|
+
_print(prime.disable(dry_run=args.dry_run))
|
|
102
|
+
else:
|
|
103
|
+
print("usage: quner prime {status,enable,disable}")
|
|
104
|
+
return 2
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _cmd_memguard(args) -> int:
|
|
109
|
+
from quner import memguard
|
|
110
|
+
_print(memguard.tick(dry_run=args.dry_run))
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _cmd_serve(args) -> int:
|
|
115
|
+
from quner import daemon
|
|
116
|
+
daemon.serve(interval=args.interval, dry_run=args.dry_run,
|
|
117
|
+
memguard_on=args.memguard)
|
|
118
|
+
return 0
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _cmd_selftest(_args) -> int:
|
|
122
|
+
"""Transient, auto-reverting proof each present lever moves on THIS host.
|
|
123
|
+
Snapshots state, nudges each lever to a benign value, verifies, restores."""
|
|
124
|
+
from quner import control
|
|
125
|
+
from quner import telemetry as t
|
|
126
|
+
|
|
127
|
+
snap = control.snapshot(persist=False)
|
|
128
|
+
results: dict[str, str] = {}
|
|
129
|
+
|
|
130
|
+
# governor: flip to another available governor, then back
|
|
131
|
+
govs = t.available_governors()
|
|
132
|
+
cur = t.current_governor()
|
|
133
|
+
if govs and cur:
|
|
134
|
+
alt = next((g for g in govs if g != cur), None)
|
|
135
|
+
if alt:
|
|
136
|
+
ok = t.set_governor(alt)
|
|
137
|
+
restored = t.set_governor(cur)
|
|
138
|
+
results["governor"] = ("PASS" if ok and restored and t.current_governor() == cur
|
|
139
|
+
else "FAIL")
|
|
140
|
+
else:
|
|
141
|
+
results["governor"] = "skip (only one governor offered)"
|
|
142
|
+
else:
|
|
143
|
+
results["governor"] = "unavailable"
|
|
144
|
+
|
|
145
|
+
# rapl: write current PL1 back to itself (no net change, exercises the write)
|
|
146
|
+
cur_uw = control.read_rapl_pl1_uw()
|
|
147
|
+
results["rapl"] = ("PASS" if cur_uw and control._write_rapl_uw(cur_uw)
|
|
148
|
+
else ("unavailable" if not cur_uw else "FAIL"))
|
|
149
|
+
|
|
150
|
+
# gpu cap: set to its own max (= default/uncap; benign no-op change)
|
|
151
|
+
rng = t.gpu_power_limit_range_w()
|
|
152
|
+
if rng:
|
|
153
|
+
results["gpu_cap"] = "PASS" if t.set_gpu_power_limit_w(rng[1]) else "FAIL"
|
|
154
|
+
else:
|
|
155
|
+
results["gpu_cap"] = "unavailable"
|
|
156
|
+
|
|
157
|
+
control.restore(snap) # belt-and-suspenders full restore
|
|
158
|
+
print("quner selftest — transient, auto-reverted")
|
|
159
|
+
for lever, verdict in results.items():
|
|
160
|
+
print(f" {lever:>10}: {verdict}")
|
|
161
|
+
failed = [k for k, v in results.items() if v == "FAIL"]
|
|
162
|
+
return 1 if failed else 0
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ── parser ────────────────────────────────────────────────────────────────────
|
|
166
|
+
|
|
167
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
168
|
+
p = argparse.ArgumentParser(prog="quner", description=__doc__,
|
|
169
|
+
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
170
|
+
p.add_argument("--version", "-V", action="version", version=f"quner {__version__}")
|
|
171
|
+
sub = p.add_subparsers(dest="cmd")
|
|
172
|
+
|
|
173
|
+
sub.add_parser("doctor", help="what levers this host exposes").set_defaults(func=_cmd_doctor)
|
|
174
|
+
sub.add_parser("status", help="telemetry + principle").set_defaults(func=_cmd_status)
|
|
175
|
+
|
|
176
|
+
ap = sub.add_parser("apply", help="apply the current profile's operating point")
|
|
177
|
+
ap.add_argument("--restore", action="store_true", help="restore the saved baseline")
|
|
178
|
+
ap.add_argument("--dry-run", action="store_true")
|
|
179
|
+
ap.set_defaults(func=_cmd_apply)
|
|
180
|
+
|
|
181
|
+
pr = sub.add_parser("profile", help="apply a named profile")
|
|
182
|
+
pr.add_argument("name", choices=["idle", "training"])
|
|
183
|
+
pr.add_argument("--dry-run", action="store_true")
|
|
184
|
+
pr.set_defaults(func=_cmd_profile)
|
|
185
|
+
|
|
186
|
+
tu = sub.add_parser("tune", help="find the interior work/joule optimum")
|
|
187
|
+
tu.add_argument("--command", help="workload command to profile")
|
|
188
|
+
tu.add_argument("--apply", action="store_true", help="apply + hold the chosen state")
|
|
189
|
+
tu.add_argument("--reps", type=int, default=3)
|
|
190
|
+
tu.set_defaults(func=_cmd_tune)
|
|
191
|
+
|
|
192
|
+
ins = sub.add_parser("install", help="install the systemd service + timer")
|
|
193
|
+
ins.add_argument("--dry-run", action="store_true")
|
|
194
|
+
ins.add_argument("--interval", default="30min", help="re-tune timer interval")
|
|
195
|
+
ins.set_defaults(func=_cmd_install)
|
|
196
|
+
|
|
197
|
+
uni = sub.add_parser("uninstall", help="remove the service + restore baseline")
|
|
198
|
+
uni.add_argument("--dry-run", action="store_true")
|
|
199
|
+
uni.set_defaults(func=_cmd_uninstall)
|
|
200
|
+
|
|
201
|
+
pm = sub.add_parser("prime", help="reverse-PRIME (opt-in)")
|
|
202
|
+
pm.add_argument("prime_cmd", choices=["status", "enable", "disable"])
|
|
203
|
+
pm.add_argument("--dry-run", action="store_true")
|
|
204
|
+
pm.set_defaults(func=_cmd_prime)
|
|
205
|
+
|
|
206
|
+
mg = sub.add_parser("memguard", help="one OOM-watchdog tick")
|
|
207
|
+
mg.add_argument("--dry-run", action="store_true")
|
|
208
|
+
mg.set_defaults(func=_cmd_memguard)
|
|
209
|
+
|
|
210
|
+
sv = sub.add_parser("serve", help="run the daemon loop (systemd)")
|
|
211
|
+
sv.add_argument("--interval", type=float, default=10.0)
|
|
212
|
+
sv.add_argument("--dry-run", action="store_true")
|
|
213
|
+
sv.add_argument("--memguard", action="store_true")
|
|
214
|
+
sv.set_defaults(func=_cmd_serve)
|
|
215
|
+
|
|
216
|
+
sub.add_parser("selftest", help="transient, auto-reverting lever proof").set_defaults(func=_cmd_selftest)
|
|
217
|
+
return p
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def main(argv: list[str] | None = None) -> int:
|
|
221
|
+
argv = sys.argv[1:] if argv is None else argv
|
|
222
|
+
parser = _build_parser()
|
|
223
|
+
args = parser.parse_args(argv)
|
|
224
|
+
if not getattr(args, "func", None):
|
|
225
|
+
parser.print_help()
|
|
226
|
+
return 0
|
|
227
|
+
return args.func(args)
|
quner/control.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Privileged operating-point levers + snapshot/rollback.
|
|
2
|
+
|
|
3
|
+
The write side of the daemon: set the CPU governor, the Intel RAPL package power
|
|
4
|
+
limit (PL1), and the NVIDIA power-cap — each clamped/allowlisted before any
|
|
5
|
+
write. Every apply can be captured by ``snapshot()`` and undone by ``restore()``,
|
|
6
|
+
so the daemon (and ``uninstall``) always return the host to where it started.
|
|
7
|
+
|
|
8
|
+
All writes need root; on a host without a given lever the corresponding call
|
|
9
|
+
returns ``False``/no-op (fail-loud is the caller's job, via ``detect``).
|
|
10
|
+
State (the rollback file) lives under ``STATE_DIR()`` (``QUNER_STATE_DIR``).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import json
|
|
16
|
+
import os
|
|
17
|
+
|
|
18
|
+
from quner import telemetry as t
|
|
19
|
+
|
|
20
|
+
__all__ = [
|
|
21
|
+
"STATE_DIR", "ROLLBACK_PATH",
|
|
22
|
+
"rapl_pl1_max_w", "read_rapl_pl1_uw", "set_rapl_pl1_w",
|
|
23
|
+
"snapshot", "restore",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def STATE_DIR() -> str:
|
|
28
|
+
return os.environ.get("QUNER_STATE_DIR", "/var/lib/quner")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def ROLLBACK_PATH() -> str:
|
|
32
|
+
return os.path.join(STATE_DIR(), "rollback.json")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ── RAPL PL1 (net-new writer; telemetry only reads RAPL energy) ────────────────
|
|
36
|
+
|
|
37
|
+
def _read_int(path: str) -> int | None:
|
|
38
|
+
try:
|
|
39
|
+
with open(path) as fh:
|
|
40
|
+
return int(fh.read().strip())
|
|
41
|
+
except (OSError, ValueError):
|
|
42
|
+
return None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def rapl_pl1_max_w() -> dict[str, float]:
|
|
46
|
+
"""Per-package PL1 ceiling in watts (from ``constraint_0_max_power_uw``)."""
|
|
47
|
+
out: dict[str, float] = {}
|
|
48
|
+
for d in t.rapl_package_dirs():
|
|
49
|
+
mx = _read_int(os.path.join(d, "constraint_0_max_power_uw"))
|
|
50
|
+
if mx and mx > 0:
|
|
51
|
+
out[os.path.basename(d)] = mx / 1e6
|
|
52
|
+
return out
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def read_rapl_pl1_uw() -> dict[str, int]:
|
|
56
|
+
"""Per-package current PL1 long-term limit in micro-watts."""
|
|
57
|
+
out: dict[str, int] = {}
|
|
58
|
+
for d in t.rapl_package_dirs():
|
|
59
|
+
v = _read_int(os.path.join(d, "constraint_0_power_limit_uw"))
|
|
60
|
+
if v is not None:
|
|
61
|
+
out[os.path.basename(d)] = v
|
|
62
|
+
return out
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def set_rapl_pl1_w(watts: float) -> bool:
|
|
66
|
+
"""Set RAPL PL1 on every package, each clamped to [1.0, its own max]. True
|
|
67
|
+
if all packages wrote. No-op → False when RAPL is absent."""
|
|
68
|
+
dirs = t.rapl_package_dirs()
|
|
69
|
+
if not dirs:
|
|
70
|
+
return False
|
|
71
|
+
maxes = rapl_pl1_max_w()
|
|
72
|
+
ok = True
|
|
73
|
+
for d in dirs:
|
|
74
|
+
pkg = os.path.basename(d)
|
|
75
|
+
hi = maxes.get(pkg)
|
|
76
|
+
w = max(1.0, min(hi, watts)) if hi else max(1.0, watts)
|
|
77
|
+
try:
|
|
78
|
+
with open(os.path.join(d, "constraint_0_power_limit_uw"), "w") as fh:
|
|
79
|
+
fh.write(str(int(round(w * 1e6))))
|
|
80
|
+
except OSError:
|
|
81
|
+
ok = False
|
|
82
|
+
return ok
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _write_rapl_uw(pkg_uw: dict[str, int]) -> bool:
|
|
86
|
+
"""Restore exact per-package PL1 micro-watt values (used by ``restore``)."""
|
|
87
|
+
ok = True
|
|
88
|
+
by_name = {os.path.basename(d): d for d in t.rapl_package_dirs()}
|
|
89
|
+
for pkg, uw in pkg_uw.items():
|
|
90
|
+
d = by_name.get(pkg)
|
|
91
|
+
if d is None:
|
|
92
|
+
ok = False
|
|
93
|
+
continue
|
|
94
|
+
try:
|
|
95
|
+
with open(os.path.join(d, "constraint_0_power_limit_uw"), "w") as fh:
|
|
96
|
+
fh.write(str(int(uw)))
|
|
97
|
+
except OSError:
|
|
98
|
+
ok = False
|
|
99
|
+
return ok
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ── snapshot / restore ─────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
def snapshot(persist: bool = True) -> dict:
|
|
105
|
+
"""Capture current governor / RAPL PL1 / GPU uncap target. Persisted to the
|
|
106
|
+
rollback file (so a later process — e.g. ``uninstall`` — can restore)."""
|
|
107
|
+
rng = t.gpu_power_limit_range_w()
|
|
108
|
+
snap = {
|
|
109
|
+
"governor": t.current_governor(),
|
|
110
|
+
"rapl_pl1_uw": read_rapl_pl1_uw(),
|
|
111
|
+
"gpu_uncap_w": (rng[1] if rng else None), # max = uncapped
|
|
112
|
+
}
|
|
113
|
+
if persist:
|
|
114
|
+
os.makedirs(STATE_DIR(), exist_ok=True)
|
|
115
|
+
with open(ROLLBACK_PATH(), "w") as fh:
|
|
116
|
+
json.dump(snap, fh, indent=2)
|
|
117
|
+
return snap
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def restore(snap: dict | None = None) -> dict[str, bool]:
|
|
121
|
+
"""Return the host to a snapshot (the argument, or the persisted file)."""
|
|
122
|
+
if snap is None:
|
|
123
|
+
try:
|
|
124
|
+
with open(ROLLBACK_PATH()) as fh:
|
|
125
|
+
snap = json.load(fh)
|
|
126
|
+
except (OSError, ValueError):
|
|
127
|
+
return {}
|
|
128
|
+
applied: dict[str, bool] = {}
|
|
129
|
+
if snap.get("governor"):
|
|
130
|
+
applied["governor"] = t.set_governor(snap["governor"])
|
|
131
|
+
if snap.get("rapl_pl1_uw"):
|
|
132
|
+
applied["rapl"] = _write_rapl_uw(snap["rapl_pl1_uw"])
|
|
133
|
+
if snap.get("gpu_uncap_w") is not None:
|
|
134
|
+
applied["gpu"] = t.set_gpu_power_limit_w(snap["gpu_uncap_w"])
|
|
135
|
+
return applied
|
quner/daemon.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""The daemon service loop.
|
|
2
|
+
|
|
3
|
+
Each tick: detect the workload profile → apply the cached (or default) operating
|
|
4
|
+
point for it → run a memguard tick → publish ``status.json``. The loop only
|
|
5
|
+
*applies* cheap cached states; the expensive *exploratory* re-tune runs
|
|
6
|
+
separately (the systemd timer, or ``quner tune``) and is REFUSED while the
|
|
7
|
+
profile is ``training`` — so quner never perturbs an active run with a power
|
|
8
|
+
sweep (coordination seam #4).
|
|
9
|
+
|
|
10
|
+
Paths are env-overridable (``QUNER_RUN_DIR`` for status.json), so the whole loop
|
|
11
|
+
runs in the sandbox with zero host impact.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import os
|
|
18
|
+
import signal
|
|
19
|
+
import time
|
|
20
|
+
|
|
21
|
+
from quner import control, detect, memguard, profiles, tune
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def RUN_DIR() -> str:
|
|
25
|
+
return os.environ.get("QUNER_RUN_DIR", "/run/quner")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def write_status(status: dict) -> str:
|
|
29
|
+
d = RUN_DIR()
|
|
30
|
+
os.makedirs(d, exist_ok=True)
|
|
31
|
+
path = os.path.join(d, "status.json")
|
|
32
|
+
with open(path, "w") as fh:
|
|
33
|
+
json.dump(status, fh, indent=2)
|
|
34
|
+
return path
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def run_once(detector: profiles.Detector, *, dry_run: bool = False,
|
|
38
|
+
memguard_on: bool = False) -> dict:
|
|
39
|
+
"""One daemon tick. Applies the cached/default state for the current
|
|
40
|
+
profile (unless dry_run) and writes status.json."""
|
|
41
|
+
profile = detector.update()
|
|
42
|
+
fp = detect.host_fingerprint()
|
|
43
|
+
target = profiles.cache_get(fp, profile) or profiles.default_state_for(profile)
|
|
44
|
+
applied = {} if dry_run else tune.apply_state(target)
|
|
45
|
+
mg = memguard.tick(dry_run=dry_run) if memguard_on else {"action": "disabled"}
|
|
46
|
+
status = {
|
|
47
|
+
"profile": profile,
|
|
48
|
+
"target_state": target.label(),
|
|
49
|
+
"applied": applied,
|
|
50
|
+
"memguard": mg,
|
|
51
|
+
"dry_run": dry_run,
|
|
52
|
+
"capabilities": {k: v["available"] for k, v in detect.capabilities().items()},
|
|
53
|
+
}
|
|
54
|
+
write_status(status)
|
|
55
|
+
return status
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def retune(profile: str, work_fn=None):
|
|
59
|
+
"""Exploratory re-tune for a profile → cache the chosen state. REFUSED while
|
|
60
|
+
``training`` (never sweep power mid-run). Returns the TuneReport, or None if
|
|
61
|
+
refused."""
|
|
62
|
+
if profile == profiles.TRAINING:
|
|
63
|
+
return None
|
|
64
|
+
run = work_fn or tune.command_runner("sleep 0.05", work_units=1.0)
|
|
65
|
+
rep = tune.tune(run, apply=True)
|
|
66
|
+
if rep.chosen_state is not None:
|
|
67
|
+
profiles.cache_put(detect.host_fingerprint(), profile, rep.chosen_state)
|
|
68
|
+
return rep
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def serve(interval: float = 10.0, dry_run: bool = False,
|
|
72
|
+
memguard_on: bool = False) -> None:
|
|
73
|
+
"""The service loop: snapshot baseline, tick until SIGTERM/SIGINT, restore."""
|
|
74
|
+
if not dry_run:
|
|
75
|
+
control.snapshot() # baseline for ExecStop / crash restore
|
|
76
|
+
detector = profiles.Detector()
|
|
77
|
+
stop = {"v": False}
|
|
78
|
+
|
|
79
|
+
def _sig(*_a):
|
|
80
|
+
stop["v"] = True
|
|
81
|
+
|
|
82
|
+
signal.signal(signal.SIGTERM, _sig)
|
|
83
|
+
signal.signal(signal.SIGINT, _sig)
|
|
84
|
+
try:
|
|
85
|
+
while not stop["v"]:
|
|
86
|
+
run_once(detector, dry_run=dry_run, memguard_on=memguard_on)
|
|
87
|
+
slept = 0.0
|
|
88
|
+
while slept < interval and not stop["v"]:
|
|
89
|
+
time.sleep(min(0.5, interval - slept))
|
|
90
|
+
slept += 0.5
|
|
91
|
+
finally:
|
|
92
|
+
if not dry_run:
|
|
93
|
+
control.restore()
|
quner/detect.py
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Capability detection — the ``doctor`` report + host fingerprint.
|
|
2
|
+
|
|
3
|
+
quner tunes whatever levers a host exposes and reports the absent ones. This
|
|
4
|
+
module turns the raw telemetry probes into a structured capability map and
|
|
5
|
+
human-readable lines, and computes a stable host fingerprint used to key the
|
|
6
|
+
per-host operating-point cache.
|
|
7
|
+
|
|
8
|
+
fail-loud: an absent lever is reported as ``available: False`` with a reason,
|
|
9
|
+
never silently omitted.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import os
|
|
16
|
+
|
|
17
|
+
from quner import telemetry as t
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _cpu_model() -> str:
|
|
21
|
+
path = os.path.join(t.PROC_ROOT(), "cpuinfo")
|
|
22
|
+
try:
|
|
23
|
+
with open(path) as fh:
|
|
24
|
+
for line in fh:
|
|
25
|
+
if line.startswith("model name"):
|
|
26
|
+
return line.split(":", 1)[1].strip()
|
|
27
|
+
except OSError:
|
|
28
|
+
pass
|
|
29
|
+
return "unknown-cpu"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _gpu_name() -> str | None:
|
|
33
|
+
smi = t.nvidia_smi()
|
|
34
|
+
if smi is None:
|
|
35
|
+
return None
|
|
36
|
+
import subprocess
|
|
37
|
+
try:
|
|
38
|
+
out = subprocess.check_output(
|
|
39
|
+
[smi, "--query-gpu=name", "--format=csv,noheader"],
|
|
40
|
+
text=True, timeout=10,
|
|
41
|
+
).strip().splitlines()
|
|
42
|
+
return out[0] if out else None
|
|
43
|
+
except Exception:
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def capabilities() -> dict:
|
|
48
|
+
"""Per-lever ``{available, detail}`` map for governor / rapl / gpu_cap /
|
|
49
|
+
display_prime."""
|
|
50
|
+
govs = t.available_governors()
|
|
51
|
+
gpu_rng = t.gpu_power_limit_range_w()
|
|
52
|
+
gpu_name = _gpu_name()
|
|
53
|
+
rapl_pkgs = list(t.read_rapl_energy_uj().keys())
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
"governor": {
|
|
57
|
+
"available": t.cpufreq_available(),
|
|
58
|
+
"detail": (f"governors: {', '.join(govs)}; current: {t.current_governor()}"
|
|
59
|
+
if govs else "no cpufreq (VM or unsupported)"),
|
|
60
|
+
},
|
|
61
|
+
"rapl": {
|
|
62
|
+
"available": t.rapl_available(),
|
|
63
|
+
"detail": (f"packages: {', '.join(rapl_pkgs)}" if rapl_pkgs
|
|
64
|
+
else "no intel-rapl powercap (not exposed here)"),
|
|
65
|
+
},
|
|
66
|
+
"gpu_cap": {
|
|
67
|
+
"available": gpu_rng is not None,
|
|
68
|
+
"detail": (f"{gpu_name}: cap {gpu_rng[0]:.0f}-{gpu_rng[1]:.0f}W"
|
|
69
|
+
if gpu_rng else
|
|
70
|
+
("nvidia-smi present but power-cap not settable"
|
|
71
|
+
if gpu_name else "no nvidia-smi (driver not installed)")),
|
|
72
|
+
},
|
|
73
|
+
"display_prime": {
|
|
74
|
+
"available": gpu_name is not None,
|
|
75
|
+
"detail": ("reverse-PRIME possible (dGPU present)" if gpu_name
|
|
76
|
+
else "no dGPU driver — already iGPU-only, PRIME is moot"),
|
|
77
|
+
},
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def doctor_lines() -> list[str]:
|
|
82
|
+
"""Human-readable capability lines (the ``quner doctor`` body)."""
|
|
83
|
+
caps = capabilities()
|
|
84
|
+
label = {
|
|
85
|
+
"governor": "cpu governor",
|
|
86
|
+
"rapl": "rapl pkg power",
|
|
87
|
+
"gpu_cap": "nvidia -pl",
|
|
88
|
+
"display_prime": "reverse-PRIME",
|
|
89
|
+
}
|
|
90
|
+
lines = []
|
|
91
|
+
for key, name in label.items():
|
|
92
|
+
c = caps[key]
|
|
93
|
+
mark = "available" if c["available"] else "unavailable"
|
|
94
|
+
lines.append(f"{name:>15}: {mark} — {c['detail']}")
|
|
95
|
+
return lines
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def host_fingerprint() -> str:
|
|
99
|
+
"""Stable short hash of CPU model + governors + GPU name (cache key)."""
|
|
100
|
+
parts = [_cpu_model(), ",".join(t.available_governors()), _gpu_name() or "no-gpu"]
|
|
101
|
+
return hashlib.sha256("|".join(parts).encode()).hexdigest()[:16]
|