Puesta en Producción de Modelos ML
De Jupyter Notebook a producción: Docker, Kubernetes, TensorFlow Serving, TorchServe, Triton, BentoML, KServe. Plataformas cloud: Vertex AI, SageMaker, Azure ML. Soluciones no-code: HF Spaces, Gradio, Replicate. CI/CD, monitorización, drift detection y optimización de costes.
¿Qué significa "poner en producción" un modelo?
Entrenar un modelo en un Jupyter Notebook es solo el 10 % del trabajo. Producción implica que tu modelo responda a peticiones reales, de usuarios reales, 24/7, de forma fiable, escalable y monitorizada.
El camino desde un prototipo funcional hasta un servicio en producción abarca múltiples etapas: refactorizar el código de entrenamiento en scripts limpios y testeables, exponer el modelo a través de una API o un framework de serving, empaquetar todo en un contenedor Docker reproducible, y desplegarlo en una infraestructura que garantice disponibilidad, escalabilidad y observabilidad. Cada una de estas etapas introduce complejidad técnica propia, y dominarlas es lo que diferencia a un data scientist de un ML engineer.
El concepto clave es que el modelo es solo una pieza de un sistema mucho mayor. En su famoso paper Hidden Technical Debt in Machine Learning Systems (Sculley et al., 2015), los investigadores de Google estimaron que el código del modelo representa menos del 5 % del código total de un sistema ML en producción. El resto lo forman pipelines de datos, infraestructura de serving, monitorización, configuración y testing.
📊 La realidad: solo el 54 % de los modelos llegan a producción
Según Gartner (2022), la mayoría de modelos de ML nunca salen del laboratorio. Las principales causas son: falta de infraestructura, datos inconsistentes entre entrenamiento y producción, y ausencia de procesos MLOps.
Además, un informe de VentureBeat estima que hasta el 87 % de los proyectos de data science no llegan a producción. Este submódulo te enseña a superar cada obstáculo paso a paso.
Si vienes del mundo del software tradicional, piensa en el despliegue de un modelo como una versión más exigente del despliegue de una aplicación web: además de preocuparte por disponibilidad, seguridad y escalabilidad, también tienes que gestionar pesos del modelo (archivos de varios GB), dependencias numéricas (CUDA, cuDNN, versiones exactas de PyTorch/TensorFlow), hardware heterogéneo (CPUs, GPUs, TPUs) y degradación silenciosa (el modelo no "crashea", simplemente empieza a dar peores predicciones si los datos cambian).
A lo largo de este submódulo recorreremos todas las herramientas y patrones que necesitas para llevar un modelo desde tu notebook local hasta un servicio de producción robusto. Si te interesa la parte de optimización de modelos para dispositivos con recursos limitados, consulta el submódulo de Edge Computing. Para entender las métricas de evaluación que se mencionan aquí, revisa la teoría de Machine Learning Clásico.
El ciclo MLOps
MLOps (Machine Learning Operations) es la disciplina que aplica prácticas DevOps al ciclo de vida de modelos de ML. No es solo "desplegar": es un bucle continuo de datos → entrenamiento → validación → despliegue → monitorización → re-entrenamiento.
El término fue popularizado a partir de 2020, pero sus raíces se remontan a la guía de Google Cloud sobre MLOps, que define tres niveles de madurez: Nivel 0 (manual, sin automatización), Nivel 1 (pipelines de ML automatizados) y Nivel 2 (CI/CD completo para pipelines de ML). La mayoría de equipos empiezan en el Nivel 0 y el objetivo es alcanzar al menos el Nivel 1 para garantizar reproducibilidad y eficiencia.
En la práctica, MLOps combina tres áreas: ingeniería de datos (ingesta, limpieza, feature engineering), ingeniería de modelos (entrenamiento, evaluación, optimización) e ingeniería de software (APIs, contenedores, CI/CD, monitorización). Un ML engineer eficaz necesita competencias en las tres.
Es importante entender que MLOps no es un producto que compras, sino una cultura y un conjunto de prácticas. Puedes empezar con herramientas sencillas (MLflow para tracking, Docker para reproducibilidad, GitHub Actions para CI/CD) e ir adoptando componentes más avanzados (Kubernetes, Triton, feature stores) a medida que tu sistema madure. Para una guía completa de herramientas, consulta el repositorio Awesome MLOps y la web ml-ops.org.
Tipos de despliegue
No todos los modelos se sirven igual. El patrón de despliegue depende de la latencia requerida, el volumen de datos y el caso de uso. Elegir el patrón adecuado es una de las decisiones arquitectónicas más importantes, porque determina la infraestructura, el coste y la experiencia de usuario.
La elección no es excluyente: muchos sistemas combinan varios patrones. Por ejemplo, un sistema de recomendación puede usar batch para pre-calcular candidatos cada noche y real-time para re-rankear los resultados según el contexto inmediato del usuario. De forma similar, un sistema de detección de fraude puede usar streaming para alertas inmediatas y batch para análisis retrospectivos más profundos.
| Aspecto | Real-time | Batch | Streaming | Edge |
|---|---|---|---|---|
| Latencia | < 100 ms | Minutos–horas | Seg–min | < 50 ms |
| Throughput | 100–10k RPS | Millones/batch | Continuo | 1 dispositivo |
| Infra típica | K8s + GPU | Spark / Airflow | Kafka + Flink | NPU / MCU |
| Escalado | Horizontal | Paralelo | Particiones | No aplica |
| Coste | Medio-alto | Bajo/medio | Medio | HW upfront |
| Ejemplo | Chatbot | Scoring CRM | Fraude financiero | Filtro cámara |
Retos de producción
Pasar de "funciona en mi máquina" a "funciona para 10 millones de usuarios" implica enfrentarse a problemas que no existen en un notebook. A continuación describimos los seis retos fundamentales que todo equipo de ML encuentra al llevar modelos a producción.
De entre estos retos, la reproducibilidad y la monitorización son los que más se subestiman. Un modelo que no se puede reproducir es un modelo que no se puede depurar. Y un modelo sin monitorización es una bomba de relojería: degradará en silencio conforme los datos cambien (fenómeno conocido como concept drift). Para profundizar en técnicas de detección de drift, consulta la sección de Monitorización y drift más adelante.
La seguridad en modelos ML añade vectores de ataque específicos respecto al software tradicional: adversarial attacks (inputs manipulados para engañar al modelo), model inversion (extraer datos de entrenamiento a partir de las predicciones) y model stealing (replicar el modelo accediendo a la API). El paper Adversarial Examples Are Not Easily Detected (Carlini & Wagner) ofrece una buena introducción a estos riesgos.
Métricas clave de producción
En producción, la accuracy del modelo es solo una de muchas métricas. Necesitas monitorizar el sistema completo: infraestructura, calidad del modelo y métricas de negocio. La combinación de estos tres niveles forma lo que se conoce como observabilidad de ML.
| Métrica | Descripción | Objetivo típico | Herramienta |
|---|---|---|---|
| Latencia p50 / p99 | Tiempo de respuesta mediano y percentil 99 | p50 < 50 ms, p99 < 200 ms | Prometheus, Datadog |
| Throughput (RPS) | Requests por segundo soportados | Depende del caso | Locust, k6, wrk |
| Availability (SLA) | Porcentaje de uptime | 99.9 % (8.7 h down/año) | Uptime Robot, PagerDuty |
| Error rate | % de peticiones que fallan (5xx) | < 0.1 % | Sentry, ELK Stack |
| GPU utilization | % de uso de la GPU | > 70 % (eficiencia) | nvidia-smi, DCGM |
| Model accuracy (online) | Accuracy medida sobre datos reales | ≥ accuracy de test | Evidently, Whylabs |
| Data drift score | Divergencia entre datos de train y producción | KS test p > 0.05 | Evidently, NannyML |
| Coste por inferencia | $ por cada predicción | Minimizar | Cloud billing APIs |
Las métricas de percentil (p50, p99) son especialmente importantes: la latencia media puede ser buena mientras que el 1 % de usuarios sufre tiempos inaceptables. Un buen SLA de producción especifica ambos percentiles. Por ejemplo, Amazon mide el p99.9 internamente para garantizar que incluso el 0.1 % de requests más lentos cumplan umbrales. Para entender en profundidad las métricas de evaluación de modelos (accuracy, precision, recall, F1, AUC-ROC), consulta el explorador de métricas en ML Clásico.
Mapa de decisión: ¿qué solución necesito?
No existe una única forma de desplegar un modelo. La solución óptima depende de tu perfil técnico, el volumen de tráfico, el presupuesto y los requisitos de latencia. El siguiente árbol de decisión te guía paso a paso hacia la herramienta más adecuada.
¿Por qué Docker para ML?
Docker resuelve el problema más antiguo del desarrollo de software: "funciona en mi máquina". En ML, este problema se multiplica: versiones de CUDA, librerías de Python, pesos del modelo, preprocesamiento… Un contenedor empaqueta todo en una unidad reproducible.
En un flujo de trabajo típico sin Docker, un data scientist entrena un modelo en su laptop con PyTorch 2.0, Python 3.10 y determinadas versiones de NumPy y Pillow. Cuando el equipo de ingeniería intenta desplegar ese modelo, descubre que el servidor tiene Python 3.8, otra versión de CUDA, y las dependencias entran en conflicto. Este escenario, lejos de ser anecdótico, es la norma en la industria y una de las principales causas de que los modelos no lleguen a producción.
Docker elimina este problema al crear un entorno inmutable y reproducible que viaja con el modelo. Da igual si lo ejecutas en tu laptop, en un servidor de CI/CD o en un clúster de Kubernetes: el contenedor siempre se comporta igual. Por eso Docker se ha convertido en un requisito casi universal para despliegue de modelos ML. Para una guía práctica paso a paso, consulta el tutorial de este submódulo, donde construimos un contenedor Docker para un modelo real.
Conceptos clave de Docker
| Concepto | Descripción | Analogía ML |
|---|---|---|
| Image | Plantilla read-only con el SO, deps y código. Se construye con docker build. |
El "checkpoint" de tu entorno |
| Container | Instancia en ejecución de una imagen. Se crea con docker run. |
El proceso de inferencia activo |
| Dockerfile | Receta que define cómo construir la imagen paso a paso. | Tu "pipeline de setup" |
| Volume | Almacenamiento persistente montado en el contenedor. | Donde guardar pesos y datos |
| Registry | Almacén de imágenes (Docker Hub, ECR, GCR, ACR). | Tu "model zoo" de contenedores |
| Layer | Cada instrucción del Dockerfile crea una capa cacheada. | Optimización: deps primero, código después |
| Compose | Orquestación multi-contenedor local (docker compose up). |
Modelo + API + base de datos juntos |
La arquitectura de Docker se basa en un modelo cliente-servidor. El Docker Engine (daemon) se ejecuta en el host y gestiona imágenes, contenedores, redes y volúmenes. Cada contenedor comparte el kernel del host pero tiene su propio espacio de usuario aislado (sistema de archivos, procesos, red). Esto es más eficiente que las máquinas virtuales, que necesitan un sistema operativo completo por cada instancia.
Lo clave para ML es que cada contenedor tiene sus propias dependencias completamente aisladas. Esto permite tener un contenedor con PyTorch 2.x y CUDA 12, otro con TensorFlow 2.14 y CUDA 11, y un tercero con Triton — todo en la misma máquina sin conflictos. Para una introducción completa a Docker, la documentación oficial es el mejor punto de partida.
Dockerfile para modelos de ML
Un buen Dockerfile para ML sigue el principio de capas ordenadas: las dependencias del sistema primero (cambian poco), luego las de Python, y por último el código y los pesos del modelo (cambian a menudo). Esto maximiza el cache de Docker.
# ═══════════════════════════════════════════════════
# Dockerfile · FastAPI + PyTorch inference server
# ═══════════════════════════════════════════════════
# ── Stage 1: base con CUDA (si necesitas GPU) ──
FROM nvidia/cuda:12.1.1-cudnn8-runtime-ubuntu22.04 AS base
ENV DEBIAN_FRONTEND=noninteractive \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
RUN apt-get update && apt-get install -y --no-install-recommends \
python3.11 python3-pip python3.11-venv && \
rm -rf /var/lib/apt/lists/*
# ── Stage 2: dependencias Python (cacheadas) ──
FROM base AS deps
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# ── Stage 3: código y modelo ──
FROM deps AS final
COPY src/ ./src/
COPY models/model.pt ./models/
# Puerto de la API
EXPOSE 8000
# Health check
HEALTHCHECK --interval=30s --timeout=5s --retries=3 \
CMD curl -f http://localhost:8000/health || exit 1
# Arrancar con uvicorn (workers = nº CPUs)
CMD ["uvicorn", "src.app:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "2"]
# ═══════════════════════════════════════════════════
# Dockerfile · TensorFlow Serving con modelo propio
# ═══════════════════════════════════════════════════
FROM tensorflow/serving:2.14.0-gpu
# Copiar modelo exportado en formato SavedModel
# Estructura: models/my_model/1/saved_model.pb
COPY models/my_model /models/my_model
# Variables de entorno
ENV MODEL_NAME=my_model \
MODEL_BASE_PATH=/models
# TFServing escucha en 8501 (REST) y 8500 (gRPC)
EXPOSE 8500 8501
# El entrypoint de la imagen base ya arranca tf_model_server
Multi-stage builds
Los modelos de ML generan imágenes enormes (PyTorch + CUDA puede superar los 8 GB). Las multi-stage builds reducen el tamaño final descartando herramientas de compilación y conservando solo lo necesario para ejecutar.
| Estrategia | Tamaño típico | Beneficio |
|---|---|---|
| Imagen base completa | 6–10 GB | Fácil, todo incluido |
| Multi-stage (runtime only) | 2–4 GB | Sin compiladores, docs, tests |
| Slim / distroless | 1–2 GB | Solo el runtime de Python + modelo |
| CPU-only (sin CUDA) | 500 MB – 1 GB | Ideal si no necesitas GPU |
💡 Best practices para imágenes ML ligeras
- Usa
--no-cache-diren pip install - Combina RUN en una sola capa:
apt-get update && apt-get install && rm -rf /var/lib/apt/lists/* - Usa
.dockerignorepara excluir notebooks, checkpoints de entrenamiento, datasets - Considera
python:3.11-slimcomo base si no necesitas CUDA - Monta los pesos del modelo como volumen si son muy grandes (> 1 GB)
GPU en Docker: nvidia-container-toolkit
Para usar GPUs NVIDIA dentro de contenedores necesitas el NVIDIA Container Toolkit (antes nvidia-docker2). Permite que el contenedor acceda a las GPUs del host sin instalar CUDA dentro de la imagen.
# ═══════════════════════════════════════════════════
# Instalar NVIDIA Container Toolkit (Ubuntu)
# ═══════════════════════════════════════════════════
# 1. Añadir repositorio
distribution=$(. /etc/os-release;echo $ID$VERSION_ID)
curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | \
sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg
curl -s -L https://nvidia.github.io/libnvidia-container/$distribution/libnvidia-container.list | \
sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list
# 2. Instalar
sudo apt-get update && sudo apt-get install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
# 3. Verificar
docker run --rm --gpus all nvidia/cuda:12.1.1-base-ubuntu22.04 nvidia-smi
# ═══════════════════════════════════════════════════
# Ejecutar tu contenedor con GPU
# ═══════════════════════════════════════════════════
# Todas las GPUs
docker run --gpus all -p 8000:8000 my-model:latest
# GPU específica
docker run --gpus '"device=0"' -p 8000:8000 my-model:latest
# Limitar memoria GPU
docker run --gpus all --shm-size=2g -p 8000:8000 my-model:latest
nvidia-smi en el host para ver la versión máxima de CUDA soportada.
Docker Compose para servicios ML
En producción raramente tienes solo el modelo. Un sistema típico incluye: API de inferencia, base de datos de features, cola de mensajes, monitorización… Docker Compose orquesta todos estos servicios localmente y sirve de plantilla para el despliegue en Kubernetes.
# ═══════════════════════════════════════════════════
# docker-compose.yml · Stack de inferencia ML
# ═══════════════════════════════════════════════════
version: "3.8"
services:
# ── Modelo de inferencia ──
inference:
build:
context: .
dockerfile: Dockerfile
ports:
- "8000:8000"
volumes:
- ./models:/app/models:ro # Pesos del modelo (read-only)
environment:
- MODEL_PATH=/app/models/model.pt
- WORKERS=2
- DEVICE=cuda
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 3
restart: unless-stopped
# ── Redis para caching de predicciones ──
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
# ── Prometheus para métricas ──
prometheus:
image: prom/prometheus:v2.48.0
ports:
- "9090:9090"
volumes:
- ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
# ── Grafana para dashboards ──
grafana:
image: grafana/grafana:10.2.0
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
depends_on:
- prometheus
volumes:
redis-data:
Registros de contenedores (Container Registries)
Una vez construida la imagen, necesitas almacenarla en un registry desde donde los servidores de producción puedan descargarla.
| Registry | Proveedor | Características | Coste |
|---|---|---|---|
| Docker Hub | Docker Inc. | El más conocido. 1 repo privado gratis. Imágenes oficiales de TF, PyTorch, NVIDIA. | Gratis (limitado) / $5–9/mes |
| Amazon ECR | AWS | Integrado con ECS/EKS. Escaneo de vulnerabilidades. Lifecycle policies. | $0.10/GB/mes |
| Google AR | GCP | Artifact Registry (sustituye a GCR). Integrado con GKE y Cloud Run. | $0.10/GB/mes |
| Azure ACR | Microsoft | Integrado con AKS. Geo-replicación. Tasks para CI/CD. | $0.17–1.67/día |
| GitHub GHCR | GitHub | Integrado con GitHub Actions. Gratis para repos públicos. | Gratis (público) / $0.008/GB |
# ═══════════════════════════════════════════════════
# Push a Amazon ECR
# ═══════════════════════════════════════════════════
# 1. Login
aws ecr get-login-password --region eu-west-1 | \
docker login --username AWS --password-stdin 123456789.dkr.ecr.eu-west-1.amazonaws.com
# 2. Tag
docker tag my-model:latest 123456789.dkr.ecr.eu-west-1.amazonaws.com/my-model:v1.2.0
# 3. Push
docker push 123456789.dkr.ecr.eu-west-1.amazonaws.com/my-model:v1.2.0
# ═══════════════════════════════════════════════════
# Push a Google Artifact Registry
# ═══════════════════════════════════════════════════
# 1. Auth
gcloud auth configure-docker europe-west1-docker.pkg.dev
# 2. Tag
docker tag my-model:latest europe-west1-docker.pkg.dev/my-project/ml-models/my-model:v1.2.0
# 3. Push
docker push europe-west1-docker.pkg.dev/my-project/ml-models/my-model:v1.2.0
docker image prune regularmente y configura lifecycle policies en tu registry
para eliminar imágenes antiguas automáticamente. Considera también
distroless images
para minimizar la superficie de ataque.
¿Por qué Kubernetes para ML?
Docker ejecuta contenedores en una máquina. Pero en producción necesitas: múltiples réplicas, autoscaling, rolling updates sin downtime, health checks, gestión de GPUs, y recuperación ante fallos. Kubernetes (K8s) es el estándar de facto para orquestar todo esto.
Kubernetes fue desarrollado originalmente por Google (inspirado en su sistema interno Borg) y liberado como open-source en 2014. Hoy en día, según la encuesta anual de la CNCF (2023), el 84 % de las organizaciones usan o evalúan Kubernetes en producción. Para ML, Kubernetes ofrece una ventaja adicional: la capacidad de gestionar GPUs como recursos, de modo que puedes solicitar "1 GPU NVIDIA T4" para tu pod de inferencia de la misma forma que solicitas CPU y memoria.
Conceptos clave de Kubernetes
En el diagrama anterior se observa la separación fundamental entre el control plane (que gestiona el estado deseado del clúster) y los worker nodes (que ejecutan los contenedores). Para inferencia ML, los worker nodes se configuran típicamente con GPUs dedicadas, y Kubernetes se encarga de asignar los pods a los nodos con GPU disponible.
| Recurso | Qué hace | Cuándo usarlo en ML |
|---|---|---|
| Deployment | Gestiona réplicas de pods con rolling updates. | Desplegar N réplicas de tu modelo de inferencia. |
| Service | Expone pods con una IP estable y load balancing. | Punto de entrada único para tus réplicas de modelo. |
| Ingress | Enrutamiento HTTP/HTTPS externo. | Exponer tu API al exterior con TLS y rutas. |
| HPA | Horizontal Pod Autoscaler. Escala pods según métricas. | Escalar inference pods según RPS o GPU %. |
| ConfigMap / Secret | Configuración y secretos inyectados en pods. | API keys, rutas de modelo, hiperparámetros de serving. |
| PersistentVolume | Almacenamiento persistente para pods. | Almacenar pesos de modelos compartidos entre réplicas. |
| Job / CronJob | Tareas puntuales o programadas. | Re-entrenamiento programado, batch inference. |
Deployment YAML para inferencia ML
Un Deployment de K8s para un modelo de inferencia con GPU, health checks y autoscaling.
# ═══════════════════════════════════════════════════
# deployment.yaml · Inference server con GPU
# ═══════════════════════════════════════════════════
apiVersion: apps/v1
kind: Deployment
metadata:
name: inference-server
labels:
app: inference
version: v2.1.0
spec:
replicas: 3 # Réplicas iniciales
selector:
matchLabels:
app: inference
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1 # +1 pod durante update
maxUnavailable: 0 # 0 downtime
template:
metadata:
labels:
app: inference
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8000"
spec:
containers:
- name: model
image: 123456789.dkr.ecr.eu-west-1.amazonaws.com/my-model:v2.1.0
ports:
- containerPort: 8000
resources:
requests:
cpu: "2"
memory: "4Gi"
nvidia.com/gpu: "1" # ← Solicita 1 GPU
limits:
cpu: "4"
memory: "8Gi"
nvidia.com/gpu: "1"
readinessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 30 # Tiempo para cargar modelo
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
initialDelaySeconds: 60
periodSeconds: 30
env:
- name: MODEL_PATH
value: /models/model.pt
- name: MAX_BATCH_SIZE
value: "16"
volumeMounts:
- name: model-weights
mountPath: /models
readOnly: true
volumes:
- name: model-weights
persistentVolumeClaim:
claimName: model-pvc
tolerations: # Permitir scheduling en nodos GPU
- key: nvidia.com/gpu
operator: Exists
effect: NoSchedule
---
# ═══════════════════════════════════════════════════
# Service · Load balancer interno
# ═══════════════════════════════════════════════════
apiVersion: v1
kind: Service
metadata:
name: inference-service
spec:
selector:
app: inference
ports:
- port: 80
targetPort: 8000
type: ClusterIP
---
# ═══════════════════════════════════════════════════
# HPA · Autoscaling basado en CPU
# ═══════════════════════════════════════════════════
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: inference-hpa
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: inference-server
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
behavior:
scaleUp:
stabilizationWindowSeconds: 60
scaleDown:
stabilizationWindowSeconds: 300 # Esperar 5 min antes de reducir
Scaling avanzado para ML
El HPA estándar escala por CPU/memoria, pero en ML queremos escalar por métricas como requests per second, latencia p99 o GPU utilization. Herramientas como KEDA lo hacen posible.
| Mecanismo | Qué escala | Métrica típica ML | Herramienta |
|---|---|---|---|
| HPA | Pods horizontalmente | CPU %, memoria, custom metrics | K8s nativo |
| VPA | Recursos de pods verticalmente | Ajustar CPU/RAM según uso real | K8s VPA addon |
| KEDA | Pods por eventos | Mensajes en cola, RPS, cron | KEDA |
| Cluster Autoscaler | Nodos del clúster | Pods pendientes sin recursos | Cloud provider |
| Karpenter | Nodos (AWS) | Provisionado inteligente de instancias GPU | Karpenter |
🎯 Scale-to-zero para ahorro de costes
Si tu modelo solo recibe tráfico en horario laboral, puedes usar KEDA o Knative para escalar a 0 réplicas cuando no hay peticiones, y levantar pods bajo demanda (cold start ~10–30s). Ideal para modelos de baja frecuencia. Alternativa: serverless con Cloud Run o AWS Lambda.
Kubernetes managed: EKS, GKE, AKS
Gestionar un clúster K8s desde cero es complejo. Los 3 grandes clouds ofrecen Kubernetes managed donde ellos se encargan del control plane y tú solo gestionas los nodos y workloads. Esta es la opción recomendada para la gran mayoría de equipos, ya que elimina la carga operativa de mantener etcd, el API server y los controllers. Puedes centrarte en desplegar tus modelos en lugar de administrar la infraestructura.
| Servicio | Cloud | GPU soporte | Extras para ML | Coste control plane |
|---|---|---|---|---|
| EKS | AWS | P4d, G5, Inf2, Trn1 | SageMaker Operators, Karpenter, EFA | $0.10/h (~$73/mes) |
| GKE | Google Cloud | A100, L4, T4, TPUs | Autopilot, GKE + Vertex AI, GPU time-sharing | Gratis (Autopilot: paga por pod) |
| AKS | Azure | A100, T4, V100 | Azure ML integration, KAITO (LLM addon) | Gratis (pay for nodes) |
Alternativas a Kubernetes
K8s es potente pero complejo. Para muchos casos de ML hay alternativas más simples que cubren el 80 % de las necesidades.
| Solución | Complejidad | GPU | Scale-to-zero | Coste mínimo | Mejor para |
|---|---|---|---|---|---|
| Kubernetes | 🔴 Alta | ✅ Full | Con KEDA | ~$150/mes | Enterprise, multi-modelo |
| Cloud Run | 🟢 Baja | ✅ L4 | ✅ Nativo | $0 (idle) | APIs con tráfico variable |
| ECS Fargate | 🟡 Media | ❌ | Con target tracking | ~$30/mes | AWS ecosystem, CPU |
| Lambda | 🟢 Baja | ❌ | ✅ Nativo | $0 (idle) | Modelos < 10 GB, CPU |
| Railway/Render | 🟢 Baja | ❌ | ✅ | $5/mes | Prototipos, demos |
¿Qué es Model Serving?
Model Serving es el proceso de exponer un modelo entrenado como un servicio que acepta peticiones y devuelve predicciones. Puedes hacerlo con una API Flask/FastAPI sencilla, pero los frameworks de serving dedicados optimizan: batching dinámico, gestión de GPU, versionado de modelos, A/B testing y monitorización.
La diferencia entre servir un modelo con una API "casera" y un framework de serving dedicado es similar a la diferencia entre un servidor web Python básico y Nginx: ambos sirven HTTP, pero uno está optimizado para rendimiento, concurrencia y producción. En el contexto de ML, los frameworks de serving añaden batching dinámico (agrupan múltiples requests para procesarlos juntos en GPU, maximizando el throughput), versionado automático (cargan nuevas versiones del modelo sin downtime) y health monitoring (detectan si el modelo está respondiendo correctamente).
El flujo estándar de serving comienza cuando un cliente (aplicación web, móvil u otro servicio) envía una petición con los datos de entrada. Un load balancer distribuye las peticiones entre las réplicas del servidor, aplicando opcionalmente políticas de A/B testing o canary. El framework de serving se encarga del pre-procesamiento, el batching (agrupar peticiones para eficiencia GPU), la inferencia propiamente dicha, y el post-procesamiento. Finalmente, el model store gestiona las versiones del modelo, permitiendo rollbacks instantáneos si una nueva versión degrada métricas.
TensorFlow Serving
TensorFlow Serving es el servidor de inferencia oficial de Google para modelos TensorFlow. Altamente optimizado, soporta batching dinámico, versionado de modelos y gRPC.
# ═══════════════════════════════════════════════════
# 1. Exportar modelo como SavedModel
# ═══════════════════════════════════════════════════
import tensorflow as tf
model = tf.keras.models.load_model("my_model.keras")
# Exportar con versión (directorio numérico)
export_path = "models/my_model/1" # ← versión 1
model.export(export_path)
print(f"✅ Modelo exportado a {export_path}")
# ═══════════════════════════════════════════════════
# 2. Levantar TF Serving con Docker
# ═══════════════════════════════════════════════════
# docker run -p 8501:8501 \
# -v $(pwd)/models/my_model:/models/my_model \
# -e MODEL_NAME=my_model \
# tensorflow/serving:2.14.0-gpu
# ═══════════════════════════════════════════════════
# 3. Hacer predicciones via REST
# ═══════════════════════════════════════════════════
import requests
import numpy as np
data = {"instances": np.random.randn(1, 224, 224, 3).tolist()}
url = "http://localhost:8501/v1/models/my_model:predict"
response = requests.post(url, json=data)
predictions = response.json()["predictions"]
print(f"🎯 Predicción: {np.argmax(predictions[0])}")
TorchServe
TorchServe es el servidor oficial de PyTorch, mantenido conjuntamente por AWS y Meta. Soporta modelos PyTorch y TorchScript, con handlers personalizables para pre/post-procesamiento.
# ═══════════════════════════════════════════════════
# 1. Crear Model Archive (.mar)
# ═══════════════════════════════════════════════════
# torch-model-archiver \
# --model-name resnet50 \
# --version 1.0 \
# --serialized-file model.pt \
# --handler image_classifier \
# --export-path model_store/
# ═══════════════════════════════════════════════════
# 2. Arrancar TorchServe
# ═══════════════════════════════════════════════════
# torchserve --start --model-store model_store \
# --models resnet50=resnet50.mar \
# --ts-config config.properties
# ═══════════════════════════════════════════════════
# 3. Handler personalizado (opcional)
# ═══════════════════════════════════════════════════
from ts.torch_handler.base_handler import BaseHandler
import torch
from torchvision import transforms
from PIL import Image
import io
class MyHandler(BaseHandler):
def preprocess(self, data):
"""Pre-proceso: decode imagen + transformaciones."""
transform = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize([0.485, 0.456, 0.406],
[0.229, 0.224, 0.225]),
])
images = []
for row in data:
image = Image.open(io.BytesIO(row["body"]))
images.append(transform(image))
return torch.stack(images)
def inference(self, data):
"""Inferencia sobre batch."""
with torch.no_grad():
return self.model(data)
def postprocess(self, output):
"""Post-proceso: softmax + top-5."""
probs = torch.nn.functional.softmax(output, dim=1)
top5 = torch.topk(probs, 5, dim=1)
return [{"top5": top5.indices[i].tolist(),
"probs": top5.values[i].tolist()}
for i in range(len(output))]
# ═══════════════════════════════════════════════════
# 4. Hacer predicciones
# ═══════════════════════════════════════════════════
import requests
with open("cat.jpg", "rb") as f:
response = requests.post(
"http://localhost:8080/predictions/resnet50",
files={"data": f}
)
print(response.json())
NVIDIA Triton Inference Server
Triton es el servidor de inferencia más completo: soporta múltiples frameworks (TF, PyTorch, ONNX, TensorRT, Python custom), múltiples modelos en el mismo servidor, y optimizaciones avanzadas (dynamic batching, model ensembles, GPU sharing).
⚡ ¿Por qué elegir Triton?
- Multi-framework: TensorFlow, PyTorch, ONNX, TensorRT, OpenVINO, Python
- Dynamic batching: Agrupa requests automáticamente para maximizar GPU throughput
- Model ensemble: Pipelines de múltiples modelos (pre → modelo → post) en un solo request
- Concurrent model execution: Varios modelos en la misma GPU con gestión de memoria
- Métricas Prometheus: Latencia, throughput, cola de batch, GPU utilization
- Soporte K8s: Helm chart oficial, integración con KServe
# ═══════════════════════════════════════════════════
# Estructura del model repository
# ═══════════════════════════════════════════════════
# model_repository/
# ├── resnet50/
# │ ├── config.pbtxt
# │ └── 1/
# │ └── model.onnx
# └── bert_qa/
# ├── config.pbtxt
# └── 1/
# └── model.plan ← TensorRT engine
# ═══════════════════════════════════════════════════
# config.pbtxt (ejemplo para ONNX)
# ═══════════════════════════════════════════════════
# name: "resnet50"
# platform: "onnxruntime_onnx"
# max_batch_size: 32
# input [{
# name: "input"
# data_type: TYPE_FP32
# dims: [3, 224, 224]
# }]
# output [{
# name: "output"
# data_type: TYPE_FP32
# dims: [1000]
# }]
# dynamic_batching {
# preferred_batch_size: [8, 16]
# max_queue_delay_microseconds: 5000
# }
# instance_group [{
# count: 2
# kind: KIND_GPU
# }]
# ═══════════════════════════════════════════════════
# Docker run
# ═══════════════════════════════════════════════════
# docker run --gpus all -p 8000:8000 -p 8001:8001 -p 8002:8002 \
# -v $(pwd)/model_repository:/models \
# nvcr.io/nvidia/tritonserver:24.01-py3 \
# tritonserver --model-repository=/models
# ═══════════════════════════════════════════════════
# Cliente Python (tritonclient)
# ═══════════════════════════════════════════════════
import tritonclient.http as httpclient
import numpy as np
client = httpclient.InferenceServerClient(url="localhost:8000")
# Verificar que el modelo está cargado
assert client.is_model_ready("resnet50"), "Modelo no disponible"
# Preparar input
input_data = np.random.randn(1, 3, 224, 224).astype(np.float32)
inputs = [httpclient.InferInput("input", input_data.shape, "FP32")]
inputs[0].set_data_from_numpy(input_data)
# Inferir
result = client.infer("resnet50", inputs)
output = result.as_numpy("output")
print(f"🎯 Clase predicha: {np.argmax(output)}")
BentoML
BentoML es un framework Python-first que simplifica todo el flujo: desde guardar el modelo hasta generar un contenedor optimizado listo para producción. Ideal para equipos que quieren deployment rápido sin escribir Dockerfiles ni YAML de K8s.
# ═══════════════════════════════════════════════════
# 1. Guardar modelo en BentoML model store
# ═══════════════════════════════════════════════════
import bentoml
import torch
model = torch.load("model.pt")
bentoml.pytorch.save_model("my_classifier", model)
# ═══════════════════════════════════════════════════
# 2. Definir Service (service.py)
# ═══════════════════════════════════════════════════
import bentoml
import numpy as np
from PIL import Image
@bentoml.service(
resources={"gpu": 1, "memory": "4Gi"},
traffic={"timeout": 30},
)
class ImageClassifier:
def __init__(self):
self.model = bentoml.pytorch.load_model("my_classifier")
self.model.eval()
@bentoml.api
async def predict(self, image: Image.Image) -> dict:
# Preprocesar
tensor = self.preprocess(image)
# Inferir
with torch.no_grad():
output = self.model(tensor.unsqueeze(0))
# Devolver resultado
prob = torch.softmax(output, dim=1)
return {"class": int(prob.argmax()), "confidence": float(prob.max())}
# ═══════════════════════════════════════════════════
# 3. Servir localmente
# ═══════════════════════════════════════════════════
# bentoml serve service:ImageClassifier
# ═══════════════════════════════════════════════════
# 4. Containerizar y desplegar
# ═══════════════════════════════════════════════════
# bentoml build # Crear Bento
# bentoml containerize my_classifier:latest # Docker image
# docker push my-registry/my_classifier:v1
KServe y Seldon Core (Kubernetes-native)
Para despliegues enterprise en Kubernetes, existen plataformas que añaden superpoderes sobre K8s: versionado, canary, A/B, autoscaling, explicabilidad…
# ═══════════════════════════════════════════════════
# KServe InferenceService con canary deployment
# ═══════════════════════════════════════════════════
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: image-classifier
spec:
predictor:
# ── Versión actual (90% tráfico) ──
canaryTrafficPercent: 10
model:
modelFormat:
name: pytorch
storageUri: "gs://my-bucket/models/resnet50/v1"
resources:
limits:
nvidia.com/gpu: "1"
memory: "8Gi"
# ── Versión canary (10% tráfico) ──
canary:
model:
modelFormat:
name: pytorch
storageUri: "gs://my-bucket/models/resnet50/v2"
resources:
limits:
nvidia.com/gpu: "1"
memory: "8Gi"
transformer:
containers:
- name: preprocess
image: my-registry/preprocess:v1
env:
- name: STORAGE_URI
value: "gs://my-bucket/config"
Comparativa de frameworks de serving
| Framework | Frameworks ML | Batching | GPU | Complejidad | Mejor para |
|---|---|---|---|---|---|
| FastAPI custom | Cualquiera | Manual | ✅ | 🟢 Baja | Prototipos, APIs simples, control total |
| TF Serving | TensorFlow | ✅ Dinámico | ✅ | 🟡 Media | Producción TF, alta eficiencia C++ |
| TorchServe | PyTorch | ✅ Dinámico | ✅ | 🟡 Media | Producción PyTorch, handlers custom |
| Triton | Multi (TF, PT, ONNX…) | ✅ Avanzado | ✅✅ | 🔴 Alta | Multi-modelo, máxima GPU efficiency |
| BentoML | Multi (Python-first) | ✅ Adaptive | ✅ | 🟢 Baja | Desarrollo rápido, containerización auto |
| KServe | Multi + Triton | ✅ (vía backend) | ✅ | 🔴 Alta | K8s enterprise, canary, scale-to-zero |
| Seldon Core | Multi | ✅ | ✅ | 🔴 Alta | Inference graphs, A/B, explicabilidad |
| vLLM | LLMs (HF) | ✅ Continuous | ✅✅ | 🟡 Media | Servir LLMs con PagedAttention |
Panorama de plataformas cloud para ML
No siempre hace falta gestionar Kubernetes y Triton. Los 3 grandes proveedores cloud ofrecen plataformas managed que abstraen la infraestructura y te permiten centrarte en el modelo. Por otra parte, existen soluciones low-code para usuarios que no necesitan control total.
El espectro de soluciones varía desde plataformas de "cero código" como
Hugging Face Spaces (donde un simple git push despliega tu modelo) hasta
infraestructura bare metal gestionada por tu equipo. La elección depende
fundamentalmente de tres factores: la complejidad técnica que tu
equipo puede asumir, los requisitos de personalización del despliegue,
y el presupuesto. No siempre más control es mejor: muchos equipos
gastan más tiempo gestionando Kubernetes que mejorando sus modelos.
Soluciones para equipos no técnicos
Si tu objetivo es crear un demo, prototipo o servir un modelo con poco tráfico sin gestionar infraestructura, estas plataformas son para ti.
git push. GPU gratuitas (limitadas) o de pago.
Perfecto para demos y compartir modelos con la comunidad.
→ huggingface.co/spaces
→ gradio.app
→ streamlit.io/cloud
→ replicate.com
→ modal.com
→ HF Endpoints
# ═══════════════════════════════════════════════════
# Demo con Gradio: clasificación de imágenes
# ═══════════════════════════════════════════════════
import gradio as gr
from transformers import pipeline
classifier = pipeline("image-classification", model="google/vit-base-patch16-224")
demo = gr.Interface(
fn=lambda img: {p["label"]: p["score"] for p in classifier(img)},
inputs=gr.Image(type="pil"),
outputs=gr.Label(num_top_classes=5),
title="🖼️ Clasificador de imágenes (ViT)",
description="Sube una imagen y el modelo la clasificará."
)
demo.launch() # → localhost:7860
# Para desplegar en HF Spaces: git push al repo del Space
Google Cloud: Vertex AI
Vertex AI es la plataforma de ML end-to-end de Google Cloud. Cubre todo el ciclo: desde datos (BigQuery, Feature Store) hasta serving (endpoints, batch prediction) pasando por entrenamiento (custom training, AutoML, Workbench).
# ═══════════════════════════════════════════════════
# Despliegue en Vertex AI con Python SDK
# ═══════════════════════════════════════════════════
from google.cloud import aiplatform
aiplatform.init(project="my-project", location="europe-west1")
# 1. Subir modelo al Model Registry
model = aiplatform.Model.upload(
display_name="image-classifier-v2",
artifact_uri="gs://my-bucket/models/resnet50/",
serving_container_image_uri=(
"europe-docker.pkg.dev/vertex-ai/prediction/"
"tf2-gpu.2-14:latest"
),
)
# 2. Crear endpoint
endpoint = aiplatform.Endpoint.create(
display_name="classifier-endpoint",
)
# 3. Desplegar modelo en endpoint
model.deploy(
endpoint=endpoint,
machine_type="n1-standard-4",
accelerator_type="NVIDIA_TESLA_T4",
accelerator_count=1,
min_replica_count=1,
max_replica_count=5, # Autoscaling
traffic_percentage=100,
)
# 4. Hacer predicciones
import numpy as np
instances = [np.random.randn(224, 224, 3).tolist()]
prediction = endpoint.predict(instances=instances)
print(prediction.predictions)
AWS: SageMaker
Amazon SageMaker es la plataforma ML más completa del ecosistema AWS. Incluye notebooks, entrenamiento, HPO, Model Registry, endpoints de inferencia en tiempo real/batch/async/serverless, y herramientas de MLOps (Pipelines, Model Monitor, Clarify).
| Componente | Qué hace | Cuándo usarlo |
|---|---|---|
| Studio | IDE web para todo el ciclo ML | Desarrollo y experimentación |
| Training Jobs | Entrenamiento en instancias cloud (GPU/Trainium) | Modelos que no caben en tu laptop |
| Real-time Endpoints | API persistente con autoscaling | Producción con tráfico constante |
| Serverless Inference | Scale-to-zero, pago por invocación | Tráfico esporádico (< 1 RPS) |
| Async Inference | Procesa requests grandes en background | Modelos lentos (> 60s), documentos largos |
| Batch Transform | Procesa datasets completos en paralelo | Scoring masivo, nightly jobs |
| Model Registry | Versiona y aprueba modelos | CI/CD y governance |
| Pipelines | Workflows ML reproducibles (DAGs) | Automatizar train → eval → deploy |
| Model Monitor | Detecta data drift y bias | Monitorización continua en producción |
# ═══════════════════════════════════════════════════
# Despliegue en SageMaker con Python SDK
# ═══════════════════════════════════════════════════
import sagemaker
from sagemaker.pytorch import PyTorchModel
session = sagemaker.Session()
role = "arn:aws:iam::123456789:role/SageMakerRole"
# 1. Crear modelo
pytorch_model = PyTorchModel(
model_data="s3://my-bucket/models/model.tar.gz",
role=role,
framework_version="2.1",
py_version="py310",
entry_point="inference.py", # Script de pre/post proceso
)
# 2. Desplegar endpoint
predictor = pytorch_model.deploy(
instance_type="ml.g4dn.xlarge", # GPU T4
initial_instance_count=1,
endpoint_name="my-classifier-v2",
)
# 3. Predicción
import numpy as np
response = predictor.predict(np.random.randn(1, 3, 224, 224).tolist())
print(response)
# 4. Limpiar (¡no olvidar para no seguir pagando!)
predictor.delete_endpoint()
Microsoft Azure: Azure Machine Learning
Azure ML ofrece un ecosistema similar a SageMaker/Vertex, integrado con el stack Microsoft (Azure DevOps, Power BI, Synapse). Destaca por su CLI v2 y integración con VS Code y GitHub Actions.
🔷 Highlights de Azure ML
- Managed Endpoints: Similar a SageMaker endpoints. Real-time y batch. Autoscaling. Blue-green deploys.
- KAITO: Add-on de K8s para desplegar LLMs en AKS con un click (Llama, Falcon, Mistral).
- Responsible AI Dashboard: Fairness, explicabilidad, error analysis integrados.
- Prompt Flow: Herramienta visual para construir aplicaciones LLM (RAG, agents).
- CLI v2: Gestión declarativa de todo el ciclo ML con YAML + CLI.
Comparativa de plataformas cloud
| Característica | Vertex AI (GCP) | SageMaker (AWS) | Azure ML |
|---|---|---|---|
| Fortaleza | BigQuery + TPUs, AutoML | Ecosistema AWS, más completo | Integración Microsoft, KAITO |
| GPUs disponibles | T4, L4, A100, H100, TPUs | T4, A10G, A100, Inf2, Trn1 | T4, V100, A100, H100 |
| Serverless inference | Cloud Run + GPU | SageMaker Serverless | Managed Online (min 1 replica) |
| Scale-to-zero | ✅ Cloud Run | ✅ Serverless endpoints | ❌ (min 1 réplica) |
| AutoML | ✅ Vertex AutoML | ✅ Autopilot | ✅ AutoML |
| Feature Store | ✅ Integrado | ✅ SageMaker FS | ✅ Managed FS |
| Model Monitoring | ✅ Integrado | ✅ Model Monitor | ✅ Data Collector |
| Pipelines | Kubeflow / TFX | SageMaker Pipelines | Azure ML Pipelines |
| LLM support | Model Garden, Gemini | Bedrock, JumpStart | Azure OpenAI, KAITO |
| Coste mínimo endpoint | ~$0.05/h (CPU) | ~$0.07/h (CPU) | ~$0.10/h (CPU) |
| Mejor para | Equipos Data + BigQuery | Ya usan AWS | Enterprise Microsoft |
Widget: estimador de coste por plataforma
CI/CD para modelos de ML
En software tradicional, CI/CD automatiza build → test → deploy. En ML añadimos pasos específicos: validación del modelo, tests de accuracy, comparación con el modelo en producción, y despliegue condicional.
La diferencia clave respecto al CI/CD clásico es que en ML tenemos dos artefactos que versionear: el código (scripts de entrenamiento, preprocesamiento, API) y el modelo (pesos, hiperparámetros, métricas). Además, los pipelines de ML incluyen un paso de evaluación que actúa como "quality gate": si el nuevo modelo no supera en accuracy al modelo en producción, el pipeline se detiene y no se despliega. Este concepto se conoce como model validation gate y es fundamental para evitar regresiones en producción.
Herramientas como CML (Continuous Machine Learning) de Iterative permiten integrar métricas de modelos directamente en pull requests de GitHub/GitLab, de modo que el equipo puede revisar el impacto de un cambio en el modelo antes de aprobar el merge. Para una guía completa del stack de herramientas CI/CD para ML, el curso gratuito Made With ML es una excelente referencia.
# ═══════════════════════════════════════════════════
# .github/workflows/ml-cicd.yml
# ═══════════════════════════════════════════════════
name: ML CI/CD Pipeline
on:
push:
branches: [main]
paths: ["src/**", "models/**", "requirements.txt"]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install -r requirements.txt
- run: pytest tests/ -v --tb=short
evaluate:
needs: test
runs-on: ubuntu-latest # O self-hosted con GPU
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with: { python-version: "3.11" }
- run: pip install -r requirements.txt
# Evaluar modelo nuevo vs producción
- name: Evaluate model
run: |
python evaluate.py \
--model models/model_new.pt \
--data data/test_set.csv \
--output metrics.json
# Quality gate: fail si accuracy < 0.90
- name: Quality gate
run: |
python -c "
import json
m = json.load(open('metrics.json'))
assert m['accuracy'] >= 0.90, f'Accuracy {m[\"accuracy\"]} < 0.90'
assert m['latency_p99'] <= 200, f'Latency {m[\"latency_p99\"]}ms > 200ms'
print('✅ Quality gate passed')
"
deploy:
needs: evaluate
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build & push Docker image
run: |
docker build -t ghcr.io/${{ github.repository }}/model:${{ github.sha }} .
docker push ghcr.io/${{ github.repository }}/model:${{ github.sha }}
- name: Deploy canary (10%)
run: |
kubectl set image deployment/inference \
model=ghcr.io/${{ github.repository }}/model:${{ github.sha }}
kubectl patch hpa inference-hpa \
-p '{"metadata":{"annotations":{"canary":"true"}}}'
Model Registry y versionado
Un Model Registry es un catálogo centralizado donde se almacenan, versionan y aprueban modelos antes de ir a producción. Es el equivalente al container registry pero para artefactos ML.
| Herramienta | Tipo | Highlights | Integración |
|---|---|---|---|
| MLflow | Open-source | Tracking, registry, serving. El más popular. Stage transitions (Staging → Production). | TF, PyTorch, Sklearn, Spark |
| Weights & Biases | SaaS / Self-hosted | Experiment tracking excepcional. Registry con lineage. Sweeps (HPO). | Todo framework Python |
| Neptune | SaaS | UI limpia. Metadata store. Comparación de runs. | TF, PyTorch, Sklearn, XGBoost |
| DVC | Open-source | Git para datos y modelos. Pipelines reproducibles. Integrado con Git. | Agnóstico (archivos) |
| Cloud natives | Managed | SageMaker Model Registry, Vertex AI Model Registry, Azure ML Registry. | Ecosistema de cada cloud |
# ═══════════════════════════════════════════════════
# MLflow: track experiment + register model
# ═══════════════════════════════════════════════════
import mlflow
import mlflow.pytorch
mlflow.set_tracking_uri("http://mlflow-server:5000")
mlflow.set_experiment("image-classification")
with mlflow.start_run(run_name="resnet50-v2"):
# Log hiperparámetros
mlflow.log_params({
"model": "resnet50",
"lr": 0.001,
"batch_size": 64,
"epochs": 20,
"optimizer": "AdamW",
})
# Entrenar modelo...
model = train(...)
# Log métricas
mlflow.log_metrics({
"accuracy": 0.923,
"f1_score": 0.918,
"latency_p99_ms": 45,
})
# Log modelo → Model Registry
mlflow.pytorch.log_model(
model,
artifact_path="model",
registered_model_name="image-classifier",
)
# ═══════════════════════════════════════════════════
# Promover modelo a producción
# ═══════════════════════════════════════════════════
from mlflow import MlflowClient
client = MlflowClient()
client.transition_model_version_stage(
name="image-classifier",
version=2,
stage="Production",
archive_existing_versions=True, # Archiva la v1
)
Estrategias de despliegue
No basta con desplegar: necesitas una estrategia que minimice el riesgo de que un modelo defectuoso afecte a los usuarios. En ML, un modelo puede fallar de formas sutiles (dar predicciones peores sin producir errores técnicos), lo que hace que las estrategias de despliegue progresivo sean especialmente importantes.
La estrategia más recomendada para ML es el canary deployment, donde la nueva versión del modelo recibe inicialmente un porcentaje pequeño de tráfico (por ejemplo, 5 %) y se monitoriza activamente. Si las métricas (accuracy, latencia, error rate) se mantienen estables o mejoran, se incrementa progresivamente el porcentaje. Si se detecta degradación, se revierte automáticamente al modelo anterior. Plataformas como KServe y Seldon Core implementan canary deployments de forma nativa.
Monitorización y detección de drift
Un modelo que era bueno ayer puede ser malo hoy si los datos cambian. Data drift (cambian las features de entrada) y concept drift (cambia la relación input → output) son los enemigos silenciosos de los modelos en producción.
Es importante entender la diferencia entre ambos tipos de drift. En el data drift, las distribuciones de las variables de entrada cambian respecto a las del entrenamiento. Por ejemplo, un modelo de clasificación de imágenes entrenado con fotos diurnas puede recibir fotos nocturnas en producción. En el concept drift, lo que cambia es la relación subyacente entre inputs y outputs: las mismas features ahora predicen algo diferente. Un ejemplo clásico es un modelo de predicción de demanda que se entrenó antes de una pandemia — el comportamiento de compra cambió fundamentalmente, invalidando la relación aprendida. Ambos tipos requieren monitorización activa y, en muchos casos, re-entrenamiento automático del modelo. Para una revisión formal, consulta el paper A Survey on Concept Drift Adaptation (Lu et al., 2020).
| Herramienta | Open / SaaS | Detecta | Highlights |
|---|---|---|---|
| Evidently | Open-source | Data drift, concept drift, data quality | Dashboards interactivos, test suites. Se integra con Airflow, Prefect. |
| NannyML | Open-source | Performance estimation sin labels | Estima accuracy sin ground truth. Ideal cuando los labels tardan días/semanas. |
| WhyLabs | SaaS | Data drift, anomalías, PII | Basado en whylogs (open-source profiling). Alertas automáticas. |
| Prometheus + Grafana | Open-source | Métricas de sistema y custom | Latencia, throughput, errores, GPU %. Alertas con AlertManager. |
| Cloud natives | Managed | Drift, skew | SageMaker Model Monitor, Vertex AI Model Monitoring, Azure Data Collector. |
# ═══════════════════════════════════════════════════
# Evidently: test de data drift
# ═══════════════════════════════════════════════════
from evidently.test_suite import TestSuite
from evidently.tests import (
TestShareOfDriftedColumns,
TestColumnDrift,
)
import pandas as pd
# Datos de referencia (entrenamiento) y actuales (producción)
reference = pd.read_csv("data/train_features.csv")
current = pd.read_csv("data/production_features_today.csv")
# Crear suite de tests
suite = TestSuite(tests=[
TestShareOfDriftedColumns(lt=0.3), # < 30% columnas con drift
TestColumnDrift("age"), # Test específico por columna
TestColumnDrift("income"),
])
suite.run(reference_data=reference, current_data=current)
# Resultado
result = suite.as_dict()
if not result["summary"]["all_passed"]:
print("⚠️ DRIFT DETECTADO — considerar re-entrenamiento")
# Enviar alerta a Slack / PagerDuty
else:
print("✅ Sin drift significativo")
# Generar dashboard HTML
suite.save_html("drift_report.html")
Observabilidad: los 3 pilares
Un sistema ML en producción necesita los 3 pilares de observabilidad: logs (qué pasó), métricas (cuánto) y traces (dónde el tiempo).
La observabilidad va más allá de la monitorización: mientras que la monitorización te dice que algo va mal, la observabilidad te permite entender por qué. En ML, esto es especialmente crítico porque los problemas suelen ser sutiles: un modelo que de repente responde más lento podría estar recibiendo imágenes de mayor resolución (data drift), o un cambio en la distribución de features podría estar provocando que el modelo explore ramas de decisión más complejas. Sin trazas detalladas que desglosen el tiempo por componente (preprocesamiento, inferencia, postprocesamiento), diagnosticar estos problemas es como buscar una aguja en un pajar.
El estándar de la industria para instrumentar observabilidad es OpenTelemetry, que proporciona una API unificada para logs, métricas y traces. Se integra con la mayoría de backends (Prometheus, Grafana, Jaeger, Datadog) y permite correlacionar los tres tipos de señales.
Stack: ELK (Elasticsearch + Logstash + Kibana), Loki + Grafana, CloudWatch Logs.
Stack: Prometheus + Grafana, Datadog, CloudWatch Metrics.
Stack: Jaeger, Zipkin, OpenTelemetry, Datadog APM.
Optimización de costes en producción
Servir modelos ML en GPU es caro. Estrategias clave para reducir costes sin sacrificar rendimiento:
El coste de GPU es el factor dominante en la factura de producción de ML. Una GPU A100 en AWS (instancia p4d.24xlarge) cuesta más de $32/hora — casi $24 000/mes si se ejecuta 24/7. Incluso GPUs más modestas como la T4 ($0.53/h) suponen ~$380/mes por instancia. Por eso, optimizar costes es tan importante como optimizar métricas. Las siguientes 8 estrategias cubren desde optimizaciones a nivel de modelo (cuantización) hasta decisiones de infraestructura (spot instances, scale-to-zero). Lo ideal es combinar varias de ellas para obtener ahorros multiplicativos.
Checklist de producción
Antes de desplegar un modelo, asegúrate de cubrir estos puntos:
| Categoría | Check | Herramienta recomendada |
|---|---|---|
| 📦 Reproducibilidad | ¿El entorno está containerizado? | Docker + requirements.txt / poetry.lock |
| 🧪 Testing | ¿Hay tests de accuracy, latencia y smoke? | pytest + locust |
| 📊 Métricas | ¿Se exponen métricas Prometheus? | prometheus_client (Python) |
| 🩺 Health checks | ¿Hay endpoint /health? | FastAPI + livenessProbe |
| 📝 Logging | ¿Se logean inputs y outputs? | structlog + ELK / Loki |
| 🔐 Seguridad | ¿Auth, rate limiting, input validation? | API Gateway + WAF |
| 🔄 Rollback | ¿Se puede revertir en < 5 min? | K8s rollback / blue-green |
| 📈 Drift | ¿Hay monitorización de drift? | Evidently + alertas |
| 💰 Costes | ¿Se conoce el coste por inferencia? | Cloud billing + dashboards |
| 📖 Docs | ¿La API está documentada? | OpenAPI / Swagger (FastAPI auto) |
Widget: decisor de arquitectura de despliegue
Recursos y referencias
La puesta en producción de modelos ML es un campo que evoluciona rápidamente. A continuación recopilamos los recursos más relevantes organizados por tipo: guías prácticas, cursos, papers académicos y repositorios. Además, a lo largo de este submódulo hemos referenciado otros submódulos de esta web que complementan el contenido:
- Edge Computing — optimización y despliegue de modelos en dispositivos con recursos limitados.
- Machine Learning Clásico — métricas de evaluación, funciones de pérdida y conceptos básicos de ML.
- Deep Learning y Big Data — procesamiento distribuido de datos y entrenamiento a gran escala.
| Recurso | Tipo | Descripción |
|---|---|---|
| ml-ops.org | Guía | Referencia completa de MLOps: principios, herramientas y patrones. |
| Made With ML | Curso | Curso gratuito de MLOps end-to-end por Goku Mohandas. |
| Full Stack Deep Learning | Curso | Curso completo de producción ML por UC Berkeley. |
| Google MLOps Guide | Whitepaper | Niveles de madurez MLOps (0→2) por Google Cloud. |
| MLOps Tools Landscape | Blog | Mapa completo de herramientas MLOps actualizado regularmente. |
| Awesome MLOps | GitHub | Lista curada de papers, herramientas y recursos de MLOps. |
| Operationalizing ML (2022) | Paper | Encuesta académica sobre prácticas de MLOps en la industria. |
| Hidden Technical Debt in ML | Paper | El paper clásico de Google sobre deuda técnica en sistemas ML. |