The problem
07 — Dual-Use: Inference Server ⇄ Gaming/Desktop
Turning a headless AI box into a desktop (or gaming machine) on demand — udev-driven auto-switching between serving models and using the GPUs interactively.
The problem
A local AI machine idles its GPUs serving models — which is great, except when you want to use the machine: to play a game, do some desktop work, or just look at it. Manually stopping the inference servers, starting the display manager, then reversing it later is a pain and easy to forget (a desktop compositor eating 1–2 GB of VRAM while the models are resident is a subtle way to get OOMs under load).
The fix on this box: a USB monitor switcher (a small USB device you plug in when you want the desktop) drives a udev rule that toggles the machine’s whole role automatically. Plug it in → headless inference stops and the desktop starts. Pull it out → the desktop stops and inference comes back. No systemctl juggling, nothing to remember.
How it works
Three small pieces:
| Piece | What it is |
|---|---|
99-usb-switcher.rules | udev rule in /etc/udev/rules.d/ matching the switcher’s USB vendor/product ID, on add and remove |
usb-switcher-on.sh | runs on plug-in: stops inference, starts the display manager, switches the monitor input |
usb-switcher-off.sh | runs on pull-out: the reverse |
The udev rule identifies the device by fingerprint, so nothing else plugged into the box can trigger the switch:
ACTION=="add", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="0610", \
RUN+="/usr/local/bin/usb-switcher-on.sh"
ACTION=="remove", SUBSYSTEM=="usb", ENV{DEVTYPE}=="usb_device", \
ATTRS{idVendor}=="05e3", ATTRS{idProduct}=="0610", \
RUN+="/usr/local/bin/usb-switcher-off.sh"
Find your device’s IDs with lsusb while it’s plugged in.
The on-script (headless → desktop)
#!/bin/bash
# Runs as root via udev — use absolute paths only
LOG=/var/log/usb-switcher.log
echo "[$(date '+%F %T')] switcher connected" >> "$LOG"
/usr/bin/systemctl stop llama-primary.service 2>>"$LOG"
/usr/bin/systemctl stop llama-secondary.service 2>>"$LOG"
/usr/bin/systemctl start gdm.service 2>>"$LOG"
# Switch the monitor to the DisplayPort/USB-C input
/usr/bin/ddcutil setvcp 60 0x1b 2>>"$LOG" || true
The off-script mirrors this: stop gdm, start both llama units, ddcutil setvcp 60 0x11 back to the other input.
Tips & tricks learned from running it
- Ordering matters. On plug-in, stop the inference servers before starting the display manager — free the GPUs first. On pull-out, stop the display manager before starting the servers — avoid a window where both want the GPUs. A GPU contention window here is the kind of thing that manifests as a flaky X session or a model failing to load.
- Inference services must be ordinary startable units. This whole design works because the llama servers are plain systemd units anyone can start/stop, rather than pinned to
always-on. The dual-use scripts start and stop them directly. - udev runs in a minimal environment. The scripts must use absolute paths (
/usr/bin/systemctl, notsystemctl) — a relative path that works in your shell will silently fail under udev. - Use
|| truefor the cosmetic part.ddcutil setvcp(which switches the monitor’s active input over DDC/CI) can fail if the monitor or the DRM context isn’t reachable; a failed input switch is logged but non-fatal, so the rest of the transition still happens. Verify your monitor’s VCP input values withddcutil detect—0x1b/0x11are device-specific. - Log every transition. Appending timestamped lines (plus stderr of each command) to
/var/log/usb-switcher.logmakes “the machine was in the wrong state” trivial to diagnose — you can see exactly which step ran and which failed. - The default state should be the server.
WantedBy=multi-user.target+ the display manager stopped at boot means a plain reboot returns the box to headless inference — the desktop is the exception you opt into with the plug. - If you don’t have a USB switcher, the same two scripts work behind any trigger: a wall switch on a USB port, a
systemdpath unit on a file, or just a pair of shell aliases. The device fingerprint in the udev rule is what makes it hands-free.
State model
plug in switcher (add) pull out switcher (remove)
┌────────────────────────────────────────┐ ┌────────────────────────────────────────┐
│ HEADLESS INFERENCING (default) │ │ DESKTOP / GAMING (interactive) │
│ • gdm STOPPED │ │ • gdm RUNNING │
│ • llama-primary RUNNING │ │ • llama-primary STOPPED │
│ • llama-secondary RUNNING │ │ • llama-secondary STOPPED │
└────────────────────────────────────────┘ └────────────────────────────────────────┘
Dependencies to verify on your own box
- The display manager unit (
gdm.servicehere — could belightdm,sddm, …) is what “desktop” means on your system. ddcutilis installed and can reach the monitor over DDC/CI (usually needs a DRM/X context).- Your monitor’s input-source VCP numbering — check with
ddcutil detect. - The USB vendor/product ID of whatever device you use as the trigger.
Related
- 01 — Architecture — why the inference servers are ordinary, individually-startable units
- 03 — Primary vs Secondary — what gets stopped/started by the switch
Source Disclaimer
- AI Generated
- Human Generated
- AI Edited
- Human Edited