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.
📊 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. Este submódulo te enseña a superar cada obstáculo.
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.
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.
| 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.
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.
| Métrica | Descripción | Objetivo típico | Herramienta |
|---|---|---|---|
| Latencia p50 / p99 | Tiempo de respuesta mediano y percentil 99 | p50 < 50ms, p99 < 200ms | Prometheus, Datadog |
| Throughput (RPS) | Requests por segundo soportados | Depende del caso | Locust, k6, wrk |
| Availability (SLA) | Porcentaje de uptime | 99.9 % (8.7h 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 |
🧮 Calculadora de coste de inferencia
Estima el coste mensual de servir tu modelo en la nube según el volumen de tráfico y el tipo de instancia.
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.
¿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.
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 |
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.
Conceptos clave de Kubernetes
| 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.
| 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.
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 |
🧪 Selector de framework de serving
Responde 4 preguntas y te recomendamos el framework óptimo para tu caso.
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.
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
☁️ Estimador de coste mensual de serving
Compara el coste estimado de servir tu modelo en las 3 grandes plataformas cloud y en alternativas managed.
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.
# ═══════════════════════════════════════════════════
# .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.
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.
| 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).
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:
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
🗺️ ¿Qué arquitectura de despliegue necesitas?
Responde a estas preguntas y te recomendamos la arquitectura, herramientas y estimación de coste para tu caso.
Recursos y referencias
| 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. |