¿Qué hace modal?
Descripción General
Modal es una plataforma cloud para ejecutar código Python de forma serverless, con un enfoque en cargas de trabajo de IA/ML. Capacidades clave:
- Cómputo GPU bajo demanda (T4, L4, A10, L40S, A100, H100, H200, B200)
- Funciones serverless con autoescalado de cero a miles de contenedores
- Imágenes de contenedor personalizadas construidas completamente en código Python
- Almacenamiento persistente mediante Volúmenes para pesos de modelos y conjuntos de datos
- Endpoints web para servir modelos y APIs
- Trabajos programados mediante cron o intervalos fijos
- Arranques en frío de menos de un segundo para inferencia de baja latencia
Todo en Modal se define como código — sin YAML, sin Dockerfiles requeridos (aunque ambos son compatibles).
Cuándo Usar Esta Habilidad
Usa esta habilidad cuando:
- Despliegues o sirvas modelos de IA/ML en la nube
- Ejecutes cómputos acelerados por GPU (entrenamiento, inferencia, ajuste fino)
- Creases APIs web serverless o endpoints
- Escales trabajos de procesamiento por lotes en paralelo
- Programes tareas recurrentes (pipelines de datos, reentrenamiento, scraping)
- Necesites almacenamiento cloud persistente para pesos de modelos o conjuntos de datos
- Quieras ejecutar código en entornos de contenedor personalizados
- Construyas colas de trabajo o sistemas de procesamiento de tareas asíncronas
Instalación y Autenticación
Instalación
uv pip install modal
El SDK de Python de Modal es compatible con Python 3.10–3.14. Esta habilidad está dirigida a la API estable modal>=1.0 (versión actual: 1.4.x).
Autenticación
Prefiere las credenciales existentes antes de crear nuevas. Solo las dos variables específicas de Modal a continuación son relevantes — no leas, cargues ni expongas ninguna otra variable de entorno o contenido de archivo .env:
- Verifica si
MODAL_TOKEN_IDyMODAL_TOKEN_SECRETya están establecidas en el entorno actual. - Si no, busca solo esas dos claves en un archivo
.envlocal (ignora todas las demás entradas) y cárgalas si es apropiado para el flujo de trabajo. - Solo recurre a
modal setupinteractivo o a generar tokens nuevos si ninguna de las fuentes anteriores proporciona esos dos valores.
modal setup
Esto abre un navegador para autenticación. Para CI/CD o entornos headless, usa variables de entorno:
export MODAL_TOKEN_ID=<tu-token-id>
export MODAL_TOKEN_SECRET=<tu-token-secret>
Si los tokens no están disponibles en el entorno o en .env, genéralos en https://modal.com/settings
Modal ofrece un tier gratuito con $30/mes en créditos.
Referencia: Consulta references/getting-started.md para una configuración detallada y un primer recorrido de la aplicación.
Conceptos Clave
App y Funciones
Una App de Modal agrupa funciones relacionadas. Las funciones decoradas con @app.function() se ejecutan de forma remota en la nube:
import modal
app = modal.App("mi-app")
@app.function()
def cuadrado(x):
return x ** 2
@app.local_entrypoint()
def main():
# .remote() se ejecuta en la nube
print(cuadrado.remote(42))
Ejecuta con modal run script.py. Despliega con modal deploy script.py.
Referencia: Consulta references/functions.md para hooks de ciclo de vida, clases, .map(), .spawn(), y más.
Imágenes de Contenedor
Modal construye imágenes de contenedor a partir de código Python. El instalador de paquetes recomendado es uv:
image = (
modal.Image.debian_slim(python_version="3.11")
.uv_pip_install("torch==2.12.0", "transformers==5.9.0", "accelerate==1.13.0")
.apt_install("git")
)
@app.function(image=image)
def inferencia(pregunta):
from transformers import pipeline
pipe = pipeline("generación-de-texto", model="meta-llama/Llama-3-8B")
return pipe(pregunta)
Métodos clave de imagen:
.uv_pip_install()— Instala paquetes Python con uv (recomendado).pip_install()— Instala con pip (alternativa).apt_install()— Instala paquetes del sistema.run_commands()— Ejecuta comandos de shell durante la construcción.run_function()— Ejecuta Python durante la construcción (ej., descargar pesos de modelo).add_local_python_source()— Agrega módulos locales.env()— Establece variables de entorno
Referencia: Consulta references/images.md para Dockerfiles, micromamba, caché, pasos de construcción GPU.
Cómputo GPU
Solicita GPUs mediante el parámetro gpu:
@app.function(gpu="H100")
def entrenar_modelo():
import torch
device = torch.device("cuda")
# Código de entrenamiento GPU aquí
# Múltiples GPUs
@app.function(gpu="H100:4")
def entrenamiento_distribuido():
...
# Cadena de respaldo GPU
@app.function(gpu=["H100", "A100-80GB", "A100-40GB"])
def inferencia_flexible():
...
GPUs disponibles: T4, L4, A10, L40S, A100-40GB, A100-80GB, RTX-PRO-6000, H100, H200, B200, B200+
- Las GPUs siempre se especifican como cadenas (ej.,
gpu="H100",gpu="H100:4"). Los objetos antiguosmodal.gpu.*están obsoletos desde v0.73.31. - Hasta 8 GPUs por contenedor (excepto A10: hasta 4)
- L40S se recomienda para inferencia (relación costo/rendimiento, 48 GB VRAM)
- H100/A100 pueden actualizarse automáticamente a H200/A100-80GB sin costo adicional
- Usa
gpu="H100!"para evitar la actualización automática
Referencia: Consulta references/gpu.md para guía de selección de GPU y entrenamiento multi-GPU.
Volúmenes (Almacenamiento Persistente)
Los volúmenes proporcionan almacenamiento de archivos persistente y distribuido:
vol = modal.Volume.from_name("pesos-modelo", create_if_missing=True)
@app.function(volumes={"/data": vol})
def guardar_modelo():
# Escribe en la ruta montada
with open("/data/modelo.pt", "wb") as f:
torch.save(model.state_dict(), f)
@app.function(volumes={"/data": vol})
def cargar_modelo():
model.load_state_dict(torch.load("/data/modelo.pt"))
- Optimizado para cargas de trabajo de escribir una vez, leer muchas (pesos de modelo, conjuntos de datos)
- Acceso CLI:
modal volume ls,modal volume put,modal volume get - Confirmación automática en segundo plano cada pocos segundos
- Monta como solo lectura o limita a un subdirectorio con
vol.with_mount_options(read_only=True, sub_path="subconjunto")
Referencia: Consulta references/volumes.md para volúmenes v2, escrituras concurrentes y mejores prácticas.
Secretos
Pasa credenciales de forma segura a las funciones:
@app.function(secrets=[modal.Secret.from_name("mis-claves-api")])
def llamar_api():
import os
api_key = os.environ["API_KEY"]
# Usa la clave
Crea secretos via CLI: modal secret create mis-claves-api API_KEY=sk-xxx
O desde un archivo .env: modal.Secret.from_dotenv()
Referencia: Consulta references/secrets.md para configuración desde el panel, múltiples secretos y plantillas.
Endpoints Web
Sirve modelos y APIs como endpoints web:
@app.function()
@modal.fastapi_endpoint()
def predecir(texto: str):
return {"resultado": model.predict(texto)}
modal serve script.py— Desarrollo con recarga en caliente y URL temporalmodal deploy script.py— Despliegue a producción con URL permanente- Compatible con FastAPI, ASGI (Starlette, FastHTML), WSGI (Flask, Django), WebSockets
- Cuerpos de solicitud de hasta 4 GiB, tamaño de respuesta ilimitado
Referencia: Consulta references/web-endpoints.md para aplicaciones ASGI/WSGI, streaming, autenticación y WebSockets.
Trabajos Programados
Ejecuta funciones según un horario:
@app.function(schedule=modal.Cron("0 9 * * *")) # Diario a las 9 AM UTC
def pipeline_diario():
# ETL, reentrenamiento, scraping, etc.
...
@app.function(schedule=modal.Period(hours=6))
def verificacion_periodica():
...
Despliega con modal deploy script.py para activar el horario.
modal.Cron("...")— Sintaxis cron estándar, estable entre desplieguesmodal.Period(hours=N)— Intervalo fijo, se reinicia al redeploy- Monitorea ejecuciones en el panel de Modal
Referencia: Consulta references/scheduled-jobs.md para sintaxis cron y gestión.
Escalado y Concurrencia
Modal autoescala contenedores automáticamente. Configura límites:
@app.function(
max_containers=100, # Límite superior
min_containers=2, # Mantener calientes para baja latencia
buffer_containers=5, # Reservar capacidad
scaledown_window=300, # Segundos de inactividad antes de apagar
)
def procesar(datos):
...
Procesa entradas en paralelo con .map():
resultados = list(procesar.map([item1, item2, item3, ...]))
Habilita el manejo concurrente de solicitudes por contenedor con @modal.concurrent. Establece
target_inputs (el objetivo del autoescalador por contenedor) por debajo de max_inputs (el límite máximo)
para mantener margen mientras se escala:
@app.function()
@modal.concurrent(max_inputs=10, target_inputs=8)
async def manejar_solicitud(req):
...
Reconfigura una Función o Cls desplegada en tiempo de invocación sin redeploy usando
Function.with_options() / Function.with_concurrency() / Function.with_batching()
(y Cls.with_options()):
Modelo = modal.Cls.from_name("mi-app", "Modelo")
rapido = Modelo.with_options(gpu="H200", max_containers=20)
rapido().generar.remote(pregunta)
Referencia: Consulta references/scaling.md para .map(), .starmap(), .spawn() y límites.
Configuración de Recursos
@app.function(
cpu=4.0, # Núcleos físicos (no vCPUs)
memory=16384, # MiB
ephemeral_disk=51200, # MiB (hasta 3 TiB)
timeout=3600, # Segundos
)
def computacion_pesada():
...
Valores predeterminados: 0.125 núcleos CPU, 128 MiB de memoria. Facturado según max(solicitud, uso).
Referencia: Consulta references/resources.md para límites y detalles de facturación.
Clases con Hooks de Ciclo de Vida
Para cargas de trabajo con estado (ej., cargar un modelo una vez y servir muchas solicitudes):
@app.cls(gpu="L40S", image=image)
class Predictor:
@modal.enter()
def cargar_modelo(self):
self.model = cargar_modelo_pesado() # Se ejecuta una vez al iniciar el contenedor
@modal.method()
def predecir(self, texto: str):
return self.model(texto)
@modal.exit()
def limpiar(self):
... # Se ejecuta al apagar el contenedor
Llama con: Predictor().predecir.remote("hola")
Sandboxes
Para ejecutar código no confiable o generado dinámicamente (por ejemplo, salida de un agente de IA o un intérprete de código), usa un modal.Sandbox — un contenedor aislado que creas y controlas programáticamente en lugar de una Función decorada:
app = modal.App.lookup("sandbox-demo", create_if_missing=True)
# Contenedor aislado; restringe la salida para cargas de trabajo no confiables
sb = modal.Sandbox.create(
app=app,
image=modal.Image.debian_slim(),
outbound_cidr_allowlist=["10.0.0.0/8"],
)
# Transmite archivos dentro/fuera mediante la API de sistema de archivos (beta)
sb.filesystem.write_text("print(2 ** 10)\n", "/tmp/trabajo.py")
contenido = sb.filesystem.read_text("/tmp/trabajo.py")
sb.terminate()
- Ejecuta comandos dentro del sandbox con su método
exec(ej., ejecutapython /tmp/trabajo.py) y lee stdout del manejador de proceso devuelto — consultareferences/api_reference.md - Restringe la conectividad con
outbound_cidr_allowlist=[...]/inbound_cidr_allowlist=[...] - Captura el sistema de archivos con
sb.snapshot_filesystem()para reutilizarlo como imagen base - Ideal para intérpretes de código, ejecución de herramientas de agente y aislamiento por usuario
Patrones de Flujo de Trabajo Comunes
Servicio de Inferencia de Modelo GPU
import modal
app = modal.App("servicio-llm")
image = (
modal.Image.debian_slim(python_version="3.11")
.uv_pip_install("vllm")
)
@app.cls(gpu="H100", image=image, min_containers=1)
class ServicioLLM:
@modal.enter()
def cargar(self):
from vllm import LLM
self.llm = LLM(model="meta-llama/Llama-3-70B")
@modal.method()
@modal.fastapi_endpoint(method="POST")
def generar(self, prompt: str, max_tokens: int = 256):
outputs = self.llm.generate([prompt], max_tokens=max_tokens)
return {"texto": outputs[0].outputs[0].text}
Pipeline de Procesamiento por Lotes
app = modal.App("pipeline-lotes")
vol = modal.Volume.from_name("datos-pipeline", create_if_missing=True)
@app.function(volumes={"/data": vol}, cpu=4.0, memory=8192)
def procesar_lote(id_lote: int):
import pandas as pd
df = pd.read_parquet(f"/data/input/chunk_{id_lote}.parquet")
resultado = transformacion_pesada(df)
resultado.to_parquet(f"/data/output/chunk_{id_lote}.parquet")
return len(resultado)
@app.local_entrypoint()
def main():
ids_lote = list(range(100))
resultados = list(procesar_lote.map(ids_lote))
print(f"Procesadas {sum(resultados)} filas en total")
Pipeline de Datos Programado
app = modal.App("pipeline-etl")
@app.function(
schedule=modal.Cron("0 */6 * * *"), # Cada 6 horas
secrets=[modal.Secret.from_name("credenciales-bd")],
)
def trabajo_etl():
import os
url_bd = os.environ["DATABASE_URL"]
# Extraer, transformar, cargar
...
Referencia CLI
| Comando | Descripción |
|---|---|
modal setup | Autenticarse con Modal |
modal run script.py | Ejecutar el entrypoint local de un script |
modal serve script.py | Servidor de desarrollo con recarga en caliente |
modal deploy script.py | Desplegar a producción |
modal volume ls <nombre> | Listar archivos en un volumen |
modal volume put <nombre> <archivo> | Subir archivo a un volumen |
modal volume get <nombre> <archivo> | Descargar archivo de un volumen |
modal secret create <nombre> K=V | Crear un secreto |
modal secret list | Listar secretos |
modal app list | Listar aplicaciones desplegadas |
modal app stop <nombre> | Detener una aplicación desplegada |
Notas de Seguridad
- Credenciales: Solo
MODAL_TOKEN_IDyMODAL_TOKEN_SECRETson necesarias para autenticarse. No leas, registres ni reenvíes ninguna otra variable de entorno o entrada de.env. - Subprocesos / servidores personalizados: Algunos patrones aquí (lanzadores de entrenamiento multi-GPU, aplicaciones
@modal.web_server) llaman asubprocess.run/subprocess.Popeno comandos de shell durante las construcciones. Mantén las listas de argumentos fijas y codificadas. Nunca construyas argumentos de subproceso o shell a partir de entrada de usuario no saneada — pasa valores no confiables como datos (archivos, variables de entorno, stdin), no como argumentos de comando. - Código no confiable: Ejecuta código generado por usuario o modelo dentro de un
modal.Sandbox(ver arriba), no en una Función regular, y restringe el acceso a la red con listas de permitidos CIDR.
Archivos de Referencia
Documentación detallada para cada tema:
references/getting-started.md— Instalación, autenticación, primera aplicaciónreferences/functions.md— Funciones, clases, hooks de ciclo de vida, ejecución remotareferences/images.md— Imágenes de contenedor, instalación de paquetes, cachéreferences/gpu.md— Tipos de GPU
…[truncado para traducción]