Map: GPU gate becomes a household tray toggle (r2 rationale and probes)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-23 01:40:54 +02:00
commit b489d2885d

View file

@ -7,8 +7,33 @@ cuts not yet mapped. The target's rationale is `docs/motion-pipeline-target.md`.
## Body facts
### Raven tray-toggle probes, 2026-10-22 23:30 to 23:36 UTC
Run read-only from Starfire over `ssh raven` (session 0, elevated), one
PowerShell 5.1 script copied to `C:\Users\Public` and deleted in the same
command. Nothing on Raven was changed: no task edits, no restarts, no GPU jobs.
They inform cut spec `motion-pipeline:cut_spec:cut-raven-gpu-tenancy-gate.r2`,
which replaces detection with the household's toggle
(`motion-pipeline:ruling:raven-gpu-tray-toggle`).
| Probe | Result |
|---|---|
| Who the household is on Raven | One account. `query user`: `madman's lullaby`, console, session 1, Active since 2026-10-18. The `\GameCult\Vili` task runs as that same user (`LogonType Interactive`, `RunLevel Limited`), so a tray process at that user's logon is in the same session and the same user as the daemon, with no admin rights. |
| Daemon listen address | `node.exe ... vili-daemon.mjs --host 0.0.0.0 --port 8824 --state-root E:\Projects\Vili\.vili`, pid 33776, session 1, at detached head `a2b9983`. Listens on `0.0.0.0:8824`, so the WireGuard peers can reach every route; a write route must check the caller itself. `GET http://127.0.0.1:8824/operator-state` from Raven answers in 3 ms. |
| Live task action versus the installer | The live `\GameCult\Vili` action passes `-IdunnRudpHealth 10.77.0.2:17870 -IdunnDaemon vili -IdunnHealthContract ... -IdunnHealthIntervalSeconds 15` to `start-vili-daemon.ps1`; `scripts/install-vili-task.ps1` does not. Rerunning that installer would rewrite the live task, so the tray gets its own installer. |
| Tray substrate | `powershell.exe` 5.1.26100, apartment `STA` by default; `System.Windows.Forms` and `System.Drawing` load (`v4.0.30319`, .NET Framework release 534296). Execution policy is Undefined at every scope (the task launcher passes `-ExecutionPolicy Bypass`). `scripts/run-hidden-powershell.vbs` already starts a hidden PowerShell with no console flash. No new dependency is needed. |
| Tray overflow | `HKCU:\Control Panel\NotifyIconSettings` exists with 47 entries: Windows 11 records each notify icon there and puts new ones in the overflow until the user promotes it. A fresh tray icon will not be on the taskbar until the household drags it there once (or turns it on under Settings, Personalization, Taskbar, Other system tray icons). |
| Startup folder | `C:\Users\Madman's Lullaby\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup`. Not used: from SSH (session 0) nothing in it can be started in session 1, while `Start-ScheduledTask` on an Interactive task can. |
| Scheduled tasks under `\GameCult\` | `Backup-Yggdrasil-Pull`, `ComfyUI`, `Vili`, all Ready. ComfyUI is a second possible heavy GPU consumer on Raven; the toggle governs Vili only. |
| Host free VRAM | `C:\WINDOWS\system32\nvidia-smi.exe --query-gpu=memory.free --format=csv,noheader,nounits`: 159 ms from a cold PowerShell, exit 0, free 5001 MiB at 23:3x UTC (the household or another consumer held about 11 GiB). No `nvidia-smi.exe` process was left running. |
| Vili's store | `E:\Projects\Vili\.vili\vili.service.cc`, 11810 bytes. Newest job file `vili-1792710792764.json`, written 23:20:12 UTC. The live gate (`a2b9983`) read `inUse true, probeOk true`. |
| cultcache-ts on Raven | `E:\Projects\CultLib` at `b9497d35`, package `0.14.1`; its build carries `pullAllBackingStores`. `SingleFileMessagePackBackingStore.push` reads the file, replaces one `(type, key)` entry and writes all, inside a write queue that belongs to one store instance. |
### Raven GPU tenancy probes, 2026-10-22 20:15 to 20:23 UTC
These informed r1's detection gate, which r2 deletes. The session, principal and
`nvidia-smi` rows still hold.
Run read-only from Starfire over `ssh -o HostName=192.168.178.165 raven`
(alias in `~/.ssh/config`; LAN address per `gamecult-ops/inventory.md`), as
`powershell -NoProfile -NonInteractive -EncodedCommand`. Nothing on Raven was
@ -21,56 +46,83 @@ category (compositor, WSL family, or "other").
| Session of the Vili daemon | `node.exe ... vili-daemon.mjs --host 0.0.0.0 --port 8824` runs in **session 1**, the console session of `madman's lullaby` (`query user`: console, ID 1, Active). It is the interactive desktop session, not session 0. |
| Vili task principal | `\GameCult\Vili`: `LogonType Interactive`, `RunLevel Limited`, boot trigger (`MSFT_TaskBootTrigger`), last run 2026-10-22 21:18:55 local, result 0. Children the daemon spawns inherit session 1. |
| SSH context | Windows `sshd` runs commands in **session 0**, elevated (`IsInRole(Administrator)` true). `gamecult-ops/runbooks/idunn-host-raven.md` already records that sshd cannot start anything in the user's session without a scheduled-task hop. |
| `SHQueryUserNotificationState` from session 0 | `hr=0`, state `2` (`QUNS_BUSY`) on every call, with the GPU idle (3D engine total 0.01 to 0.02 %). Session 0 has no shell, so this value says nothing about the user's desktop. The call must run in session 1, which is where the daemon and its children are. It was **not** observed from session 1: doing that without a task edit is impossible from SSH. |
| `nvidia-smi.exe` | Present at `C:\WINDOWS\system32\nvidia-smi.exe`. `--query-gpu=memory.total,memory.used,memory.free,utilization.gpu`: `16380, 1592, 14516, 2` MiB/%. 63 ms cold, about 49 ms per call (10 calls 491 ms). Per-process `--query-compute-apps` returns `[N/A]` memory under WDDM. |
| `\GPU Engine(*engtype_3D)\Utilization Percentage` | Readable from session 0 as admin; 115 instances. `Get-Counter -SampleInterval 1 -MaxSamples 1` takes 1.08 s wall, 16 ms CPU warm, 328 ms CPU on first use in a process. Summed with `Measure-Object` it needs no instance names. Not tried as a Limited token in session 1. |
| `\GPU Adapter Memory(*)\Dedicated Usage` | Readable; 1.0 s wall; 1597 MiB on the RTX 4060 Ti, 0 on the Parsec virtual adapter. |
| `\GPU Process Memory(*)\Dedicated Usage` | Readable; 1.0 s wall. Two instances above 50 MiB: the desktop compositor (1643 MiB, session 1) and one session-0 process outside the WSL family (1405 MiB). Per-process figures overlap: they sum above the adapter total. |
| WSL attribution | The Kimodo worker container was resident (`/health` ok, uptime 3454 s, 6 generations) while `vmmemWSL` (and `vmwp`) had **no** `GPU Process Memory` instance. Host counters do not attribute WSL CUDA memory to a nameable host process. WSL `nvidia-smi` reported the same `1592 MiB used, 14516 MiB free` as the host. |
| PowerShell cost | `powershell.exe` 5.1 cold start 266 ms CPU, `Add-Type` for the shell32 call 78 ms CPU, first counter read 328 ms CPU: about 0.7 s CPU per cold spawn. A warm loop iteration (QUNS + `nvidia-smi` + one 3D counter read) is about 1.1 s wall and 16 ms of its own CPU. |
| `SHQueryUserNotificationState` from session 0 | `hr=0`, state `2` (`QUNS_BUSY`) on every call, with the GPU idle. Session 0 has no shell, so this value says nothing about the user's desktop. |
| `nvidia-smi.exe` | Present at `C:\WINDOWS\system32\nvidia-smi.exe`. `--query-gpu=memory.total,memory.used,memory.free,utilization.gpu`: `16380, 1592, 14516, 2` MiB/%. 63 ms cold, about 49 ms per call. Per-process `--query-compute-apps` returns `[N/A]` memory under WDDM. |
| `\GPU Engine(*engtype_3D)\Utilization Percentage` | Readable from session 0 as admin; 115 instances; `Get-Counter` 1.08 s wall. |
| `\GPU Process Memory(*)\Dedicated Usage` | Per-process figures overlap and sum above the adapter total. |
| WSL attribution | Host counters do not attribute WSL CUDA memory to a nameable host process. Host `nvidia-smi.exe` does see a WSL container's allocation: cut report h1 measured free falling 14292 to 1866 MiB under a 12 GiB holder. |
| Adapters | `NVIDIA GeForce RTX 4060 Ti` driver 32.0.16.1088, 16 GiB; `Parsec Virtual Display Adapter`. |
What follows from these, and what does not:
### What the detection gate taught (r1, h1, h2, s1, s2)
- The daemon's own session sees the user's desktop, so the tenancy probe runs as
a child of the daemon. No fork about where it runs: the scheduled task already
puts it in the right session.
- Free VRAM must come from the host (`nvidia-smi.exe`), not from torch in WSL:
Soul's s1 probe on cut `vili-encoder-placement` showed `mem_get_info` inside a
container blind to another container's 12 GiB. Whether host `nvidia-smi` sees
a WSL container's allocation is **unverified** (the resident worker's share
could not be separated from the compositor's); the gate cut checks it first.
- The 3D-engine signal cannot be used to tell Vili's own work from the
household's by process, and need not be: Kimodo runs on CUDA (compute
engines), games on the 3D engine. Summing only `engtype_3D` leaves Vili out.
- A long-lived probe child costs about 1 ms of CPU per second at a 15 s period;
a fresh PowerShell per sample would cost about 45 ms per second. Neither
loads the GPU.
- Host free VRAM cannot tell Vili's encoder child from a household load: the
child sizes itself to the VRAM that was free (s1 `self-tenancy-refusal`).
Discounting Vili's own VRAM during a job blinds the gate to a household load
that starts mid-job (s2 `hold-blinds-household-vram`).
- Live, the probe child went silent during 2 of 5 of Vili's own encoder jobs and
the gate stayed stale after the job (s2 `own-load-starves-probe`; Raven had
770 MB of host RAM available at one stall). Every repair added a watchdog, a
drain or a baseline, and each brought its own finding.
- After a job ends, host free VRAM is back at 14.6 to 15.2 GiB at the next
2-second sample (s2), so one `nvidia-smi` read before a job, with no job
holding the slot, sees the card as the household left it.
## Model page
| Kind | Named by | Over time | Decided by |
|---|---|---|---|
| GPU tenancy state (`gpu` in `gamecult.vili.operator_state.v1`) | the daemon, one per host | rewritten every 5 s with the operator state; holds `inUse`, `since`, `checkedAt`, `probeOk` and nothing else | the daemon's tenancy gate, from host samples; never persisted as truth, rebuilt from samples at start |
| GPU permission (`gamecult.vili.gpu_permission.v1` in `.vili/vili.service.cc`, key `raven`) | the daemon, one per host | written only when the household changes it, before the change is acknowledged; read once at daemon start; absent means not allowed | the household, through the tray, through the daemon's loopback-only `POST /gpu-permission`; the daemon is its one writer |
| GPU status (`gpu` in `gamecult.vili.operator_state.v1`) | the daemon | rewritten every 5 s with the operator state; `{ allowed, changedAt, jobRunning, lastRefusal }` | derived from the permission, the slot and the last refusal; display only, never read back |
| Motion job (`.vili/jobs/<jobId>.json`) | `vili-<epoch ms>` | written once when a job ends | the daemon; a job refused at admission writes none |
| Text embedding (`.vili/embeddings/text-embeddings.cc`) | `sha256(identity, text)` | append on miss | the worker's cache |
## Rationale
- **Refuse, do not queue (recommended, pending the operator).** `/motion/generate`
is synchronous today. Holding a request open for as long as a game runs, or
adding a persisted job queue, are bigger machines than the one consumer needs:
RPS clip generation is a script an agent runs, and it can retry a typed
`gpu-busy`. A queue belongs with the authoring UX's takes, when that target
exists (`motion-pipeline:follow_up:vili-authoring-ux-target-2`).
- **Fail closed.** If the probe child dies or stops answering, the gate reports
in use. A wrongly deferred clip costs minutes; a wrongly admitted encoder child
pages a game's VRAM and thrashes Raven.
- **The household decides; nothing detects.** The operator replaced detection
with a tray toggle (`motion-pipeline:ruling:raven-gpu-tray-toggle`). The
detection gate's findings were the cost of guessing intent from counters; a
yes from the person at the desk needs no guess, no hysteresis and no probe.
- **The daemon is the one owner of the permission; the tray is a lowering.**
The tray reads and sets the state only through the daemon's HTTP route, so
admission and the stored value cannot disagree and the tray holds no state of
its own. The alternative, the tray writing a CultCache document the daemon
reads, would give the document two writers on two clocks and make the daemon
poll a file to learn its own admission rule.
- **Off on a fresh install, and off when the document is missing.** The safe
default for the household: Vili does nothing on Raven's GPU until someone at
the desk says yes. A stored yes survives restarts and reboots.
- **Loopback only for the write.** The daemon listens on `0.0.0.0`; the
household's switch must not be flippable from a WireGuard peer. A request
from a non-loopback address is refused 403. Agents read the state through
`GET /gpu-permission` or `/operator-state` and never set it.
- **One cache instance for the daemon.** The 5-second surface writer built a
new store per call. Two store instances writing one file each read, modify and
write in their own queue, so a permission write could be lost under a
concurrent surface write. One module-level cache serializes every write to
`vili.service.cc`.
- **One slot, one fresh VRAM read, fail closed.** h2's slot stays: one job at a
time, the rest refused at once with source `slot`. The slot is taken
synchronously before the VRAM read, so two requests cannot both pass it. A
failed or unparseable read refuses with source `vram`, so a job never starts
onto a full card on a guess. The permission is checked again after the read.
- **Refuse, do not queue** (`motion-pipeline:ruling:gpu-defer-refuse-typed`).
`/motion/generate` is synchronous; the consumer retries a typed `gpu-busy`
after `retry-after`. A queue belongs with the authoring UX's takes
(`motion-pipeline:follow_up:vili-authoring-ux-target-2`).
- **A logon task, not the Startup folder.** `Start-ScheduledTask` on an
Interactive task puts the tray in the household's session from SSH at deploy
time; a Startup folder entry would wait for the next logon. The task pattern
matches `\GameCult\Vili`.
- **The privacy rule is moot.** "Never record program names" protected the
household from a probe that watched its programs. With no probe, nothing in
Vili observes the household's programs; the only household fact Vili holds is
the yes or no the household chose.
- **The child's own VRAM check is placement, not tenancy.** `plan_placement`
still sizes accelerate's `max_memory` from torch's view and can still refuse;
it answers "can the encoder be placed", not "may heavy work start".
still sizes accelerate's `max_memory` from torch's view and can still refuse
(source `placement`); it answers "can the encoder be placed", not "may heavy
work start".
## Cuts not yet mapped
- RPS beats through Vili (`motion-pipeline:follow_up:rps-clips-from-vili`), after
the gate and `vili-fbx-export`.
the gate and `vili-fbx-export`. They wait on the household's yes on Raven.