Québec, Canada

403-1381 1re Avenue

+1 581.849.27.96

bdgouthiere@gmail.com

File d'attente, jobs longs et webhooks : ne plus attendre le GPU

Ou : comment déposer un travail au guichet, rentrer chez soi, et être sûr de récupérer le résultat.

La réponse courte : sur un endpoint serverless Runpod, un job de plus d’une minute se lance sur la route asynchrone /run, qui renvoie aussitôt un identifiant. Le résultat se récupère ensuite par /status, ou arrive tout seul sur l’adresse d’un webhook. Quatre délais gouvernent le tout : dix minutes de calcul au plus par défaut, vingt-quatre heures de vie pour le job, trente minutes de conservation du résultat, et trois tentatives au total pour le webhook. Qui connaît ces quatre chiffres ne perd plus de job, et ce savoir vaut pour toute l’architecture serverless sur GPU, images comme vidéo.

La route synchrone /runsync existe aussi, mais elle convient mal aux jobs longs. Elle attend 90 secondes par défaut, et garde le résultat une minute seulement : si la connexion tombe pendant l’attente, le job finit, il est facturé, et son résultat disparaît avant que vous puissiez le demander. Elle accepte en revanche des requêtes plus lourdes, 20 Mo contre 10 pour /run.

Sur le pipeline vidéo de ce site, chaque clip WAN 2.2 part sur /run, avec l’image source en base64 dans la requête, puis un script interroge /status toutes les quinze secondes. C’est simple, robuste, et suffisant pour une production par lots. La documentation des requêtes détaille les autres routes, qui servent surtout quand quelque chose tourne mal.

Pour qui : un développeur qui pilote des jobs GPU de plusieurs minutes, vidéo, entraînement court, traitement par lots, et ne veut ni bloquer son serveur ni perdre un résultat payé.

À partir de : 0,00031 $ la seconde de RTX 4090 en serverless, 0,00044 $ pour une RTX 5090, facturées pendant le calcul, jamais pendant l’attente en file.

Pour démarrer : ouvrir un compte Runpod, déployer un worker serverless, et soumettre les jobs sur /run avec un webhook et une politique de délais adaptée.

Jobs asynchrones et webhooks sur GPU serverless : routes, états et délais

RouteRôleÀ savoir
POST /runsoumet un job asynchrone10 Mo au plus, 1 000 requêtes par 10 secondes
POST /runsyncsoumet et attend90 s d’attente par défaut, résultat gardé 1 minute
GET /status/{id}état et résultatrésultat gardé 30 minutes après la fin
GET /stream/{id}sortie progressivepour les workers qui produisent par morceaux
POST /cancel/{id}annule un job100 requêtes par 10 secondes
POST /retry/{id}relance un jobseulement s’il est en échec ou hors délai, même identifiant
POST /purge-queuevide la fileles jobs en attente seulement
GET /healthcompteursjobs en file, workers actifs

Un job passe par des états que la documentation liste précisément : IN_QUEUE tant qu’il attend un worker, IN_PROGRESS ou RUNNING pendant le calcul, puis COMPLETED, FAILED, CANCELLED ou TIMED_OUT. L’attente en file n’est pas facturée, le calcul l’est.

Deux délais se règlent job par job, dans un champ policy de la requête. Le délai d’exécution, dix minutes par défaut et jusqu’à sept jours, arrête un calcul qui s’éternise. La durée de vie, vingt-quatre heures par défaut, compte depuis la soumission, attente comprise : à son expiration, le job est supprimé et /status répond 404. Un troisième réglage, lowPriority, soumet un job qui ne déclenche pas le démarrage de nouveaux workers, utile pour du travail de fond qui peut attendre.

Voici la boucle du pipeline, réduite à l’essentiel, avec un webhook en plus :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# Soumet un job en asynchrone, puis attend le résultat sans bloquer indéfiniment
import os
import time

import requests

BASE = f"https://api.runpod.ai/v2/{os.environ['RUNPOD_ENDPOINT_ID']}"
ENTETES = {"Authorization": f"Bearer {os.environ['RUNPOD_API_KEY']}"}


def soumettre(entree: dict) -> str:
    corps = {
        "input": entree,
        "webhook": "https://exemple.org/runpod/fin",  # prévenu à la fin, trois tentatives au total
        "policy": {"executionTimeout": 900_000, "ttl": 3_600_000},  # 15 min de calcul, 1 h de vie
    }
    r = requests.post(f"{BASE}/run", headers=ENTETES, json=corps, timeout=60)
    r.raise_for_status()
    return r.json()["id"]  # à enregistrer avant toute attente


def attendre(job_id: str, limite: int = 1800) -> dict:
    debut = time.time()
    while time.time() - debut < limite:
        time.sleep(15)
        s = requests.get(f"{BASE}/status/{job_id}", headers=ENTETES, timeout=30).json()
        if s["status"] == "COMPLETED":
            return s["output"]
        if s["status"] in ("FAILED", "CANCELLED", "TIMED_OUT"):
            raise RuntimeError(f"job {job_id} : {s['status']}")
    raise TimeoutError(f"job {job_id} toujours en cours, à reprendre par /status")

Le délai de la boucle, trente minutes, n’est pas choisi au hasard. Les premières scènes de la vidéo sur kDrive abandonnaient au bout de dix minutes, alors que certains jours un clip restait neuf à dix minutes en file avant d’être lancé. Ces clips finissaient quand même, facturés, et il fallait les rechercher à la main par leur identifiant, avant l’expiration des trente minutes de conservation. Les scènes suivantes attendent trente minutes. La leçon vaut pour tout job long : l’identifiant s’enregistre avant l’attente, et la limite de la boucle doit dépasser la pire file d’attente constatée, pas le temps de calcul moyen.

Le webhook, enfin, évite de garder un processus en attente. Runpod envoie le résultat à l’adresse indiquée dès la fin du job, et retente deux fois, à dix secondes d’intervalle, si votre serveur ne répond pas 200. Trois tentatives en vingt secondes, c’est peu : un redéploiement au mauvais moment suffit à les manquer. Gardez donc un polling de rattrapage pour les jobs sans nouvelles, tant que le résultat est conservé.

La bille compte plus que l'attente : sans elle, le résultat existe, mais personne ne sait où le chercher.

Si vous voulez le coût réel d’une production entière

84 clips WAN 2.2 pilotés par cette boucle, et la facture qui va avec, carte par carte, sont détaillés dans le retour d’expérience d’un pipeline vidéo.

Si la file d’attente s’allonge

Des jobs qui restent longtemps en IN_QUEUE peuvent signaler un plafond de workers trop bas pour le rythme des soumissions, ou des cartes indisponibles. Le réglage est expliqué dans combien de workers lancer.

Si vous écrivez le worker qui reçoit ces jobs

Le handler reçoit le champ input de la requête et renvoie ce que /status restituera. Son écriture et son déploiement sont décrits dans un worker serverless sur mesure.