GPU brain¶
Rig-E's language model does not run on the rig. It runs on a rented RunPod GPU pod serving
Qwen/Qwen2.5-32B-Instruct-AWQ through vLLM. The pod is off almost all the time.
Sending Rig-E a message in the Rig Cockpit wakes the pod and starts billing. That is the intended behaviour, not an accident — but it is the one thing to know before you type.
Where do I talk to it¶
The web cockpit — rig-cockpit.dashecorp.com — and nowhere else. Manager tab, then pick
rig-e in the "Talking to" selector.
"Rig Cockpit" names three different clients and only that one can reach the GPU. The iOS app cannot: it has five read-only tabs and no chat surface at all, so it can neither wake the pod nor ask it anything. The Capacitor shell would inherit the web cockpit's chat, but it has not shipped. See cockpit.md for what each surface can and cannot do.
1. What it is and what it costs¶
| GPU | Chosen per wake, cheapest eligible in datacenter EU-RO-1 on RunPod secure cloud — not a fixed card. An RTX PRO 4500 32 GB when one is free; the 2026-09-12 wake got an A100 80GB PCIe |
| Price | Whatever card the pod lands on, under the RUNPOD_MAX_PRICE_PER_HOUR=2.00 cap (rig-conductor/deploy/k8s/deployment.yaml); the walk still ranks cheapest-first. Last observed: A100 80GB PCIe at 1.59 USD/h (2026-09-12, the first wake after rc#2092). Billed only while the pod is up — a stopped pod costs nothing. The old flat $0.72/h in this row predated the wake fix |
| Model / window | Qwen/Qwen2.5-32B-Instruct-AWQ, 16,384-token context (input + output). The runtime reserves 2,048 for output and ~930 for tool schemas, leaving ≈13,400 for one prompt (rig-agent-runtime/src/agent/providers/context-budget.js, measured twice against the live pod's own 400 reply) |
| Weights | network volume qwen-weights (bjn7d94ej9). The volume, not the pod id, is the identity: a pod that loses its GPU is replaced on the same volume automatically |
| Time to usable | ≈3 minutes observed on the first wake after rc#2092 (2026-09-12). The older ≈9-minute figure (wake → RUNNING ≈3 min, then vLLM loading ≈6 min) is the slow case and is still what Rig-E's own reply quotes. RUNNING never means usable |
| Typical cycle | wake → answer → automatic idle stop 15 min after ready ≈ 20 min billed, so ≈ $0.24 on a 0.72/h card and ≈ $0.53 on the 1.59/h A100 — the cost of one conversation is set by which card the wake landed |
| Hard ceiling | 6 h wall clock (MAX_RUNTIME_MINUTES=360) — a wedged pod cannot bill overnight |
| Who owns on/off | rig-conductor on the GCP k3s cluster. Nothing else talks to RunPod; the key lives in rig-conductor-secrets/runpod-api-key (see credentials.md) |
Timings and cost are the first live operator run, 2026-09-09 12:26Z. That run also exercised the replacement path: RunPod refused to start the old pod (its GPU was gone) and the conductor created a new one on the same volume without anyone touching a config file.
2. How to wake it¶
The normal way: just chat. Open Rig Cockpit, pick Rig-E, send a message. If the pod is asleep, Rig-E asks the conductor to wake it and tells you so in the reply (§5). Nothing else to do — wait a few minutes and ask again. Asking again inside 15 minutes does not fire a second wake.
The manual way (cockpit unavailable, or you want it warm before you start). start is in-cluster
only. The cockpit worker proxies GET /api/pods/{name}, and POST …/stop since rig-cockpit-worker#84, but never
start. Both writes carry the conductor's operator-token gate (rig-conductor#2369), enforced since 2026-09-25
(rig-conductor#2436): without X-Rig-Operator-Token they answer 403. The token lives in the rig-conductor-gate-secrets
Secret. The commands below read it on the node and hand it to curl on stdin (-H @-), so it never appears in an
argument, the output or shell history. Operator only (credential policy): an agent never runs these.
gcloud compute ssh invotek-k3s --zone=europe-north1-b --project=invotek-github-infra \
--account=stig@dashecorp.com --command='
IP=$(kubectl -n rig-conductor get svc rig-conductor-api -o jsonpath="{.spec.clusterIP}")
kubectl -n rig-conductor get secret rig-conductor-gate-secrets -o jsonpath="{.data.rig-operator-token}" | base64 -d \
| sed "s/^/X-Rig-Operator-Token: /" | curl -s -H @- -X POST "http://$IP:8080/api/pods/qwen/start"; echo
'
| Status | Meaning |
|---|---|
200 |
outcome AlreadyRunning / Started / Created — a pod is up (still minutes from usable if it was just started) |
202 |
outcome Waiting — a start or create is still in flight. Poll the GET, do not re-POST expecting a different answer |
409 |
the wake failed; detail says why |
503 |
RunPod not configured (RUNPOD_API_KEY / RUNPOD_VOLUME_ID) |
404 |
unknown pod name — the only name is qwen |
403 |
missing or wrong X-Rig-Operator-Token (the gate refuses before anything else is checked) |
The wake runs on the conductor's application lifetime, not on your HTTP request, so a dropped connection (Cloudflare Access cuts the origin at 100 s) never leaves a half-started pod. Retrying joins the same in-flight wake.
3. How to see whether it is up¶
gcloud compute ssh invotek-k3s --zone=europe-north1-b --project=invotek-github-infra \
--account=stig@dashecorp.com --command='
IP=$(kubectl -n rig-conductor get svc rig-conductor-api -o jsonpath="{.spec.clusterIP}")
curl -s "http://$IP:8080/api/pods/qwen"; echo
'
A real answer (2026-09-09, pod asleep):
{"name":"qwen","configured":true,"status":"EXITED","isRunning":false,"podId":"bsyo7ucc9d6nkm",
"proxyUrl":"https://bsyo7ucc9d6nkm-8000.proxy.runpod.net","podsOnVolume":2,
"datacenter":"EU-RO-1","lookup":"FOUND","readySince":null}
Usable = isRunning: true AND readySince non-null. Nothing else counts.
| Field | How to read it |
|---|---|
status |
RunPod's own state: PROVISIONING STARTING RUNNING EXITED ERROR TERMINATED, or UNKNOWN. RUNNING does not mean usable — vLLM then loads the model for ~6 minutes |
isRunning |
RUNNING with at least one GPU attached — the only state that can serve. Still not "ready" |
readySince |
The one that matters: when the idle monitor first saw vLLM answer /metrics since this boot. null while booting, loading, or down |
proxyUrl |
Set whenever any pod is named, including a dead one. Never read it without isRunning |
podsOnVolume |
Should be 1. 2 means a stale pod and its replacement both exist — see §6 |
lookup |
FOUND the volume was read · UNREADABLE RunPod's API did not answer, so a neighbouring UNKNOWN status is a blip and not a verdict · NOT_FOUND no pod on the volume at all · UNCONFIGURED key or volume env missing |
Conductor log lines to grep¶
gcloud compute ssh invotek-k3s --zone=europe-north1-b --project=invotek-github-infra \
--account=stig@dashecorp.com --command='
kubectl -n rig-conductor logs deploy/rig-conductor-api --since=2h | grep RunPodIdleMonitor
'
| Line | Means |
|---|---|
watching pod qwen on volume …: stop after 00:15:00 idle once ready …, boot grace 00:20:00, hard cap 06:00:00 |
the monitor armed at conductor start |
pod qwen (<id>) ready after <elapsed> (boot grace 00:20:00) |
model ready — the idle clock starts here, and <elapsed> is the RUNNING→ready model load |
pod qwen (<id>) idle 00:15:00 (Idle) -> stop requested (ok=True) |
the normal automatic stop |
pod qwen (<id>) not ready within boot grace … -> stop requested |
vLLM never came up; stopped as a dead model |
pod qwen (<id>) exceeded runtime cap 06:00:00 -> stop requested |
the 6 h backstop fired |
N pods on volume … — classifying every one (warn, every tick) |
more than one pod on the volume — §6 |
Rig-E's own side is kubectl -n dashecorp logs rig-e-rig-agent-runtime-0; grep
[chat] provider not ready, [chat] wake requested for pod qwen, STUCK: up but not answering,
and the boot line openai-api endpoint:.
4. How to stop it¶
Usually you don't. RunPodIdleMonitor in the conductor stops the pod after 15 idle minutes
counted from model-ready (rig-conductor#2056 — before 2026-09-09 the window started at RUNNING,
so the model-load minutes were spent as idle and a woken pod stopped almost immediately). A pod that
never becomes ready is stopped after RUNPOD_BOOT_GRACE_MINUTES (20). A pod up 6 hours is stopped
regardless of what the idle streak can see.
Manual stop:
gcloud compute ssh invotek-k3s --zone=europe-north1-b --project=invotek-github-infra \
--account=stig@dashecorp.com --command='
IP=$(kubectl -n rig-conductor get svc rig-conductor-api -o jsonpath="{.spec.clusterIP}")
kubectl -n rig-conductor get secret rig-conductor-gate-secrets -o jsonpath="{.data.rig-operator-token}" | base64 -d \
| sed "s/^/X-Rig-Operator-Token: /" | curl -s -H @- -X POST "http://$IP:8080/api/pods/qwen/stop"; echo
'
It stops every pod attached to the volume, which is what you want when a stale pod and its
replacement are both billing. 200 with a per-pod ok list (ok is the conjunction) · 404 no pod
on the volume · 503 the volume lookup was unreadable and nothing was stopped — retry. 403 means the token was
missing or wrong, and nothing was stopped.
A "Stop GPU" button on the cockpit Manager tab is open at dashecorp/rig-cockpit-worker#85, and a
status tile at #87. Until they merge the cockpit can only read pod status
(#86 allowlisted GET /api/pods/{name}), and the iOS app has no chat and no pod surface at all.
5. What you will see in chat while it wakes¶
Verbatim, from rig-agent-runtime/src/chat/wake-policy.js. All of them come back as normal
assistant messages with HTTP 200 — a red error bubble is never expected here.
| Reply | State | Normal? |
|---|---|---|
| "Waking the GPU brain — that takes about 9 minutes. Ask me again in a few minutes." | pod was asleep; exactly one wake fired | ✅ the expected first message |
| "Still waking — N min elapsed, usually ready by ~9. Ask again shortly." | inside the 15-minute window; no second wake | ✅ |
| "The GPU is up and the model is still loading — that takes a few more minutes. Ask again shortly." | RUNNING, vLLM loading, no wake on this conductor's clock |
✅ |
| "Still waking — N min elapsed; the GPU is up and the model is loading. Ask again shortly." | same, with a wake on the clock | ✅ |
| "Still not up after N minutes — I've asked the conductor to wake it again. Try again in a few minutes." | past the 15-minute window; a second wake fired | ⚠️ slow, not yet broken. If it repeats, §6 |
| "The model server has been up for N minutes and still is not answering — it may be stuck. Someone needs to look at the pod." | RUNNING for over 20 minutes without answering |
❌ a real fault; also logged at error level |
| "I can't reach the rig conductor right now — try again in a minute." | the conductor lookup was UNREADABLE — nothing was woken |
❌ conductor or RunPod-API problem |
6. When something is wrong¶
| Symptom | What to check | Where |
|---|---|---|
Raw HTTP 502 / a generic provider-failure bubble instead of one of the §5 replies |
Rig-E must be on image tag node-sha-418a948… or later — wake-on-chat (#682) and the model-loading window (#689) are in that tag. An older tag also ignores llm.baseUrlFrom and, having no baseUrl, would talk to api.openai.com |
kubectl -n dashecorp get pod rig-e-rig-agent-runtime-0 -o jsonpath='{.spec.containers[0].image}' vs image.tag in rig-gitops/apps/dashecorp/rig-e-helmrelease.yaml; and the boot log grep "openai-api endpoint:" |
| "still loading" past 20 minutes | the reply flips to the stuck wording and logs at error level; vLLM crashed or OOM'd coming up. The conductor's 20-minute boot grace should stop the pod — confirm it did, or you are paying for a dead model | kubectl -n dashecorp logs rig-e-rig-agent-runtime-0 \| grep STUCK; conductor grep "not ready within boot grace"; then the §3 GET |
| "I can't reach the rig conductor right now" | is the conductor pod up, and is RunPod's API answering? lookup: UNREADABLE on the GET is the same signal. Nothing was woken, so nothing new is billing |
kubectl -n rig-conductor get pods; the §3 GET |
podsOnVolume: 2, warn every tick |
a stale pod and its replacement are both attached — a conductor rollout mid-wake loses the sibling-termination tail (rig-conductor#2058, open). Harmless while both are EXITED; expensive if either is RUNNING |
the §3 GET; conductor grep "classifying every one". The §4 manual stop stops every pod on the volume |
| The pod is not stopping | the monitor only stops from the Ready phase after a full idle streak, and any running or waiting vLLM request resets it. A stop RunPod refused keeps the phase and retries a full window later. A conductor restart forgets readySince until the next tick re-observes the pod. Backstop is the 6 h cap |
conductor grep -E "stop requested\|ready after"; then the §4 manual stop |
Owning docs¶
This page is the runbook; these are the sources of truth.
| Doc | Covers |
|---|---|
rig-conductor docs/runpod-pod-control.md |
Canonical. The three routes with full response shapes, the volume-first wake and GPU selection, the idle phase machine, every env var and default |
rig-agent-runtime docs/2026-09-09-wake-on-chat.md |
The asleep → waking → loading → ready state table, every reply, and the live 2026-09-09 timeline |
rig-agent-runtime docs/2026-09-09-openai-endpoint-from-conductor.md |
llm.baseUrlFrom, the proxyUrl trust rules, caching and the once-per-turn re-resolve |
rig-gitops docs/2026-09-03-rig-e-deploy.md |
Rig-E's HelmRelease, its capability boundary, and the activation order |
See also kubernetes.md for where the conductor runs, and agents.md for Rig-E's place in the agent fleet.