VRAMfarm-Logo VRAMfarm
DE
Registrieren

12. November 2026 · VRAMfarm-Team

GPU-API für Batch-Jobs: Server nachts starten, Job ausführen, danach stoppen

Eine nächtliche Uhr startet einen GPU-Server, der einen Batch-Job ausführt und sich selbst abschaltet: GPU-API für Batch-Jobs

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 über running zu stopped. Startet eine Maschine nicht, nehmen wir kostenlos eine andere, und die id bleibt gleich. Landet der Server ohne Ihr Zutun bei stopped, sagt error, 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 einem trap oder finally stehen.
  • Fehler kommen als {"error": "...", "code": "..."}. Verzweigen Sie anhand von code. 402 heißt zu wenig Guthaben, 409 heißt, dass keine Maschine dieses Typs frei ist, und bei 429, 502 und 503 lohnt 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 cmd fertig 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 scp oder 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:

  1. Zuerst die erwarteten Kosten. Lesen Sie vor dem Start den Live-Preis mit GET /api/offers?gpu=rtx-4090&count=1 (das Feld price ist Euro pro Stunde für die ganze Maschine, alle Karten zusammen). Die erwarteten Kosten sind Preis × erwartete Stunden plus Traffic. Setzen Sie MAX_EUR auf 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.
  2. Zeit- und Geldgrenzen in der Schleife. cost_eur wächst mit jeder Minute des Laufs, und traffic_eur kommt 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.
  3. Ein Stopp bei jedem Ausgang. trap ... EXIT in bash, finally in Python. Zweimal stoppen schadet nicht.
  4. 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.)

  1. 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.

Fragen zum Thema

Gibt es eine CLI, ein SDK oder einen Terraform-Provider?

Nein. Die API ist schlichtes HTTPS mit JSON, Sie brauchen also nur curl, Python requests oder eine beliebige HTTP-Bibliothek. Einen Terraform-Provider oder eine Kubernetes-Integration gibt es nicht.

Kann ich Preise und Verfügbarkeit vor dem Start prüfen?

Ja. GET /api/offers listet verfügbare Maschinen mit Stundenpreis, Land und Traffic-Tarif, und GET /api/offer-types liefert für jede Karte die günstigste zuverlässige Maschine und wie viele frei sind. Die Preise sind live, prüfen Sie also direkt vor dem Start.

Kann mein Skript mein Guthaben abfragen?

Ja. GET /api/account liefert balance_eur, dazu, ob Ihre E-Mail-Adresse bestätigt ist, ob ein SSH-Schlüssel hinterlegt ist und wie viele Server Sie gleichzeitig betreiben können.

Woher kennt ein Skript auf dem Server seine eigene Server-ID?

Dafür gibt es keine eingebaute Variable. Die Antwort auf den Start enthält die id; geben Sie sie also selbst an den Server weiter, etwa indem Sie sie per SSH in eine Datei schreiben, falls ein Skript dort die API aufrufen muss.

Finden Sie die GPU, auf die Ihr Modell passt

Wählen Sie ein Modell und sehen Sie die günstigste Karte mit genug VRAM, minutengenau in Euro abgerechnet.