Commit 44263086 by PLN (Algolia)

feat(perf): gpu-mode switch — the real dGPU-off lever + cockpit surfacing

The last unclaimed thermal lever. Our 2026-07-24 finding: on this Plasma-Wayland
box Xwayland pins the RTX 2060 'active' regardless of runtime-PM (control=auto,
--rtd3), so the dGPU never actually suspends live and keeps drawing ~4-5 W + idle
heat. The ONLY real off is EnvyControl's 'integrated' mode (blacklist nvidia) — but
that's a persistent config change that needs a re-login, categorically unlike the
instant perf.sh modes (governor/EPP/RAPL). So it gets its own tool, not a perf-mode.

gpu-mode.sh (new, at repo root beside perf.sh)
----------------------------------------------
A guarded EnvyControl front-end: `query` (default — manager mode + live dGPU
runtime state, with the right advice per mode), `integrated` (dGPU off; warns you
lose CUDA/NVENC for demucs/audio-ML until you switch back), `hybrid` (restore with
aggressive --rtd3 3 = D3cold-when-idle). Every switch confirms (skip with -y), runs
`sudo envycontrol ...` (prompts for your password), then prints the exact re-login
step (reboot safest; verify with `./gpu-mode.sh query` + `nvidia-smi`).

Deliberately NOT added to the passwordless perf-audio sudoers whitelist: this is a
rare, considered, re-login-required switch — the web Bridge and tray must never be
able to power-cycle the GPU config unattended. Runs by hand only.

Cockpit surfacing (Bridge + tray)
---------------------------------
- perf.py: gpu_manager_mode() reads `envycontrol --query`, cached 300s (the mode is
  static within a login session; a re-login restarts this --user service and
  refreshes the cache), defensive → None if envycontrol absent. Added to snapshot()
  as `gpu_manager`.
- Bridge UI: the `gpu` chip folds it in — shows `off↻` when integrated-but-pending-
  re-login (dot green: this is the quiet goal), and in hybrid its title now names the
  manager mode and points at `./gpu-mode.sh integrated` for a real off.
- Tray tooltip mirrors it (`gpu awake·hybrid`, or `off↻` when armed).

