Hugging Face ZeroGPU
Règles et patterns pour les démos ML sur Hugging Face Spaces avec le matériel ZeroGPU. Couvre @spaces.GPU, le réglage de la durée et du quota, l'isolation des processus, le modèle de disponibilité CUDA, la sécurité de la concurrence et les contraintes de construction CUDA.
Scope
Cette skill est pour les Spaces Gradio SDK utilisant le matériel ZeroGPU. Docker et Static Spaces ne peuvent pas être planifiés sur ZeroGPU, et les apps Streamlit s'exécutent désormais comme des Spaces Docker — cette skill s'applique donc uniquement à Gradio. Pour le codage Gradio général (composants, mises en page, event listeners), consultez la skill huggingface-gradio dans ce repo. La documentation ZeroGPU officielle se trouve à https://huggingface.co/docs/hub/spaces-zerogpu — référez-vous-y pour la GPU de support actuelle, les listes de versions runtime et les seuils de tier, qui changent au fil du temps.
Fichiers de référence
| Référence | Quand lire |
|---|---|
references/concurrency.md |
Lisez toujours en parallèle de SKILL.md lors de l'écriture de code ZeroGPU — les handlers s'exécutent en parallèle par défaut |
references/how-zerogpu-works.md |
Quand vous raisonnez sur les cold-starts, la réutilisation de workers, pourquoi le warmup au scope du module ne se porte pas aux requêtes, ou pourquoi retourner des tenseurs CUDA se bloque |
references/how-quota-works.md |
Quand vous choisissez des valeurs duration, déboguez les erreurs illegal duration vs quota exceeded, ou expliquez pourquoi le défaut 60s bloque les tâches courtes |
references/cuda-and-deps.md |
Quand vous installez des packages dépendants de CUDA (ex. flash-attn), épinglez les torch side-cars, ou lisez les tags de nom de wheel |
Matériel
ZeroGPU expose deux tailles de GPU qui correspondent à une fraction de la carte de support :
size |
Tranche de GPU de support | Coût de quota |
|---|---|---|
large (par défaut) |
Moitié | 1x |
xlarge |
Complète | 2x |
Le large par défaut donne la moitié d'une GPU physique, donc la bande passante de mémoire et le calcul sont significativement inférieurs aux spécifications de la carte complète. Utilisez xlarge uniquement quand la charge de travail a vraiment besoin de la mémoire ou du calcul supplémentaires.
La GPU de support change sans préavis. ZeroGPU a déjà migré à plusieurs reprises entre les générations de GPU ; les écrits antérieurs peuvent nommer A100 ou H200, mais ceux-ci sont périmés. Pour la GPU de support actuelle et la VRAM exacte par taille, consultez toujours la documentation ZeroGPU avant de dimensionner les charges de travail.
Pattern de base
import spaces
import torch
from transformers import pipeline
pipe = pipeline("text-generation", model="...", device="cuda")
@spaces.GPU
def generate(prompt: str) -> str:
return pipe(prompt, max_new_tokens=100)[0]["generated_text"]
Règles clés :
- Instanciez les modèles au scope du module et appelez
.to("cuda")en tête. ZeroGPU gère le mappage réel du device de façon transparente (voir le modèle de disponibilité CUDA ci-dessous). - Décorez les fonctions GPU avec
@spaces.GPU. Le décorateur est un no-op en dehors de ZeroGPU, il est donc sûr de le conserver dans tous les environnements. - Définissez
durationpour correspondre au pire cas réaliste de la charge de travail (par défaut 60s). La plateforme pré-vérifie laduration demandéepar rapport auquota restantde l'utilisateur — non pas contre le temps d'exécution réel — donc une tâche de 10 secondes restée au défaut 60s échoue avecquota exceededdès que le quota restant de l'utilisateur tombe sous 60s. Unedurationdéclarée plus petite classe également plus haut dans la file d'attente au niveau du nœud. Voir « Duration and Quota » ci-dessous. torch.compilen'est PAS supporté. Utilisez plutôt la compilation ahead-of-time de PyTorch (AoTI) (torch 2.8+).- Utilisez
size="xlarge"avec parcimonie. Il alloue la GPU de support complète, mais coûte 2x le quota et tend à mettre en queue plus longtemps.
@spaces.GPU(duration=120)
def generate_image(prompt: str):
return pipe(prompt).images[0]
Modèle de disponibilité CUDA
L'accès à la GPU réelle est uniquement disponible à l'intérieur des fonctions décorées par @spaces.GPU. En dehors de ces fonctions, la GPU n'est pas attachée au processus.
Cependant, import spaces monkey-patche torch de sorte que :
torch.cuda.is_available()retourneTrueglobalement.- les appels
.to("cuda")/device="cuda"au scope du module réussissent sans erreur.
C'est intentionnel. Les appels model.to("cuda") au scope du module enregistrent les tenseurs auprès du backend ZeroGPU, qui les écrit dans un répertoire de décharge disque lors d'une étape de « pack » au démarrage et libère la RAM correspondante. Quand un appel @spaces.GPU arrive, un processus worker GPU forké diffuse ces poids du disque vers la VRAM via un pipeline de mémoire épinglée. Les workers chauds (réutilisés entre les requêtes sur le même slot GPU) conservent les poids résidents sur la GPU et ignorent l'étape disque → VRAM. La règle côté utilisateur : écrivez device="cuda" au scope du module et ça marche — voir references/how-zerogpu-works.md pour le cycle de vie complet.
| Action | Où | Pourquoi |
|---|---|---|
model.to("cuda") / pipe(..., device="cuda") |
Scope du module | ZeroGPU enregistre le tenseur et gère la migration de device |
| Calcul CUDA réel (inférence, etc.) | À l'intérieur de @spaces.GPU |
La GPU réelle n'est attachée que pendant l'appel décoré |
Branchement sur torch.cuda.is_available() |
Évitez de vous fier à cela | Retourne toujours True en raison du monkey-patch |
Ne lancez pas l'inférence ou les kernels CUDA au scope du module — la GPU réelle n'est pas attachée, donc les opérations s'exécutent silencieusement sur CPU ou échouent.
L'idiome de sélection de device fonctionne toujours
L'idiome standard reste correct sous ZeroGPU :
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = AutoModel.from_pretrained("...").to(device)
- ZeroGPU —
is_available()estTrue(monkey-patchée), le modèle est donc enregistré pour la migration de device automatique. - Dedicated GPU Spaces / GPU locale —
is_available()est genuinelyTrue. - CPU Spaces / CPU locale — se résout en
"cpu".
Ne codez pas en dur device="cuda" — ça casse sur les environnements CPU-only.
Le chargement rapide est le bon défaut
Chargez les modèles au scope du module, pas de façon lazy au premier appel. Le processus Space démarre avant l'arrivée d'un utilisateur, donc le coût du cold-start est payé une fois. Le chargement lazy (global model; if model is None: ..., les wrappers @lru_cache, les fonctions factory instanciant au premier appel) pousse juste ce coût vers le premier utilisateur.
Développement local : il suffit d'installer spaces
Ne wrappez pas import spaces dans try/except et ne redéfinissez pas spaces.GPU comme un fallback no-op pour les exécutions locales. En dehors de ZeroGPU, le package spaces est déjà un true no-op :
- Le comportement lourd (monkey-patching CUDA, init client, hooks de démarrage) est gated sur la variable env
SPACES_ZERO_GPU, définie uniquement sur ZeroGPU. @spaces.GPUretourne la fonction non décorée inchangée en dehors de ZeroGPU.import spacesau top-level effectue uniquement des imports légers.
L'image de base du Gradio SDK installe spaces sur chaque tier de matériel. Donc même après duplication d'un Space vers une GPU dédiée (T4, L4, A10G, etc.) ou CPU basique, aucun changement de code n'est nécessaire — import spaces réussit toujours et @spaces.GPU devient un passthrough transparent.
Anti-pattern
try:
import spaces
except ImportError:
class spaces: # type: ignore
@staticmethod
def GPU(func=None, **kwargs):
return func if func else (lambda f: f)
Problèmes :
- Le fallback doit mimer chaque forme d'appel
@spaces.GPU— décorateur nu,duration=...,size=..., générateurs, helpersaoti_*— et s'écarte à mesure que l'APIspacesgrandit. - Il cache
spacesderequirements.txt, même si le Space en a besoin au moment du déploiement. - Il résout un non-problème : le vrai package est déjà un no-op localement.
Faites ceci à la place
Ajoutez spaces aux dépendances et importez-le sans condition :
import spaces
@spaces.GPU
def generate(prompt: str) -> str:
...
Duration et Quota
Trois choses se passent quand vous déclarez @spaces.GPU(duration=N) :
- Vérification de max de tier — chaque tier de visiteur a un plafond
durationpar appel. Déclarer unedurationsupérieure au plafond échoue immédiatement avecZeroGPU illegal duration, indépendamment du quota restant. (Les numéros de tier changent au fil du temps — voir la documentation ZeroGPU.) - Pré-vérification de quota — la plateforme compare la
duration demandéepar rapport auquota restantde l'utilisateur. Sirestant < demandée, l'appel échoue avecZeroGPU quota exceeded— même si le travail réel aurait convenu. Le message d'erreur affiche les numéros explicites, ex."60s demandés vs. 30s restants". Une tâche de 10 secondes restée au défaut 60s bloque donc l'utilisateur dès que son quota restant tombe sous 60s. - Priorité de file d'attente — la file d'attente est au niveau du nœud (les requêtes de tous les Spaces sur le même nœud sont en concurrence pour les slots GPU), et une
durationdéclarée plus courte classe plus haut.
Les trois favorisent la déclaration de la plus petite duration réaliste — y compris pour les tâches courtes. Un explicite @spaces.GPU(duration=15) sur une tâche de 10 secondes évite les rejets prématurés quota exceeded et classe plus haut en file d'attente.
xlargedouble la requête.requested = N * 2quandsize="xlarge", à la fois pour la vérification de max de tier et la pré-vérification de quota. Donc@spaces.GPU(duration=60, size="xlarge")est en interne une requête 120s.
Duration dynamique pour les charges de travail variables
Pour les charges de travail dont le runtime dépend des inputs, passez un callable qui estime par requête. Une duration statique haute verrouille les utilisateurs low-tier (dont le plafond de tier peut être plus petit que la valeur statique) et réserve inutilement le quota pour les inputs légers.
def estimate_duration(prompt, steps):
return int(steps * 3.5)
@spaces.GPU(duration=estimate_duration)
def generate(prompt, steps):
return pipe(prompt, num_inference_steps=steps).images[0]
Pour la distinction complète entre illegal duration vs quota exceeded, les limites d'exécutions par jour, la fenêtre de quota 24h et la facturation pay-as-you-go, voir references/how-quota-works.md.
Isolation des processus et Pickle
Les fonctions décorées par @spaces.GPU s'exécutent dans un processus séparé géré par le scheduler ZeroGPU. Les arguments et valeurs de retour traversent la limite du processus via sérialisation pickle.
Conséquences :
- Seuls les objets picklables peuvent être passés ou retournés. Les poignées de fichiers ouvertes, connexions à base de données, verrous, lambdas et closures sur un état non picklable lèveront
PicklingError. - Ne retournez PAS les tenseurs CUDA directement. Unpickler un tenseur CUDA dans le processus principal déclenche
torch.cuda._lazy_init(), que ZeroGPU bloque. Convertissez en CPU d'abord : retourneztensor.cpu()outensor.cpu().numpy(). - Les tenseurs CPU, arrays numpy, PIL Images et objets Python simples fonctionnent bien.
- Les objets volumineux entraînent une surcharge de sérialisation. Préférez les retours légers (tenseurs, arrays, chemins de fichiers, strings base64) aux graphes d'objets complexes.
Sémantique gr.State à travers la limite
Parce que les handlers s'exécutent dans un processus séparé, les valeurs gr.State sont picklées à chaque yield — elles ne sont PAS partagées par référence.
- Le générateur reçoit une copie de l'état (
id()diffère de celui de l'appelant). - Les mutations en place à l'intérieur du générateur sont invisibles aux autres handlers jusqu'à ce que l'état muté soit explicitement cédé.
- Yielder
gr.update()pour un slotgr.Stateignore la mise à jour — les autres handlers continuent à voir la valeur pré-yield. - Chaque yield retournant l'objet state crée une nouvelle copie via pickle.
Recommandations pratiques :
- Ne supposez PAS la sémantique par référence pour
gr.Statesur ZeroGPU. Le code qui mute l'état dans un générateur et s'attend à ce qu'un autre handler voit ces mutations utilisera silencieusement des données périmées. - Chaque yield incluant une valeur
gr.Statedéclenche un allez-retour pickle complet. Pour un état volumineux (sessions modèle, buffers de frames), minimisez la fréquence de yield — idéalement une fois à la fin. Utilisezgr.update()pour le slot state sur les yields intermédiaires. - Les tenseurs CUDA à l'intérieur de l'état doivent être déplacés vers CPU avant le yield — même problème
torch.cuda._lazy_init()que ci-dessus.
Concurrence
Les handlers s'exécutent concurremment par défaut sur ZeroGPU. C'est obligatoire, pas opt-in. Le code qui fonctionnait en tests single-user peut silencieusement corrompre ou fuir des données en production.
Trois règles. Traitement complet avec exemples dans references/concurrency.md.
- Pas d'état global mutable. Les requêtes concurrentes s'écrasent mutuellement.
- Pas de chemins de fichiers fixes pour les outputs. Les requêtes concurrentes écrasent le même fichier. Utilisez
tempfilepour les chemins uniques. - Les globals en lecture seule sont sûrs. Les objets modèle, tokenizers, configs chargés une fois au démarrage et uniquement lus pendant les requêtes sont sûrs et encouragés.
Granularité d'appel
Chaque entrée dans une fonction @spaces.GPU porte un coût non trivial — allez-retour pickle à travers la limite du processus, warm-up du worker, ré-attachement CUDA, et un nouveau passage par la file d'attente au niveau du nœud. Appeler une fonction décorée depuis dans une boucle chaude multiplie ces coûts et ajoute un nouveau mode d'échec : une itération ultérieure peut échouer à acquérir un slot GPU, paralysant tout le job à mi-chemin.
Décorez la fonction externe qui possède la boucle, pas le worker par itération :
# Éviter — N entrées GPU pour N frames
def process_video(frames):
return [process_frame(f) for f in frames]
@spaces.GPU(duration=...)
def process_frame(frame):
...
# Préférer — une entrée GPU pour la vidéo entière
@spaces.GPU(duration=...)
def process_video(frames):
return [process_frame(f) for f in frames]
def process_frame(frame):
...
Si la boucle mélange du travail CPU lourd avec du travail GPU, wrapper la boucle entière facture ce temps CPU contre le quota de l'utilisateur. Quand ce coût est matériel, batching du travail GPU pour que le pré/post-processing CPU reste en dehors du décorateur est une optimisation situationnelle — pas le défaut.
Contraintes de construction CUDA
HF Spaces construit les images Docker dans un environnement CPU-only. Sur ZeroGPU, la phase de construction n'a pas de nvcc parce que l'image de base est python:3.13 (les Spaces GPU dédiées utilisent nvidia/cuda:*-devel-* et ont nvcc à la construction). Un package dépendant de CUDA dont la seule distribution est sdist — ex. bare flash-attn — ne peut donc pas être installé via requirements.txt sur ZeroGPU. Seules les wheels pré-construites fonctionnent.
Le runtime ZeroGPU a nvcc disponible, monté depuis une image devel CUDA à /cuda-image depuis 2025-07 (initialement ajouté pour le support AoTI). C'est ce qui rend les workflows torch.export / AoTI possibles à l'intérieur des appels @spaces.GPU.
En résumé : installez chaque package dépendant de CUDA depuis une wheel pré-construite. Si aucune wheel n'est disponible sur PyPI, construisez-en une en externe (ex. hébergez sur HF Hub) et épinglez l'URL. Pour flash-attn, la page releases upstream propose une matrice de wheels assez complète couvrant la plupart des combinaisons Python × CUDA × torch.
Pour la lecture de tags wheel (cxx11 ABI, cu12torch2.X, cp3XX), la dérive de torch-family side-car, et le fallback kernels-community, voir references/cuda-and-deps.md.
Caching des exemples
Le comportement de gr.Examples dépend de l'environnement. Sur ZeroGPU spécifiquement :
cache_examplesdefaults àTrue(Spaces définitGRADIO_CACHE_EXAMPLES=true).cache_modedefaults à"lazy"(Spaces définitGRADIO_CACHE_MODE=lazyuniquement sur ZeroGPU).
ZeroGPU defaults à lazy parce que le caching rapide pré-exécute chaque exemple au démarrage de l'app, mais ZeroGPU n'a pas de GPU attachée au démarrage — uniquement lors de la gestion des requêtes. Le caching rapide des exemples liés à la GPU échouerait là.
Quand cache_examples=True, le paramètre run_on_click / run_examples_on_click est silencieusement ignoré. Si votre app s'appuie sur le comportement click-remplissage uniquement, définissez cache_examples=False explicitement pour le préserver.
Pour reproduire le comportement de caching d'exemples ZeroGPU localement :
GRADIO_CACHE_EXAMPLES=true GRADIO_CACHE_MODE=lazy python app.py
Gestion des dépendances
Épingle python_version dans le frontmatter du README
Épingler python_version est effectivement requis pour ZeroGPU. Le default du runtime est actuellement Python 3.10, donc un environnement local utilisant 3.11+ échouera à installer sur le Space sans une épingle explicite. Épinglez à une version supportée par ZeroGPU (3.12 est un bon défaut) ; la liste supportée officielle vit dans la documentation ZeroGPU — ne codez pas en dur la liste complète, référez-vous aux docs.
# frontmatter README.md
python_version: "3.12"
Les formes "3.12" et "3.12.12" sont acceptées.
N'épinglez pas spaces dans requirements.txt
La plateforme Space épingle sa propre version de spaces. Une épingle conflictuelle dans requirements.txt fait échouer la résolution pip au moment de la construction.
Règle : N'incluez pas
spacesdansrequirements.txt.
Comment réaliser cela dépend de votre tooling :
requirements.txtécrit à la main : omettez simplementspaces.- uv (
pyproject.toml-managed) : déclarezspacesdanspyproject.tomlpour que uv co-résolve les contraintes transitives (notammentpsutil, quespacesépingle), puis excluez-le de l'export :uv export --no-hashes --no-dev --no-emit-package spaces -o requirements.txtSans
spacesdanspyproject.toml, uv ne peut pas voir ses contraintes transitives et peut résoudre des versions incompatibles au moment de la construction. - pip-tools (
pip-compile) / Poetry : utilisez le mécanisme d'exclusion équivalent.
Épinglez torch pour correspondre aux tags wheel
Si vous installez une wheel dépendante de CUDA via direct URL, le nom de fichier wheel encode le torch major.minor contre lequel il a été construit (ex. cu12torch2.8). Épinglez torch==X.Y.Z dans requirements.txt pour correspondre — sinon pip peut résoudre torch à une version différente et le Space échoue au premier import. Les détails et l'alternative kernels-community sont dans references/cuda-and-deps.md.