CULTIVA IA

ZeroGPU Demos — Guia de Implementacion

Patrones, reglas y codigo production-ready para desplegar demos de IA con GPU gratuita en HuggingFace Spaces. Proyecto: generador de copy + imagen para CULTIVA IA.
Gradio SDK · ZeroGPU
Python 3.12
Mistral-7B + SDXL
Concurrencia segura
20s
Duration texto
Mistral-7B · copy mkt
45s
Duration imagen
SDXL · xlarge
2x
Coste quota xlarge
vs large (default)
0
spaces en requirements
NO incluir — rompe build
📁
Estructura del Proyecto
Archivos
Configuracion
cultiva-ia-demo/
  app.py # punto de entrada Gradio
  requirements.txt # sin 'spaces'
  README.md # frontmatter HF
  modules/
    text_gen.py # @spaces.GPU texto
    image_gen.py # @spaces.GPU imagen
    utils.py # helpers CPU
✓
Separacion clara CPU / GPU Solo las funciones con computo real llevan @spaces.GPU. El resto (parsing, validacion, logging) permanece en CPU y no consume quota.
yaml README.md
---
title: CULTIVA IA — Demo Generativa
emoji: 🌱
colorFrom: green
colorTo: blue
sdk: gradio
sdk_version: "5.x"
app_file: app.py
pinned: false
python_version: "3.12"  # obligatorio para ZeroGPU
license: apache-2.0
---
text requirements.txt
# NO incluir 'spaces' aqui — la plataforma lo gestiona
gradio>=5.0
torch==2.8.0          # pin para coincidir con wheel flash-attn
transformers>=4.46
diffusers>=0.31
accelerate>=1.0
Pillow>=10.0
sentencepiece
protobuf
# flash-attn: wheel precompilado (no sdist — ZeroGPU sin nvcc en build)
https://github.com/Dao-AILab/flash-attention/releases/download/v2.7.4.post1/flash_attn-2.7.4.post1+cu12torch2.8cxx11abiTRUE-cp312-cp312-linux_x86_64.whl
✍️
modules/text_gen.py — Generador de Copy
python modules/text_gen.py
import spaces
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

# Carga eager en module scope — ZeroGPU registra el tensor
_MODEL_ID = "mistralai/Mistral-7B-Instruct-v0.3"
_tokenizer = AutoTokenizer.from_pretrained(_MODEL_ID)
_model = AutoModelForCausalLM.from_pretrained(
    _MODEL_ID,
    torch_dtype=torch.bfloat16,
    device_map="cuda",   # ZeroGPU monkey-patchea is_available()
)

# duration=20 — evita quota exceeded en tareas cortas
@spaces.GPU(duration=20)
def generate_copy(producto: str, tono: str) -> str:
    """Genera 3 variantes de copy de marketing. CPU-safe return."""
    prompt = _build_prompt(producto, tono)
    inputs = _tokenizer(prompt, return_tensors="pt").to("cuda")
    with torch.no_grad():
        out = _model.generate(
            **inputs,
            max_new_tokens=350,
            temperature=0.8,
            do_sample=True,
        )
    # Retornar string, no tensor CUDA — evita torch.cuda._lazy_init()
    return _tokenizer.decode(out[0], skip_special_tokens=True)
⚠
No retornar tensores CUDA Los tensores cruzan el boundary via pickle. Retornar un tensor CUDA en el proceso principal dispara torch.cuda._lazy_init() que ZeroGPU bloquea. Convertir siempre: .cpu() o decodificar a string.
🎨
modules/image_gen.py — Generador SDXL
python modules/image_gen.py
import spaces
import torch
import tempfile
import os
from diffusers import StableDiffusionXLPipeline

# Carga eager — peso en disk offload hasta que llega @GPU
_pipe = StableDiffusionXLPipeline.from_pretrained(
    "stabilityai/stable-diffusion-xl-base-1.0",
    torch_dtype=torch.float16,
    use_safetensors=True,
).to("cuda")

# xlarge: SDXL necesita VRAM full; duration=45 estimado para 30 steps
@spaces.GPU(duration=45, size="xlarge")
def generate_image(descripcion: str, estilo: str) -> str:
    """Retorna path a fichero temporal — picklable y ligero."""
    prompt = f"professional product photo, {descripcion}, {estilo} style"
    image = _pipe(
        prompt=prompt,
        num_inference_steps=30,
        guidance_scale=7.5,
        width=1024, height=1024,
    ).images[0]

    # tempfile: evita colisiones en concurrencia (varios usuarios a la vez)
    tmp = tempfile.NamedTemporaryFile(
        suffix=".png", delete=False,
        dir="/tmp/cultiva_outputs"
    )
    image.save(tmp.name)
    return tmp.name  # str path — picklable, no PIL Image en cross-process
