Todos los artículos

Orquestar Modelos, No Solo Contenedores: Kubernetes en la Era de la IA

Escuchar el artículo (experimental) · 17:19

00:00 / 17:19

Un nodo router repartiendo peticiones hacia un rack de servidores de modelos con GPUs de distintos tamaños, con líneas discontinuas escapando del clúster hacia APIs de modelos externas

La mayor parte de mi trabajo vive en la capa de aplicación. Es donde se ganan los productos: los flujos, las APIs, las partes que los usuarios pueden señalar. Muchos productos se apoyan directamente en un único proveedor, un modelo de OpenAI o de Anthropic detrás de una sola clave, y están perfectamente bien servidos así. Pero cuando un producto crece más allá de un modelo, o empieza a tocar su propio hardware, pasa a apoyarse en una capa de la que sus usuarios nunca han oído hablar: la que decide, en el instante después de pulsar enter, qué modelo les responde, en qué pieza de hardware, y qué pasa si ese hardware está saturado o desaparece. Nadie le da las gracias a esa capa cuando funciona. Tiene exactamente un requisito, y es innegociable: no puede fallarle a la gente que ni sabe que la está usando.

Y ya no se trata solo de los productos que entregamos, sino también de cómo los construimos. Las herramientas de código en mi máquina hacen los mismos movimientos de los que trata este artículo: Claude Code delega subtareas ligeras en un modelo pequeño y rápido y deja el razonamiento difícil en uno grande, Cursor enruta entre sus propios modelos y APIs frontera según la acción, y el relato de Anthropic sobre su sistema de investigación multiagente describe exactamente esta forma, con un agente líder Opus orquestando subagentes Sonnet más baratos. El enrutamiento modelo-por-tarea se está convirtiendo discretamente en cómo se hace el software, no solo en cómo se sirve.

Mientras tanto, por todas partes hay equipos firmando con proveedores de APIs frontera (OpenAI, Anthropic, Google, xAI, MiniMax) mientras alojan además familias de pesos abiertos como Llama, Qwen, DeepSeek o Mistral en las GPUs que lograron conseguir, y alguien tiene que orquestar todos esos modelos con el hardware disponible. "¿Qué tal conoces Kubernetes?" es una pregunta de 2019. La pregunta de 2026 es: dado un pool fijo de GPUs, un conjunto de modelos de pesos abiertos que quieres alojar y un conjunto de modelos frontera que solo puedes alquilar a través de una API, ¿puedes hacer que todo eso parezca un único endpoint aburrido? Construí una prueba de concepto funcional que responde a la pregunta: un router de modelos, un conjunto de deployments conscientes de las GPUs y una demo que corre en un portátil. Este post es el razonamiento que hay detrás.

La economía grita utilización

Algo de contexto sobre por qué esta habilidad concreta importa de repente. Las Friday Charts de Blockworks sacaron las cuentas esta semana: los hyperscalers y las neoclouds habrán gastado unos 2 billones de dólares en capex a final de año, un solo armario de GPUs de los nuevos pods Vera Rubin de Nvidia se estima en 9 millones de dólares, y la newsletter abre con Jensen Huang declarando que "el centro de datos es la nueva unidad de computación". El mismo número señala que un gigavatio de capacidad de centro de datos produce ya cerca de 500 billones de tokens, frente a aproximadamente cero en 2023.

Probablemente no eres un hyperscaler, pero los números siguen doliendo a pie de calle: una sola tarjeta de gama alta puede costar bastante más de 40.000€ antes de que exista un servidor donde ponerla, y decidir cómo repartir esa tarjeta (entera, por la mitad, en séptimos) es un ejercicio de planificación por derecho propio, motivo por el cual los fabricantes de GPUs publican guías de dimensionamiento y matrices de configuración para acompañar a los compradores por las opciones.

Para ser claros, no todos los equipos necesitan preocuparse, y no es una elección binaria entre una API y tu propio clúster. Es una escalera, y cada peldaño hacia abajo te da más control sobre lo que hay debajo del endpoint:

  • Todo API. Un modelo de OpenAI o Anthropic detrás de una clave, o un deployment de Azure OpenAI con su catálogo fijo de modelos y cuotas de tokens por minuto. Tus palancas son cuotas, caching y elegir el tier de modelo adecuado para cada tarea.
  • Inferencia alquilada. Los Hugging Face Inference Endpoints, RunPod y sus pares, varios de los cuales he usado. Eliges el modelo y la tarjeta, otro gestiona las máquinas, y el contador cobra por GPU-hora estés ocupado o parado.
  • GPUs en la nube que operas tú. VMs con GPU facturadas por hora en GCP, AWS o Azure (una vez que el proveedor te concede cuota, porque la escasez es real), o node pools de GPU unidos a un clúster de Kubernetes gestionado como GKE, EKS o AKS, muchas veces a precios spot si tu carga tolera el desalojo. El contador se parece al del peldaño anterior; la diferencia es que la orquestación por debajo del endpoint pasa a ser tuya.
  • Metal que posees. Tarjetas compradas y montadas on-premise o en colocation: la tarjeta de 40.000€ de antes, amortizada en vez de medida por contador. Compensa con utilización sostenida o restricciones duras de residencia de datos, y Kubernetes también corre en bare metal, así que los mismos manifiestos te siguen hasta allí.

