¿Has intentado llevar un modelo de lenguaje de un simple script de Python a un entorno de producción real? Pasar de «funciona en mi Jupyter Notebook» a «soporta cientos de peticiones concurrentes con baja latencia» es un salto cuántico. Si te lanzas a desplegar LLMs sin una estrategia de orquestación, te encontrarás rápidamente con un caos de errores de memoria, costos de GPU disparados y servicios que se caen en cuanto llega el primer pico de tráfico.
Para escalar de verdad, necesitas la combinación ganadora: Kubernetes para la orquestación y vLLM para la inferencia de alto rendimiento. En esta guía, vamos a construir una infraestructura robusta para desplegar y escalar modelos de lenguaje grandes (LLMs) utilizando estrategias de optimización avanzadas, desde el auto-scaling basado en métricas reales hasta la gestión de costos con instancias Spot.
🎯 Objetivos de Aprendizaje
Después de completar esta guía, podrás:
- Desplegar LLMs usando vLLM en Kubernetes
- Configurar auto-scaling basado en métricas de GPU
- Implementar estrategias de caching y optimización
- Gestionar múltiples modelos en producción
- Monitorear rendimiento y costos de inferencia
📋 Prerrequisitos
- Conocimientos básicos de Kubernetes
- Experiencia con Docker y Helm
- Familiaridad con LLMs y vLLM
- Cluster Kubernetes con GPUs (opcional pero recomendado)
🏗️ Arquitectura de Despliegue
Componentes Principales
Antes de escribir una sola línea de YAML, necesitamos entender cómo encajan las piezas en el ecosistema. No es solo levantar un contenedor; es crear un flujo donde el tráfico entra, se distribuye y se escala según la demanda real de inferencia.

