Cadena de confianza del modelo (1/4): KServe y el Open Inference Protocol, el plano de control que le falta a tu serving
Este blog ha dedicado buena parte de sus 145 artículos a lo que ocurre dentro del proceso de inferencia y otra buena parte a lo que ocurre alrededor. Falta un hilo que atraviese todo eso y responda a una pregunta que en una auditoría se hace en treinta segundos y en producción no se contesta en tres semanas: cuando el endpoint devuelve un token, ¿de dónde salió ese modelo, quién lo firmó, y quién demuestra que el binario que corre en la GPU es el que se aprobó?
Esta serie recorre esa cadena hacia atrás desde el único punto visible, el endpoint: el plano de control y el contrato de API (este artículo), el registro y la distribución de los bytes (2/4), la firma y la procedencia que permiten fiarse de ellos (3/4), y la identidad y el aislamiento de quien los ejecuta (4/4). Todo con proyectos bajo gobernanza de CNCF, LF AI & Data o Linux Foundation: una cadena de confianza que depende de un producto propietario no es una cadena, es un contrato de soporte.
TL;DR
- Un
Deploymentde vLLM sirve tokens perfectamente. Lo que no da es contrato: ni versionado de modelo, ni rollout por porcentaje, ni scale-to-zero, ni una superficie común para los treinta artefactos no-LLM de una factoría de inferencia. - KServe aporta el vocabulario:
InferenceService,ServingRuntime/ClusterServingRuntime, storage initializer ystorageUri. Es CNCF incubating desde el 29 de septiembre de 2025, tras siete años como KFServing y una etapa en LF AI & Data. La cadencia se ha acelerado: v0.18.0 el 29 de abril de 2026 y v0.19.0 el 14 de junio de 2026. - La rama LLM es reciente y distinta: la CRD
LLMInferenceService(serving.kserve.io/v1alpha1) llegó en la v0.16 de noviembre de 2025, sobre llm-d, conspec.prefill,spec.worker,spec.parallelismyspec.router. - El Open Inference Protocol (V2) es el contrato:
/v2/health/*,/v2/models/<m>,/v2/models/<m>/infer, en REST y gRPC, implementado por Triton, OpenVINO Model Server, Seldon MLServer y AMD Inference Server. Es tensor-céntrico, y el mundo LLM se estandarizó de facto sobre la API de OpenAI: KServe sirve las dos, con las de OpenAI bajo prefijoopenai/. - Lo honesto: la portabilidad real de un LLM hoy la da la API de OpenAI, no el V2. Aceptar la doble vía es correcto; fingir que hay un estándar único, no.
- KServe sobra si sirves un modelo en un nodo. Empieza a pagar a partir del quinto artefacto heterogéneo, y compite en otro eje que los operators de LLM: contrato y ciclo de vida frente a motor y topología.
La analogía: el jefe de estación y el ancho de vía
Una factoría de inferencia se parece a una estación de ferrocarril con varias líneas.
El jefe de estación no conduce ningún tren. Su trabajo es el cuadro de servicio: qué composición sale de qué andén, cuántas unidades se enganchan a cada hora, cuándo se retira una máquina a taller, y —el día que llega material nuevo— cómo desviar al tren de pruebas un diez por ciento de los viajeros antes de comprometer el servicio entero. Eso es el plano de control. KServe no genera un solo token, pero decide qué corre, con qué configuración, con cuántas réplicas y con qué reparto de tráfico. Los trenes son los motores —vLLM, SGLang, TensorRT-LLM, Triton, TEI—, y un jefe de estación competente no se casa con un fabricante.
Y luego está el ancho de vía: mientras todos construyan para el mismo, cualquier locomotora rueda por cualquier línea y cambiar de proveedor es una decisión comercial, no una obra civil. Ese ancho es el Open Inference Protocol, el contrato que hace que un cliente escrito contra Triton funcione contra OpenVINO Model Server sin tocar una línea.
La analogía tiene un giro que en España se entiende sin explicación: el ancho acordado no es el ancho por el que circula el material nuevo. La red convencional se construyó en ancho ibérico y la alta velocidad en ancho internacional, y la solución práctica no fue reconvertir la red, sino poner cambiadores de ancho en los puntos de frontera. En la inferencia ha pasado lo mismo: el V2 es el ancho acordado y la API de OpenAI es el ancho por el que circula todo lo generativo. Lo que sostiene la interoperabilidad no es un estándar único: es el cambiador. Volveremos a él.
El problema: un Deployment sirve tokens, no ofrece contrato
En vLLM en Kubernetes montamos un servicio con Deployment, Service, HPA y un initContainer que baja los pesos. Funciona, y para un modelo en un nodo es la respuesta correcta. Lo que no da, y no puede dar sin reinventarlo cada vez:
Contrato de API estable. El endpoint es “lo que exponga la imagen que has puesto”: pasar de vLLM a TensorRT-LLM cambia rutas, formato de error y nombres de métricas, y los clientes se enteran en producción.
Versionado con semántica. El modelo es una ruta dentro de un volumen; saber qué versión sirve un pod exige kubectl exec. No hay campo declarativo que diga “este servicio sirve el artefacto X” y se pueda auditar desde fuera.
Rollout progresivo de modelo. Un RollingUpdate sustituye pods; no reparte tráfico entre dos versiones con un porcentaje. Para eso hace falta un gateway L7 delante —lo vimos en canary, blue-green y shadow— o un plano de control que lo emita.
Scale-to-zero. El HPA no baja de una réplica: para un modelo consultado doce veces al día, eso es una GPU de 80 GB inmovilizada veinticuatro horas.
Y, sobre todo, superficie común para lo que no es un LLM. Una factoría real no sirve tres LLM: sirve dos o tres LLM, cuatro modelos de embeddings, un par de rerankers, clasificadores de tickets, un detector de PII, un OCR, visión industrial y tres artefactos de scikit-learn que nadie recuerda quién entrenó. Quince o treinta artefactos, quince runtimes, quince formas de decir “estoy listo”. El coste no está en servir el primero: está en que el número treinta no cueste treinta veces lo que el primero.
KServe: de KFServing a proyecto incubado de la CNCF
KServe nació en 2019 como KFServing, dentro de Kubeflow, en una colaboración entre Google, IBM, Bloomberg, NVIDIA y Seldon. En febrero de 2022 se donó a la LF AI & Data Foundation; en septiembre de 2022 se separó de Kubeflow y se renombró. El salto relevante: el 29 de septiembre de 2025 la CNCF lo aceptó directamente en nivel incubating, sin pasar por sandbox, con anuncio público el 11 de noviembre. Las cifras que declaró entonces —más de 300 contribuidores, 19 mantenedores, más de 30 organizaciones adoptantes (Bloomberg, Red Hat, Cloudera, Nutanix, SAP, NVIDIA)— son de parte interesada, pero la lista es verificable y las 625 organizaciones contribuidoras del panel de la CNCF sostienen la tesis de gobernanza no capturada por un vendor.
Sobre la cadencia conviene ser preciso, porque el tópico de “KServe va lento” ya no describe la realidad:
| Versión | Fecha | Qué trajo |
|---|---|---|
| v0.15.0 | 31 de marzo de 2025 | LocalModelCache, inferencia multi-nodo, API de embeddings compatible con OpenAI, Gateway API en modo raw, integración KEDA |
| v0.16.0 | noviembre de 2025 | LLMInferenceService; renombrado de RawDeployment a Standard y de Serverless a KNative; rollout progresivo en modo raw |
| v0.18.0 | 29 de abril de 2026 | Autoescalado WVA + KEDA/HPA para LLMISvc, ruta /v1/responses (Responses API de OpenAI), Pod Security Standards restricted por defecto en LLMISvc, ModelCache con ámbito de namespace |
| v0.19.0 | 14 de junio de 2026 | Enrutado por nombre de modelo, adaptadores LoRA estáticos, balanceo entre GPU heterogéneas, enrutado dual REST/gRPC en modo Standard, migración automática a InferencePool v1 |
Dos releases mayores en menos de dos meses, con el grueso del changelog en la rama LLM. El proyecto no va lento: ha girado, del serving predictivo clásico al generativo. Y eso tiene consecuencias en las dos direcciones.
La negativa se llama ModelMesh. Era el modo de alta densidad —cientos de modelos pequeños compartiendo procesos, con carga y descarga dinámica— y era, sobre el papel, la respuesta natural al problema de los treinta artefactos heterogéneos. Su última release fue la v0.12.0 del 9 de julio de 2023, y el repositorio kserve/modelmesh-serving quedó archivado en solo lectura el 14 de abril de 2026. Red Hat publicó guía específica para migrar a modo Standard. Si alguien propone hoy ModelMesh en un diseño, es señal de que la documentación que ha leído tiene tres años.
Anatomía del plano de control
InferenceService: predictor, transformer, explainer
La CRD nuclear es InferenceService (serving.kserve.io/v1beta1), y su aportación —la que se convirtió en vocabulario común del campo— es descomponer un servicio de inferencia en tres componentes con contrato propio:
predictor: obligatorio. DeclaramodelFormat, opcionalmenteruntime, elstorageUride donde salen los pesos y elprotocolVersionque expone.transformer: opcional. Pre y post-procesado. Que sea un componente separado, y no código dentro del servidor, es lo que permite escalarlo aparte —suele ser CPU-bound mientras el predictor es GPU-bound— y versionarlo aparte.explainer: opcional, y el que peor ha envejecido: el protocolo V2 no soporta el endpointexplain, que solo existe en V1. Quien necesite explicabilidad con contrato estándar tiene aquí un hueco real.
Los tres comparten ComponentExtensionSpec: minReplicas, maxReplicas, scaleTarget, scaleMetric, containerConcurrency, timeout y canaryTrafficPercent.
ServingRuntime y ClusterServingRuntime: el catálogo de trenes
Un ServingRuntime es una plantilla de pod que sabe servir uno o varios formatos. Declara supportedModelFormats (con name, version, autoSelect y priority), protocolVersions (v1, v2) y containers, con args parametrizados por plantillas al estilo {{.Name}} que se sustituyen con metadatos del InferenceService.
La selección es lo que hace que esto escale. Si el InferenceService nombra un runtime, el controlador lo busca primero en el namespace y después a nivel de clúster; si no lo nombra, elige entre los que tienen autoSelect: true para ese modelFormat, desempatando por priority. Es lo que permite a una plataforma publicar un catálogo curado —“aquí, huggingface se sirve con este vLLM y estos flags”— sin que los equipos de producto tengan que saber qué es --gpu-memory-utilization. En multi-tenant, los ClusterServingRuntime son de la plataforma y los InferenceService de los equipos: la línea de separación de responsabilidades más limpia que ofrece KServe.
El storage initializer y storageUri
El campo storageUri dispara el storage initializer: un init container que descarga el artefacto a /mnt/models antes de que arranque el servidor. Esquemas soportados a julio de 2026: s3://, gs://, https://, pvc://, hf://, oci:// y repositorios git; las credenciales se adjuntan vía ServiceAccount con secretos anotados. Ese oci:// conecta con el segundo artículo de la serie: los pesos pueden vivir en el mismo registro que las imágenes, con el mismo control de acceso y la misma capacidad de firma.
Los modos de despliegue
Aquí se decide casi todo lo operativo. Se selecciona con la anotación serving.kserve.io/deploymentMode.
| KNative (antes Serverless) | Standard (antes RawDeployment) | ModelMesh | |
|---|---|---|---|
| Recursos emitidos | Knative Service + Revision, VirtualService con Istio | Deployment, Service, HPA, HTTPRoute de Gateway API | Runtime propio de alta densidad |
| Dependencias | Knative Serving + Istio (o gateway alternativo) | Kubernetes 1.32+, cert-manager 1.15.0+, Gateway API v1.2.1 y un controlador | Componentes propios |
| Autoescalado | KPA (concurrencia / RPS) | HPA o KEDA | — |
| Scale-to-zero | Sí, con activador que retiene la petición | No para peticiones HTTP | — |
canaryTrafficPercent | Sí, por pesos de revisión en Istio | Se ignora en silencio (issue #5335, abierta desde el 2 de abril de 2026) | — |
| Sidecars en el pod | queue-proxy (+ istio-proxy) | Ninguno | — |
| Estado a julio 2026 | Vigente | Vigente y el modo por defecto de la rama LLM | Archivado el 14 de abril de 2026 |
La lectura de esa tabla hay que decirla en voz alta: las dos capacidades más citadas de KServe —scale-to-zero y canary por porcentaje— viven en el modo que arrastra Knative e Istio, y el modo hacia el que ha girado el proyecto es el otro.
La rama LLM: LLMInferenceService
Desde la v0.16 KServe tiene una CRD distinta para LLM: LLMInferenceService, en serving.kserve.io/v1alpha1. No es una extensión del InferenceService clásico ni comparte versión de API, y confundirlas es el primer error de quien llega desde documentación antigua.
Está construida sobre llm-d, el proyecto que Red Hat, Google, IBM, CoreWeave y NVIDIA donaron a la CNCF. El reparto de papeles que declara el propio proyecto es limpio: KServe es el plano de control —ciclo de vida, escalado, gobierno operativo— y llm-d aporta el scheduling distribuido (utilización de GPU, profundidad de cola, residencia de caché, SLA). En la analogía: llm-d decide por qué vía va cada tren; KServe decide qué trenes existen y cuándo se retiran.
Los campos que importan: spec.model (URI, nombre de invocación, criticidad de scheduling y adaptadores LoRA, con reconciliación de adaptadores estáticos desde la v0.19, lo que conecta con multi-LoRA serving); spec.template (nodo único, o el pool de decode); spec.worker (multi-nodo, que dispara LeaderWorkerSet); spec.prefill (el desagregado que analizamos en disaggregated serving convertido en campo declarativo); spec.parallelism (tensor, data y expert parallelism); y spec.router (gateway, route, scheduler), donde KServe se apoya en la Gateway API Inference Extension —v1.5.0 del 19 de abril de 2026, ya en GA— con migración automática a InferencePool v1 desde la v0.19.
El endpoint que expone es compatible con OpenAI: /v1/chat/completions con streaming, y desde la v0.18 también /v1/responses. Subrayémoslo, porque es la clave de la sección siguiente: la rama LLM de KServe no habla V2. Habla OpenAI.
Para multi-nodo existe el runtime kserve-huggingfaceserver-multinode, basado en vLLM sobre Ray, con workerSpec.tensorParallelSize y workerSpec.pipelineParallelSize. Sus restricciones son duras: exige PVC ReadWriteMany, solo funciona en modo Standard, no admite autoescalado y requiere exactamente un pod head. Un nodo de referencia 4×H100 SXM con NVLink no necesita nada de esto —un 70B cabe con tensor parallel 4 en un solo nodo—; hace falta a partir de ahí.
El Open Inference Protocol: el contrato en sí
Las rutas y el payload
El Open Inference Protocol (históricamente “V2 Inference Protocol”) vive en kserve/open-inference-protocol, con especificación REST y gRPC y versionado bajo SemVer 2.0. Su superficie HTTP es deliberadamente pequeña:
| Recurso | Verbo | Ruta |
|---|---|---|
| Metadatos del servidor | GET | /v2 |
| Liveness | GET | /v2/health/live |
| Readiness del servidor | GET | /v2/health/ready |
| Metadatos del modelo | GET | /v2/models/<modelo>[/versions/<v>] |
| Readiness del modelo | GET | /v2/models/<modelo>[/versions/<v>]/ready |
| Inferencia | POST | /v2/models/<modelo>[/versions/<v>]/infer |
El payload es tensorial: id, una lista de inputs —cada uno con name, shape, datatype y data— y opcionalmente los outputs deseados. Los tipos son BOOL, enteros con y sin signo de 8 a 64 bits, coma flotante de 16, 32 y 64 bits, y BYTES. En gRPC el mismo contrato es GRPCInferenceService, con seis RPC: ServerLive, ServerReady, ModelReady, ServerMetadata, ModelMetadata y ModelInfer. Para embeddings a alto volumen, la diferencia de serialización no es cosmética.
Quién lo implementa de verdad
El repositorio de la especificación declara como adoptantes a KServe, NVIDIA Triton, Seldon MLServer y Core v2, OpenVINO Model Server, AMD Inference Server y TorchServe. No todas las entradas valen lo mismo a julio de 2026:
- Triton lo implementa de forma explícita y documentada —“Triton expone endpoints HTTP/REST y gRPC basados en los protocolos de inferencia estándar propuestos por el proyecto KServe”—, más extensiones propias. Es la implementación de referencia de facto.
- OpenVINO Model Server expone la API KServe en gRPC y REST, la de TensorFlow Serving y endpoints compatibles con OpenAI: el ejemplo más claro de servidor que habla los dos anchos de vía.
- TorchServe obliga a ser honesto. Su documentación oficial lleva el aviso: “This project is no longer actively maintained. While existing releases remain available, there are no planned updates, bug fixes, new features, or security patches.” Un adoptante en mantenimiento cero no es un adoptante: es una fila que nadie ha actualizado.
Sobre la salud del estándar, el dato más elocuente no aparece en ninguna nota de prensa: el repositorio de la especificación no tiene ni una sola release publicada, acumula del orden de treinta commits y setenta y cinco estrellas, y su gobernanza son reuniones mensuales y un canal de Slack. Existe además un fork “vendor neutral” en open-inference/open-inference-protocol con más commits que el original y ninguna tracción. Traducido: el V2 es un contrato estable, ampliamente implementado y que casi no evoluciona. Para un formato de serialización eso puede ser virtud; para un estándar que aspire a cubrir lo generativo, es una condena.
La extensión generate y por qué no ganó
El estándar sí intentó cubrir la generación: en la especificación hay un generate_rest.yaml con dos rutas —/v2/models/<m>/versions/<v>/generate y .../generate_stream— y un esquema minimalista: petición con text_input y parameters (temperature, top_p, max_tokens con valor por defecto 20, stop, details); respuesta con text_output, model_name, model_version y, si se piden, finish_reason y logprobs; streaming por server-sent events. Es limpio, basta para un completado simple y no lo usa nadie: la documentación de inferencia generativa de KServe, a julio de 2026, enumera exactamente cuatro endpoints, y ninguno es generate:
| Tarea | Endpoint en KServe |
|---|---|
| Generación de texto | openai/v1/completions |
| Chat | openai/v1/chat/completions |
| Embeddings | openai/v1/embeddings |
| Reranking | openai/v1/rerank |
El prefijo openai/ existe, según la propia documentación, “para evitar confusión con el protocolo de la API V1”, y se cambia con KSERVE_OPENAI_ROUTE_PREFIX. Es fontanería que dice mucho: el estándar propio quedó relegado a un prefijo defensivo mientras el ajeno se lleva el tráfico.
La tensión, sin adornos
El V2 resolvió el problema de 2021, y lo resolvió bien. Un contrato tensorial, tipado, con metadatos y healthchecks, en REST y gRPC, es lo que hace falta para servir un clasificador, un modelo de visión, un ranker o un embedding, y sigue siendo la respuesta correcta para esa mitad del catálogo: cambiar un servidor de embeddings de MLServer a OpenVINO Model Server sin tocar clientes es el ancho de vía funcionando.
Y el mundo LLM se estandarizó en otro sitio, no por comité, sino porque todo el ecosistema de clientes —SDK, frameworks de agentes, gateways, evaluación— se escribió contra la API de OpenAI, y cualquier motor que quisiera ser usado tuvo que hablarla. Qué significa para una arquitectura:
- No hay un contrato: hay dos, y ambos son legítimos. Diseñar como si hubiera uno violenta la mitad del catálogo.
- La portabilidad del LLM es superficial. Cubre bien chat y completado; en cuanto se usan logprobs detallados, guided decoding, cabeceras de prioridad, parámetros de prefix caching o tool calling con esquemas concretos, se sale del subconjunto común y se vuelve a atar al motor. Es la advertencia de structured output: lo portable es el sobre, no siempre el contenido.
- El cambiador de ancho es el gateway. Lo que hace conmutable el conjunto no es un estándar único, sino la capa de traducción y enrutado de elegir gateway OSS de inferencia LLM y el router como gateway L7.
- Al cruzar pierdes el versionado. El V2 tiene
/versions/<v>en la ruta; la API de OpenAI solo tiene el campomodel. La versión hay que codificarla en el nombre (asistente-70b-2026-07) o gestionarla en el plano de control. Es la diferencia entre poder auditar qué versión respondió a una petición y no poder.
Cuatro manifiestos que se pueden aplicar
Runtime de plataforma para el nodo de referencia del blog, 4×H100 SXM 80 GB con NVLink, con tensor parallel 4:
apiVersion: serving.kserve.io/v1alpha1
kind: ClusterServingRuntime
metadata:
name: vllm-h100-tp4
spec:
annotations:
prometheus.kserve.io/port: "8080"
prometheus.kserve.io/path: "/metrics"
supportedModelFormats:
- name: huggingface
version: "1"
autoSelect: true
priority: 10
protocolVersions:
- v2
containers:
- name: kserve-container
# fijar SIEMPRE por digest en produccion; el tag es solo legibilidad
image: vllm/vllm-openai:v0.11.0
command: ["python3", "-m", "vllm.entrypoints.openai.api_server"]
args:
- --port=8080
- --model=/mnt/models
- --served-model-name={{.Name}}
- --tensor-parallel-size=4
- --max-model-len=32768
- --gpu-memory-utilization=0.92
- --enable-prefix-caching
- --kv-cache-dtype=fp8
resources:
requests: { cpu: "16", memory: 128Gi, nvidia.com/gpu: "4" }
limits: { cpu: "16", memory: 128Gi, nvidia.com/gpu: "4" }
# la sonda que evita que el pod muera mientras carga el modelo
startupProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 15
failureThreshold: 40 # hasta 10 minutos de carga
readinessProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 10
livenessProbe:
httpGet: { path: /health, port: 8080 }
periodSeconds: 30
El InferenceService que lo consume, con KEDA sobre métricas de vLLM:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: asistente-70b
namespace: inferencia
annotations:
serving.kserve.io/deploymentMode: "Standard"
serving.kserve.io/autoscalerClass: "keda"
spec:
predictor:
minReplicas: 1
maxReplicas: 4
model:
runtime: vllm-h100-tp4
modelFormat: { name: huggingface }
# el registro OCI del articulo 2/4 de esta serie
storageUri: "oci://registry.example.internal/modelos/asistente-70b:2026-07"
autoScaling:
metrics:
- type: External
external:
metric:
backend: "prometheus"
serverAddress: "http://prometheus.monitoring.svc.cluster.local:9090"
query: 'sum(vllm:num_requests_waiting{model_name="asistente-70b"})'
target:
type: Value
value: "8"
El canary, con su letra pequeña:
apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
name: asistente-70b
namespace: inferencia
annotations:
# OJO: canaryTrafficPercent solo surte efecto en modo KNative.
# En modo Standard se ignora en silencio (issue kserve/kserve#5335, abierta).
serving.kserve.io/deploymentMode: "KNative"
spec:
predictor:
canaryTrafficPercent: 10
minReplicas: 1
maxReplicas: 4
model:
runtime: vllm-h100-tp4
modelFormat: { name: huggingface }
storageUri: "oci://registry.example.internal/modelos/asistente-70b:2026-08"
KServe mantiene en el estado tres referencias —LatestReadyRevision, LatestRolledoutRevision y PreviousRolledoutRevision—: promocionar es poner el porcentaje a 100, revertir es fijar el tráfico en la anterior, y una revisión que no pasa a ready recibe cero tráfico automáticamente.
Y las dos llamadas. Contra el contrato V2, para un clasificador servido con MLServer o Triton:
# metadatos: que modelo es, que entradas espera, que tipos
curl -s http://clasificador.inferencia.example/v2/models/clasificador-tickets | jq .
# readiness del modelo concreto, no del servidor
curl -s -o /dev/null -w '%{http_code}\n' \
http://clasificador.inferencia.example/v2/models/clasificador-tickets/ready
curl -s -X POST \
http://clasificador.inferencia.example/v2/models/clasificador-tickets/infer \
-H 'Content-Type: application/json' \
-d '{ "id": "req-4711",
"inputs": [ { "name": "input-0", "shape": [1, 4], "datatype": "FP32",
"data": [5.1, 3.5, 1.4, 0.2] } ],
"outputs": [ { "name": "output-0" } ] }'
Contra el mismo plano de control, pero en la superficie compatible con OpenAI del LLM:
curl -s -N -X POST \
http://asistente-70b.inferencia.example/openai/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{ "model": "asistente-70b",
"messages": [ {"role": "user",
"content": "Resume el parte de incidencias de anoche."} ],
"max_tokens": 256,
"stream": true }'
Dos contratos, dos formas de payload, el mismo InferenceService como unidad de gobierno. Esa es la propuesta de valor de un plano de control: el contrato lo elige el modelo; la gestión es única.
Autoescalado y el problema del cero
Tres autoescaladores conviven, y elegir mal es la causa más habitual de decepción. KPA, en modo KNative, escala por concurrencia o RPS y es el único con scale-to-zero de verdad, porque Knative aporta un activador que retiene la petición mientras el pod arranca; su métrica, eso sí, no distingue una petición de 30 tokens de una de 8.000. HPA, en Standard, escala por CPU o memoria: para LLM es inútil, porque la CPU de un pod de vLLM saturado y la de uno ocioso se parecen demasiado. KEDA, en Standard, es la opción correcta, y es lo que desarrollamos en autoscaling con KEDA: se activa con serving.kserve.io/autoscalerClass: "keda" y autoScaling.metrics acepta métricas externas contra Prometheus —vllm:num_requests_running o vllm:num_requests_waiting— o, con el add-on de OpenTelemetry, métricas empujadas. Desde la v0.18 hay además Workload Variant Autoscaling, que usa HPA o KEDA como backend.
Y ahora el problema serio, que ningún autoescalador resuelve: el cero. Un modelo de 70B en fp8 son del orden de 70 GB que mover hasta la HBM, y ese camino —del disco a la HBM— se mide en minutos: la primera petición tras la inactividad tarda eso, o falla.
Peor: la documentación de instalación es explícita a julio de 2026 —“Scale from Zero is currently not supported in Standard mode for HTTP requests”—. El modo hacia el que ha girado el proyecto, y el único que soporta KEDA, no sabe levantar desde cero ante una petición HTTP: KEDA puede bajar a cero (enfriamiento por defecto de 300 segundos), pero la vuelta requiere un activador que Standard no tiene. Quien quiera cero real con activación HTTP sigue necesitando Knative.
Las palancas, por orden de eficacia: no escalar a cero el modelo caliente (minReplicas: 1 para lo que tiene SLA; el cero es para la cola larga); atacar el tiempo de carga, la única solución que no es un parche, con las técnicas de acelerar el cold start; usar LocalModelCache —desde la v0.15, extendido a LLMInferenceService en la v0.19— para precargar artefactos en PVC de nodo; y poner sondas correctas, que es lo siguiente.
Trampas operativas
La sonda que mata el pod mientras carga el modelo. La más frecuente y la más cara de diagnosticar, porque el síntoma es un CrashLoopBackOff sin error en los logs del servidor. Hasta principios de 2026 las plantillas de vLLM de KServe usaban initialDelaySeconds de 120 a 300 segundos en liveness, con el resultado descrito en la issue #5062 (12 de febrero de 2026, ya resuelta): los modelos rápidos esperaban de más y los grandes morían antes de terminar de cargar. La solución aplicada: startupProbe de hasta diez minutos y sondas sin retardo inicial. Un runtime heredado hay que revisarlo a mano. En modo KNative hay una variante peor (issue #3795, cerrada como not planned): quien muere no es el contenedor del modelo sino el queue-proxy, que no responde OK dentro del plazo y se lleva por delante el pod entero.
canaryTrafficPercent ignorado en silencio. Merece trampa propia porque no hay error, ni evento, ni condición en el estado: en modo Standard el campo simplemente no hace nada. El equipo cree que sirve el 10 % con la versión nueva y está sirviendo el 100 %. Mientras la issue #5335 siga abierta, en Standard el canary se hace con pesos de HTTPRoute a mano, o en el gateway.
Canary y GPU escasa no se llevan bien. Un canary al 10 % de un 70B con tensor parallel 4 no cuesta el 10 % de los recursos: cuesta un nodo entero de 4 GPU, porque la granularidad mínima es una réplica completa. Durante la ventana hay dos versiones ocupando el doble de hardware, o hay que sacrificar capacidad de la estable. Es la diferencia entre canary de microservicio y canary de modelo, y la razón por la que a menudo compensa más un shadow sobre tráfico duplicado.
storageUri y credenciales. Los fallos del storage initializer son siempre los mismos: ServiceAccount sin el secreto anotado, bucket cifrado con KMS sobre un rol sin permiso de descifrado, endpoint S3 compatible pero no exactamente S3, o 403 intermitentes —la v0.18 trae corrección específica—. Se diagnostica en los logs del init container, no en los del servidor. Y una recomendación de serie: no descargues por https:// desde internet en producción; ese es justamente el problema del artículo 2/4.
El tamaño del modelo frente al emptyDir. Por defecto el artefacto aterriza en un volumen efímero sobre el disco del nodo. Setenta gigabytes por réplica llenan un disco de sistema con facilidad sorprendente, y entonces el kubelet desaloja pods por presión de disco, incluidos los que no tienen nada que ver. Hay que fijar sizeLimit o usar pvc:// dedicado. Para multi-nodo la restricción es más dura —PVC ReadWriteMany obligatorio—, lo que descarta buena parte del bloque local y empuja a sistemas de fichero compartidos, con las consecuencias de almacenamiento para IA.
La dependencia de Knative e Istio. El coste real del modo KNative: dos planos de control más que versionar y correlacionar con Kubernetes, y dos sidecars —queue-proxy e istio-proxy— en cada pod, con su CPU, su memoria y su salto de red por petición. En una plataforma que ya tiene malla es coste marginal; en un clúster dedicado a inferencia hay que justificarlo con las capacidades que se vayan a usar de verdad.
Observabilidad: el nombre de la métrica. El ServingRuntime declara anotaciones prometheus.kserve.io/port y /path, pero las métricas que salen son las del motor, no las de KServe: vllm:num_requests_waiting, vllm:time_to_first_token_seconds, vllm:gpu_cache_usage_perc. El plano de control no normaliza nombres entre runtimes, así que un dashboard escrito contra vLLM no vale para Triton, y la correlación con DCGM sigue siendo trabajo propio. La v0.19 eleva el estado del escalado HPA/KEDA a las condiciones del servicio y emite eventos en las transiciones de readiness: justo lo que hacía falta para alertar sobre el plano de control y no solo sobre el motor.
Mapa de decisión: cuándo KServe, y cuándo sobra
La comparación útil no es “KServe frente a OME, vLLM Production Stack, Dynamo o llm-d”. Esos cuatro los cubrimos en operators de inferencia LLM y compiten en otro eje:
| Eje | Qué se decide | Quién compite |
|---|---|---|
| Contrato y plano de control | Qué CRD describe un servicio, qué API ven los clientes, cómo se versiona el modelo, cómo se hace el rollout, cómo conviven LLM y no-LLM | KServe, Seldon Core v2 |
| Motor y topología | Qué engine ejecuta, cómo se desagrega prefill/decode, cómo se enruta con conocimiento de caché, cómo se hace tensor parallel multi-nodo | vLLM Production Stack, OME, NVIDIA Dynamo, llm-d |
Los ejes son ortogonales, y LLMInferenceService es la prueba: KServe no reimplementó el scheduling distribuido, se apoyó en llm-d. Quien elige KServe no elige en lugar de un operator de LLM: elige la capa de contrato sobre la que ese operator vive.
Elige KServe cuando el catálogo es heterogéneo —LLM más embeddings, rerankers, clasificadores y visión—, su ventaja decisiva y que ninguno de los cuatro operators tiene: un servicio como el de TEI en producción encaja en el mismo vocabulario que el LLM. También cuando necesitas un contrato desacoplado del motor porque tienes clientes que no controlas; cuando necesitas gobierno declarativo y auditable —qué artefacto, de qué origen, con qué runtime, en un objeto versionable en git y sujeto a políticas de admisión—, la pieza que hace posible el resto de esta serie; y cuando quieres un catálogo de runtimes de plataforma separado de los servicios de los equipos.
KServe sobra cuando sirves un modelo, en un nodo, con hasta tres réplicas: el Deployment de vLLM en Kubernetes es la respuesta correcta. Cuando tu carga es solo LLM y la prioridad es el rendimiento por GPU, porque ahí el valor está en el motor. Cuando no vas a usar ni scale-to-zero ni canary por revisión, porque lo que queda es un Deployment con más YAML. Y cuando ya tienes un operator de LLM y solo sirves LLM: montar KServe encima por completitud es sumar CRDs sin sumar decisiones.
Para una factoría de inferencia
Primera: separa el catálogo de runtimes del de servicios, y hazlo el día uno. Los ClusterServingRuntime son de la plataforma; los InferenceService, de los equipos. Esa frontera convierte “servir el modelo treinta” en un cambio de cinco líneas en lugar de una negociación sobre flags de vLLM, y encaja con GitOps con Flux: el runtime se revisa como código de plataforma, el servicio como configuración de producto.
Segunda: asume los dos contratos y pon el cambiador de ancho donde toca. V2 para lo tensorial, API de OpenAI para lo generativo, y la traducción en el gateway, no en cada cliente. Y como la API de OpenAI no versiona en la ruta, codifica la versión en el nombre del modelo y haz que ese nombre sea el mismo identificador que aparece en el registro, en la firma y en las trazas. Es lo que hace una petición rastreable hasta el artefacto.
Tercera: mide el arranque antes de prometer elasticidad. El tiempo desde que el pod se programa hasta que la sonda de readiness da verde, con el modelo real y desde el origen real, decide si el scale-to-zero es una palanca de FinOps o una máquina de incidentes. Ese número, y no el precio de la GPU-hora, es el que hay que llevar a la conversación sobre coste.
Y aquí esta pieza se queda corta a propósito. KServe da el contrato, el ciclo de vida y el rollout, pero todo su modelo de confianza cuelga de una cadena de texto: storageUri. El storage initializer descarga lo que haya en ese destino y lo pone delante de la GPU sin preguntar nada más; nada en el InferenceService garantiza que el artefacto sea el que se aprobó, que no haya cambiado desde la última auditoría, ni que el origen sea legítimo. El siguiente artículo va exactamente ahí: de dónde salen los bytes del modelo, y por qué un registro OCI con ORAS, Harbor, MLflow o el Model Registry de Kubeflow es la diferencia entre distribuir modelos y esparcirlos.
Ver también
- Operators de inferencia LLM en Kubernetes: OME, vLLM Production Stack, NVIDIA Dynamo y llm-d — el eje complementario: motor y topología frente al contrato que trata este artículo.
- vLLM en Kubernetes: la pieza de inferencia LLM que sí escala — el
Deploymentplano del que KServe es el paso siguiente, y el punto donde KServe todavía sobra. - Autoscaling de LLM en Kubernetes con KEDA — las métricas de vLLM que alimentan el bloque
autoScalingdel predictor. - Canary, blue-green y shadow para modelos LLM — qué hacer cuando
canaryTrafficPercentno está disponible, y por qué el canary de modelo no cuesta lo que parece. - Acelerar el cold start: carga de modelos con Tensorizer — la única forma real de que el scale-to-zero deje de ser una promesa.
- Cadena de confianza del modelo (2/4): registro y distribución de modelos con OCI y ORAS — a dónde apunta ese
storageUri, y cómo se convierte en un artefacto gobernado.
Fuentes
- CNCF, KServe becomes a CNCF incubating project (11 de noviembre de 2025) — https://www.cncf.io/blog/2025/11/11/kserve-becomes-a-cncf-incubating-project/
- CNCF, KServe project page (incubating, aceptado el 29 de septiembre de 2025) — https://www.cncf.io/projects/kserve/
- CNCF TOC, Project Moving Levels Checklist: KServe joining CNCF at Incubation level — https://github.com/cncf/toc/issues/1905
- KServe, Releases (v0.15.0, v0.16.0, v0.18.0, v0.19.0) — https://github.com/kserve/kserve/releases
- KServe, Inference Protocol V2 (Open Inference Protocol) — https://kserve.github.io/website/docs/concepts/architecture/data-plane/v2-protocol
- KServe, open-inference-protocol (especificación, adoptantes, gobernanza) — https://github.com/kserve/open-inference-protocol
- KServe, open-inference-protocol — generate_rest.yaml — https://github.com/kserve/open-inference-protocol/blob/main/specification/protocol/generate_rest.yaml
- open-inference, Vendor neutral fork of kserve/open-inference-protocol — https://github.com/open-inference/open-inference-protocol
- KServe, Generative Inference — Runtime Overview (endpoints con prefijo
openai/) — https://kserve.github.io/website/docs/model-serving/generative-inference/overview - KServe, Understanding LLMInferenceService — https://kserve.github.io/website/docs/model-serving/generative-inference/llmisvc/llmisvc-overview
- KServe, Cloud-Native AI Inference at Scale using KServe and llm-d (5 de marzo de 2026) — https://kserve.github.io/website/blog/cloud-native-ai-inference-kserve-llm-d
- KServe, Serving Runtime (ServingRuntime y ClusterServingRuntime) — https://kserve.github.io/website/docs/concepts/resources/servingruntime
- KServe, Kubernetes Deployment Installation Guide (K8s 1.32+, cert-manager 1.15.0+, Gateway API v1.2.1, sin scale-from-zero en Standard) — https://kserve.github.io/website/docs/admin-guide/kubernetes-deployment
- KServe, Canary Rollout Strategy — https://kserve.github.io/website/docs/model-serving/predictive-inference/rollout-strategies/canary
- KServe, Autoscaling with KEDA — https://kserve.github.io/website/docs/model-serving/predictive-inference/autoscaling/keda-autoscaler
- KServe, Multi-node/Multi-GPU Inference (PVC RWX, sin autoescalado) — https://kserve.github.io/website/docs/model-serving/generative-inference/multi-node
- KServe, Storage Options for Model Artifacts — https://kserve.github.io/website/docs/model-serving/storage/overview
- KServe, issue #5335 — Support canaryTrafficPercent for RawDeployment mode via Gateway API HTTPRoute weights (abierta, 2 de abril de 2026) — https://github.com/kserve/kserve/issues/5335
- KServe, issue #5062 — Add startupProbe to vLLM main containers (12 de febrero de 2026) — https://github.com/kserve/kserve/issues/5062
- KServe, modelmesh-serving (última release v0.12.0 de julio de 2023; repositorio archivado el 14 de abril de 2026) — https://github.com/kserve/modelmesh-serving
- Red Hat, Converting ModelMesh and Serverless InferenceServices to RawDeployment (Standard) Mode — https://access.redhat.com/articles/7134025
- NVIDIA, Triton Inference Server — Inference Protocols and APIs — https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/customization_guide/inference_protocols.html
- OpenVINO, What is OpenVINO Model Server (API KServe, TFS y endpoints compatibles con OpenAI) — https://docs.openvino.ai/2026/model-server/ovms_what_is_openvino_model_server.html
- PyTorch, TorchServe — Notice: Limited Maintenance — https://docs.pytorch.org/serve/
- Kubernetes SIG Network, Gateway API Inference Extension (v1.5.0, 19 de abril de 2026) — https://github.com/kubernetes-sigs/gateway-api-inference-extension