huggingface-zerogpu

Par huggingface · skills

Démos IA et calcul GPU avec Gradio Spaces et Hugging Face Spaces ZeroGPU. À utiliser lors de l'écriture ou de la révision de code utilisant `@spaces.GPU`, de la configuration de `python_version` ou de `requirements.txt` pour un Space ZeroGPU, ou lors du traitement de contraintes de code spécifiques à ZeroGPU — isolation de processus par pickle, sémantique de `gr.State` à travers la frontière worker, absence de `torch.compile` (utiliser AoTI à la place), builds CUDA wheel uniquement (pas de `nvcc` au build ou à l'exécution), dimensionnement large vs xlarge, et callables de durée dynamique. Veiller à utiliser cette skill chaque fois que l'utilisateur mentionne ZeroGPU, `@spaces.GPU`, ou le package Python `spaces`, ou rencontre des erreurs de code spécifiques à ZeroGPU comme `PicklingError` à travers la frontière worker, `illegal duration`, ou des échecs de build de wheel `flash-attn` — même lorsque l'utilisateur ne demande pas explicitement de conseils de codage ZeroGPU. Se déclenche sur `import spaces` ou `@spaces.GPU` dans le code.

npx skills add https://github.com/huggingface/skills --skill huggingface-zerogpu

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 :

  1. 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).
  2. 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.
  3. Définissez duration pour correspondre au pire cas réaliste de la charge de travail (par défaut 60s). La plateforme pré-vérifie la duration demandée par rapport au quota restant de 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 avec quota exceeded dès que le quota restant de l'utilisateur tombe sous 60s. Une duration déclarée plus petite classe également plus haut dans la file d'attente au niveau du nœud. Voir « Duration and Quota » ci-dessous.
  4. torch.compile n'est PAS supporté. Utilisez plutôt la compilation ahead-of-time de PyTorch (AoTI) (torch 2.8+).
  5. 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() retourne True globalement.
  • 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 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)
  • ZeroGPUis_available() est True (monkey-patchée), le modèle est donc enregistré pour la migration de device automatique.
  • Dedicated GPU Spaces / GPU localeis_available() est genuinely True.
  • 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.GPU retourne la fonction non décorée inchangée en dehors de ZeroGPU.
  • import spaces au 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 :

  1. Le fallback doit mimer chaque forme d'appel @spaces.GPU — décorateur nu, duration=..., size=..., générateurs, helpers aoti_* — et s'écarte à mesure que l'API spaces grandit.
  2. Il cache spaces de requirements.txt, même si le Space en a besoin au moment du déploiement.
  3. 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) :

  1. Vérification de max de tier — chaque tier de visiteur a un plafond duration par appel. Déclarer une duration supérieure au plafond échoue immédiatement avec ZeroGPU illegal duration, indépendamment du quota restant. (Les numéros de tier changent au fil du temps — voir la documentation ZeroGPU.)
  2. Pré-vérification de quota — la plateforme compare la duration demandée par rapport au quota restant de l'utilisateur. Si restant < demandée, l'appel échoue avec ZeroGPU 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.
  3. 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 duration dé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.

xlarge double la requête. requested = N * 2 quand size="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 : retournez tensor.cpu() ou tensor.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 slot gr.State ignore 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.State sur 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.State dé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. Utilisez gr.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.

  1. Pas d'état global mutable. Les requêtes concurrentes s'écrasent mutuellement.
  2. Pas de chemins de fichiers fixes pour les outputs. Les requêtes concurrentes écrasent le même fichier. Utilisez tempfile pour les chemins uniques.
  3. 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_examples defaults à True (Spaces définit GRADIO_CACHE_EXAMPLES=true).
  • cache_mode defaults à "lazy" (Spaces définit GRADIO_CACHE_MODE=lazy uniquement 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 spaces dans requirements.txt.

Comment réaliser cela dépend de votre tooling :

  • requirements.txt écrit à la main : omettez simplement spaces.
  • uv (pyproject.toml-managed) : déclarez spaces dans pyproject.toml pour que uv co-résolve les contraintes transitives (notamment psutil, que spaces épingle), puis excluez-le de l'export :
    uv export --no-hashes --no-dev --no-emit-package spaces -o requirements.txt

    Sans spaces dans pyproject.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.

Skills similaires