Del segundo peldaño hacia abajo el contador nunca se detiene, y la mayor palanca que un equipo normal controla por completo pasa a ser la fracción ociosa de la GPU-hora que ya paga. La orquestación no es un capricho de plataforma; es la diferencia entre una flota que se gana su coste y un calefactor carísimo. Kubernetes no es la única manera de llegar ahí: Slurm sigue siendo el dueño del mundo HPC, Ray trae su propio scheduler, y cualquier nube te venderá encantada un endpoint gestionado que esconde el problema detrás de un margen. Pero si tu producto ya corre en Kubernetes, el movimiento pragmático es dejar que la misma plataforma haga bin-packing también de los modelos: cargas heterogéneas sobre máquinas heterogéneas, de forma declarativa, junto a las cargas que ya ejecuta.

Tres fuerzas, un diseño

Toda configuración multi-modelo que he visto está tensionada por las mismas tres fuerzas:

  • Los modelos son heterogéneos. Algunos corren en tus GPUs (Qwen, Llama, y el resto del mundo de pesos abiertos), otros solo existen detrás de una API (los tiers frontera, sea quien sea con quien firmes). Difieren en ventana de contexto, soporte de herramientas, coste, latencia e implicaciones de residencia de datos.
  • El hardware es heterogéneo y finito. Una flota real es lo que pudiste conseguir: unas A100, un pool de L4, H100 el próximo trimestre si los dioses de los pedidos sonríen. Cada modelo tiene una o dos ubicaciones con sentido económico, y el resto es desperdicio.
  • El código de producto no quiere saber nada de esto. Quiere un endpoint compatible con OpenAI y un nombre de modelo, y debe seguir funcionando cuando se cambia de proveedor, se firma un contrato o se quema una tarjeta.

La PoC reparte la responsabilidad según esas líneas. Kubernetes es dueño de la colocación: qué pod aterriza en qué GPU, expresado en el lenguaje que ya habla (resource requests, node selectors, taints). Un pequeño router es dueño de la selección: qué backend sirve esta petición, con failover. Un registro es dueño de la política: qué existe, qué sabe hacer, cuánto cuesta. Vive en un ConfigMap recargado en caliente, para que "firmamos con un nuevo proveedor" sea un kubectl apply, no un deploy de producto.

La colocación es donde está el dinero

La parte de la PoC que defendería con más fuerza es su GPU playbook, porque aquí es donde el conocimiento de Kubernetes y el de GPUs tienen que encontrarse:

El GPU playbook de la PoC: cada carga, el hardware en el que aterriza y la técnica que la hace caber.
Carga Hardware Técnica
Qwen3-32B 2x A100-80GB Paralelismo de tensores, TP=2, NVLink en un solo nodo
Qwen3-8B 1x L4-24GB Cuantización AWQ 4-bit para caber en la tarjeta barata
Embeddings Un séptimo de una A100 Slice MIG, 1g.10gb, aislado por hardware
Experimentos dev GPUs compartidas Time-slicing, cuatro cargas por tarjeta
Modelos frontera GPUs de nadie API externa detrás del mismo endpoint

Cada fila es un pequeño argumento. El modelo de 32B no cabe en una tarjeta de 80GB con margen para respirar (solo los pesos son ~64GB en bf16), así que se parte entre dos, y el manifiesto fija el producto de GPU exacto vía nodeSelector, porque "dos GPUs en algún lugar del clúster" y "dos A100 con NVLink en el mismo nodo" son máquinas radicalmente distintas. El modelo de 8B va en dirección contraria: cuantizar hasta que quepa en la tarjeta que de verdad puedes comprar, y luego escalar horizontalmente. La memoria que libera la cuantización no es solo encaje, es KV cache, y la KV cache es concurrencia: la cuantización a menudo te compra rendimiento, no solo pesos más pequeños.