Validation: 24 perf tests pass (2 new — parse+cache, and absent→None; snapshot-shape
now asserts dgpu/gpu_manager/thermald). Live: `./gpu-mode.sh query` reads
hybrid + active(0000:01:00.0) and fires the Xwayland warning; Bridge /api/perf serves
gpu_manager=hybrid; both services restarted clean.
parent f7898292
#!/bin/bash
# gpu-mode — the discrete-GPU lever for the ParVagues laptop (EnvyControl front-end).
#
# WHY this exists, separate from perf.sh: perf.sh modes (silent/cool/...) are
# INSTANT and reversible (governor, EPP, RAPL watt caps). Turning the dGPU off is
# NOT — it's a persistent config change that only takes effect after a re-login.
# On this Plasma-Wayland box Xwayland pins the RTX 2060 'active' no matter the
# runtime-PM settings, so hybrid+RTD3 never actually suspends it live; the only
# real dGPU-off is EnvyControl's 'integrated' mode (blacklists nvidia) + re-login.
# That's ~4-5 W and a chunk of idle heat reclaimed for Silent sets.
#
# Deliberately NOT in the passwordless perf-audio sudoers whitelist: this is a
# rare, considered, re-login-required switch — not something the web Bridge or the
# tray should ever fire unattended. Run it by hand, with your sudo password.
set -u
RED=$'\e[31m'; GRN=$'\e[32m'; YEL=$'\e[33m'; CYA=$'\e[36m'; BLD=$'\e[1m'; RST=$'\e[0m'
say() { printf '%s\n' "$*"; }
info() { printf '%s%s%s\n' "$CYA" "$*" "$RST"; }
ok() { printf '%s✓ %s%s\n' "$GRN" "$*" "$RST"; }
warn() { printf '%s! %s%s\n' "$YEL" "$*" "$RST"; }
die() { printf '%s✗ %s%s\n' "$RED" "$*" "$RST" >&2; exit 1; }
command -v envycontrol >/dev/null 2>&1 || die "envycontrol not installed (pacman -S envycontrol)."
# Live runtime state of the dGPU (independent of the persistent manager mode).
dgpu_runtime() {
local d cls vendor status
for d in /sys/bus/pci/devices/*; do
cls=$(cat "$d/class" 2>/dev/null) || continue
case "$cls" in 0x0300*|0x0302*) ;; *) continue ;; esac
vendor=$(cat "$d/vendor" 2>/dev/null)
[ "$vendor" = "0x8086" ] && continue # skip the Intel iGPU
status=$(cat "$d/power/runtime_status" 2>/dev/null || echo "?")
printf '%s (%s)' "$status" "$(basename "$d")"
return
done
printf 'none on PCI'
}
show_status() {
local mode; mode=$(envycontrol --query 2>/dev/null | tr -d '[:space:]')
info "GPU manager mode : ${BLD}${mode:-unknown}${RST}${CYA} (persistent; changes take effect after re-login)"
info "dGPU runtime now : ${BLD}$(dgpu_runtime)${RST}"
case "$mode" in
integrated) ok "dGPU is OFF after login — lowest heat/power. No CUDA/NVENC until you switch back to hybrid." ;;
hybrid) warn "dGPU present & driver-loaded; Xwayland keeps it 'active' on this box → run 'integrated' for a real off." ;;
nvidia) warn "dGPU-only mode — highest power. 'hybrid' or 'integrated' to save heat." ;;
esac
}
confirm() { # confirm "<prompt>" — skipped with -y
[ "${ASSUME_YES:-0}" = "1" ] && return 0
local ans; printf '%s%s%s ' "$YEL" "$1 [y/N]" "$RST"; read -r ans
case "$ans" in y|Y|yes|YES) return 0 ;; *) die "aborted." ;; esac
}
relogin_note() {
say ""
warn "Not live yet — the switch applies on the NEXT login."
say " Safest: ${BLD}reboot${RST} (guarantees the nvidia modules load/unload cleanly)."
say " Usually enough: log out and back in (Plasma → Leave → Log Out)."
say " Verify after: ${BLD}./gpu-mode.sh query${RST} and ${BLD}nvidia-smi${RST} (should fail in integrated)."
}
usage() {
cat <<EOF
${BLD}gpu-mode${RST} — discrete-GPU lever (EnvyControl front-end)
Usage: $0 <command> [-y]
Commands:
${BLD}query${RST} | -q Show manager mode + live dGPU runtime state (default)
${BLD}integrated${RST} Turn the dGPU OFF (blacklist nvidia). Coolest/quietest;
loses CUDA/NVENC until you switch back. Needs re-login.
${BLD}hybrid${RST} Restore the dGPU, runtime-D3 power-managed (--rtd3 3).
Needs re-login. (Note: Xwayland pins it 'active' live anyway.)
-y Skip the confirmation prompt (for scripting).
The switch itself runs 'sudo envycontrol ...' — you'll be asked for your password.
EOF
}
ASSUME_YES=0
CMD="query"
for a in "$@"; do
case "$a" in
-y|--yes) ASSUME_YES=1 ;;
query|-q|--query) CMD="query" ;;
integrated) CMD="integrated" ;;
hybrid) CMD="hybrid" ;;
-h|--help|help) usage; exit 0 ;;
*) die "unknown argument: $a (try -h)" ;;
esac
done
case "$CMD" in
query)
show_status
;;
integrated)
show_status; say ""
warn "This turns the dGPU OFF and blacklists the nvidia driver."
say " • Coolest/quietest state — best for Silent sets."
say " • You LOSE CUDA / NVENC (demucs & audio-ML GPU accel) until you run 'hybrid' again."
confirm "Switch to integrated (dGPU off)?"
sudo envycontrol -s integrated || die "envycontrol failed (see output above)."
ok "EnvyControl set to integrated."
relogin_note
;;
hybrid)
show_status; say ""
info "Restoring hybrid mode with aggressive runtime-D3 (--rtd3 3 = D3cold when idle)."
confirm "Switch to hybrid (dGPU back on, power-managed)?"
sudo envycontrol -s hybrid --rtd3 3 || die "envycontrol failed (see output above)."
ok "EnvyControl set to hybrid (--rtd3 3)."
relogin_note
;;
esac
...@@ -34,7 +34,7 @@ from PyQt5.QtCore import QTimer, Qt, QProcess, QRect ...@@ -34,7 +34,7 @@ from PyQt5.QtCore import QTimer, Qt, QProcess, QRect
# perf.py lives under tools/bridge. # perf.py lives under tools/bridge.
sys.path.insert(0, str(Path(__file__).resolve().parent / "tools" / "bridge")) sys.path.insert(0, str(Path(__file__).resolve().parent / "tools" / "bridge"))
from perf import (Thermals, detect_mode, MODES, SCRIPT, write_desired, # noqa: E402 from perf import (Thermals, detect_mode, MODES, SCRIPT, write_desired, # noqa: E402
dgpu_state, thermald_active, power_caps_w) dgpu_state, thermald_active, power_caps_w, gpu_manager_mode)
import launchers as LA # noqa: E402 import launchers as LA # noqa: E402
SERVICE = "perf-tray" # systemd --user unit controlling autostart SERVICE = "perf-tray" # systemd --user unit controlling autostart
...@@ -259,8 +259,17 @@ class PerfTray: ...@@ -259,8 +259,17 @@ class PerfTray:
hottest = max(cores) if cores else pkg hottest = max(cores) if cores else pkg
w = power_caps_w() w = power_caps_w()
g = dgpu_state() g = dgpu_state()
gm = gpu_manager_mode()
cap = f"{w['pl1']}/{w['pl2']}W" if w.get("pl1") is not None else "–" cap = f"{w['pl1']}/{w['pl2']}W" if w.get("pl1") is not None else "–"
gpu = "none" if not g.get("present") else ("asleep" if g.get("status") == "suspended" else "awake") if not g.get("present"):
gpu = "none"
elif gm == "integrated":
# config'd off; live status stays active until re-login (↻ = pending)
gpu = "off" if g.get("status") == "suspended" else "off↻"
else:
gpu = "asleep" if g.get("status") == "suspended" else "awake"
if gm:
gpu = f"{gpu}·{gm}"
td = "on" if thermald_active() else "off" td = "on" if thermald_active() else "off"
label = MODES.get(mode, ("?",))[0] label = MODES.get(mode, ("?",))[0]
......
...@@ -99,6 +99,34 @@ def thermald_active(): ...@@ -99,6 +99,34 @@ def thermald_active():
return False return False
# EnvyControl mode is a *persistent* GPU config (distinct from dgpu_state()'s live
# runtime power). Switching to 'integrated' powers the dGPU fully off, but only
# after a re-login — so within a login session the value never changes and we
# cache it (a re-login restarts this --user service, refreshing the cache anyway).
_GPU_MODE_CACHE = {"val": None, "ts": 0.0, "done": False}
_GPU_MODE_TTL = 300 # s
def gpu_manager_mode():
"""EnvyControl graphics mode: 'integrated' (dGPU off) | 'hybrid' | 'nvidia',
or None if envycontrol is absent/errors (the cockpit just omits the chip).
Cached for _GPU_MODE_TTL to avoid spawning envycontrol on every refresh."""
now = time.monotonic()
if _GPU_MODE_CACHE["done"] and now - _GPU_MODE_CACHE["ts"] < _GPU_MODE_TTL:
return _GPU_MODE_CACHE["val"]
val = None
try:
r = subprocess.run(["envycontrol", "--query"],
capture_output=True, text=True, timeout=5)
if r.returncode == 0:
out = (r.stdout or "").strip().lower()
val = next((m for m in ("integrated", "hybrid", "nvidia") if m in out), None)
except (OSError, subprocess.SubprocessError):
val = None
_GPU_MODE_CACHE.update(val=val, ts=now, done=True)
return val
_RAPL = "/sys/class/powercap/intel-rapl:0" _RAPL = "/sys/class/powercap/intel-rapl:0"
...@@ -369,6 +397,7 @@ def snapshot(therm: "Thermals | None" = None) -> dict: ...@@ -369,6 +397,7 @@ def snapshot(therm: "Thermals | None" = None) -> dict:
"power_w": t.power_draw_w(), "power_w": t.power_draw_w(),
"watts": power_caps_w(), "watts": power_caps_w(),
"dgpu": dgpu_state(), "dgpu": dgpu_state(),
"gpu_manager": gpu_manager_mode(),
"thermald": thermald_active(), "thermald": thermald_active(),
"script_available": script_available(), "script_available": script_available(),
"modes": {k: v[0] for k, v in MODES.items()}, "modes": {k: v[0] for k, v in MODES.items()},
......
...@@ -39,6 +39,30 @@ def test_detect_mode_silent_vs_cool(monkeypatch): ...@@ -39,6 +39,30 @@ def test_detect_mode_silent_vs_cool(monkeypatch):
assert P.detect_mode() == "cool" assert P.detect_mode() == "cool"
def test_gpu_manager_mode_parse_and_cache(monkeypatch):
import types
calls = {"n": 0}
def fake_run(cmd, **kw):
calls["n"] += 1
return types.SimpleNamespace(returncode=0, stdout="hybrid\n", stderr="")
monkeypatch.setattr(P.subprocess, "run", fake_run)
P._GPU_MODE_CACHE.update(val=None, ts=0.0, done=False)
assert P.gpu_manager_mode() == "hybrid"
assert P.gpu_manager_mode() == "hybrid" # served from cache
assert calls["n"] == 1 # only one subprocess spawn
def test_gpu_manager_mode_absent(monkeypatch):
def boom(cmd, **kw):
raise FileNotFoundError("envycontrol")
monkeypatch.setattr(P.subprocess, "run", boom)
P._GPU_MODE_CACHE.update(val=None, ts=0.0, done=False)
assert P.gpu_manager_mode() is None # feature just no-ops
def test_set_mode_rejects_unknown(): def test_set_mode_rejects_unknown():
ok, msg = P.set_mode("turbo") ok, msg = P.set_mode("turbo")
assert ok is False and "unknown mode" in msg assert ok is False and "unknown mode" in msg
...@@ -53,7 +77,8 @@ def test_set_mode_guards_missing_script(monkeypatch): ...@@ -53,7 +77,8 @@ def test_set_mode_guards_missing_script(monkeypatch):
def test_snapshot_shape(): def test_snapshot_shape():
s = P.snapshot() s = P.snapshot()
for k in ("mode", "package_c", "temp_state", "cores_c", "fans_rpm", for k in ("mode", "package_c", "temp_state", "cores_c", "fans_rpm",
"freq_max_mhz", "throttle_delta", "script_available", "modes"): "freq_max_mhz", "throttle_delta", "script_available", "modes",
"dgpu", "gpu_manager", "thermald"):
assert k in s assert k in s
assert s["mode"] in P.MODES assert s["mode"] in P.MODES
assert isinstance(s["cores_c"], list) assert isinstance(s["cores_c"], list)
......
...@@ -196,11 +196,17 @@ function paintState(s){ ...@@ -196,11 +196,17 @@ function paintState(s){
const w=s.watts||{}, cap=$("#cap"); const w=s.watts||{}, cap=$("#cap");
cap.innerHTML=`<span class="k">cap</span> ${w.pl1!=null?`${w.pl1}/${w.pl2}W`:"–"}`; cap.innerHTML=`<span class="k">cap</span> ${w.pl1!=null?`${w.pl1}/${w.pl2}W`:"–"}`;
cap.title=w.pl1!=null?`RAPL package power cap: ${w.pl1}W sustained · ${w.pl2}W burst`:"no RAPL cap readable"; cap.title=w.pl1!=null?`RAPL package power cap: ${w.pl1}W sustained · ${w.pl2}W burst`:"no RAPL cap readable";
const g=s.dgpu||{present:false}, gpu=$("#gpu"); const g=s.dgpu||{present:false}, gm=s.gpu_manager, gpu=$("#gpu");
if(g.present){ if(g.present && gm==="integrated"){
// EnvyControl config'd the dGPU off; live status stays 'active' until re-login.
const pending=g.status!=="suspended";
gpu.innerHTML=`<span class="k">gpu</span><span class="d ok"></span>off${pending?"↻":""}`;
gpu.title="EnvyControl: integrated — dGPU off"+(pending?" after re-login (still active until you log out / reboot)":"");
}else if(g.present){
const asleep=g.status==="suspended"; const asleep=g.status==="suspended";
gpu.innerHTML=`<span class="k">gpu</span><span class="d ${asleep?"ok":"warn"}"></span>${asleep?"asleep":"awake"}`; gpu.innerHTML=`<span class="k">gpu</span><span class="d ${asleep?"ok":"warn"}"></span>${asleep?"asleep":"awake"}`;
gpu.title=`discrete GPU: ${g.status} · runtime PM ${g.control}`+(asleep?"":" — a client holds it awake (check nvidia-smi)"); gpu.title=`discrete GPU: ${g.status} · runtime PM ${g.control}`+(gm?` · manager ${gm}`:"")
+(asleep?"":" — held awake (Xwayland on this box); ./gpu-mode.sh integrated for a real off");
}else{ }else{
gpu.innerHTML=`<span class="k">gpu</span><span class="d off"></span>none`; gpu.innerHTML=`<span class="k">gpu</span><span class="d off"></span>none`;
gpu.title="no discrete GPU present"; gpu.title="no discrete GPU present";
......
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