<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Guidellm on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/guidellm/</link><description>Recent content in Guidellm on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Tue, 16 Jun 2026 12:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/guidellm/index.xml" rel="self" type="application/rss+xml"/><item><title>El harness reproducible: medir coste, rendimiento y energía en un solo experimento auditable</title><link>https://blog.lo0.es/posts/harness-reproducible-medicion-coste-rendimiento-energia/</link><pubDate>Tue, 16 Jun 2026 12:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/harness-reproducible-medicion-coste-rendimiento-energia/</guid><description>&lt;blockquote>
&lt;p>Notación: importes en &lt;strong>euros (N €)&lt;/strong>, decimales con coma, millares con espacio fino. No se usa el símbolo de dólar (en este sitio es delimitador de fórmula).&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La serie &amp;ldquo;datos&amp;rdquo; ha producido tres ejes de medición independientes: coste por millón de tokens (OpenCost + LiteLLM), rendimiento bajo SLO (GuideLLM + AIPerf) y energía por token (DCGM + Kepler). El problema es que los tres se han medido en artículos distintos, con cargas distintas y en momentos distintos: no son comparables entre sí. Este artículo de cierre describe el &lt;strong>harness integrado&lt;/strong> que ejecuta los tres ejes &lt;strong>en el mismo experimento&lt;/strong>, sobre el mismo nodo (4×H100 SXM, referencia genérica), con todos los metadatos fijados, la salida en JSON/CSV versionados y un Job de Kubernetes idempotente. El resultado es el &lt;strong>scorecard de 3 ejes&lt;/strong> (€/1M tok, Wh/token, TTFT/ITL P99) que permite comparar configuraciones sobre una &lt;strong>frontera de Pareto multi-objetivo&lt;/strong> y auditar cualquier cifra con el banco para reproducirla.&lt;/p>
&lt;hr>
&lt;h2 id="por-qué-los-tres-ejes-deben-medirse-juntos">Por qué los tres ejes deben medirse juntos&lt;/h2>
&lt;p>El post de apertura de la serie (&lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">Los tres ejes&lt;/a>) estableció la identidad:&lt;/p>
$$\text{CPM} = \frac{\text{coste/h}}{\text{throughput (tok/s)} \times 3{,}6 \times 10^{-3}}$$
$$\text{energía/token (Wh)} = \frac{\text{potencia media (W)}}{\text{throughput (tok/s)} \times 3\,600}$$
&lt;p>El throughput es el denominador común. Si se mide en experimentos distintos —diferente hora, diferente carga, diferente temperatura de GPU— el CPM y la energía/token &lt;strong>no comparten denominador&lt;/strong>: son tres anécdotas, no un scorecard. El harness los captura en la misma ventana temporal, sobre la misma carga, con ventanas de Prometheus alineadas al segundo. Solo así la fila del scorecard es coherente por construcción.&lt;/p>
&lt;p>La segunda razón es la reproducibilidad. El post &lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo y reproducibilidad en el benchmarking&lt;/a> listó doce sesgos que invalidan comparaciones: motor no pinneado, tokenizador no declarado, longitud de entrada/salida no fijada, warmup ausente, cliente fuera del cluster. El harness elimina todos ellos porque los metadatos son parte del Job, no de la documentación.&lt;/p>
&lt;hr>
&lt;h2 id="arquitectura-del-banco-integrado">Arquitectura del banco integrado&lt;/h2>
&lt;p>El harness tiene cuatro capas. Cada una es OSS, exporta a Prometheus y convive en el mismo namespace de Kubernetes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Capa&lt;/th>
&lt;th>Herramienta(s)&lt;/th>
&lt;th>Métrica primaria&lt;/th>
&lt;th>Protocolo de export&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Rendimiento&lt;/strong>&lt;/td>
&lt;td>GuideLLM (sweep SLO) + AIPerf&lt;/td>
&lt;td>TTFT P99, ITL P99, goodput (tok/s)&lt;/td>
&lt;td>JSON/CSV nativo + &lt;code>/metrics&lt;/code> OpenMetrics&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Coste&lt;/strong>&lt;/td>
&lt;td>OpenCost + LiteLLM proxy&lt;/td>
&lt;td>CPM (€/1M tok), coste/petición&lt;/td>
&lt;td>API REST + Prometheus scrape&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Energía (GPU)&lt;/strong>&lt;/td>
&lt;td>DCGM Exporter&lt;/td>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code> (W), &lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code> (mJ)&lt;/td>
&lt;td>DaemonSet Prometheus&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Energía (pod)&lt;/strong>&lt;/td>
&lt;td>Kepler&lt;/td>
&lt;td>&lt;code>kepler_container_joules_total&lt;/code>, energía por pod&lt;/td>
&lt;td>DaemonSet Prometheus&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>MLPerf Power se usa como &lt;strong>referencia de comparabilidad externa&lt;/strong>: sus resultados publicados, con hardware documentado al detalle, permiten calibrar si las cifras del harness son plausibles. El banco propio no pretende ser un submission de MLPerf, sino ser &lt;strong>reproducible en el propio cluster&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:820px;margin:1rem auto;">
&lt;svg viewBox="0 0 820 320" role="img" aria-label="Arquitectura del harness: Job Kubernetes orquesta GuideLLM y AIPerf como generadores de carga, OpenCost y LiteLLM miden coste, DCGM y Kepler miden energia, todo confluye en Prometheus y el scorecard JSON" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.2;marker-end:url(#ah)}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3;marker-end:url(#ah)}&lt;/style>
&lt;defs>&lt;marker id="ah" 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="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="10" y="10" width="800" height="300" rx="10"/>
&lt;text x="20" y="28" class="tl">Namespace: benchmark&lt;/text>
&lt;rect class="bx" x="30" y="40" width="180" height="70" rx="6"/>
&lt;text x="40" y="60" class="tl">Job benchmark-run&lt;/text>
&lt;text x="40" y="76" class="ts">GuideLLM sweep (SLO)&lt;/text>
&lt;text x="40" y="92" class="ts">AIPerf concurrencia fija&lt;/text>
&lt;rect class="bx" x="30" y="130" width="180" height="60" rx="6"/>
&lt;text x="40" y="150" class="tl">LiteLLM proxy&lt;/text>
&lt;text x="40" y="168" class="ts">token counting + CPM&lt;/text>
&lt;rect class="bx" x="30" y="210" width="180" height="60" rx="6"/>
&lt;text x="40" y="230" class="tl">DaemonSet DCGM&lt;/text>
&lt;text x="40" y="248" class="ts">POWER_USAGE, ENERGY&lt;/text>
&lt;rect class="bx" x="30" y="280" width="180" height="28" rx="6"/>
&lt;text x="40" y="298" class="tl">DaemonSet Kepler&lt;/text>
&lt;rect class="bx" x="280" y="40" width="180" height="70" rx="6"/>
&lt;text x="290" y="60" class="tl">Inference endpoint&lt;/text>
&lt;text x="290" y="78" class="ts">vLLM / SGLang&lt;/text>
&lt;text x="290" y="96" class="ts">modelo pinneado, FP8/FP16&lt;/text>
&lt;rect class="bx" x="280" y="130" width="180" height="60" rx="6"/>
&lt;text x="290" y="150" class="tl">OpenCost&lt;/text>
&lt;text x="290" y="168" class="ts">€/GPU-h por pod/ns&lt;/text>
&lt;rect class="bx" x="540" y="60" width="160" height="80" rx="6"/>
&lt;text x="550" y="82" class="tl">Prometheus&lt;/text>
&lt;text x="550" y="100" class="ts">scrape 15 s&lt;/text>
&lt;text x="550" y="116" class="ts">retención 30 días&lt;/text>
&lt;rect class="bx" x="540" y="170" width="160" height="80" rx="6"/>
&lt;text x="550" y="192" class="tl">Scorecard exporter&lt;/text>
&lt;text x="550" y="210" class="ts">PromQL → JSON/CSV&lt;/text>
&lt;text x="550" y="228" class="ts">versionado en git&lt;/text>
&lt;path class="ar" d="M210,75 L280,75"/>
&lt;path class="ar" d="M460,155 L540,155"/>
&lt;path class="ar" d="M210,155 L280,155"/>
&lt;path class="dsh" d="M210,240 L540,200"/>
&lt;path class="dsh" d="M210,294 L540,220"/>
&lt;path class="ar" d="M700,100 L700,170"/>
&lt;text x="550" y="278" class="ts">una fila por (modelo, config, hardware)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="el-job-de-kubernetes-yaml-completo">El Job de Kubernetes: YAML completo&lt;/h2>
&lt;p>El experimento se ejecuta como un &lt;strong>Kubernetes Job&lt;/strong> versionado. Todos los metadatos relevantes son variables de entorno declaradas en el manifiesto: ni en scripts ad-hoc, ni en documentación externa. El Job es idempotente (mismo nombre = misma corrida) y deja trazas en el log del pod y en el volumen de salida.&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">batch/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">Job&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">bench-llama3-70b-fp8-h100x4-20260616&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">namespace&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">benchmark&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>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="nt">bench/model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama3-70b&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">bench/precision&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">bench/engine&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">vllm-0.9.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">bench/hardware&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">h100x4-sxm&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">bench/isl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;1024&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 class="nt">bench/osl&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;256&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 class="nt">bench/concurrency&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;32&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 class="nt">bench/tokenizer&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">meta-llama-3-tokenizer-v3&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">bench/run-id&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="s2">&amp;#34;20260616T1200&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 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">backoffLimit&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">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">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">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">restartPolicy&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">Never&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">serviceAccountName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bench-runner&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">volumes&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">results&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">persistentVolumeClaim&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">claimName&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">bench-results-pvc&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">initContainers&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="c"># Warmup: 60 s de tráfico previo al experimento&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">warmup&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">ghcr.io/vllm-project/guidellm:0.4.2&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">command&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">guidellm&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&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">target&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">http://vllm-svc.inference.svc.cluster.local:8000&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">rate-type&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">concurrent&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">rate&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="s2">&amp;#34;4&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 class="l">max-seconds&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="s2">&amp;#34;60&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 class="l">data&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">prompt_tokens=1024,output_tokens=256&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">env&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">GUIDELLM_ENV&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">value&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">production&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">guidellm-sweep&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">ghcr.io/vllm-project/guidellm:0.4.2&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">command&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">guidellm&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&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">target&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">http://vllm-svc.inference.svc.cluster.local:8000&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">rate-type&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">sweep&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">max-seconds&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="s2">&amp;#34;120&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 class="l">data&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">prompt_tokens=1024,output_tokens=256&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">output-path&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">/results/guidellm-$(BENCH_RUN_ID).json&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">env&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">BENCH_RUN_ID&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">valueFrom&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">fieldRef&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">fieldPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metadata.labels[&amp;#39;bench/run-id&amp;#39;]&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">volumeMounts&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">results&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">aiperf-concurrent&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">nvcr.io/nvidia/aiperf:0.2.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">command&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">aiperf&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">profile&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">url&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">http://vllm-svc.inference.svc.cluster.local:8000/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="l">model&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">meta-llama/Meta-Llama-3-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="l">concurrency&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="s2">&amp;#34;4,8,16,32&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 class="l">input-tokens&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="s2">&amp;#34;1024&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 class="l">output-tokens&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="s2">&amp;#34;256&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 class="l">num-requests&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="s2">&amp;#34;200&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 class="l">output&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">/results/aiperf-$(BENCH_RUN_ID).json&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">volumeMounts&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">results&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&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">name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">scorecard-exporter&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">python:3.12-slim&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">command&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">python&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">/scripts/export_scorecard.py&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">run-id&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="s2">&amp;#34;$(BENCH_RUN_ID)&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 class="l">prometheus&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">http://prometheus.monitoring.svc.cluster.local: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="l">output&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">/results/scorecard-$(BENCH_RUN_ID).json&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">env&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">BENCH_RUN_ID&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">valueFrom&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">fieldRef&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">fieldPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">metadata.labels[&amp;#39;bench/run-id&amp;#39;]&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">volumeMounts&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">results&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">mountPath&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">/results&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El nombre del Job (&lt;code>bench-llama3-70b-fp8-h100x4-20260616&lt;/code>) es el identificador único de la corrida. Cambiarlo es suficiente para registrar una variante. Los labels son los metadatos que el &lt;code>scorecard-exporter&lt;/code> lee para enriquecer el JSON de salida.&lt;/p>
&lt;hr>
&lt;h2 id="capa-de-rendimiento-guidellm-y-aiperf">Capa de rendimiento: GuideLLM y AIPerf&lt;/h2>
&lt;h3 id="guidellm--el-sweep-dirigido-por-slo">GuideLLM — el sweep dirigido por SLO&lt;/h3>
&lt;p>GuideLLM (proyecto vLLM) genera patrones de tráfico realistas —synchronous, concurrent, poisson, throughput, &lt;strong>sweep&lt;/strong>— y captura distribuciones completas de TTFT e ITL (&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat Developer&lt;/a>). El modo &lt;code>sweep&lt;/code> barre de idle a saturación en 10 rondas e identifica el &lt;strong>codo&lt;/strong>: la carga máxima donde el goodput ≈ throughput bajo el SLO declarado. La salida es JSON/CSV con todos los percentiles por ronda, lista para versionarse.&lt;/p>
&lt;p>Para el harness, el SLO de referencia del banco es:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Umbral&lt;/th>
&lt;th>Percentil&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>TTFT&lt;/td>
&lt;td>500 ms&lt;/td>
&lt;td>P99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ITL (TPOT)&lt;/td>
&lt;td>50 ms/tok&lt;/td>
&lt;td>P95&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tasa de error&lt;/td>
&lt;td>0,5 %&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El comando del Job ya aparece en el YAML anterior. La salida JSON incluye por ronda: tasa (req/s), TTFT (P50/P95/P99), ITL (P50/P95/P99), throughput (tok/s) y goodput (tok/s). El goodput bajo el SLO del codo es el valor que entra en el scorecard.&lt;/p>
&lt;p>Para la capa de verificación cruzada de datos y comparabilidad con resultados publicados, véase el post &lt;a href="https://blog.lo0.es/posts/guidellm-validacion-slo-bajo-carga/">GuideLLM a fondo&lt;/a>.&lt;/p>
&lt;h3 id="aiperf--el-perfilador-de-concurrencia-de-nvidia">AIPerf — el perfilador de concurrencia de NVIDIA&lt;/h3>
&lt;p>AIPerf (sucesor de GenAI-Perf, repositorio &lt;code>ai-dynamo/aiperf&lt;/code>) mide TTFT, ITL, throughput y latencias en distribución a concurrencias fijas (&lt;a href="https://github.com/ai-dynamo/aiperf">GitHub ai-dynamo/aiperf&lt;/a>). Donde GuideLLM da el sweep automático hasta el codo, AIPerf da el perfil detallado a concurrencias concretas (4, 8, 16, 32 en el ejemplo): permite caracterizar la curva throughput-latencia punto a punto.&lt;/p>
&lt;p>Los dos son complementarios: GuideLLM halla el codo de forma automática; AIPerf lo confirma y caracteriza el comportamiento en el entorno de ese codo. Ambas salidas van al volumen de resultados con el mismo &lt;code>BENCH_RUN_ID&lt;/code>. El análisis profundo de AIPerf/GenAI-Perf está en &lt;a href="https://blog.lo0.es/posts/nvidia-genai-perf-a-fondo/">GenAI-Perf a fondo&lt;/a>.&lt;/p>
&lt;p>Métricas que el harness extrae de AIPerf para el scorecard:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># TTFT P99 a concurrencia 16 (la más cercana al codo del sweep):&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aiperf profile ... --concurrency &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.results[] | select(.concurrency==16) | .ttft_ms.p99&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Goodput (tok/s) a concurrencia 16:&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">aiperf profile ... --concurrency &lt;span class="m">16&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.results[] | select(.concurrency==16) | .output_token_throughput&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;hr>
&lt;h2 id="capa-de-coste-opencost-y-litellm">Capa de coste: OpenCost y LiteLLM&lt;/h2>
&lt;h3 id="opencost--el-denominador-en-euros-por-gpu-hora">OpenCost — el denominador en euros por GPU-hora&lt;/h3>
&lt;p>OpenCost (CNCF incubating, &lt;a href="https://opencost.io/">opencost.io&lt;/a>) asigna coste de Kubernetes a namespace, label, pod y contenedor en tiempo real (&lt;a href="https://github.com/opencost/opencost">GitHub opencost/opencost&lt;/a>). Para el harness, OpenCost resuelve la pregunta: &lt;strong>¿cuánto cuesta en euros por hora el pod &lt;code>vllm-svc&lt;/code> durante la ventana del experimento?&lt;/strong>&lt;/p>
&lt;p>El harness llama a la API REST de OpenCost al terminar el experimento:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Coste del namespace inference en la ventana del experimento (1 hora)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s &lt;span class="s2">&amp;#34;http://opencost.monitoring.svc.cluster.local:9003/allocation&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;window=2026-06-16T12:00:00Z,2026-06-16T13:00:00Z&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;aggregate=namespace&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;namespace=inference&amp;#39;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data[0].inference.totalCost&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El coste devuelto (en EUR, configurado con precios reales del nodo) se divide por el throughput medido en esa misma ventana para obtener el CPM:&lt;/p>
$$\text{CPM} = \frac{\text{coste}_\text{namespace/h} \times 10^6}{\text{goodput (tok/s)} \times 3\,600}$$
&lt;p>El post &lt;a href="https://blog.lo0.es/posts/opencost-cost-allocation-kubernetes/">OpenCost: cost allocation en Kubernetes&lt;/a> detalla la configuración de precios y la asignación por label de GPU.&lt;/p>
&lt;h3 id="litellm--el-token-counter-por-petición">LiteLLM — el token counter por petición&lt;/h3>
&lt;p>LiteLLM (&lt;a href="https://www.litellm.ai/">litellm.ai&lt;/a>, &lt;a href="https://github.com/BerriAI/litellm">GitHub BerriAI/litellm&lt;/a>) actúa como proxy OpenAI-compatible con contabilidad de tokens por petición, modelo y equipo. En el harness, GuideLLM y AIPerf apuntan al endpoint de LiteLLM (que a su vez reenvía a vLLM): cada petición queda registrada con &lt;code>prompt_tokens&lt;/code>, &lt;code>completion_tokens&lt;/code> y &lt;code>cost&lt;/code> (usando el pricing custom configurado para el nodo on-prem).&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"># litellm-config.yaml (fragmento de pricing custom on-prem)&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">model_list&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">model_name&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">llama3-70b-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">litellm_params&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">model&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">openai/meta-llama/Meta-Llama-3-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">api_base&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">http://vllm-svc.inference.svc.cluster.local:8000/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">api_key&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="l">sk-dummy&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">input_cost_per_token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.00000109&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c"># 1,09 EUR/1M tok on-prem&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_cost_per_token&lt;/span>&lt;span class="p">:&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="m">0.00000109&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Los registros de LiteLLM se exportan a Prometheus como &lt;code>litellm_request_total_tokens&lt;/code> y &lt;code>litellm_spend_metric_total&lt;/code>, que el scorecard exporter consume para calcular el CPM real por tipo de token (input vs output).&lt;/p>
&lt;hr>
&lt;h2 id="capa-de-energía-dcgm-y-kepler">Capa de energía: DCGM y Kepler&lt;/h2>
&lt;h3 id="dcgm-exporter--potencia-y-energía-gpu">DCGM Exporter — potencia y energía GPU&lt;/h3>
&lt;p>DCGM Exporter (&lt;a href="https://github.com/NVIDIA/dcgm-exporter">GitHub NVIDIA/dcgm-exporter&lt;/a>, &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html">docs&lt;/a>) expone métricas de GPU en &lt;code>/metrics&lt;/code> para Prometheus como DaemonSet en los nodos GPU. Las dos métricas de energía que usa el harness:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica DCGM&lt;/th>
&lt;th>Tipo&lt;/th>
&lt;th>Unidad&lt;/th>
&lt;th>Uso en el harness&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_POWER_USAGE&lt;/code>&lt;/td>
&lt;td>gauge&lt;/td>
&lt;td>W&lt;/td>
&lt;td>potencia instantánea por GPU&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code>&lt;/td>
&lt;td>counter&lt;/td>
&lt;td>mJ&lt;/td>
&lt;td>energía acumulada desde boot&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Para calcular la energía consumida durante la ventana del experimento (sin la línea de base idle), el harness hace la diferencia del counter antes y después del sweep:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Despliegue como DaemonSet (fragmento del chart oficial)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm repo add gpu-helm-charts &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> https://nvidia.github.io/dcgm-exporter/helm-charts
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">helm install dcgm-exporter gpu-helm-charts/dcgm-exporter &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --namespace monitoring &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set serviceMonitor.enabled&lt;span class="o">=&lt;/span>&lt;span class="nb">true&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --set serviceMonitor.interval&lt;span class="o">=&lt;/span>15s
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El campo &lt;code>DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/code> (en mJ) permite calcular la energía del experimento con exactitud de contador de hardware, no de estimación:&lt;/p>
$$\text{energía experimento (J)} = \left(\text{counter}_\text{fin} - \text{counter}_\text{inicio}\right) \times 10^{-3}$$
$$\text{Wh/token} = \frac{\text{energía experimento (J)}}{3\,600 \times \text{tokens generados}}$$
&lt;h3 id="kepler--energía-a-nivel-de-pod">Kepler — energía a nivel de pod&lt;/h3>
&lt;p>Kepler (CNCF sandbox, &lt;a href="https://github.com/sustainable-computing-io/kepler">GitHub sustainable-computing-io/kepler&lt;/a>) usa eBPF para estimar el consumo energético a nivel de contenedor y pod, exportando &lt;code>kepler_container_joules_total&lt;/code> a Prometheus (&lt;a href="https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/">Red Hat Emerging Technologies&lt;/a>). Combina RAPL (CPU/DRAM), NVML (GPU) y modelos de regresión cuando no hay sensores disponibles.&lt;/p>
&lt;p>En el harness, Kepler complementa a DCGM: DCGM da la medición de hardware de la GPU (más precisa para cargas GPU-intensivas como la inferencia LLM), mientras Kepler atribuye la energía al pod de vLLM específico (incluida la contribución de CPU del nodo). La métrica principal que consume el harness:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Energía del pod vllm-svc durante el experimento (J)&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="kr">increase&lt;/span>&lt;span class="o">(&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="nv">kepler_container_joules_total&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="nl">container_namespace&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">inference&lt;/span>&lt;span class="p">&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 class="nl">container_name&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">vllm&lt;/span>&lt;span class="p">&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 class="p">}[&lt;/span>&lt;span class="err">${BENCH_DURATION}&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="o">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El análisis comparativo de DCGM vs Kepler vs Zeus está en &lt;a href="https://blog.lo0.es/posts/herramientas-energia-deploy-precision-overhead/">Herramientas de energía: deploy, precisión y overhead&lt;/a>.&lt;/p>
&lt;h3 id="referencia-mlperf-power">Referencia MLPerf Power&lt;/h3>
&lt;p>MLPerf Power (&lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">MLCommons&lt;/a>, paper IEEE HPCA 2025) establece el protocolo de medición de eficiencia energética de sistemas ML con medición externa de alta precisión. El banco propio &lt;strong>no es un submission MLPerf&lt;/strong> (requiere vatímetros externos y revisión por comité), pero sus resultados publicados son la referencia de calibración: si el harness da un resultado del mismo orden que el submission MLPerf del mismo hardware con el mismo modelo, la medición es plausible. Si difiere más de un factor 2, hay un problema metodológico. Véase el post &lt;a href="https://blog.lo0.es/posts/mlperf-power-eficiencia-energetica/">MLPerf Power: eficiencia energética&lt;/a> para los datos de referencia actuales.&lt;/p>
&lt;hr>
&lt;h2 id="unificación-de-métricas-las-queries-promql">Unificación de métricas: las queries PromQL&lt;/h2>
&lt;p>El &lt;code>scorecard-exporter&lt;/code> es el contenedor del Job que, al terminar GuideLLM y AIPerf, recoge todas las métricas de Prometheus y construye el JSON de la fila del scorecard. Las queries clave:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-promql" data-lang="promql">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Potencia media GPU durante el sweep (W) — nodo 4×H100&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="kr">avg_over_time&lt;/span>&lt;span class="o">(&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="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_POWER_USAGE&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="err">${BENCH_DURATION}:&lt;/span>&lt;span class="s">15s&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="o">)&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="c1"># Energía total GPU durante el sweep (mJ → convertir a J)&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="o">(&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="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">offset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mi">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="o">-&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="k">sum&lt;/span>&lt;span class="o">(&lt;/span>&lt;span class="nv">DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">Hostname&lt;/span>&lt;span class="o">=~&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">gpu-node-.*&lt;/span>&lt;span class="p">&amp;#34;}&lt;/span>&lt;span class="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">offset&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="err">$&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="err">BENCH_DURATION&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="o">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">*&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="mf">0.001&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="c1"># Tokens generados durante el sweep (desde LiteLLM)&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="kr">increase&lt;/span>&lt;span class="o">(&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="nv">litellm_request_total_tokens&lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="nl">model&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">llama3-70b-fp8&lt;/span>&lt;span class="p">&amp;#34;,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nl">token_type&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="p">&amp;#34;&lt;/span>&lt;span class="s">completion&lt;/span>&lt;span class="p">&amp;#34;}[&lt;/span>&lt;span class="err">${BENCH_DURATION}&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="o">)&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="c1"># Coste del namespace inference en la ventana (desde OpenCost API)&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="c1"># → llamada REST al iniciar y al finalizar el Job, diferencia de accrual&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>La variable &lt;code>${BENCH_DURATION}&lt;/code> es la duración real del sweep (en formato Prometheus, ej. &lt;code>22m&lt;/code>), que el exporter calcula como &lt;code>end_ts - start_ts&lt;/code> y sustituye en todas las queries.&lt;/p>
&lt;p>El JSON de salida de cada corrida tiene esta estructura:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;run_id&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;20260616T1200&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;metadata&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;meta-llama/Meta-Llama-3-70B-Instruct&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;engine&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;vllm-0.9.1&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;precision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;fp8&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hardware&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;4xH100-SXM-80GB&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;isl_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;osl_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tokenizer&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;meta-llama-3-tokenizer-v3&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slo_ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">500&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;slo_itl_p95_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">50&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bench_tool_guidellm&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.4.2&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;bench_tool_aiperf&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.2.0&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;dcgm_exporter&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;3.3.9&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;kepler&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;0.10.2&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;performance&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;goodput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3120&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">487&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;itl_p95_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">42&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;throughput_peak_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3890&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;elbow_concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">28&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cost&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cpm_eur_1m&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.97&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;gpu_cost_eur_h&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">10.8&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;cost_window_eur&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">3.24&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;energy&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wh_per_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.00044&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;j_per_token&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.58&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;power_mean_w&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">4924&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;energy_total_kwh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.287&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;pue&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">1.4&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;wh_per_token_pue_adjusted&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">0.000616&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;carbon&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;grid_intensity_gco2_kwh&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">40&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;co2_per_1m_tokens_g&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mf">24.6&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Todos los campos son calculados a partir de las mismas ventanas temporales. El JSON se versiona en git junto al código del harness. La reproducción de cualquier fila del scorecard es: &lt;code>git checkout &amp;lt;run-id&amp;gt; &amp;amp;&amp;amp; kubectl apply -f job.yaml&lt;/code>.&lt;/p>
&lt;hr>
&lt;h2 id="el-scorecard-de-3-ejes-tabla-e-interpretación">El scorecard de 3 ejes: tabla e interpretación&lt;/h2>
&lt;p>La siguiente tabla ilustra cómo se comparan cinco configuraciones del mismo modelo (Llama 3 70B) sobre el mismo nodo de referencia (4×H100 SXM) con el harness. Las cifras son &lt;strong>ilustrativas de orden de magnitud&lt;/strong> (el banco las rellena con mediciones reales):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Config&lt;/th>
&lt;th>Motor&lt;/th>
&lt;th>Precisión&lt;/th>
&lt;th>CPM (€/1M)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;th>Wh/tok&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P95 (ms)&lt;/th>
&lt;th>CO2 /1M (g, FR)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>A&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP16&lt;/td>
&lt;td>1,64&lt;/td>
&lt;td>1.890&lt;/td>
&lt;td>0,00074&lt;/td>
&lt;td>498&lt;/td>
&lt;td>49&lt;/td>
&lt;td>29,6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>B&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>0,97&lt;/td>
&lt;td>3.120&lt;/td>
&lt;td>0,00044&lt;/td>
&lt;td>487&lt;/td>
&lt;td>42&lt;/td>
&lt;td>17,6&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>C&lt;/strong>&lt;/td>
&lt;td>SGLang 0.4&lt;/td>
&lt;td>FP8&lt;/td>
&lt;td>0,88&lt;/td>
&lt;td>3.410&lt;/td>
&lt;td>0,00041&lt;/td>
&lt;td>421&lt;/td>
&lt;td>38&lt;/td>
&lt;td>16,4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>D&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8, ISL 512&lt;/td>
&lt;td>1,31&lt;/td>
&lt;td>2.240&lt;/td>
&lt;td>0,00056&lt;/td>
&lt;td>294&lt;/td>
&lt;td>31&lt;/td>
&lt;td>22,4&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>E&lt;/strong>&lt;/td>
&lt;td>vLLM 0.9&lt;/td>
&lt;td>FP8, max-batch 128&lt;/td>
&lt;td>0,84&lt;/td>
&lt;td>3.590&lt;/td>
&lt;td>0,00040&lt;/td>
&lt;td>721&lt;/td>
&lt;td>55&lt;/td>
&lt;td>16,0&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cómo se lee la tabla:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>B vs A&lt;/strong>: FP8 sobre FP16 reduce el CPM un 41 % y la energía por token un 41 % por el mismo throughput más alto. Ambos cumplen el SLO (TTFT P99 &amp;lt; 500 ms, ITL P95 &amp;lt; 50 ms). B domina a A en los tres ejes: es Pareto-superior.&lt;/li>
&lt;li>&lt;strong>C vs B&lt;/strong>: SGLang da goodput un 9 % mayor y CPM un 9 % menor con FP8. Ambos cumplen el SLO. C es Pareto-superior a B (si el motor es indiferente para el stack).&lt;/li>
&lt;li>&lt;strong>D vs B&lt;/strong>: ISL más corto (512 tok) baja la latencia (TTFT P99 294 ms vs 487 ms) pero baja el goodput y sube el CPM. Si el caso de uso exige TTFT &amp;lt; 300 ms, D es el candidato; si TTFT &amp;lt; 500 ms es suficiente, B es mejor en coste y energía.&lt;/li>
&lt;li>&lt;strong>E vs C&lt;/strong>: &lt;code>max-batch 128&lt;/code> sube el goodput (+5 %) y baja el CPM, pero el TTFT P99 rompe el SLO (721 ms &amp;gt; 500 ms) y el ITL P95 también (55 ms &amp;gt; 50 ms). E tiene el mejor throughput bruto &lt;strong>pero está fuera del SLO&lt;/strong>: no es un candidato válido para el caso de uso de chat interactivo.&lt;/li>
&lt;/ul>
&lt;p>La &lt;strong>frontera de Pareto&lt;/strong> del scorecard, bajo el SLO declarado, incluye solo A, B, C y D (E queda fuera por SLO roto). De esas cuatro, C domina a B que domina a A. D solo entra en la frontera si el caso de uso exige TTFT P99 &amp;lt; 300 ms. La decisión no es un número; es la fila entera más el SLO.&lt;/p>
&lt;hr>
&lt;h2 id="la-frontera-de-pareto-multi-objetivo">La frontera de Pareto multi-objetivo&lt;/h2>
&lt;p>Con tres métricas de minimización —CPM (€/1M tok), Wh/tok y TTFT P99 (ms)— la frontera de Pareto se define como el conjunto de configuraciones donde ninguna domina a otra en los tres ejes simultáneamente. Formalmente, la config \( i \) &lt;strong>domina&lt;/strong> a la config \( j \) si:&lt;/p>
$$\text{CPM}_i \leq \text{CPM}_j \;\land\; \text{Wh/tok}_i \leq \text{Wh/tok}_j \;\land\; \text{TTFT}_{i} \leq \text{TTFT}_{j}$$
&lt;p>con al menos una desigualdad estricta. El scorecard permite calcularla sobre el conjunto de corridas con una operación vectorial sencilla. En el ejemplo de la tabla, la frontera bajo SLO (con E excluido) es \(\{C, D\}\): C domina en coste/energía/goodput; D domina en latencia. Entre C y D la elección depende del SLO de TTFT del caso de uso concreto.&lt;/p>
&lt;div class="diagram" style="max-width:700px;margin:1rem auto;">
&lt;svg viewBox="0 0 700 320" role="img" aria-label="Frontera de Pareto en dos ejes: CPM EUR por millon de tokens en eje Y y goodput tok por s en eje X; los puntos A B C D E marcados; E excluido por SLO roto; la curva de Pareto pasa por C y D" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.ts{font:11px sans-serif;fill:currentColor}.tl{font:600 12px sans-serif;fill:currentColor}.pt{fill:currentColor}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}&lt;/style>
&lt;line class="ax" x1="70" y1="30" x2="70" y2="270"/>
&lt;line class="ax" x1="70" y1="270" x2="660" y2="270"/>
&lt;text x="10" y="155" class="ts" transform="rotate(-90 10 155)">CPM (EUR/1M tok) ↑ peor&lt;/text>
&lt;text x="320" y="300" class="ts">goodput (tok/s) → mejor&lt;/text>
&lt;text x="70" y="26" class="ts">1,70&lt;/text>
&lt;text x="70" y="80" class="ts">1,40&lt;/text>
&lt;text x="70" y="134" class="ts">1,10&lt;/text>
&lt;text x="70" y="188" class="ts">0,90&lt;/text>
&lt;text x="70" y="242" class="ts">0,84&lt;/text>
&lt;text x="115" y="284" class="ts">1.800&lt;/text>
&lt;text x="263" y="284" class="ts">2.200&lt;/text>
&lt;text x="410" y="284" class="ts">3.100&lt;/text>
&lt;text x="520" y="284" class="ts">3.400&lt;/text>
&lt;text x="600" y="284" class="ts">3.600&lt;/text>
&lt;circle class="pt" cx="130" cy="55" r="5"/>
&lt;text x="140" y="52" class="tl">A (FP16)&lt;/text>
&lt;circle class="pt" cx="380" cy="170" r="5"/>
&lt;text x="390" y="167" class="tl">B (FP8)&lt;/text>
&lt;circle class="pt" cx="500" cy="152" r="5"/>
&lt;text x="510" y="148" class="tl">C (SGLang FP8) ★&lt;/text>
&lt;circle class="pt" cx="270" cy="195" r="5"/>
&lt;text x="280" y="192" class="tl">D (ISL 512)&lt;/text>
&lt;circle class="pt" cx="570" cy="138" r="4" opacity="0.4"/>
&lt;text x="580" y="135" class="ts" opacity="0.4">E (SLO roto)&lt;/text>
&lt;path class="dsh" d="M130,55 L270,195 L500,152"/>
&lt;text x="190" y="148" class="ts">frontera Pareto&lt;/text>
&lt;text x="72" y="286" class="ts">(A domina solo en latencia; C+D en la frontera bajo SLO)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="diseño-reproducible-los-doce-metadatos-obligatorios">Diseño reproducible: los doce metadatos obligatorios&lt;/h2>
&lt;p>El checklist que cierra el dossier de sesgo (&lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo y reproducibilidad&lt;/a>). Para que el JSON del harness sea auditable, los doce campos deben estar presentes en cada corrida:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>#&lt;/th>
&lt;th>Campo&lt;/th>
&lt;th>Ejemplo&lt;/th>
&lt;th>Por qué es obligatorio&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>Versión del motor de serving&lt;/td>
&lt;td>&lt;code>vllm-0.9.1&lt;/code>&lt;/td>
&lt;td>el mismo modelo puede rendir diferente en distintas versiones&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>2&lt;/td>
&lt;td>Versión del modelo (commit/tag)&lt;/td>
&lt;td>&lt;code>meta-llama/Meta-Llama-3-70B-Instruct@sha256:abc&lt;/code>&lt;/td>
&lt;td>evita ambigüedad entre variantes del mismo nombre&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>Precisión&lt;/td>
&lt;td>&lt;code>fp8&lt;/code>&lt;/td>
&lt;td>FP16 vs FP8 cambia throughput y latencia&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>4&lt;/td>
&lt;td>Tokenizador y versión&lt;/td>
&lt;td>&lt;code>meta-llama-3-tokenizer-v3&lt;/code>&lt;/td>
&lt;td>el ISL/OSL en tokens depende del tokenizador&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>ISL (input sequence length)&lt;/td>
&lt;td>&lt;code>1024&lt;/code> tok&lt;/td>
&lt;td>cambia el tiempo de prefill y el codo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>OSL (output sequence length)&lt;/td>
&lt;td>&lt;code>256&lt;/code> tok&lt;/td>
&lt;td>cambia el tiempo de decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>Hardware (modelo y cantidad)&lt;/td>
&lt;td>&lt;code>4×H100-SXM-80GB&lt;/code>&lt;/td>
&lt;td>base de cualquier comparación&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>Concurrencia máxima probada&lt;/td>
&lt;td>&lt;code>32&lt;/code>&lt;/td>
&lt;td>define el rango del sweep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>Versión de la herramienta de bench&lt;/td>
&lt;td>&lt;code>guidellm-0.4.2&lt;/code>, &lt;code>aiperf-0.2.0&lt;/code>&lt;/td>
&lt;td>distintas versiones pueden dar métricas distintas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>SLO declarado&lt;/td>
&lt;td>&lt;code>TTFT P99 &amp;lt; 500 ms, ITL P95 &amp;lt; 50 ms&lt;/code>&lt;/td>
&lt;td>el codo depende del SLO; sin él no hay goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>11&lt;/td>
&lt;td>Duración de warmup&lt;/td>
&lt;td>&lt;code>60 s&lt;/code>&lt;/td>
&lt;td>sin warmup el KV cache está frío y las primeras rondas son sesgadas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>12&lt;/td>
&lt;td>Versión del harness / run-id&lt;/td>
&lt;td>&lt;code>20260616T1200&lt;/code>&lt;/td>
&lt;td>identifica la corrida para reproducirla con &lt;code>git checkout&lt;/code>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Un scorecard sin cualquiera de estos doce campos es una anécdota. Con los doce, es un dato auditable. El run-id en el nombre del Job (&lt;code>bench-llama3-70b-fp8-h100x4-20260616&lt;/code>) incorpora implícitamente los campos 1-3 y 7, forzando que el nombre del Job cambie con cada variante — lo que hace imposible solapar corridas en el mismo namespace.&lt;/p>
&lt;hr>
&lt;h2 id="idempotencia-y-versionado-de-resultados">Idempotencia y versionado de resultados&lt;/h2>
&lt;p>El Job de Kubernetes es idempotente: si se vuelve a aplicar el mismo manifiesto (mismo nombre), el Job ya existe y Kubernetes no lo relanza (política &lt;code>backoffLimit: 0&lt;/code>). Esto evita corridas accidentales duplicadas. Para relanzar, hay que borrar el Job (&lt;code>kubectl delete job &amp;lt;nombre&amp;gt;&lt;/code>) o cambiar el &lt;code>run-id&lt;/code>.&lt;/p>
&lt;p>Los resultados se versionan con la siguiente convención de directorios en el repositorio git del harness:&lt;/p>
&lt;pre tabindex="0">&lt;code>results/
20260616T1200/
guidellm-20260616T1200.json
aiperf-20260616T1200.json
scorecard-20260616T1200.json
20260617T0900/
guidellm-20260617T0900.json
aiperf-20260617T0900.json
scorecard-20260617T0900.json
scorecard-aggregate.csv # todas las filas, para análisis y gráficas
&lt;/code>&lt;/pre>&lt;p>El &lt;code>scorecard-aggregate.csv&lt;/code> es la tabla del scorecard en formato plano: cada fila es una corrida, cada columna un campo del JSON. Se genera con:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># Genera el CSV agregado a partir de todos los JSON en results/&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/aggregate_scorecards.py results/ &amp;gt; scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>El CSV es lo que entra en las gráficas de Pareto, en los informes y en las decisiones de arquitectura. Está versionado en git; su diff es el cambio de configuración.&lt;/p>
&lt;hr>
&lt;h2 id="cómo-se-conecta-con-los-otros-posts-de-la-serie">Cómo se conecta con los otros posts de la serie&lt;/h2>
&lt;p>El harness no es un artículo autónomo: es la síntesis de los veintiocho artículos. Cada herramienta tiene su deep dive:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Rendimiento&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/guidellm-validacion-slo-bajo-carga/">GuideLLM a fondo&lt;/a> — sweep, SLO, goodput, codo; &lt;a href="https://blog.lo0.es/posts/nvidia-genai-perf-a-fondo/">GenAI-Perf / AIPerf a fondo&lt;/a> — concurrencia, rate sweep, métricas de distribución.&lt;/li>
&lt;li>&lt;strong>Coste&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/opencost-cost-allocation-kubernetes/">OpenCost: cost allocation en Kubernetes&lt;/a> — asignación por namespace/label, precios custom on-prem.&lt;/li>
&lt;li>&lt;strong>Energía&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/herramientas-energia-deploy-precision-overhead/">Herramientas de energía: deploy, precisión y overhead&lt;/a> — DCGM vs Kepler vs Zeus, overhead de instrumentación, precisión relativa; &lt;a href="https://blog.lo0.es/posts/mlperf-power-eficiencia-energetica/">MLPerf Power: eficiencia energética&lt;/a> — la referencia externa de calibración.&lt;/li>
&lt;li>&lt;strong>Marco teórico&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">Los tres ejes: coste, rendimiento y energía&lt;/a> — la identidad CPM/energía/throughput que justifica medir juntos.&lt;/li>
&lt;li>&lt;strong>Sesgo&lt;/strong>: &lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo y reproducibilidad en el benchmarking&lt;/a> — los doce sesgos que el harness elimina.&lt;/li>
&lt;/ul>
&lt;p>El harness cierra el dossier de la serie porque hace posible la promesa del artículo de apertura: &amp;ldquo;cuando alguien rebata un número de la propuesta, la respuesta no es &amp;rsquo;lo dice un blog&amp;rsquo;, sino &amp;rsquo;este es el banco, esta la metodología, reprodúcelo&amp;rsquo;&amp;rdquo;. Con el run-id y el repo git, reproducir es un comando.&lt;/p>
&lt;hr>
&lt;h2 id="flujo-completo-de-una-sesión-de-benchmarking">Flujo completo de una sesión de benchmarking&lt;/h2>
&lt;p>El procedimiento operativo estándar del harness, de principio a fin:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 1. Asegurarse de que el endpoint de inferencia está activo&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl get svc vllm-svc -n inference
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 2. Verificar que DCGM y Kepler están scrapeando&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">curl -s http://prometheus.monitoring.svc.cluster.local:9090/api/v1/query &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data-urlencode &lt;span class="s1">&amp;#39;query=DCGM_FI_DEV_POWER_USAGE&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> jq &lt;span class="s1">&amp;#39;.data.result | length&amp;#39;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 3. Registrar el counter de energía ANTES del experimento&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">PRE_ENERGY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl &lt;span class="nb">exec&lt;/span> -n monitoring dcgm-exporter-xxx -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -s localhost:9400/metrics &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> grep DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{sum+=$2} END{print sum}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 4. Lanzar el Job (nombre único por corrida)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl apply -f jobs/bench-llama3-70b-fp8-h100x4-20260616.yaml
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 5. Esperar a que termine&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl &lt;span class="nb">wait&lt;/span> --for&lt;span class="o">=&lt;/span>&lt;span class="nv">condition&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="nb">complete&lt;/span> job/bench-llama3-70b-fp8-h100x4-20260616 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> -n benchmark --timeout&lt;span class="o">=&lt;/span>3600s
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 6. Registrar el counter de energía DESPUÉS&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="nv">POST_ENERGY&lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="k">$(&lt;/span>kubectl &lt;span class="nb">exec&lt;/span> -n monitoring dcgm-exporter-xxx -- &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> curl -s localhost:9400/metrics &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> &lt;span class="p">|&lt;/span> grep DCGM_FI_DEV_TOTAL_ENERGY_CONSUMPTION &lt;span class="p">|&lt;/span> awk &lt;span class="s1">&amp;#39;{sum+=$2} END{print sum}&amp;#39;&lt;/span>&lt;span class="k">)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 7. Recoger los resultados del volumen&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">kubectl cp benchmark/bench-run-pod:/results/ ./results/20260616T1200/
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 8. Calcular la energía del experimento (mJ → kWh)&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/calc_energy.py &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --pre &lt;span class="nv">$PRE_ENERGY&lt;/span> --post &lt;span class="nv">$POST_ENERGY&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --run-id 20260616T1200
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 9. Añadir fila al CSV agregado&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">python scripts/aggregate_scorecards.py results/ &amp;gt; scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="c1"># 10. Versionar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git add results/20260616T1200/ scorecard-aggregate.csv
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">git commit -m &lt;span class="s2">&amp;#34;bench: llama3-70b fp8 4xH100 ISL1024 OSL256 (20260616T1200)&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Los pasos 6-9 son candidatos a automatizarse como un segundo Job de post-procesado que se lanza al completarse el principal (con &lt;code>ownerReferences&lt;/code> o un CronJob de limpieza). En el estado básico del harness, los pasos manuales son precisamente los que fuerzan la revisión del resultado antes de versionarlo.&lt;/p>
&lt;hr>
&lt;h2 id="coste-del-experimento">Coste del experimento&lt;/h2>
&lt;p>Un sweep de GuideLLM de 10 rondas a 120 segundos por ronda ocupa el nodo ~22-26 minutos. El perfil de AIPerf a 4 concurrencias (200 peticiones cada una) añade ~8-12 minutos. Total por corrida: ~35-40 minutos de nodo 4×H100.&lt;/p>
&lt;p>A coste amortizado de referencia (~10,8 €/h para 4×H100 on-prem):&lt;/p>
$$\text{coste por corrida} \approx 10{,}8 \times \frac{38}{60} \approx 6{,}84 \text{ EUR}$$
&lt;p>A precio cloud europeo de referencia (4 × 2,73 = 10,92 €/h):&lt;/p>
$$\text{coste por corrida (cloud)} \approx 10{,}92 \times \frac{38}{60} \approx 6{,}92 \text{ EUR}$$
&lt;p>Un programa de benchmarking continuo (10 configuraciones × 3 modelos × 2 sweeps/semana) suma ~415 EUR/semana. Por eso el harness incluye sweeps cortos (5 rondas, ~15 minutos) para CI y sweeps completos para releases.&lt;/p>
&lt;hr>
&lt;h2 id="lo-que-el-harness-no-mide-y-por-qué">Lo que el harness no mide (y por qué)&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dimensión ausente&lt;/th>
&lt;th>Motivo&lt;/th>
&lt;th>Dónde se cubre&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Calidad del output (MMLU, HumanEval)&lt;/td>
&lt;td>requiere banco de evaluación de calidad, no de serving&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">Benchmarks de calidad LLM&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput de training&lt;/td>
&lt;td>el harness es exclusivamente para inferencia&lt;/td>
&lt;td>fuera del scope de la serie &amp;ldquo;datos&amp;rdquo;&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste de red y almacenamiento&lt;/td>
&lt;td>OpenCost puede atribuirlo, pero requiere configuración adicional&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/posts/opencost-cost-allocation-kubernetes/">OpenCost: cost allocation&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Intensidad de carbono horaria&lt;/td>
&lt;td>usar ElectricityMaps API + la energía medida&lt;/td>
&lt;td>&lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">Del vatio al carbono: PUE y mix de red&lt;/a>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Latencia de red cliente-servidor&lt;/td>
&lt;td>el Job corre dentro del cluster; latencia WAN requiere cliente externo&lt;/td>
&lt;td>documentar como metadato adicional&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>GuideLLM · GitHub (proyecto vLLM) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&lt;/a>&lt;/li>
&lt;li>GuideLLM · PyPI — &lt;a href="https://pypi.org/project/guidellm/">https://pypi.org/project/guidellm/&lt;/a>&lt;/li>
&lt;li>Red Hat Developer · desplegar y benchmarkear vLLM con GuideLLM en Kubernetes — &lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes&lt;/a>&lt;/li>
&lt;li>Red Hat Developer · GuideLLM: evaluar despliegues LLM para inferencia real — &lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference&lt;/a>&lt;/li>
&lt;li>AIPerf · GitHub (ai-dynamo/aiperf) — &lt;a href="https://github.com/ai-dynamo/aiperf">https://github.com/ai-dynamo/aiperf&lt;/a>&lt;/li>
&lt;li>GenAI-Perf · documentación NVIDIA Triton — &lt;a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html">https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/perf_analyzer/genai-perf/README.html&lt;/a>&lt;/li>
&lt;li>NVIDIA · LLM performance benchmarking con GenAI-Perf — &lt;a href="https://developer.nvidia.com/blog/llm-performance-benchmarking-measuring-nvidia-nim-performance-with-genai-perf">https://developer.nvidia.com/blog/llm-performance-benchmarking-measuring-nvidia-nim-performance-with-genai-perf&lt;/a>&lt;/li>
&lt;li>NVIDIA NIM Benchmarking · métricas (TTFT, ITL, throughput) — &lt;a href="https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html">https://docs.nvidia.com/nim/benchmarking/llm/latest/metrics.html&lt;/a>&lt;/li>
&lt;li>OpenCost · web oficial — &lt;a href="https://opencost.io/">https://opencost.io/&lt;/a>&lt;/li>
&lt;li>OpenCost · GitHub (opencost/opencost) — &lt;a href="https://github.com/opencost/opencost">https://github.com/opencost/opencost&lt;/a>&lt;/li>
&lt;li>OpenCost · CNCF blog (sandbox → incubating) — &lt;a href="https://www.cncf.io/blog/2022/12/06/opencost-a-new-cncf-sandbox-project-for-real-time-kubernetes-cost-monitoring/">https://www.cncf.io/blog/2022/12/06/opencost-a-new-cncf-sandbox-project-for-real-time-kubernetes-cost-monitoring/&lt;/a>&lt;/li>
&lt;li>LiteLLM · web oficial — &lt;a href="https://www.litellm.ai/">https://www.litellm.ai/&lt;/a>&lt;/li>
&lt;li>LiteLLM · GitHub (BerriAI/litellm) — &lt;a href="https://github.com/BerriAI/litellm">https://github.com/BerriAI/litellm&lt;/a>&lt;/li>
&lt;li>LiteLLM · Spend Tracking docs — &lt;a href="https://docs.litellm.ai/docs/proxy/cost_tracking">https://docs.litellm.ai/docs/proxy/cost_tracking&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · documentación oficial — &lt;a href="https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html">https://docs.nvidia.com/datacenter/dcgm/latest/gpu-telemetry/dcgm-exporter.html&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · GitHub — &lt;a href="https://github.com/NVIDIA/dcgm-exporter">https://github.com/NVIDIA/dcgm-exporter&lt;/a>&lt;/li>
&lt;li>NVIDIA DCGM Exporter · métricas CSV — &lt;a href="https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/dcp-metrics-included.csv">https://github.com/NVIDIA/dcgm-exporter/blob/main/etc/dcp-metrics-included.csv&lt;/a>&lt;/li>
&lt;li>Kepler (Kubernetes-based Efficient Power Level Exporter) · GitHub — &lt;a href="https://github.com/sustainable-computing-io/kepler">https://github.com/sustainable-computing-io/kepler&lt;/a>&lt;/li>
&lt;li>Red Hat Emerging Technologies · Introducing Kepler — &lt;a href="https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/">https://next.redhat.com/2023/08/22/introducing-kepler-efficient-power-monitoring-for-kubernetes/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference Datacenter benchmark — &lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">https://mlcommons.org/benchmarks/inference-datacenter/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Power presentado en IEEE HPCA 2025 — &lt;a href="https://mlcommons.org/2025/03/ml-commons-power-hpca/">https://mlcommons.org/2025/03/ml-commons-power-hpca/&lt;/a>&lt;/li>
&lt;li>IEEE Xplore · MLPerf Power paper (μWatts to MWatts) — &lt;a href="https://ieeexplore.ieee.org/document/10946778/">https://ieeexplore.ieee.org/document/10946778/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 results (septiembre 2025) — &lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/&lt;/a>&lt;/li>
&lt;li>Red Hat · Efficient and reproducible LLM inference: MLPerf Inference v5.1 — &lt;a href="https://www.redhat.com/en/blog/efficient-and-reproducible-llm-inference-red-hat-mlperf-inference-v51-results">https://www.redhat.com/en/blog/efficient-and-reproducible-llm-inference-red-hat-mlperf-inference-v51-results&lt;/a>&lt;/li>
&lt;li>BentoML · Beyond tokens-per-second: cost, speed and quality in LLM inference — &lt;a href="https://www.bentoml.com/blog/beyond-tokens-per-second-how-to-balance-speed-cost-and-quality-in-llm-inference">https://www.bentoml.com/blog/beyond-tokens-per-second-how-to-balance-speed-cost-and-quality-in-llm-inference&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>GuideLLM a fondo: validar el SLO bajo carga y dimensionar desde el codo</title><link>https://blog.lo0.es/posts/guidellm-validacion-slo-bajo-carga/</link><pubDate>Sun, 14 Jun 2026 04:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/guidellm-validacion-slo-bajo-carga/</guid><description>&lt;blockquote>
&lt;p>Notación: importes en &lt;strong>euros (N €)&lt;/strong>, decimales con coma. No se usa el símbolo de dólar
(en este sitio es delimitador de fórmula).&lt;/p>
&lt;/blockquote>
&lt;h2 id="qué-cubre-este-artículo">Qué cubre este artículo&lt;/h2>
&lt;p>Tercer artículo del track de &lt;strong>benchmarking&lt;/strong> (B3). En B2 se vio el catálogo de herramientas; aquí
entramos en &lt;strong>GuideLLM&lt;/strong> a fondo, porque es la que responde la pregunta operativa que dimensiona una
plataforma: &lt;strong>¿hasta dónde puedo cargar este motor sin romper el SLO?&lt;/strong> No es un benchmark de
catálogo (&amp;ldquo;X tokens/s&amp;rdquo;), es un &lt;strong>sweep dirigido por SLO&lt;/strong> que encuentra el &lt;strong>codo&lt;/strong> —la capacidad
segura— y del que sale el número de réplicas y el coste por token reales. Veremos sus modos de
carga, cómo se define el SLO, cómo se lee la salida y cómo se convierte el codo en sizing y en
euros. Con comandos reales; sin recomendaciones, solo la mecánica.&lt;/p>
&lt;hr>
&lt;h2 id="por-qué-guidellm">Por qué GuideLLM&lt;/h2>
&lt;p>GuideLLM (proyecto vLLM) genera &lt;strong>patrones de tráfico realistas y configurables&lt;/strong> y captura
&lt;strong>distribuciones completas&lt;/strong> de TTFT, ITL y comportamiento de extremo a extremo, para evaluación
&lt;strong>dirigida por SLO&lt;/strong>, con sweeps reproducibles que identifican el rango de operación seguro
(&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat&lt;/a>).
A diferencia de un micro-bench, &lt;strong>mide el motor, no el cliente&lt;/strong> (carga multi-proceso), y a
diferencia de MLPerf, &lt;strong>dimensiona tu caso concreto&lt;/strong> (tu modelo, tu SLO, tu carga). Es la
herramienta del medio: ni para tunear un flag, ni para comparar fabricantes, sino para &lt;strong>decidir
cuántas GPUs necesitas&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="los-modos-de-carga-rate-types">Los modos de carga (rate-types)&lt;/h2>
&lt;p>GuideLLM ofrece varios modos a través de &lt;code>--rate-type&lt;/code>, y elegir el correcto es la mitad del
trabajo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>&lt;code>--rate-type&lt;/code>&lt;/th>
&lt;th>Qué hace&lt;/th>
&lt;th>Cuándo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>synchronous&lt;/strong>&lt;/td>
&lt;td>una petición cada vez&lt;/td>
&lt;td>latencia base, sin concurrencia&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>concurrent&lt;/strong>&lt;/td>
&lt;td>mantiene N peticiones simultáneas fijas&lt;/td>
&lt;td>medir a una concurrencia concreta&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>poisson&lt;/strong>&lt;/td>
&lt;td>peticiones por segundo según Poisson&lt;/td>
&lt;td>simular tráfico real (llegadas aleatorias)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>throughput&lt;/strong>&lt;/td>
&lt;td>satura el motor al máximo&lt;/td>
&lt;td>capacidad bruta máxima&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>sweep&lt;/strong>&lt;/td>
&lt;td>barrido automático de idle a máximo&lt;/td>
&lt;td>&lt;strong>hallar el codo&lt;/strong> (el más útil)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>En modo &lt;code>concurrent&lt;/code>, el &lt;code>--rate &amp;quot;1,2,4&amp;quot;&lt;/code> lanza tres benchmarks a esas concurrencias
(&lt;a href="https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794">Medium · GuideLLM en OpenShift&lt;/a>).
El modo &lt;strong>poisson&lt;/strong> es el más realista para inferencia online (las peticiones no llegan a intervalos
regulares), y el &lt;strong>sweep&lt;/strong> es el que automatiza la búsqueda del punto de saturación.&lt;/p>
&lt;hr>
&lt;h2 id="el-sweep-automático-de-idle-al-codo">El sweep automático: de idle al codo&lt;/h2>
&lt;p>El modo estrella. Al correr un perfil de &lt;strong>sweep&lt;/strong>, GuideLLM &lt;strong>incrementa automáticamente la tasa de
petición desde idle hasta el máximo throughput a lo largo de 10 rondas&lt;/strong>, y produce un &lt;strong>informe HTML
interactivo&lt;/strong> con latencia y throughput detallados (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat · GuideLLM en Kubernetes&lt;/a>).
Durante el sweep, &lt;strong>al detectar la saturación identifica la iteración anterior —el codo— y la
devuelve como capacidad estimada&lt;/strong>; si no detecta saturación, hay que &lt;strong>extender el sweep más allá
del codo&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="El sweep de GuideLLM: 10 rondas de idle a maximo, el throughput satura y la latencia se dispara, marcando el codo como capacidad segura bajo SLO" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.cv{fill:none;stroke:currentColor;stroke-width:1.6}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="40" x2="60" y2="200"/>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;text x="20" y="120" class="ts" transform="rotate(-90 20 120)">métrica&lt;/text>
&lt;text x="330" y="228" class="ts">rondas del sweep (idle → máximo) →&lt;/text>
&lt;path class="cv" d="M60,190 C200,150 300,118 410,110 C520,104 620,100 700,98"/>
&lt;text x="600" y="92" class="ts">throughput (satura)&lt;/text>
&lt;path class="cv" d="M60,182 C260,178 360,168 430,148 C520,116 600,66 700,44"/>
&lt;text x="600" y="40" class="ts">latencia P99 (se dispara)&lt;/text>
&lt;line class="dsh" x1="430" y1="40" x2="430" y2="200"/>
&lt;text x="392" y="56" class="tl">codo&lt;/text>
&lt;text x="345" y="216" class="ts">capacidad estimada (segura bajo SLO)&lt;/text>
&lt;text x="60" y="245" class="ts">El sweep recorre 10 rondas; el codo es la última ronda antes de que el P99 rompa el SLO. Hay que extenderlo PAST el codo para verlo.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="ejemplo-trabajado-leer-un-sweep-de-10-rondas">Ejemplo trabajado: leer un sweep de 10 rondas&lt;/h2>
&lt;p>Una salida ilustrativa de un sweep sobre un 70B en 8×H100 (SLO: TTFT P99 &amp;lt; 500 ms), por ronda:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Ronda&lt;/th>
&lt;th>Tasa (req/s)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2&lt;/td>
&lt;td>95&lt;/td>
&lt;td>19&lt;/td>
&lt;td>480&lt;/td>
&lt;td>480&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>3&lt;/td>
&lt;td>8&lt;/td>
&lt;td>140&lt;/td>
&lt;td>20&lt;/td>
&lt;td>1.900&lt;/td>
&lt;td>1.900&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>5&lt;/td>
&lt;td>14&lt;/td>
&lt;td>240&lt;/td>
&lt;td>22&lt;/td>
&lt;td>3.100&lt;/td>
&lt;td>3.060&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>6&lt;/td>
&lt;td>17&lt;/td>
&lt;td>460&lt;/td>
&lt;td>24&lt;/td>
&lt;td>3.400&lt;/td>
&lt;td>3.330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>7&lt;/td>
&lt;td>20&lt;/td>
&lt;td>760&lt;/td>
&lt;td>31&lt;/td>
&lt;td>3.700&lt;/td>
&lt;td>2.520&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>9&lt;/td>
&lt;td>26&lt;/td>
&lt;td>1.500&lt;/td>
&lt;td>48&lt;/td>
&lt;td>3.950&lt;/td>
&lt;td>900&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>10&lt;/td>
&lt;td>30&lt;/td>
&lt;td>2.400&lt;/td>
&lt;td>71&lt;/td>
&lt;td>4.000&lt;/td>
&lt;td>380&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cómo se lee: hasta la &lt;strong>ronda 6&lt;/strong> (17 req/s) el TTFT P99 cumple el SLO (460 ms) y el goodput ≈
throughput (3.330 ≈ 3.400). En la &lt;strong>ronda 7&lt;/strong> el P99 ya rompe (760 ms) y el goodput &lt;strong>cae&lt;/strong> a 2.520.
El &lt;strong>codo&lt;/strong> está entre la 6 y la 7: la capacidad segura es la de la ronda 6, &lt;strong>~3.330 tok/s de
goodput&lt;/strong>. Las rondas 9 y 10 dan más throughput bruto (3.950, 4.000) pero con goodput de 900 y 380
—el sistema &amp;ldquo;rinde&amp;rdquo; sirviendo sobre todo peticiones que incumplen—. Quien reporte &amp;ldquo;4.000 tok/s&amp;rdquo;
describe la ronda 10, donde el sistema está roto. El número defendible es &lt;strong>3.330 tok/s bajo TTFT
P99 &amp;lt; 500 ms&lt;/strong>, y ese es el que entra en el sizing. Fíjate también en que el sweep tuvo que llegar
hasta la ronda 10 (P99 de 2.400 ms) para &lt;strong>ver&lt;/strong> dónde rompía: por eso hay que extenderlo más allá
del codo.&lt;/p>
&lt;hr>
&lt;h2 id="definir-el-slo-el-número-que-decide-el-codo">Definir el SLO: el número que decide el codo&lt;/h2>
&lt;p>El &amp;ldquo;codo&amp;rdquo; no es absoluto: depende del &lt;strong>SLO&lt;/strong> que definas. Un SLO típico de inferencia online:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Umbral de ejemplo&lt;/th>
&lt;th>Qué protege&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>TTFT P99&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 500 ms&lt;/td>
&lt;td>la espera al primer token (interactividad)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPOT / ITL P95&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 50 ms/token&lt;/td>
&lt;td>la &amp;ldquo;velocidad de tecleo&amp;rdquo; percibida&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Tasa de error&lt;/strong>&lt;/td>
&lt;td>&amp;lt; 0,1 %&lt;/td>
&lt;td>fiabilidad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El &lt;strong>goodput&lt;/strong> —el throughput que cumple ese SLO— es la métrica que define el codo: la capacidad
segura es la máxima carga donde el goodput ≈ throughput. Cambiar el SLO mueve el codo: un SLO de
TTFT más estricto (200 ms) da un codo más temprano (menos capacidad segura) que uno laxo (1 s). Por
eso &lt;strong>el SLO se fija antes del sweep&lt;/strong>, y se reporta junto al resultado: una capacidad &amp;ldquo;de 3.330
tok/s&amp;rdquo; sin decir bajo qué SLO no significa nada.&lt;/p>
&lt;hr>
&lt;h2 id="ejecución-el-comando">Ejecución: el comando&lt;/h2>
&lt;p>Un sweep dirigido por SLO contra un endpoint vLLM:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">guidellm benchmark &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --target &lt;span class="s2">&amp;#34;http://vllm:8000&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --rate-type sweep &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-seconds &lt;span class="m">120&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data &lt;span class="s2">&amp;#34;prompt_tokens=1024,output_tokens=256&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --output-path resultados.json
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Parámetros clave: &lt;code>--rate-type sweep&lt;/code> (el barrido automático), &lt;code>--data&lt;/code> (la distribución de
longitudes de la carga, que debe parecerse a tu tráfico real), &lt;code>--max-seconds&lt;/code> (duración por ronda),
&lt;code>--target&lt;/code> (el endpoint). Para el SLO, GuideLLM permite definir las restricciones de latencia que
marcan el goodput. La salida va a &lt;code>--output-path&lt;/code> en &lt;strong>JSON, YAML o CSV&lt;/strong>, además del informe HTML
interactivo.&lt;/p>
&lt;hr>
&lt;h2 id="definir-la-carga-el---data-decide-el-resultado">Definir la carga: el &lt;code>--data&lt;/code> decide el resultado&lt;/h2>
&lt;p>El parámetro &lt;code>--data&lt;/code> (la distribución de longitudes de prompt y salida) cambia el codo tanto como el
SLO. Tres formas, de menos a más fiel:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Carga&lt;/th>
&lt;th>&lt;code>--data&lt;/code>&lt;/th>
&lt;th>Fidelidad&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Longitud fija&lt;/td>
&lt;td>&lt;code>prompt_tokens=1024,output_tokens=256&lt;/code>&lt;/td>
&lt;td>baja: el tráfico real no es fijo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distribución sintética&lt;/td>
&lt;td>rangos de longitud&lt;/td>
&lt;td>media&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Trazas reales&lt;/td>
&lt;td>dataset de tu tráfico&lt;/td>
&lt;td>alta: el codo que aplica&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La trampa: un sweep con prompts cortos de longitud fija da un codo optimista que &lt;strong>no se parece a
producción&lt;/strong>, donde los prompts largos dominan el coste de prefill y adelantan el codo. Para
dimensionar de verdad, alimenta GuideLLM con la &lt;strong>distribución real&lt;/strong> de tu tráfico (longitudes de
prompt y salida medidas en producción). El codo de un sweep solo es tan realista como la carga que
le metes; con &lt;code>--data&lt;/code> irreal, dimensionas para un tráfico que no existe.&lt;/p>
&lt;p>Además, el perfil de llegada importa: &lt;code>--rate-type poisson&lt;/code> simula llegadas aleatorias (como el
tráfico real online), mientras que &lt;code>concurrent&lt;/code> mantiene N fijas (como un batch). Un servicio online
medido con &lt;code>concurrent&lt;/code> puede dar un codo distinto al real; para inferencia interactiva, &lt;strong>poisson&lt;/strong>
es el perfil honesto.&lt;/p>
&lt;hr>
&lt;h2 id="la-salida-qué-leer">La salida: qué leer&lt;/h2>
&lt;p>GuideLLM produce reportes &lt;strong>estandarizados y exportables&lt;/strong> para dashboards, análisis y seguimiento de
regresiones, en JSON, YAML y CSV (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).
Lo que importa leer, por ronda del sweep:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT&lt;/strong> (P50, P95, P99) — la latencia al primer token.&lt;/li>
&lt;li>&lt;strong>ITL/TPOT&lt;/strong> (P50, P95, P99) — entre tokens.&lt;/li>
&lt;li>&lt;strong>Throughput&lt;/strong> (req/s y tok/s) — el bruto.&lt;/li>
&lt;li>&lt;strong>Goodput&lt;/strong> — el que cumple el SLO (el número honesto).&lt;/li>
&lt;/ul>
&lt;p>El informe HTML interactivo permite ver las &lt;strong>distribuciones completas&lt;/strong> (no solo medias), que es
donde se ve la cola de latencia que una media oculta. Para el sizing y la comparación, el JSON es lo
que se versiona y se mete en el harness reproducible.&lt;/p>
&lt;hr>
&lt;h2 id="del-codo-al-sizing-y-al-coste">Del codo al sizing y al coste&lt;/h2>
&lt;p>Aquí GuideLLM se conecta con el resto de la serie. Del codo sale el &lt;strong>número de réplicas&lt;/strong> y el
&lt;strong>coste por token&lt;/strong>:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 200" role="img" aria-label="Del codo de GuideLLM al sizing: el goodput por replica, la carga objetivo, el numero de replicas y el coste por token" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#gm)}&lt;/style>
&lt;defs>&lt;marker id="gm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="60" width="160" height="48" rx="6"/>&lt;text x="32" y="80" class="tl">Codo (GuideLLM)&lt;/text>&lt;text x="32" y="97" class="ts">goodput por réplica&lt;/text>
&lt;path class="ar" d="M180,84 L225,84"/>
&lt;rect class="bx" x="225" y="60" width="160" height="48" rx="6"/>&lt;text x="237" y="80" class="tl">Carga objetivo&lt;/text>&lt;text x="237" y="97" class="ts">tok/s en pico (SLO)&lt;/text>
&lt;path class="ar" d="M385,84 L430,84"/>
&lt;rect class="bx" x="430" y="60" width="160" height="48" rx="6"/>&lt;text x="442" y="80" class="tl">Nº de réplicas&lt;/text>&lt;text x="442" y="97" class="ts">carga ÷ goodput&lt;/text>
&lt;path class="ar" d="M590,84 L635,84"/>
&lt;rect class="bx" x="635" y="60" width="125" height="48" rx="6"/>&lt;text x="647" y="80" class="tl">Coste/token&lt;/text>&lt;text x="647" y="97" class="ts">€/h ÷ goodput&lt;/text>
&lt;text x="20" y="150" class="ts">El goodput del codo (no el throughput bruto) es el denominador del sizing y del coste por token.&lt;/text>
&lt;text x="20" y="172" class="ts">Dimensionar con el throughput bruto sobredimensiona el goodput y rompe el SLO en producción.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>El cálculo: si el codo da un &lt;strong>goodput de 3.330 tok/s por réplica&lt;/strong> y tu carga objetivo en pico es
de &lt;strong>20.000 tok/s&lt;/strong>, necesitas &lt;strong>6 réplicas&lt;/strong> (20.000 ÷ 3.330 ≈ 6,0). Y el coste por token sale del
coste de la réplica (de OpenCost, ~11 €/h) dividido por su goodput: ~0,92 €/1M tokens. Es el puente
con el &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a> y con el coste
por token: &lt;strong>el denominador correcto es el goodput del codo&lt;/strong>, no el throughput de catálogo.
Dimensionar con el throughput bruto te deja corto de capacidad útil y rompe el SLO en producción.&lt;/p>
&lt;hr>
&lt;h2 id="despliegue-como-job-de-kubernetes">Despliegue como Job de Kubernetes&lt;/h2>
&lt;p>GuideLLM se ejecuta como un &lt;strong>Job de Kubernetes dentro del cluster&lt;/strong> para benchmarkear modelos
servidos por la plataforma de orquestación (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).
Esto es importante para la fidelidad: el generador de carga corre &lt;strong>dentro del cluster&lt;/strong>, en la misma
red que el endpoint, así que mide el motor sin la latencia de red de un cliente externo. El Job se
parametriza con el target, el rate-type y la carga, y vuelca el JSON a un volumen o a un almacén para
versionarlo. Correrlo como Job también facilita la automatización (CronJob para benchmarking
periódico, o disparado por CI).&lt;/p>
&lt;hr>
&lt;h2 id="validación-de-slo-en-ci-detectar-regresiones">Validación de SLO en CI: detectar regresiones&lt;/h2>
&lt;p>El uso más valioso a medio plazo: un &lt;strong>Job que corre un sweep corto en cada cambio&lt;/strong> (release de
vLLM, cambio de config) y &lt;strong>compara el goodput con la línea base&lt;/strong>. Si el goodput cae más de un
umbral o el codo se adelanta, falla el pipeline. Como GuideLLM exporta JSON estandarizado para
&lt;strong>seguimiento de regresiones&lt;/strong>, comparar dos corridas es trivial. Así una regresión de rendimiento
—que es una regresión de coste y de capacidad— se detecta en el commit, no en producción. No hace
falta el sweep completo en cada commit: un sweep corto que cubra el codo basta para detectar la
regresión; el exhaustivo, para los releases.&lt;/p>
&lt;hr>
&lt;h2 id="comparar-configuraciones-con-sweeps">Comparar configuraciones con sweeps&lt;/h2>
&lt;p>GuideLLM brilla para &lt;strong>decidir entre configuraciones&lt;/strong> corriendo el mismo sweep contra cada una. El
protocolo justo: misma carga (&lt;code>--data&lt;/code>), mismo SLO, mismo hardware, variar solo la config. Ejemplos
de decisión que un sweep resuelve con datos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Decisión&lt;/th>
&lt;th>Qué comparar&lt;/th>
&lt;th>Qué revela el codo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>FP16 vs FP8&lt;/td>
&lt;td>dos despliegues del mismo modelo&lt;/td>
&lt;td>FP8 suele dar codo más alto (más goodput)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>max-num-seqs&lt;/code>&lt;/td>
&lt;td>distintos valores del flag&lt;/td>
&lt;td>el que maximiza goodput bajo SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>vLLM vs SGLang&lt;/td>
&lt;td>dos motores, mismo modelo&lt;/td>
&lt;td>qué motor da más goodput a tu SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>tamaño de KV/precisión&lt;/td>
&lt;td>configs de KV cache&lt;/td>
&lt;td>el efecto en la capacidad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cada sweep da un codo; el codo más alto bajo el mismo SLO gana. Es el experimento controlado que
llena la fila del cuadro de mando (artículo B8): no &amp;ldquo;X es más rápido&amp;rdquo; en abstracto, sino &amp;ldquo;X da Y
tok/s de goodput más bajo este SLO concreto, lo que se traduce en Z réplicas menos y W € menos por
millón de tokens&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h2 id="observar-los-tres-ejes-durante-el-sweep">Observar los tres ejes durante el sweep&lt;/h2>
&lt;p>Un truco que conecta con el track de coste y energía: &lt;strong>mientras corre el sweep, captura las métricas
de GPU con DCGM&lt;/strong>. Así, de cada ronda sacas a la vez el goodput (GuideLLM), la potencia media (DCGM →
J/token) y el coste (precio del nodo → €/token). En una sola campaña de sweep mides los tres ejes del
cuadro de mando para cada punto de operación, y quedan coherentes por construcción (mismo instante,
misma carga). En vez de un benchmark de rendimiento aislado, obtienes la &lt;strong>fila completa&lt;/strong> —coste,
rendimiento, energía— del codo, que es justo lo que la propuesta necesita. Alinea las ventanas
temporales (la potencia de DCGM y las métricas de GuideLLM deben cubrir el mismo intervalo) o el
J/token no corresponde al goodput medido.&lt;/p>
&lt;hr>
&lt;h2 id="interpretar-las-distribuciones-no-solo-el-codo">Interpretar las distribuciones, no solo el codo&lt;/h2>
&lt;p>El codo es el titular, pero el informe HTML de GuideLLM da &lt;strong>distribuciones completas&lt;/strong>, y ahí hay
información que el codo resume. Tres lecturas adicionales:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>La forma de la cola.&lt;/strong> Dos configs con el mismo P99 pueden tener colas distintas: una con P99 a
500 ms y P99,9 a 600 ms es estable; otra con P99 a 500 ms y P99,9 a 3.000 ms tiene una cola larga
que afectará a algunos usuarios de forma severa. La media y hasta el P99 lo ocultan; la
distribución lo enseña.&lt;/li>
&lt;li>&lt;strong>La separación TTFT/ITL.&lt;/strong> Ver las dos distribuciones por separado dice si el cuello es el prefill
(TTFT alto) o el decode (ITL alto), lo que orienta la optimización (más concurrencia vs chunked
prefill, etc.).&lt;/li>
&lt;li>&lt;strong>La dispersión entre rondas.&lt;/strong> Si el goodput varía mucho entre rondas a la misma tasa, el sistema
es inestable bajo carga —algo que un único número de capacidad no revela—.&lt;/li>
&lt;/ul>
&lt;p>El codo dimensiona; las distribuciones diagnostican. Para una propuesta, el codo es el dato; para
operar y optimizar, las distribuciones son donde se ve qué arreglar.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-en-el-harness-reproducible">GuideLLM en el harness reproducible&lt;/h2>
&lt;p>GuideLLM encaja como el motor de carga del harness reproducible (artículo S4). El patrón:&lt;/p>
&lt;ol>
&lt;li>Un &lt;strong>script/Job&lt;/strong> que despliega el motor con la config pineada, calienta, corre el sweep y vuelca
el JSON con todos los metadatos (modelo, versión, hardware, &lt;code>--data&lt;/code>, SLO).&lt;/li>
&lt;li>&lt;strong>Nombrado por fecha y config&lt;/strong> para versionar las corridas.&lt;/li>
&lt;li>&lt;strong>Almacén&lt;/strong> (repo git de JSONs o bucket) para reproducir y comparar.&lt;/li>
&lt;li>&lt;strong>DCGM&lt;/strong> capturando en paralelo para añadir la energía a cada punto.&lt;/li>
&lt;/ol>
&lt;p>Así, &amp;ldquo;reproducir el codo de esta config&amp;rdquo; es un comando, no una tarde, y comparar dos releases es un
diff de JSON. Es lo que convierte el benchmarking de una actividad puntual en una capacidad continua
—y lo que permite que la cifra de capacidad de la propuesta venga con el banco para reproducirla.&lt;/p>
&lt;hr>
&lt;h2 id="el-coste-de-un-sweep-en-euros">El coste de un sweep (en euros)&lt;/h2>
&lt;p>Un sweep de 10 rondas a ~2 minutos por ronda ocupa el nodo ~20–30 minutos. A coste amortizado de
~11 €/h, son &lt;strong>~4–5,5 € por sweep completo&lt;/strong> de un modelo. No es mucho por corrida, pero un programa
de benchmarking continuo (cada release, cada config, varios modelos) suma; por eso conviene
automatizar el sweep como Job reproducible y correr sweeps &lt;strong>cortos&lt;/strong> en CI (solo alrededor del codo)
reservando el exhaustivo para los releases. El coste de medir es parte del coste de la plataforma —y
pequeño frente al de dimensionar mal—.&lt;/p>
&lt;hr>
&lt;h2 id="el-slo-por-caso-de-uso-el-codo-se-mueve-con-él">El SLO por caso de uso: el codo se mueve con él&lt;/h2>
&lt;p>Como el SLO define el codo, distintos casos de uso dan capacidades distintas &lt;strong>sobre el mismo
hardware&lt;/strong>. Conviene tenerlo presente al dimensionar:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Caso de uso&lt;/th>
&lt;th>SLO típico&lt;/th>
&lt;th>Efecto en el codo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Chat interactivo&lt;/td>
&lt;td>TTFT P99 &amp;lt; 500 ms, ITL &amp;lt; 50 ms&lt;/td>
&lt;td>codo temprano (latencia manda)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Copilot de código&lt;/td>
&lt;td>TTFT P99 &amp;lt; 300 ms&lt;/td>
&lt;td>codo aún más temprano&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch / resúmenes&lt;/td>
&lt;td>sin SLO de TTFT, maximizar throughput&lt;/td>
&lt;td>codo tardío (casi throughput puro)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Agente (multi-paso)&lt;/td>
&lt;td>latencia por paso acotada&lt;/td>
&lt;td>depende del nº de pasos&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La misma GPU sirve &lt;strong>más&lt;/strong> carga de batch que de chat interactivo, porque el SLO de batch es laxo. Por
eso dimensionar requiere &lt;strong>un sweep por perfil de carga&lt;/strong>: no hay &amp;ldquo;la capacidad del nodo&amp;rdquo;, hay &amp;ldquo;la
capacidad del nodo para este SLO&amp;rdquo;. Mezclar cargas con SLOs distintos en el mismo pool sin separarlas
es una fuente clásica de SLOs rotos —el batch satura y el chat interactivo lo paga—. La solución
conecta con el scheduling: separar pools o prioridades por SLO.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-vs-los-otros-cuándo-usar-cuál">GuideLLM vs los otros: cuándo usar cuál&lt;/h2>
&lt;p>Para situarlo frente a las herramientas de B2:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Pregunta&lt;/th>
&lt;th>Herramienta&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;¿Hasta dónde cargo sin romper el SLO?&amp;rdquo;&lt;/td>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>sweep dirigido por SLO, el codo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Cuál es la capacidad máxima del endpoint?&amp;rdquo;&lt;/td>
&lt;td>AIPerf&lt;/td>
&lt;td>detección automática de saturación&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Mejora mi cambio de flag en vLLM?&amp;rdquo;&lt;/td>
&lt;td>vllm bench serve&lt;/td>
&lt;td>rápido, propio del motor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Qué hardware/motor es mejor en abstracto?&amp;rdquo;&lt;/td>
&lt;td>leer MLPerf&lt;/td>
&lt;td>comparabilidad cross-vendor&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>GuideLLM es la herramienta del &lt;strong>dimensionamiento bajo SLO&lt;/strong>: cuando la pregunta es operativa
(cuántas réplicas, qué coste por token a mi SLO), es la respuesta más directa. AIPerf y GuideLLM se
solapan (ambos multi-proceso); la diferencia práctica es que GuideLLM está más orientado al SLO y al
informe reproducible, y AIPerf a la capacidad máxima con detección automática. Muchos equipos usan
los dos; lo que no se debe es comparar un resultado de uno con el de otro como si fueran la misma
medida.&lt;/p>
&lt;hr>
&lt;h2 id="checklist-de-un-sweep-defendible">Checklist de un sweep defendible&lt;/h2>
&lt;p>Para que el codo de un sweep sostenga una decisión de sizing:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Paso&lt;/th>
&lt;th>Verificación&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Carga realista&lt;/td>
&lt;td>&lt;code>--data&lt;/code> con la distribución de tu tráfico (o poisson)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLO declarado&lt;/td>
&lt;td>TTFT/TPOT con percentil y umbral fijados&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Sweep extendido&lt;/td>
&lt;td>llega hasta que el P99 rompe (pasa el codo)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Goodput, no throughput&lt;/td>
&lt;td>la capacidad es el goodput bajo SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Job en el cluster&lt;/td>
&lt;td>sin latencia de red de cliente externo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Salida versionada&lt;/td>
&lt;td>JSON con todos los metadatos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>DCGM en paralelo&lt;/td>
&lt;td>energía por punto, opcional pero recomendado&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Si puedes marcar las siete casillas, el codo es un dato auditable; si falta cualquiera, es una
anécdota. El sizing de la propuesta cuelga de que estas siete estén verdes.&lt;/p>
&lt;hr>
&lt;h2 id="límites-y-trampas-data-driven">Límites y trampas (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>No extender el sweep más allá del codo.&lt;/strong> Si no detecta saturación, la capacidad estimada es la
última ronda probada, no el codo real. Extiéndelo hasta que el P99 rompa.&lt;/li>
&lt;li>&lt;strong>Carga irreal.&lt;/strong> Un &lt;code>--data&lt;/code> de longitudes fijas no se parece a tu tráfico; usa la distribución
real (o poisson) para un codo que aplique.&lt;/li>
&lt;li>&lt;strong>Reportar throughput, no goodput.&lt;/strong> El codo se define por el goodput bajo SLO; el throughput
bruto sobreestima la capacidad.&lt;/li>
&lt;li>&lt;strong>SLO no declarado.&lt;/strong> Una capacidad sin el SLO bajo el que se midió no es comparable ni
defendible.&lt;/li>
&lt;li>&lt;strong>Cliente fuera del cluster.&lt;/strong> Correr GuideLLM desde fuera mete latencia de red en la medida;
córrelo como Job dentro del cluster.&lt;/li>
&lt;/ol>
&lt;p>Con GuideLLM dominado, tienes la herramienta que convierte el rendimiento en un dato accionable: el
codo, el sizing y el coste por token, todo de un sweep reproducible. El siguiente artículo (B4) entra
en AIPerf de NVIDIA; este cierra la validación de SLO, que es la que dimensiona la propuesta.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>GuideLLM responde la única pregunta de rendimiento que dimensiona una plataforma: &lt;strong>¿hasta dónde
cargo sin romper el SLO?&lt;/strong> Y la responde con la disciplina correcta —un sweep de idle a saturación
que halla el codo, definido por el goodput bajo un SLO declarado, sobre una carga que se parece a la
real—. De ese codo sale todo lo que la propuesta necesita: el número de réplicas, el coste por token
(con el goodput como denominador, no el throughput de catálogo) y, si capturas DCGM durante el sweep,
también la energía por token. El error que invalida el ejercicio es el de siempre: reportar el
throughput máximo en vez del goodput, medir con una carga irreal, o no extender el sweep hasta ver
dónde rompe. Hecho bien —como Job dentro del cluster, con la distribución real, el SLO fijado y la
salida en JSON versionado— GuideLLM convierte el rendimiento de una cifra de marketing en un dato
reproducible que aguanta una auditoría y dimensiona una arquitectura soberana con números. El codo
no es el punto más rápido; es el punto donde tu plataforma sigue cumpliendo lo que prometió.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/nvidia-genai-perf-a-fondo/">GenAI-Perf a fondo&lt;/a> — el perfilador de NVIDIA y cómo se compara con GuideLLM ficha a ficha (rate-types, sweep de concurrencia, métricas).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/comparativa-motores-serving-pareto/">Comparativa de motores de serving (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — tras validar los SLOs con GuideLLM, esta comparativa decide qué motor cumple la frontera de Pareto goodput-latencia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo de medición y reproducibilidad&lt;/a> — los errores de setup que hacen que un sweep de concurrencia resulte no reproducible, aunque la herramienta esté bien configurada.&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>Red Hat · GuideLLM: evaluar despliegues LLM para inferencia real — &lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference&lt;/a>&lt;/li>
&lt;li>Red Hat · desplegar y benchmarkear vLLM con GuideLLM en Kubernetes — &lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes&lt;/a>&lt;/li>
&lt;li>GuideLLM · PyPI — &lt;a href="https://pypi.org/project/guidellm/">https://pypi.org/project/guidellm/&lt;/a>&lt;/li>
&lt;li>GuideLLM · GitHub (proyecto vLLM) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&lt;/a>&lt;/li>
&lt;li>Medium · GuideLLM en OpenShift (rate-types, ejemplo) — &lt;a href="https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794">https://medium.com/@jajodia.nirjhar/exploring-guidellm-benchmarking-a-live-llm-on-openshift-ccc2d0841794&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Catálogo de herramientas de benchmark LLM: ficha práctica a fondo</title><link>https://blog.lo0.es/posts/herramientas-benchmark-llm-ficha-a-ficha/</link><pubDate>Sun, 14 Jun 2026 02:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/herramientas-benchmark-llm-ficha-a-ficha/</guid><description>&lt;blockquote>
&lt;p>Notación: importes en &lt;strong>euros (N €)&lt;/strong>, decimales con coma. No se usa el símbolo de dólar
(en este sitio es delimitador de fórmula).&lt;/p>
&lt;/blockquote>
&lt;h2 id="qué-cubre-este-artículo">Qué cubre este artículo&lt;/h2>
&lt;p>Segundo artículo del track de &lt;strong>benchmarking&lt;/strong> (B2). La &lt;a href="https://blog.lo0.es/posts/benchmarking-llm-frameworks-estado-del-arte/">introducción B1&lt;/a>
fijó las métricas y la metodología; este artículo es el &lt;strong>manual práctico&lt;/strong>: para cada
herramienta, qué mide, &lt;strong>cómo se invoca de verdad&lt;/strong> (con el comando), qué formato de salida da
y cuándo elegirla. El objetivo es que, tras leerlo, sepas qué ejecutar para medir tu motor y
puedas reproducir el número. Sin recomendaciones universales; solo la mecánica de cada
herramienta y el criterio para escoger.&lt;/p>
&lt;hr>
&lt;h2 id="cómo-elegir-el-mapa-de-herramientas">Cómo elegir: el mapa de herramientas&lt;/h2>
&lt;p>Recordando la división de B1, las herramientas se ordenan en dos ejes: &lt;strong>micro-bench&lt;/strong>
(mono-proceso, para tunear un motor) vs &lt;strong>generador de carga&lt;/strong> (multi-proceso, para medir
capacidad real), y &lt;strong>propias del motor&lt;/strong> vs &lt;strong>agnósticas del endpoint&lt;/strong>.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 250" role="img" aria-label="Mapa de herramientas de benchmark por dos ejes: micro-bench mono-proceso vs generador de carga multi-proceso, y propio del motor vs agnostico del endpoint" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.bx{fill:none;stroke:currentColor;stroke-width:1.2}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;line class="ax" x1="390" y1="40" x2="390" y2="200"/>
&lt;text x="70" y="216" class="ts">micro-bench (mono-proceso)&lt;/text>
&lt;text x="560" y="216" class="ts">generador de carga (multi-proceso)&lt;/text>
&lt;text x="400" y="52" class="ts">↑ agnóstico del endpoint&lt;/text>
&lt;text x="400" y="195" class="ts">↓ propio del motor&lt;/text>
&lt;rect class="bx" x="150" y="150" width="150" height="34" rx="5"/>&lt;text x="225" y="171" text-anchor="middle" class="ts">vLLM bench / SGLang bench&lt;/text>
&lt;rect class="bx" x="470" y="70" width="120" height="34" rx="5"/>&lt;text x="530" y="91" text-anchor="middle" class="ts">AIPerf&lt;/text>
&lt;rect class="bx" x="470" y="115" width="120" height="34" rx="5"/>&lt;text x="530" y="136" text-anchor="middle" class="ts">GuideLLM&lt;/text>
&lt;rect class="bx" x="610" y="93" width="110" height="34" rx="5"/>&lt;text x="665" y="114" text-anchor="middle" class="ts">LLMPerf&lt;/text>
&lt;rect class="bx" x="150" y="60" width="150" height="34" rx="5"/>&lt;text x="225" y="81" text-anchor="middle" class="ts">MLPerf (suite estándar)&lt;/text>
&lt;text x="60" y="240" class="ts">Para tunear un motor concreto: micro-bench. Para medir capacidad bajo SLO: generador de carga. Para comparar fabricantes: MLPerf.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="datasets-la-carga-sintética-importa-tanto-como-la-herramienta">Datasets: la carga sintética importa tanto como la herramienta&lt;/h2>
&lt;p>Antes de las fichas, un punto que cambia los resultados sin que se note: &lt;strong>qué carga le metes&lt;/strong>.
Las herramientas aceptan distintos tipos de dataset, y cada uno mide algo distinto:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Dataset&lt;/th>
&lt;th>Qué simula&lt;/th>
&lt;th>Sesgo que introduce&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>random&lt;/strong> (longitudes fijas)&lt;/td>
&lt;td>carga uniforme controlada&lt;/td>
&lt;td>irreal: el tráfico no es de longitud fija&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>sharegpt&lt;/strong> (conversaciones reales)&lt;/td>
&lt;td>distribución realista de prompts&lt;/td>
&lt;td>el estándar de facto para comparar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>trazas propias&lt;/strong>&lt;/td>
&lt;td>tu tráfico real&lt;/td>
&lt;td>el más fiel, pero específico de tu caso&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La trampa: un benchmark con prompts cortos de longitud fija da un throughput altísimo que &lt;strong>no
se parece a producción&lt;/strong>, donde las longitudes varían y los prompts largos dominan el coste de
prefill. Para una cifra defendible, usa &lt;strong>sharegpt&lt;/strong> (comparabilidad) o, mejor, &lt;strong>trazas de tu
propio tráfico&lt;/strong> (fidelidad). Y declara siempre la distribución de longitudes (prompt/salida)
junto al número, porque dos benchmarks con datasets distintos &lt;strong>no son comparables&lt;/strong> aunque
usen la misma herramienta.&lt;/p>
&lt;hr>
&lt;h2 id="vllm-bench-serve">vLLM bench serve&lt;/h2>
&lt;p>Qué mide: TTFT, TPOT, throughput y latencias del &lt;strong>servidor vLLM&lt;/strong> bajo una carga sintética.
Clase: micro-bench (mono-proceso). Es la herramienta para &lt;strong>tunear vLLM&lt;/strong> y ver el efecto de
sus optimizaciones (&lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">decode&lt;/a>, &lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">prefill&lt;/a>).&lt;/p>
&lt;p>Invocación típica (&lt;a href="https://docs.vllm.ai/en/latest/cli/bench/serve/">vLLM · CLI de benchmark&lt;/a>):&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">vllm bench serve &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --backend vllm &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --model meta-llama/Llama-3.1-8B-Instruct &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --endpoint /v1/completions &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dataset-name sharegpt &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --num-prompts &lt;span class="m">1000&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Parámetros clave: &lt;code>--num-prompts&lt;/code> (carga), &lt;code>--dataset-name&lt;/code> (sharegpt, random, etc.),
&lt;code>--request-rate&lt;/code> (peticiones/s). La salida es un &lt;strong>resumen por consola&lt;/strong> con TTFT, TPOT,
throughput y percentiles, y puede volcarse a JSON. Para barrer concurrencias existe &lt;strong>&lt;code>vllm bench sweep serve&lt;/code>&lt;/strong>, que automatiza el sweep (&lt;a href="https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/">vLLM · sweep&lt;/a>).&lt;/p>
&lt;p>Límite: mono-proceso, se satura en el cliente a alta concurrencia; menos flexible que GuideLLM
en datasets y patrones de carga. Úsalo para iterar rápido sobre la config de vLLM, no para
medir la capacidad máxima a escala.&lt;/p>
&lt;hr>
&lt;h2 id="sglang-bench">SGLang bench&lt;/h2>
&lt;p>Qué mide: el equivalente para el motor &lt;strong>SGLang&lt;/strong> (TTFT, TPOT, throughput). Clase: micro-bench.
Uso: tunear SGLang y compararlo consigo mismo entre configuraciones. La mecánica es análoga a
&lt;code>vllm bench serve&lt;/code>: una carga sintética, salida por consola/JSON con las mismas métricas. Si
evalúas SGLang vs vLLM, &lt;strong>no&lt;/strong> los compares con sus micro-benchs respectivos (clases iguales
pero implementaciones distintas): usa un generador de carga agnóstico (GuideLLM/AIPerf) contra
ambos endpoints, para que la herramienta no sea la variable.&lt;/p>
&lt;hr>
&lt;h2 id="aiperf-nvidia-ex-genai-perf">AIPerf (NVIDIA, ex genai-perf)&lt;/h2>
&lt;p>Qué mide: TTFT, ITL, throughput y latencia sobre &lt;strong>cualquier endpoint compatible&lt;/strong> (vLLM, NIM,
TGI, SGLang). Clase: generador de carga &lt;strong>multi-proceso&lt;/strong>. Es el sucesor de genai-perf
(jubilado el 15-abr-2026). Su rasgo distintivo: durante el sweep &lt;strong>detecta la saturación de la
GPU&lt;/strong> y devuelve la iteración anterior como &lt;code>estimatedCapacity&lt;/code>; por eso hay que extender el
sweep &lt;strong>más allá del codo&lt;/strong> (&lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">AIPerf&lt;/a>).&lt;/p>
&lt;p>Salida: métricas estructuradas (JSON) con distribuciones de TTFT/ITL y el &lt;code>estimatedCapacity&lt;/code>.
Úsalo cuando quieras la &lt;strong>capacidad real&lt;/strong> de un endpoint, sea cual sea el motor, con la
detección automática del punto de saturación.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-proyecto-vllm">GuideLLM (proyecto vLLM)&lt;/h2>
&lt;p>Qué mide: distribuciones completas de TTFT, ITL y comportamiento de extremo a extremo, para
evaluación &lt;strong>dirigida por SLO&lt;/strong>. Clase: generador de carga multi-proceso. Es la herramienta
recomendada para benchmarkear servidores vLLM en producción: más flexible que &lt;code>vllm bench serve&lt;/code> en carga de datasets, formato de peticiones y patrones de tráfico, con &lt;strong>progreso en
vivo y generación automática de informe&lt;/strong> (&lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">Red Hat&lt;/a>).&lt;/p>
&lt;p>Invocación típica:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="cl">guidellm benchmark &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --target &lt;span class="s2">&amp;#34;http://localhost:8000&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --rate-type throughput &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --max-requests &lt;span class="m">1000&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --data &lt;span class="s2">&amp;#34;samples=1000,prompt_tokens=1024,output_tokens=256&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Parámetros clave: &lt;code>--rate-type&lt;/code> (&lt;code>synchronous&lt;/code>, &lt;code>concurrent&lt;/code>, &lt;code>throughput&lt;/code>, o por tasa),
&lt;code>--data&lt;/code> (especificación de la carga: nº de muestras y longitudes de prompt/salida), &lt;code>--target&lt;/code>
(el endpoint). Genera &lt;strong>sweeps reproducibles&lt;/strong> para hallar el rango de operación seguro bajo
SLO, con distribuciones completas (no solo medias). Salida: informe con percentiles y, a
menudo, exportable a JSON/HTML. Es la opción por defecto para responder &amp;ldquo;¿hasta dónde cargo
este motor sin romper el SLO?&amp;rdquo;.&lt;/p>
&lt;hr>
&lt;h2 id="llmperf-anyscaleray">LLMPerf (Anyscale/Ray)&lt;/h2>
&lt;p>Qué mide: throughput y latencia a nivel de inferencia. Clase: generador de carga. Uso:
validación de endpoints, históricamente muy extendido en el ecosistema Ray/Anyscale. Es una
opción sólida y conocida, aunque menos centrada en distribuciones y sweeps que GuideLLM/AIPerf.
Encaja si ya operas en Ray o quieres una herramienta simple para validar un endpoint.&lt;/p>
&lt;hr>
&lt;h2 id="inference-benchmarker-hugging-face">inference-benchmarker (Hugging Face)&lt;/h2>
&lt;p>Qué mide: latencia y throughput de endpoints de inferencia, con orientación a generar informes
comparables. Clase: generador de carga. Uso: alternativa OSS dentro del ecosistema Hugging
Face, útil si ya trabajas con TGI o el stack de HF. Como las demás de su clase, su valor está
en medir capacidad real con carga distribuida; la elección entre ésta, GuideLLM y AIPerf
suele venir por el ecosistema en el que ya operas más que por diferencias de fondo en lo que
miden.&lt;/p>
&lt;hr>
&lt;h2 id="guidellm-vs-aiperf-cuál-de-los-dos-generadores-de-carga">GuideLLM vs AIPerf: cuál de los dos generadores de carga&lt;/h2>
&lt;p>Son las dos opciones serias de carga multi-proceso, y se solapan mucho. Las diferencias
prácticas que inclinan la elección:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Criterio&lt;/th>
&lt;th>GuideLLM&lt;/th>
&lt;th>AIPerf&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Origen&lt;/td>
&lt;td>proyecto vLLM (Red Hat)&lt;/td>
&lt;td>NVIDIA (sucesor de genai-perf)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Foco&lt;/td>
&lt;td>evaluación dirigida por &lt;strong>SLO&lt;/strong>, sweeps reproducibles&lt;/td>
&lt;td>capacidad real, &lt;strong>estimatedCapacity&lt;/strong> automático&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Patrones de carga&lt;/td>
&lt;td>síncrono, concurrente, por tasa&lt;/td>
&lt;td>sweep con detección de saturación&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Informe&lt;/td>
&lt;td>progreso en vivo + informe automático&lt;/td>
&lt;td>métricas estructuradas (JSON)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Ecosistema&lt;/td>
&lt;td>vLLM / OpenShift AI&lt;/td>
&lt;td>NVIDIA NIM / Triton / vLLM&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>En la práctica: si tu pregunta es &amp;ldquo;¿hasta dónde cargo sin romper el SLO?&amp;rdquo;, &lt;strong>GuideLLM&lt;/strong> la
responde de forma más directa con sus sweeps dirigidos por SLO; si tu pregunta es &amp;ldquo;¿cuál es la
capacidad máxima de este endpoint?&amp;rdquo;, &lt;strong>AIPerf&lt;/strong> la da con su detección automática del codo. Muchos
equipos usan los dos: GuideLLM para el SLO operativo, AIPerf para la capacidad de referencia. Lo
que &lt;strong>no&lt;/strong> debes hacer es comparar un resultado de GuideLLM con uno de AIPerf como si fueran la
misma medida: aunque ambos sean multi-proceso, su metodología de sweep difiere; elige uno para
una comparación dada y mantenlo.&lt;/p>
&lt;hr>
&lt;h2 id="mlperf-inference-mlcommons">MLPerf Inference (MLCommons)&lt;/h2>
&lt;p>Qué mide: rendimiento bajo &lt;strong>escenarios normalizados&lt;/strong> (Offline, Server, Interactive) con
reglas estrictas. A diferencia de las anteriores, &lt;strong>MLPerf no se &amp;ldquo;ejecuta&amp;rdquo; para tu caso del
día a día&lt;/strong>: es una suite de comparación entre fabricantes cuyos resultados se &lt;strong>leen&lt;/strong>
(publicados por NVIDIA, AMD, Intel, etc.). Úsalo para comparar hardware/motores entre sí con
reglas idénticas, no para dimensionar tu carga concreta —para eso, GuideLLM/AIPerf sobre tu
endpoint—.&lt;/p>
&lt;hr>
&lt;h2 id="tabla-comparativa-práctica">Tabla comparativa práctica&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Herramienta&lt;/th>
&lt;th>Clase&lt;/th>
&lt;th>Comando base&lt;/th>
&lt;th>Salida&lt;/th>
&lt;th>Cuándo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vllm bench serve&lt;/strong>&lt;/td>
&lt;td>micro&lt;/td>
&lt;td>&lt;code>vllm bench serve&lt;/code>&lt;/td>
&lt;td>consola/JSON&lt;/td>
&lt;td>tunear vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>vllm bench sweep&lt;/strong>&lt;/td>
&lt;td>micro (sweep)&lt;/td>
&lt;td>&lt;code>vllm bench sweep serve&lt;/code>&lt;/td>
&lt;td>consola/JSON&lt;/td>
&lt;td>barrer concurrencia en vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang bench&lt;/strong>&lt;/td>
&lt;td>micro&lt;/td>
&lt;td>bench del repo SGLang&lt;/td>
&lt;td>consola/JSON&lt;/td>
&lt;td>tunear SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AIPerf&lt;/strong>&lt;/td>
&lt;td>carga&lt;/td>
&lt;td>&lt;code>aiperf profile …&lt;/code>&lt;/td>
&lt;td>JSON + estimatedCapacity&lt;/td>
&lt;td>capacidad real, multi-endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>carga&lt;/td>
&lt;td>&lt;code>guidellm benchmark …&lt;/code>&lt;/td>
&lt;td>informe + JSON/HTML&lt;/td>
&lt;td>SLO, sweep reproducible&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLMPerf&lt;/strong>&lt;/td>
&lt;td>carga&lt;/td>
&lt;td>script Ray/LLMPerf&lt;/td>
&lt;td>JSON&lt;/td>
&lt;td>validar endpoint (Ray)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLPerf&lt;/strong>&lt;/td>
&lt;td>suite&lt;/td>
&lt;td>(se leen resultados)&lt;/td>
&lt;td>resultados oficiales&lt;/td>
&lt;td>comparar fabricantes&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="resumen-qué-ejecutas-para-cada-pregunta">Resumen: qué ejecutas para cada pregunta&lt;/h2>
&lt;p>Para no perderse en el catálogo, el mapeo directo de pregunta a herramienta:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Tu pregunta&lt;/th>
&lt;th>Herramienta&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&amp;ldquo;¿Mejora mi cambio de config en vLLM?&amp;rdquo;&lt;/td>
&lt;td>&lt;code>vllm bench serve&lt;/code> (+ sweep)&lt;/td>
&lt;td>rápido, propio del motor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Hasta dónde cargo sin romper el SLO?&amp;rdquo;&lt;/td>
&lt;td>GuideLLM&lt;/td>
&lt;td>sweeps dirigidos por SLO&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Cuál es la capacidad máxima del endpoint?&amp;rdquo;&lt;/td>
&lt;td>AIPerf&lt;/td>
&lt;td>detección automática del codo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿vLLM o SGLang para mi carga?&amp;rdquo;&lt;/td>
&lt;td>GuideLLM/AIPerf contra ambos&lt;/td>
&lt;td>misma herramienta, motor variable&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Qué hardware/motor es mejor en abstracto?&amp;rdquo;&lt;/td>
&lt;td>leer MLPerf&lt;/td>
&lt;td>comparabilidad cross-vendor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&amp;ldquo;¿Hay regresión en este release?&amp;rdquo;&lt;/td>
&lt;td>sweep corto en CI vs línea base&lt;/td>
&lt;td>detección continua&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla que subyace a toda la tabla: &lt;strong>micro-bench para iterar sobre un motor, generador de
carga para medir capacidad y decidir, MLPerf para comparar fabricantes&lt;/strong>. Y, para cualquier
comparación entre sistemas, la misma herramienta para todos los candidatos. Si tienes claras
estas tres categorías, la elección concreta es secundaria.&lt;/p>
&lt;hr>
&lt;h2 id="metodología-paso-a-paso-de-un-benchmark">Metodología paso a paso de un benchmark&lt;/h2>
&lt;p>Una corrida fiable no es &amp;ldquo;lanzar el comando&amp;rdquo;: es un procedimiento. Los pasos, con la herramienta
de carga (GuideLLM/AIPerf) contra tu endpoint:&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 170" role="img" aria-label="Flujo de un benchmark: desplegar el motor, calentar, barrer concurrencia, recoger métricas y comparar" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#wm)}&lt;/style>
&lt;defs>&lt;marker id="wm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;rect class="bx" x="20" y="50" width="120" height="44" rx="6"/>&lt;text x="80" y="69" text-anchor="middle" class="tl">1 · Desplegar&lt;/text>&lt;text x="80" y="85" text-anchor="middle" class="ts">motor + config&lt;/text>
&lt;path class="ar" d="M140,72 L165,72"/>
&lt;rect class="bx" x="165" y="50" width="120" height="44" rx="6"/>&lt;text x="225" y="69" text-anchor="middle" class="tl">2 · Calentar&lt;/text>&lt;text x="225" y="85" text-anchor="middle" class="ts">descartar warm-up&lt;/text>
&lt;path class="ar" d="M285,72 L310,72"/>
&lt;rect class="bx" x="310" y="50" width="120" height="44" rx="6"/>&lt;text x="370" y="69" text-anchor="middle" class="tl">3 · Sweep&lt;/text>&lt;text x="370" y="85" text-anchor="middle" class="ts">pasar el codo&lt;/text>
&lt;path class="ar" d="M430,72 L455,72"/>
&lt;rect class="bx" x="455" y="50" width="120" height="44" rx="6"/>&lt;text x="515" y="69" text-anchor="middle" class="tl">4 · Recoger&lt;/text>&lt;text x="515" y="85" text-anchor="middle" class="ts">JSON con percentiles&lt;/text>
&lt;path class="ar" d="M575,72 L600,72"/>
&lt;rect class="bx" x="600" y="50" width="120" height="44" rx="6"/>&lt;text x="660" y="69" text-anchor="middle" class="tl">5 · Comparar&lt;/text>&lt;text x="660" y="85" text-anchor="middle" class="ts">goodput vs SLO&lt;/text>
&lt;text x="20" y="130" class="ts">Fijar modelo, precisión, hardware, dataset y SLO antes del paso 1; pinearlos en la salida del paso 4.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;ol>
&lt;li>&lt;strong>Desplegar&lt;/strong> el motor con la config exacta a medir (modelo, precisión, flags), pineada.&lt;/li>
&lt;li>&lt;strong>Calentar&lt;/strong>: lanzar unas peticiones y descartarlas, para que el prefix cache y el
autotuning no inflen los primeros números.&lt;/li>
&lt;li>&lt;strong>Sweep&lt;/strong>: barrer concurrencias crecientes (1, 8, 16, 24, 32…) &lt;strong>más allá del codo&lt;/strong>, para
ver dónde se dispara la latencia.&lt;/li>
&lt;li>&lt;strong>Recoger&lt;/strong> la salida en JSON con percentiles (TTFT/ITL/throughput/goodput) y los metadatos.&lt;/li>
&lt;li>&lt;strong>Comparar&lt;/strong>: leer el goodput bajo el SLO, no el throughput máximo.&lt;/li>
&lt;/ol>
&lt;p>Saltarse el paso 2 (warm-up) o no pasar el codo en el 3 son los dos errores que más sesgan el
resultado.&lt;/p>
&lt;hr>
&lt;h2 id="comparar-dos-motores-el-protocolo-justo">Comparar dos motores: el protocolo justo&lt;/h2>
&lt;p>Si el objetivo es elegir entre vLLM y SGLang (o TRT-LLM), el protocolo que evita conclusiones
falsas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Misma herramienta de carga&lt;/strong> (GuideLLM o AIPerf) contra ambos endpoints — nunca el
micro-bench de cada motor.&lt;/li>
&lt;li>&lt;strong>Mismo dataset y distribución de longitudes.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Mismo hardware y precisión.&lt;/strong>&lt;/li>
&lt;li>&lt;strong>Mismo SLO&lt;/strong> para calcular el goodput de ambos.&lt;/li>
&lt;li>Variar &lt;strong>solo el motor&lt;/strong>; todo lo demás, fijo.&lt;/li>
&lt;/ul>
&lt;p>Solo así la diferencia que midas es del motor y no de la herramienta, el dataset o el hardware.
Es el experimento controlado que sostiene la fila del cuadro de mando (artículo B8).&lt;/p>
&lt;hr>
&lt;h2 id="formato-de-salida-y-comparabilidad">Formato de salida y comparabilidad&lt;/h2>
&lt;p>Lo que registres junto al número es lo que lo hace comparable. Una salida útil incluye, en
JSON para versionarla:&lt;/p>
&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-json" data-lang="json">&lt;span class="line">&lt;span class="cl">&lt;span class="p">{&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;tool&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;guidellm&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;version&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;x.y.z&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;model&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;Llama-3.1-70B&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;precision&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;FP16&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;hardware&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="s2">&amp;#34;8xH100 SXM NVLink&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;load&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;prompt_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">1024&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;output_tokens&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">256&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;concurrency&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">16&lt;/span>&lt;span class="p">},&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;results&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="p">{&lt;/span>&lt;span class="nt">&amp;#34;ttft_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">180&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;ttft_p99_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">460&lt;/span>&lt;span class="p">,&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl"> &lt;span class="nt">&amp;#34;itl_p50_ms&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">22&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;throughput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3400&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="nt">&amp;#34;goodput_tok_s&amp;#34;&lt;/span>&lt;span class="p">:&lt;/span> &lt;span class="mi">3330&lt;/span>&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="p">}&lt;/span>
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Guardar esto por cada corrida convierte un benchmark en un dato auditable: cualquiera reproduce
la cifra con la misma herramienta, versión, modelo, hardware y carga. Es el material del
harness reproducible (artículo S4), y la diferencia entre un número defendible y una captura de
consola.&lt;/p>
&lt;hr>
&lt;h2 id="ejemplo-trabajado-leer-la-salida-de-un-sweep">Ejemplo trabajado: leer la salida de un sweep&lt;/h2>
&lt;p>Una salida ilustrativa de un sweep de GuideLLM sobre un 70B en 8×H100 (SLO: P99 de TTFT &amp;lt;
500 ms), tal como la leerías:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrencia&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>ITL P50 (ms)&lt;/th>
&lt;th>Throughput (tok/s)&lt;/th>
&lt;th>Goodput (tok/s)&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>240&lt;/td>
&lt;td>20&lt;/td>
&lt;td>2.100&lt;/td>
&lt;td>2.100&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>460&lt;/td>
&lt;td>22&lt;/td>
&lt;td>3.400&lt;/td>
&lt;td>3.330&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>24&lt;/td>
&lt;td>980&lt;/td>
&lt;td>31&lt;/td>
&lt;td>3.900&lt;/td>
&lt;td>2.420&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>1.800&lt;/td>
&lt;td>54&lt;/td>
&lt;td>4.000&lt;/td>
&lt;td>800&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cómo se lee: el &lt;strong>codo&lt;/strong> está entre 16 y 24. A concurrencia 16, el P99 (460 ms) cumple el SLO
y el goodput (3.330 tok/s) ≈ throughput. A 24, el throughput sube poco (3.400 → 3.900) pero el
P99 ya viola el SLO y el goodput &lt;strong>cae&lt;/strong> a 2.420. A 32, el throughput es máximo (4.000) pero el
goodput se desploma a 800: el sistema &amp;ldquo;rinde mucho&amp;rdquo; sirviendo peticiones que incumplen. La
&lt;strong>capacidad defendible&lt;/strong> es la de concurrencia 16 (3.330 tok/s útiles), y ese es el número que
entra en el &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a> y en
el coste por token. Quien reporte &amp;ldquo;4.000 tok/s&amp;rdquo; está describiendo el punto donde el sistema ya
no cumple su SLO.&lt;/p>
&lt;hr>
&lt;h2 id="automatizar-el-harness">Automatizar el harness&lt;/h2>
&lt;p>Una corrida manual no escala a un programa de benchmarking. La automatización mínima:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Script idempotente&lt;/strong> que despliega el motor, calienta, corre el sweep y guarda el JSON con
todos los metadatos (modelo, versión, hardware, dataset, SLO).&lt;/li>
&lt;li>&lt;strong>Nombrado por fecha y config&lt;/strong>, para versionar las corridas y comparar en el tiempo.&lt;/li>
&lt;li>&lt;strong>Almacén de resultados&lt;/strong> (un repo git de JSONs, o un bucket) para que cualquiera reproduzca y
compare.&lt;/li>
&lt;/ul>
&lt;p>El objetivo es que reproducir un número sea un comando, no una tarde. Es la base del harness del
artículo S4, y lo que convierte el benchmarking de una actividad puntual en una capacidad
continua de la plataforma.&lt;/p>
&lt;hr>
&lt;h2 id="integración-en-ci-benchmarking-continuo">Integración en CI: benchmarking continuo&lt;/h2>
&lt;p>El siguiente nivel es medir &lt;strong>en cada cambio&lt;/strong>: un job de CI que, al actualizar el motor o la
config, lanza un sweep corto contra un entorno de pruebas y &lt;strong>compara con la línea base&lt;/strong>. Si
el goodput cae más de un umbral, falla el pipeline. Así una regresión de rendimiento se detecta
en el commit, no en producción. Cuidado con dos cosas: el benchmarking en CI consume GPU-horas
(presupuéstalo) y necesita un entorno estable (mismo hardware) para que la comparación sea
válida. No hace falta el sweep completo en cada commit: un sweep corto que cubra el codo basta
para detectar regresiones; el sweep exhaustivo, para los releases.&lt;/p>
&lt;hr>
&lt;h2 id="el-coste-de-medir-en-euros">El coste de medir (en euros)&lt;/h2>
&lt;p>Benchmarkear &lt;strong>consume GPU-horas&lt;/strong>, y eso tiene un coste que conviene presupuestar. Un sweep
serio sobre un nodo 8×H100 puede ocupar las tarjetas un par de horas; a coste amortizado de
~11 €/h, son ~22 € por sweep completo, más si barres varios modelos y precisiones. No es mucho
por corrida, pero un programa de benchmarking continuo (cada release, cada cambio de config)
suma. La regla práctica: automatiza el harness para que cada corrida sea barata y reproducible,
y mide lo que vas a usar para decidir, no por exhaustividad. El coste de medir es parte del
coste de la plataforma —pequeño frente al de servir, pero real—.&lt;/p>
&lt;hr>
&lt;h2 id="observar-durante-el-benchmark-una-corrida-tres-ejes">Observar durante el benchmark: una corrida, tres ejes&lt;/h2>
&lt;p>Un truco que ahorra trabajo y conecta la serie: &lt;strong>mientras corre el sweep, captura también las
métricas de GPU&lt;/strong>. Con DCGM exportando a Prometheus durante la corrida, registras a la vez:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Eje&lt;/th>
&lt;th>Fuente durante el sweep&lt;/th>
&lt;th>Métrica&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Rendimiento&lt;/td>
&lt;td>la herramienta (GuideLLM/AIPerf)&lt;/td>
&lt;td>TTFT, ITL, throughput, goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Energía&lt;/td>
&lt;td>DCGM&lt;/td>
&lt;td>potencia (W) → J/token&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste&lt;/td>
&lt;td>precio del nodo (OpenCost)&lt;/td>
&lt;td>€/hora → CPM por punto&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Así, de &lt;strong>una sola corrida&lt;/strong> sacas los tres números del mismo punto de operación: a concurrencia
16, el goodput (3.330 tok/s), la potencia media (de DCGM, que dividida por el throughput da los
J/token) y el coste por token (con el precio del nodo). En vez de tres campañas separadas,
mides los tres ejes a la vez, y quedan &lt;strong>coherentes por construcción&lt;/strong> porque corresponden al
mismo instante y la misma carga. Es lo que hace el harness del artículo S4, y la razón de
exportar DCGM durante el benchmark aunque solo busques rendimiento: la energía y el coste salen
casi gratis si los capturas en la misma ventana.&lt;/p>
&lt;p>La advertencia metodológica: alinea las &lt;strong>ventanas temporales&lt;/strong>. La potencia de DCGM y las
métricas de la herramienta tienen que cubrir exactamente el mismo intervalo (sin warm-up ni
apagado), o el J/token no corresponde al throughput medido. Misma ventana, mismos tres números.&lt;/p>
&lt;hr>
&lt;h2 id="el-benchmark-es-también-finops-y-energía">El benchmark es también FinOps y energía&lt;/h2>
&lt;p>Una idea que conecta este artículo con el resto de la serie: &lt;strong>medir rendimiento es, de hecho,
medir coste y energía&lt;/strong>. Por la identidad del &lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">artículo de apertura&lt;/a>,
el goodput es el denominador del coste por token y de la energía por token. Cuando un sweep
revela que la config A da 3.330 tok/s de goodput y la config B da 4.000, no estás midiendo solo
velocidad: estás midiendo que B cuesta menos euros y menos vatios por token. Por eso el JSON de
salida de un benchmark debería acompañarse del coste de hierro (de OpenCost) para calcular el
CPM real de cada punto del sweep: throughput × precio del nodo = coste por token. El
benchmarking no es un eje aislado; es la herramienta que, indirectamente, más mueve el coste y
la energía de la plataforma, y la que llena la columna de rendimiento del cuadro de mando con
números que se traducen directamente a euros.&lt;/p>
&lt;p>La consecuencia operativa: no benchmarkees el rendimiento en el vacío. Cada corrida que guardes
con su throughput y su goodput debería poder cruzarse con el coste del nodo (€/hora) y la
energía (J/token) para dar las tres caras del mismo punto de operación. Esa es la forma de que
el track de benchmarking alimente al de FinOps y al de energía, en vez de vivir aparte.&lt;/p>
&lt;hr>
&lt;h2 id="estado-del-arte-2026-y-límites">Estado del arte 2026 y límites&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Migración a multi-proceso&lt;/strong>: AIPerf (ex genai-perf) y GuideLLM consolidan la medición de
capacidad real; los micro-benchs quedan para tunear motores.&lt;/li>
&lt;li>&lt;strong>GuideLLM como estándar OSS&lt;/strong> de evaluación dirigida por SLO con informe automático.&lt;/li>
&lt;li>&lt;strong>Cuidado con comparar entre clases&lt;/strong>: un micro-bench y un generador de carga no son
comparables; fija la clase y la herramienta.&lt;/li>
&lt;li>&lt;strong>Versión y dataset importan&lt;/strong>: el mismo comando con otro dataset o versión da otro número;
pínealos.&lt;/li>
&lt;li>&lt;strong>MLPerf no dimensiona tu caso&lt;/strong>: compara fabricantes, no sustituye un sweep sobre tu carga.&lt;/li>
&lt;/ul>
&lt;p>Con el catálogo práctico cubierto, el siguiente artículo del track (B3) entra en GuideLLM y la
validación de SLO bajo carga a fondo. La herramienta es el medio; el dato reproducible, el fin.&lt;/p>
&lt;h2 id="errores-que-invalidan-un-benchmark">Errores que invalidan un benchmark&lt;/h2>
&lt;p>Para terminar, la lista de lo que convierte una corrida en basura, por frecuencia:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Error&lt;/th>
&lt;th>Efecto&lt;/th>
&lt;th>Arreglo&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Comparar clases distintas&lt;/td>
&lt;td>hasta 7× de diferencia espuria&lt;/td>
&lt;td>misma herramienta para todos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No descartar el warm-up&lt;/td>
&lt;td>TTFT artificialmente bajo&lt;/td>
&lt;td>calentar y descartar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No pasar el codo&lt;/td>
&lt;td>no conoces la capacidad segura&lt;/td>
&lt;td>extender el sweep&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Dataset irreal (longitud fija)&lt;/td>
&lt;td>throughput que no aplica&lt;/td>
&lt;td>sharegpt o trazas propias&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Reportar media, no P99&lt;/td>
&lt;td>oculta la cola&lt;/td>
&lt;td>percentiles siempre&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenizer ajeno al modelo&lt;/td>
&lt;td>tok/s y coste/token sesgados&lt;/td>
&lt;td>contar con el del modelo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No pinear versión/config&lt;/td>
&lt;td>irreproducible&lt;/td>
&lt;td>guardar todo en el JSON&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Cualquiera de estos basta para que el número no sea defendible. Un benchmark es tan bueno como
su metodología: la herramienta importa menos que ejecutarla bien y registrarlo todo.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>El catálogo de herramientas de benchmark se reduce a una decisión simple —micro-bench para
tunear un motor, generador de carga para medir capacidad, MLPerf para comparar fabricantes— y a
una disciplina que pesa más que la elección: &lt;strong>el método&lt;/strong>. La misma &lt;code>guidellm benchmark&lt;/code> da un
dato de oro o uno inútil según el dataset, el warm-up, hasta dónde llegue el sweep y qué
registres en la salida. Para una propuesta de arquitectura soberana, el rendimiento solo cuenta
si viene con su comando, su versión, su carga y su goodput bajo SLO —y, cruzado con el coste en
euros y la energía por token, se convierte en la columna del cuadro de mando que decide qué
motor y qué configuración sostienen la plataforma. La herramienta la eliges en cinco minutos; la
metodología es lo que hace que el número aguante una auditoría.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/nvidia-genai-perf-a-fondo/">GenAI-Perf a fondo&lt;/a> — ficha ampliada del perfilador de NVIDIA: métricas (TTFT/TPOT/ISL/OSL), invocación contra endpoint OpenAI-compatible y comparación con GuideLLM/LLMPerf.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/comparativa-motores-serving-pareto/">Comparativa de motores de serving (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — una vez medido el goodput con estas herramientas, aquí se ven qué motor gana en cada punto de la frontera de Pareto.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo de medición y reproducibilidad&lt;/a> — las fuentes de sesgo que invalidan resultados aunque la herramienta esté bien configurada: warmup, dataset real vs sintético, entorno compartido.&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>vLLM · CLI de benchmark (&lt;code>bench serve&lt;/code>) — &lt;a href="https://docs.vllm.ai/en/latest/cli/bench/serve/">https://docs.vllm.ai/en/latest/cli/bench/serve/&lt;/a>&lt;/li>
&lt;li>vLLM · &lt;code>bench sweep serve&lt;/code> — &lt;a href="https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/">https://docs.vllm.ai/en/latest/cli/bench/sweep/serve/&lt;/a>&lt;/li>
&lt;li>Red Hat · desplegar y benchmarkear vLLM con GuideLLM en Kubernetes — &lt;a href="https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes">https://developers.redhat.com/articles/2025/12/24/how-deploy-and-benchmark-vllm-guidellm-kubernetes&lt;/a>&lt;/li>
&lt;li>GuideLLM · GitHub (proyecto vLLM) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&lt;/a>&lt;/li>
&lt;li>NVIDIA AIPerf · guía de benchmarking — &lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/&lt;/a>&lt;/li>
&lt;li>Medium · Benchmarking LLM Serving Performance (guía) — &lt;a href="https://medium.com/@kimdoil1211/benchmarking-llm-serving-performance-a-comprehensive-guide-db94b1bfe8cf">https://medium.com/@kimdoil1211/benchmarking-llm-serving-performance-a-comprehensive-guide-db94b1bfe8cf&lt;/a>&lt;/li>
&lt;/ul></description></item><item><title>Benchmarking de inferencia LLM: frameworks, métricas y estado del arte (ficha a ficha)</title><link>https://blog.lo0.es/posts/benchmarking-llm-frameworks-estado-del-arte/</link><pubDate>Sat, 13 Jun 2026 02:30:00 +0200</pubDate><guid>https://blog.lo0.es/posts/benchmarking-llm-frameworks-estado-del-arte/</guid><description>&lt;blockquote>
&lt;p>Notación: importes en &lt;strong>euros (N €)&lt;/strong>, decimales con coma. El rendimiento es poco
sensible al país, pero su coste asociado (coste/token) se expresa en € y enlaza con el
&lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">artículo de apertura&lt;/a>.&lt;/p>
&lt;/blockquote>
&lt;h2 id="qué-cubre-esta-introducción">Qué cubre esta introducción&lt;/h2>
&lt;p>Tercer artículo de la serie de datos, &lt;em>deep dive&lt;/em> del eje de &lt;strong>rendimiento&lt;/strong>. Medir el
rendimiento de un motor de inferencia parece trivial —&amp;quot;¿cuántos tokens por segundo?&amp;quot;— y es
justo donde más se engaña la gente: dos herramientas pueden reportar resultados que difieren
en un factor de 7 para el mismo sistema. Este artículo inventaría las métricas que importan
y &lt;strong>cómo se definen&lt;/strong>, por qué la arquitectura de la herramienta sesga el dato, cómo se
halla el punto de saturación con un &lt;em>sweep&lt;/em> de concurrencia, y la ficha de cada framework.
Sin recomendaciones: la elección de motor se decide en el artículo de Pareto (B8); aquí
solo están los hechos y la metodología, porque &lt;strong>un benchmark sin metodología publicada no
es comparable&lt;/strong>.&lt;/p>
&lt;hr>
&lt;h2 id="las-métricas-de-rendimiento">Las métricas de rendimiento&lt;/h2>
&lt;p>No hay una métrica de rendimiento, hay cinco, y mezclarlas es la primera fuente de error:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Definición&lt;/th>
&lt;th>Unidad&lt;/th>
&lt;th>Fase que domina&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>TTFT&lt;/strong> (Time To First Token)&lt;/td>
&lt;td>tiempo desde que se envía el prompt hasta el primer token&lt;/td>
&lt;td>ms&lt;/td>
&lt;td>prefill&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TPOT / ITL&lt;/strong> (Time Per Output Token / Inter-Token Latency)&lt;/td>
&lt;td>tiempo medio entre tokens de salida una vez empezada la generación&lt;/td>
&lt;td>ms/token&lt;/td>
&lt;td>decode&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Request throughput&lt;/strong>&lt;/td>
&lt;td>ciclos completos petición-respuesta por segundo a la concurrencia probada&lt;/td>
&lt;td>req/s&lt;/td>
&lt;td>ambas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Token throughput&lt;/strong>&lt;/td>
&lt;td>tokens totales (entrada + salida) por segundo entre todas las peticiones concurrentes&lt;/td>
&lt;td>tok/s&lt;/td>
&lt;td>ambas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Goodput&lt;/strong>&lt;/td>
&lt;td>porcentaje de peticiones que &lt;strong>cumplen el SLO&lt;/strong> definido&lt;/td>
&lt;td>tok/s útiles&lt;/td>
&lt;td>ambas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>P50 / P95 / P99&lt;/strong>&lt;/td>
&lt;td>percentiles de latencia (no la media)&lt;/td>
&lt;td>ms&lt;/td>
&lt;td>—&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Definiciones precisas, porque cada herramienta las calcula a su manera (&lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">Anyscale ·
métricas de latencia y throughput&lt;/a>):
el &lt;strong>TTFT&lt;/strong> es lo que un usuario espera antes de ver el primer carácter, dominado por el
cómputo de prefill; la &lt;strong>ITL&lt;/strong> es el tiempo medio entre tokens sucesivos de salida y fija la
&amp;ldquo;velocidad de tecleo&amp;rdquo; percibida de la respuesta; el &lt;strong>request throughput&lt;/strong> son ciclos
completos por segundo; el &lt;strong>token throughput&lt;/strong> son tokens totales (entrada más salida) por
segundo entre todas las peticiones concurrentes.&lt;/p>
&lt;h3 id="la-descomposición-de-la-latencia">La descomposición de la latencia&lt;/h3>
&lt;p>La latencia total de una petición de \(N\) tokens de salida se descompone:&lt;/p>
$$\text{latencia} \approx \text{TTFT} + (N-1)\times \text{TPOT}$$
&lt;p>Por eso TTFT y TPOT se reportan &lt;strong>por separado&lt;/strong>: una misma media de latencia esconde
perfiles muy distintos. Un sistema con TTFT alto y TPOT bajo (prefill caro, decode rápido)
y otro al revés pueden tener la misma latencia media para una longitud concreta, pero se
comportan de forma opuesta al cambiar el tamaño de la respuesta. Para una experiencia de
chat interactivo manda el TTFT y el TPOT; para un batch de resúmenes largos, el token
throughput. Medir la media oculta ambas realidades.&lt;/p>
&lt;h3 id="goodput-la-métrica-honesta">Goodput: la métrica honesta&lt;/h3>
&lt;p>El &lt;strong>throughput bruto&lt;/strong> (TPS, RPS) dice cuánto trabajo hace el sistema; el &lt;strong>goodput&lt;/strong> dice
cuánto de ese trabajo &lt;strong>cumple tus estándares de calidad de servicio&lt;/strong> (SLO)
(&lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">Anyscale&lt;/a>). Un motor puede
presumir de 10.000 tok/s agregados, pero si la mitad de las peticiones violan el SLO de P99
de TTFT, su goodput es 5.000. La cifra que se defiende en una propuesta es el &lt;strong>goodput&lt;/strong>,
no el throughput de catálogo: es lo único que se traduce en usuarios satisfechos y en un
coste por token honesto.&lt;/p>
&lt;hr>
&lt;h2 id="cómo-se-instrumenta-cada-métrica-y-dónde-se-cuela-el-error">Cómo se instrumenta cada métrica (y dónde se cuela el error)&lt;/h2>
&lt;p>Antes de comparar números conviene saber &lt;strong>dónde empieza y acaba cada reloj&lt;/strong>, porque dos
herramientas pueden llamar &amp;ldquo;TTFT&amp;rdquo; a cosas distintas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>TTFT lado cliente vs lado servidor.&lt;/strong> El TTFT medido por el cliente incluye la latencia
de red y de la cola del gateway; el medido por el servidor, no. Para una comparación de
motores interesa el del servidor; para la experiencia de usuario, el del cliente. Mezclar
ambos invalida la comparación.&lt;/li>
&lt;li>&lt;strong>Requiere streaming.&lt;/strong> El TTFT y la ITL solo se pueden medir si la respuesta llega en
&lt;em>streaming&lt;/em> (token a token). Si la herramienta mide sobre respuestas completas, no hay
TTFT real: hay latencia total disfrazada.&lt;/li>
&lt;li>&lt;strong>Conteo de tokens.&lt;/strong> El throughput en tok/s depende de &lt;strong>qué tokenizer&lt;/strong> cuenta los
tokens. Si la herramienta usa un tokenizer distinto al del modelo, el número de tokens
—y por tanto el tok/s y el coste/token— está sesgado. Hay que contar con el tokenizer del
modelo servido.&lt;/li>
&lt;li>&lt;strong>Warm-up y prefix cache.&lt;/strong> Las primeras peticiones de un benchmark se benefician del
prefix cache caliente y dan TTFT artificialmente bajo; hay que descartar el warm-up o el
resultado infla la realidad.&lt;/li>
&lt;/ul>
&lt;p>Estas cuatro decisiones de instrumentación explican buena parte de las discrepancias entre
herramientas. Un número de rendimiento sin especificar dónde se mide el reloj y con qué
tokenizer no es comparable, por muy preciso que parezca.&lt;/p>
&lt;hr>
&lt;h2 id="la-arquitectura-de-la-herramienta-sesga-el-dato">La arquitectura de la herramienta sesga el dato&lt;/h2>
&lt;p>Aquí está la trampa que invalida la mitad de los benchmarks publicados. Las herramientas se
dividen en dos clases por la &lt;strong>arquitectura del cliente que genera la carga&lt;/strong>, y esa
arquitectura determina si la medida es fiable a alta concurrencia:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Micro-bench mono-proceso&lt;/strong> (vLLM bench, SGLang bench, genai-perf): un cliente Python
con asyncio en un solo proceso. Útiles para experimentos rápidos sobre un motor concreto,
pero la arquitectura mono-proceso &lt;strong>introduce un cuello de botella en el lado cliente&lt;/strong>
que &lt;strong>sesga los datos a alta concurrencia&lt;/strong> (&lt;a href="https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e">genAI-perf y vLLM&lt;/a>):
el cliente no consigue generar carga suficiente y mides el límite del cliente, no el del
motor.&lt;/li>
&lt;li>&lt;strong>Carga multi-proceso&lt;/strong> (GuideLLM, AIPerf): reparten la generación de carga entre varios
procesos, evitando ese límite. Son la clase que ha emergido para medir a escala real.&lt;/li>
&lt;/ul>
&lt;p>La magnitud del sesgo es enorme: a 1.000 QPS, un benchmark mono-proceso llegó a procesar
&lt;strong>75.574 tokens&lt;/strong> frente a los &lt;strong>545.733 tokens&lt;/strong> de una arquitectura distribuida — una
&lt;strong>discrepancia de 7,2×&lt;/strong> en la capacidad de medición para el mismo sistema ([búsqueda]).
Quien compare dos motores con herramientas de clases distintas no está comparando los
motores: está comparando los clientes de benchmark.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 210" role="img" aria-label="Dos clases de herramientas de benchmark: cliente mono-proceso que se satura a alta concurrencia frente a generador de carga multi-proceso que mide el motor" xmlns="http://www.w3.org/2000/svg">
&lt;style>.bx{fill:none;stroke:currentColor;stroke-width:1.3}.dsh{fill:none;stroke:currentColor;stroke-width:1.3;stroke-dasharray:5 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}.ar{fill:none;stroke:currentColor;stroke-width:1.3;marker-end:url(#bm)}&lt;/style>
&lt;defs>&lt;marker id="bm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="currentColor"/>&lt;/marker>&lt;/defs>
&lt;text x="20" y="26" class="tl">Mono-proceso (vLLM bench, SGLang bench, genai-perf)&lt;/text>
&lt;rect class="bx" x="20" y="36" width="150" height="40" rx="6"/>
&lt;text x="32" y="62" class="ts">1 cliente asyncio&lt;/text>
&lt;path class="ar" d="M170,58 L215,58"/>
&lt;rect class="dsh" x="215" y="36" width="120" height="40" rx="6"/>
&lt;text x="227" y="56" class="ts">cuello cliente&lt;/text>
&lt;text x="227" y="71" class="ts">sesga a alta conc.&lt;/text>
&lt;path class="ar" d="M335,58 L380,58"/>
&lt;rect class="bx" x="380" y="36" width="110" height="40" rx="6"/>
&lt;text x="392" y="62" class="ts">motor (vLLM…)&lt;/text>
&lt;text x="510" y="52" class="ts">mides el cliente,&lt;/text>
&lt;text x="510" y="68" class="ts">no el motor&lt;/text>
&lt;text x="20" y="118" class="tl">Multi-proceso (GuideLLM, AIPerf)&lt;/text>
&lt;rect class="bx" x="20" y="128" width="150" height="46" rx="6"/>
&lt;text x="32" y="148" class="ts">N procesos de carga&lt;/text>
&lt;text x="32" y="164" class="ts">(carga real)&lt;/text>
&lt;path class="ar" d="M170,151 L380,151"/>
&lt;rect class="bx" x="380" y="131" width="110" height="40" rx="6"/>
&lt;text x="392" y="155" class="ts">motor (vLLM…)&lt;/text>
&lt;text x="510" y="145" class="ts">mides el motor;&lt;/text>
&lt;text x="510" y="161" class="ts">7,2× más capacidad&lt;/text>
&lt;/svg>
&lt;/div>
&lt;hr>
&lt;h2 id="frameworks-ficha-a-ficha">Frameworks, ficha a ficha&lt;/h2>
&lt;h3 id="vllm-bench-y-sglang-bench--micro-bench-del-motor">vLLM bench y SGLang bench — micro-bench del motor&lt;/h3>
&lt;p>Qué miden: TTFT, TPOT y throughput del propio motor (vLLM o SGLang). Clase: micro-bench
mono-proceso. Uso: experimentos rápidos para tunear un motor concreto y ver el efecto de
sus optimizaciones (ver &lt;a href="https://blog.lo0.es/posts/decode-optimizaciones-vllm/">decode&lt;/a> y
&lt;a href="https://blog.lo0.es/posts/prefill-optimizaciones-vllm/">prefill&lt;/a>). Límite: se saturan en el cliente a
alta concurrencia; no sirven para medir la capacidad real a escala.&lt;/p>
&lt;h3 id="aiperf--el-de-nvidia-ex-genai-perf-multi-proceso">AIPerf — el de NVIDIA (ex genai-perf), multi-proceso&lt;/h3>
&lt;p>Qué mide: TTFT, ITL, throughput y latencia sobre &lt;strong>vLLM, NIM, TGI y cualquier endpoint
compatible&lt;/strong>. Clase: carga multi-proceso. Dato de estado del arte: NVIDIA &lt;strong>jubiló
genai-perf y lo sustituyó por AIPerf el 15 de abril de 2026&lt;/strong>. AIPerf, durante el &lt;em>sweep&lt;/em>,
&lt;strong>detecta la saturación de la GPU&lt;/strong> e identifica la iteración anterior, devolviéndola como
&lt;code>estimatedCapacity&lt;/code>; si no detecta saturación, &lt;code>estimatedCapacity&lt;/code> es la última iteración
probada —por eso el sweep tiene que extenderse &lt;strong>más allá del codo&lt;/strong> (&lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">AIPerf&lt;/a>).&lt;/p>
&lt;h3 id="guidellm--del-proyecto-vllm-orientado-a-slo">GuideLLM — del proyecto vLLM, orientado a SLO&lt;/h3>
&lt;p>Qué mide: distribuciones completas de TTFT, ITL y comportamiento de extremo a extremo, para
evaluación &lt;strong>dirigida por SLO&lt;/strong>. Clase: carga multi-proceso. Diferenciador: genera patrones
de tráfico realistas y configurables en modos &lt;strong>síncrono, concurrente y por tasa&lt;/strong>,
incluyendo &lt;strong>sweeps reproducibles&lt;/strong> para identificar rangos de operación seguros
(&lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">Red Hat · GuideLLM&lt;/a>,
&lt;a href="https://github.com/vllm-project/guidellm">GuideLLM · GitHub&lt;/a>). Es la herramienta para
responder &amp;ldquo;¿hasta dónde puedo cargar este motor sin romper el SLO?&amp;rdquo;.&lt;/p>
&lt;h3 id="llmperf--el-clásico-de-anyscaleray">LLMPerf — el clásico de Anyscale/Ray&lt;/h3>
&lt;p>Qué mide: throughput y latencia a nivel de inferencia. Uso: validación de endpoints, muy
extendido históricamente. Clase: carga. Límite: menos centrado en distribuciones y sweeps
que GuideLLM/AIPerf.&lt;/p>
&lt;h3 id="mlperf-inference--el-estándar-de-la-industria">MLPerf Inference — el estándar de la industria&lt;/h3>
&lt;p>Qué mide: rendimiento bajo &lt;strong>escenarios normalizados&lt;/strong> con reglas estrictas, para
comparabilidad entre fabricantes. Mantenedor: &lt;strong>MLCommons&lt;/strong>. Es el patrón oro de
comparabilidad cross-vendor; se desarrolla abajo en detalle.&lt;/p>
&lt;h3 id="tabla-comparativa">Tabla comparativa&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Herramienta&lt;/th>
&lt;th>Clase&lt;/th>
&lt;th>Qué mide&lt;/th>
&lt;th>Mantenedor&lt;/th>
&lt;th>Cuándo usarla&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM bench&lt;/strong>&lt;/td>
&lt;td>micro mono-proceso&lt;/td>
&lt;td>TTFT, TPOT, throughput de vLLM&lt;/td>
&lt;td>vLLM (OSS)&lt;/td>
&lt;td>tunear vLLM, experimentos rápidos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang bench&lt;/strong>&lt;/td>
&lt;td>micro mono-proceso&lt;/td>
&lt;td>métricas del motor SGLang&lt;/td>
&lt;td>SGLang (OSS)&lt;/td>
&lt;td>tunear SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>AIPerf&lt;/strong> (ex genai-perf)&lt;/td>
&lt;td>carga multi-proceso&lt;/td>
&lt;td>TTFT, ITL, throughput; estimatedCapacity&lt;/td>
&lt;td>NVIDIA (OSS)&lt;/td>
&lt;td>capacidad real, multi-endpoint&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>GuideLLM&lt;/strong>&lt;/td>
&lt;td>carga multi-proceso&lt;/td>
&lt;td>distribuciones, SLO, sweeps&lt;/td>
&lt;td>vLLM (OSS)&lt;/td>
&lt;td>validar SLO, hallar el codo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LLMPerf&lt;/strong>&lt;/td>
&lt;td>carga&lt;/td>
&lt;td>throughput y latencia&lt;/td>
&lt;td>Anyscale/Ray (OSS)&lt;/td>
&lt;td>validación de endpoints&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>MLPerf Inference&lt;/strong>&lt;/td>
&lt;td>suite estándar&lt;/td>
&lt;td>escenarios server/offline/interactive&lt;/td>
&lt;td>MLCommons&lt;/td>
&lt;td>comparabilidad cross-vendor&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;hr>
&lt;h2 id="el-sweep-de-concurrencia-hallar-el-codo">El sweep de concurrencia: hallar el codo&lt;/h2>
&lt;p>La medida más útil para dimensionar no es un número, es una &lt;strong>curva&lt;/strong>: cómo cambian la
latencia y el throughput a medida que sube la concurrencia. Subir la concurrencia mantiene
la GPU más ocupada y sube el RPS, pero a partir de cierto punto &lt;strong>dispara el TTFT, la ITL y
la latencia de extremo a extremo&lt;/strong> ([búsqueda]). El objetivo del &lt;em>sweep&lt;/em> es encontrar el
&lt;strong>codo&lt;/strong> (&lt;em>knee&lt;/em>): la concurrencia máxima donde el throughput sigue subiendo sin que la
latencia rompa el SLO.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 260" role="img" aria-label="Curva del sweep de concurrencia: el throughput sube y satura mientras la latencia se dispara más allá del codo, que marca la capacidad segura bajo SLO" xmlns="http://www.w3.org/2000/svg">
&lt;style>.ax{fill:none;stroke:currentColor;stroke-width:1}.cv{fill:none;stroke:currentColor;stroke-width:1.6}.dsh{fill:none;stroke:currentColor;stroke-width:1;stroke-dasharray:4 3}.tl{font:600 12px sans-serif;fill:currentColor}.ts{font:11px sans-serif;fill:currentColor}&lt;/style>
&lt;line class="ax" x1="60" y1="40" x2="60" y2="200"/>
&lt;line class="ax" x1="60" y1="200" x2="720" y2="200"/>
&lt;text x="20" y="120" class="ts" transform="rotate(-90 20 120)">métrica&lt;/text>
&lt;text x="360" y="228" class="ts">concurrencia →&lt;/text>
&lt;path class="cv" d="M60,190 C200,150 300,120 400,112 C520,104 620,100 700,98"/>
&lt;text x="600" y="92" class="ts">throughput (satura)&lt;/text>
&lt;path class="cv" d="M60,180 C260,176 360,170 430,150 C520,120 600,70 700,46"/>
&lt;text x="600" y="40" class="ts">latencia (se dispara)&lt;/text>
&lt;line class="dsh" x1="430" y1="40" x2="430" y2="200"/>
&lt;text x="392" y="56" class="tl">codo (knee)&lt;/text>
&lt;text x="362" y="216" class="ts">capacidad segura bajo SLO&lt;/text>
&lt;text x="60" y="250" class="ts">Antes del codo, subir concurrencia da más throughput "gratis". Después, la latencia rompe el SLO sin apenas ganar throughput.&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>Por eso AIPerf extiende el sweep &lt;strong>más allá del codo&lt;/strong>: solo viendo dónde se dispara la
latencia se puede devolver la capacidad segura (&lt;code>estimatedCapacity&lt;/code>). Esta curva es la
materia prima del &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a>:
del codo sale el número de réplicas y el coste por token a la carga objetivo.&lt;/p>
&lt;hr>
&lt;h3 id="ejemplo-trabajado-lectura-de-un-sweep">Ejemplo trabajado: lectura de un sweep&lt;/h3>
&lt;p>Un sweep &lt;strong>ilustrativo&lt;/strong> sobre un nodo de ejemplo (un 70B en 8×H100, SLO de P99 de TTFT &amp;lt;
500 ms), para ver cómo se lee el codo:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Concurrencia&lt;/th>
&lt;th>RPS&lt;/th>
&lt;th>TTFT P50 (ms)&lt;/th>
&lt;th>TTFT P99 (ms)&lt;/th>
&lt;th>Token tput (tok/s)&lt;/th>
&lt;th>Goodput&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>1&lt;/td>
&lt;td>2&lt;/td>
&lt;td>80&lt;/td>
&lt;td>110&lt;/td>
&lt;td>350&lt;/td>
&lt;td>100 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>8&lt;/td>
&lt;td>14&lt;/td>
&lt;td>110&lt;/td>
&lt;td>240&lt;/td>
&lt;td>2.100&lt;/td>
&lt;td>100 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>16&lt;/td>
&lt;td>22&lt;/td>
&lt;td>180&lt;/td>
&lt;td>460&lt;/td>
&lt;td>3.400&lt;/td>
&lt;td>98 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>24&lt;/td>
&lt;td>26&lt;/td>
&lt;td>320&lt;/td>
&lt;td>980&lt;/td>
&lt;td>3.900&lt;/td>
&lt;td>62 %&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>32&lt;/td>
&lt;td>27&lt;/td>
&lt;td>540&lt;/td>
&lt;td>1.800&lt;/td>
&lt;td>4.000&lt;/td>
&lt;td>20 %&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Lectura: hasta concurrencia ~16 el throughput crece y el P99 se mantiene bajo el SLO
(goodput ~100 %). Entre 16 y 24 está el &lt;strong>codo&lt;/strong>: el throughput ya casi no sube (3.400 →
3.900 tok/s) pero el P99 se dispara (460 → 980 ms) y el goodput se desploma (98 % → 62 %).
A concurrencia 32 el throughput bruto es máximo (4.000 tok/s) pero &lt;strong>el goodput es 20 %&lt;/strong>:
el sistema &amp;ldquo;rinde mucho&amp;rdquo; sirviendo sobre todo peticiones que violan el SLO. La capacidad
segura defendible es la de concurrencia ~16, no la del throughput máximo. Este es el número
que entra en el &lt;a href="https://blog.lo0.es/posts/capacity-planning-inferencia-llm-on-premise/">capacity planning&lt;/a>
y en el coste por token: a 3.400 tok/s útiles, no a 4.000 tok/s brutos.&lt;/p>
&lt;hr>
&lt;h2 id="mlperf-inference-el-estándar-de-comparabilidad">MLPerf Inference: el estándar de comparabilidad&lt;/h2>
&lt;p>Para comparar &lt;strong>entre fabricantes y motores&lt;/strong> con reglas idénticas existe &lt;strong>MLPerf
Inference&lt;/strong> (MLCommons). La categoría de datacenter se centra en dos escenarios, más uno
opcional (&lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">MLCommons · datacenter&lt;/a>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Escenario&lt;/th>
&lt;th>Qué simula&lt;/th>
&lt;th>Métrica&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Offline&lt;/strong>&lt;/td>
&lt;td>throughput bruto procesando todo el dataset en batch&lt;/td>
&lt;td>máximo throughput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Server&lt;/strong>&lt;/td>
&lt;td>entorno interactivo: peticiones de una en una según Poisson a un RPS medio&lt;/td>
&lt;td>RPS bajo límites de TTFT y TPOT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Interactive&lt;/strong> (opcional)&lt;/td>
&lt;td>como server pero con &lt;strong>límites de latencia más estrictos&lt;/strong>&lt;/td>
&lt;td>RPS bajo SLO duro&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El escenario &lt;strong>Server&lt;/strong> es el realista para inferencia online: el generador de carga manda
peticiones siguiendo una distribución de Poisson y exige cumplir &lt;strong>cotas concretas de TTFT
y TPOT&lt;/strong>. &lt;strong>MLPerf Inference v5.0&lt;/strong> (abril 2025) introdujo un benchmark de &lt;strong>405B a gran
escala&lt;/strong> y uno &lt;strong>interactivo de 70B de baja latencia&lt;/strong>, ofreciendo benchmarks de lenguaje a
todas las escalas (7B a 405B), diversidad de arquitecturas (incluido MoE) y escenarios
(&lt;a href="https://mlcommons.org/2025/04/llm-inference-v5/">MLCommons · v5.0&lt;/a>); &lt;strong>v5.1&lt;/strong> (septiembre
2025) amplió los resultados con participación récord
(&lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">MLCommons · v5.1&lt;/a>).&lt;/p>
&lt;p>El valor de MLPerf es la &lt;strong>comparabilidad&lt;/strong>: todos miden lo mismo bajo las mismas reglas. Su
límite es que esas reglas pueden no coincidir con tu carga (tu distribución de longitudes,
tu SLO concreto), así que sirve para comparar hardware/motores entre sí, no necesariamente
para dimensionar tu caso —para eso, el sweep propio.&lt;/p>
&lt;hr>
&lt;h2 id="el-sesgo-de-medición-y-la-reproducibilidad">El sesgo de medición y la reproducibilidad&lt;/h2>
&lt;p>Que dos benchmarks den resultados muy distintos para el mismo sistema no es un accidente:
el sesgo sistemático de medición en benchmarks de producción está &lt;strong>caracterizado en la
literatura&lt;/strong> (&lt;a href="https://arxiv.org/html/2605.24217">arXiv 2605.24217&lt;/a>), y hay trabajo
dedicado a las &lt;strong>meta-métricas y buenas prácticas&lt;/strong> del benchmarking de rendimiento a nivel
de sistema (&lt;a href="https://arxiv.org/pdf/2508.10251">arXiv 2508.10251&lt;/a>). Las fuentes de sesgo más
comunes:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Fuente de sesgo&lt;/th>
&lt;th>Efecto&lt;/th>
&lt;th>Mitigación&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Cliente mono-proceso&lt;/td>
&lt;td>infravalora el throughput a alta concurrencia&lt;/td>
&lt;td>usar carga multi-proceso&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distribución de longitudes irreal&lt;/td>
&lt;td>resultados que no aplican a tu tráfico&lt;/td>
&lt;td>usar trazas realistas&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Medir media en vez de percentiles&lt;/td>
&lt;td>oculta la cola de latencia&lt;/td>
&lt;td>reportar P95/P99&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Warm-up no controlado&lt;/td>
&lt;td>el prefix cache infla los primeros resultados&lt;/td>
&lt;td>descartar el warm-up&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>No fijar versión de motor/modelo&lt;/td>
&lt;td>irreproducible&lt;/td>
&lt;td>pinear todo y publicarlo&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La conclusión metodológica de este artículo: &lt;strong>un benchmark sin metodología publicada no es
comparable&lt;/strong>. Para que un número de rendimiento sostenga una propuesta tiene que venir con
la herramienta, su versión, el modelo y precisión, la distribución de carga y el SLO. El
artículo de síntesis S4 monta un harness reproducible que fija todo eso.&lt;/p>
&lt;hr>
&lt;h2 id="checklist-de-un-benchmark-reproducible">Checklist de un benchmark reproducible&lt;/h2>
&lt;p>Para que un número de rendimiento sea defendible ante un comité, tiene que venir con todo
lo que permite reproducirlo. El mínimo que se publica junto al resultado:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Qué fijar&lt;/th>
&lt;th>Por qué&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Herramienta + versión&lt;/td>
&lt;td>cada una mide distinto; la versión cambia el comportamiento&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Modelo + precisión (FP16/FP8/INT4)&lt;/td>
&lt;td>la precisión cambia throughput y calidad&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Hardware (GPU, nº, interconexión)&lt;/td>
&lt;td>un 8×H100 NVLink no es 8×H100 PCIe&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Motor + versión + flags&lt;/td>
&lt;td>vLLM/SGLang/TRT-LLM y su configuración&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Distribución de longitudes (in/out)&lt;/td>
&lt;td>el tráfico real no es de longitud fija&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Niveles de concurrencia del sweep&lt;/td>
&lt;td>hay que pasar el codo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SLO (qué percentil, qué umbral)&lt;/td>
&lt;td>define el goodput&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tratamiento del warm-up&lt;/td>
&lt;td>descartarlo o sesga el resultado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Tokenizer usado para contar&lt;/td>
&lt;td>afecta a tok/s y coste/token&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La regla práctica: si no puedes entregar esta tabla junto a la cifra, la cifra no es un
dato, es una anécdota. El harness reproducible del artículo S4 automatiza el registro de
todos estos parámetros para que cualquiera —incluido quien rebata la propuesta— pueda
reproducir el número exacto.&lt;/p>
&lt;hr>
&lt;h2 id="rendimiento--calidad">Rendimiento ≠ calidad&lt;/h2>
&lt;p>Una advertencia que evita el error más caro: estas herramientas miden &lt;strong>velocidad y
throughput, no acierto&lt;/strong>. Un motor puede ser rapidísimo sirviendo un modelo que responde
mal. La &lt;strong>calidad&lt;/strong> se mide con otra familia de herramientas —&lt;strong>lm-evaluation-harness&lt;/strong>,
&lt;strong>HELM&lt;/strong>, &lt;em>leaderboards&lt;/em> de tareas— que es &lt;strong>otro eje&lt;/strong> del cuadro de mando (artículo B7).
Confundir &amp;ldquo;rápido&amp;rdquo; con &amp;ldquo;bueno&amp;rdquo; es montar una plataforma que sirve respuestas malas muy
deprisa. En la frontera de Pareto final, rendimiento y calidad son dos ejes distintos que
hay que ver juntos, nunca uno en lugar del otro.&lt;/p>
&lt;p>La otra familia de herramientas, para referencia (se desarrolla en B7):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Herramienta&lt;/th>
&lt;th>Qué mide&lt;/th>
&lt;th>Trampa habitual&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>lm-evaluation-harness&lt;/strong>&lt;/td>
&lt;td>acierto en cientos de tareas estandarizadas&lt;/td>
&lt;td>contaminación del dataset de test&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HELM&lt;/strong>&lt;/td>
&lt;td>evaluación holística (acierto, robustez, sesgo, eficiencia)&lt;/td>
&lt;td>pesado de ejecutar&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LiveBench / leaderboards dinámicos&lt;/strong>&lt;/td>
&lt;td>tareas que rotan para evitar contaminación&lt;/td>
&lt;td>comparabilidad temporal&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>El punto de datos: la &lt;strong>contaminación&lt;/strong> —que el modelo haya visto el test en su
entrenamiento— infla las métricas de calidad igual que el warm-up infla las de rendimiento.
Por eso los leaderboards dinámicos rotan las preguntas. Calidad y rendimiento comparten esa
lección: el método de medida sesga el resultado tanto como el sistema medido.&lt;/p>
&lt;hr>
&lt;h2 id="el-trade-off-latencia-vs-throughput">El trade-off latencia vs throughput&lt;/h2>
&lt;p>Una propiedad que el sweep deja ver y que conviene tener presente: &lt;strong>latencia y throughput
tiran en direcciones opuestas&lt;/strong>. El batching agrupa peticiones para amortizar el coste de
mover los pesos desde la VRAM —sube el throughput— pero cada petición espera a que se forme
el lote, lo que &lt;strong>sube la latencia individual&lt;/strong>. Hay dos regímenes de operación, y el
benchmark sirve para situarse en el correcto:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Régimen&lt;/th>
&lt;th>Optimiza&lt;/th>
&lt;th>Configuración&lt;/th>
&lt;th>Caso&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>Latencia&lt;/strong>&lt;/td>
&lt;td>TTFT/TPOT bajos&lt;/td>
&lt;td>batch pequeño, baja concurrencia&lt;/td>
&lt;td>chat interactivo, copilotos&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Throughput&lt;/strong>&lt;/td>
&lt;td>tok/s máximos&lt;/td>
&lt;td>batch grande, alta concurrencia&lt;/td>
&lt;td>batch nocturno, ingestión&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>No existe un único &amp;ldquo;mejor&amp;rdquo; punto: existe el mejor punto &lt;strong>para tu SLO&lt;/strong>. Un benchmark que
reporta solo el throughput máximo está describiendo el régimen de throughput e ignorando si
ese punto cumple la latencia que tu caso necesita. Por eso el &lt;strong>goodput&lt;/strong> —throughput bajo
el SLO— es la métrica que reconcilia los dos regímenes: mide cuánto throughput consigues
&lt;strong>sin&lt;/strong> salirte de la latencia aceptable. El sweep recorre la curva entre ambos regímenes; tu
SLO marca dónde, en esa curva, está tu sistema.&lt;/p>
&lt;hr>
&lt;h2 id="la-conexión-con-coste-y-energía">La conexión con coste y energía&lt;/h2>
&lt;p>El rendimiento no es un eje aislado: por la identidad del &lt;a href="https://blog.lo0.es/posts/tres-ejes-coste-rendimiento-energia-inferencia-llm/">artículo de apertura&lt;/a>,
el throughput es el denominador del coste por token y de la energía por token. Un sweep que
encuentra un codo a 4.000 tok/s en vez de 2.800 no es solo &amp;ldquo;más rápido&amp;rdquo;: baja el CPM de
~1,09 a ~0,76 €/1M tok &lt;strong>y&lt;/strong> la energía por token en la misma proporción, sobre el mismo
hierro. Por eso el benchmarking de rendimiento es la herramienta que, indirectamente, más
mueve el coste: cada mejora de goodput se traduce en euros y en vatios por token. El número
que conecta los tres ejes es el &lt;strong>goodput&lt;/strong> —el throughput que cumple el SLO—, no el
throughput de catálogo.&lt;/p>
&lt;hr>
&lt;h2 id="estado-del-arte-2026">Estado del arte 2026&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Migración genai-perf → AIPerf&lt;/strong> (15-abr-2026): NVIDIA consolida su benchmarking en una
herramienta multi-proceso con detección de saturación.&lt;/li>
&lt;li>&lt;strong>GuideLLM&lt;/strong> como estándar OSS de evaluación dirigida por SLO con sweeps reproducibles.&lt;/li>
&lt;li>&lt;strong>MLPerf Inference v5.0/v5.1&lt;/strong> amplía a 405B, interactivo de 70B y MoE, con participación
récord: la comparabilidad cross-vendor madura.&lt;/li>
&lt;li>&lt;strong>Sesgo de medición caracterizado&lt;/strong>: la comunidad reconoce que el método de medida importa
tanto como el sistema medido; crece el énfasis en reproducibilidad y meta-métricas.&lt;/li>
&lt;/ul>
&lt;hr>
&lt;h2 id="límites-y-trampas-data-driven">Límites y trampas (data-driven)&lt;/h2>
&lt;ol>
&lt;li>&lt;strong>Comparar herramientas de clases distintas.&lt;/strong> Un micro-bench mono-proceso y una carga
multi-proceso no son comparables; la diferencia puede ser 7×. Fija la clase.&lt;/li>
&lt;li>&lt;strong>Throughput de catálogo en vez de goodput.&lt;/strong> El número honesto es el que cumple el SLO.&lt;/li>
&lt;li>&lt;strong>Medias en vez de percentiles.&lt;/strong> La media oculta la cola; reporta P95/P99.&lt;/li>
&lt;li>&lt;strong>No extender el sweep más allá del codo.&lt;/strong> Sin ver dónde se dispara la latencia no
conoces la capacidad segura.&lt;/li>
&lt;li>&lt;strong>Confundir rendimiento con calidad.&lt;/strong> Son ejes distintos; rápido no es bueno.&lt;/li>
&lt;li>&lt;strong>No pinear versiones.&lt;/strong> Motor, modelo, precisión y carga sin fijar = irreproducible = no
defendible.&lt;/li>
&lt;/ol>
&lt;p>El siguiente artículo del track (B2) entra en el catálogo de herramientas a fondo; este fija
las métricas y la metodología. Con el rendimiento medido de forma reproducible, el cuadro de
mando puede cruzarlo con el coste (en €) y la energía para la decisión final.&lt;/p>
&lt;h2 id="cierre">Cierre&lt;/h2>
&lt;p>El benchmarking de rendimiento parece el eje más &amp;ldquo;objetivo&amp;rdquo; de los tres —al fin y al cabo,
son tokens por segundo— y es justo donde más se manipula, casi siempre sin mala intención:
una herramienta mono-proceso aquí, una media en vez de un P99 allá, un throughput de
catálogo en vez del goodput. La diferencia entre un número de marketing y un dato defendible
no está en el motor medido, sino en la &lt;strong>metodología&lt;/strong>: la clase de herramienta, dónde se
mide el reloj, con qué tokenizer, hasta dónde llega el sweep y qué SLO define el goodput.
Para una propuesta de arquitectura soberana, el rendimiento solo vale si se entrega con esa
ficha de reproducibilidad —y, cruzado con el coste en euros y la energía por token, se
convierte en la columna de la frontera de Pareto que decide qué motor y qué configuración
sostienen la plataforma. El número que se defiende no es el más alto: es el &lt;strong>goodput
reproducible&lt;/strong>.&lt;/p>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/comparativa-motores-serving-pareto/">Comparativa de motores de serving (vLLM/SGLang/TRT-LLM/Dynamo)&lt;/a> — la elección del motor tras medir el goodput: de la metodología de esta ficha a la frontera de Pareto.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/sesgo-medicion-reproducibilidad-bench/">Sesgo de medición y reproducibilidad&lt;/a> — fuentes de sesgo sistemático en benchmarks de rendimiento y cómo controlarlas para obtener datos defendibles.&lt;/li>
&lt;/ul>
&lt;h2 id="fuentes">Fuentes&lt;/h2>
&lt;ul>
&lt;li>Anyscale · métricas de latencia y throughput de LLM — &lt;a href="https://docs.anyscale.com/llm/serving/benchmarking/metrics">https://docs.anyscale.com/llm/serving/benchmarking/metrics&lt;/a>&lt;/li>
&lt;li>Red Hat · GuideLLM: evaluar despliegues LLM para inferencia real — &lt;a href="https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference">https://developers.redhat.com/articles/2025/06/20/guidellm-evaluate-llm-deployments-real-world-inference&lt;/a>&lt;/li>
&lt;li>GuideLLM · GitHub (proyecto vLLM) — &lt;a href="https://github.com/vllm-project/guidellm">https://github.com/vllm-project/guidellm&lt;/a>&lt;/li>
&lt;li>NVIDIA AIPerf · guía de benchmarking (ex genai-perf) — &lt;a href="https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/">https://lucaberton.com/blog/nvidia-aiperf-llm-inference-benchmarking-guide/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference datacenter (escenarios) — &lt;a href="https://mlcommons.org/benchmarks/inference-datacenter/">https://mlcommons.org/benchmarks/inference-datacenter/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.0 (405B + 70B interactivo) — &lt;a href="https://mlcommons.org/2025/04/llm-inference-v5/">https://mlcommons.org/2025/04/llm-inference-v5/&lt;/a>&lt;/li>
&lt;li>MLCommons · MLPerf Inference v5.1 — &lt;a href="https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/">https://mlcommons.org/2025/09/mlperf-inference-v5-1-results/&lt;/a>&lt;/li>
&lt;li>arXiv 2605.24217 · sesgo sistemático de medición en benchmarks de inferencia LLM — &lt;a href="https://arxiv.org/html/2605.24217">https://arxiv.org/html/2605.24217&lt;/a>&lt;/li>
&lt;li>arXiv 2508.10251 · meta-métricas y buenas prácticas de benchmarking de rendimiento — &lt;a href="https://arxiv.org/pdf/2508.10251">https://arxiv.org/pdf/2508.10251&lt;/a>&lt;/li>
&lt;li>Medium · LLM Inference Benchmarking (genAI-perf y vLLM) — &lt;a href="https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e">https://kchandan.medium.com/llm-inference-benchmarking-genai-perf-and-vllm-5dd06b57428e&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>