La fila de los embeddings es mi favorita, porque es donde el dinero muere en silencio en la mayoría de los clústeres. Un modelo de embeddings necesita unos 2GB, y la misma matemática aplica a rerankers, clasificadores de guardrails y modelos de transcripción de la clase de Whisper, los pequeños servicios siempre encendidos que todo producto de IA acumula. Dale a cualquiera de ellos una A100 entera y ~97% de una tarjeta carísima quedará ociosa para siempre. MIG, Multi-Instance GPU, corta el silicio en siete slices aislados; el Deployment pide nvidia.com/mig-1g.10gb: 1 como cualquier otro recurso, y una tarjeta sirve siete de ellos, cada uno aislado por hardware de sus vecinos. El time-slicing hace algo parecido para clústeres de dev, sin aislamiento alguno: un proceso glotón puede provocar OOM al resto, y exactamente por eso esa configuración de la PoC está marcada como solo-dev.

Nada de esto es exótico. Es Kubernetes corriente (resource requests, labels, probes, disruption budgets) apuntado a un recurso poco familiar. Ese es el verdadero significado de "aprovechar Kubernetes en la era de la IA": la plataforma no cambió, cambió la carga de trabajo.

El motor de serving también es una elección

Todo lo que corre en el clúster se sirve con vLLM, y eso es una preferencia probada, no un default. La propiedad que lo vende es contraintuitiva: mejora bajo presión. El continuous batching hace que, cuantas más peticiones concurrentes le lances a una instancia, más tokens por GPU-hora produzca; el tráfico bajo es el caso patológico, no la hora punta. También probé Ollama por el camino, y la comparación enseñó la misma lección que la tabla de colocación: las herramientas codifican suposiciones sobre su carga de trabajo. Ollama es excelente para lo que fue construido (una persona, una máquina), pero bajo carga concurrente fue claramente más lento en mis pruebas, y cambiar rápido entre modelos distintos le dio problemas reales, porque cada cambio descarga un modelo y carga otro. Una instancia de vLLM nunca tiene ese problema porque nunca se cambia de ropa: un modelo por servidor durante toda su vida, y el router de arriba decide qué servidor merece cada petición.

El enrutamiento es una política, no un if

El router habla /v1/chat/completions y resuelve nombres en dos modos deliberadamente distintos. Un nombre de modelo exacto (qwen3-32b) sigue una cadena de failover explícita, escrita por el operador: primero el vLLM del clúster, una API frontera como desbordamiento. Sin astucia. Un alias (chat-default, chat-long-context) es, en cambio, un conjunto de requisitos (ventana de contexto mínima, soporte de herramientas) más una estrategia: cheapest-healthy ordena todos los backends que cualifican por coste combinado; prefer-cluster mantiene el tráfico en hardware propio salvo que nada del clúster cualifique, lo que además funciona como interruptor de residencia de datos.

Los aliases son la mitad que cambia la forma de trabajar de un equipo. El código de producto pide un resultado, no un vendor, así que "el 8B ya aguanta este tráfico" o "manda el contexto largo al proveedor que mejor lo precie" se convierten en ediciones del registro que salen en segundos. Un circuit breaker por backend salta los backends enfermos, cada respuesta lleva una cabecera x-router-backend para que siempre sepas quién respondió, y el streaming solo hace failover antes del primer byte: con tokens fluyendo, repetir la generación a espaldas del usuario duplicaría el coste y empalmaría dos respuestas distintas.

Una cosa que el router se niega a hacer: encolar. vLLM ya mantiene la cola honesta, y esconderla detrás de una segunda destruiría la mejor señal de autoescalado del sistema. Lo que me lleva a la trampa.

La trampa de la utilización de GPU

Si te llevas un solo detalle operativo de este post, que sea este: la utilización de GPU es una señal de autoescalado engañosa para servir LLMs. El continuous batching mantiene la tarjeta ocupada tanto si sirve dos peticiones cómodamente como si se ahoga con doscientas: ambas leen ~100%. Escala según la utilización y estarás escalando según el ruido.

El número que refleja lo que sienten los usuarios es la profundidad de la cola: el num_requests_waiting de vLLM, peticiones admitidas pero aún no programadas en el batch. La PoC escala el tier de trabajo de 8B exactamente con esa métrica vía KEDA: hacia arriba rápido, porque las colas se retroalimentan; hacia abajo despacio y de pod en pod, porque cada réplica nueva paga minutos de carga de modelo y el flapping es caro. ¿Y cuando el pool de L4 entero se agota? Eso ya no es un problema de autoescalado. La cadena de failover desborda hacia las APIs externas, el tier elástico que alquilas precisamente porque no lo posees.

