<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Prefill on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/prefill/</link><description>Recent content in Prefill on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Fri, 22 May 2026 01:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/prefill/index.xml" rel="self" type="application/rss+xml"/><item><title>Disaggregated serving: prefill y decode en pods especializados</title><link>https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/</link><pubDate>Fri, 22 May 2026 01:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/</guid><description>&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>La inferencia LLM tiene dos fases con perfiles opuestos: &lt;strong>prefill&lt;/strong> (procesar el prompt entero de golpe) es compute-bound, &lt;strong>decode&lt;/strong> (generar token a token) es memory-bandwidth-bound. Ejecutarlas en la misma GPU obliga a elegir entre dos hardware óptimos incompatibles, y deja entre el 60 % y el 80 % de la capacidad de pico sin usar. La industria ha consolidado el patrón en 2026: &lt;strong>disaggregated serving&lt;/strong> — pods separados para cada fase, conectados por un canal de transferencia de KV cache (NIXL sobre UCX, RDMA, o NCCL en su defecto). DistServe demostró 7,4× más request rate a igual SLO; NVIDIA Dynamo 1.0 (GA en GTC 2026) lleva el patrón a producción a escala datacenter. Mezclar hardware heterogéneo —H100 para prefill, GPUs commodity para decode— recorta hasta el 48 % del coste por token. Este artículo explica el porqué, el cómo, y los números que importan para una infraestructura on-premise típica.&lt;/p>
&lt;h2 id="estás-aquí-deploy">Estás aquí: Deploy&lt;/h2>
&lt;p>Disaggregated serving es una decisión arquitectónica de la etapa &lt;strong>Deploy&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a>. No cambia el modelo, no cambia los datos, no cambia las evals — sólo cambia &lt;strong>cómo se reparten los pods de inferencia sobre el hardware GPU&lt;/strong>. Pero ese cambio mueve el throughput agregado entre 2× y 7×.&lt;/p>
&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:#ff8a4c;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(#dsm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#dsm)}&lt;/style>
&lt;defs>&lt;marker id="dsm" 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 · topología de pods prefill/decode y transferencia de KV cache&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="la-analogía-la-cocina-con-dos-brigadas">La analogía: la cocina con dos brigadas&lt;/h2>
&lt;p>Una cocina industrial seria —cualquiera que sirva más de 50 cubiertos por noche— funciona con dos brigadas distintas y dos espacios físicos separados.&lt;/p>
&lt;p>La &lt;strong>brigada de prep&lt;/strong> empieza al alba. Su trabajo es la &lt;em>mise en place&lt;/em>: cortar, marinar, blanquear, hervir fondos, preparar componentes complejos. Equipamiento: cuchillos buenos, fogones grandes, hornos de convección, ollas de 40 litros. Es trabajo intensivo en capacidad y se hace de golpe. Cuando termina, queda todo en bandejas etiquetadas listas para usar.&lt;/p>
&lt;p>La &lt;strong>brigada de pase&lt;/strong> entra a media tarde. Su trabajo es el servicio: tomar las bandejas de la prep, calentar porciones, emplatar, montar el pase. Equipamiento: salamandras, planchas pequeñas, espátulas finas, mucha vajilla. Es trabajo de muñeca, de ritmo, de no fallar al cliente que tiene el plato delante. La capacidad por hora importa menos que la latencia por plato.&lt;/p>
&lt;p>Si haces que &lt;strong>la misma persona&lt;/strong> haga prep y pase, las dos cosas sufren. El cocinero está parado mientras hace mise en place a media tarde. Tiene que parar a emplatar cuando entran cinco pedidos a la vez. Su equipo de trabajo está diseñado para uno o para el otro, no para ambos.&lt;/p>
&lt;p>Las cocinas serias resolvieron esto hace décadas: brigadas separadas, espacios separados, equipo separado. Lo único que cruza entre ambas son las bandejas de mise en place.&lt;/p>
&lt;p>Las &lt;strong>bandejas son el KV cache&lt;/strong>. La separación es &lt;strong>disaggregated serving&lt;/strong>. El pase de la prep al servicio es la &lt;strong>transferencia de KV cache&lt;/strong>, hoy resuelta con NIXL sobre RDMA. Y los pods especializados son las dos brigadas con sus equipos óptimos.&lt;/p>
&lt;h2 id="recap-rápido-prefill-y-decode">Recap rápido: prefill y decode&lt;/h2>
&lt;p>Una petición a un LLM atraviesa siempre dos fases:&lt;/p>
&lt;p>&lt;strong>Prefill.&lt;/strong> Coger el prompt completo (por ejemplo, 4.000 tokens) y procesarlo de una sola pasada por todas las capas del modelo. El resultado es el KV cache de esos 4.000 tokens (ver el &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">artículo previo sobre KV cache&lt;/a> si quieres recordar qué guarda exactamente). Este paso es masivamente paralelo: todos los tokens van a la vez por las matrices de atención, lo que se traduce en multiplicaciones de matrices enormes y densas. La GPU está al 90-95 % de uso de compute. &lt;strong>TTFT&lt;/strong> (time to first token) lo determina esta fase.&lt;/p>
&lt;p>&lt;strong>Decode.&lt;/strong> Una vez está el KV cache listo, el modelo genera tokens uno por uno. Cada token nuevo es una pasada por todas las capas con un solo vector de query, leyendo todo el KV cache acumulado para calcular la atención. No hay paralelismo entre tokens (cada uno depende del anterior). Lo que limita aquí no es el compute sino el ancho de banda: cada paso hay que leer los pesos completos del modelo desde HBM. La GPU está al 20-40 % de uso de compute, pero al 90 % de uso del HBM. &lt;strong>TBT&lt;/strong> (time between tokens) lo determina esta fase.&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Fase&lt;/th>
&lt;th>Característica&lt;/th>
&lt;th>Cuello de botella&lt;/th>
&lt;th>Métrica clave&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Prefill&lt;/td>
&lt;td>Cómputo masivo paralelo sobre N tokens de golpe&lt;/td>
&lt;td>TFLOPS (compute)&lt;/td>
&lt;td>TTFT&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Decode&lt;/td>
&lt;td>Streaming de pesos desde HBM, 1 token cada vez&lt;/td>
&lt;td>Bandwidth HBM&lt;/td>
&lt;td>TBT (inter-token latency)&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 290" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Utilizacion compute vs bandwidth en prefill y decode">
&lt;style>
.bar { stroke: #333; stroke-width: 1; }
.b-compute { fill: #2a9d8f; }
.b-bandwidth { fill: #e76f51; }
.b-low { fill-opacity: 0.35; }
.ax { stroke: #333; stroke-width: 1.5; }
.grid { stroke: #ddd; stroke-width: 1; stroke-dasharray: 3,3; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.tag { font: 600 12px sans-serif; }
&lt;/style>
&lt;text x="360" y="22" text-anchor="middle" class="lbl">Utilización de la GPU durante cada fase (orden de magnitud típico)&lt;/text>
&lt;line class="ax" x1="100" y1="240" x2="100" y2="60"/>
&lt;line class="ax" x1="100" y1="240" x2="680" y2="240"/>
&lt;line class="grid" x1="100" y1="78" x2="680" y2="78"/>
&lt;line class="grid" x1="100" y1="114" x2="680" y2="114"/>
&lt;line class="grid" x1="100" y1="150" x2="680" y2="150"/>
&lt;line class="grid" x1="100" y1="186" x2="680" y2="186"/>
&lt;text x="90" y="63" text-anchor="end" class="lbl-sm">100%&lt;/text>
&lt;text x="90" y="117" text-anchor="end" class="lbl-sm">75%&lt;/text>
&lt;text x="90" y="153" text-anchor="end" class="lbl-sm">50%&lt;/text>
&lt;text x="90" y="189" text-anchor="end" class="lbl-sm">25%&lt;/text>
&lt;text x="90" y="243" text-anchor="end" class="lbl-sm">0%&lt;/text>
&lt;text x="240" y="270" text-anchor="middle" class="lbl">PREFILL&lt;/text>
&lt;text x="240" y="284" text-anchor="middle" class="lbl-sm">compute-bound&lt;/text>
&lt;text x="540" y="270" text-anchor="middle" class="lbl">DECODE&lt;/text>
&lt;text x="540" y="284" text-anchor="middle" class="lbl-sm">memory-bound&lt;/text>
&lt;rect x="160" y="69" width="65" height="171" class="bar b-compute"/>
&lt;text x="193" y="62" text-anchor="middle" class="tag" fill="#2a9d8f">95%&lt;/text>
&lt;text x="193" y="255" text-anchor="middle" class="lbl-sm">compute&lt;/text>
&lt;rect x="245" y="132" width="65" height="108" class="bar b-bandwidth b-low"/>
&lt;text x="278" y="125" text-anchor="middle" class="tag" fill="#e76f51">60%&lt;/text>
&lt;text x="278" y="255" text-anchor="middle" class="lbl-sm">HBM&lt;/text>
&lt;rect x="460" y="177" width="65" height="63" class="bar b-compute b-low"/>
&lt;text x="493" y="170" text-anchor="middle" class="tag" fill="#2a9d8f">35%&lt;/text>
&lt;text x="493" y="255" text-anchor="middle" class="lbl-sm">compute&lt;/text>
&lt;rect x="545" y="78" width="65" height="162" class="bar b-bandwidth"/>
&lt;text x="578" y="71" text-anchor="middle" class="tag" fill="#e76f51">90%&lt;/text>
&lt;text x="578" y="255" text-anchor="middle" class="lbl-sm">HBM&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La asimetría es estructural: prefill quema el compute y deja la memoria a media, decode hace lo contrario. &lt;strong>Una GPU diseñada para ser excelente en ambos a la vez es una GPU diseñada para estar mal aprovechada todo el tiempo.&lt;/strong>&lt;/p>
&lt;h2 id="por-qué-juntarlas-en-la-misma-gpu-es-un-mal-negocio">Por qué juntarlas en la misma GPU es un mal negocio&lt;/h2>
&lt;p>Hasta 2023, la asunción universal era ejecutar prefill y decode &lt;strong>en el mismo proceso de inferencia, sobre la misma GPU&lt;/strong>. El motor scheduler (vLLM, TGI, Triton) decidía en cada ciclo si hacer prefill de una petición nueva o decode de las que ya estaban en marcha. La intuición era que compartir hardware ahorra coste.&lt;/p>
&lt;p>La intuición es incorrecta. El problema tiene tres caras:&lt;/p>
&lt;p>&lt;strong>Interferencia en latencia.&lt;/strong> Cuando el motor decide hacer prefill de una petición nueva, &lt;strong>interrumpe&lt;/strong> todos los decodes en curso. Eso sube el TBT de las otras peticiones. El usuario que estaba viendo tokens caer fluidos en su pantalla nota un parón de varios cientos de milisegundos. Esto se conoce como &lt;em>prefill-decode interference&lt;/em> y degrada la experiencia de forma visible a medida que sube la concurrencia.&lt;/p>
&lt;p>&lt;strong>Hardware sub-óptimo para cada fase.&lt;/strong> Una H100 SXM tiene 989 TFLOPS BF16 de compute y 3,35 TB/s de HBM3. Es excelente para prefill, donde el compute es el límite. Para decode, donde lo único que importa es el bandwidth, esos 989 TFLOPS están desaprovechados al 60-70 %. Inversamente, una GPU con menos compute pero similar bandwidth relativo (RTX 4090, L40S) resolvería el decode igual de bien por una fracción del precio.&lt;/p>
&lt;p>&lt;strong>Utilización agregada baja.&lt;/strong> En workloads reales con Llama 3 70B y outputs de 512 tokens, &lt;strong>alrededor del 80 % del wall-clock se gasta en decode&lt;/strong>. Eso quiere decir que el 80 % del presupuesto de tu cluster H100 está haciendo lecturas de memoria, no cálculos. Es como pagar un Ferrari para usarlo en cola de aparcamiento.&lt;/p>
&lt;h2 id="la-idea-pods-especializados-kv-cache-como-entregable">La idea: pods especializados, KV cache como entregable&lt;/h2>
&lt;p>Disaggregated serving rompe el ciclo de inferencia en dos servicios distintos:&lt;/p>
&lt;p>&lt;strong>Pod de prefill.&lt;/strong> Recibe el prompt, ejecuta el prefill, produce el KV cache. Hardware: GPUs con alto compute (H100, H200, B200). Optimizado para batching agresivo y throughput, no para latencia individual: si llegan 32 prompts en 100 ms, los procesa juntos.&lt;/p>
&lt;p>&lt;strong>Pod de decode.&lt;/strong> Recibe el KV cache ya construido, ejecuta la generación token a token, streamea al cliente. Hardware: GPUs con buen bandwidth pero idealmente más baratas por TFLOPS (RTX 4090, L40S, A100, incluso A30 según el caso). Optimizado para latencia por token (TBT bajo).&lt;/p>
&lt;p>Entre ambos: una &lt;strong>transferencia de KV cache&lt;/strong> sobre la red, que puede ser nodo-local (shared memory, NVLink), intra-rack (RDMA con InfiniBand o RoCE) o cross-rack (NIXL sobre UCX). El coste de esta transferencia escala linealmente con la longitud del contexto, y es la clave económica del esquema.&lt;/p>
&lt;div class="diagram" style="max-width: 720px; margin: 1.5rem auto;">
&lt;svg viewBox="0 0 720 340" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Arquitectura monolitica vs disaggregated">
&lt;style>
.node { stroke: #333; stroke-width: 1.5; }
.n-mono { fill: #ffe9d6; }
.n-prefill { fill: #d9f5d6; }
.n-decode { fill: #d6eaff; }
.n-router { fill: #fffae6; }
.lbl { font: 600 13px sans-serif; fill: #222; }
.lbl-sm { font: 11px sans-serif; fill: #444; }
.lbl-section { font: 700 14px sans-serif; fill: #222; }
.arr { stroke: #444; stroke-width: 1.6; fill: none; marker-end: url(#ah4); }
.arr-int { stroke: #c1121f; stroke-width: 1.4; fill: none; stroke-dasharray: 5,3; marker-end: url(#ah4r); }
&lt;/style>
&lt;defs>
&lt;marker id="ah4" 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="#444"/>&lt;/marker>
&lt;marker id="ah4r" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="5" markerHeight="5" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c1121f"/>&lt;/marker>
&lt;/defs>
&lt;text x="170" y="25" text-anchor="middle" class="lbl-section">Monolítico (aggregated)&lt;/text>
&lt;rect x="40" y="50" width="260" height="220" rx="10" class="node n-mono"/>
&lt;text x="170" y="78" text-anchor="middle" class="lbl">GPU única&lt;/text>
&lt;text x="170" y="98" text-anchor="middle" class="lbl-sm">scheduler decide cada ciclo:&lt;/text>
&lt;rect x="65" y="115" width="100" height="40" rx="5" class="node n-prefill"/>
&lt;text x="115" y="140" text-anchor="middle" class="lbl-sm">prefill&lt;/text>
&lt;rect x="180" y="115" width="100" height="40" rx="5" class="node n-decode"/>
&lt;text x="230" y="140" text-anchor="middle" class="lbl-sm">decode&lt;/text>
&lt;path class="arr-int" d="M165,128 L180,128"/>
&lt;path class="arr-int" d="M180,145 L165,145"/>
&lt;text x="170" y="180" text-anchor="middle" class="lbl-sm" fill="#c1121f">interferencia en cada cambio&lt;/text>
&lt;text x="170" y="200" text-anchor="middle" class="lbl-sm">→ TBT sube cuando llega prefill&lt;/text>
&lt;text x="170" y="230" text-anchor="middle" class="lbl-sm">una HW óptima para ambos:&lt;/text>
&lt;text x="170" y="250" text-anchor="middle" class="lbl-sm">imposible&lt;/text>
&lt;text x="540" y="25" text-anchor="middle" class="lbl-section">Disaggregated&lt;/text>
&lt;rect x="370" y="50" width="150" height="100" rx="10" class="node n-prefill"/>
&lt;text x="445" y="80" text-anchor="middle" class="lbl">pod prefill&lt;/text>
&lt;text x="445" y="102" text-anchor="middle" class="lbl-sm">H100 / H200 / B200&lt;/text>
&lt;text x="445" y="120" text-anchor="middle" class="lbl-sm">compute alto, batching&lt;/text>
&lt;text x="445" y="138" text-anchor="middle" class="lbl-sm">agresivo&lt;/text>
&lt;rect x="560" y="50" width="150" height="100" rx="10" class="node n-decode"/>
&lt;text x="635" y="80" text-anchor="middle" class="lbl">pod decode&lt;/text>
&lt;text x="635" y="102" text-anchor="middle" class="lbl-sm">4090 / L40S / A100&lt;/text>
&lt;text x="635" y="120" text-anchor="middle" class="lbl-sm">bandwidth alto, TBT&lt;/text>
&lt;text x="635" y="138" text-anchor="middle" class="lbl-sm">estable&lt;/text>
&lt;path class="arr" d="M520,100 L560,100"/>
&lt;text x="540" y="92" text-anchor="middle" class="lbl-sm">KV cache&lt;/text>
&lt;text x="540" y="115" text-anchor="middle" class="lbl-sm">NIXL/RDMA&lt;/text>
&lt;rect x="450" y="180" width="180" height="50" rx="8" class="node n-router"/>
&lt;text x="540" y="200" text-anchor="middle" class="lbl">router (vLLM/Dynamo)&lt;/text>
&lt;text x="540" y="218" text-anchor="middle" class="lbl-sm">distribuye prompts y streams&lt;/text>
&lt;path class="arr" d="M445,150 L500,180"/>
&lt;path class="arr" d="M635,150 L580,180"/>
&lt;text x="540" y="260" text-anchor="middle" class="lbl-sm" fill="#2a9d8f">→ TBT estable, TTFT bajo&lt;/text>
&lt;text x="540" y="280" text-anchor="middle" class="lbl-sm">coste: transferencia KV cache&lt;/text>
&lt;text x="540" y="298" text-anchor="middle" class="lbl-sm">~5-50 ms según interconnect&lt;/text>
&lt;/svg>
&lt;/div>
&lt;h2 id="el-protocolo-de-transferencia-la-economía-del-movimiento">El protocolo de transferencia: la economía del movimiento&lt;/h2>
&lt;p>El KV cache transferido en un Llama 3 70B con 4K de contexto pesa aproximadamente &lt;strong>2,6 GB&lt;/strong> (80 layers × 8 KV heads × 128 dim × 4 096 tokens × 2 (K y V) × 2 bytes en BF16). Mover 2,6 GB entre dos GPUs no es trivial:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Canal&lt;/th>
&lt;th style="text-align:right">Bandwidth efectivo&lt;/th>
&lt;th style="text-align:right">Tiempo para 2,6 GB&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>NVLink intra-nodo (NVSwitch)&lt;/td>
&lt;td style="text-align:right">~450 GB/s&lt;/td>
&lt;td style="text-align:right">~6 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Shared memory (mismo nodo, PCIe 5)&lt;/td>
&lt;td style="text-align:right">~60 GB/s&lt;/td>
&lt;td style="text-align:right">~45 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RDMA InfiniBand 400 Gbps&lt;/td>
&lt;td style="text-align:right">~50 GB/s&lt;/td>
&lt;td style="text-align:right">~55 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>RDMA RoCE 200 Gbps&lt;/td>
&lt;td style="text-align:right">~25 GB/s&lt;/td>
&lt;td style="text-align:right">~105 ms&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TCP/IP 10 GbE&lt;/td>
&lt;td style="text-align:right">~1 GB/s&lt;/td>
&lt;td style="text-align:right">~2,6 s&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Lectura inmediata: por encima de InfiniBand-grade, la transferencia es cómoda. Por debajo, lleva al traste el TTFT que estamos intentando mejorar. &lt;strong>Disaggregated serving es viable sólo con interconexión decente&lt;/strong> — no es un patrón para clusters montados con switches Ethernet de consumo.&lt;/p>
&lt;p>NVIDIA respondió a esto con &lt;strong>NIXL&lt;/strong> (NVIDIA Inference Transfer Library), publicada a mediados de 2025: una librería que abstrae el transporte (UCX, NCCL, RDMA verbs directos, shared memory) y elige el mejor camino disponible automáticamente. vLLM la integra desde finales de 2025 mediante el &lt;code>NixlConnector&lt;/code>. Es ahora el default de facto para nuevos despliegues.&lt;/p>
&lt;h2 id="implementaciones-reales-en-mayo-2026">Implementaciones reales en mayo 2026&lt;/h2>
&lt;p>El recorrido del patrón en dos años:&lt;/p>
&lt;pre tabindex="0">&lt;code>2024 ene · DistServe (HKU + UCSD): 7,4× requests al mismo SLO
2024 may · SplitWise (Microsoft): variante con hardware heterogéneo
2024 dic · vLLM disagg experimental (SharedStorage + PyNcclConnector)
2025 mar · NIXL release (NVIDIA): librería de transferencia unificada
2025 jul · vLLM NixlConnector estable
2025 nov · SGLang, llm-d, MoonCake adoptan el patrón
2026 mar · NVIDIA Dynamo 1.0 GA (GTC 2026): production-ready a escala datacenter
&lt;/code>&lt;/pre>&lt;p>A día de hoy, &lt;strong>el patrón es el default&lt;/strong> en cualquier framework de serving serio. Los que siguen monolíticos son los pequeños o los educativos.&lt;/p>
&lt;p>Tres opciones realistas para una infraestructura on-premise:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>vLLM disagg con NixlConnector.&lt;/strong> El camino más abierto, requiere desplegar dos sets de pods de vLLM (uno con &lt;code>--kv-transfer-config '{&amp;quot;kv_role&amp;quot;:&amp;quot;producer&amp;quot;}'&lt;/code>, otro con &lt;code>&amp;quot;kv_role&amp;quot;:&amp;quot;consumer&amp;quot;&lt;/code>) y un proxy router. Suficiente para clusters de 4-16 GPUs.&lt;/li>
&lt;li>&lt;strong>SGLang con disagg.&lt;/strong> Equivalente conceptual, mejor performance en algunos workloads MoE.&lt;/li>
&lt;li>&lt;strong>NVIDIA Dynamo 1.0.&lt;/strong> El que se está imponiendo a escala datacenter. Cubre routing, KV cache management, monitorización y scheduling en un solo plano de control. Más pesado, pero la solución de referencia si tu cluster crece por encima de 32 GPUs.&lt;/li>
&lt;/ol>
&lt;h2 id="los-números-que-importan">Los números que importan&lt;/h2>
&lt;p>Lo que la disaggregation desbloquea, en términos directos:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Métrica&lt;/th>
&lt;th>Aggregated (monolítico)&lt;/th>
&lt;th>Disaggregated&lt;/th>
&lt;th>Mejora&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Goodput (req/s al SLO)&lt;/td>
&lt;td>baseline&lt;/td>
&lt;td>1,4 – 2×&lt;/td>
&lt;td>hasta 2×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>TTFT bajo carga alta&lt;/td>
&lt;td>sube agresivo desde QPS 4&lt;/td>
&lt;td>estable hasta QPS 7+&lt;/td>
&lt;td>~2×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Request rate al mismo SLO (DistServe paper)&lt;/td>
&lt;td>baseline&lt;/td>
&lt;td>7,4×&lt;/td>
&lt;td>7,4×&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Throughput MoE en Blackwell (Dynamo, GB300 NVL72)&lt;/td>
&lt;td>baseline (Hopper)&lt;/td>
&lt;td>hasta 50×&lt;/td>
&lt;td>depende del modelo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Coste por token (heterogéneo H100 + commodity)&lt;/td>
&lt;td>baseline (todo H100)&lt;/td>
&lt;td>-48 %&lt;/td>
&lt;td>casi mitad&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Hay que leer estos números con cuidado: los más espectaculares (7× y 50×) requieren hardware específico (Blackwell GB200/GB300 NVL72) y modelos específicos (MoE grandes). El &lt;strong>rango realista para un on-premise típico es 1,4-2× en goodput y -30 a -50 % en coste por token&lt;/strong>, dependiendo de cuán heterogénea sea la mezcla de GPUs y de cuán optimizada esté la transferencia de KV cache.&lt;/p>
&lt;h2 id="heterogeneidad-la-versión-radical">Heterogeneidad: la versión radical&lt;/h2>
&lt;p>El paso lógico siguiente, propuesto por SplitWise en 2024 y madurado en 2025-2026 (Cronus, Tessera y otros), es &lt;strong>mezclar tipos de GPU&lt;/strong>: GPUs caras de cómputo alto para prefill, GPUs commodity con buen bandwidth para decode.&lt;/p>
&lt;p>Coste indicativo (precios de mercado típicos a mediados de 2026):&lt;/p>
&lt;ul>
&lt;li>&lt;strong>H100 SXM&lt;/strong>: ~30-40 k$ capex, ~3-4 $/h amortizado. Perfil compute-pesado.&lt;/li>
&lt;li>&lt;strong>L40S&lt;/strong>: ~8-10 k$ capex, ~1,5 $/h. Perfil intermedio, 864 GB/s de bandwidth.&lt;/li>
&lt;li>&lt;strong>RTX 4090&lt;/strong>: ~1,5 k$ capex, ~0,30 $/h. Perfil compute-modesto pero 1 TB/s de bandwidth GDDR6X — suficiente para decode de modelos hasta ~30B parámetros.&lt;/li>
&lt;/ul>
&lt;p>Un cluster mixto realista para servir un modelo 8B:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× RTX 4090 (prefill batch) → ~3.000 $ capex, ~0,60 $/h
4× RTX 4090 (decode pool) → ~6.000 $ capex, ~1,20 $/h
TOTAL → ~9.000 $ capex, ~1,80 $/h
&lt;/code>&lt;/pre>&lt;p>Frente a la alternativa monolítica equivalente en throughput:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× H100 SXM (todo en uno) → ~70.000 $ capex, ~7 $/h
&lt;/code>&lt;/pre>&lt;p>El mismo throughput a una fracción del capex y a la cuarta parte del coste por hora, &lt;strong>a costa de complejidad operativa&lt;/strong>: ahora tienes dos pools que coordinar, una red de transferencia que cuidar, y un scheduler que no es trivial.&lt;/p>
&lt;p>Para modelos más grandes (Llama 3 70B), el decode pool ya no cabe en una 4090 individual (el modelo no entra en 24 GB ni siquiera cuantizado a INT4 con margen). Ahí la mezcla razonable es H100 para prefill + L40S o A100 80GB para decode, con ahorro típico del 30-40 % sobre la opción todo-H100.&lt;/p>
&lt;h2 id="aplicado-a-hardware-on-premise-típico">Aplicado a hardware on-premise típico&lt;/h2>
&lt;h3 id="caso-1--una-o-dos-rtx-4090-monolítico-sigue-ganando">Caso 1 — Una o dos RTX 4090: monolítico sigue ganando&lt;/h3>
&lt;p>Con una sola GPU no hay disaggregation que valga: el patrón requiere mínimo dos GPUs en pods separados. Con dos 4090, técnicamente puedes intentarlo (una para prefill, otra para decode con KV cache transferido por PCIe 5 o RDMA básico), pero el overhead de transferencia se come la ganancia para modelos pequeños donde el prefill ya es rápido.&lt;/p>
&lt;p>&lt;strong>Recomendación:&lt;/strong> mantener monolítico (vLLM tradicional, &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">bien configurado con KV cache cuantizado&lt;/a>). El siguiente nivel justificable de complejidad es un cluster con interconexión rápida.&lt;/p>
&lt;h3 id="caso-2--cluster-4h100-sxm-320-gb-nvlink-el-sweet-spot">Caso 2 — Cluster 4×H100 SXM (320 GB, NVLink): el sweet spot&lt;/h3>
&lt;p>Configuración mínima realista para disaggregation seria, sirviendo un modelo 70B en producción:&lt;/p>
&lt;pre tabindex="0">&lt;code>2× H100 (TP=2) → 2 pods de prefill
2× H100 (TP=2) → pods de decode con varias instancias compartiendo TP
NIXL sobre NVLink → transferencia KV cache &amp;lt;6 ms
Router (vLLM o Dynamo) → distribución de prompts y stream
&lt;/code>&lt;/pre>&lt;p>Resultado realista esperado: goodput &lt;strong>1,6-1,9× respecto al mismo cluster en monolítico&lt;/strong>, con TTFT estable hasta cargas de QPS 7-8 (frente al QPS 4 al que empieza a degradar el monolítico).&lt;/p>
&lt;p>Si la mezcla heterogénea es posible (añadir 4-8 L40S al cluster para hacer el decode pool), el coste por token cae adicionalmente entre un 25 % y un 35 %, manteniendo el modelo 70B servido íntegro.&lt;/p>
&lt;h2 id="posición-dentro-de-la-arquitectura">Posición dentro de la arquitectura&lt;/h2>
&lt;p>Disaggregated serving es una &lt;strong>capa transversal&lt;/strong> a casi todo lo discutido en artículos previos. Toca:&lt;/p>
&lt;ul>
&lt;li>El &lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache&lt;/a> porque es el artefacto que se transfiere entre pods. Sin entender bien cuánto pesa el cache y cómo crece con el contexto, no se puede dimensionar la transferencia.&lt;/li>
&lt;li>El &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">fine-tuning continuo&lt;/a> porque el multi-LoRA hot-swap conserva su semántica: cada pod (prefill o decode) carga los adapters por separado, y el router decide qué adapter aplicar en cada fase.&lt;/li>
&lt;li>La topología del cluster: cambia la HW recomendada, el networking exigido y el modelo de costes.&lt;/li>
&lt;/ul>
&lt;p>Si estás diseñando una infraestructura de inferencia para 2026 desde cero, &lt;strong>disaggregation deja de ser opcional&lt;/strong> para cualquier cluster que exceda 4 GPUs de capacidad. Si estás modernizando una existente, es la actualización con mejor retorno por euro invertido — siempre que el networking entre pods sea decente (NVLink intra-nodo o RDMA intra-rack como mínimo).&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>NIXL en detalle&lt;/strong>: cómo elige el transporte óptimo, cómo se configura UCX, qué pasa cuando RDMA falla y hay que degradar a TCP.&lt;/li>
&lt;li>&lt;strong>Scheduler de routing&lt;/strong>: cómo decide el orquestador qué pod recibe qué petición, batching dinámico, manejo de prioridades.&lt;/li>
&lt;li>&lt;strong>Multi-tenant disagg&lt;/strong>: aislamiento de KV cache entre tenants, ACLs por adapter, multi-LoRA sobre pods especializados.&lt;/li>
&lt;li>&lt;strong>Disagg + prefix caching&lt;/strong>: cómo se combina con el patrón de reutilización de KV cache cuando varios prompts comparten prefijo (system prompt común).&lt;/li>
&lt;li>&lt;strong>Disagg en edge / inferencia local&lt;/strong>: viabilidad sobre hardware doméstico (4090 + Mac Studio, por ejemplo), donde la transferencia depende de Thunderbolt o Ethernet residencial.&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/pipeline-llmops-seis-etapas/">El pipeline LLMOps de seis etapas&lt;/a> — el mapa maestro al que pertenece la etapa Deploy. Este post entra en una decisión arquitectónica concreta dentro de esa etapa.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/cluster-h100-plataforma-multi-tenant/">El cluster GPU como plataforma multi-tenant&lt;/a> — el patrón de capas Gateway/Quota/Isolation/Observability sobre el cual la disaggregation aquí descrita se sitúa: el cluster H100 que sirve a varios tenants combina ambos patrones.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/operators-llm-kubernetes/">Operators de inferencia LLM en Kubernetes&lt;/a> — los operators (vLLM Production Stack, NVIDIA Dynamo, llm-d, OME) que materializan en Kubernetes los pods especializados de prefill y decode.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/kv-cache-fundamentos/">KV cache: la memoria de trabajo que sostiene la inferencia LLM&lt;/a> — el artefacto exacto que se transfiere entre pods, con la fórmula completa de su tamaño.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/pagedattention-deep-dive/">PagedAttention por dentro: bloques, tabla de páginas, evicción y el estado del arte del KV cache en 2026&lt;/a> — la mecánica del KV cache que la disaggregation explota a nivel del bloque, y el panorama de optimizaciones derivadas (vAttention, LMCache, RadixAttention).&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — cómo el multi-LoRA hot-swap convive con la disaggregation: cada pod carga adapters por separado, el router elige.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Zhong et al., &lt;em>DistServe: Disaggregating Prefill and Decoding for Goodput-optimized Large Language Model Serving&lt;/em> (OSDI 2024).&lt;/li>
&lt;li>Patel et al., &lt;em>SplitWise: Efficient Generative LLM Inference Using Phase Splitting&lt;/em> (ISCA 2024).&lt;/li>
&lt;li>NVIDIA, &lt;em>NVIDIA Dynamo 1.0: Production-Ready Disaggregated Inference&lt;/em> (GTC 2026, marzo): &lt;a href="https://developer.nvidia.com/blog/nvidia-dynamo-1-production-ready/">https://developer.nvidia.com/blog/nvidia-dynamo-1-production-ready/&lt;/a>.&lt;/li>
&lt;li>NVIDIA, &lt;em>NIXL: NVIDIA Inference Transfer Library&lt;/em> — documentación oficial.&lt;/li>
&lt;li>vLLM, &lt;em>Disaggregated Prefilling&lt;/em>: &lt;a href="https://docs.vllm.ai/en/stable/features/disagg_prefill/">https://docs.vllm.ai/en/stable/features/disagg_prefill/&lt;/a>.&lt;/li>
&lt;li>vLLM, &lt;em>NixlConnector Usage Guide&lt;/em>: &lt;a href="https://docs.vllm.ai/en/stable/features/nixl_connector_usage/">https://docs.vllm.ai/en/stable/features/nixl_connector_usage/&lt;/a>.&lt;/li>
&lt;li>Hao AI Lab, &lt;em>Disaggregated Inference: 18 Months Later&lt;/em> (UCSD, 2025) — retrospectiva técnica del paper DistServe.&lt;/li>
&lt;/ul></description></item></channel></rss>