From de781186241e66af429355036af56e29e0e3fce8 Mon Sep 17 00:00:00 2001 From: Connor Johnstone Date: Mon, 14 Sep 2026 15:46:32 -0400 Subject: [PATCH] Streaming stack is on-demand, not always-on MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Measured rather than assumed: the compositor and Sunshine together hold 220 MiB on the 3060 — sway ~112, Sunshine ~108. That card also runs the local LLM, and with an 11.3 GB model resident on a 12 GB card the free headroom goes from 992 MiB to 772. Losing a fifth of what is left matters for KV cache growth, so leaving it running was the wrong default. sway-headless and sunshine no longer start at boot. `stream on` brings them up, `stream status` reports state and VRAM, and sunshine-idle-stop.timer checks every five minutes whether any Moonlight client still holds a connection to 47984/47989/48010 — stopping the stack after 15 idle minutes so a forgotten session does not sit on memory the inference stack wants. Note for future edits: `systemctl --user disable` on a unit dotbot has symlinked into ~/.config/systemd/user deletes the symlink itself, because systemd treats it as a linked unit. Re-run ./install after disabling one. --- hosts/mainframe/bin/stream | 63 +++++++++++++++++++ .../systemd/sunshine-idle-stop.service | 7 +++ .../systemd/sunshine-idle-stop.timer | 11 ++++ hosts/mainframe/systemd/sway-headless.service | 5 +- meta/hosts/mainframe.conf.yaml | 6 +- 5 files changed, 88 insertions(+), 4 deletions(-) create mode 100755 hosts/mainframe/bin/stream create mode 100644 hosts/mainframe/systemd/sunshine-idle-stop.service create mode 100644 hosts/mainframe/systemd/sunshine-idle-stop.timer 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`)