<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Canary on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/canary/</link><description>Recent content in Canary on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 01 Jun 2026 16:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/canary/index.xml" rel="self" type="application/rss+xml"/><item><title>Canary, blue-green y shadow para modelos LLM: cómo desplegar una versión nueva sin tirar el SLO</title><link>https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/</link><pubDate>Mon, 01 Jun 2026 16:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/</guid><description>&lt;blockquote>
&lt;p>Este post complementa los de &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a> (el autoscaler convive con el rollout y debe respetarlo), &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a> (las métricas que actúan como gate vienen de ahí), &lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs&lt;/a> (la eval que decide si el nuevo modelo está listo), &lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge&lt;/a> (la técnica que pone el &amp;ldquo;quality&amp;rdquo; en el gate de canary) y &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a> (el step previo del que sale el modelo nuevo).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>Promocionar una versión nueva de un modelo LLM al cluster productivo sin cortar tráfico ni romper SLO exige despliegue progresivo. Las tres estrategias canónicas —&lt;strong>blue-green&lt;/strong>, &lt;strong>canary&lt;/strong>, &lt;strong>shadow&lt;/strong>— responden a preguntas distintas y tienen costes distintos. &lt;strong>Blue-green&lt;/strong>: pool completo nuevo levantado en paralelo, conmutación atómica del load balancer. Rollback instantáneo (volver a apuntar al pool viejo); exige el doble de GPUs durante la ventana. &lt;strong>Canary&lt;/strong>: el tráfico se reparte progresivamente entre la versión vieja y la nueva (1 % → 5 % → 25 % → 100 %), midiendo en cada salto gates de regresión; consume incrementalmente menos hardware pero expone usuarios reales al modelo nuevo desde el primer porcentaje. &lt;strong>Shadow / mirror&lt;/strong>: el viejo modelo sirve el 100 % del tráfico real al cliente y, en paralelo, una copia de cada request va al nuevo modelo sin devolver su respuesta al usuario; aísla del riesgo de calidad pero gasta GPU del nuevo en respuestas que nadie consume, y no funciona bien con streaming SSE largo. La elección depende de tres factores: presupuesto GPU disponible, criticidad del servicio y disponibilidad de eval automática rápida. Las &lt;strong>cinco métricas de regresión&lt;/strong> que cualquier canary LLM gatear son: TTFT P95, error rate (HTTP 5xx + &lt;code>finish_reason=&amp;quot;length&amp;quot;&lt;/code> prematuro), quality score con LLM-as-judge sobre golden set, drift estadístico de embeddings de output (Wasserstein o KL contra distribución del baseline) y coste por request (tokens/s y kW/request). En Kubernetes, &lt;strong>Argo Rollouts&lt;/strong> gestiona el tráfico y los &lt;code>AnalysisTemplate&lt;/code> como gates automáticos; &lt;strong>Flagger&lt;/strong> es la alternativa más opinionada. vLLM v1 &lt;strong>no soporta hot model swap&lt;/strong> robusto a mayo 2026, así que la unidad de rollout es la &lt;strong>réplica entera&lt;/strong> (deployment v2 al lado de deployment v1). Los tres pitfalls específicos: sticky sessions del LB rompen la comparabilidad estadística del canary (un cliente A siempre cae al nuevo, B al viejo — las poblaciones no son equivalentes); eval semántica con LLM-as-judge tarda 2–8 segundos por sample y no sirve como gate en tiempo real (se usa en post-análisis o offline pre-promoción); el streaming SSE complica el shadow porque hay que descartar la respuesta del nuevo modelo sin afectar a la del viejo. Este post incluye un manifest Argo Rollouts mínimo aplicable a un cluster genérico con NVIDIA GPU Operator.&lt;/p>
&lt;h2 id="estás-aquí-deploy-y-la-transición-a-retrain">Estás aquí: DEPLOY (y la transición a RETRAIN)&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy con transición a Retrain">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.semiactive{fill:#cfead0;stroke-width:2}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#crm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#crm)}&lt;/style>
&lt;defs>&lt;marker id="crm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY · canary cierra el círculo iniciado en RETRAIN&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box semiactive"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;p>Un modelo nuevo no aparece por arte de magia en el cluster: viene del bucle de &lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">retrain&lt;/a> o de una actualización del proveedor de pesos. El paso entre &amp;ldquo;tengo un artefacto que pasó eval offline&amp;rdquo; y &amp;ldquo;está sirviendo el 100 % del tráfico&amp;rdquo; es exactamente este post.&lt;/p>
&lt;h2 id="la-analogía-el-estreno-de-una-obra-en-teatro">La analogía: el estreno de una obra en teatro&lt;/h2>
&lt;p>Una compañía de teatro va a estrenar una nueva versión de una obra que lleva un año en cartel con éxito. La compañía sabe varias cosas duras: el público actual paga por una experiencia consistente; un mal estreno daña el negocio durante meses; pero no estrenar nada deja a la compañía obsoleta frente a la competencia.&lt;/p>
&lt;p>Las tres rutas de estreno que la dirección puede elegir son las mismas tres del rollout LLM.&lt;/p>
&lt;p>&lt;strong>Ensayo general a puerta cerrada (shadow / mirror).&lt;/strong> Los actores nuevos representan la obra entera ante un teatro vacío. No hay público; nadie compra entrada. Tres pases enteros sirven para comprobar continuidad, tiempos y química del reparto. Es &lt;strong>caro&lt;/strong> porque hay sueldos y alquiler del teatro, pero &lt;strong>no expone al público al riesgo&lt;/strong>. Útil cuando el reparto nuevo está sin probar y el director quiere ver cómo aguanta una función completa antes de venderla. En LLM: el modelo nuevo procesa cada request real en paralelo al viejo pero sus respuestas se descartan; gastas GPU del nuevo en respuestas que nadie ve.&lt;/p>
&lt;p>&lt;strong>Reparto por funciones, alternando (canary).&lt;/strong> En lugar de cambiar todo el reparto de golpe, las funciones de jueves son del reparto nuevo, las del viernes del viejo, las de sábado mitad y mitad. La dirección lee los comentarios del libro de visitas y la afluencia de público función a función, decidiendo al cabo de dos semanas si promociona el reparto nuevo a titular o lo retira. &lt;strong>Más barato&lt;/strong> que el ensayo general porque las funciones venden entrada igual, pero &lt;strong>expone público real al riesgo&lt;/strong> desde el primer jueves. En LLM: el tráfico se reparte progresivamente entre la versión vieja y la nueva, midiendo gates en cada salto.&lt;/p>
&lt;p>&lt;strong>Doble compañía con cambio atómico (blue-green).&lt;/strong> La compañía contrata el reparto nuevo, lo prepara durante un mes a puerta cerrada, y un sábado anuncia: &amp;ldquo;a partir del próximo estreno todas las funciones son con el reparto nuevo&amp;rdquo;. Si la primera función va mal, se vuelve al reparto viejo en 24 horas — pero durante ese mes de preparación se paga &lt;strong>doble sueldo&lt;/strong> a las dos compañías. En LLM: dos pools completos del mismo tamaño, conmutación instantánea del LB de uno a otro, rollback en segundos si las métricas se rompen.&lt;/p>
&lt;p>La analogía sostiene también la decisión: la elección depende de &lt;strong>cuán crítica&lt;/strong> sea la obra para el negocio (criticidad del servicio LLM), &lt;strong>cuánto presupuesto&lt;/strong> hay para sostener dos repartos a la vez (presupuesto GPU), y &lt;strong>cuánta confianza&lt;/strong> se tiene en el nuevo reparto a partir de los ensayos de cámara (eval offline previa al canary).&lt;/p>
&lt;h2 id="las-tres-estrategias-en-detalle">Las tres estrategias en detalle&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="tres estrategias de rollout LLM comparadas">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.bg{fill:#d8eecf;stroke:#373}.cn{fill:#dfe9f5;stroke:#356}.sh{fill:#ead8f5;stroke:#634}.title{font:600 13px sans-serif;fill:#222}.h{font:700 12px sans-serif;fill:#222}.l{font:11px sans-serif;fill:#222}.n{font:italic 10px sans-serif;fill:#444}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Tres estrategias de rollout LLM con sus tradeoffs&lt;/text>
&lt;rect x="20" y="40" width="250" height="280" class="b bg"/>
&lt;text x="145" y="62" text-anchor="middle" class="h">BLUE-GREEN&lt;/text>
&lt;text x="30" y="90" class="l">Pool v1 (azul) sirve 100%&lt;/text>
&lt;text x="30" y="106" class="l">Pool v2 (verde) levantado en idle&lt;/text>
&lt;text x="30" y="122" class="l">Conmutación LB: v1 → v2 instantánea&lt;/text>
&lt;text x="30" y="138" class="l">Rollback: re-conmutar a v1&lt;/text>
&lt;text x="30" y="170" class="h">+ Rollback instantáneo&lt;/text>
&lt;text x="30" y="190" class="h">+ Test E2E en pool real&lt;/text>
&lt;text x="30" y="216" class="h">− Doble GPU durante ventana&lt;/text>
&lt;text x="30" y="236" class="h">− Switch grande = riesgo total&lt;/text>
&lt;text x="30" y="270" class="n">Caso típico: actualización menor&lt;/text>
&lt;text x="30" y="284" class="n">de proveedor (FP8 → FP4 nueva&lt;/text>
&lt;text x="30" y="298" class="n">versión del mismo modelo).&lt;/text>
&lt;rect x="285" y="40" width="250" height="280" class="b cn"/>
&lt;text x="410" y="62" text-anchor="middle" class="h">CANARY&lt;/text>
&lt;text x="295" y="90" class="l">v1 sirve mayoría · v2 fracción&lt;/text>
&lt;text x="295" y="106" class="l">Reparto progresivo: 1→5→25→100%&lt;/text>
&lt;text x="295" y="122" class="l">Gate de regresión entre saltos&lt;/text>
&lt;text x="295" y="138" class="l">Auto-rollback si gate falla&lt;/text>
&lt;text x="295" y="170" class="h">+ Exposición controlada&lt;/text>
&lt;text x="295" y="190" class="h">+ GPU incremental, no doble&lt;/text>
&lt;text x="295" y="216" class="h">− Usuarios reales en muestra&lt;/text>
&lt;text x="295" y="236" class="h">− Sticky sessions rompen muestreo&lt;/text>
&lt;text x="295" y="270" class="n">Caso típico: cambio de modelo&lt;/text>
&lt;text x="295" y="284" class="n">(Llama 70B → Llama 3.3 70B&lt;/text>
&lt;text x="295" y="298" class="n">fine-tuned por dominio).&lt;/text>
&lt;rect x="550" y="40" width="250" height="280" class="b sh"/>
&lt;text x="675" y="62" text-anchor="middle" class="h">SHADOW · MIRROR&lt;/text>
&lt;text x="560" y="90" class="l">v1 sirve 100% al usuario&lt;/text>
&lt;text x="560" y="106" class="l">v2 recibe copia de cada request&lt;/text>
&lt;text x="560" y="122" class="l">Respuesta v2 se descarta&lt;/text>
&lt;text x="560" y="138" class="l">Comparación offline v1 vs v2&lt;/text>
&lt;text x="560" y="170" class="h">+ Cero exposición al riesgo&lt;/text>
&lt;text x="560" y="190" class="h">+ Tráfico real en v2 sin daño&lt;/text>
&lt;text x="560" y="216" class="h">− GPU del v2 100% sin valor user&lt;/text>
&lt;text x="560" y="236" class="h">− Mal fit con streaming SSE largo&lt;/text>
&lt;text x="560" y="270" class="n">Caso típico: validación pre-canary&lt;/text>
&lt;text x="560" y="284" class="n">de modelo de arquitectura distinta&lt;/text>
&lt;text x="560" y="298" class="n">(denso → MoE).&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="blue-green">Blue-green&lt;/h3>
&lt;p>El operador mantiene dos pools de réplicas idénticos en tamaño: el &lt;strong>azul&lt;/strong> (versión productiva v1) y el &lt;strong>verde&lt;/strong> (versión candidata v2). Cuando v2 está validado offline (eval pasada, smoke tests), el switch del LoadBalancer redirige el 100 % del tráfico de azul a verde &lt;strong>en un solo paso&lt;/strong>. Si las métricas del SLO se rompen, el switch vuelve atrás en segundos.&lt;/p>
&lt;p>Coste: &lt;strong>2× GPUs&lt;/strong> durante toda la ventana (preparación de v2 + ventana de observación post-switch). Para un cluster de 16 GPUs sirviendo Llama 70B con TP=4 (4 réplicas), preparar el blue-green requiere 16 GPUs adicionales durante 1–3 días.&lt;/p>
&lt;p>Riesgo: el switch es &lt;strong>atómico&lt;/strong> — si v2 tiene un problema que no apareció en eval offline pero sí aparece a escala (por ejemplo, edge cases que solo se ven a 200 RPS), &lt;strong>el 100 % de usuarios lo nota a la vez&lt;/strong>. El rollback es instantáneo, pero las requests del primer minuto post-switch ya se vieron afectadas. Por tanto blue-green es preferible cuando se tiene &lt;strong>alta confianza&lt;/strong> en v2 (cambio menor: misma arquitectura, mismo formato, solo nueva versión de pesos) y se prioriza &lt;strong>rollback inmediato&lt;/strong> sobre exposición gradual.&lt;/p>
&lt;h3 id="canary">Canary&lt;/h3>
&lt;p>El operador despliega v2 con un número pequeño de réplicas (típicamente 1) junto al pool de v1. El LoadBalancer reparte progresivamente el tráfico siguiendo un cronograma: 1 % durante 30 minutos → 5 % durante 1 hora → 25 % durante 2 horas → 50 % durante 4 horas → 100 %. Entre cada salto, un &lt;strong>gate de análisis&lt;/strong> evalúa métricas de regresión sobre el tráfico que ya está cayendo en v2. Si el gate falla, el rollback retira el tráfico de v2 automáticamente y deja v1 sirviendo todo.&lt;/p>
&lt;p>Coste: &lt;strong>incremental&lt;/strong>. Al inicio (1 % de tráfico) basta una réplica v2; al 50 % se necesita la mitad de réplicas v2 que el total de v1. Pico de GPU adicional durante el canary: ~30–50 % por encima del baseline.&lt;/p>
&lt;p>Riesgo: usuarios reales &lt;strong>están viendo v2&lt;/strong> desde el primer 1 %. Si v2 produce respuestas con calidad degradada pero TTFT y error rate normales, los usuarios afectados perciben la degradación sin que el gate la detecte (a menos que el gate incluya quality drift, que tarda). Por tanto canary es preferible cuando se tiene &lt;strong>confianza media&lt;/strong> en v2 (cambio significativo: arquitectura o entrenamiento distinto) y se acepta que un % bajo de usuarios sea conejillo.&lt;/p>
&lt;h3 id="shadow--mirror">Shadow / mirror&lt;/h3>
&lt;p>El LoadBalancer envía el 100 % del tráfico real a v1 (que responde al cliente) &lt;strong>y duplica cada request&lt;/strong> hacia v2 (cuya respuesta se descarta o se guarda para análisis offline). El cliente nunca ve v2; nunca está expuesto al riesgo.&lt;/p>
&lt;p>Coste: &lt;strong>100 % adicional del compute de v2&lt;/strong> sin valor de usuario directo durante toda la ventana de shadow. Para un cluster de 16 GPUs sirviendo Llama 70B con TP=4 (4 réplicas), un shadow del mismo tamaño consume 16 GPUs adicionales &lt;strong>a tiempo completo&lt;/strong>.&lt;/p>
&lt;p>Riesgo: el shadow es &lt;strong>el más seguro&lt;/strong> para el usuario. Pero tiene dos limitaciones serias: (a) si v2 tiene un cuello de botella que causa que la copia de request al shadow tarde mucho, el proxy de shadowing puede consumir conexiones del LB; debe estar &lt;strong>out-of-band&lt;/strong> (asíncrono); (b) el &lt;strong>streaming SSE largo&lt;/strong> complica la mirroring porque hay que mantener dos streams paralelos y descartar uno mientras el otro fluye al cliente. Patrón habitual: shadow solo de requests no-streaming (completiones cortas, classification), eval offline manual de las requests con streaming.&lt;/p>
&lt;h2 id="las-cinco-métricas-de-regresión-que-actúan-como-gate">Las cinco métricas de regresión que actúan como gate&lt;/h2>
&lt;p>Sin gates automáticos, el &amp;ldquo;canary&amp;rdquo; es solo un nombre bonito para &amp;ldquo;rollout manual con un porcentaje variable&amp;rdquo;. Los gates son la pieza que convierte el canary en una operación defendible.&lt;/p>
&lt;p>&lt;strong>Métrica 1 — TTFT P95.&lt;/strong> Comparación P95 del nuevo modelo contra P95 del baseline (v1) en ventanas de 5 minutos. Gate: &lt;code>ttft_p95(v2) / ttft_p95(v1) &amp;lt; 1.10&lt;/code>. Detecta regresiones de latencia de prefill (modelo nuevo más lento) o problemas de motor (config subóptima). Fuente: &lt;code>vllm:time_to_first_token_seconds_bucket&lt;/code> —ver &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a>—.&lt;/p>
&lt;p>&lt;strong>Métrica 2 — Error rate.&lt;/strong> Suma de HTTP 5xx + 4xx no esperados + tasa de &lt;code>finish_reason=&amp;quot;length&amp;quot;&lt;/code> prematuro (respuestas cortadas porque el modelo nuevo no genera EOS). Gate: &lt;code>error_rate(v2) - error_rate(v1) &amp;lt; 0.01&lt;/code> (1 punto porcentual). Detecta crashes del motor, tokenizer roto, problemas de generación. Fuente: &lt;code>vllm:request_success_total{status=...}&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Métrica 3 — Quality score (LLM-as-judge).&lt;/strong> Sobre un &lt;strong>golden set&lt;/strong> de 200–1 000 prompts representativos, se ejecutan v1 y v2 offline y un modelo juez (típicamente más grande: GPT-4 class, Claude, Llama 405B local) puntúa cada par. Gate típico: &lt;code>mean_score(v2) &amp;gt;= mean_score(v1) - 0.05&lt;/code>. Esta métrica &lt;strong>no se mide en tiempo real durante el canary&lt;/strong> — la inferencia del juez tarda 2–8 segundos por sample y no escala como gate inline. Se usa como &lt;strong>gate offline pre-promoción&lt;/strong> (antes de empezar el canary) y como &lt;strong>post-mortem&lt;/strong> sobre muestra de tráfico real capturado durante el canary. Ver &lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge&lt;/a> para la mecánica.&lt;/p>
&lt;p>&lt;strong>Métrica 4 — Drift estadístico de output.&lt;/strong> Para cada request que cae en v2 durante el canary, embeber la respuesta con un modelo de embedding ligero (e5, BGE) y comparar la distribución de embeddings de v2 contra la distribución del baseline v1 sobre la misma ventana. Métricas usables: &lt;strong>Wasserstein distance&lt;/strong>, divergencia KL, o más simple, comparar medias y varianzas por dimensión. Gate: distancia normalizada &amp;lt; umbral calibrado (típicamente Wasserstein &amp;lt; 0.15). Detecta cambios sutiles en estilo, longitud, vocabulario que LLM-as-judge no captura sin pasar también por él. Es &lt;strong>rápida&lt;/strong>: el embedding ligero tarda ~50 ms por respuesta.&lt;/p>
&lt;p>&lt;strong>Métrica 5 — Coste por request.&lt;/strong> Tokens out / request y kW / request. Gate: &lt;code>cost_per_request(v2) / cost_per_request(v1) &amp;lt; 1.20&lt;/code>. Detecta modelos nuevos que generan respuestas significativamente más largas o que consumen más energía por la misma carga (degradación de quantization, fallo de optimizations). Sin este gate, una &amp;ldquo;actualización&amp;rdquo; puede duplicar la factura silenciosamente.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Tipo&lt;/th>
&lt;th>Latencia de medida&lt;/th>
&lt;th>Gate típico&lt;/th>
&lt;th>Detección&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT P95&lt;/td>
&lt;td>Cuantitativa&lt;/td>
&lt;td>5 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 110% baseline&lt;/code>&lt;/td>
&lt;td>Regresión de latencia&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Error rate&lt;/td>
&lt;td>Cuantitativa&lt;/td>
&lt;td>1 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 1pp sobre baseline&lt;/code>&lt;/td>
&lt;td>Crashes, generation broken&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Quality (LLM-judge)&lt;/td>
&lt;td>Semántica offline&lt;/td>
&lt;td>horas, sobre golden&lt;/td>
&lt;td>&lt;code>&amp;gt; baseline − 0.05&lt;/code>&lt;/td>
&lt;td>Calidad funcional&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Drift estadístico&lt;/td>
&lt;td>Estadística&lt;/td>
&lt;td>~5 min&lt;/td>
&lt;td>Wasserstein &amp;lt; 0.15&lt;/td>
&lt;td>Estilo, longitud, vocabulario&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste por request&lt;/td>
&lt;td>Cuantitativa&lt;/td>
&lt;td>5 min&lt;/td>
&lt;td>&lt;code>&amp;lt; 120% baseline&lt;/code>&lt;/td>
&lt;td>Eficiencia económica/energética&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="la-mecánica-en-kubernetes-argo-rollouts">La mecánica en Kubernetes: Argo Rollouts&lt;/h2>
&lt;p>Argo Rollouts extiende el &lt;code>Deployment&lt;/code> estándar de Kubernetes con un nuevo recurso &lt;code>Rollout&lt;/code> que orquesta la progresión del tráfico y los análisis automáticos. Se integra con cualquier service mesh (Istio, Linkerd) o controlador de ingress que soporte traffic splitting (NGINX, Traefik, Gateway API).&lt;/p>
&lt;p>Ejemplo mínimo de canary 1 → 5 → 25 → 100 % con gates de TTFT y error rate:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-yaml" data-lang="yaml">&lt;span class="line">&lt;span class="cl">&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Rollout&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">replicas&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">4&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">strategy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">canary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">canaryService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-canary&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stableService&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-stable&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">trafficRouting&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">nginx&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">stableIngress&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b-ingress&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">steps&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">30m }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">25&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }, { templateName: drift-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">50&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">pause&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">duration&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">4h }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">analysis&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templates&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">templateName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }, { templateName: drift-gate }] }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">setWeight&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">100&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">selector&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">matchLabels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">template&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">labels&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">app&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-llama70b } }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">containers&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">image&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm/vllm-openai:v0.10.0&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">args&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="w"> &lt;/span>--&lt;span class="l">model=/models/llama-70b-fp8-v2 ] &lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># versión nueva&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nn">---&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">apiVersion&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">argoproj.io/v1alpha1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">kind&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">AnalysisTemplate&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">metadata&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>{&lt;span class="w"> &lt;/span>&lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-error-gate }&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="nt">spec&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">metrics&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">ttft-p95-ratio&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">count&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">successCondition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">result &amp;lt; 1.10&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.observability.svc:9090&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket{version=&amp;#34;v2&amp;#34;}[5m])))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> /
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket{version=&amp;#34;v1&amp;#34;}[5m])))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">error-rate-diff&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">interval&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">1m&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">count&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">5&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">failureLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">successCondition&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">result &amp;lt; 0.01&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">provider&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">prometheus&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">address&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://prometheus.observability.svc:9090&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">query&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">|&lt;/span>&lt;span class="sd">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(rate(vllm:request_total{version=&amp;#34;v2&amp;#34;,status=~&amp;#34;5..&amp;#34;}[5m])) / sum(rate(vllm:request_total{version=&amp;#34;v2&amp;#34;}[5m]))
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> -
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="sd"> sum(rate(vllm:request_total{version=&amp;#34;v1&amp;#34;,status=~&amp;#34;5..&amp;#34;}[5m])) / sum(rate(vllm:request_total{version=&amp;#34;v1&amp;#34;}[5m]))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Si cualquiera de los &lt;code>AnalysisTemplate&lt;/code> falla, Argo Rollouts retrocede automáticamente: pone weight=0 en el canary, alerta al operador, mantiene v1 sirviendo el 100 %. La operación humana se reduce a investigar el fallo y decidir si re-lanzar o abortar.&lt;/p>
&lt;p>&lt;strong>Flagger&lt;/strong> ofrece una alternativa más opinionada: la progresión del weight es automática en función del éxito de las métricas en vez de pausa fija; el operador define un objetivo (&lt;code>maxWeight: 100&lt;/code>, &lt;code>stepWeight: 10&lt;/code>, &lt;code>metrics: [...]&lt;/code>) y Flagger sube o baja según comportamiento. Ambas son maduras en mayo 2026; la elección suele venir dictada por qué service mesh ya está en el cluster.&lt;/p>
&lt;h2 id="el-detalle-de-vllm-por-qué-no-se-hace-hot-swap-del-modelo">El detalle de vLLM: por qué no se hace &amp;ldquo;hot swap&amp;rdquo; del modelo&lt;/h2>
&lt;p>A mayo 2026, vLLM v1 &lt;strong>no soporta cambio caliente&lt;/strong> del modelo dentro de la misma réplica sin reiniciar el motor. El comando &lt;code>--model&lt;/code> se evalúa al arranque; cambiarlo requiere re-instanciar el &lt;code>LLMEngine&lt;/code>, lo que reinicia conexiones y descarta el KV cache. Por tanto la &lt;strong>unidad de rollout es la réplica entera&lt;/strong>: no se hace &amp;ldquo;v1 carga el modelo nuevo en una de sus GPUs&amp;rdquo; sino &amp;ldquo;se levanta una réplica v2 al lado de una réplica v1 y se reparte tráfico vía LB&amp;rdquo;.&lt;/p>
&lt;p>TensorRT-LLM con Triton tiene un mecanismo similar: cambiar el modelo exige reload del backend Triton. SGLang tampoco soporta hot swap robusto. La consecuencia operativa: el rollout LLM siempre va a costar GPUs adicionales durante la ventana, y la elección entre blue-green, canary y shadow es exactamente la pregunta de &lt;strong>cuántas adicionales&lt;/strong> y &lt;strong>cuánto tiempo&lt;/strong>.&lt;/p>
&lt;h2 id="los-tres-pitfalls-específicos-del-rollout-llm">Los tres pitfalls específicos del rollout LLM&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — sticky sessions rompen la comparabilidad del canary.&lt;/strong> Si el LoadBalancer hace session affinity por IP del cliente (común en NGINX, Traefik con &lt;code>loadbalancer.kubernetes.io/session-affinity: ClientIP&lt;/code>), un usuario A siempre cae en v2 mientras B siempre cae en v1. Las distribuciones de carga, perfiles de prompt y comportamiento de cliente &lt;strong>no son aleatorias&lt;/strong> entre los dos pools, lo que invalida estadísticamente cualquier comparación de gates. Solución: para canary, desactivar session affinity (&lt;code>sessionAffinity: None&lt;/code>) o usar affinity por request-id aleatorio. Si la app cliente exige sticky por funcionalidad (memoria conversacional persistida en cache), el canary no es la estrategia adecuada — usar blue-green o shadow.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — LLM-as-judge no es gate inline en tiempo real.&lt;/strong> La tentación de usar quality score como gate live es alta, pero la latencia del juez (2–8 s por sample) hace inviable evaluar más que un sampling del 1–2 % del tráfico, y los resultados llegan con minutos de retraso. Soluciones operativas: (a) &lt;strong>eval offline pre-canary&lt;/strong> sobre golden set como pre-requisito para arrancar (si falla, ni se inicia el canary); (b) durante el canary, &lt;strong>capturar requests + responses&lt;/strong> de v2 a tiempo real y correr el juez &lt;strong>asíncrono&lt;/strong> en un job batch que termina antes del siguiente salto; (c) usar drift estadístico de embeddings como &lt;strong>proxy rápido&lt;/strong> de calidad inline, y reservar el juez para gates intermedios entre saltos.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — streaming SSE complica el shadow.&lt;/strong> El mirror de tráfico clásico (NGINX &lt;code>mirror&lt;/code>, Istio &lt;code>MirrorPolicy&lt;/code>) está pensado para HTTP de request/response — copia la request, deja al servidor primario responder al cliente, y duplica la request al secundario descartando la respuesta. Con SSE, la respuesta del secundario es un &lt;strong>stream continuo de varios segundos&lt;/strong>, y mantener dos streams en paralelo carga doblemente al proxy. Soluciones: (a) shadow &lt;strong>solo de requests no-streaming&lt;/strong> (chat sin stream, embeddings, classification, batch eval), (b) shadow del tráfico streaming pero con &lt;strong>timeout corto&lt;/strong> en el secundario (descartar el shadow si tarda más de 30 s), (c) reemplazar el shadow por canary con weight pequeño (1 %) que sí soporta streaming bien.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;p>Para un cluster genérico de &lt;strong>4 nodos × 4×H100 SXM 80 GB = 16 GPUs&lt;/strong>, sirviendo Llama 70B FP8 con TP=4 (4 réplicas posibles, una por nodo):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Blue-green&lt;/strong>: imposible mantener dos pools completos de 4 réplicas sin GPUs adicionales. Solución práctica: blue-green con pools reducidos (2 réplicas v1 + 2 réplicas v2) durante la ventana, &lt;strong>degradación de capacidad aceptada&lt;/strong> (mitad del SLO de RPS sostenido), o disponer de un cluster paralelo (otro nodo) reservado para rollouts.&lt;/li>
&lt;li>&lt;strong>Canary&lt;/strong>: factible. Empezar con 3 réplicas v1 + 1 réplica v2 (25 % weight nominal pero también peso variable de tráfico). Avanzar a 2 v1 + 2 v2 al 50 %, luego 1 v1 + 3 v2, finalmente 0 v1 + 4 v2.&lt;/li>
&lt;li>&lt;strong>Shadow&lt;/strong>: complicado por el coste de GPU. Reservar para validación pre-canary de cambios mayores, durante una ventana corta (4–8 horas) con tráfico shadowed limitado a una muestra (10–20 % de requests, no 100 %).&lt;/li>
&lt;/ul>
&lt;p>Para clusters de &lt;strong>8 nodos GPU&lt;/strong>, los tres patrones son sostenibles. La regla operativa: el presupuesto de rollout es típicamente el &lt;strong>25–30 % de la capacidad sostenida&lt;/strong> del cluster — comprar para el pico + ese head-room cuadra los números del &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a>.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Rollouts multi-region&lt;/strong>: cómo coordinar canary cuando el cluster está distribuido geográficamente.&lt;/li>
&lt;li>&lt;strong>A/B testing de prompts&lt;/strong> (no de modelos): el mismo modelo con dos system prompts distintos, medir conversion.&lt;/li>
&lt;li>&lt;strong>Rollback de embeddings&lt;/strong>: cambiar el modelo de embeddings de un sistema RAG implica re-embedir todo el corpus — la mecánica de canary es distinta. Ver &lt;a href="https://blog.lo0.es/posts/rag-corpus-curation-fundamentos/">RAG corpus curation&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Feature flags para LLM&lt;/strong>: granularidad por tenant o por feature dentro del mismo modelo.&lt;/li>
&lt;li>&lt;strong>Continuous deployment&lt;/strong> end-to-end: integración con el retrain pipeline para que un nuevo adapter se promocione automáticamente tras pasar evals.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a> — el autoscaler convive con el canary y debe respetar las particiones de tráfico.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a> — las métricas que actúan como gate vienen de aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning&lt;/a> — define el head-room necesario para rollouts.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs&lt;/a> — la eval offline que valida v2 antes de empezar el canary.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge&lt;/a> — la técnica de quality score como gate offline.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a> — de donde sale el modelo nuevo que entra al canary.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">Cinco niveles de madurez&lt;/a> — Argo Rollouts es pieza del nivel 4–5.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/router-inferencia-llm-gateway-l7/">El router de inferencia LLM&lt;/a> — la pieza que en este post llamamos &amp;ldquo;LoadBalancer&amp;rdquo; desmontada como capa de pleno derecho: catálogo de modelos, traffic splitting L7, política transversal, failover y prefix-aware routing. El reparto 1 % → 5 % → 25 % → 100 % se materializa allí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/runbooks-incident-response-llm-keep-kafka/">Runbooks de incident response para LLM con Keep + Kafka&lt;/a> — el rollback automático del canary cuando &lt;code>ttft_p95(v2)/ttft_p95(v1) &amp;gt; 1.30&lt;/code> es el runbook RB-06; allí está el workflow Keep YAML completo y el encaje en compliance.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Argo Rollouts project — &lt;code>argoproj.io/argo-rollouts&lt;/code> (CRD &lt;code>Rollout&lt;/code> y &lt;code>AnalysisTemplate&lt;/code>).&lt;/li>
&lt;li>Flagger project — &lt;code>fluxcd.io/flagger&lt;/code> (alternativa con progresión automática).&lt;/li>
&lt;li>Istio — &lt;em>Traffic Mirroring&lt;/em> (mirror configurable a nivel &lt;code>VirtualService&lt;/code>).&lt;/li>
&lt;li>NGINX Ingress — &lt;em>Canary annotations&lt;/em> (&lt;code>nginx.ingress.kubernetes.io/canary-*&lt;/code>).&lt;/li>
&lt;li>vLLM project — issue tracker sobre hot model swap (estado a mayo 2026: en diseño, no production-ready).&lt;/li>
&lt;li>Hou et al. — &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized LLM Serving&lt;/em> (OSDI 2024) — referencia sobre métricas de goodput aplicables a gates de canary.&lt;/li>
&lt;li>Bürkner et al. — &lt;em>Statistical methods for detecting model drift in production&lt;/em> (artículos varios sobre Wasserstein y KL en monitoring ML).&lt;/li>
&lt;/ul></description></item></channel></rss>