ℹ
xlarge + 2x quota Con size="xlarge" el duration declarado se duplica internamente. duration=45 equivale a consumir 90s de quota. Usar solo cuando SDXL genuinamente necesita la VRAM completa.
⚡
Concurrencia — Reglas Criticas
  • ERROR No usar estado mutable global. Dos requests concurrentes sobreescriben el mismo dict/lista. Cada llamada debe ser pura o usar variables locales.
  • ERROR No usar rutas fijas para outputs. /tmp/output.png fija sera sobreescrita por otro usuario. Usar tempfile.NamedTemporaryFile siempre.
  • OK Globals read-only son seguros. _model, _tokenizer, _pipe cargados una vez en startup y solo leidos durante requests: correcto y recomendado.
  • AVISO gr.State se serializa en cada yield. No asumir semántica de referencia. Mutar estado en un generator es invisible para otros handlers hasta que se yielda explicitamente.
  • AVISO Decorar la funcion exterior, no el bucle interior. Llamar a @spaces.GPU N veces en un loop multiplica costes de pickle + queue. Envolver el bucle entero con un solo decorator.
⏱
Duration y Quota — Logica de Errores
1
Tier check
duration ≤ cap del tier
2
Quota check
remaining ≥ requested
3
Queue
menor duration = mayor prioridad
4
Ejecucion
GPU adjunta al proceso
Error Causa Solucion
ZeroGPU illegal duration duration > cap del tier del usuario Reducir duration o usar callable estimador
ZeroGPU quota exceeded remaining quota < requested (aunque la tarea dure menos) Declarar el minimo realista de duration, no el maximo posible
PicklingError Retornar objeto no picklable (tensor CUDA, file handle...) Convertir a CPU/string/path antes de retornar
Build failed (flash-attn) sdist sin nvcc en build phase Instalar wheel precompilado por URL directa en requirements.txt
✓
Duration dinamico para cargas variables Si los pasos de SDXL son configurables por el usuario, pasar duration=lambda desc, steps: int(steps * 1.5) en lugar de un fijo. Evita bloquear usuarios de tier bajo con duraciones sobredimensionadas.
🖥
Hardware ZeroGPU — Sizing
size Slice GPU Quota cost Uso recomendado
large (default) 1/2 GPU fisica 1x Mistral-7B, embeddings, clasificacion, texto
xlarge GPU completa 2x SDXL, modelos >13B, inferencia batched pesada
⚠
GPU backing cambia sin aviso Documentacion anterior menciona A100 o H200 — pueden estar desactualizados. Consultar siempre los docs oficiales de ZeroGPU antes de dimensionar cargas que dependan de VRAM especifica.
AccionDondePor que
.to("cuda") Module scope ✓ ZeroGPU registra el tensor → disk offload
Inferencia / kernels CUDA Dentro de @spaces.GPU ✓ GPU real solo disponible en la funcion decorada
torch.compile() Nunca ✗ No soportado en ZeroGPU — usar AoTI (torch 2.8+)
is_available() para branch Evitar Siempre True (monkey-patch) — no indica GPU real disponible
🚫
Anti-Patrones vs Patrones Correctos
python app.py — NO hacer esto
# INCORRECTO: spaces ya es no-op fuera de ZeroGPU
try:
    import spaces
except ImportError:
    class spaces:
        @staticmethod
        def GPU(func=None, **kwargs):
            return func if func else (lambda f: f)
# Problemas:
# 1. Debe imitar TODA la API de spaces (size=, aoti_*, generadores)
# 2. Oculta 'spaces' de requirements.txt — falla en deploy
# 3. Soluciona un no-problema: el paquete real ya es no-op local
python app.py — correcto
# CORRECTO: import incondicional + decorator
import spaces  # no-op local, activo en ZeroGPU

@spaces.GPU(duration=20)
def generate_copy(producto: str, tono: str) -> str:
    ...

# Correcto en todos los entornos:
# - ZeroGPU: decorator activo, GPU disponible en la call
# - GPU dedicada (T4, A10G): decorator transparente pass-through
# - CPU local: decorator transparente pass-through
# Sin cambios de codigo entre entornos
✓
Agregar spaces a pyproject.toml para que uv co-resuelva sus dependencias transitivas (psutil), pero excluirlo del export: uv export --no-emit-package spaces -o requirements.txt
python
# INCORRECTO: lazy load con global
_model = None

@spaces.GPU
def generate(p):
    global _model
    if _model is None:
        # 1er usuario paga cold-start
        # ademas: race condition con concurrencia
        _model = load_model()
    return _model(p)
python
# CORRECTO: carga en module scope
# Cold-start pagado una vez en startup
# No hay race condition — read-only en requests
_model = load_model().to("cuda")

@spaces.GPU(duration=20)
def generate(p):
    return _model(p)
✅
Checklist de Deploy — CULTIVA IA Demo
  • OK python_version: "3.12" en README.md frontmatter
  • OK spaces NO incluido en requirements.txt
  • OK torch==2.8.0 pinado para coincidir con wheel de flash-attn
  • OK flash-attn instalado desde URL de wheel precompilado (cu12torch2.8)
  • OK sdk: gradio (no Docker, no Streamlit) — ZeroGPU solo disponible en Gradio
  • OK Modelos cargados eager en module scope, no en primera request
  • OK duration=20s (texto) y 45s (imagen) — valores minimos realistas
  • OK Outputs a tempfile.NamedTemporaryFile — sin colisiones entre usuarios
  • OK Retornos: strings y paths, nunca tensores CUDA directos
  • PENDIENTE cache_examples=False para demos donde el click-populate es el UX esperado