Y la capa de aplicación tampoco puede fingir que nada de esto existe. Un cuello de botella de GPU acaba aflorando en el producto, y las buenas respuestas viven ahí: backpressure, timeouts, un modo degradado. Un modelo más pequeño con una etiqueta honesta vale más que un spinner que muere a los sesenta segundos.

Lo que la PoC demuestra, y lo que no

Todo corre sin una sola GPU: un overlay local sustituye cada Deployment de vLLM por un mock que habla la misma API y exporta las mismas métricas, de modo que el enrutamiento, el failover y las señales de escalado pueden demostrarse en un clúster kind de portátil. Mata el deployment del 32B a mitad de la demo y mira cómo la misma petición vuelve respondida por el tier "externo". La lógica de enrutamiento es pura y está cubierta por tests unitarios; los manifiestos se renderizan con kustomize en CI.

Sigue siendo una PoC, y prefiero decirlo antes de que lo descubras: no hay autenticación, ni rate limiting, ni presupuestos por tenant, y las claves externas son Secrets planos. Esa es la primera iteración de producción, y nada de ello cambia la arquitectura. Los conceptos de plataforma se transfieren casi intactos entre distribuciones y nubes; lo que decide el éxito es si la economía de la carga de trabajo se entendió antes de escribir el YAML.

La capa de aplicación sigue siendo donde paso la mayoría de mis días, y sigue siendo donde se ganan los productos. Pero debajo de los productos de IA que crecen más allá de un modelo hay ahora una capa haciendo el trabajo de infraestructura más difícil de la década: coger una restricción de la era de los 2 billones en capex, un zoo de modelos y una pila fija de hardware, y hacer que se comporten como un endpoint fiable, intercambiable y aburrido, para gente que nunca sabrá que existe. Tiene que funcionar. No puede fallarles. La PoC es mi manera de demostrar que no tiene por qué hacerlo.

Preguntas que responde este artículo

  • ¿Necesito GPUs propias para montar un producto de IA o me basta con una clave de API?

    Una clave basta para muchos productos, y la elección no es binaria. Es una escalera de cuatro peldaños: todo API detrás de un único proveedor, inferencia alquilada en servicios como los Hugging Face Inference Endpoints o RunPod, GPUs en la nube que operas tú, y metal que posees y montas en rack. Cada peldaño hacia abajo te da más control sobre lo que hay debajo del endpoint, y desde el segundo peldaño pagas por GPU-hora estés ocupado o parado.

  • ¿No es Kubernetes matar moscas a cañonazos solo para servir unos pocos modelos?

    No es la única manera, y las alternativas son reales: Slurm sigue siendo el dueño del mundo HPC, Ray trae su propio scheduler, y cualquier nube te vende un endpoint gestionado que esconde el problema detrás de un margen. El argumento pragmático a favor de Kubernetes es más estrecho de lo que sugiere el ruido: si tu producto ya corre en él, deja que la misma plataforma haga bin-packing de los modelos junto a las cargas que ya ejecuta. Lo que usa la PoC es Kubernetes corriente, es decir resource requests, labels, probes y disruption budgets, apuntado a un recurso poco familiar.

  • ¿Por qué usar vLLM en el clúster en vez de Ollama?

    vLLM mejora bajo presión, y esa es la propiedad que lo vende. El continuous batching hace que más peticiones concurrentes produzcan más tokens por GPU-hora, así que el tráfico bajo es el caso patológico y no la hora punta. Ollama es excelente para una persona y una máquina, pero bajo carga concurrente fue claramente más lento en mis pruebas, y cambiar entre modelos le dio problemas reales, porque cada cambio descarga un modelo y carga otro. Un servidor vLLM mantiene un modelo durante toda su vida, y el router de arriba decide a qué servidor va cada petición.

  • ¿Qué pasa cuando todas las GPUs ya están ocupadas?

    Eso deja de ser un problema de autoescalado y se convierte en uno de failover: la cadena desborda hacia las APIs externas, el tier elástico que alquilas precisamente porque no lo posees. Por debajo de ese punto, la PoC escala el tier de 8B con el num_requests_waiting de vLLM vía KEDA, hacia arriba rápido porque las colas se retroalimentan y hacia abajo despacio porque cada réplica nueva paga minutos de carga de modelo. La capa de aplicación todavía tiene que responder por el resto con backpressure, timeouts y un modo degradado, porque un modelo más pequeño con una etiqueta honesta vale más que un spinner que muere a los sesenta segundos.