Logo VRAMfarm VRAMfarm
IT
Registrati

12 novembre 2026 · Team VRAMfarm

API GPU per job batch: avvia un server di notte, esegui il lavoro, fermalo a fine job

Un orologio notturno che avvia un server GPU, esegue un job batch e lo spegne da solo: API GPU per job batch

Un job notturno che richiede 40 minuti su una RTX 4090 costa 0,39 € a notte se è uno script ad avviare il server, eseguire il lavoro e fermarlo. Se invece tieni lo stesso server acceso giorno e notte perché il job possa partire alle 2, costa 14,16 € al giorno. La differenza sta in quattro chiamate API e una riga di cron. Gli script qui sotto lo fanno in curl e in Python, e aggiungono la parte che quasi tutti gli esempi saltano: un tetto di spesa che ferma il server qualunque cosa vada storta.

Prima della prima esecuzione: token, chiave, credito

Un token. In Impostazioni → Token API, crea un token solo per questo job, con un nome tipo “nightly-batch”. Inizia con gf_live_ e viene mostrato una sola volta, quindi salvalo subito: nei secret della tua CI, oppure in un file leggibile solo dal tuo utente, caricato come GF_TOKEN. Puoi avere fino a 10 token attivi e revocarli uno per uno.

Un token può avviare, elencare e fermare server. Non può cambiare la password o la chiave SSH e non può creare nuovi token, quindi un token finito nelle mani sbagliate può costarti tempo GPU, ma non può prendersi l’account. La colonna Ultimo utilizzo mostra quando ogni token è stato usato l’ultima volta. Chiama l’API dal tuo server, dal portatile o dalla CI, mai da una pagina web, dove chiunque potrebbe leggere il token.

Una chiave SSH in Impostazioni → Accesso ai server. Viene installata su ogni nuovo server, e lo script la usa per eseguire il job e recuperare i risultati.

Un credito che copra almeno un’ora di tutti i server attivi, compreso quello che avvii. È un controllo, non un addebito: paghi i minuti che usi.

Controlla tutti e tre con una sola chiamata:

curl -s https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"

Ti servono "verified": true, "ssh_key_set": true e un balance_eur che copra il job di stanotte.

Le quattro chiamate

Chiamata Cosa fa Cosa usi della risposta
POST /api/instances Avvia un server id
GET /api/instances/{id} Stato di un server status, access.ssh, cost_eur, traffic_eur, error
GET /api/instances?status=running I tuoi server attivi quello che è rimasto acceso da un’esecuzione fallita
POST /api/instances/stop Ferma un server {"ok": true}

Il corpo della richiesta di avvio prende gpu (per esempio rtx-4090) e, se vuoi, count, template, image, cmd, disk e ws_restore. La risposta arriva subito con "status": "provisioning". La preparazione della macchina richiede 2–5 minuti con i nostri template e 8–15 con una tua immagine, e non si paga. La fatturazione parte quando lo stato diventa running, al minuto, con un minimo di 15 minuti per avvio.

Qualche comportamento da conoscere prima di scrivere il ciclo:

  • Lo stato passa da provisioning a running a stopped. Se una macchina non parte, ne prendiamo un’altra gratis e l’id resta lo stesso. Se il server finisce in stopped senza che tu l’abbia fermato, error dice perché.
  • Fermare due volte non fa danni. Fermare un server già fermo restituisce {"ok": true, "already": true}, quindi la chiamata di stop può stare in un trap o in un finally senza controlli.
  • Gli errori arrivano come {"error": "...", "code": "..."}. Decidi in base a code. 402 significa credito insufficiente, 409 che non c’è una macchina libera di quel tipo, e con 429, 502 e 503 conviene riprovare dopo una pausa.
  • Limiti: 60 richieste al minuto per token, 10 avvii al minuto e fino a 4 server contemporaneamente. Controlla lo stato ogni 5–10 secondi, non più spesso.

Tutti i campi e gli errori sono elencati nella documentazione dell’API.

Il nostro template via SSH o la tua immagine Docker

Ci sono due modi per portare il tuo codice sul server.

template: "pytorch" ti dà PyTorch, CUDA e Jupyter, pronti in 2–5 minuti. Lo script copia gli input via scp, avvia il job via SSH e recupera gli output. Se il job ha bisogno di una cartella salvata, ws_restore (attivo di default) la ripristina, e "save": true allo stop la conserva.

template: "custom" con image esegue una tua immagine pubblica da Docker Hub o da GitHub Container Registry, con tutte le dipendenze già dentro. Fissa un tag come ghcr.io/you/batch:2026-11-12, non latest, così il job di ieri notte e quello di stanotte eseguono lo stesso codice. cmd è un comando (fino a 4.000 caratteri) che gira con bash in background appena il container è su, con l’output in /var/log/gf-cmd.log, quindi l’immagine deve contenere bash. Per l’automazione contano tre differenze:

  • il container resta acceso anche dopo la fine di cmd, quindi il tuo script deve comunque fermarlo;
  • l’arresto automatico per inattività non ferma mai i server avviati da una tua immagine, quindi uno dimenticato resta acceso finché non lo fermi tu o finisce il credito;
  • la cartella salvata non funziona ancora con le immagini personalizzate, quindi i risultati devono lasciare il server prima dello stop, via scp o caricati dal job stesso.

