12 novembre 2026 · Équipe VRAMfarm
API GPU pour les tâches par lots : lancer un serveur la nuit, exécuter, arrêter à la fin
Une tâche nocturne qui demande 40 minutes sur une RTX 4090 coûte 0,39 € par nuit si un script démarre le serveur, exécute la tâche et l’arrête. Laissez le même serveur allumé en permanence pour que la tâche puisse tourner à 2 h du matin, et il coûte 14,16 € par jour. La différence tient en quatre appels d’API et une ligne cron. Les scripts ci-dessous le font en curl et en Python, et ajoutent ce que la plupart des exemples oublient : un plafond de dépenses qui arrête le serveur quoi qu’il arrive.
Avant la première exécution : token, clé, solde
Un token. Dans Paramètres → Tokens API, créez un token réservé à cette tâche, avec un nom comme « nightly-batch ». Il commence par gf_live_ et n’est affiché qu’une fois : enregistrez-le tout de suite, dans les secrets de votre CI ou dans un fichier lisible par votre seul utilisateur, chargé sous le nom GF_TOKEN. Vous pouvez avoir jusqu’à 10 tokens actifs et révoquer chacun séparément.
Un token peut lancer, lister et arrêter des serveurs. Il ne peut pas changer votre mot de passe ou votre clé SSH, ni créer de nouveaux tokens : un token qui fuite peut vous coûter du temps GPU, mais pas vous faire perdre le compte. La colonne Dernière utilisation indique quand chaque token a servi pour la dernière fois. Appelez l’API depuis votre propre serveur, votre portable ou votre CI, jamais depuis une page web, où n’importe qui pourrait lire le token.
Une clé SSH dans Paramètres → Accès aux serveurs. Elle est installée sur chaque nouveau serveur, et le script s’en sert pour lancer la tâche et récupérer les résultats.
Un solde qui couvre au moins une heure de tous vos serveurs en marche, y compris celui que vous lancez. C’est une vérification, pas un prélèvement : vous payez les minutes que vous utilisez.
Vérifiez les trois en un seul appel :
curl -s https://vramfarm.com/api/account -H "Authorization: Bearer $GF_TOKEN"
Vous devez voir "verified": true, "ssh_key_set": true et un balance_eur qui couvre la tâche de ce soir.
Les quatre appels
| Appel | Ce qu’il fait | Ce que vous utilisez dans la réponse |
|---|---|---|
POST /api/instances |
Lance un serveur | id |
GET /api/instances/{id} |
État d’un serveur | status, access.ssh, cost_eur, traffic_eur, error |
GET /api/instances?status=running |
Vos serveurs en marche | tout ce qui reste d’une exécution ratée |
POST /api/instances/stop |
Arrête un serveur | {"ok": true} |
Le corps du lancement prend gpu (par exemple rtx-4090) et, en option, count, template, image, cmd, disk et ws_restore. La réponse revient immédiatement avec "status": "provisioning". La préparation de la machine prend 2 à 5 minutes avec nos modèles et 8 à 15 avec votre propre image, et rien n’en est facturé. La facturation commence quand l’état passe à running, à la minute, avec un minimum de 15 minutes par lancement.
Quelques comportements à connaître avant d’écrire la boucle :
- L’état passe de
provisioningàrunningpuis àstopped. Si une machine ne démarre pas, nous en prenons une autre gratuitement, et l’idreste le même. Si le serveur se retrouvestoppedsans votre intervention,erroren donne la raison. - Arrêter deux fois ne pose pas de problème. Arrêter un serveur déjà arrêté renvoie
{"ok": true, "already": true}: l’appel d’arrêt peut donc se trouver dans untrapou unfinallysans vérification. - Les erreurs arrivent sous la forme
{"error": "...", "code": "..."}. Aiguillez surcode.402signifie un solde insuffisant,409qu’aucune machine de ce type n’est libre, et429,502et503valent la peine d’être réessayés après une pause. - Les limites : 60 requêtes par minute et par token, 10 lancements par minute, et jusqu’à 4 serveurs en même temps. Interrogez l’état toutes les 5 à 10 secondes, pas plus souvent.
Chaque champ et chaque erreur figurent dans la documentation de l’API.
Notre modèle via SSH, ou votre propre image Docker
Il y a deux façons de mettre votre code sur le serveur.
template: "pytorch" vous donne PyTorch, CUDA et Jupyter, prêts en 2 à 5 minutes. Le script copie les entrées avec scp, lance la tâche via SSH et récupère les sorties. Si la tâche a besoin d’un dossier enregistré, ws_restore (activé par défaut) le restaure, et "save": true à l’arrêt le conserve.
template: "custom" avec image fait tourner votre propre image publique depuis Docker Hub ou GitHub Container Registry, avec toutes les dépendances intégrées. Fixez une étiquette comme ghcr.io/you/batch:2026-11-12, pas latest, pour que la tâche d’hier soir et celle de ce soir exécutent le même code. cmd est une commande (jusqu’à 4 000 caractères) lancée avec bash en arrière-plan dès que le conteneur est démarré, avec sa sortie dans /var/log/gf-cmd.log : l’image doit donc contenir bash. Trois différences comptent pour l’automatisation :
- le conteneur continue de tourner après la fin de
cmd, votre script doit donc quand même l’arrêter ; - l’arrêt automatique en cas d’inactivité n’arrête jamais les serveurs lancés depuis votre propre image : un serveur oublié tourne jusqu’à ce que vous l’arrêtiez ou que le solde soit épuisé ;
- le dossier enregistré ne fonctionne pas encore avec les images personnalisées : les résultats doivent quitter le serveur avant l’arrêt, par
scpou envoyés par la tâche elle-même.
Cette dernière option correspond au schéma « soumettre un script et récupérer le résultat » sans SSH : l’image exécute la tâche, pousse les résultats vers votre bucket ou un dépôt Hugging Face, et écrit un fichier témoin quand elle a fini. Le transfert des données est traité dans transférer des jeux de données vers un serveur GPU.
Le script nocturne en bash
Cette version utilise le modèle PyTorch. Elle a besoin de curl, jq et OpenSSH sur la machine qui l’exécute.
#!/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 # arrêter si cette exécution a coûté autant
MAX_MIN=120 # ou si la tâche a tourné aussi longtemps
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 # attendre jusqu'à 20 minutes
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 neuf : les ports sont réutilisés par d'autres serveurs au fil du temps
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 # le trap arrête le serveur
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)"
La tâche démarre avec nohup et écrit exit_code quand elle se termine : une connexion SSH coupée ne la tue pas, et le script se contente d’interroger un petit fichier. Le trap ... EXIT arrête le serveur quelle que soit la sortie : succès, plafond atteint, scp raté ou Ctrl+C.
Planifiez-le avec cron, en chargeant le token depuis un fichier que vous seul pouvez lire :
0 2 * * * . $HOME/.gf_token && /opt/batch/nightly.sh >> /opt/batch/nightly.log 2>&1
Dans GitHub Actions, on: schedule: - cron: "0 2 * * *" fait la même chose, avec le token dans secrets.GF_TOKEN et la clé SSH privée dans un autre secret.
La même tâche en Python, avec votre propre image
Ici, la tâche se trouve dans votre image Docker, et cmd la démarre. Le script ne copie jamais de code : il attend le fichier témoin, récupère le résultat et arrête le serveur.
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})
Deux remarques. Le serveur dispose de SSH même avec votre propre image, mais scp suppose que l’image contienne les outils OpenSSH habituels ; si la vôtre est minimaliste, laissez plutôt la tâche envoyer ses résultats vers un bucket. Et si la tâche génère des images, ComfyUI a sa propre API HTTP pour mettre des prompts en file d’attente, présentée dans ComfyUI sur un GPU loué.
Réfléchir au plafond de dépenses
Il n’existe pas de réglage de budget par tâche : le plafond se trouve donc dans votre script. Construisez-le en couches, pour qu’une seule défaillance ne vous coûte pas une nuit :
- D’abord le coût attendu. Avant le lancement, lisez le prix en direct avec
GET /api/offers?gpu=rtx-4090&count=1(le champpriceest en euros par heure pour toute la machine, toutes cartes comprises). Le coût attendu vaut prix × heures prévues, plus le trafic. RéglezMAX_EURsur environ 1,5 à 2 fois ce montant. Si le prix de ce soir dépasse ce que vous êtes prêt à payer, sautez l’exécution ou essayez une autre carte. - Des plafonds de temps et d’argent dans la boucle.
cost_euraugmente à chaque minute d’exécution, ettraffic_eury ajoute le trafic, comptabilisé avec un certain délai. Vérifiez les deux, plus une limite de durée réelle, comme dans les scripts ci-dessus. - Un arrêt à chaque sortie.
trap ... EXITen bash,finallyen Python. Arrêter deux fois est sans conséquence. - Un balayage le matin. Une deuxième tâche cron liste les serveurs en marche et arrête tout ce qui est plus ancien que votre tâche la plus longue. Elle rattrape le cas où la machine qui exécutait votre script est tombée en cours de route :
# mêmes variables API, H et J que dans le script nocturne
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 est la version GNU ; sur Mac, utilisez date -u -v-4H +%FT%TZ.)
- Le solde comme dernier rempart. Le solde est prépayé : c’est la limite absolue de ce qui peut être dépensé. Rechargez pour environ un mois de tâches, pas pour un an. Nous vous envoyons un e-mail quand il reste moins d’une heure d’utilisation. Mais ne comptez pas sur l’épuisement du solde : un arrêt à solde nul n’enregistre rien, et le disque est supprimé avec les résultats qu’il contient.
Quand aucune machine de votre carte n’est libre à 2 h du matin (409), attendez quelques minutes et réessayez, ou rabattez-vous sur une deuxième carte avec autant de mémoire ou plus. Les prix et la facturation à la minute sont expliqués dans le guide de la facturation. Pour la mémoire du modèle, le sélecteur de VRAM vous indique quelles cartes conviennent à votre tâche.