12. November 2026 · VRAMfarm-Team
GPU-API für Batch-Jobs: Server nachts starten, Job ausführen, danach stoppen
Ein nächtlicher Job, der auf einer RTX 4090 40 Minuten braucht, kostet 0,39 € pro Nacht, wenn ein Skript den Server startet, den Job ausführt und ihn wieder stoppt. Lassen Sie denselben Server rund um die Uhr laufen, damit der Job um 2 Uhr nachts starten kann, kostet er 14,16 € pro Tag. Der Unterschied sind vier API-Aufrufe und eine cron-Zeile. Die Skripte hier erledigen das in curl und Python und ergänzen den Teil, den die meisten Beispiele weglassen: eine Ausgabengrenze, die den Server stoppt, egal was schiefgeht.
Vor dem ersten Lauf: Token, Schlüssel, Guthaben
Ein Token. Erstellen Sie unter Einstellungen → API-Tokens einen Token nur für diesen Job, mit einem Namen wie „nightly-batch“. Er beginnt mit gf_live_ und wird nur einmal angezeigt, speichern Sie ihn also sofort: in den Secrets Ihrer CI oder in einer Datei, die nur Ihr Benutzer lesen kann, geladen als GF_TOKEN. Sie können bis zu 10 aktive Tokens haben und jeden einzeln widerrufen.
Ein Token kann Server starten, auflisten und stoppen. Er kann weder Ihr Passwort noch Ihren SSH-Schlüssel ändern und keine neuen Tokens erstellen; ein geleakter Token kann Sie also GPU-Zeit kosten, aber nicht das Konto übernehmen. Die Spalte Zuletzt genutzt zeigt, wann jeder Token zuletzt verwendet wurde. Rufen Sie die API von Ihrem eigenen Server, Laptop oder aus Ihrer CI auf, nie aus einer Webseite, wo jeder den Token lesen könnte.
Ein SSH-Schlüssel unter Einstellungen → Serverzugang. Er wird auf jedem neuen Server installiert, und das Skript nutzt ihn, um den Job auszuführen und die Ergebnisse zu holen.
Ein Guthaben, das mindestens eine Stunde aller laufenden Server abdeckt, den neu gestarteten eingeschlossen. Das ist eine Prüfung, keine Abbuchung: Sie zahlen die Minuten, die Sie nutzen.
Alle drei prüfen Sie mit einem Aufruf:
curl -s https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"
Sie wollen "verified": true, "ssh_key_set": true und ein balance_eur, das den Job heute Nacht abdeckt.
Die vier Aufrufe
| Aufruf | Was er tut | Was Sie aus der Antwort nutzen |
|---|---|---|
POST /api/instances |
Startet einen Server | id |
GET /api/instances/{id} |
Status eines Servers | status, access.ssh, cost_eur, traffic_eur, error |
GET /api/instances?status=running |
Ihre laufenden Server | alles, was von einem gescheiterten Lauf übrig ist |
POST /api/instances/stop |
Stoppt einen Server | {"ok": true} |
Der Body beim Start nimmt gpu (zum Beispiel rtx-4090) und optional count, template, image, cmd, disk und ws_restore. Die Antwort kommt sofort, mit "status": "provisioning". Die Vorbereitung der Maschine dauert mit unseren Templates 2–5 Minuten, mit Ihrem eigenen Image 8–15, und nichts davon wird berechnet. Die Abrechnung beginnt, wenn der Status zu running wird, minutengenau, mit einem Minimum von 15 Minuten pro Start.
Ein paar Verhaltensweisen sollten Sie kennen, bevor Sie die Schleife schreiben:
- Der Status geht von
provisioningüberrunningzustopped. Startet eine Maschine nicht, nehmen wir kostenlos eine andere, und dieidbleibt gleich. Landet der Server ohne Ihr Zutun beistopped, sagterror, warum. - Zweimal stoppen ist unbedenklich. Ein Stopp für einen bereits gestoppten Server liefert
{"ok": true, "already": true}; der Stopp-Aufruf kann also ohne Prüfungen in einemtrapoderfinallystehen. - Fehler kommen als
{"error": "...", "code": "..."}. Verzweigen Sie anhand voncode.402heißt zu wenig Guthaben,409heißt, dass keine Maschine dieses Typs frei ist, und bei429,502und503lohnt sich ein neuer Versuch nach einer Pause. - Limits: 60 Anfragen pro Minute und Token, 10 Starts pro Minute und bis zu 4 Server gleichzeitig. Fragen Sie den Status alle 5–10 Sekunden ab, nicht öfter.
Jedes Feld und jeder Fehler steht in der API-Dokumentation.
Unser Template über SSH oder Ihr eigenes Docker-Image
Es gibt zwei Wege, Ihren Code auf den Server zu bringen.
template: "pytorch" liefert PyTorch, CUDA und Jupyter, bereit in 2–5 Minuten. Das Skript kopiert die Eingaben per scp, startet den Job über SSH und holt die Ergebnisse. Braucht der Job einen gespeicherten Ordner, holt ws_restore (standardmäßig an) ihn zurück, und "save": true beim Stopp behält ihn.
template: "custom" mit image startet Ihr eigenes öffentliches Image von Docker Hub oder GitHub Container Registry, mit allen Abhängigkeiten fest eingebaut. Legen Sie einen Tag wie ghcr.io/you/batch:2026-11-12 fest, nicht latest, damit der Job von gestern Nacht und der von heute denselben Code ausführen. cmd ist ein Befehl (bis zu 4.000 Zeichen), der mit bash im Hintergrund läuft, sobald der Container steht, mit seiner Ausgabe in /var/log/gf-cmd.log; das Image braucht also bash. Für die Automatisierung zählen drei Unterschiede:
- der Container läuft weiter, wenn
cmdfertig ist, Ihr Skript muss ihn also trotzdem stoppen; - der automatische Stopp bei Leerlauf stoppt nie Server mit Ihrem eigenen Image, ein vergessener läuft also, bis Sie ihn stoppen oder das Guthaben aufgebraucht ist;
- der gespeicherte Ordner funktioniert mit eigenen Images noch nicht, Ergebnisse müssen den Server also vor dem Stopp verlassen, per
scpoder indem der Job sie selbst hochlädt.
Die letzte Variante ist das Muster „Skript abschicken, Ergebnis zurückbekommen“ ohne SSH: Das Image führt den Job aus, schiebt die Ergebnisse in Ihren Bucket oder in ein Hugging-Face-Repo und schreibt eine Markierungsdatei, wenn es fertig ist. Wie Daten hinein- und herauskommen, steht unter Datensätze auf einen GPU-Server übertragen.
Das nächtliche Skript in bash
Diese Version nutzt das PyTorch-Template. Sie braucht curl, jq und OpenSSH auf der Maschine, auf der sie läuft.
#!/usr/bin/env bash
set -euo pipefail
API=https://vramfarm.com/api
H="Authorization: Bearer $GF_TOKEN"; J="Content-Type: application/json"
MAX_EUR=3.00 # stoppen, wenn dieser Lauf so viel gekostet hat
MAX_MIN=120 # oder wenn der Job so lange gelaufen ist
ID=$(curl -sf "$API/instances" -H "$H" -H "$J" \
-d '{"gpu":"rtx-4090","template":"pytorch","ws_restore":false}' | jq -r .id)
trap 'curl -s "$API/instances/stop" -H "$H" -H "$J" -d "{\"id\":$ID}" >/dev/null' EXIT
for i in $(seq 1 120); do # bis zu 20 Minuten warten
S=$(curl -sf "$API/instances/$ID" -H "$H")
ST=$(jq -r .status <<<"$S")
[ "$ST" = running ] && break
[ "$ST" = stopped ] && { echo "did not start: $(jq -r .error <<<"$S")"; exit 1; }
sleep 10
done
[ "$ST" = running ] || { echo "still not running, giving up"; exit 1; }
HOST=$(jq -r .access.ssh.host <<<"$S"); PORT=$(jq -r .access.ssh.port <<<"$S")
KH=$(mktemp) # frische known_hosts: Ports werden mit der Zeit von anderen Servern wiederverwendet
O=(-o StrictHostKeyChecking=accept-new -o UserKnownHostsFile="$KH" -o ServerAliveInterval=30)
scp -P "$PORT" "${O[@]}" ./input.tar root@"$HOST":/workspace/
ssh -p "$PORT" "${O[@]}" root@"$HOST" 'cd /workspace && tar xf input.tar &&
(nohup sh -c "python run.py > run.log 2>&1; echo \$? > exit_code" </dev/null >/dev/null 2>&1 &)'
START=$(date +%s)
until ssh -p "$PORT" "${O[@]}" root@"$HOST" test -f /workspace/exit_code; do
SPENT=$(curl -sf "$API/instances/$ID" -H "$H" | jq '.cost_eur + .traffic_eur')
MIN=$(( ($(date +%s) - START) / 60 ))
if awk -v s="$SPENT" -v m="$MAX_EUR" 'BEGIN{exit !(s >= m)}' || [ "$MIN" -ge "$MAX_MIN" ]; then
echo "cap reached: EUR $SPENT after $MIN min"
scp -P "$PORT" "${O[@]}" root@"$HOST":/workspace/run.log ./ || true
exit 2 # der trap stoppt den Server
fi
sleep 60
done
scp -P "$PORT" "${O[@]}" root@"$HOST":/workspace/run.log root@"$HOST":/workspace/output.tar ./results/
echo "job exit code: $(ssh -p "$PORT" "${O[@]}" root@"$HOST" cat /workspace/exit_code)"
Der Job startet mit nohup und schreibt exit_code, wenn er endet; eine abgebrochene SSH-Verbindung beendet ihn also nicht, und das Skript fragt nur eine kleine Datei ab. Der trap ... EXIT stoppt den Server auf jedem Weg nach draußen: bei Erfolg, beim Erreichen der Grenze, bei einem gescheiterten scp oder bei Ctrl+C.
Planen Sie es mit cron ein und laden Sie den Token aus einer Datei, die nur Sie lesen können:
0 2 * * * . $HOME/.gf_token && /opt/batch/nightly.sh >> /opt/batch/nightly.log 2>&1
In GitHub Actions erledigt on: schedule: - cron: "0 2 * * *" dasselbe, mit dem Token in secrets.GF_TOKEN und dem privaten SSH-Schlüssel in einem weiteren Secret.
Derselbe Job in Python, mit Ihrem eigenen Image
Hier steckt der Job in Ihrem Docker-Image, und cmd startet ihn. Das Skript kopiert nie Code: Es wartet auf die Markierungsdatei, holt das Ergebnis und stoppt.
import os, subprocess, tempfile, time, requests
API = "https://vramfarm.com/api"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['GF_TOKEN']}"
MAX_EUR, MAX_MIN = 3.0, 120
def call(method, path, **kw):
for _ in range(5):
r = s.request(method, API + path, timeout=30, **kw)
if r.status_code not in (429, 502, 503):
break
time.sleep(int(r.headers.get("Retry-After", 30)))
if not r.ok:
raise SystemExit(f"{r.status_code} {r.json().get('code')}: {r.json().get('error')}")
return r.json()
sid = call("POST", "/instances", json={
"gpu": "rtx-4090", "template": "custom",
"image": "ghcr.io/you/batch:2026-11-12",
"cmd": "mkdir -p /workspace && cd /workspace && python /app/run.py > run.log 2>&1; echo $? > exit_code",
})["id"]
try:
while (srv := call("GET", f"/instances/{sid}"))["status"] != "running":
if srv["status"] == "stopped":
raise SystemExit(f"did not start: {srv['error']}")
time.sleep(10)
ssh = srv["access"]["ssh"]
opts = ["-o", "StrictHostKeyChecking=accept-new",
"-o", f"UserKnownHostsFile={tempfile.mkstemp()[1]}"]
target = f"root@{ssh['host']}"
t0 = time.time()
while subprocess.run(["ssh", "-p", str(ssh["port"]), *opts, target,
"test -f /workspace/exit_code"]).returncode != 0:
srv = call("GET", f"/instances/{sid}")
spent = srv["cost_eur"] + srv["traffic_eur"]
if spent >= MAX_EUR or time.time() - t0 > MAX_MIN * 60:
raise SystemExit(f"cap reached at EUR {spent:.2f}")
time.sleep(60)
subprocess.run(["scp", "-P", str(ssh["port"]), *opts,
f"{target}:/workspace/output.tar", "results/"], check=True)
finally:
call("POST", "/instances/stop", json={"id": sid})
Zwei Hinweise. Der Server bekommt auch mit Ihrem eigenen Image SSH, aber für scp muss das Image die üblichen OpenSSH-Werkzeuge enthalten; ist Ihres minimal, lassen Sie den Job seine Ergebnisse stattdessen in einen Bucket hochladen. Und wenn der Job Bilder erzeugt, hat ComfyUI eine eigene HTTP-API, um Prompts in die Warteschlange zu stellen, gezeigt in ComfyUI auf einer gemieteten GPU.
Gedanken zur Ausgabengrenze
Eine Budget-Einstellung pro Job gibt es nicht, die Grenze lebt also in Ihrem Skript. Bauen Sie sie in Schichten auf, damit ein einzelner Fehler nicht eine ganze Nacht kostet:
- Zuerst die erwarteten Kosten. Lesen Sie vor dem Start den Live-Preis mit
GET /api/offers?gpu=rtx-4090&count=1(das Feldpriceist Euro pro Stunde für die ganze Maschine, alle Karten zusammen). Die erwarteten Kosten sind Preis × erwartete Stunden plus Traffic. Setzen SieMAX_EURauf etwa das 1,5- bis 2-Fache davon. Liegt der Preis heute Nacht über dem, was Sie zahlen wollen, lassen Sie den Lauf aus oder versuchen eine andere Karte. - Zeit- und Geldgrenzen in der Schleife.
cost_eurwächst mit jeder Minute des Laufs, undtraffic_eurkommt für den Traffic hinzu, der mit etwas Verzögerung gezählt wird. Prüfen Sie beide, dazu ein Zeitlimit, wie in den Skripten oben. - Ein Stopp bei jedem Ausgang.
trap ... EXITin bash,finallyin Python. Zweimal stoppen schadet nicht. - Ein Aufräumlauf am Morgen. Ein zweiter cron-Job listet laufende Server auf und stoppt alles, was älter ist als Ihr längster Job. Das fängt den Fall ab, dass die Maschine, auf der Ihr Skript lief, mitten im Lauf ausgefallen ist:
# dieselben Variablen API, H und J wie im nächtlichen Skript
curl -s "$API/instances?status=running" -H "$H" |
jq -r --arg cut "$(date -u -d '-4 hours' +%FT%TZ)" '.instances[] | select(.started_at < $cut) | .id' |
while read -r id; do curl -s "$API/instances/stop" -H "$H" -H "$J" -d "{\"id\":$id}"; done
(date -d ist die GNU-Version; auf einem Mac nehmen Sie date -u -v-4H +%FT%TZ.)
- Das Guthaben als äußere Mauer. Das Guthaben ist im Voraus bezahlt und damit die absolute Grenze dessen, was ausgegeben werden kann. Laden Sie für etwa einen Monat an Jobs auf, nicht für ein Jahr. Wir schicken Ihnen eine E-Mail, wenn weniger als eine Stunde Laufzeit übrig ist. Verlassen Sie sich aber nicht darauf, dass es ausgeht: Ein Stopp bei Guthaben null speichert nichts, und die Festplatte wird samt den Ergebnissen gelöscht.
Ist um 2 Uhr nachts keine Maschine mit Ihrer Karte frei (409), warten Sie ein paar Minuten und versuchen es erneut, oder weichen Sie auf eine zweite Karte mit gleich viel oder mehr Speicher aus. Preise und minutengenaue Abrechnung erklärt der Ratgeber zur Abrechnung. Für den Speicherbedarf des Modells zeigt Ihnen die VRAM-Auswahl, welche Karten zu Ihrem Job passen.