Quest’ultima strada è lo schema “invio uno script e ricevo l’output” senza SSH: l’immagine esegue il job, carica i risultati sul tuo bucket o su un repository Hugging Face e scrive un file marcatore quando ha finito. Come spostare i dati dentro e fuori lo trovi in spostare dataset su un server GPU.

Lo script notturno in bash

Questa versione usa il template PyTorch. Sulla macchina che lo esegue servono curl, jq e OpenSSH.

#!/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      # ferma se questa esecuzione è costata tanto
MAX_MIN=120       # o se il job gira da tanto tempo

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                       # aspetta fino a 20 minuti
  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)    # known_hosts nuovo: col tempo le porte vengono riusate da altri server
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                                        # il trap ferma il 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)"

Il job parte con nohup e scrive exit_code quando finisce, quindi una connessione SSH caduta non lo uccide e lo script si limita a controllare un file piccolo. Il trap ... EXIT ferma il server in ogni caso di uscita: successo, tetto raggiunto, scp fallito o Ctrl+C.

Pianificalo con cron, caricando il token da un file che solo tu puoi leggere:

0 2 * * *  . $HOME/.gf_token && /opt/batch/nightly.sh >> /opt/batch/nightly.log 2>&1

In GitHub Actions, on: schedule: - cron: "0 2 * * *" fa lo stesso, con il token in secrets.GF_TOKEN e la chiave SSH privata in un altro secret.

Lo stesso job in Python, con la tua immagine

Qui il job vive nella tua immagine Docker e lo avvia cmd. Lo script non copia mai codice: aspetta il file marcatore, recupera il risultato e ferma il server.

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})

Due note. Il server ha l’accesso SSH anche con la tua immagine, ma per scp l’immagine deve contenere i soliti strumenti OpenSSH; se la tua è minimale, fai caricare i risultati al job su un bucket. E se il job genera immagini, ComfyUI ha una sua API HTTP per mettere in coda i prompt, che trovi in ComfyUI su una GPU a noleggio.

Come pensare il tetto di spesa

Non esiste un’impostazione di budget per singolo job, quindi il tetto vive nel tuo script. Costruiscilo a strati, così un singolo guasto non ti costa una notte:

  1. Prima il costo previsto. Prima dell’avvio, leggi il prezzo in tempo reale con GET /api/offers?gpu=rtx-4090&count=1 (il campo price è in euro all’ora per l’intera macchina, tutte le sue schede). Il costo previsto è prezzo × ore previste, più il traffico. Imposta MAX_EUR a circa 1,5–2 volte quel valore. Se il prezzo di stanotte è sopra quello che sei disposto a pagare, salta l’esecuzione o prova un’altra scheda.
  2. Tetti di tempo e di spesa nel ciclo. cost_eur cresce a ogni minuto dell’esecuzione, e traffic_eur aggiunge il traffico, che viene conteggiato con un certo ritardo. Controlla entrambi, più un limite di tempo reale, come negli script qui sopra.
  3. Uno stop a ogni uscita. trap ... EXIT in bash, finally in Python. Fermare due volte non fa danni.
  4. Un controllo al mattino. Un secondo job cron elenca i server attivi e ferma quelli più vecchi del tuo job più lungo. Copre il caso in cui la macchina che esegue lo script sia morta a metà:
# stesse variabili API, H e J dello script notturno
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 è la versione GNU; su Mac usa date -u -v-4H +%FT%TZ.)

  1. Il credito come muro esterno. Il credito è prepagato, quindi è il limite assoluto di quanto si può spendere. Ricarica per circa un mese di job, non per un anno. Ti mandiamo un’email quando resta meno di un’ora di utilizzo. Però non contare sull’esaurimento del credito: uno stop a credito zero non salva niente, e il disco viene cancellato con i risultati sopra.

Quando alle 2 di notte non c’è una macchina libera con la tua scheda (409), aspetta qualche minuto e riprova, oppure ripiega su una seconda scheda con la stessa memoria o di più. Prezzi e fatturazione al minuto sono spiegati nella guida alla fatturazione. Per la memoria del modello, il selettore di VRAM ti dice quali schede vanno bene per il tuo job.

Domande sull’argomento

Esiste una CLI, un SDK o un provider Terraform?

No. L'API è semplice HTTPS con JSON, quindi bastano curl, requests di Python o qualsiasi libreria HTTP. Non c'è un provider Terraform né un'integrazione con Kubernetes.

Posso controllare prezzi e disponibilità prima di avviare?

Sì. GET /api/offers elenca le macchine disponibili con prezzo orario, paese e tariffa del traffico, e GET /api/offer-types indica la macchina affidabile più economica per ogni scheda e quante ne sono libere. I prezzi sono in tempo reale, quindi controllali subito prima dell'avvio.

Il mio script può leggere il credito?

Sì. GET /api/account restituisce balance_eur, insieme a se la tua email è confermata, se è impostata una chiave SSH e quanti server puoi tenere attivi contemporaneamente.

Come fa uno script sul server a conoscere l'ID del proprio server?

Non c'è una variabile già pronta. La risposta dell'avvio contiene l'id, quindi se uno script sul server deve chiamare l'API passaglielo tu, per esempio scrivendolo in un file via SSH.

Trova la GPU in cui entra il tuo modello

Scegli un modello e vedi la scheda più economica con VRAM sufficiente, con fatturazione al minuto in euro.