Estrategias de Despliegue
Dependiendo de tu carga de trabajo, puedes elegir entre distintos patrones:
- Single Model per Pod: Aislamiento completo para máxima estabilidad.
- Multi-Model per Pod: Optimización de recursos si tienes modelos pequeños.
- Model Sharding: Distribución de modelos grandes (como Llama 3 70B) en múltiples GPUs.
- Dynamic Loading: Carga de modelos bajo demanda para ahorrar costes.
🚀 Despliegue Básico con vLLM
1. Preparación del Cluster
Lo primero es asegurarnos de que Kubernetes «ve» tus GPUs. Si no tienes instalado el operador de NVIDIA, el cluster tratará a tus nodos como máquinas CPU estándar y el despliegue fallará.
# Verificar GPUs disponibles
kubectl get nodes -o json | jq '.items[].status.capacity."nvidia.com/gpu"'
# Instalar NVIDIA GPU Operator (si no está instalado)
helm repo add nvidia https://nvidia.github.io/gpu-operator
helm repo update
helm install gpu-operator nvidia/gpu-operator \
--create-namespace \
--namespace gpu-operator
2. Crear Namespace y ConfigMaps
Vamos a organizar el caos creando un espacio dedicado y una configuración centralizada. Usar un ConfigMap nos permite cambiar parámetros del modelo (como el DTYPE o la longitud máxima) sin tener que reconstruir la imagen de Docker.
# vllm-namespace.yaml
apiVersion: v1
kind: Namespace
metadata:
name: vllm-system
labels:
name: vllm-system
# vllm-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vllm-config
namespace: vllm-system
data:
MODEL_NAME: "microsoft/DialoGPT-medium"
MODEL_REVISION: "main"
DTYPE: "float16"
MAX_MODEL_LEN: "2048"
GPU_MEMORY_UTILIZATION: "0.9"
MAX_NUM_SEQS: "256"
TENSOR_PARALLEL_SIZE: "1"
3. Despliegue con Helm
Aquí es donde ocurre la magia. Al definir el Deployment, es crucial configurar correctamente los recursos y, sobre todo, los probes.
# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deployment
namespace: vllm-system
spec:
replicas: 1
selector:
matchLabels:
app: vllm
template:
metadata:
labels:
app: vllm
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
ports:
- containerPort: 8000
envFrom:
- configMapRef:
name: vllm-config
resources:
limits:
nvidia.com/gpu: 1
requests:
nvidia.com/gpu: 1
# startupProbe: cubre la carga del modelo. Hasta que no pasa,
# liveness y readiness ni siquiera se evalúan.
# 60 fallos x 10s = 10 minutos de margen para cargar los pesos.
startupProbe:
httpGet:
path: /health
port: 8000
failureThreshold: 60
periodSeconds: 10
livenessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 10
readinessProbe:
httpGet:
path: /health
port: 8000
periodSeconds: 5
Sin startupProbe el pod entra en CrashLoopBackOff
Un livenessProbe con initialDelaySeconds: 30 da por muerto al contenedor a los 30 segundos. Cargar los pesos de un modelo de 13B desde disco a la GPU tarda varios minutos, así que Kubernetes lo mata justo antes de que termine de arrancar, lo reinicia, y vuelve a matarlo: un bucle infinito que además consume la GPU en cada intento.
El startupProbe existe precisamente para esto: mientras no tiene éxito, los otros dos probes quedan suspendidos. Ajusta failureThreshold al tiempo real de carga de tu modelo con margen — es preferible pasarse que quedarse corto.
4. Servicio e Ingress
Para que tu aplicación pueda hablar con el modelo, necesitamos exponerlo mediante un Service y, para el acceso externo, un Ingress.
# vllm-service.yaml
apiVersion: v1
kind: Service
metadata:
name: vllm-service
namespace: vllm-system
spec:
selector:
app: vllm
ports:
- port: 80
targetPort: 8000
type: ClusterIP
# vllm-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vllm-ingress
namespace: vllm-system
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
spec:
rules:
- host: vllm.yourdomain.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vllm-service
port:
number: 80
📊 Auto-Scaling con HPA
Si tu servicio recibe un pico de tráfico, no quieres que la latencia se dispare. Pero ojo: escalar LLMs no es igual que escalar una web de microservicios.
Configuración de Métricas
Primero, asegúrate de tener el metrics-server para que el HPA tenga datos de dónde tirar.
# metrics-server.yaml (si no está instalado)
apiVersion: v1
kind: ServiceAccount
metadata:
name: metrics-server
namespace: kube-system
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: metrics-server
namespace: kube-system
spec:
template:
spec:
containers:
- name: metrics-server
image: k8s.gcr.io/metrics-server/metrics-server:v0.6.3
args:
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP
HPA para vLLM
El truco está en las métricas. No queremos escalar por CPU, queremos escalar por la carga real de inferencia.
# vllm-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-hpa
namespace: vllm-system
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-deployment
minReplicas: 1
maxReplicas: 10
metrics:
# Métrica principal: peticiones esperando en la cola del scheduler.
# Es la que refleja de verdad que el servicio no da abasto.
- type: Pods
pods:
metric:
name: vllm:num_requests_waiting
target:
type: AverageValue
averageValue: "5"
# Secundaria: ocupación de la KV cache. Cerca del 100% significa
# que no caben más peticiones concurrentes por mucha GPU libre que haya.
- type: Pods
pods:
metric:
name: vllm:kv_cache_usage_perc
target:
type: AverageValue
averageValue: "900m" # 0.9 = 90%
behavior:
scaleUp:
# Un pod nuevo tarda minutos en cargar el modelo: no sirve
# de nada escalar agresivamente, solo duplica el consumo de GPU.
stabilizationWindowSeconds: 120
scaleDown:
stabilizationWindowSeconds: 600
No escales por CPU en un servicio de inferencia
Es el error más habitual al reutilizar el HPA de una aplicación web. Durante la generación de tokens el proceso está esperando a la GPU, no calculando: la CPU se queda baja aunque el servicio esté saturado y la cola creciendo. Un HPA por CPU al 70% sencillamente no dispara nunca, o dispara cuando ya da igual.
Las dos métricas que sí correlacionan con saturación real son la longitud de la cola (vllm:num_requests_waiting) y la ocupación de la KV cache, que es lo que limita de verdad la concurrencia. Requieren prometheus-adapter para exponerlas al HPA. Tienes el detalle de ambas en Monitoreo de LLMs.
🔧 Optimizaciones Avanzadas
Una vez que el despliegue básico funciona, el siguiente paso es exprimir cada ciclo de la GPU y cada GB de memoria.
1. Model Caching y Warm-up
Evita que cada nuevo Pod tenga que descargar gigabytes de pesos desde Hugging Face. Usar un initContainer para precargar el modelo en un volumen compartido acelera el arranque drásticamente.
# vllm-deployment-optimized.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deployment-optimized
namespace: vllm-system
spec:
template:
spec:
initContainers:
- name: model-cache
image: vllm/vllm-openai:latest
command: ["/bin/sh", "-c"]
args:
- |
python -c "
from vllm import LLM
llm = LLM(model='microsoft/DialoGPT-medium', download_dir='/tmp/models')
print('Model cached successfully')
"
volumeMounts:
- name: model-cache
mountPath: /tmp/models
containers:
- name: vllm
image: vllm/vllm-openai:latest
env:
- name: VLLM_CACHE_DIR
value: /tmp/models
volumeMounts:
- name: model-cache
mountPath: /tmp/models
volumes:
- name: model-cache
emptyDir: {}
2. Multi-Model Deployment
Si tienes recursos sobrantes, puedes servir varios modelos ligeros desde una misma configuración.
# multi-model-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: multi-model-config
namespace: vllm-system
data:
models.json: |
[
{
"name": "gpt2-medium",
"model": "microsoft/DialoGPT-medium",
"max_model_len": 1024
},
{
"name": "gpt2-large",
"model": "microsoft/DialoGPT-large",
"max_model_len": 1024
}
]
3. GPU Memory Optimization
Para maximizar la concurrencia, ajustamos cómo vLLM gestiona la memoria de la GPU. El chunked_prefill es clave para evitar picos de latencia.
# vllm-gpu-optimized.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-gpu-optimized
namespace: vllm-system
spec:
template:
spec:
containers:
- name: vllm
env:
- name: VLLM_GPU_MEMORY_UTILIZATION
value: "0.95"
- name: VLLM_MAX_NUM_SEQS
value: "128"
- name: VLLM_MAX_NUM_BATCHED_TOKENS
value: "4096"
- name: VLLM_ENABLE_CHUNKED_PREFILL
value: "true"
resources:
limits:
nvidia.com/gpu: 1
memory: 32Gi
requests:
nvidia.com/gpu: 1
memory: 16Gi
📈 Monitoreo y Observabilidad
No puedes optimizar lo que no puedes medir. vLLM expone métricas nativas que debemos capturar.
Métricas de vLLM
Configuramos un ServiceMonitor para que Prometheus recoja las métricas automáticamente.
# prometheus-service-monitor.yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: vllm-monitor
namespace: vllm-system
spec:
selector:
matchLabels:
app: vllm
endpoints:
- port: metrics
path: /metrics
interval: 30s
Dashboard de Grafana
Con estos datos, puedes montar un panel en Grafana que te diga exactamente qué está pasando con la latencia y la utilización de la GPU.
{
"dashboard": {
"title": "vLLM Performance Dashboard",
"panels": [
{
"title": "GPU Utilization",
"type": "graph",
"targets": [
{
"expr": "nvidia_gpu_utilization{namespace=\"vllm-system\"}",
"legendFormat": "{{pod}}"
}
]
},
{
"title": "Request Latency",
"type": "graph",
"targets": [
{
"expr": "histogram_quantile(0.95, rate(vllm_request_duration_seconds_bucket[5m]))",
"legendFormat": "95th percentile"
}
]
}
]
}
}
🔄 Estrategias de Actualización
Actualizar un modelo o la versión de vLLM en producción sin interrumpir el servicio es vital.
Rolling Updates
La forma estándar de Kubernetes para actualizar gradualmente sin downtime.
# vllm-deployment-rolling.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-deployment
namespace: vllm-system
spec:
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
# ... resto de la configuración
Blue-Green Deployment
Para cambios críticos, preferimos un despliegue Blue-Green donde levantamos la versión nueva por completo antes de redirigir el tráfico.
# blue-green-deployment.sh
#!/bin/bash
# Crear nueva versión (green)
kubectl apply -f vllm-deployment-green.yaml
# Esperar a que esté listo
kubectl wait --for=condition=available --timeout=300s deployment/vllm-deployment-green -n vllm-system
# Cambiar el servicio al green
kubectl patch service vllm-service -n vllm-system -p '{"spec":{"selector":{"version":"green"}}}'
# Verificar que funciona
# ... tests ...
# Eliminar blue
kubectl delete deployment vllm-deployment-blue -n vllm-system
🛡️ Seguridad y Compliance
Network Policies
No permitas que cualquier Pod en el cluster acceda a tu servicio de inferencia. Restringe el acceso solo desde tu Ingress.
# vllm-network-policy.yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: vllm-network-policy
namespace: vllm-system
spec:
podSelector:
matchLabels:
app: vllm
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: ingress-nginx
ports:
- protocol: TCP
port: 8000
egress:
- to:
- podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
Secret Management
base64 no es cifrado
Un Secret de Kubernetes guarda el valor en base64, que es codificación, no cifrado: cualquiera que lea el YAML lo descodifica con un comando. Un manifiesto así no se versiona en Git jamás.
Para GitOps usa SOPS, Sealed Secrets o External Secrets Operator. Tienes la comparativa de los tres en Secretos en GitOps.
Referencia del objeto (para crearlo desde línea de comandos, no para commitearlo):
# Crea el Secret sin que el token pase nunca por un archivo del repo
kubectl create secret generic vllm-secrets \
--namespace vllm-system \
--from-literal=huggingface-token="$HF_TOKEN" \
--from-literal=api-key="$VLLM_API_KEY"
# Así lo consume el Deployment: por referencia, sin exponer el valor
env:
- name: HUGGING_FACE_HUB_TOKEN
valueFrom:
secretKeyRef:
name: vllm-secrets
key: huggingface-token
📊 Cost Optimization
La GPU es el recurso más caro. Si no la gestionas bien, la factura de la nube te sorprenderá.
Spot Instances y Preemptible
Si tu aplicación puede tolerar pequeñas interrupciones (o tienes un buen HPA), usa instancias Spot para ahorrar hasta un 70-90%.
# spot-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm-spot
namespace: vllm-system
spec:
template:
spec:
tolerations:
- key: "cloud.google.com/gke-spot"
operator: "Equal"
value: "true"
effect: "NoSchedule"
nodeSelector:
cloud.google.com/gke-spot: "true"
# ... resto de configuración
Auto-scaling basado en costos
Puedes incluso configurar el HPA para que sea consciente del coste horario de tu proveedor.
# cost-based-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: vllm-cost-hpa
namespace: vllm-system
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: vllm-deployment
minReplicas: 1
maxReplicas: 5
metrics:
- type: External
external:
metric:
name: cloud_provider_cost_per_hour
target:
type: AverageValue
averageValue: 2.0
🔍 Troubleshooting
Problemas Comunes
-
Out of Memory (OOM):
# Verificar logs kubectl logs -f deployment/vllm-deployment -n vllm-system # Ajustar configuración kubectl edit configmap vllm-config -n vllm-system -
GPU Not Available:
# Verificar GPU allocation kubectl describe node <node-name> # Check GPU operator kubectl get pods -n gpu-operator -
Slow Inference:
# Verificar métricas kubectl exec -it deployment/vllm-deployment -n vllm-system -- curl http://localhost:8000/metrics # Ajustar batch size kubectl edit configmap vllm-config -n vllm-system
🎯 Mejores Prácticas
Para resumir, si quieres un despliegue de nivel producción, sigue estos pilares:
Performance
- Usa GPU A100/H100 para mejor rendimiento
- Configura
tensor_parallel_sizepara múltiples GPUs - Implementa caching de modelos
- Monitorea constantemente métricas
Reliability
- Implementa health checks apropiados
- Usa rolling updates para zero-downtime
- Configura resource limits y requests
- Implementa circuit breakers
Security
- Usa secrets para tokens de API
- Implementa network policies
- Audita logs de acceso
- Mantén modelos actualizados
Cost Management
- Usa spot instances cuando sea posible
- Implementa auto-scaling inteligente
- Monitorea costos en tiempo real
- Optimiza uso de GPU
📚 Recursos Adicionales
🤝 Contribuir
Esta guía es parte del proyecto Frikiteam Docs. Si encuentras errores o quieres contribuir mejoras:
- Fork el repositorio
- Crea una rama para tu feature
- Envía un Pull Request
¡Gracias por contribuir al conocimiento compartido!
