diff --git a/hosts/mainframe/bin/stream b/hosts/mainframe/bin/stream new file mode 100755 index 0000000..6e7ddac --- /dev/null +++ b/hosts/mainframe/bin/stream @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Start and stop the Moonlight streaming stack on demand. +# +# It is not left running because it costs ~220 MiB of VRAM on the 3060 (sway +# ~112, Sunshine ~108) and that card also holds the local LLM. With an 11.3 GB +# model resident on a 12 GB card, 220 MiB is roughly a fifth of the remaining +# headroom — enough to matter for KV cache growth. +set -euo pipefail + +UNIT=sway-headless.service +IDLE_MINUTES="${STREAM_IDLE_MINUTES:-15}" +STAMP="${XDG_RUNTIME_DIR:-/tmp}/stream.lastseen" + +# A Moonlight client holds connections to the control and RTSP ports. +clients_connected() { + ss -Htn state established '( sport = :47984 or sport = :47989 or sport = :48010 )' 2>/dev/null | grep -q . +} + +vram() { + local used total + read -r used total < <(nvidia-smi --query-gpu=memory.used,memory.total \ + --format=csv,noheader,nounits 2>/dev/null | tr -d ',') + printf '%s MiB used of %s, %s free' "$used" "$total" "$((total - used))" +} + +case "${1:-status}" in + on|start) + systemctl --user start "$UNIT" + sleep 6 + date +%s > "$STAMP" + printf 'streaming up — pair or connect at https://%s:47990\n' "$(uname -n)" + printf 'GPU: %s\n' "$(vram)" + ;; + off|stop) + systemctl --user stop "$UNIT" + sleep 3 + rm -f "$STAMP" + printf 'streaming down\nGPU: %s\n' "$(vram)" + ;; + status) + printf 'compositor: %s\nsunshine: %s\nclients: %s\nGPU: %s\n' \ + "$(systemctl --user is-active "$UNIT")" \ + "$(systemctl --user is-active sunshine.service)" \ + "$(clients_connected && echo connected || echo none)" \ + "$(vram)" + ;; + --idle-check) + # Called from sunshine-idle-stop.timer. Releases the GPU when nobody has + # been connected for IDLE_MINUTES, so a forgotten session does not sit on + # VRAM the inference stack wants. + systemctl --user is-active --quiet "$UNIT" || exit 0 + if clients_connected; then date +%s > "$STAMP"; exit 0; fi + last=$(cat "$STAMP" 2>/dev/null || echo 0) + if (( last == 0 )); then date +%s > "$STAMP"; exit 0; fi + if (( ($(date +%s) - last) >= IDLE_MINUTES * 60 )); then + echo "no Moonlight client for ${IDLE_MINUTES}m; stopping to free the GPU" + systemctl --user stop "$UNIT" + rm -f "$STAMP" + fi + ;; + *) + echo "usage: stream [on|off|status]" >&2; exit 2 ;; +esac diff --git a/hosts/mainframe/systemd/sunshine-idle-stop.service b/hosts/mainframe/systemd/sunshine-idle-stop.service new file mode 100644 index 0000000..a95de32 --- /dev/null +++ b/hosts/mainframe/systemd/sunshine-idle-stop.service @@ -0,0 +1,7 @@ +[Unit] +Description=Release the GPU when no Moonlight client is connected +ConditionHost=mainframe + +[Service] +Type=oneshot +ExecStart=%h/.dotfiles/hosts/mainframe/bin/stream --idle-check diff --git a/hosts/mainframe/systemd/sunshine-idle-stop.timer b/hosts/mainframe/systemd/sunshine-idle-stop.timer new file mode 100644 index 0000000..a096da0 --- /dev/null +++ b/hosts/mainframe/systemd/sunshine-idle-stop.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Check every 5 minutes whether the streaming stack can be shut down +ConditionHost=mainframe + +[Timer] +OnBootSec=5min +OnUnitActiveSec=5min +AccuracySec=30s + +[Install] +WantedBy=timers.target diff --git a/hosts/mainframe/systemd/sway-headless.service b/hosts/mainframe/systemd/sway-headless.service index 6ea0b31..25f9beb 100644 --- a/hosts/mainframe/systemd/sway-headless.service +++ b/hosts/mainframe/systemd/sway-headless.service @@ -18,5 +18,6 @@ ExecStopPost=/usr/bin/systemctl --user stop sunshine.service Restart=on-failure RestartSec=5s -[Install] -WantedBy=default.target +# Deliberately NOT WantedBy=default.target. The 3060 also holds the local LLM, +# and this stack costs ~220 MiB of VRAM. Start it with `stream on`; the +# idle-stop timer puts it away again. diff --git a/meta/hosts/mainframe.conf.yaml b/meta/hosts/mainframe.conf.yaml index 38b1b95..8a2e7d2 100644 --- a/meta/hosts/mainframe.conf.yaml +++ b/meta/hosts/mainframe.conf.yaml @@ -12,6 +12,8 @@ - link: ~/.config/sway/headless.conf: hosts/mainframe/sway/headless.conf ~/.config/systemd/user/sway-headless.service: hosts/mainframe/systemd/sway-headless.service + ~/.config/systemd/user/sunshine-idle-stop.service: hosts/mainframe/systemd/sunshine-idle-stop.service + ~/.config/systemd/user/sunshine-idle-stop.timer: hosts/mainframe/systemd/sunshine-idle-stop.timer ~/.config/systemd/user/sunshine.service: hosts/mainframe/systemd/sunshine.service ~/.config/inbox-tidy: hosts/mainframe/config/inbox-tidy ~/.config/llmfit: hosts/mainframe/config/llmfit @@ -41,5 +43,5 @@ description: Reloading user units - command: systemctl --user enable --now inbox-tidy.timer movie-recs.timer rent-utilities.timer ddns.timer description: Enabling the singleton timers (mainframe only) - - command: systemctl --user enable sway-headless.service sunshine.service - description: Enabling the headless compositor and Sunshine (not started here — it takes the GPU) + - command: systemctl --user enable --now sunshine-idle-stop.timer + description: Enabling the streaming idle-stop watchdog (the stack itself starts on demand via `stream on`)