
Desplegar un LLM con Modal: de pip install al endpoint de producción
La mayoría de las guías sobre el autoalojamiento de LLM evitan la parte más difícil: la infraestructura. Luchas con los drivers de CUDA, gestionas imágenes Docker, configuras el autoescalado, y aun así acabas pagando por GPUs inactivas a las 3 de la madrugada. Modal elimina todo eso. Escribes Python, despliegas y obtienes una URL.
Esta guía te lleva paso a paso para desplegar un LLM de código abierto en Modal con vLLM como motor de inferencia. Al final, tendrás un endpoint de API compatible con OpenAI en vivo sobre GPUs H100 que escala a cero cuando nadie lo usa.
¿Qué es Modal (y por qué usarlo para LLMs)?
Modal es una plataforma de cómputo serverless construida específicamente para cargas de trabajo de IA. Piensa en AWS Lambda, pero con soporte de GPU, facturación por segundo y una experiencia de desarrollador nativa en Python. Sin YAML, sin Dockerfiles, sin Kubernetes — defines toda tu infraestructura en un script de Python y despliegas con un solo comando.
Por qué se ha convertido en la opción preferida para desplegar LLMs:
- Facturación scale-to-zero — no pagas nada cuando tu endpoint no está procesando peticiones
- Precios de GPU por segundo — H100 a ~$3,95/h, A100 80 GB a ~$2,50/h, facturado por segundo
- Arranques en frío sub-segundo — los contenedores arrancan rápido, especialmente con snapshots de memoria
- $30/mes de créditos gratuitos — suficiente para experimentar sin cargo en tarjeta
- Sin DevOps — sin builds de Docker, sin Terraform, sin gestión de clústeres
Si estás ejecutando LLMs localmente y quieres darles una API decente sin gestionar servidores, Modal es el camino más corto.
Modal vs. RunPod vs. Lambda
| Característica | Modal | RunPod | Lambda |
|---|---|---|---|
| Modelo de facturación | Por segundo, scale-to-zero | Por segundo, cargo mínimo | Por hora, siempre activo |
| Arranque en frío | 2-4 segundos | 6-12 segundos (grande) | N/A (persistente) |
| Disponibilidad GPU | H100, A100, L40S, T4 | A100, H100, A6000 | H100, A100 |
| Infraestructura | Python puro, sin archivos de config | Docker, más control | Acceso completo a VM |
| Nivel gratuito | $30/mes en créditos | Ninguno | Ninguno |
| Ideal para | Cargas en ráfagas/dev | Tráfico de inferencia estable | Entrenamiento de alta utilización |
Conclusión: Modal gana para cargas de trabajo en ráfagas y desarrollo. Si tu utilización de GPU supera consistentemente el 40%, una instancia dedicada en RunPod o Lambda es más barata. Para todo lo demás — prototipado, APIs intermitentes, demos — el modelo scale-to-zero de Modal ahorra dinero de verdad.
Requisitos previos
Antes de empezar, necesitas tres cosas:
- Python 3.10+ instalado localmente
- Una cuenta de Modal — regístrate gratis en modal.com
- Una cuenta de Hugging Face — para acceder a los modelos (la mayoría son privados)
Eso es todo. Sin GPU en tu máquina local, sin toolkit CUDA, sin Docker.
Paso 1: Instalar Modal y autenticarse
Abre un terminal e instala el paquete Python de Modal:
pip install modalLuego ejecuta el comando de configuración para vincular tu entorno local con tu cuenta de Modal:
modal setupEsto abre una ventana del navegador para autenticarse. Una vez confirmado, Modal almacena un token localmente. No tendrás que hacerlo de nuevo.
Paso 2: Definir la imagen del contenedor
Los contenedores de Modal se definen en Python. Especificas la imagen base, instalas dependencias y configuras variables de entorno — todo como código. Crea un archivo llamado app.py:
import modal
# Definir la imagen del contenedor con CUDA, Python y vLLM
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install(
"vllm==0.13.0",
"huggingface-hub==0.36.0",
)
)
app = modal.App("llm-endpoint", image=vllm_image)Algunos puntos a destacar. No hay Dockerfile — esa cadena modal.Image lo reemplaza por completo. La imagen base incluye NVIDIA CUDA 12.8 con Ubuntu 22.04, y encima instalamos vLLM y el cliente de Hugging Face Hub.
Paso 3: Configurar el almacenamiento de modelos con Volumes
Los pesos de los LLM son grandes (un modelo de 7.000 millones de parámetros ocupa ~14 GB en fp16). No quieres descargarlos cada vez que arranque un contenedor. Los Modal Volumes te dan almacenamiento persistente que se monta directamente en tus contenedores:
# Volúmenes persistentes para cachear los pesos del modelo
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"Aquí usamos Qwen3-4B-Thinking (FP8) — un modelo cuantizado de 4.000 millones de parámetros que es rápido, capaz y cabe en una sola GPU. Puedes cambiarlo por cualquier modelo de Hugging Face: Llama 3.1 8B, Mistral 7B, o cualquier cosa que soporte vLLM.
¿Por qué FP8? Reduce el uso de memoria aproximadamente a la mitad en comparación con fp16, lo que significa que puedes ejecutar modelos más grandes en la misma GPU — o modelos más pequeños en GPUs más baratas. Si tienes curiosidad sobre los compromisos de cuantización, nuestra guía para ejecutar LLMs localmente cubre los formatos de precisión en detalle.
Paso 4: Crear la función servidor de vLLM
Aquí es donde ocurre la magia de Modal. Decoras una función Python con requisitos de GPU, configuración de escalado y una anotación de servidor web. Modal se encarga del resto:
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager", # Arranques en frío más rápidos
]
subprocess.Popen(" ".join(cmd), shell=True)Analicemos los decoradores clave:
gpu="H100:1"— solicita una sola GPU H100. Cambia a"A100-80GB:1"para inferencia más barata, o"H100:2"para modelos de 70B+scaledown_window=15 * MINUTES— mantiene el contenedor caliente durante 15 minutos tras la última petición, luego escala a cero@modal.concurrent(max_inputs=32)— permite hasta 32 peticiones simultáneas por contenedor (vLLM gestiona el batching internamente)@modal.web_server(port=8000)— expone el servidor HTTP de vLLM directamente como endpoint web de Modal--enforce-eager— omite la compilación de grafos CUDA para arranques en frío más rápidos (compromiso: rendimiento máximo ligeramente inferior)
El scaledown_window es tu principal palanca de costes. Configúralo en 5 minutos para desarrollo, 15-30 minutos para APIs de producción con tráfico regular.
Paso 5: Desplegar a producción
Un comando. Eso es todo:
modal deploy app.pyModal construye la imagen del contenedor, la sube a su registro y devuelve una URL en vivo:
✓ Created objects.
├── 🔨 Created mount /app.py
├── 🔨 Created volume huggingface-cache
├── 🔨 Created volume vllm-cache
└── 🔨 Created web function serve => https://your-workspace--llm-endpoint-serve.modal.runEl primer despliegue tarda unos minutos porque descarga los pesos del modelo en el volumen. Los despliegues posteriores (y los arranques en frío) son mucho más rápidos ya que los pesos están cacheados.
Para desarrollo, usa modal serve app.py — recarga en caliente al modificar archivos y te da una URL temporal.
Paso 6: Llamar a tu endpoint (compatible con OpenAI)
Tu servidor vLLM desplegado expone una API compatible con OpenAI en /v1/chat/completions. Puedes usar el SDK estándar de Python de OpenAI para llamarlo — solo apunta la URL base a tu endpoint de Modal:
from openai import OpenAI
client = OpenAI(
api_key="not-needed", # vLLM no requiere autenticación por defecto
base_url="https://your-workspace--llm-endpoint-serve.modal.run/v1",
)
response = client.chat.completions.create(
model="Qwen/Qwen3-4B-Thinking-2507-FP8",
messages=[
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "Explain what vLLM is in two sentences."},
],
temperature=0.7,
max_tokens=256,
)
print(response.choices[0].message.content)Esto también funciona con curl:
curl -X POST https://your-workspace--llm-endpoint-serve.modal.run/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen3-4B-Thinking-2507-FP8",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 128
}'Cualquier herramienta que soporte una API compatible con OpenAI funcionará — LangChain, LlamaIndex, tu propia aplicación. Si estás enrutando peticiones entre varios endpoints de LLM, una herramienta de gateway para LLM puede ayudarte a gestionar failover y balanceo de carga.
Consejos de optimización de costes
La facturación por segundo de Modal ya es más eficiente que los precios por hora, pero puedes sacarle aún más partido:
1. Usar cuantización FP8
Los modelos FP8 usan aproximadamente la mitad de la VRAM que sus equivalentes fp16. Un Qwen3-8B en FP8 cabe en una sola H100, mientras que la versión fp16 necesita la mayor parte de los 80 GB de esa GPU. Menos VRAM significa que puedes usar GPUs más baratas (A100 40 GB, L40S) para modelos más pequeños.
2. Ajustar la ventana de scale-down
El parámetro scaledown_window controla cuánto tiempo un contenedor permanece caliente tras la última petición:
| Escenario | Ventana recomendada | Por qué |
|---|---|---|
| Desarrollo/pruebas | 5 minutos | Ahorrar dinero, los arranques en frío son aceptables |
| API interna (ocasional) | 10-15 minutos | Equilibrio coste vs latencia |
| Producción (tráfico regular) | 20-30 minutos | Minimizar arranques en frío |
| Producción de alto tráfico | Usar min_containers=1 | Mantener uno siempre caliente |
3. Elegir la GPU correcta
No elijas siempre H100. Los modelos más pequeños no la necesitan:
| Tamaño del modelo | GPU recomendada | Coste aprox./hora |
|---|---|---|
| 1-4B parámetros | L4 o T4 | $0,59 – $0,80 |
| 7-8B parámetros | A10 o L40S | $1,10 – $1,95 |
| 13-14B parámetros | A100 40 GB | $2,10 |
| 30-70B parámetros | A100 80 GB o H100 | $2,50 – $3,95 |
| 70B+ parámetros | H100 x2 | $7,90 |
4. Activar el caché de prompts
Si tus cargas de trabajo implican prompts de sistema repetidos o prefijos compartidos, el caché automático de prefijos de vLLM puede reducir significativamente la latencia y el cómputo. Actívalo añadiendo --enable-prefix-caching al comando vLLM serve. Para una inmersión más profunda en cómo funciona el caché en diferentes proveedores, consulta nuestra guía de caché de prompts para LLM.
5. Usar --enforce-eager para optimizar arranques en frío
Por defecto, vLLM compila grafos CUDA al arrancar, lo que tarda 1-3 minutos extra. La bandera --enforce-eager omite esta compilación. Cambias ~10-15% de rendimiento máximo por arranques en frío dramáticamente más rápidos. Para cargas de trabajo en ráfagas donde la latencia importa más que el rendimiento bruto, casi siempre es la decisión correcta.
Más allá: modelos con ajuste fino
Una vez que te sientas cómodo desplegando modelos base, el siguiente paso natural es desplegar tu propia versión ajustada. El flujo de trabajo es idéntico — simplemente apuntas MODEL_NAME a tu repositorio de Hugging Face o a un volumen de Modal que contenga tus pesos ajustados.
Modal también admite la ejecución de tareas de ajuste fino directamente en sus GPUs. Puedes entrenar un adaptador LoRA en Modal, guardarlo en un volumen y desplegar el modelo fusionado — todo sin salir de la plataforma. Nuestra guía de ajuste fino de LLM cubre la parte de entrenamiento en profundidad.
El app.py completo
Aquí está el script de despliegue completo en un bloque listo para copiar y pegar:
import modal
# --- Definición de la imagen ---
vllm_image = (
modal.Image.from_registry(
"nvidia/cuda:12.8.0-devel-ubuntu22.04", add_python="3.12"
)
.entrypoint([])
.pip_install("vllm==0.13.0", "huggingface-hub==0.36.0")
)
# --- Volúmenes para caché del modelo ---
hf_cache = modal.Volume.from_name("huggingface-cache", create_if_missing=True)
vllm_cache = modal.Volume.from_name("vllm-cache", create_if_missing=True)
# --- Configuración del modelo ---
MODEL_NAME = "Qwen/Qwen3-4B-Thinking-2507-FP8"
MODEL_REVISION = "953532f942706930ec4bb870569932ef63038fdf"
app = modal.App("llm-endpoint", image=vllm_image)
N_GPU = 1
MINUTES = 60
VLLM_PORT = 8000
@app.function(
gpu=f"H100:{N_GPU}",
scaledown_window=15 * MINUTES,
timeout=10 * MINUTES,
volumes={
"/root/.cache/huggingface": hf_cache,
"/root/.cache/vllm": vllm_cache,
},
)
@modal.concurrent(max_inputs=32)
@modal.web_server(port=VLLM_PORT, startup_timeout=10 * MINUTES)
def serve():
import subprocess
cmd = [
"vllm", "serve",
MODEL_NAME,
"--revision", MODEL_REVISION,
"--served-model-name", MODEL_NAME,
"--host", "0.0.0.0",
"--port", str(VLLM_PORT),
"--tensor-parallel-size", str(N_GPU),
"--enforce-eager",
]
subprocess.Popen(" ".join(cmd), shell=True)Despliega con modal deploy app.py, cambia MODEL_NAME por cualquier modelo de Hugging Face, y ya estás en vivo.
Preguntas frecuentes
¿Cuánto cuesta ejecutar un LLM en Modal?
Depende de la GPU y de cuánto tiempo tu endpoint permanezca caliente. Un Qwen3-4B en una H100 cuesta ~$3,95/h de uso activo. Con scale-to-zero y una ventana de scale-down de 15 minutos, un endpoint poco usado podría costar $5-15/mes. El crédito gratuito mensual de $30 cubre mucha experimentación.
¿Modal escala a cero?
Sí — ese es uno de sus principales argumentos de venta. Cuando no llegan peticiones durante la duración de tu scaledown_window, el contenedor se apaga y dejas de pagar. La siguiente petición activa un arranque en frío (típicamente 2-10 segundos dependiendo del tamaño del modelo y si usas --enforce-eager).
¿Puedo desplegar Llama 3.1 o Mistral en Modal?
Por supuesto. Cambia la constante MODEL_NAME por cualquier modelo compatible con vLLM: meta-llama/Llama-3.1-8B-Instruct, mistralai/Mistral-7B-Instruct-v0.3, o cientos de otros en Hugging Face. Para modelos de 70B+, cambia N_GPU a 2 y usa gpu="H100:2".
¿Cómo se comparan los arranques en frío con RunPod?
Los arranques en frío de Modal son típicamente de 2-4 segundos para el contenedor en sí, más el tiempo de carga del modelo. Con los pesos del modelo cacheados en un Volume y --enforce-eager activado, estás mirando 10-30 segundos en total para un modelo de 7-8B. Los arranques en frío serverless de RunPod van desde menos de 200 ms (cacheado) hasta 6-12 segundos para contenedores más grandes — aunque su modelo always-on evita los arranques en frío por completo.
¿El endpoint vLLM de Modal es realmente compatible con OpenAI?
Sí. vLLM implementa los mismos endpoints /v1/chat/completions, /v1/completions y /v1/models que usa OpenAI. Puedes apuntar el SDK oficial de Python openai a tu URL de Modal y funciona de inmediato. El streaming, el function calling y el modo JSON funcionan todos.
¿Necesito una GPU en mi máquina local?
No. Tu máquina local solo ejecuta el CLI de Modal. Todo el trabajo de GPU ocurre en la infraestructura cloud de Modal. Podrías desplegar desde un Chromebook si quisieras.
¿Cómo añado autenticación a mi endpoint?
Los endpoints web de Modal son públicos por defecto. Para producción, añade una verificación simple de clave API en el código de tu aplicación, o usa las funciones de autenticación web integradas de Modal. También puedes configurar una capa de proxy usando una gateway de LLM que gestione la autenticación, la limitación de velocidad y el enrutamiento.
¿Cuál es la diferencia entre modal serve y modal deploy?
modal serve crea un endpoint temporal que recarga en caliente cuando editas tu código — perfecto para desarrollo. modal deploy crea un endpoint persistente, listo para producción, con una URL estable. Usa serve mientras iteras, deploy cuando estés listo para lanzar.
¿Puedo usar SGLang en lugar de vLLM?
Sí. La documentación de Modal incluye ejemplos de SGLang junto a vLLM. SGLang tiende a tener menos sobrecarga para cargas de trabajo con mucho decode y modelos pequeños. vLLM es generalmente mejor para cargas de trabajo mixtas con mucho prefill. Ambos producen endpoints compatibles con OpenAI.
¿Cómo se compara esto con desplegar en Railway o Render?
Plataformas como Railway, Render y Fly.io son geniales para aplicaciones web, pero no ofrecen instancias de GPU. Modal está diseñado específicamente para cargas de trabajo de GPU con facturación por segundo y autoescalado. Si necesitas servir un LLM, Modal (o RunPod) es la herramienta correcta — las plataformas PaaS tradicionales no pueden hacerlo.