Estructura del Proyecto
Archivos
Configuracion
Arbol de archivos
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.
README.md frontmatter
---
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
---
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
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
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.pngfija sera sobreescrita por otro usuario. Usartempfile.NamedTemporaryFilesiempre. -
OK
Globals read-only son seguros.
_model,_tokenizer,_pipecargados 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.GPUN veces en un loop multiplica costes de pickle + queue. Envolver el bucle entero con un solo decorator.
Duration y Quota — Logica de Errores
Flujo de verificacion al llamar @spaces.GPU
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.
Modelo de disponibilidad CUDA
| Accion | Donde | Por 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
✗ Anti-patron: try/except import spaces
# 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
✓ Patron correcto: import directo
# 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
✗ Anti-patron: lazy loading
# 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)
✓ Patron correcto: eager loading
# 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
Configuracion y dependencias
-
OK
python_version: "3.12"en README.md frontmatter -
OK
spacesNO incluido en requirements.txt -
OK
torch==2.8.0pinado 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
Codigo y seguridad de concurrencia
- 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=Falsepara demos donde el click-populate es el UX esperado