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