<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Continuous-Batching on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/continuous-batching/</link><description>Recent content in Continuous-Batching on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sat, 30 May 2026 16:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/continuous-batching/index.xml" rel="self" type="application/rss+xml"/><item><title>Continuous batching: la peluquería con 8 sillones que no espera al cliente lento — Orca, vLLM, chunked prefill y goodput</title><link>https://blog.lo0.es/posts/continuous-batching-fundamentos/</link><pubDate>Sat, 30 May 2026 16:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/continuous-batching-fundamentos/</guid><description>&lt;blockquote>
&lt;p>Este post complementa los de &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> (el artefacto que continuous batching gestiona), &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention&lt;/a> (la pieza de memoria que lo hace viable), &lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving&lt;/a> (la siguiente capa de optimización), &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding&lt;/a>, &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA&lt;/a> y &lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE&lt;/a> (las tres extensiones que conviven con el scheduler en producción).&lt;/p>
&lt;/blockquote>
&lt;h2 id="estás-aquí-deploy">Estás aquí: DEPLOY&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Deploy">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#7ad88f;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#cbm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#cbm)}&lt;/style>
&lt;defs>&lt;marker id="cbm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: DEPLOY · scheduler iterativo, una pieza por debajo de PagedAttention&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box idle"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box active"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>El static batching del HuggingFace Transformers original (era pre-2022) sub-utilizaba sistemáticamente el GPU por dos razones estructurales. Primera: la unidad de scheduling era la &lt;strong>request completa&lt;/strong>; la más larga del batch bloqueaba a todas las demás hasta terminar (head-of-line blocking severo, P95 TTFT cinco a diez veces peor del razonable). Segunda: cada slot del batch reservaba memoria para &lt;code>max_seq_len&lt;/code> aunque el output real fuese mucho menor; el padding waste documentado estaba entre el &lt;strong>60 % y el 80 %&lt;/strong> y la GPU SM utilization sostenida en workloads reales caía al &lt;strong>20-40 %&lt;/strong>. &lt;strong>Orca&lt;/strong> (Yu et al., OSDI 2022, FriendliAI + Seoul National University) introdujo la idea que destrabó todo: la unidad de scheduling deja de ser la request, pasa a ser &lt;strong>una iteración del decoder&lt;/strong> — un token. Tras cada iteración el scheduler puede añadir nuevas requests al batch y retirar las terminadas. &lt;strong>vLLM&lt;/strong> (Kwon et al., SOSP 2023, UC Berkeley) lo materializó open-source y production-grade gracias a &lt;strong>PagedAttention&lt;/strong>, que resuelve la fragmentación del KV cache que el continuous batching teórico provocaría con asignación dinámica. &lt;strong>SARATHI / Sarathi-Serve&lt;/strong> (Microsoft Research India, OSDI 2024) cerró el último hueco: los &lt;em>stalls&lt;/em> del prefill que pausaban los decodes activos cuando llegaba una request nueva, mediante &lt;strong>chunked prefill&lt;/strong> (dividir un prefill largo en chunks pequeños y mezclarlos con decodes en el mismo step) y &lt;em>stall-free batching&lt;/em>. &lt;strong>DistServe&lt;/strong> (Zhong et al., OSDI 2024) reformuló la métrica clave: lo que importa es &lt;strong>goodput&lt;/strong> (requests/s &lt;strong>cumpliendo SLO de TTFT y TPOT&lt;/strong>), no throughput puro. En mayo 2026, vLLM v1 trae scheduler unificado con chunked prefill always-on; SGLang añade RadixAttention que da hits de prefix-cache cross-request; TensorRT-LLM lo llama &lt;em>in-flight batching&lt;/em>; llama.cpp lo soporta nativo. Las tres tensiones operacionales son &lt;strong>speculative decoding&lt;/strong> (nested raggedness), &lt;strong>multi-LoRA&lt;/strong> (cada request con su adapter) y &lt;strong>MoE&lt;/strong> (cada experto ve poquísimos tokens por step a batch típico). Este post desmonta el mecanismo, las matemáticas (utilización GPU, goodput vs throughput), las tres variantes (Orca → vLLM → Sarathi-Serve), los pitfalls (preempt-on-OOM, starvation, HoL inverso) y los números reales con configuraciones de producción.&lt;/p>
&lt;h2 id="la-analogía-la-peluquería-con-8-sillones">La analogía: la peluquería con 8 sillones&lt;/h2>
&lt;p>Una peluquería con 8 sillones y un único peluquero brillante que se mueve entre ellos. Llegan clientes con necesidades muy distintas: unos quieren un corte de 15 minutos, otros un tinte con base de 2 horas, otros un alisado de 90 minutos. La pregunta es cómo organizar el flujo.&lt;/p>
&lt;p>La &lt;strong>estrategia tradicional&lt;/strong> (lo que hacía HuggingFace Transformers en su &lt;code>generate()&lt;/code> original) es sentar a 8 clientes a la vez, todos al mismo tiempo, y no aceptar a nadie nuevo hasta que &lt;strong>el último&lt;/strong> termine. Si entre esos 8 hay uno de 2 horas, los 7 que querían el corte de 15 minutos están sentados parados durante 1 hora y 45 minutos. El peluquero acaba su trabajo con los rápidos y se queda mirando a los sillones vacíos hasta que el del tinte termine. Cuando todos están listos, entran otros 8. Es el &lt;strong>static batching&lt;/strong>, y lo único que evita que sea peor es que la GPU no se queja como un cliente humano.&lt;/p>
&lt;p>La &lt;strong>estrategia continua&lt;/strong> (Orca, vLLM) cambia la unidad de planificación. El peluquero no piensa &amp;ldquo;voy a hacer un cliente completo y luego el siguiente&amp;rdquo;; piensa &amp;ldquo;en cada &lt;strong>tick&lt;/strong> doy un paso de trabajo en cada sillón ocupado y, cada vez que un sillón se libera, llamo al siguiente cliente de la cola &lt;strong>sin esperar a que los demás terminen&lt;/strong>&amp;rdquo;. El cliente del corte rápido sale a los 15 minutos, su sillón se ocupa inmediatamente con el siguiente, y los lentos siguen su ritmo sin retrasar a nadie. El peluquero &lt;strong>nunca está parado&lt;/strong>.&lt;/p>
&lt;p>La &lt;strong>estrategia continua con prefill chunked&lt;/strong> (SARATHI / Sarathi-Serve) añade una distinción más sutil. Algunos clientes necesitan una &lt;strong>fase inicial larga&lt;/strong> (un análisis capilar de 10 minutos antes del corte; en términos LLM, el &lt;em>prefill&lt;/em> del prompt). Sin chunked prefill, el peluquero tenía que parar todos los demás sillones para hacer el análisis del cliente nuevo de un tirón — ese era un &lt;em>stall&lt;/em> visible en el TPOT de los activos. Con chunked prefill, el análisis se divide en piezas de 2 minutos que se intercalan entre los cortes activos de los demás. Los clientes en curso ya no notan parones; el cliente nuevo tarda un poco más en empezar su corte propiamente, pero la peluquería entera no se congela.&lt;/p>
&lt;p>Y la métrica que importa: el dueño no quiere maximizar &amp;ldquo;clientes atendidos por hora&amp;rdquo; a costa de que algunos se vayan furiosos. Quiere maximizar &amp;ldquo;clientes atendidos por hora &lt;strong>dentro del SLA de tiempo&lt;/strong>&amp;rdquo; — eso es el &lt;strong>goodput&lt;/strong>, contribución de DistServe.&lt;/p>
&lt;h2 id="el-problema-que-continuous-batching-resuelve">El problema que continuous batching resuelve&lt;/h2>
&lt;p>Hay dos patologías estructurales del static batching que merecen ser explicadas con números concretos.&lt;/p>
&lt;p>&lt;strong>Padding waste.&lt;/strong> Cada slot del batch reservaba memoria para &lt;code>max_seq_len&lt;/code> (prompt + max output), aunque el output real terminase en muchos menos tokens. Para un batch de 32 con output lengths distribuidos heterogéneamente (la mitad ≤50 tokens, una cola larga hasta 4 000), el desperdicio típico de memoria era del 60-80 %. Esto se traducía directamente en concurrencia desperdiciada: con la misma VRAM, en lugar de servir 32 requests con asignación inteligente, servías 8.&lt;/p>
&lt;p>&lt;strong>HoL blocking (Head-of-Line).&lt;/strong> La unidad de scheduling era la request completa. Un batch que contenía una request de 500 tokens y 31 de ≤50 tokens corría 450 iteraciones extra &amp;ldquo;vacías&amp;rdquo; (la GPU ejecutaba forward passes para los 32 slots, aunque 31 ya hubiesen terminado). Coste computacional desperdiciado: ~84 % del tiempo del &lt;em>tail&lt;/em> en el ejemplo.&lt;/p>
&lt;p>&lt;strong>Resultado medible.&lt;/strong> GPU SM utilization sostenida en workloads reales bajo static batching: &lt;strong>20-40 %&lt;/strong>. Es decir, ~70 % del compute del datacenter no se aprovechaba. Cuando llegaron los primeros benchmarks de Orca y vLLM mostrando 10-24× mejora de throughput, no era exageración de marketing; era recuperar todo ese compute desperdiciado.&lt;/p>
&lt;h2 id="orca-osdi-22-la-idea-que-cambió-todo">Orca (OSDI &amp;lsquo;22): la idea que cambió todo&lt;/h2>
&lt;p>El paper de Yu, Jeong, Kim, Kim y Chun en OSDI 2022 (&amp;ldquo;Orca: A Distributed Serving System for Transformer-Based Generative Models&amp;rdquo;, del Seoul National University + FriendliAI) introdujo dos contribuciones que han quedado como base de todo lo que vino después.&lt;/p>
&lt;p>&lt;strong>Iteration-level scheduling.&lt;/strong> En lugar de planificar a nivel de request completa, planifica a nivel de &lt;strong>una iteración del decoder&lt;/strong>: el step que genera UN token. Tras cada iteración el scheduler puede (a) añadir nuevas requests al batch, (b) retirar requests que han generado EOS o llegado a &lt;code>max_tokens&lt;/code>, (c) reordenar prioridades. El engine de cómputo ejecuta exactamente una iteración sobre el batch actual.&lt;/p>
&lt;p>&lt;strong>Selective batching.&lt;/strong> Aquí está la sutileza no obvia. El problema técnico de batchear requests con longitudes y estados de KV cache distintos es que algunas operaciones (los GEMMs de Q, K, V projections y FFN) son &lt;strong>insensibles a la posición&lt;/strong> y se pueden batchear concatenando tokens, mientras que la &lt;strong>atención&lt;/strong> sí es sensible al estado per-request (cada request tiene su propio KV cache de longitud distinta). La solución de Orca: batchear los GEMMs (concatenar todos los tokens del step en un tensor &lt;code>[total_tokens, hidden]&lt;/code>) y ejecutar la atención &lt;strong>secuencialmente por request&lt;/strong>.&lt;/p>
&lt;p>Resultado paper: hasta &lt;strong>36.9× throughput&lt;/strong> vs FasterTransformer en GPT-3 175B al mismo nivel de latencia. Orca no es open-source — solo está documentado en el paper. FriendliAI lo comercializa como Friendli Engine. Pero la idea se publicó y fue adoptada por todos.&lt;/p>
&lt;h2 id="vllm-sosp-23-la-materialización-open-source">vLLM (SOSP &amp;lsquo;23): la materialización open-source&lt;/h2>
&lt;p>Lo que Orca describió en concepto, vLLM lo materializó en producción. El paper de Kwon, Li, Zhuang, Sheng et al. (UC Berkeley Sky Computing Lab, SOSP 2023) introduce &lt;strong>PagedAttention&lt;/strong> —el detalle está en &lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention deep dive&lt;/a>— pero también consolida el continuous batching como práctica universal.&lt;/p>
&lt;p>La razón por la que PagedAttention es &lt;strong>prerrequisito&lt;/strong> del continuous batching práctico es la fragmentación. Si vas a insertar y retirar requests dinámicamente del batch, y cada request tiene un KV cache que crece en cada iteración, la asignación contigua tradicional fragmenta la HBM hasta dejarla inservible. PagedAttention parte el KV cache en bloques de tamaño fijo (default 16 tokens) asignados on-demand desde un pool global. El memory waste cae de ~60-80 % a &lt;strong>menos del 4 %&lt;/strong> (solo el último bloque parcialmente lleno por sequence).&lt;/p>
&lt;p>Métricas paper vLLM (2023):&lt;/p>
&lt;ul>
&lt;li>vs HuggingFace Transformers (static, sin continuous): hasta &lt;strong>24× throughput&lt;/strong>.&lt;/li>
&lt;li>vs HuggingFace TGI (que ya tenía continuous batching primitivo): &lt;strong>~3.5×&lt;/strong>.&lt;/li>
&lt;li>vs FasterTransformer: &lt;strong>2-4×&lt;/strong> a misma latencia.&lt;/li>
&lt;/ul>
&lt;p>Lo que diferencia operacionalmente vLLM de Orca: vLLM ejecuta atención en un único kernel CUDA fusionado (&lt;code>paged_attention_kernel&lt;/code>) sobre bloques no contiguos; Orca describía la atención request-by-request secuencial. Y vLLM expone APIs OpenAI-compatible que permiten dropear el engine en stacks existentes sin tocar el cliente.&lt;/p>
&lt;h2 id="chunked-prefill-sarathi--sarathi-serve-osdi-24">Chunked prefill (SARATHI / Sarathi-Serve, OSDI &amp;lsquo;24)&lt;/h2>
&lt;p>Hay un detalle que el continuous batching de Orca/vLLM original no resolvía: cuando una request &lt;strong>nueva&lt;/strong> entra al batch, su &lt;strong>prefill&lt;/strong> (procesamiento del prompt completo de una vez) puede tardar cientos de milisegundos. Durante ese tiempo, los decodes activos de las otras requests están esencialmente pausados — el GPU está dedicado al prefill nuevo. Esto se observaba como &lt;strong>spikes en TPOT&lt;/strong> (&amp;ldquo;inter-token latency&amp;rdquo;) cada vez que entraba una request larga, lo que rompía SLAs estrictos.&lt;/p>
&lt;p>SARATHI (Agrawal et al., arXiv 2308.16369, agosto 2023) y luego Sarathi-Serve (mismo grupo de Microsoft Research India, OSDI 2024, arXiv 2403.02310) introducen dos ideas combinadas:&lt;/p>
&lt;p>&lt;strong>Chunked prefill.&lt;/strong> Un prefill largo (e.g., 8 192 tokens) se divide en chunks (e.g., 2 048 tokens) que se procesan uno por iteración. En lugar de un step de 200 ms procesando 8K tokens, cuatro steps de 50 ms procesando 2K cada uno.&lt;/p>
&lt;p>&lt;strong>Decode-maximal batching (&amp;ldquo;stall-free&amp;rdquo;).&lt;/strong> En cada iteración, el scheduler primero llena el batch con los decodes activos (cada uno cuesta 1 token), y solo el espacio sobrante se usa para chunks de prefill nuevos. Resultado: los decodes activos siguen avanzando 1 token cada iteración &lt;strong>sin pausar&lt;/strong>, mientras la request nueva va completando su prefill en bocados pequeños.&lt;/p>
&lt;p>La observación que lo justifica: prefill es &lt;strong>compute-bound&lt;/strong> (procesa N tokens de golpe → satura FLOPs) mientras decode es &lt;strong>memory-bound&lt;/strong> (1 token por step → infrautiliza compute, la GPU espera por HBM). Mezclar prefill chunks con decodes en el mismo step explota el slack de arithmetic intensity: los decodes &amp;ldquo;se piggyback&amp;rdquo; sobre el compute libre del prefill chunk.&lt;/p>
&lt;p>Números:&lt;/p>
&lt;ul>
&lt;li>SARATHI original (LLaMA-13B en A6000): decode throughput &lt;strong>+10×&lt;/strong>, end-to-end &lt;strong>+1.33×&lt;/strong>.&lt;/li>
&lt;li>Sarathi-Serve (Mistral-7B en A100): &lt;strong>2.6×&lt;/strong> serving capacity vs vLLM puro. Yi-34B en 2×A100: &lt;strong>3.7×&lt;/strong>. Falcon-180B con pipeline parallel: &lt;strong>5.6×&lt;/strong>.&lt;/li>
&lt;/ul>
&lt;p>Adopción mayo 2026: &lt;strong>always-on&lt;/strong> en vLLM v1 (default desde v0.8.0, enero 2025), SGLang, TensorRT-LLM. Configuración clave en vLLM: &lt;code>--max-num-batched-tokens&lt;/code> (token budget por step; default 2048). Subirlo prioriza throughput, bajarlo prioriza TPOT bajo.&lt;/p>
&lt;h2 id="goodput-la-métrica-que-importa-distserve-osdi-24">Goodput: la métrica que importa (DistServe, OSDI &amp;lsquo;24)&lt;/h2>
&lt;p>La métrica clásica &amp;ldquo;throughput&amp;rdquo; (requests/s o tokens/s) tiene un problema cuando hay SLOs. Un servidor puede reportar 1 000 req/s mientras el P99 TTFT es 30 segundos y el SLO es 1 segundo — solo unas 200 req/s realmente cumplen el contrato.&lt;/p>
&lt;p>&lt;strong>DistServe&lt;/strong> (Zhong et al., OSDI 2024) formaliza &lt;strong>goodput&lt;/strong> como la métrica correcta: &lt;code>goodput = max request rate sostenido cumpliendo los SLOs (TTFT bound AND TPOT bound)&lt;/code>. La definición práctica suele ser: máximo rate con ≥90 % de requests dentro de ambos SLOs.&lt;/p>
&lt;p>Por qué importa para el scheduler:&lt;/p>
&lt;ul>
&lt;li>Optimizar throughput puro lleva a maximizar batch size, lo que infla P99 TPOT.&lt;/li>
&lt;li>Optimizar goodput &lt;strong>limita&lt;/strong> el batch size cuando el TPOT empieza a violar SLO, prefiere requests pequeñas si el batch ya tiene tail, y deja recursos disponibles para nuevas requests.&lt;/li>
&lt;/ul>
&lt;p>Resultado DistServe: hasta &lt;strong>7.4× más requests servidas&lt;/strong> o &lt;strong>12.6× SLO más estricto&lt;/strong> vs vLLM al mismo SLO attainment. La ganancia viene de &lt;strong>desagregar prefill y decode en GPUs distintas&lt;/strong> (eliminando la interferencia entre fases), pero la idea de optimizar para goodput es independiente y aplicable a cualquier scheduler.&lt;/p>
&lt;p>Operacionalmente esto se traduce en monitorización:&lt;/p>
&lt;pre tabindex="0">&lt;code>goodput_proxy = histogram_quantile(0.95, vllm:time_to_first_token_seconds_bucket) &amp;lt; SLO_TTFT
AND histogram_quantile(0.95, vllm:time_per_output_token_seconds_bucket) &amp;lt; SLO_TPOT
&lt;/code>&lt;/pre>&lt;h2 id="el-scheduler-iterativo-en-acción">El scheduler iterativo en acción&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 320" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Continuous batching scheduler timeline">
&lt;style>
.req1{fill:#cdebd0;stroke:#2a7a40;stroke-width:1.2;rx:3}
.req2{fill:#d4ecff;stroke:#1f5fa8;stroke-width:1.2;rx:3}
.req3{fill:#fff4d6;stroke:#a48000;stroke-width:1.2;rx:3}
.req4{fill:#f6caca;stroke:#a52a2a;stroke-width:1.2;rx:3}
.req5{fill:#e6d0ff;stroke:#5a2db0;stroke-width:1.2;rx:3}
.req6{fill:#f6e0c8;stroke:#a76b1f;stroke-width:1.2;rx:3}
.empty{fill:#f0f0f0;stroke:#999;stroke-width:1;stroke-dasharray:3 2;rx:3}
.pre{fill:#ffd76b;stroke:#a48000;stroke-width:1.2;rx:3}
.lbl{font:600 11px sans-serif;fill:#222}
.sub{font:400 9px sans-serif;fill:#555}
.tick{stroke:#444;stroke-width:1}
&lt;/style>
&lt;p>&lt;text x="20" y="20" class="lbl">Static batching — 4 slots, padding hasta max_len, llega req nueva → espera&lt;/text>
&lt;text x="20" y="38" class="sub">slot 1&lt;/text>
&lt;rect x="60" y="30" width="200" height="14" class="req1"/>
&lt;rect x="260" y="30" width="200" height="14" class="empty"/>
&lt;rect x="460" y="30" width="80" height="14" class="req5"/>
&lt;rect x="540" y="30" width="200" height="14" class="empty"/>
&lt;text x="20" y="58" class="sub">slot 2&lt;/text>
&lt;rect x="60" y="50" width="80" height="14" class="req2"/>
&lt;rect x="140" y="50" width="320" height="14" class="empty"/>
&lt;rect x="460" y="50" width="200" height="14" class="req6"/>
&lt;rect x="660" y="50" width="80" height="14" class="empty"/>
&lt;text x="20" y="78" class="sub">slot 3&lt;/text>
&lt;rect x="60" y="70" width="400" height="14" class="req3"/>
&lt;rect x="460" y="70" width="280" height="14" class="empty"/>
&lt;text x="20" y="98" class="sub">slot 4&lt;/text>
&lt;rect x="60" y="90" width="120" height="14" class="req4"/>
&lt;rect x="180" y="90" width="280" height="14" class="empty"/>
&lt;rect x="460" y="90" width="100" height="14" class="req5"/>
&lt;rect x="560" y="90" width="180" height="14" class="empty"/>
&lt;line x1="460" y1="20" x2="460" y2="115" class="tick" stroke-dasharray="2 2"/>
&lt;text x="465" y="115" class="sub">batch reciclado solo cuando TODOS terminan ↑&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="155" class="lbl">Continuous batching — slot libre se rellena INMEDIATO en cada tick&lt;/text>
&lt;text x="20" y="173" class="sub">slot 1&lt;/text>
&lt;rect x="60" y="165" width="200" height="14" class="req1"/>
&lt;rect x="260" y="165" width="200" height="14" class="req5"/>
&lt;rect x="460" y="165" width="160" height="14" class="req6"/>
&lt;rect x="620" y="165" width="120" height="14" class="empty"/>
&lt;text x="20" y="193" class="sub">slot 2&lt;/text>
&lt;rect x="60" y="185" width="80" height="14" class="req2"/>
&lt;rect x="140" y="185" width="150" height="14" class="req5"/>
&lt;rect x="290" y="185" width="200" height="14" class="req6"/>
&lt;rect x="490" y="185" width="250" height="14" class="req4"/>
&lt;text x="20" y="213" class="sub">slot 3&lt;/text>
&lt;rect x="60" y="205" width="400" height="14" class="req3"/>
&lt;rect x="460" y="205" width="160" height="14" class="req6"/>
&lt;rect x="620" y="205" width="120" height="14" class="req4"/>
&lt;text x="20" y="233" class="sub">slot 4&lt;/text>
&lt;rect x="60" y="225" width="120" height="14" class="req4"/>
&lt;rect x="180" y="225" width="180" height="14" class="req5"/>
&lt;rect x="360" y="225" width="200" height="14" class="req6"/>
&lt;rect x="560" y="225" width="180" height="14" class="req3"/>
&lt;text x="60" y="252" class="sub">cada barra de color = 1 iteración del decoder (1 token) de una request&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="280" class="lbl">Chunked prefill — prefill nuevo se intercala con decodes activos (sin stall)&lt;/text>
&lt;text x="60" y="300" class="sub">prefill chunk 1&lt;/text>
&lt;rect x="150" y="290" width="60" height="14" class="pre"/>
&lt;text x="220" y="300" class="sub">decode tick activos&lt;/text>
&lt;rect x="340" y="290" width="60" height="14" class="req1"/>
&lt;rect x="400" y="290" width="60" height="14" class="req2"/>
&lt;rect x="460" y="290" width="60" height="14" class="req3"/>
&lt;rect x="520" y="290" width="60" height="14" class="pre"/>
&lt;text x="585" y="300" class="sub">prefill chunk 2 (mismo step que los decodes)&lt;/text>
&lt;/svg>&lt;/p>
&lt;/div>
&lt;h2 id="la-matemática-que-importa">La matemática que importa&lt;/h2>
&lt;p>Tres fórmulas explican gran parte del comportamiento operacional.&lt;/p>
&lt;p>&lt;strong>Utilización GPU bajo static batching.&lt;/strong> Con un batch de tamaño &lt;code>B&lt;/code> cuyos &lt;code>seq_len_i&lt;/code> son las longitudes reales y &lt;code>max(seq_len_i)&lt;/code> es la longitud que define el padding:&lt;/p>
&lt;p>$$U_{\text{static}} = \frac{\sum_i \text{seq_len}_i}{B \cdot \max_i \text{seq_len}_i}$$&lt;/p>
&lt;p>Para &lt;code>B=32&lt;/code>, 30 sequences de 50 tokens y 2 de 500: &lt;code>U = (30·50 + 2·500) / (32·500) = 2500/16000 = 15.6 %&lt;/code>. Cuatro de cada cinco ciclos de GPU desperdiciados.&lt;/p>
&lt;p>&lt;strong>Utilización GPU bajo continuous batching (idealizada).&lt;/strong>&lt;/p>
&lt;p>$$U_{\text{continuous}} \approx 1 - \frac{T_{\text{scheduler}}}{T_{\text{iteration}}}$$&lt;/p>
&lt;p>Con overhead de scheduler ~50-200 µs e iteration time ~10-30 ms: &lt;code>U &amp;gt; 95 %&lt;/code>. La unidad de pérdida ya no es padding, es overhead de planificación, y este último es despreciable comparado con el forward pass.&lt;/p>
&lt;p>&lt;strong>Goodput vs throughput.&lt;/strong>&lt;/p>
&lt;p>$$\text{Goodput}(R) = R \cdot P(\text{latency} &amp;lt; \text{SLO})$$&lt;/p>
&lt;p>donde &lt;code>R&lt;/code> es el request rate ofrecido. Curva típica: goodput crece linealmente con &lt;code>R&lt;/code> hasta el knee de saturación, luego &lt;strong>cae&lt;/strong> porque &lt;code>P(SLO)&lt;/code> se desploma cuando el sistema se congestiona. El punto óptimo está justo antes del knee, no en el peak de throughput.&lt;/p>
&lt;p>Ejemplo: a &lt;code>R=100 req/s&lt;/code> con &lt;code>P(SLO)=0.99&lt;/code>, goodput = 99. A &lt;code>R=200 req/s&lt;/code> con &lt;code>P(SLO)=0.4&lt;/code>, goodput = 80. &lt;strong>Más carga ofrecida, menos goodput útil&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Token budget de chunked prefill.&lt;/strong> En cada step de vLLM con chunked prefill activo:&lt;/p>
&lt;p>$$\text{prefill_tokens_this_step} = \text{max_num_batched_tokens} - \text{num_decodes_active}$$&lt;/p>
&lt;p>Cada decode activo cuesta 1 token del budget; el resto se rellena con chunks de prefill nuevos. Si &lt;code>max_num_batched_tokens = 2048&lt;/code> y &lt;code>num_decodes_active = 200&lt;/code>, hay 1 848 tokens para prefill (un chunk de 1 848 o varios chunks pequeños).&lt;/p>
&lt;h2 id="implementaciones-reales-en-mayo-2026">Implementaciones reales en mayo 2026&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Engine&lt;/th>
&lt;th>Scheduler V actual&lt;/th>
&lt;th>Chunked prefill default&lt;/th>
&lt;th>Notas relevantes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM v1&lt;/strong> (default ≥0.8.0)&lt;/td>
&lt;td>V1 unificado&lt;/td>
&lt;td>always-on&lt;/td>
&lt;td>EngineCore aislado en proceso separado; prefix caching con eviction O(1); preempt-mode &lt;code>recompute&lt;/code> default; backends xgrammar/outlines.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang&lt;/strong>&lt;/td>
&lt;td>propio (PyTorch eco)&lt;/td>
&lt;td>sí&lt;/td>
&lt;td>&lt;strong>RadixAttention&lt;/strong> da prefix-cache hits cross-request; CPU scheduler no-bloqueante; lider en latencia estable a alta concurrencia.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TensorRT-LLM&lt;/strong>&lt;/td>
&lt;td>propietario &amp;ldquo;in-flight batching&amp;rdquo;&lt;/td>
&lt;td>sí&lt;/td>
&lt;td>Políticas &lt;code>GUARANTEED_NO_EVICT&lt;/code> (conservador, default) y &lt;code>MAX_UTILIZATION&lt;/code> (agresivo, riesgo de pausa por KV full). Compile-time vs runtime.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Triton + tensorrtllm_backend&lt;/strong>&lt;/td>
&lt;td>&lt;code>gpt_model_type: inflight_fused_batching&lt;/code>&lt;/td>
&lt;td>sí&lt;/td>
&lt;td>&lt;code>max_queue_delay_microseconds&lt;/code> para agrupar requests recién llegadas. Decoupled mode para streaming SSE.&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>llama.cpp&lt;/strong> (llama-server)&lt;/td>
&lt;td>propio&lt;/td>
&lt;td>&lt;code>--cont-batching&lt;/code> ON desde 2024&lt;/td>
&lt;td>&lt;code>-np N&lt;/code> slots paralelos; sin PagedAttention (KV contiguo por slot) → menos flexible pero más simple. Endpoint &lt;code>:8080/metrics&lt;/code>.&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Configuración vLLM v1 production-ready 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">vllm serve meta-llama/Llama-3.1-70B-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> --tensor-parallel-size &lt;span class="m">4&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> --max-num-batched-tokens &lt;span class="m">4096&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> --max-num-seqs &lt;span class="m">256&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> --enable-chunked-prefill &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --enable-prefix-caching &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --preemption-mode recompute &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --scheduling-policy fcfs &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="cl">&lt;span class="se">&lt;/span> --gpu-memory-utilization 0.92
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Equivalente SGLang:&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">python -m sglang.launch_server &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-70B-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> --tp &lt;span class="m">4&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> --chunked-prefill-size &lt;span class="m">4096&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> --max-running-requests &lt;span class="m">256&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> --enable-radix-cache
&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;h2 id="las-tres-tensiones-operacionales">Las tres tensiones operacionales&lt;/h2>
&lt;p>&lt;strong>Continuous batching + speculative decoding.&lt;/strong> Speculative decoding produce 1 a &lt;code>γ+1&lt;/code> tokens por step según la tasa de aceptación. El batch deja de ser uniforme en tokens producidos por iteración — &lt;em>nested raggedness&lt;/em>. PagedAttention lo absorbe (el KV cache puede crecer a velocidades distintas por request en el mismo step), pero el planificador pierde simetría. A QPS bajo (asistente conversacional) la combinación es excelente: vLLM reporta hasta &lt;strong>2.8× speedup&lt;/strong>. A QPS alto, el draft consume slots del decode pool y puede &lt;em>reducir&lt;/em> goodput agregado. Regla del pulgar: deshabilitar speculative cuando &lt;code>gpu_cache_usage &amp;gt; 0.85&lt;/code>. Detalle completo en &lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding&lt;/a>.&lt;/p>
&lt;p>&lt;strong>Continuous batching + multi-LoRA.&lt;/strong> Cada request del batch puede usar un adapter distinto (vía SGMV, ver &lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a>). Worst case: cada request del batch un adapter distinto y rank distinto → throughput cae hasta 50 % vs base sin LoRA. Best case: todos los requests mismo adapter → equivalente a base sin LoRA. Mitigación práctica: agrupar adapters por rank en el routing previo al engine; setear &lt;code>--max-lora-rank&lt;/code> al máximo realmente servido, no por exceso.&lt;/p>
&lt;p>&lt;strong>Continuous batching + MoE.&lt;/strong> Cada experto ve &lt;code>batch · k / N&lt;/code> tokens por step. Con DeepSeek-V3 (256 experts, k=8) y batch=32 en decode, cada experto procesa solo 1 token de media — compute starvation total. Para igualar el throughput por GPU de un dense, MoE necesita batches &lt;strong>&amp;raquo;10× mayores&lt;/strong>, lo que presiona el KV cache. &lt;strong>Wide-EP&lt;/strong> (ver &lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference&lt;/a>) distribuye los expertos en muchas GPUs y permite batches efectivos por experto mayores, a costa de comms all-to-all que añaden milisegundos por step.&lt;/p>
&lt;h2 id="métricas-que-hay-que-monitorizar">Métricas que hay que monitorizar&lt;/h2>
&lt;p>Las métricas Prometheus expuestas por vLLM (prefijo &lt;code>vllm:&lt;/code>, en scrapeo &lt;code>vllm_&lt;/code>):&lt;/p>
&lt;ul>
&lt;li>&lt;code>vllm:time_to_first_token_seconds&lt;/code> (Histogram) — TTFT incluyendo queue.&lt;/li>
&lt;li>&lt;code>vllm:time_per_output_token_seconds&lt;/code> (Histogram) — TPOT.&lt;/li>
&lt;li>&lt;code>vllm:e2e_request_latency_seconds&lt;/code> (Histogram) — End-to-end.&lt;/li>
&lt;li>&lt;code>vllm:num_requests_running&lt;/code> (Gauge) — batch activo.&lt;/li>
&lt;li>&lt;code>vllm:num_requests_waiting&lt;/code> (Gauge) — queue depth.&lt;/li>
&lt;li>&lt;code>vllm:num_requests_swapped&lt;/code> (Gauge) — preemptadas a CPU.&lt;/li>
&lt;li>&lt;code>vllm:gpu_cache_usage_perc&lt;/code> (Gauge) — fracción KV cache ocupada.&lt;/li>
&lt;li>&lt;code>vllm:gpu_prefix_cache_hit_rate&lt;/code> (Gauge) — prefix cache hits.&lt;/li>
&lt;li>&lt;code>vllm:num_preemptions_total&lt;/code> (Counter) — preempts. Cualquier valor sostenido es red flag.&lt;/li>
&lt;/ul>
&lt;p>Reglas operacionales prácticas:&lt;/p>
&lt;ul>
&lt;li>Zona estable bajo carga sostenida: &lt;code>gpu_cache_usage_perc ∈ [0.7, 0.9]&lt;/code>.&lt;/li>
&lt;li>Warning a &lt;code>&amp;gt;0.95&lt;/code> (preempt inminente).&lt;/li>
&lt;li>Crítico si &lt;code>num_requests_waiting&lt;/code> crece más rápido que &lt;code>num_requests_running&lt;/code>: el server no absorbe; escalar.&lt;/li>
&lt;/ul>
&lt;h2 id="pitfalls-operacionales">Pitfalls operacionales&lt;/h2>
&lt;p>&lt;strong>Preempt-on-OOM.&lt;/strong> Cuando &lt;code>gpu_cache_usage&lt;/code> llega a ~1.0 con requests pendientes que necesitan crecer su KV → vLLM preempta. V1 hace &lt;code>RECOMPUTE&lt;/code> por defecto (descarta KV, regenera cuando vuelve); V0 hacía &lt;code>SWAP&lt;/code> (mueve a CPU). &lt;code>RECOMPUTE&lt;/code> mejor para sequences cortas (regenerar barato); &lt;code>SWAP&lt;/code> mejor para sequences largas. Métrica &lt;code>vllm:num_preemptions_total&lt;/code> debe ser cero o casi cero en estado estable.&lt;/p>
&lt;p>&lt;strong>HoL blocking inverso (memory monopoly).&lt;/strong> Una request muy larga ocupa muchos bloques KV → requests pequeñas no caben en batch aunque el compute esté libre. Chunked prefill mitiga el bloqueo del compute durante prefill nuevo, pero no resuelve el monopolio de memoria. Solución parcial: políticas de límite por request (&lt;code>max_tokens&lt;/code> agresivo) o prioridades.&lt;/p>
&lt;p>&lt;strong>Starvation.&lt;/strong> FCFS puede dejar requests pendientes mucho tiempo si las activas no terminan. vLLM soporta &lt;code>--scheduling-policy priority&lt;/code> con cabecera &lt;code>x-priority&lt;/code>. Trabajos recientes (NeurIPS 2024 &lt;em>Efficient LLM Scheduling by Learning to Rank&lt;/em>, arXiv:2501.14312 &lt;em>Locality-aware Fair Scheduling&lt;/em>) proponen schedulers con quantum-based starvation prevention; no integrados aún en vLLM mainline.&lt;/p>
&lt;p>&lt;strong>Chunk size mal calibrado.&lt;/strong> Chunk pequeño (512) → TPOT bajo, TTFT alto, memory overhead por más accesos al KV. Chunk grande (8192+) → TTFT bajo, TPOT spikes durante el chunk. Regla: empezar en 2 048, medir P95 TPOT, ajustar.&lt;/p>
&lt;p>&lt;strong>Batch size cap.&lt;/strong> &lt;code>--max-num-seqs&lt;/code> alto → más concurrencia pero P99 TPOT explota. Bajo → throughput desperdiciado. Rule of thumb: &lt;code>max_num_seqs ≈ HBM_for_KV / (avg_seq_len × bytes_per_token_KV)&lt;/code>.&lt;/p>
&lt;h2 id="implicaciones-en-hardware-on-premise">Implicaciones en hardware on-premise&lt;/h2>
&lt;p>&lt;strong>En una RTX 4090 (24 GB).&lt;/strong> llama.cpp con &lt;code>--cont-batching -np 4-8&lt;/code> es el patrón natural. Modelos típicos: Llama 3 8B Q4_K_M con ~8 slots paralelos, throughput agregado del orden de cientos de tok/s. vLLM también funciona si caben los pesos (Llama 3 8B BF16 sí; el 70B no entra entero), aunque PagedAttention en consumer da menos retorno que en datacenter.&lt;/p>
&lt;p>&lt;strong>En un cluster genérico 4×H100 SXM (320 GB, NVLink).&lt;/strong> Aquí vLLM v1 / SGLang son el estándar de facto. Configuraciones típicas:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama 3 70B FP8 + TP=4&lt;/strong>: docenas de sesiones concurrentes con P95 TPOT bajo 50 ms, decenas de miles tok/s agregados a batch moderado.&lt;/li>
&lt;li>&lt;strong>Llama 3 70B AWQ-INT4 + TP=2&lt;/strong> + el resto del cluster para concurrencia adicional o multi-LoRA con SGMV.&lt;/li>
&lt;li>&lt;strong>DeepSeek-V3&lt;/strong> requiere setups mayores (8-16 H100) para entrar entero en FP8; con Wide-EP el continuous batching pasa a operar sobre batches mucho mayores y la economía cambia (ver MoE).&lt;/li>
&lt;/ul>
&lt;p>La regla de pulgar mayo 2026: &lt;strong>vLLM v1 con chunked prefill always-on y prefix caching enabled es la configuración por defecto sensata para cualquier modelo dense que quepa cómodamente; SGLang ofrece mejor latencia estable a alta concurrencia gracias al solapamiento CPU-scheduler/GPU-step; TensorRT-LLM da pico de throughput a alta concurrencia con la rigidez del compile-time&lt;/strong>.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Locality-aware fair scheduling&lt;/strong> (arXiv:2501.14312) y schedulers learning-to-rank (NeurIPS 2024): siguiente generación de algoritmos que cierran el trade-off fairness vs prefix-cache locality.&lt;/li>
&lt;li>&lt;strong>Smooth goodput&lt;/strong> (arXiv:2410.14257): refinamiento de la métrica DistServe usando max slowdown en vez de SLO binario.&lt;/li>
&lt;li>&lt;strong>Triton tensorrtllm_backend en producción&lt;/strong>: decoupled mode para streaming, ensemble con pre/post-processing, autoscaling con KServe.&lt;/li>
&lt;li>&lt;strong>vLLM speculators v0.3.0&lt;/strong> y framework de training de drafters compatible vLLM.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo&lt;/a> — el artefacto que continuous batching gestiona; sin entenderlo no se entiende por qué la unidad mínima es la iteración del decoder.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention deep dive&lt;/a> — la pieza de memoria sin la cual continuous batching dinámico fragmentaría la HBM; deep-dive al block manager.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode en pods especializados&lt;/a> — la siguiente capa: cuando continuous batching ya está exprimido, separar prefill y decode da otra ronda de mejoras (origen del paper DistServe que aporta el concepto de goodput).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/speculative-decoding-fundamentos/">Speculative decoding&lt;/a> — primera tensión operacional del scheduler: cada request del batch puede producir 1 a γ+1 tokens por step.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/multi-lora-serving-fundamentos/">Multi-LoRA serving&lt;/a> — segunda tensión: heterogeneous batching con SGMV permite que cada request use su adapter, pero el scheduler debe agrupar por rank para mantener throughput.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference&lt;/a> — tercera tensión: cada experto ve poquísimos tokens por step a batch típico, forzando batches mucho mayores que en dense.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro donde Deploy es la etapa 4.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Yu, G.-I., Jeong, J., Kim, G.-W., Kim, S., Chun, B.-G. &lt;em>Orca: A Distributed Serving System for Transformer-Based Generative Models&lt;/em>. OSDI 2022. &lt;a href="https://www.usenix.org/system/files/osdi22-yu.pdf">https://www.usenix.org/system/files/osdi22-yu.pdf&lt;/a>&lt;/li>
&lt;li>Kwon, W. et al. &lt;em>Efficient Memory Management for Large Language Model Serving with PagedAttention&lt;/em>. SOSP 2023. &lt;a href="https://arxiv.org/abs/2309.06180">https://arxiv.org/abs/2309.06180&lt;/a>&lt;/li>
&lt;li>Agrawal, A. et al. &lt;em>SARATHI: Efficient LLM Inference by Piggybacking Decodes with Chunked Prefills&lt;/em>. 2023. &lt;a href="https://arxiv.org/abs/2308.16369">https://arxiv.org/abs/2308.16369&lt;/a>&lt;/li>
&lt;li>Agrawal, A. et al. &lt;em>Taming Throughput-Latency Tradeoff in LLM Inference with Sarathi-Serve&lt;/em>. OSDI 2024. &lt;a href="https://arxiv.org/abs/2403.02310">https://arxiv.org/abs/2403.02310&lt;/a>&lt;/li>
&lt;li>Zhong, Y. et al. &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving&lt;/em>. OSDI 2024. &lt;a href="https://arxiv.org/abs/2401.09670">https://arxiv.org/abs/2401.09670&lt;/a>&lt;/li>
&lt;li>Sheng, Y. et al. &lt;em>S-LoRA: Serving Thousands of Concurrent LoRA Adapters&lt;/em>. MLSys 2024. &lt;a href="https://arxiv.org/abs/2311.03285">https://arxiv.org/abs/2311.03285&lt;/a>&lt;/li>
&lt;li>&lt;em>Efficient LLM Scheduling by Learning to Rank&lt;/em>. NeurIPS 2024.&lt;/li>
&lt;li>&lt;em>Locality-aware Fair Scheduling&lt;/em>. 2025. &lt;a href="https://arxiv.org/abs/2501.14312">https://arxiv.org/abs/2501.14312&lt;/a>&lt;/li>
&lt;li>vLLM V1 alpha release (ene 2025): &lt;a href="https://blog.vllm.ai/2025/01/27/v1-alpha-release.html">https://blog.vllm.ai/2025/01/27/v1-alpha-release.html&lt;/a>&lt;/li>
&lt;li>vLLM Anatomy (sep 2025): &lt;a href="https://blog.vllm.ai/2025/09/05/anatomy-of-vllm.html">https://blog.vllm.ai/2025/09/05/anatomy-of-vllm.html&lt;/a>&lt;/li>
&lt;li>vLLM Large-Scale Serving (dic 2025): &lt;a href="https://blog.vllm.ai/2025/12/17/large-scale-serving.html">https://blog.vllm.ai/2025/12/17/large-scale-serving.html&lt;/a>&lt;/li>
&lt;li>vLLM speculators v0.3.0 (dic 2025): &lt;a href="https://blog.vllm.ai/2025/12/13/speculators-v030.html">https://blog.vllm.ai/2025/12/13/speculators-v030.html&lt;/a>&lt;/li>
&lt;li>Anyscale, &lt;em>Continuous Batching for LLM Inference&lt;/em> (2023): &lt;a href="https://www.anyscale.com/blog/continuous-batching-llm-inference">https://www.anyscale.com/blog/continuous-batching-llm-inference&lt;/a>&lt;/li>
&lt;li>vLLM metrics docs: &lt;a href="https://docs.vllm.ai/en/latest/design/metrics/">https://docs.vllm.ai/en/latest/design/metrics/&lt;/a>&lt;/li>
&lt;li>vLLM optimization: &lt;a href="https://docs.vllm.ai/en/stable/configuration/optimization/">https://docs.vllm.ai/en/stable/configuration/optimization/&lt;/a>&lt;/li>
&lt;li>SGLang: &lt;a href="https://github.com/sgl-project/sglang">https://github.com/sgl-project/sglang&lt;/a>&lt;/li>
&lt;li>TensorRT-LLM performance tuning: &lt;a href="https://nvidia.github.io/TensorRT-LLM/performance/performance-tuning-guide/tuning-max-batch-size-and-max-num-tokens.html">https://nvidia.github.io/TensorRT-LLM/performance/performance-tuning-guide/tuning-max-batch-size-and-max-num-tokens.html&lt;/a>&lt;/li>
&lt;li>Red Hat, &lt;em>5 steps to triage vLLM performance&lt;/em> (mar 2026): &lt;a href="https://developers.redhat.com/articles/2026/03/09/5-steps-triage-vllm-performance">https://developers.redhat.com/articles/2026/03/09/5-steps-triage-vllm-performance&lt;/a>&lt;/li>
&lt;li>NVIDIA blog, &lt;em>Chunked Prefill with TensorRT-LLM&lt;/em>: &lt;a href="https://developer.nvidia.com/blog/streamlining-ai-inference-performance-and-deployment-with-nvidia-tensorrt-llm-chunked-prefill/">https://developer.nvidia.com/blog/streamlining-ai-inference-performance-and-deployment-with-nvidia-tensorrt-llm-chunked-prefill/&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>