Self-hosted AI services for ONE host: Arch Linux, RTX 5090 + RTX 4070 Ti, rootless Podman Quadlets. Every service starts on demand; at boot only the monitoring tier and the default strata variant (5090, 1M context) start.
This repo packages the AI lab of the ai box as Podman Quadlets (declarative
container units managed by systemd --user). services.json is the single
source of truth (ports, cards, auth); scripts/ai-lab and the Ryoku bar widget
(plugin/ailab) start and stop services; scripts/gpu-arbiter.py keeps the
GPU rules when they do.
┌──────────────────────────────────────────────────────────────────┐
│ Host "ai" (Arch Linux, desktop on the iGPU) │
│ │
│ systemd-ai ── Podman network (containers reach each other by │
│ container name) │
│ model APIs (1143x) web apps (31xx) │
│ ├── strata 0.0.0.0:11434 ├── open-webui :3100 │
│ ├── llama-cpp-5090 :11435 ├── comfyui :3101 │
│ ├── llama-cpp-4070ti :11436 ├── sketchlab :3102 │
│ ├── rizzo :11437 ├── hyperframes :3103 │
│ └── llama-cpp-both :11438 ├── hermes :3104 (opt-in)
│ ├── dsh 127.0.0.1:3105 (opt-in)
│ └── docling 127.0.0.1:3108
│ RTX 5090 32 GB + RTX 4070 Ti 12 GB, shared by the rules in │
│ docs/gpu-assignment.md. Caddy files are kept, not deployed. │
└──────────────────────────────────────────────────────────────────┘
| Service (unit) | Port | Card | Auth | Description |
|---|---|---|---|---|
strata-5090 / -4070ti / -both |
11434 |
per variant | API key | Strata (Qwen) OpenAI-compatible API, one variant at a time |
maya-5090-256 / -512 / -1024, maya-both-256 / -512 / -1024 |
11440 |
per variant (exclusive) | none by default | Project Maya (GLM-5.3-Flash) dashboard + OpenAI/Anthropic API, one variant at a time |
llama-cpp-5090 |
11435 |
5090 | API key | llama.cpp router, 262K context default |
llama-cpp-4070ti |
11436 |
4070 Ti | API key | llama.cpp router, 65K context default |
llama-cpp-both |
11438 |
both (exclusive) | API key | llama.cpp router, layer split over both cards |
rizzo |
11437 |
4070 Ti | none | Rizzo Flow, Jev-compatible System One decisions |
open-webui |
3100 |
- | account | chat frontend over strata + the three llama.cpp servers |
comfyui-5090 / -4070ti |
3101 |
the card strata is not on | none | image generation |
sketchlab |
3102 |
- | none | diagramming SPA; same-origin /v1 proxy to strata (key server-side) |
hyperframes |
3103 |
- | none | HyperFrames GCP Cloud Run worker (needs a GCS bucket; local renders: scripts/hyperframes-render.sh) |
hermes (opt-in) |
3104 |
- | account | containerized Hermes gateway; this host runs Hermes natively instead |
deepseek-harness (opt-in) |
127.0.0.1:3105 |
- | token | dsh agent runtime, host loopback only |
docling-cpu / -4070ti |
127.0.0.1:3108 |
- / 4070 Ti | none | docling-serve document conversion API (the docling MCP's remote backend), loopback only |
embeddings |
127.0.0.1:3109 |
- | API key (llama-cpp keys.txt) | llama.cpp CPU server + bge-m3 Q8_0 (dense 1024); the memory stack's embedder, own model dir (never a card dir) |
openviking |
3110 |
- | root API key | OpenViking memory server (shared Hermes memory provider; VLM=strata, embeddings=systemd-embeddings) |
honcho-database |
none (ai.network) | - | password | PostgreSQL 15 + pgvector for Honcho |
honcho-api |
3111 |
- | JWT | Honcho API (AUTH_USE_AUTH=true; LLM=strata, embeddings=systemd-embeddings) |
honcho-deriver |
none (ai.network) | - | none | Honcho background worker (memory extraction; /metrics on 9090 in-network) |
prometheus |
127.0.0.1:3107 |
- | none | metrics store; scrapes every service, OTLP receiver for Open WebUI; boot |
grafana |
3106 |
- | account | dashboards (provisioned from monitoring/), LAN with login; on demand |
node-exporter |
127.0.0.1:9100 |
- | none | host CPU/RAM/disk/net/temp; boot |
gpu-exporter |
127.0.0.1:9835 |
- | none | both cards via NVML (UUID labels, holds no VRAM); boot |
podman-exporter |
127.0.0.1:9882 |
- | none | container state/stats via the user podman socket; boot |
blackbox-exporter |
127.0.0.1:9115 |
- | none | HTTP probes of every service health URL; boot |
victorialogs |
127.0.0.1:9428 |
- | none | log store (30d/5GB); boot |
fluent-bit |
127.0.0.1:2020 |
- | none | ships px's user journal to VictoriaLogs; boot |
Containers are named systemd-<service> (both GPU variants of strata and
ComfyUI share systemd-strata / systemd-comfyui). Published ports bind
0.0.0.0 (plain HTTP; trusted LAN only, never port-forwarded) except dsh.
git clone git@github.com:dark5un/ai-lab-quadlets.git ~/workspace/github.com/dark5un/ai-lab-quadlets
cd ~/workspace/github.com/dark5un/ai-lab-quadlets
./install.sh # configs, units, images; nothing started, nothing at boot
./install.sh --no-images # configs + units only (fast; re-run after editing a quadlet)install.sh is written for this host (it refuses to run unless nvidia-smi
lists an RTX 5090 and an RTX 4070 Ti) and is idempotent. It:
- checks podman (rootless), nvidia-smi, python3, openssl, and hints at
sudo pacman -S python-huggingface-hubifhfis missing; - writes configs to
~/.config/containers/config(dirs 700, files 600): secrets viascripts/generate-secrets.sh, eachconfig/<svc>/*.examplecopied once, and the managed lines rewritten every run: Open WebUI's four backends + keys andWEBUI_URL(http://$(hostnamectl --static).local:3100, mDNS by systemd-resolved), sketchlab's strata key, dsh's keys (if opted in), the memory group's keys (ov.confrendered fromconfig/openviking/ ov.conf.example; honcho'sLLM_OPENAI_API_KEY+EMBEDDING_API_KEY); - runs
scripts/install-strata.sh(strata image if missing + its 3 units); - renders the units (
scripts/render-units.sh, GPU UUIDs from nvidia-smi), removes legacy units, maskspodman-user-wait-network-online, reloads once; - creates the data dirs and links
~/.local/bin/ai-lab+~/.local/bin/hf-download; - pulls/builds images one at a time (
scripts/build-images.sh;--rebuildrebuilds the local ones). Sketchlab builds from~/workspace/github.com/dark5un/sketchlab.app, HyperFrames from~/workspace/github.com/heygen-com/hyperframes.
Open WebUI runs with ENABLE_PERSISTENT_CONFIG=false (managed line): its
env file is authoritative, so the backends and keys always come from
service.env and a key rotation reaches it. Settings changed in the admin UI
are not persisted and reset on the next restart; change the env file (or
install.sh) instead. Accounts and chats live in the open-webui-data
volume and are kept.
Opt-ins: --with-deepseek-harness, --with-hermes. ./uninstall.sh stops
everything and removes units, the bar plugin and the ~/.local/bin links;
data and configs stay.
ai-lab status # every service: state, port, health
ai-lab start open-webui # start (GPU rules applied); --dry-run shows the plan
ai-lab stop comfyui # groups work: strata, comfyui
ai-lab strata 5090 # 5090 | 4070ti | both | off
ai-lab docling cpu # docling-serve: cpu | 4070ti | off
ai-lab url sketchlab # browser URLThe AI Lab bar widget (plugin/ailab, Ryoku QS Bar) does the same with one
click per service and a strata selector. Never systemctl --user start a GPU
unit directly: that bypasses the arbiter (strata's Conflicts= then stops
strata instead of moving it).
scripts/reset-comfyui.sh [--dry-run] [--backup] wipes ComfyUI's
settings/DB/caches and keeps models, inputs, outputs and custom nodes.
scripts/rotate-secrets.sh [--yes] rotates the Open WebUI, strata and
llama.cpp secrets and rewrites every client's copy.
Strata runs as one variant at a time, all on port 11434 with the same
container name (systemd-strata) and API key; a card and a context make the
variant. Past the trained 262,144 tokens, Strata's setup adds YaRN rope
scaling (x2 at 524K, x4 at 1M), fixed per engine start, so each context is its
own unit with its own setup config. The KV cache streams from RAM (32K tokens
resident in VRAM), so a bigger window costs mostly RAM.
| variant | GPU(s) | context | model |
|---|---|---|---|
strata-5090, -5090-524k, -5090-1m |
RTX 5090 32 GB | 256K / 524K / 1M | IQ3_S |
strata-4070ti, -4070ti-524k, -4070ti-1m |
RTX 4070 Ti 12 GB | 256K / 524K / 1M | IQ3_XXS |
strata-both |
5090 + 4070 Ti | 256K | IQ3_XXS (layer split; slower than the 5090 alone, so no bigger contexts) |
The coder group (Qwen3.8 Coder IQ1_M on the 4070 Ti, port 11439, container
systemd-strata-coder) runs one variant at a time BESIDE a 5090 variant
("duo"): strata-coder-128k (128K, 4 batch slots), strata-coder (256K, 2
slots), strata-coder-524k, strata-coder-1m. It owns
the 4070 Ti: strata on that card or any other 4070 Ti service stops it.
Batch slots ("parallel": N in the config, Strata's docs/BATCHING.md) decode
up to N conversations together: nobody waits for a whole answer. Measured on
this box: 256K+2 costs nothing solo and beats the queue for 2 clients; 256K+4
starves the expert cache (slower than queueing); 128K+4 is the concurrency
pick (4 clients in ~7 s). 524K/1M stay at one at a time (slots there cost
6-12 GB pinned RAM each).
./scripts/install-strata.sh (run by install.sh) builds localhost/strata-harness:multi from
~/workspace/github.com/dark5un/strata-harness (the harness fork; every
strata variant runs it and serves the agent tab at /harness) for CUDA 120 + 89 (if missing;
--rebuild forces it), renders the units with the GPU UUIDs and CPU pinning,
derives the 524K / 1M configs of the 4070 Ti and the coder from their 256K
ones (context + YaRN args only; identical to what setup writes), derives the
128K coder config from the 256K one (context only, no YaRN), sets the coder's
batch slots ("parallel": 2 at 256K, 4 at 128K), and reloads
systemd. Only the default variant strata-5090-1m starts at boot ([Install]);
every other variant stays on demand — switch with the AI Lab bar
widget or:
./scripts/ai-lab strata 4070ti-1m # 5090[-524k|-1m] | 4070ti[-524k|-1m] | both | off; --dry-run previews
./scripts/ai-lab coder 128k # 128k | 256k | 524k | 1m | off (the coder, beside a 5090 variant)
./scripts/ai-lab strata duo 5090-1m 524k # a 5090 variant + the coder
./scripts/ai-lab strata # prints the live variant, e.g. 5090-1m+coder-524kWhen strata has to leave its card (a GPU service starts there, or the coder starts), scripts/gpu-arbiter.py moves it to the other card at the same context (5090-1m <-> 4070ti-1m); strata-both drops to the plain card variant.
Data: the model files (~/.local/share/strata/{models,mtp,packs}, ~86 GB) are
shared; each variant keeps its own setup in config-<variant>/strata-<quant>.json
(KV/context ladder, card choice AND the API key, which wins over the env
API_KEY). A variant with an empty config dir runs Strata's setup on its
first start from the unit's CONTEXT/KV/GPU(S) env; delete the json to
re-run it. Shared settings and the key live in
~/.config/containers/config/strata/service.env (mode 600). The harness tab's
per-session trusted workspaces (M9) mount from ~/.local/share/strata/workspaces
to /data/workspaces in every variant — place project dirs into that tree to
share them with a session; nothing else on the host is visible to the agent.
Every variant also sets STRATA_HARNESS_ROOTS=/data/workspaces: a harness
session created without picked folders is fenced to that tree from the first
message (pick folders in the tab's workspace chip to override per session).
Upgrading Strata (last: 6f32ec0 / engine 0.1.39 -> 6674a00 / 0.1.40.4, 2026-10-08; decode/prefill within noise of the old build, strata-both decode +7%):
podman tag localhost/strata-harness:multi localhost/strata-harness:multi-<old-commit> # rollback point
for v in 5090 4070ti both; do d=~/.local/share/strata/config-$v
cp -p $d/strata-iq3_s.json $d/strata-iq3_s.json.<old-commit>; done
git -C ~/workspace/github.com/Niko1221/Strata pull --ff-only
./scripts/install-strata.sh --rebuild
rm ~/.local/share/strata/config-*/strata-iq3_s.json # re-run setup: new defaults
./scripts/ai-lab strata 5090 && ./scripts/ai-lab strata 4070ti && ./scripts/ai-lab strata both
chmod 600 ~/.local/share/strata/config-*/strata-iq3_s.json # setup writes 644Re-running setup reuses the model data (no download) and keeps the API key
(setup takes it from service.env); it only refreshes the json's engine args.
Upstream rewrote its git history on 2026-10-06 (#1276): a clone from before
that cannot pull; move it with git branch pre-cleanup-backup && git checkout -B main origin/main (done here). Rollback: retag the old image to :multi
and restore the saved jsons.
GPU pinning: the strata units add the CDI device by UUID
(AddDevice=nvidia.com/gpu=GPU-…) so nvidia-smi (which Strata's setup reads)
and CUDA see the same cards in the same order. This is safe here because the
CDI spec is /var/run/cdi/nvidia.yaml, regenerated every boot by
nvidia-cdi-refresh; a static /etc/cdi/nvidia.yaml can go stale when
/dev/nvidiaN minors reshuffle, which is why the other services pin with
CUDA_VISIBLE_DEVICES=<uuid> instead.
Maya (GLM-5.3-Flash, the Project Maya quant family) runs as one variant at a
time on port 11440 with the container name systemd-maya. A card and a
context make the variant; every variant is fully exclusive — starting any
maya stops every strata instance (and vice versa), because Maya pins nearly
all free RAM (~70 GB) for its expert tier and fills the card(s) it runs on.
| variant | GPU(s) | context | pack |
|---|---|---|---|
maya-5090-256, -512, -1024 |
RTX 5090 32 GB | 256K / 512K / 1M | Maya-S-v2 IQ2_XXS |
maya-both-256, -512, -1024 |
5090 + 4070 Ti (layer split) | 256K / 512K / 1M | Maya-S24 |
There is no 4070 Ti variant: the S-packs' working set (~43 GB) does not fit 12 GB. Measured on this box (v1.1.2): maya-both S24 30-37 tok/s, expert cache 67-70%; maya-5090 S-v2 ~29 tok/s. S24 keeps ~1.5 GB less on-GPU, so the 5090 alone holds more experts; at 1M the S24 split needs the 4070 Ti's ~11.5 GB.
./scripts/install-maya.sh writes the per-context configs
(~/src/project-maya/maya-maya-<variant>.json, copies of the installed base
configs with only --max-context patched) and renders the units with the GPU
UUIDs. Nothing starts at boot. Switch with the AI Lab bar widget or:
./scripts/ai-lab maya 512 # the 5090 at 512K (S-v2)
./scripts/ai-lab maya both 1024 # both cards at 1M (S24)
./scripts/ai-lab maya off # stop the group
./scripts/ai-lab maya # prints the live variant, e.g. both-1024The units set StopTimeout=120: Maya saves its sessions on stop (a 164K-token
conversation takes ~15-30 s), and podman's 10 s default SIGTERM window would
end every switch in SIGKILL (exit 137, unit "failed"). The engine binary is
compiled inside the localhost/maya-runtime:latest container from the
bind-mounted ~/src/project-maya checkout (./setup.sh --setup --rebuild
there); after a git pull in that repo, rebuild in-container and the next
variant start picks it up.
Strata has priority: it is the only service that owns a card. ai-lab start|stop|toggle|strata and the bar widget go through
scripts/gpu-arbiter.py, which applies:
| service | card |
|---|---|
| strata-5090 / -4070ti / -both | 5090 / 4070 Ti / both |
| llama-cpp-5090 | 5090 |
| llama-cpp-4070ti, rizzo | 4070 Ti |
| llama-cpp-both (exclusive) | both (layer split, 5090 main) |
| maya-* (exclusive) | 5090 or both; any maya start stops every strata instance and vice versa |
comfyui (comfyui-5090 / comfyui-4070ti) |
the card strata is NOT on (strata off: 4070 Ti) |
| docling-4070ti | 4070 Ti |
- Starting a GPU service on the card strata occupies moves strata to the other card (strata-both drops to the other card). Strata off stays off.
- Starting a single-card strata variant stops every other service on that card, except ComfyUI, which moves to the other card.
- Starting strata-both stops every GPU service.
- Starting llama-cpp-both (exclusive) stops every other GPU service, strata included; starting any other GPU service stops llama-cpp-both first.
- Other GPU services may share a card with each other.
ai-lab start <name> --dry-run prints the plan. The strata units' Conflicts=
is only a backstop: a bare systemctl --user start of a conflicting unit
stops the other side instead of moving it.
Host API: http://<host-LAN-IP>:11434/v1 (the unit publishes on 0.0.0.0;
every request must carry the API key — from the host itself use
http://127.0.0.1:11434/v1). From another container on systemd-ai, use
http://systemd-strata:8080/v1; configure the key from
~/.config/containers/config/strata/service.env in that client. Model data is
kept under ~/.local/share/strata.
Strata defaults to Qwen IQ3_S at a 262,144-token context for every model setup.
Monitor RAM/VRAM on first run because long-context KV cache raises memory use.
After install, verify the services are up:
podman ps
LAN_IP=$(ip -4 route get 1.1.1.1 | awk '{for (i=1; i<=NF; i++) if ($i=="src") {print $(i+1); exit}}')
curl -sS -o /dev/null -w 'Open WebUI: %{http_code}\n' "http://${LAN_IP}:3100/"Auth status mirrors the "auth" field in services.json
(none | api-key | account | token).
| Service | URL | Auth |
|---|---|---|
| Open WebUI | http://<host-LAN-IP>:3100 |
account (login required) |
| ComfyUI | http://<host-LAN-IP>:3101 |
none |
| Containerized Hermes Gateway (optional) | http://<host-LAN-IP>:3104 |
account |
| Sketch Lab | http://<host-LAN-IP>:3102 |
none (proxies Strata on /v1/, key server-side) |
| DeepSeek Harness (optional) | http://127.0.0.1:3105 (loopback only) |
one-time token |
| llama.cpp APIs | http://<host-LAN-IP>:11435/v1 (5090), :11436 (4070 Ti), :11438 (both) |
API key (~/.config/containers/config/llama-cpp/keys.txt) |
| Rizzo | http://<host-LAN-IP>:11437/v1/systemone |
none |
| Strata (optional) | http://<host-LAN-IP>:11434/v1 |
API key |
| HyperFrames API | http://<host-LAN-IP>:3103 |
none |
| docling-serve | http://127.0.0.1:3108 (loopback only; UI at /ui) |
none |
| Embeddings (memory stack) | http://127.0.0.1:3109/v1 (loopback only) |
API key (llama-cpp keys.txt) |
| OpenViking (memory stack) | http://<host>:3110 (tailnet only: ufw rules on tailscale0) |
root API key (config/openviking/service.env) |
| Honcho (memory stack) | http://<host>:3111 (tailnet only: ufw rules on tailscale0) |
JWT (config/honcho/service.env AUTH_JWT_SECRET) |
LAN exposure decision (deliberate): this stack runs on a trusted home LAN.
The unauthenticated endpoints (ComfyUI, Sketch Lab, HyperFrames, Rizzo) are
intentionally reachable from the LAN for convenience; they are
never port-forwarded to the Internet. Strata (11434) and the llama.cpp servers
are likewise LAN-bound but key-gated (llama.cpp's /health answers without a
key). Strata: every request must carry the API key from
~/.config/containers/config/strata/service.env. If the trust model changes,
rebind strata to 127.0.0.1 in services.json + quadlets and expose it
per-device via a VPN instead.
The container serves the UI/API but the default installer does not download
checkpoint weights. Put compatible checkpoints under
~/.local/share/comfyui/models/checkpoints/ (plus any workflow-specific VAE,
text encoder, or LoRA files in their matching models/ subdirectories), then
refresh the model list in ComfyUI. Check backend readiness at
http://<host-LAN-IP>:3101/system_stats; an empty checkpoints directory
means workflows cannot run yet. The CUDA image can expose multiple NVIDIA GPUs;
its torch.cuda runtime selects CUDA devices, while llama.cpp has its own
per-service GPU assignment.
ufw is active on this host and opens only the ports that should be reachable from other machines (today: 11434 strata, 3101 ComfyUI); everything else answers on the host itself. Open a port per service, explicitly:
sudo ufw allow 3100/tcp # LAN + tailnet
sudo ufw allow in on tailscale0 to any port 11434 proto tcp # tailnet only
sudo ufw allow in on tailscale0 to any port 3110 proto tcp # OpenViking (Mac Hermes)
sudo ufw allow in on tailscale0 to any port 3111 proto tcp # Honcho (Mac Hermes)DeepSeek Harness stays on loopback and never needs a rule. The memory stack (embeddings/openviking/honcho-*) is CPU-only and never touches the GPU arbiter; honcho-database and honcho-deriver have no host port at all.
The installer deliberately does not deploy or start Caddy, install mkcert, or
modify certificate trust. The source files (quadlets/caddy.container and
config/caddy/) and host Caddy configuration/data are retained for manual use.
Default app services are plain HTTP on their direct host ports; do not expose
them outside a trusted network.
./install.sh --no-images recreates every config and data dir.
The Podman network systemd-ai is created by the ai.network quadlet. If it's missing:
systemctl --user daemon-reload
systemctl --user restart ai-network.serviceThe container is built from containers/deepseek-harness/Containerfile, which
packages the official @deepseek-ai/dsh npm package unpatched, started as
stock dsh web (no interpreter wrapper). The entrypoint rewrites the web
profile's patchReload from live to startup: the live watcher requires the
Cordis HMR service, which is absent in this headless composition, and dsh
crashes without the rewrite.
Preset model connections (strata, 262k context) ship as
config/deepseek-harness/settings.yaml.example; the installer copies it to
~/.local/share/deepseek-harness/settings.yaml and expects STRATA_API_KEY /
LLAMA_CPP_API_KEY in the dsh service.env. The llama.cpp router route is
commented out in the template: a hand-declared provider must list at least one
model or dsh refuses the entire llm-pi-ai settings section at boot, and the
router serves no models until GGUFs are placed in
~/.local/share/llama.cpp/cards/<card> (hf-download --card). Uncomment the block and list the served
model id(s) once the router has models.
The web UI's "Open configuration file" button does not work in this container
(no desktop opener inside the image); edit
~/.local/share/deepseek-harness/settings.yaml on the host instead — dsh
hot-reloads it.
To force a rebuild: ./scripts/build-images.sh --rebuild deepseek-harness.
dsh puts a browser-trust fence on all /api endpoints and binds 127.0.0.1
only (upstream safety). The Quadlet runs it with Network=host, so that
loopback bind lands on the host loopback directly: use
http://127.0.0.1:3105 on the host. This service is deliberately not exposed
to the LAN, and because the browser's hostname is literally 127.0.0.1, the
stock isLoopback check passes with no patches or trusted-host injection.
dsh requires a one-time token to set a browser cookie. The quickest way:
./scripts/ai-lab url deepseek-harness prints the ready-to-open URL (the bar
plugin's LAUNCH button uses the same). Or grab it from the logs:
podman logs systemd-deepseek-harness | grep "?token="Then open http://127.0.0.1:3105/?token=<token> in your browser.
The token in
podman logsis sensitive: logs live in the host's journald and anyone with access to your user session can read them. Treat a leaked token like a leaked password — restart the container to mint a new one.
quadlets/hermes.container (image docker.io/nousresearch/hermes-agent:latest)
is deployed only with ./install.sh --with-hermes; this host runs Hermes
natively (hermes-gateway.service). Its dashboard (:3104) credentials are
generated into ~/.config/containers/config/hermes-service/service.env.
journalctl --user -u open-webui.service -n 20 --no-pager
podman logs systemd-deepseek-harness
podman logs systemd-llama-cpp-5090This stack is tailored to one host: GPU services are named after the card
they run on, and quadlets/*.container.in templates carry the placeholders
__GPU_5090_UUID__ / __GPU_4070TI_UUID__. scripts/render-units.sh NAME...
fills them from nvidia-smi (cards matched by name, never by index) into
~/.config/containers/systemd/. See docs/gpu-assignment.md.
| llama.cpp | port | cards | context | KV | batch / ubatch | config |
|---|---|---|---|---|---|---|
| llama-cpp-5090 | 11435 | 5090 | 262,144 | q4_0 | 1024 / 256 | config/llama-cpp-5090/ |
| llama-cpp-4070ti | 11436 | 4070 Ti | 65,536 | q4_0 | 512 / 128 | config/llama-cpp-4070ti/ |
| llama-cpp-both | 11438 | 5090 + 4070 Ti, layer split (exclusive) | 262,144 | q4_0 | 1024 / 256 | config/llama-cpp-both/ |
All three share one key file (~/.config/containers/config/llama-cpp/keys.txt);
each serves only the models linked for it (next section).
The installer downloads no weights: a llama.cpp server is healthy but lists no models until you link some.
- Library: the Hugging Face cache,
~/.cache/huggingface/hub(one copy of every file, downloaded withhf download). - Per server:
~/.local/share/llama.cpp/cards/<card>/<name>/<file(s)>.gguf, HARDLINKS to the library files (same btrfs subvolume, so a model on two servers costs disk once). Eachllama-cpp-<card>unit mounts only its owncards/<card>dir read-only at/models, so a server never advertises a model linked for another card. - The router lists one model per subdir; the model id is the subdir name.
Split models (
-00001-of-0000N) keep every part side by side in that one subdir. The default name is the file name minus.ggufand the split suffix.
hf-download unsloth/Qwen3.8-27B-GGUF UD-Q4_K_XL --card 5090
hf-download bartowski/Llama-3.2-3B-Instruct-GGUF IQ4_XS --card 5090,4070ti
hf-download Qwen/Qwen2.5-3B-Instruct-GGUF 'qwen2.5-3b-instruct-fp16-*' --card both --name qwen2.5-3b-fp16
hf-download --remove Llama-3.2-3B-Instruct-IQ4_XS --card 4070ti # unlink; the library keeps the file
hf-download --list # models per server + libraryAfter linking, refresh-presets.py --card <card> --write writes per-model
ctx-size overrides (the model's trained context, capped by that server's VRAM;
both sums the two cards) into ~/.config/containers/config/llama-cpp-<card>/presets.ini,
and a running llama-cpp-<card> restarts (the router scans its dir only at
startup). scripts/download-gguf-series.sh does the same for a list
(repo|filter|cards per line, see scripts/gguf-download-list.example).
To delete a file from the library for good: unlink it from every card, then
hf cache rm model/<org>/<repo>.
Requires hf: sudo pacman -S python-huggingface-hub.
Sketch Lab's AI panel works out of the box: since v0.6.0 the app's own
nginx proxies /v1/ to Strata and injects the API key server-side (from
config/sketchlab/service.env), so the default endpoint is same-origin
and the default model is the loaded Strata model — no key entry, no CORS.
Since v0.6.2 nginx resolves Strata per request, so Sketch Lab starts while
Strata is off and /v1/ answers 502 until Strata is started. Details:
docs/sketchlab-local-models.md.
To point it at a different OpenAI-compatible endpoint instead:
- Open Sketch Lab at
http://<host-LAN-IP>:3102 - Click the AI button in the editor
- Set the endpoint to any OpenAI-compatible server the browser can reach
(e.g.
http://<host-LAN-IP>:11435/v1, llama.cpp on the 5090, with its key from~/.config/containers/config/llama-cpp/keys.txt) - Select a model from the dropdown (populated from
/v1/models)
Note: the /v1/ proxy on :3102 is unauthenticated on the LAN like the
sketchlab app itself — anyone who can reach :3102 can use the Strata key.
For AI agents: the Sketch Lab repo
github.com/dark5un/sketchlab.app
ships a zero-dependency stdio MCP server (mcp/server.mjs, tools
sketchlab_icons / sketchlab_validate / sketchlab_diagram) that
validates diagrams with the app's own parser and returns ready-to-open
?g= URLs; setup snippets for Hermes, Claude Code and Strata are in its
README.
HyperFrames is an HTML-to-video render engine. Two images, two purposes:
- Local renders:
scripts/hyperframes-render.sh <composition-dir> <out.mp4>. A one-shot CLI container (localhost/hyperframes-render:latest, built frompackages/cli/src/docker/Dockerfile.renderon first use). It needs no service running. A 2 s 1080p composition renders in about 5 s on the CPU. - The
hyperframesservice on :3103 runs the GCP Cloud Run adapter (packages/gcp-cloud-run). It is only the worker half of a distributed render:GET /healthz, andPOST /withAction=plan/renderChunk/assemble, each of which downloads from and uploads to a GCS bucket. It cannot render local files; without a bucket and Google credentials every action fails. Start it only if you drive it with the@hyperframes/gcp-cloud-runSDK against your own bucket.
The installer builds the service image from a hyperframes repo checkout; by hand:
cd /path/to/hyperframes
podman build -f packages/gcp-cloud-run/Dockerfile -t localhost/hyperframes:latest .Prometheus + Grafana + VictoriaLogs, all rootless quadlets (plan:
~/Documents/plans/ai-lab-monitoring-plan.md). The monitoring tier is the
stack's one deliberate boot exception: Prometheus, the exporters and the
log pair enable at boot (they hold no GPU/VRAM); Grafana stays on demand
with the web apps. ai-lab start|stop monitoring (or the MONITORING row
in the bar plugin) controls the whole group.
- What is scraped: strata + coder (
/metrics, bearer = the strata key), the llama.cpp routers (/metrics?model=X&autoload=falseper model in the card dir — never autoloads), ComfyUI (the exporter node baked into the image), every service's health URL via blackbox, both GPUs via NVML, the host via node-exporter, containers via the user podman socket, and Open WebUI via OTLP push into Prometheus. - Configs are GENERATED from services.json by
scripts/render-prometheus.py(run by install.sh): registering a service puts it under monitoring. Never edit~/.config/containers/config/{prometheus,blackbox,fluent-bit}/by hand. - Dashboards live in git under
monitoring/grafana/dashboards/and are provisioned read-only: AI Lab Overview (mode timeline, both cards, RAM, tok/s, health grid), Strata (upstream), llama.cpp, Open WebUI, ComfyUI, Node Exporter Full (1860), NVIDIA (14574/25547), Podman (21559). Sources + revisions in monitoring/README.md. - Logs: fluent-bit reads px's user journal (read-only) and pushes to VictoriaLogs (30d/5GB); Grafana queries it via the victoriametrics-logs-datasource plugin. Pitfalls encoded in the quadlet: fluent-bit must be 4.2+ (4.0 cannot read this ZSTD journal) and must NOT use keep-id (the journal ACL grants px, who is container root; keep-id maps to nobody and silently reads 0 records).
- Grafana: http://:3106, admin / password from
~/.config/containers/config/grafana/service.env(generate-secrets.sh). - Monitoring never loads or keeps awake any model: llama.cpp scrapes use
autoload=falseand the routers set no idle timer; the GPU exporter holds no VRAM; strata scrape cost measured <1% of decode tok/s.
The Ryoku bar plugin (plugin/ailab/ in this repo) shows the stack state in
the top bar: a glyph with the running/total count, and a panel listing every
service in services.json with start/stop toggles and health. Clicking a
service name or its LAUNCH button opens the service URL in your default
browser (with the dsh auth token appended automatically) and closes the
panel.
- Install:
ryoku plugin add plugin/ailab --bar --yes(from the repo root). The authoring copy at~/Documents/ryoku-plugins/ailabis a symlink into this repo, so there is a single source of truth. - Settings: QS Bar Settings > Community > AI Lab — running/total count badge and poll interval.
- Remove:
ryoku plugin remove ailab. - The plugin's only external commands are
systemctl --user,curl(127.0.0.1 health checks),python3(registry parsing) andxdg-open(launch), declared inmanifest.json; keep that list honest if the CLI grows commands.
Apache 2.0 — see LICENSE.