<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Slo on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/slo/</link><description>Recent content in Slo on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Mon, 01 Jun 2026 15:30:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/slo/index.xml" rel="self" type="application/rss+xml"/><item><title>Observabilidad GPU para inferencia LLM: las doce métricas DCGM y vLLM que dictan la salud de tu producción</title><link>https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/</link><pubDate>Mon, 01 Jun 2026 15:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/</guid><description>&lt;blockquote>
&lt;p>Este post complementa los de &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> (la capa de tracing por encima de las métricas), &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning&lt;/a> (qué se dimensionó y qué se debe vigilar) y &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> (el mecanismo que explica varias de las métricas del motor).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La observabilidad de un cluster de inferencia LLM se construye sobre &lt;strong>dos fuentes complementarias&lt;/strong>: las métricas del hardware GPU expuestas por &lt;strong>DCGM (Data Center GPU Manager) Exporter&lt;/strong> —parte del NVIDIA GPU Operator— y las métricas del &lt;strong>motor de inferencia&lt;/strong> (vLLM, SGLang, TensorRT-LLM) expuestas en &lt;code>/metrics&lt;/code> Prometheus-compatibles. Ninguna de las dos basta sola. La métrica clásica de &lt;code>nvidia-smi&lt;/code> llamada &lt;em>GPU utilization&lt;/em> es engañosa para LLMs: marca alto cuando hay &lt;strong>cualquier kernel&lt;/strong> ejecutándose, sin distinguir tensor cores ardiendo de SMs esperando por HBM. La cabina de pilotaje completa tiene &lt;strong>doce métricas DCGM en cuatro familias&lt;/strong> (compute: &lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>, &lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>, &lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>; memoria: &lt;code>DCGM_FI_DEV_FB_USED&lt;/code>, &lt;code>DCGM_FI_DEV_FB_FREE&lt;/code>, &lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>; térmico-energético: &lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>, &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>, &lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>; salud: &lt;code>DCGM_FI_DEV_XID_ERRORS&lt;/code>, &lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>, &lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>) y &lt;strong>cinco métricas del motor vLLM&lt;/strong> (&lt;code>vllm:num_requests_running&lt;/code>, &lt;code>vllm:num_requests_waiting&lt;/code>, &lt;code>vllm:gpu_cache_usage_perc&lt;/code>, &lt;code>vllm:time_to_first_token_seconds&lt;/code>, &lt;code>vllm:time_per_output_token_seconds&lt;/code>). Cada una tiene un umbral verde/ámbar/rojo defendible, una PromQL para alerta, y al menos una falsa lectura habitual que confunde al operador junior. Las &lt;strong>seis alertas críticas&lt;/strong> que cualquier cluster productivo debe disparar son: HBM &amp;gt; 92 %, throttle por térmico o por power, XID error, ECC double-bit, KV cache pool &amp;gt; 95 %, y TTFT P95 fuera de SLO durante 5 minutos. El objetivo de tener este panel: que el operador de turno diagnostique el origen de una degradación en &lt;strong>menos de cinco minutos&lt;/strong>, sin abrir consola SSH a las GPUs. Cuando esto se cumple, el cluster ha pasado a operación profesional; mientras no, se opera por intuición.&lt;/p>
&lt;h2 id="estás-aquí-observe-la-otra-mitad-del-tracing">Estás aquí: OBSERVE (la otra mitad del tracing)&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í: Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#c9a8e9;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#obm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#obm)}&lt;/style>
&lt;defs>&lt;marker id="obm" 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í: OBSERVE · métricas (DCGM + motor) complementan al tracing&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 idle"/>&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 active"/>&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 idle"/>&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>El tracing —ya cubierto en &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a>— responde &lt;em>qué pasó en esta request concreta&lt;/em>. Las métricas responden &lt;em>qué está pasando en el cluster en agregado&lt;/em>. Son complementarias: una alerta del lado de métricas te dice &amp;ldquo;el clúster está degradando&amp;rdquo;, el tracing te dice &amp;ldquo;y esta es la traza concreta que te lo demuestra&amp;rdquo;. Un cluster sin tracing pero con métricas opera; un cluster sin métricas pero con tracing &lt;strong>no opera, debuggea&lt;/strong>.&lt;/p>
&lt;h2 id="la-analogía-la-cabina-de-un-avión-moderno">La analogía: la cabina de un avión moderno&lt;/h2>
&lt;p>En un avión comercial moderno, el panel de instrumentos del piloto tiene más de 70 indicadores activos. Si solo hubiese uno —el altímetro, por ejemplo— el avión volaría hacia el suelo en el primer momento de baja visibilidad. Hace falta el altímetro &lt;strong>y&lt;/strong> el indicador de actitud, &lt;strong>y&lt;/strong> el de velocidad, &lt;strong>y&lt;/strong> el de viraje, &lt;strong>y&lt;/strong> el de combustible, &lt;strong>y&lt;/strong> los de presión de aceite de cada motor, &lt;strong>y&lt;/strong> las temperaturas de salida de turbina. Cada uno responde una pregunta distinta. Y todos juntos cubren la pregunta operacional: &lt;em>¿está el avión sano, está donde debe, y va donde queremos?&lt;/em>&lt;/p>
&lt;p>La observabilidad de un cluster de inferencia LLM funciona igual. Una sola métrica —&amp;ldquo;GPU utilization 99 %&amp;quot;— no responde nada útil. Es como mirar solo el cuentakilómetros del coche para diagnosticar por qué hace ruido el motor. La cabina completa es &lt;strong>doce instrumentos del lado de hardware más cinco del lado del motor de inferencia&lt;/strong>, organizados en familias que responden preguntas distintas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Compute y eficiencia&lt;/strong>: &lt;em>¿están los tensor cores haciendo el trabajo que esperamos o están esperando?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Memoria&lt;/strong>: &lt;em>¿queda VRAM para nuevas requests o estamos al borde del OOM?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Térmico y energético&lt;/strong>: &lt;em>¿el hardware está sano o está limitando el throughput silenciosamente?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Salud y errores&lt;/strong>: &lt;em>¿hay degradación del hardware en curso (ECC, XID, NVLink)?&lt;/em>&lt;/li>
&lt;li>&lt;strong>Motor de inferencia&lt;/strong>: &lt;em>¿la cola crece, el KV pool está saturado, el SLO se está cumpliendo?&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Las cuatro primeras responden a &amp;ldquo;¿la GPU está bien?&amp;rdquo;. La quinta responde a &amp;ldquo;¿está dando el servicio que prometimos?&amp;rdquo;. Las dos preguntas son distintas y ambas deben tener respuesta a un golpe de vista.&lt;/p>
&lt;h2 id="por-qué-nvidia-smi-gpu-util-engaña-en-llms">Por qué &lt;code>nvidia-smi&lt;/code> &lt;code>GPU-Util&lt;/code> engaña en LLMs&lt;/h2>
&lt;p>La métrica clásica que aparece en &lt;code>nvidia-smi&lt;/code> como &lt;code>GPU-Util&lt;/code> corresponde a &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code>. Su definición oficial: &amp;ldquo;porcentaje del tiempo durante el cual uno o más kernels estuvieron ejecutándose en la GPU&amp;rdquo;. El problema en LLMs: la fase de decode es &lt;strong>memory-bound&lt;/strong>, no compute-bound. Cuando el motor de inferencia hace decode token a token, la GPU pasa el 90 % del tiempo esperando que la HBM termine de entregar los pesos del modelo y el KV cache. Hay un kernel corriendo (lectura de HBM); por tanto &lt;code>GPU-Util&lt;/code> reporta valores cercanos al 100 %. Pero los tensor cores están parados — el cuello de botella es la memoria, no el compute.&lt;/p>
&lt;p>Resultado práctico: el operador ve &amp;ldquo;GPU-Util 99 %&amp;rdquo; en Grafana y asume &amp;ldquo;GPU saturada, no se puede meter más carga&amp;rdquo;. Pero la realidad puede ser &amp;ldquo;compute al 25 %, HBM saturada al 95 %&amp;rdquo;, lo que cambia las decisiones operativas (quantization, batch size, paralelismo). La métrica clásica miente por simplificación.&lt;/p>
&lt;p>Lo correcto es mirar las &lt;strong>tres métricas de profiling DCGM&lt;/strong> del subsistema &lt;code>_FI_PROF_*&lt;/code>:&lt;/p>
&lt;ul>
&lt;li>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code> — ratio de warps activos sobre máximos por SM. &lt;em>¿Hay trabajo paralelo?&lt;/em>&lt;/li>
&lt;li>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code> — % de ciclos con tensor cores efectivamente activos. &lt;em>¿Está el compute trabajando?&lt;/em>&lt;/li>
&lt;li>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code> — % de ciclos con la HBM transfiriendo. &lt;em>¿Está la memoria saturada?&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>Una decode-bound GPU típica de Llama 70B en H100 muestra: SM occupancy 35–55 %, tensor active 15–30 %, DRAM active 80–95 %. Esa es la &amp;ldquo;GPU saturada&amp;rdquo; real para LLMs. Las tres juntas distinguen los regímenes; cada una sola no dice nada accionable.&lt;/p>
&lt;h2 id="cómo-se-montan-en-producción">Cómo se montan en producción&lt;/h2>
&lt;p>La parte de plataforma se cubre en &lt;a href="https://blog.lo0.es/posts/cinco-niveles-madurez-plataforma-llm-on-premise/">Cinco niveles de madurez&lt;/a> (nivel 4 — GPU plane) y &lt;a href="https://blog.lo0.es/posts/siete-fases-despliegue-plataforma-llm-on-premise/">Siete fases de despliegue&lt;/a> (fase F5). Para el observador, las piezas clave son:&lt;/p>
&lt;p>&lt;strong>NVIDIA GPU Operator.&lt;/strong> Manifiestos Helm que despliegan en cada nodo GPU: drivers, container toolkit, MIG manager y &lt;strong>DCGM Exporter&lt;/strong>. Este último expone &lt;code>/metrics&lt;/code> en formato Prometheus con todos los &lt;code>DCGM_FI_*&lt;/code> listados arriba. Se scrapea desde el Prometheus interno del cluster.&lt;/p>
&lt;p>&lt;strong>Motor de inferencia.&lt;/strong> vLLM expone &lt;code>/metrics&lt;/code> en el puerto 8000 (default) con métricas &lt;code>vllm:*&lt;/code>. SGLang lo expone también con prefijo &lt;code>sglang:&lt;/code>. TensorRT-LLM lo expone vía Triton Inference Server con prefijo &lt;code>nv_inference:&lt;/code>. La convención básica de nombres es similar entre los tres motores; los umbrales y queries de este post asumen vLLM, pero se traducen.&lt;/p>
&lt;p>&lt;strong>ServiceMonitor / PodMonitor.&lt;/strong> Recurso del operador de Prometheus que indica qué scrapear. Ejemplo mínimo:&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">monitoring.coreos.com/v1&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">PodMonitor&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>&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-inference&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">selector&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">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 }&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">podMetricsEndpoints&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">port&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metrics&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">15s&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;strong>Dashboards.&lt;/strong> El operador de NVIDIA publica dashboards Grafana de referencia para DCGM en &lt;code>nvidia/dcgm-exporter&lt;/code> (repo oficial). vLLM publica uno en &lt;code>vllm-project/vllm&lt;/code> (carpeta &lt;code>examples/&lt;/code>). Ambos sirven como base; cada equipo añade los paneles propios de su SLO.&lt;/p>
&lt;h2 id="las-doce-métricas-dcgm-organizadas-por-familia">Las doce métricas DCGM organizadas por familia&lt;/h2>
&lt;div class="diagram" style="max-width:820px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 820 360" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="doce métricas DCGM en cuatro familias">
&lt;style>.b{stroke:#333;stroke-width:1.4;rx:6}.fc{fill:#dfe9f5;stroke:#356}.fm{fill:#eef0d0;stroke:#7a3}.ft{fill:#f4e3cf;stroke:#a63}.fs{fill:#f6e2e2;stroke:#a33}.title{font:600 13px sans-serif;fill:#222}.fam{font:700 11px sans-serif;fill:#222}.met{font:10px monospace;fill:#222}.note{font:italic 10px sans-serif;fill:#555}&lt;/style>
&lt;text x="410" y="20" text-anchor="middle" class="title">Cabina DCGM: 12 métricas en 4 familias&lt;/text>
&lt;rect x="20" y="40" width="195" height="290" class="b fc"/>
&lt;text x="117" y="60" text-anchor="middle" class="fam">COMPUTE&lt;/text>
&lt;text x="30" y="90" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="105" class="met">SM_OCCUPANCY&lt;/text>
&lt;text x="30" y="135" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="150" class="met">PIPE_TENSOR_ACTIVE&lt;/text>
&lt;text x="30" y="180" class="met">DCGM_FI_PROF_&lt;/text>&lt;text x="30" y="195" class="met">DRAM_ACTIVE&lt;/text>
&lt;text x="30" y="240" text-anchor="start" class="note">¿Compute trabaja o&lt;/text>
&lt;text x="30" y="254" text-anchor="start" class="note">espera por HBM?&lt;/text>
&lt;rect x="220" y="40" width="195" height="290" class="b fm"/>
&lt;text x="317" y="60" text-anchor="middle" class="fam">MEMORIA&lt;/text>
&lt;text x="230" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="230" y="105" class="met">FB_USED&lt;/text>
&lt;text x="230" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="230" y="150" class="met">FB_FREE&lt;/text>
&lt;text x="230" y="180" class="met">DCGM_FI_DEV_NVLINK_&lt;/text>&lt;text x="230" y="195" class="met">BANDWIDTH_TOTAL&lt;/text>
&lt;text x="230" y="240" class="note">¿Queda VRAM para&lt;/text>
&lt;text x="230" y="254" class="note">nuevas requests?&lt;/text>
&lt;rect x="420" y="40" width="195" height="290" class="b ft"/>
&lt;text x="517" y="60" text-anchor="middle" class="fam">TÉRMICO · ENERGÉTICO&lt;/text>
&lt;text x="430" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="430" y="105" class="met">GPU_TEMP&lt;/text>
&lt;text x="430" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="430" y="150" class="met">POWER_USAGE&lt;/text>
&lt;text x="430" y="180" class="met">DCGM_FI_DEV_CLOCK_&lt;/text>&lt;text x="430" y="195" class="met">THROTTLE_REASONS&lt;/text>
&lt;text x="430" y="240" class="note">¿Hardware sano o&lt;/text>
&lt;text x="430" y="254" class="note">limitando silenciosamente?&lt;/text>
&lt;rect x="620" y="40" width="180" height="290" class="b fs"/>
&lt;text x="710" y="60" text-anchor="middle" class="fam">SALUD&lt;/text>
&lt;text x="630" y="90" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="105" class="met">XID_ERRORS&lt;/text>
&lt;text x="630" y="135" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="150" class="met">ECC_DBE_VOL_TOTAL&lt;/text>
&lt;text x="630" y="180" class="met">DCGM_FI_DEV_&lt;/text>&lt;text x="630" y="195" class="met">RETIRED_DBE&lt;/text>
&lt;text x="630" y="240" class="note">¿Hay degradación&lt;/text>
&lt;text x="630" y="254" class="note">del silicio en curso?&lt;/text>
&lt;text x="410" y="350" text-anchor="middle" class="note">Cada familia responde una pregunta distinta · ninguna basta sola&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h3 id="familia-1--compute">Familia 1 — Compute&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/strong> — Ratio de warps activos por SM sobre el máximo posible. Valor entre 0 y 1.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde&lt;/strong>: 0.30–0.70 (régimen típico LLM en decode).&lt;/li>
&lt;li>&lt;strong>Ámbar&lt;/strong>: &amp;lt; 0.20 sostenido (batch demasiado pequeño, GPU infrautilizada en paralelismo).&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: 0.95 sostenido con DRAM_ACTIVE bajo (kernel patológico saturando SMs).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/strong> — % de ciclos con tensor cores ejecutando. La métrica clave de &amp;ldquo;¿el compute está produciendo?&amp;rdquo;.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde en prefill&lt;/strong>: 50–80 %.&lt;/li>
&lt;li>&lt;strong>Verde en decode&lt;/strong>: 15–30 % (decode es memory-bound, no es síntoma de problema).&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;lt; 5 % sostenido en prefill o el motor no usa los tensor cores (mala config, formato incompatible).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/strong> — % de ciclos con HBM transfiriendo datos. Métrica clave para detectar saturación de memoria.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde en decode&lt;/strong>: 60–85 %.&lt;/li>
&lt;li>&lt;strong>Ámbar&lt;/strong>: &amp;gt; 90 % sostenido (HBM cuello de botella firme — explica la TPOT alta).&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;gt; 95 % sostenido con KV cache pool &amp;lt; 70 % (algo está pidiendo HBM que no es el motor; investigar leaks).&lt;/li>
&lt;/ul>
&lt;h3 id="familia-2--memoria">Familia 2 — Memoria&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_FB_USED&lt;/code>&lt;/strong> — Frame Buffer (HBM) usado en MiB.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde&lt;/strong>: 70–85 % del total.&lt;/li>
&lt;li>&lt;strong>Ámbar&lt;/strong>: 86–92 %.&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;gt; 92 % (riesgo de OOM en el siguiente paged-attention allocation).&lt;/li>
&lt;/ul>
&lt;p>PromQL para porcentaje sobre cluster: &lt;code>100 * sum(DCGM_FI_DEV_FB_USED) / sum(DCGM_FI_DEV_FB_TOTAL)&lt;/code>.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_FB_FREE&lt;/code>&lt;/strong> — Frame Buffer libre. Complementaria de la anterior; útil para alertas absolutas (&lt;code>&amp;lt; 4096 MiB libres&lt;/code>).&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>&lt;/strong> — Bandwidth NVLink agregado en MB/s. Para topologías TP (tensor parallel) que cruzan GPUs vía NVLink, esta métrica revela si el reparto de paralelismo está saturando el bus.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde&lt;/strong>: variable según topología. En 4×H100 SXM con NVLink 4.0, capacidad teórica 450 GB/s por GPU. Régimen TP=4 típico: 50–150 GB/s sostenido.&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;gt; 90 % capacidad sostenido (revisar si el modelo cabría con TP menor o pipeline parallel).&lt;/li>
&lt;/ul>
&lt;h3 id="familia-3--térmico-y-energético">Familia 3 — Térmico y energético&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>&lt;/strong> — Temperatura del die en °C.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde&lt;/strong>: &amp;lt; 75 °C.&lt;/li>
&lt;li>&lt;strong>Ámbar&lt;/strong>: 75–82 °C.&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;gt; 83 °C (cerca del thermal throttle automático de H100; revisar ventilación, caudal de aire, temperatura de entrada al rack).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/strong> — Consumo en watts. Para H100 SXM, TDP nominal 700 W. Útil para tres cosas: detectar workload inusualmente bajo (sospechar idle o stall), facturar coste energético real, y disparar alertas si el draw se acerca al límite de la PDU.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/strong> — Bitmap codificado con las razones de throttle activas. Es la métrica que &lt;strong>silenciosamente explica&lt;/strong> las degradaciones de TPOT.&lt;/p>
&lt;p>Bits relevantes:&lt;/p>
&lt;ul>
&lt;li>&lt;code>0x0000000000000001&lt;/code> — Idle (no es problema).&lt;/li>
&lt;li>&lt;code>0x0000000000000002&lt;/code> — App clocks setting.&lt;/li>
&lt;li>&lt;code>0x0000000000000004&lt;/code> — SW Power Cap (límite de software, p. ej. por &lt;code>nvidia-smi -pl&lt;/code>).&lt;/li>
&lt;li>&lt;code>0x0000000000000008&lt;/code> — HW Slowdown.&lt;/li>
&lt;li>&lt;code>0x0000000000000010&lt;/code> — Sync Boost (NVIDIA Sync).&lt;/li>
&lt;li>&lt;code>0x0000000000000020&lt;/code> — SW Thermal Slowdown (límite térmico de software).&lt;/li>
&lt;li>&lt;code>0x0000000000000040&lt;/code> — HW Thermal Slowdown (límite térmico de hardware — emergencia).&lt;/li>
&lt;li>&lt;code>0x0000000000000080&lt;/code> — HW Power Brake Slowdown (caída de tensión PSU).&lt;/li>
&lt;li>&lt;code>0x0000000000000100&lt;/code> — Display Clock Setting.&lt;/li>
&lt;/ul>
&lt;p>Cualquier throttle salvo &lt;code>Idle&lt;/code> con valor &amp;gt; 0 sostenido &lt;strong>es alerta&lt;/strong>. La degradación de TPOT con &lt;code>DRAM_ACTIVE&lt;/code> ya alto y throttle térmico activo es el clásico &amp;ldquo;el rack está mal ventilado, no es problema del motor&amp;rdquo;.&lt;/p>
&lt;h3 id="familia-4--salud">Familia 4 — Salud&lt;/h3>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_XID_ERRORS&lt;/code>&lt;/strong> — Contador acumulado de XID errors del driver. Los XID son códigos de evento crítico que NVIDIA documenta exhaustivamente (XID 13: graphics engine exception; XID 31: GPU memory page fault; XID 43: reset channel verif error; XID 79: GPU has fallen off the bus; XID 95: uncontained ECC error; etc.). &lt;strong>Cualquier incremento es alerta inmediata&lt;/strong>: muchos XID requieren reset del nodo o RMA de la GPU.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>&lt;/strong> — Errores ECC double-bit volátiles (no corregibles). A diferencia de los single-bit (que ECC corrige silenciosamente y se contabilizan en &lt;code>DCGM_FI_DEV_ECC_SBE_*&lt;/code>), los double-bit &lt;strong>corrompen datos&lt;/strong>. Cualquier valor &amp;gt; 0 es alerta crítica: la GPU debe ser drenada y revisada.&lt;/p>
&lt;p>&lt;strong>&lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>&lt;/strong> — Páginas físicas de HBM retiradas por double-bit errors acumulados. NVIDIA retira páginas defectuosas automáticamente para prevenir corrupción futura. Más de 4–8 páginas retiradas en una GPU sugiere degradación del silicio: documentar y planificar reemplazo en próxima ventana de mantenimiento.&lt;/p>
&lt;h2 id="las-cinco-métricas-del-motor-de-inferencia-vllm">Las cinco métricas del motor de inferencia (vLLM)&lt;/h2>
&lt;p>Las métricas DCGM responden &amp;ldquo;¿está sana la GPU?&amp;rdquo;. Las del motor responden &amp;ldquo;¿está el servicio cumpliendo el SLO?&amp;rdquo;. Sin ellas, sabes que el hardware funciona pero no sabes si los clientes están contentos.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:num_requests_running&lt;/code>&lt;/strong> — Requests actualmente en el batch. Si llega al &lt;code>--max-num-seqs&lt;/code> configurado y no baja, el motor está saturado en concurrencia (revisar VRAM y rebalancear vía autoscaler — ver &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a>).&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/strong> — Requests en cola, sin entrar al batch. Cualquier valor &amp;gt; 0 sostenido durante minutos indica que el cluster no escala con la carga. &lt;strong>Esta es la métrica primaria para HPA&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/strong> — % del KV cache pool usado.&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Verde&lt;/strong>: 50–80 %.&lt;/li>
&lt;li>&lt;strong>Ámbar&lt;/strong>: 80–92 %.&lt;/li>
&lt;li>&lt;strong>Rojo&lt;/strong>: &amp;gt; 92 % (riesgo de &lt;strong>preempt-on-OOM&lt;/strong>: vLLM tirará requests para liberar memoria, lo que aumenta TTFT visiblemente).&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>&lt;code>vllm:time_to_first_token_seconds&lt;/code>&lt;/strong> — Histograma de TTFT por request. Se consume como &lt;code>histogram_quantile(0.95, sum by(le)(rate(vllm:time_to_first_token_seconds_bucket[5m])))&lt;/code>. Comparado contra el SLO de TTFT P95 dispara la alerta primaria de servicio.&lt;/p>
&lt;p>&lt;strong>&lt;code>vllm:time_per_output_token_seconds&lt;/code>&lt;/strong> — Histograma de TPOT. Equivalente al anterior pero para fluidez de streaming. Comparado contra el SLO de TPOT P95 dispara la alerta secundaria.&lt;/p>
&lt;h2 id="las-seis-alertas-que-deben-pagear-en-producción">Las seis alertas que deben pagear en producción&lt;/h2>
&lt;p>Cualquier cluster productivo serio dispara estas seis alertas a un canal con rotación de guardia. Sin estas, el SLO se cumple por suerte, no por proceso.&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">groups&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">gpu-llm-critical&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">rules&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">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuHbmNearOom&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">expr&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 class="w"> &lt;/span>&lt;span class="l">(DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL) &amp;gt; 92&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">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">2m&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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;HBM de {{ $labels.gpu }} en {{ $value }}% — riesgo OOM&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuThermalOrPowerThrottle&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">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">(DCGM_FI_DEV_CLOCK_THROTTLE_REASONS != 0) and ignoring(reason) (DCGM_FI_DEV_CLOCK_THROTTLE_REASONS != 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">for&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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;GPU {{ $labels.gpu }} en throttle (reasons={{ $value }})&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuXidErrorDetected&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">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">increase(DCGM_FI_DEV_XID_ERRORS[5m]) &amp;gt; 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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;XID error en GPU {{ $labels.gpu }} — investigar inmediatamente&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">GpuEccDoubleBit&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">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">DCGM_FI_DEV_ECC_DBE_VOL_TOTAL &amp;gt; 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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">critical }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;ECC double-bit en GPU {{ $labels.gpu }} — drenar nodo&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">VllmKvCachePoolNearFull&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">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm:gpu_cache_usage_perc &amp;gt; 0.95&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">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">3m&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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;KV cache pool &amp;gt; 95% en {{ $labels.instance }}&amp;#34;&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>- &lt;span class="nt">alert&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">VllmTtftP95OutOfSlo&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">expr&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">histogram_quantile(0.95, sum by(le, instance)(rate(vllm:time_to_first_token_seconds_bucket[5m]))) &amp;gt; 1.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">for&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">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">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">severity&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">warning }&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">annotations&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">summary&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;TTFT P95 sobre SLO ({{ $value }}s &amp;gt; 1.5s)&amp;#34;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Estas seis cubren el 80 % de los incidentes que afectan a SLO. El 20 % restante exige investigación con tracing (ver &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a>).&lt;/p>
&lt;h2 id="tabla-maestra-umbrales-y-queries">Tabla maestra: umbrales y queries&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Verde&lt;/th>
&lt;th>Ámbar&lt;/th>
&lt;th>Rojo&lt;/th>
&lt;th>Query base (PromQL)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>SM occupancy&lt;/td>
&lt;td>0.30–0.70&lt;/td>
&lt;td>0.15–0.30&lt;/td>
&lt;td>&amp;lt; 0.10 sostenido&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_SM_OCCUPANCY&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tensor active (decode)&lt;/td>
&lt;td>15–30 %&lt;/td>
&lt;td>&amp;lt; 10 %&lt;/td>
&lt;td>&amp;lt; 3 %&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_PIPE_TENSOR_ACTIVE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DRAM active&lt;/td>
&lt;td>60–85 %&lt;/td>
&lt;td>85–95 %&lt;/td>
&lt;td>&amp;gt; 95 % con KV bajo&lt;/td>
&lt;td>&lt;code>DCGM_FI_PROF_DRAM_ACTIVE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>FB used&lt;/td>
&lt;td>70–85 %&lt;/td>
&lt;td>86–92 %&lt;/td>
&lt;td>&amp;gt; 92 %&lt;/td>
&lt;td>&lt;code>100 * DCGM_FI_DEV_FB_USED / DCGM_FI_DEV_FB_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>NVLink BW&lt;/td>
&lt;td>&amp;lt; 70 % cap&lt;/td>
&lt;td>70–90 % cap&lt;/td>
&lt;td>&amp;gt; 90 % cap&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_NVLINK_BANDWIDTH_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>GPU temp&lt;/td>
&lt;td>&amp;lt; 75 °C&lt;/td>
&lt;td>75–82 °C&lt;/td>
&lt;td>&amp;gt; 83 °C&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_GPU_TEMP&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Power usage&lt;/td>
&lt;td>&amp;lt; 90% TDP&lt;/td>
&lt;td>90–98 % TDP&lt;/td>
&lt;td>&amp;gt; 98 % TDP&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throttle reasons&lt;/td>
&lt;td>0 o Idle&lt;/td>
&lt;td>App/SW&lt;/td>
&lt;td>HW Therm/Power&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_CLOCK_THROTTLE_REASONS&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>XID errors&lt;/td>
&lt;td>sin cambio&lt;/td>
&lt;td>—&lt;/td>
&lt;td>cualquier delta&lt;/td>
&lt;td>&lt;code>increase(DCGM_FI_DEV_XID_ERRORS[5m])&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ECC DBE&lt;/td>
&lt;td>0&lt;/td>
&lt;td>—&lt;/td>
&lt;td>&amp;gt; 0&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_ECC_DBE_VOL_TOTAL&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Retired pages&lt;/td>
&lt;td>&amp;lt; 4&lt;/td>
&lt;td>4–8&lt;/td>
&lt;td>&amp;gt; 8&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_RETIRED_DBE&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KV cache used&lt;/td>
&lt;td>50–80 %&lt;/td>
&lt;td>80–92 %&lt;/td>
&lt;td>&amp;gt; 92 %&lt;/td>
&lt;td>&lt;code>vllm:gpu_cache_usage_perc&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Requests waiting&lt;/td>
&lt;td>0&lt;/td>
&lt;td>1–5 sostenido&lt;/td>
&lt;td>&amp;gt; 10 sostenido&lt;/td>
&lt;td>&lt;code>vllm:num_requests_waiting&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT P95&lt;/td>
&lt;td>&amp;lt; SLO&lt;/td>
&lt;td>80–100 % SLO&lt;/td>
&lt;td>&amp;gt; SLO&lt;/td>
&lt;td>ver query alerta arriba&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TPOT P95&lt;/td>
&lt;td>&amp;lt; SLO&lt;/td>
&lt;td>80–100 % SLO&lt;/td>
&lt;td>&amp;gt; SLO&lt;/td>
&lt;td>&lt;code>histogram_quantile(0.95, sum by(le)(rate(vllm:time_per_output_token_seconds_bucket[5m])))&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;h2 id="tres-pitfalls-que-confunden-al-operador-junior">Tres pitfalls que confunden al operador junior&lt;/h2>
&lt;p>&lt;strong>Pitfall 1 — &amp;ldquo;GPU-Util al 99 % = saturada&amp;rdquo;.&lt;/strong> Como se explicó al inicio: &lt;code>DCGM_FI_DEV_GPU_UTIL&lt;/code> se enciende con cualquier kernel. Lo correcto es mirar las tres &lt;code>_PROF_*&lt;/code> (SM occupancy, tensor active, DRAM active) juntas. &lt;em>GPU util 99 % + tensor active 8 % + DRAM active 92 %&lt;/em> = &amp;ldquo;saturada por memoria, no compute&amp;rdquo;; &lt;em>GPU util 99 % + tensor active 75 % + DRAM active 50 %&lt;/em> = &amp;ldquo;saturada por compute, prefill heavy&amp;rdquo;. Las dos situaciones piden palancas distintas.&lt;/p>
&lt;p>&lt;strong>Pitfall 2 — confundir ECC single-bit (SBE) con double-bit (DBE).&lt;/strong> Los single-bit se corrigen silenciosamente y son &lt;strong>inevitables&lt;/strong> en cualquier HBM bajo carga (radiación cósmica, fluctuaciones de tensión). Un contador SBE creciendo lentamente no es alerta — es física. El DBE sí: corrompe datos. Distinguir las dos métricas evita falsas alarmas y falsos negativos a partes iguales.&lt;/p>
&lt;p>&lt;strong>Pitfall 3 — alertar sobre &lt;code>num_requests_waiting &amp;gt; 0&lt;/code> sin contexto.&lt;/strong> Un valor instantáneo de 1 o 2 durante un pico es normal. Lo que importa es la cola &lt;strong>sostenida&lt;/strong>: usar &lt;code>for: 5m&lt;/code> con umbral 3–5. Sin esa ventana, el sistema satura el canal de alertas con ruido.&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×H100 SXM 80 GB con NVLink intra-nodo&lt;/strong>:&lt;/p>
&lt;ul>
&lt;li>DCGM Exporter desplegado vía NVIDIA GPU Operator, un DaemonSet por nodo GPU.&lt;/li>
&lt;li>Prometheus interno con retención 30 días para métricas de alta frecuencia, 1 año para downsampled (Thanos/Mimir si el volumen lo justifica).&lt;/li>
&lt;li>Grafana con tres dashboards estándar: hardware GPU (DCGM), motor (vLLM), SLO (TTFT/TPOT/RPS contra objetivos escritos).&lt;/li>
&lt;li>Alertmanager con rotación de guardia y rate-limiting por silencio agrupado por nodo.&lt;/li>
&lt;li>Cardinalidad controlada: &lt;code>gpu&lt;/code> (id local), &lt;code>node&lt;/code>, &lt;code>pod&lt;/code>, &lt;code>model&lt;/code> — no añadir &lt;code>request_id&lt;/code> ni labels de alta cardinalidad a métricas (eso es trabajo del tracing).&lt;/li>
&lt;/ul>
&lt;p>Volumen estimado para un cluster de 16 GPUs con scraping cada 15 s: ~2 millones de samples/min, ~25 GB/día de Prometheus crudo. Manejable con un Prometheus por cluster + retention; si el equipo escala a &amp;gt; 64 GPUs, considerar Thanos sidecar o VictoriaMetrics. Ver &lt;a href="https://blog.lo0.es/posts/catalogo-herramientas-oss-llmops/">Catálogo de herramientas OSS LLMOps&lt;/a> para alternativas equivalentes.&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>Tracing de cargas LLM&lt;/strong>: ya cubierto en &lt;a href="https://blog.lo0.es/posts/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Autoscaling&lt;/strong> basado en estas métricas: ver &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a>.&lt;/li>
&lt;li>&lt;strong>Runbooks de incident response&lt;/strong>: cómo cada una de estas alertas se traduce a acción concreta (drain, restart, RMA, escalado, rollback).&lt;/li>
&lt;li>&lt;strong>Cost accounting&lt;/strong>: usar &lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> y &lt;code>vllm:request_success_total&lt;/code> para showback de coste por tenant.&lt;/li>
&lt;li>&lt;strong>Monitorización de fairness multi-tenant&lt;/strong>: cuando varios tenants comparten cluster, qué métricas detectan que uno está acaparando el KV cache.&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/tracing-llm-otel-genai/">Tracing LLM con OpenTelemetry GenAI&lt;/a> — la otra mitad de la observabilidad.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">Capacity planning para inferencia LLM on-premise&lt;/a> — qué se dimensionó y, por tanto, qué umbrales son defendibles aquí.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> — explica por qué &lt;code>num_requests_running&lt;/code>, &lt;code>num_requests_waiting&lt;/code> y &lt;code>gpu_cache_usage_perc&lt;/code> son las métricas operativas del motor.&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> — la observabilidad LLM-aware vive en el nivel 4.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Siete capas del stack de inferencia LLM on-premise&lt;/a> — DCGM Exporter es pieza de la capa de plataforma.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a> — usa estas métricas como input.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/anatomia-metricas-dcgm-vllm-anomalias/">Anatomía de las doce métricas DCGM y cinco vLLM&lt;/a> — profundización con analogía y anomalía documentada en producción para cada métrica, con cifras de incidentes públicos (Meta Llama 3, &lt;em>Story of Two GPUs&lt;/em>, issues vLLM, KBs Dell/Lenovo).&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> — la traducción de cada alerta crítica a acción concreta (drain, reset, RMA, rollback) con workflow YAML, schema Kafka WORM y encaje en ISO 27035, ENS, NIS2, EU AI Act art. 73.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>NVIDIA — &lt;em>DCGM Exporter&lt;/em> (repo &lt;code>nvidia/dcgm-exporter&lt;/code>, métricas y unidades documentadas).&lt;/li>
&lt;li>NVIDIA — &lt;em>DCGM Field Identifiers reference&lt;/em> (lista completa de &lt;code>DCGM_FI_*&lt;/code>).&lt;/li>
&lt;li>NVIDIA — &lt;em>XID Errors documentation&lt;/em> (catálogo de códigos XID y procedimientos de remediación).&lt;/li>
&lt;li>NVIDIA — &lt;em>NVIDIA GPU Operator&lt;/em> (Helm chart oficial).&lt;/li>
&lt;li>vLLM project — &lt;code>examples/production_monitoring/&lt;/code> (PromQL y dashboards Grafana de referencia).&lt;/li>
&lt;li>Prometheus — &lt;em>Histogram and summary best practices&lt;/em> (para construir queries de percentiles defendibles).&lt;/li>
&lt;li>NVIDIA — &lt;em>H100 Tensor Core GPU datasheet&lt;/em> (TDP, HBM bandwidth, NVLink capacities).&lt;/li>
&lt;/ul></description></item><item><title>Capacity planning para inferencia LLM on-premise: cómo dimensionar GPUs a partir de un SLO</title><link>https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/</link><pubDate>Mon, 01 Jun 2026 15:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/</guid><description>&lt;blockquote>
&lt;p>Este post complementa los de &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> (la pieza que domina el presupuesto de VRAM), &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> (lo que define la utilización efectiva del compute) y &lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Siete capas del stack&lt;/a> (las piezas que el sizing presupone). Antes de leer este, asegúrate de que tu equipo tiene escritos los SLOs que va a perseguir; sin esa entrada el cálculo no es defendible.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El capacity planning de inferencia LLM no responde a &amp;ldquo;cuántos tokens/segundo da una GPU&amp;rdquo; — esa pregunta carece de respuesta universal porque el throughput depende de la concurrencia, el reparto prefill/decode, la longitud de contexto, el motor de inferencia y la quantization. La pregunta correcta tiene tres entradas (&lt;strong>SLO&lt;/strong>: TTFT P95, TPOT P95, RPS sostenidos), una &lt;strong>referencia de hardware&lt;/strong> (modelo de GPU, VRAM, ancho HBM, FLOPs efectivos) y un &lt;strong>modelo&lt;/strong> (parámetros, arquitectura GQA/MHA/MoE, formato de pesos). El cálculo se resuelve en dos presupuestos acoplados que se cruzan. &lt;strong>Presupuesto de VRAM&lt;/strong>: del total de la GPU restas pesos del modelo y activaciones, lo que queda es &lt;strong>KV cache budget&lt;/strong>, y de ahí derivas la &lt;strong>concurrencia máxima&lt;/strong> posible al contexto promedio que esperas. &lt;strong>Presupuesto de tiempo&lt;/strong>: el motor (vLLM, SGLang, TensorRT-LLM) tiene un techo de tokens/s en decode dado por el ancho de HBM y otro en prefill dado por el FLOP útil; de ahí derivas la &lt;strong>TPOT esperada&lt;/strong> y, dividiendo prefill_tokens entre el throughput de prefill, la &lt;strong>TTFT esperada&lt;/strong>. Ambos presupuestos deben cumplir el SLO &lt;strong>simultáneamente&lt;/strong>: el que esté más ajustado dicta el dimensionamiento. Sobre el ejemplo Llama 70B BF16 con tensor parallel 4 en 4×H100 SXM, una sola réplica satura a ~28 requests concurrentes y entrega ~3 200 tokens/s de decode agregado con TPOT mediano de 35 ms; para 200 RPS sostenidos a un perfil de 800 tokens de prompt + 250 de output, hacen falta entre 4 y 5 réplicas con un colchón del 25 % sobre el pico observado. La quantization (FP8 → INT4) divide entre 1.5 y 4× el coste de VRAM y de tiempo de decode, pero degrada calidad de forma medible — no se asume gratis, se valida con evals. Las cinco trampas habituales: confundir media con P95, ignorar el reparto prefill/decode del workload real, dimensionar sin head-room para retrain ni rollback, olvidar que la GPU al 100 % de SM util no significa nada si la HBM está saturada, y no documentar los supuestos del cálculo (un sizing sin supuestos escritos es un cálculo desechable).&lt;/p>
&lt;h2 id="estás-aquí-deploy-con-un-pie-en-observe">Estás aquí: DEPLOY (con un pie en OBSERVE)&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 un pie en Observe">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ffb347;stroke-width:3}.semiactive{fill:#ffe1b3;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(#cpm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#cpm)}&lt;/style>
&lt;defs>&lt;marker id="cpm" 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í: capacity planning · cierra DEPLOY y abre la conversación con OBSERVE&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 semiactive"/>&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 idle"/>&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>El capacity planning es una pieza con doble residencia. Vive en &lt;strong>DEPLOY&lt;/strong> porque sin un sizing válido no se compra hardware ni se configura el motor de inferencia. Pero su &lt;strong>input son las observaciones reales&lt;/strong>: distribución de longitudes de prompt y output, mezcla prefill/decode del workload, P95 reales que ya se están viendo en preproducción. Sin esos datos el cálculo es una servilleta — defendible solo hasta que llegue el primer cliente que no encaja en la media asumida.&lt;/p>
&lt;h2 id="la-analogía-el-hotel-con-habitaciones-de-tamaño-variable">La analogía: el hotel con habitaciones de tamaño variable&lt;/h2>
&lt;p>Imagina un hotel donde las habitaciones no tienen tamaño fijo: cada huésped paga por los metros cuadrados que necesita, y la planta del edificio se reorganiza dinámicamente para acomodar a quien llega. La dirección quiere maximizar ocupación, pero tiene dos restricciones reales y una métrica de calidad.&lt;/p>
&lt;p>&lt;strong>Restricción 1 — espacio físico.&lt;/strong> La planta tiene 1 000 m² totales. Si entra una familia que necesita 200 m², esa familia ocupa esa superficie y no se puede entregar al siguiente huésped. La habitación más grande limita cuántos huéspedes simultáneos caben.&lt;/p>
&lt;p>&lt;strong>Restricción 2 — personal de servicio.&lt;/strong> Hay 10 recepcionistas. Cada uno puede gestionar el check-in de un huésped cada dos minutos. Cuando llegan 60 huéspedes en una hora, los últimos esperan en cola; el tiempo desde que entran a recepción hasta que reciben su llave depende de cuántos hay delante.&lt;/p>
&lt;p>&lt;strong>Métrica de calidad — promesa de tiempo.&lt;/strong> La carta dice &amp;ldquo;check-in en menos de 15 minutos&amp;rdquo;. Si llegan demasiados huéspedes a la vez, esa promesa se rompe aunque haya espacio físico libre.&lt;/p>
&lt;p>El &lt;strong>espacio físico&lt;/strong> es la VRAM de la GPU. Cada &lt;strong>habitación&lt;/strong> es una request con su KV cache (más grande cuanto más larga la conversación). Los &lt;strong>recepcionistas&lt;/strong> son los compute units (Streaming Multiprocessors + Tensor Cores). El &lt;strong>check-in&lt;/strong> es la fase de prefill; las &lt;strong>noches&lt;/strong> que el huésped pasa después son los pasos de decode. La &lt;strong>promesa de 15 minutos&lt;/strong> es el SLO de TTFT P95.&lt;/p>
&lt;p>El capacity planning del hotel es exactamente este: dado el perfil esperado de huéspedes (cuántos llegan por hora, cuánto espacio piden de media, cuántos minutos toleran de espera), calcular cuántas plantas y cuántos recepcionistas hace falta. No se hace estimando &amp;ldquo;habitaciones por hora&amp;rdquo; en abstracto — se hace cruzando los dos presupuestos con la promesa de tiempo. La analogía sostiene el cálculo hasta el final.&lt;/p>
&lt;h2 id="las-tres-entradas-del-slo">Las tres entradas del SLO&lt;/h2>
&lt;p>Antes de poner un solo número en la hoja, hay que escribir las tres dimensiones del SLO. Sin esto el cálculo es estética, no ingeniería.&lt;/p>
&lt;p>&lt;strong>TTFT P95 (Time-To-First-Token).&lt;/strong> El tiempo desde que el cliente envía la request hasta que recibe el primer token. Está dominado por la fase de prefill (procesar el prompt entero de una vez) más la cola del scheduler. Para chat conversacional, un objetivo razonable está entre &lt;strong>0.5 y 2 segundos P95&lt;/strong>. Para asistentes de programación con prompts grandes (5–10 K tokens de contexto), entre &lt;strong>2 y 4 s P95&lt;/strong>. Por debajo de 500 ms entra en regla de UX para conversaciones tipo voz, pero exige compromisos serios de arquitectura.&lt;/p>
&lt;p>&lt;strong>TPOT P95 (Time-Per-Output-Token).&lt;/strong> El tiempo entre tokens consecutivos durante decode. Domina la &amp;ldquo;fluidez percibida&amp;rdquo; del streaming. Por encima de &lt;strong>80 ms/token&lt;/strong> el lector humano percibe pausas; por debajo de &lt;strong>30 ms/token&lt;/strong> la salida fluye más rápido de lo que se lee. Objetivo industrial habitual: &lt;strong>40–60 ms P95&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>RPS sostenidos cumpliendo SLO.&lt;/strong> El throughput que el sistema debe soportar &lt;strong>sin violar&lt;/strong> TTFT ni TPOT. Esto es la métrica clave de DistServe llamada &lt;strong>goodput&lt;/strong> —ver &lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a>—. &amp;ldquo;200 RPS pico&amp;rdquo; no es lo mismo que &amp;ldquo;200 RPS con TTFT P95 ≤ 1.5 s&amp;rdquo;. Sin la condición de SLO, el número de RPS no significa nada.&lt;/p>
&lt;p>Estas tres dimensiones se acompañan de un &lt;strong>perfil de workload&lt;/strong>: distribución de longitudes de prompt y de output. Las medianas no bastan; hace falta P50, P95, P99. Un perfil mal medido es el principal motivo de sizing fallido.&lt;/p>
&lt;h2 id="la-fórmula-central-dos-presupuestos-que-se-cruzan">La fórmula central: dos presupuestos que se cruzan&lt;/h2>
&lt;p>El cálculo se resuelve en dos cuentas independientes que después se cruzan. La menor de las dos manda.&lt;/p>
&lt;h3 id="presupuesto-de-vram">Presupuesto de VRAM&lt;/h3>
&lt;p>Para una GPU con VRAM total $V$, el espacio disponible para KV cache es:&lt;/p>
&lt;p>$$V_{\text{kv}} = V - V_{\text{model}} - V_{\text{activations}} - V_{\text{overhead}}$$&lt;/p>
&lt;p>donde:&lt;/p>
&lt;ul>
&lt;li>$V_{\text{model}}$ es el tamaño de los pesos: para un modelo de $P$ parámetros en formato $b$ bytes/parámetro, $V_{\text{model}} = P \cdot b$. Llama 70B BF16 = $70 \times 10^9 \times 2 = 140$ GB. En tensor parallel TP=4, cada GPU lleva $140 / 4 = 35$ GB.&lt;/li>
&lt;li>$V_{\text{activations}}$ son los buffers intermedios del forward pass. Para vLLM con batch razonable, entre &lt;strong>2 y 6 GB&lt;/strong> por GPU dependiendo de batch size y longitud máxima.&lt;/li>
&lt;li>$V_{\text{overhead}}$ son CUDA context, NCCL buffers, pool de PagedAttention, paged blocks reservados. &lt;strong>2–4 GB&lt;/strong> típicos.&lt;/li>
&lt;/ul>
&lt;p>El KV cache budget por GPU queda como el residuo. Para H100 SXM 80 GB con Llama 70B TP=4 BF16:&lt;/p>
&lt;p>$$V_{\text{kv}} = 80 - 35 - 4 - 3 = 38 \text{ GB por GPU} = 152 \text{ GB agregados sobre TP=4}$$&lt;/p>
&lt;p>El coste por token de KV cache para un modelo con $L$ capas, $H_{\text{kv}}$ heads KV (GQA), dimensión por head $d_h$, en formato $b$ bytes:&lt;/p>
&lt;p>$$\text{kv_per_token} = 2 \cdot L \cdot H_{\text{kv}} \cdot d_h \cdot b$$&lt;/p>
&lt;p>El factor 2 es porque se guardan K y V. Para Llama 70B (L=80, $H_{\text{kv}}$=8 con GQA, $d_h$=128, BF16 = 2 bytes):&lt;/p>
&lt;p>$$\text{kv_per_token} = 2 \cdot 80 \cdot 8 \cdot 128 \cdot 2 = 327,680 \text{ bytes} = 320 \text{ KB/token}$$&lt;/p>
&lt;p>Y la concurrencia máxima al contexto promedio $C$:&lt;/p>
&lt;p>$$N_{\text{max}} = \frac{V_{\text{kv}}}{C \cdot \text{kv_per_token}}$$&lt;/p>
&lt;p>Con $V_{\text{kv}}$ agregado de 152 GB y un contexto promedio de 1 500 tokens (800 prompt + 700 generados en el peor instante de la conversación):&lt;/p>
&lt;p>$$N_{\text{max}} = \frac{152 \times 10^9}{1,500 \cdot 320 \times 10^3} \approx 316 \text{ requests concurrentes}$$&lt;/p>
&lt;p>Este es el &lt;strong>techo físico&lt;/strong> de concurrencia para esa réplica. No es lo que vas a usar — es lo que &lt;strong>no puedes superar&lt;/strong> sin OOM. El número operativo está bastante por debajo (head-room para spikes).&lt;/p>
&lt;h3 id="presupuesto-de-tiempo">Presupuesto de tiempo&lt;/h3>
&lt;p>Aquí entran dos sub-cálculos: el de &lt;strong>decode&lt;/strong> (memory-bound) y el de &lt;strong>prefill&lt;/strong> (compute-bound).&lt;/p>
&lt;p>&lt;strong>Decode TPOT.&lt;/strong> Por cada token que se genera, hay que pasear los pesos del modelo (relevantes para esa request) y leer el KV cache acumulado. El cuello de botella es el ancho de banda HBM. Para una GPU con ancho $B$ GB/s y un modelo de $V_{\text{model_per_gpu}}$ GB de pesos:&lt;/p>
&lt;p>$$\text{tpot}&lt;em>{\text{teórico}} \approx \frac{V&lt;/em>{\text{model_per_gpu}}}{B}$$&lt;/p>
&lt;p>Para H100 SXM con HBM3 a 3.35 TB/s y Llama 70B TP=4 BF16 (35 GB/GPU):&lt;/p>
&lt;p>$$\text{tpot}_{\text{teórico}} \approx \frac{35}{3,350} \approx 10.4 \text{ ms/token}$$&lt;/p>
&lt;p>Este es el &lt;strong>mejor caso teórico&lt;/strong> con batch=1 y eficiencia HBM al 100 %. En la práctica vLLM en H100 con Llama 70B TP=4 alcanza &lt;strong>12–18 ms/token&lt;/strong> a batch bajo y &lt;strong>30–45 ms/token&lt;/strong> a batch alto (con concurrencia 32, los tokens compiten por la HBM compartida). El número operacional defendible: &lt;strong>35 ms/token&lt;/strong> en concurrencia 24–32.&lt;/p>
&lt;p>&lt;strong>Prefill throughput.&lt;/strong> El prefill procesa N tokens de prompt en un único forward pass. Es compute-bound: cuello en FLOPs. Para H100 SXM con 989 TFLOPs BF16 sostenidos y Llama 70B (cada forward pass cuesta aproximadamente $2 \cdot P \cdot N$ FLOPs por sequence de longitud N):&lt;/p>
&lt;p>$$\text{prefill_tps} = \frac{4 \cdot \text{TFLOPs} \cdot \eta}{2 \cdot P} = \frac{4 \cdot 989 \times 10^{12} \cdot 0.5}{2 \cdot 70 \times 10^9} \approx 14,000 \text{ tokens/s}$$&lt;/p>
&lt;p>(el factor 4 son las GPUs en TP, $\eta$ es eficiencia real entre 0.4 y 0.6 en H100). Un prompt de 800 tokens tarda en prefill:&lt;/p>
&lt;p>$$\text{prefill_time} = \frac{800}{14,000} \approx 57 \text{ ms}$$&lt;/p>
&lt;p>Sumando una cola típica de 100–300 ms a concurrencia alta, &lt;strong>TTFT P95 ≈ 350–500 ms&lt;/strong> para ese perfil. Muy por debajo del objetivo de 1.5 s — hay margen.&lt;/p>
&lt;h3 id="el-cruce">El cruce&lt;/h3>
&lt;p>La concurrencia operativa real $N_{\text{op}}$ es el mínimo entre &lt;strong>el techo de VRAM&lt;/strong>, la concurrencia a la que el &lt;strong>TPOT empieza a degradar&lt;/strong> por encima del SLO, y la concurrencia a la que el &lt;strong>TTFT empieza a degradar&lt;/strong> por encima del SLO (cola de prefill). Para el ejemplo:&lt;/p>
&lt;ul>
&lt;li>VRAM techo: 316.&lt;/li>
&lt;li>TPOT degrada a 80 ms (SLO) alrededor de concurrencia &lt;strong>~80–100&lt;/strong> (medido empíricamente con benchmark, no fórmula cerrada).&lt;/li>
&lt;li>TTFT degrada a 1.5 s alrededor de concurrencia &lt;strong>~40–60&lt;/strong> por cola de prefill.&lt;/li>
&lt;/ul>
&lt;p>La concurrencia operativa de la réplica es &lt;strong>~50&lt;/strong>. Aplicando un 25 % de head-room para spikes y rebalanceos, &lt;strong>concurrencia objetivo por réplica ≈ 35–40&lt;/strong>.&lt;/p>
&lt;h2 id="hoja-de-cálculo-paso-a-paso-llama-70b-bf16-en-4h100-sxm">Hoja de cálculo paso a paso: Llama 70B BF16 en 4×H100 SXM&lt;/h2>
&lt;p>Entrada del ejercicio:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>SLO&lt;/strong>: TTFT P95 ≤ 1.5 s; TPOT P95 ≤ 60 ms; &lt;strong>200 RPS sostenidos&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Workload&lt;/strong>: prompt P50=600, P95=1 200, P99=2 500; output P50=180, P95=500, P99=900. Promedio prompt 800, output 250.&lt;/li>
&lt;li>&lt;strong>Hardware genérico&lt;/strong>: 4×H100 SXM 80 GB con NVLink, motor vLLM v1, tensor parallel 4, BF16.&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>Paso 1 — VRAM por GPU.&lt;/strong> Pesos 35 GB, activaciones 4 GB, overhead 3 GB → KV budget 38 GB/GPU = 152 GB agregados. KV/token a Llama 70B GQA = 320 KB. Techo de tokens vivos en cache: $152 \times 10^9 / 320 \times 10^3 \approx 475,000$ tokens. Al contexto promedio operacional (800 prompt + 200 ya generados = 1 000 tokens vivos por request), techo de concurrencia $\approx 475$.&lt;/p>
&lt;p>&lt;strong>Paso 2 — duración media de una request.&lt;/strong> Prefill 800 tokens / 14 000 tps = 57 ms. Decode 250 tokens × 35 ms/token = 8 750 ms. Total $\approx 8.8$ s por request.&lt;/p>
&lt;p>&lt;strong>Paso 3 — throughput de la réplica.&lt;/strong> Si la réplica sostiene concurrencia operativa 40 y cada request dura 8.8 s, la réplica entrega aproximadamente $40 / 8.8 \approx 4.5$ requests/s en régimen estacionario.&lt;/p>
&lt;p>&lt;strong>Paso 4 — número de réplicas.&lt;/strong> Para 200 RPS objetivo: $200 / 4.5 \approx 45$ réplicas. Eso son &lt;strong>45 × 4 = 180 GPUs&lt;/strong>. Demasiado: este sizing no funciona porque el coste por request es alto.&lt;/p>
&lt;p>&lt;strong>Paso 5 — revisar palancas.&lt;/strong> Antes de comprar más hardware, hay tres palancas que se exploran en este orden:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Quantization.&lt;/strong> Bajar a FP8 reduce pesos a 17.5 GB/GPU (queda más VRAM para KV cache → más concurrencia), aproximadamente duplica tokens/s en decode (HBM saturada por la mitad), y degrada calidad MMLU típicamente 0.5–1.5 puntos en modelos como Llama 70B. Reescribiendo el cálculo en FP8: TPOT baja a ~18 ms, tiempo total por request a 4.7 s, RPS por réplica sube a ~8.5, &lt;strong>réplicas necesarias ≈ 24, equivalente a 96 GPUs&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Speculative decoding.&lt;/strong> Con un drafter pequeño y aceptación del 60–70 %, TPOT efectivo cae 30–40 %. RPS por réplica sube a ~12, &lt;strong>réplicas ≈ 17 = 68 GPUs&lt;/strong>.&lt;/li>
&lt;li>&lt;strong>Disaggregated serving.&lt;/strong> Separar prefill workers y decode workers permite escalar cada uno a la mezcla real del workload —ver &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving&lt;/a>—. Suele recortar otro 20–40 % bajo workloads asimétricos.&lt;/li>
&lt;/ol>
&lt;p>&lt;strong>Paso 6 — sizing recomendado.&lt;/strong> Para el ejemplo, con FP8 + speculative decoding y un head-room del 25 %: &lt;strong>20 réplicas vLLM TP=4 sobre 80 H100 SXM&lt;/strong>. Si el equipo no quiere depender de quantization agresiva (BF16 puro para máxima fidelidad), el cálculo sube a &lt;strong>30 réplicas = 120 GPUs&lt;/strong> y obliga a renegociar SLO o presupuesto.&lt;/p>
&lt;p>&lt;strong>Paso 7 — escribir los supuestos.&lt;/strong> Esta es la parte que ningún sizing válido se salta. En el repo del equipo, junto al cálculo:&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="c"># sizing/llama70b-prod.yaml&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">fecha&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="ld">2026-06-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">slo&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">ttft_p95_ms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1500&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">tpot_p95_ms&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">60&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">rps_target&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">200&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">workload&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">prompt_tokens_p50&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">600&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">prompt_tokens_p95&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">1200&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">output_tokens_p50&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">180&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">output_tokens_p95&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">500&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">asunto&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">chat productivo con RAG ligero&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">modelo&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">arquitectura&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama-70b-instruct&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">formato_pesos&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">fp8&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">motor&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-v1&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">hardware&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">gpu&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">H100-SXM-80GB&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">topologia&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">TP=4 con NVLink intra-nodo&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">red_inter_replica&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 class="l">GbE&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">optimizaciones&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="l">paged_attention&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="l">chunked_prefill&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="l">speculative_decoding (drafter llama-1.1b, aceptación esperada 65%)&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">asunciones_criticas&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">utilizacion_hbm_eficiente&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.55&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">head_room_pico_sobre_p95&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.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">aceptacion_speculative_min&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.55&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">plan_validacion&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="l">benchmark vllm bench serve antes de procurement&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="l">canary 10% durante 7 días post-deploy&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Sin este YAML, el cálculo no es reproducible un mes después.&lt;/p>
&lt;h2 id="caso-moe-mixtral-822b-141-b-totales-39-b-activos">Caso MoE: Mixtral 8×22B (~141 B totales, 39 B activos)&lt;/h2>
&lt;p>Los MoE cambian el cálculo en una dimensión clave: los pesos totales son grandes pero los pesos &lt;strong>activos por token&lt;/strong> son pequeños. Para Mixtral 8×22B con top-2 routing:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>VRAM de pesos&lt;/strong>: $141 \times 2 = 282$ GB BF16. Con TP=4 → 70 GB/GPU. No cabe en H100 80 GB con KV cache + activaciones. Hace falta TP=8 (~35 GB/GPU) o FP8 con TP=4 (~35 GB/GPU).&lt;/li>
&lt;li>&lt;strong>Decode TPOT&lt;/strong>: dominado por los pesos &lt;strong>leídos por token&lt;/strong>, que son $\sim 39 / 8 \cdot 2 \approx 9.75$ GB/GPU con TP=4 (un experto top-2 por token, dividido entre 4 GPUs). En H100 con HBM 3.35 TB/s, &lt;strong>TPOT teórico ≈ 3 ms/token&lt;/strong>. En la práctica, 10–20 ms a concurrencia razonable.&lt;/li>
&lt;li>&lt;strong>Prefill&lt;/strong>: similar al modelo denso de los pesos activos, ~39 B FLOPs/token.&lt;/li>
&lt;/ul>
&lt;p>El sizing MoE suele entregar más RPS por GPU que un denso equivalente — el coste por token bajo compensa el extra de VRAM. Ver &lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference&lt;/a> para el detalle del routing y por qué el batch alto es decisivo para que cada experto vea suficientes tokens.&lt;/p>
&lt;h2 id="tabla-de-sensibilidad-contexto-y-quantization">Tabla de sensibilidad: contexto y quantization&lt;/h2>
&lt;p>Para Llama 70B sobre 4×H100 SXM (TP=4), concurrencia operativa por réplica con SLO TTFT 1.5 s / TPOT 60 ms:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Contexto promedio&lt;/th>
&lt;th>BF16&lt;/th>
&lt;th>FP8&lt;/th>
&lt;th>INT4 (AWQ)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>500 tokens&lt;/td>
&lt;td>55&lt;/td>
&lt;td>110&lt;/td>
&lt;td>180&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>1 000 tokens&lt;/td>
&lt;td>40&lt;/td>
&lt;td>80&lt;/td>
&lt;td>130&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2 000 tokens&lt;/td>
&lt;td>24&lt;/td>
&lt;td>50&lt;/td>
&lt;td>85&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4 000 tokens&lt;/td>
&lt;td>12&lt;/td>
&lt;td>26&lt;/td>
&lt;td>48&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8 000 tokens&lt;/td>
&lt;td>6&lt;/td>
&lt;td>13&lt;/td>
&lt;td>25&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Números aproximados de benchmark vLLM público a junio 2026, con variación ±20 % según versión del motor y headroom adoptado. Para validar en tu hardware: &lt;code>vllm bench serve&lt;/code> con tu perfil de prompts reales.&lt;/p>
&lt;h2 id="las-cinco-trampas-habituales">Las cinco trampas habituales&lt;/h2>
&lt;p>&lt;strong>Trampa 1 — confundir media con P95.&lt;/strong> El throughput medio de una hora puede ser 50 RPS pero el pico de 5 minutos llegar a 180 RPS. Dimensionar contra la media garantiza romper SLO en cada pico. Regla: dimensionar contra P95 horario, con head-room del 20–30 % sobre P95.&lt;/p>
&lt;p>&lt;strong>Trampa 2 — no medir el reparto prefill/decode real.&lt;/strong> Un workload de &amp;ldquo;RAG con respuestas cortas&amp;rdquo; tiene 70–80 % del tiempo de GPU en prefill; un &amp;ldquo;writing assistant que genera ensayos&amp;rdquo; tiene 80 % en decode. Las optimizaciones útiles (chunked prefill vs speculative decoding) cambian radicalmente. Sin medirlo, se compra hardware mal balanceado.&lt;/p>
&lt;p>&lt;strong>Trampa 3 — dimensionar sin head-room para retrain ni rollback.&lt;/strong> El cluster productivo no es solo el motor de inferencia: hay batch de re-embeddings cuando cambia el modelo de embeddings, eval continuo de canary —ver &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow&lt;/a>—, fine-tune ligero, hot stand-by para rollback. Reservar &lt;strong>15–25 % de capacidad&lt;/strong> para esos workloads no negociables.&lt;/p>
&lt;p>&lt;strong>Trampa 4 — &amp;ldquo;GPU al 100 % de SM utilization&amp;rdquo; como objetivo.&lt;/strong> SM occupancy del 95 % con HBM saturada produce el mismo throughput que SM al 60 % con HBM saturada. El cuello de botella en decode es la HBM. Optimizar para &amp;ldquo;GPU usage 100 %&amp;rdquo; sin mirar HBM utilization y arithmetic intensity hace gastar más en GPU sin ganar throughput. Ver &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a> para qué métricas mirar realmente.&lt;/p>
&lt;p>&lt;strong>Trampa 5 — no documentar los supuestos.&lt;/strong> Un sizing sin YAML reproducible (workload, modelo, motor, head-room, asunciones críticas) deja al equipo sin manera de saber qué cambió cuando el cluster ya no llega a SLO seis meses después. Documentar es barato; perder un trimestre depurando, no.&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×H100 SXM 80 GB con NVLink intra-nodo y 25 GbE entre nodos&lt;/strong>, las configuraciones recurrentes en mayo 2026 son:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Modelo&lt;/th>
&lt;th>Formato&lt;/th>
&lt;th>TP&lt;/th>
&lt;th>Réplicas que caben&lt;/th>
&lt;th>RPS típico por nodo (ctx 1K)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Llama 8B&lt;/td>
&lt;td>BF16&lt;/td>
&lt;td>1&lt;/td>
&lt;td>4 (una por GPU)&lt;/td>
&lt;td>240–320&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 8B&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>1&lt;/td>
&lt;td>4&lt;/td>
&lt;td>450–600&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 70B&lt;/td>
&lt;td>BF16&lt;/td>
&lt;td>4&lt;/td>
&lt;td>1&lt;/td>
&lt;td>30–45&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 70B&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>4&lt;/td>
&lt;td>1&lt;/td>
&lt;td>60–90&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Llama 70B&lt;/td>
&lt;td>INT4 AWQ&lt;/td>
&lt;td>2&lt;/td>
&lt;td>2&lt;/td>
&lt;td>90–130&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Mixtral 8×22B&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>4&lt;/td>
&lt;td>1&lt;/td>
&lt;td>90–140&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Qwen 72B&lt;/td>
&lt;td>BF16&lt;/td>
&lt;td>4&lt;/td>
&lt;td>1&lt;/td>
&lt;td>28–42&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Estos números son &lt;strong>órdenes de magnitud para empezar la conversación&lt;/strong>, no compromisos. El sizing definitivo se valida con &lt;code>vllm bench serve&lt;/code> o &lt;code>genai-perf&lt;/code> (NVIDIA) usando el perfil de prompts/outputs reales del cliente. La asimetría prefill/decode del workload de cada caso puede mover estos números un 30–50 % arriba o abajo.&lt;/p>
&lt;p>Para clusters de &lt;strong>8×H100 SXM&lt;/strong> (típico de servidores DGX o réplicas equivalentes), las opciones se abren a TP=8 para modelos clase 405B o multi-réplica TP=2 para modelos 70B con mayor densidad. La métrica que decide es siempre la misma: &lt;strong>tokens cumpliendo SLO por kW&lt;/strong> y por euro de hardware amortizado.&lt;/p>
&lt;h2 id="cómo-se-valida-el-sizing-antes-de-comprar">Cómo se valida el sizing antes de comprar&lt;/h2>
&lt;p>El sizing en hoja de cálculo es la primera mitad. La segunda es el benchmark de validación.&lt;/p>
&lt;p>&lt;strong>Stage 1 — sizing servilleta.&lt;/strong> Las fórmulas de este post sobre el SLO y el workload esperado. Salida: número aproximado de réplicas y topología.&lt;/p>
&lt;p>&lt;strong>Stage 2 — micro-benchmark sintético.&lt;/strong> En una GPU prestada o alquilada por días, levantar el motor con el modelo elegido y correr &lt;code>vllm bench serve&lt;/code> con prompts de longitudes representativas. Validar TPOT, prefill TPS y techo de concurrencia. Calibrar el factor de eficiencia HBM ($\eta$) usado en las fórmulas.&lt;/p>
&lt;p>&lt;strong>Stage 3 — load test con tráfico realista.&lt;/strong> Generar tráfico siguiendo la distribución real del workload del cliente (no Poisson, no constante: la traza real). Medir P50/P95/P99 de TTFT, TPOT, throughput. Confirmar el head-room.&lt;/p>
&lt;p>&lt;strong>Stage 4 — canary en producción.&lt;/strong> Con el cluster dimensionado, encaminar el 5–10 % del tráfico real durante 7–14 días antes de cerrar el procurement de hardware adicional. Ver &lt;a href="https://blog.lo0.es/posts/canary-blue-green-shadow-modelos-llm/">Canary, blue-green y shadow&lt;/a> para la mecánica.&lt;/p>
&lt;p>Saltar de Stage 1 a procurement total es la causa más frecuente de cluster sobredimensionado en el 40 % y subdimensionado en el 60 % al mismo tiempo, en regiones distintas del workload. Cuatro semanas de validación bien hechas ahorran cuatro meses de refactor.&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>Las métricas de observabilidad&lt;/strong> que cierran el bucle del sizing en producción — ver &lt;a href="https://blog.lo0.es/posts/observabilidad-gpu-dcgm-llm/">Observabilidad GPU para inferencia LLM&lt;/a>.&lt;/li>
&lt;li>&lt;strong>El autoscaling&lt;/strong> que ajusta réplicas a la curva real de tráfico — ver &lt;a href="https://blog.lo0.es/posts/autoscaling-llm-kubernetes-keda/">Autoscaling LLM en Kubernetes&lt;/a>.&lt;/li>
&lt;li>&lt;strong>El cost accounting&lt;/strong> detallado por tenant (showback / chargeback) sobre el hardware dimensionado.&lt;/li>
&lt;li>&lt;strong>El sizing para fine-tuning continuo&lt;/strong> (PEFT y entrenamiento ligero) que comparte cluster con la inferencia.&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/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — el componente que domina el presupuesto de VRAM.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> — qué define la utilización efectiva del compute y la métrica goodput.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving prefill/decode&lt;/a> — palanca avanzada para workloads asimétricos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference&lt;/a> — cómo cambian las cuentas con modelos MoE.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia&lt;/a> — qué cuesta y qué ahorra cada formato.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/siete-capas-stack-inferencia-llm-on-premise/">Siete capas del stack de inferencia LLM on-premise&lt;/a> — las piezas que el sizing presupone.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/entornos-mixtos-nvidia-intel-servidores-nucs/">Entornos mixtos NVIDIA + Intel para inferencia LLM&lt;/a> — el sizing cierra mejor cuando se acepta heterogeneidad: embeddings y reranker en Intel Xeon AMX liberan H100 para el LLM grande, sin comprar más GPU.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Kwon et al. — &lt;em>vLLM: Easy, Fast, and Cheap LLM Serving with PagedAttention&lt;/em> (SOSP 2023).&lt;/li>
&lt;li>Zhong et al. — &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized LLM Serving&lt;/em> (OSDI 2024).&lt;/li>
&lt;li>Agrawal et al. — &lt;em>Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve&lt;/em> (OSDI 2024).&lt;/li>
&lt;li>NVIDIA — &lt;em>H100 Tensor Core GPU Architecture Whitepaper&lt;/em> (memoria HBM3, bandwidth, FLOPs sostenidos).&lt;/li>
&lt;li>vLLM project — &lt;code>vllm bench serve&lt;/code> reference (CLI de benchmarking incluida en el repo).&lt;/li>
&lt;li>NVIDIA — &lt;code>genai-perf&lt;/code> (herramienta oficial para benchmark de servicios LLM).&lt;/li>
&lt;/ul></description></item></channel></rss>