<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Sgmv on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/sgmv/</link><description>Recent content in Sgmv on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Sat, 30 May 2026 14:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/sgmv/index.xml" rel="self" type="application/rss+xml"/><item><title>Multi-LoRA serving: el traductor único con mil glosarios — base compartido, miles de adapters concurrentes y el kernel SGMV</title><link>https://blog.lo0.es/posts/multi-lora-serving-fundamentos/</link><pubDate>Sat, 30 May 2026 14:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/multi-lora-serving-fundamentos/</guid><description>&lt;blockquote>
&lt;p>Este post complementa el de &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a>. El fine-tuning continuo es el productor de los adapters; multi-LoRA serving es el consumidor que los pone a trabajar. Sin esta capa, todo el ciclo de feedback se rompe en el último kilómetro. También se cruza con &lt;a href="https://blog.lo0.es/posts/alignment-moderno-dpo-kto-orpo-simpo/">Alignment moderno (DPO/KTO/ORPO/SimPO)&lt;/a> (cada política de alignment puede vivir como un adapter distinto) y &lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization&lt;/a> (el base cuantizado libera memoria para muchos más adapters).&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(#mlm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mlm)}&lt;/style>
&lt;defs>&lt;marker id="mlm" 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 · base compartido, N adapters concurrentes en una sola GPU&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 patrón dominante en 2026 no es un modelo por cliente sino &lt;strong>un único modelo base de propósito general más N adapters LoRA finos&lt;/strong> por tarea, cliente, idioma o dominio. El motivo es obvio: un LoRA con rank 16 sobre Llama-3-70B ocupa ~400 MB; un fine-tuning completo ocupa ~140 GB. Decenas o cientos de adapters por base es manejable; decenas o cientos de bases es prohibitivo. Lo no obvio es &lt;strong>cómo servirlos concurrentemente&lt;/strong> sin recargar pesos cada vez que cambia el adapter (matando el batching) ni replicar el base (matando la memoria). La respuesta cristalizó en 2024 con dos papers complementarios: &lt;strong>S-LoRA&lt;/strong> (Sheng et al., Stanford + UC Berkeley, MLSys 2024) introdujo &lt;em>unified paging&lt;/em> —los pesos de los adapters viven en el mismo pool de memoria que el KV cache, ambos paginables— y &lt;em>heterogeneous batching&lt;/em> —un batch puede tener requests con adapters distintos y rank distintos sin padding—; &lt;strong>Punica&lt;/strong> (Chen et al., UW + Duke, MLSys 2024) introdujo el kernel CUDA que se ha convertido en estándar de facto: &lt;strong>SGMV&lt;/strong> (Segmented Gather Matrix-Vector multiplication), que computa en una sola pasada &lt;code>Y += Σ_i X_i · A_i · B_i&lt;/code> agrupando requests por adapter. SGMV está hoy debajo de &lt;strong>vLLM, LoRAX (Predibase), SGLang y TGI&lt;/strong>. El resultado operacional medible: hasta &lt;strong>2 000 adapters concurrentes en una sola GPU&lt;/strong> (S-LoRA paper), hasta &lt;strong>4× throughput vs vLLM naive&lt;/strong> y hasta &lt;strong>30× vs HuggingFace PEFT&lt;/strong>. El precio: overhead típico &lt;strong>10-30 %&lt;/strong> de latencia por capa con adapter activo en batch heterogéneo, &lt;strong>prácticamente cero&lt;/strong> cuando todos los requests del batch usan el mismo adapter, &lt;strong>20-40 %&lt;/strong> en el peor caso. Este post desmonta el mecanismo, las matemáticas (memoria por adapter, overhead por rank), la tabla comparativa de implementaciones, los pitfalls (cold start, rank dispar, fragmentación) y la economía real en H100 con base Llama-3-70B FP8 + 200 adapters.&lt;/p>
&lt;h2 id="la-analogía-el-traductor-único-con-mil-glosarios">La analogía: el traductor único con mil glosarios&lt;/h2>
&lt;p>Imagina una agencia de traducción especializada con un único traductor senior, brillante, que maneja con fluidez quince idiomas y todos los dominios técnicos generales. Ese traductor es &lt;strong>caro de contratar y caro de entrenar&lt;/strong>: necesitó años de formación y una experiencia que no se replica fácilmente. Pero a la agencia llegan textos de clientes muy distintos: un bufete que usa terminología jurídica específica de su jurisdicción, un fabricante con nomenclatura interna de piezas, un hospital con abreviaturas clínicas propias. Cada cliente tiene su jerga.&lt;/p>
&lt;p>La agencia no contrata un traductor por cliente —sería ridículo, son el 90 % del trabajo común—. Lo que hace es &lt;strong>mantener un glosario por cliente&lt;/strong>: una libreta pequeña, fácil de actualizar, que contiene los términos específicos y cómo se traducen para ese cliente. Cuando el traductor recibe un texto, abre el glosario del cliente que toca y trabaja con él al lado. Al traducir cada palabra, consulta primero si está en el glosario; si está, usa la versión específica; si no, usa su conocimiento general.&lt;/p>
&lt;p>Los glosarios viven &lt;strong>en una estantería compartida&lt;/strong>, ordenados por uso reciente: los más consultados a mano, los antiguos en archivo. Cuando un cliente nuevo llega, su glosario se trae del archivo a la estantería. Cuando el escritorio se llena, el glosario menos usado vuelve al archivo.&lt;/p>
&lt;p>Y lo más importante: el traductor puede tener &lt;strong>varios glosarios abiertos a la vez&lt;/strong> porque está trabajando en paralelo con cinco textos de cinco clientes. No es un glosario por documento; es un glosario por cliente, y los documentos del cliente A usan su glosario, los del cliente B el suyo, todos en la misma mesa.&lt;/p>
&lt;p>La analogía se sostiene en cinco mapeos:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>El traductor único&lt;/strong> = el modelo base (Llama-3-70B, Qwen2.5-72B). Caro de entrenar, una sola copia en VRAM.&lt;/li>
&lt;li>&lt;strong>Cada glosario&lt;/strong> = un adapter LoRA. Pequeño (~150-400 MB), específico, fácil de actualizar.&lt;/li>
&lt;li>&lt;strong>La estantería con los glosarios a mano&lt;/strong> = el pool de adapters cacheados en VRAM (típicamente 50-200 a la vez con base FP8 en H100 80 GB).&lt;/li>
&lt;li>&lt;strong>El archivo&lt;/strong> = el storage de adapters en MinIO/S3/HF Hub. Cientos o miles, fetcheados on-demand.&lt;/li>
&lt;li>&lt;strong>Trabajar en paralelo con varios glosarios abiertos&lt;/strong> = batch heterogéneo con SGMV. El kernel que hace la consulta agrupada al glosario correcto por cada palabra del batch.&lt;/li>
&lt;/ul>
&lt;h2 id="el-mecanismo-desnudo-qué-hace-un-lora-y-por-qué-se-puede-servir-multi-tenant">El mecanismo desnudo: qué hace un LoRA y por qué se puede servir multi-tenant&lt;/h2>
&lt;p>Un adapter LoRA modifica una matriz &lt;code>W&lt;/code> del modelo base sumándole un producto de bajo rango:&lt;/p>
&lt;p>$$W&amp;rsquo; = W + B A$$&lt;/p>
&lt;p>donde &lt;code>W ∈ R^{d_out × d_in}&lt;/code> es la matriz original (los pesos del base), &lt;code>A ∈ R^{r × d_in}&lt;/code> y &lt;code>B ∈ R^{d_out × r}&lt;/code> son las matrices entrenables del adapter, y &lt;code>r&lt;/code> es el &lt;strong>rank&lt;/strong> (típicamente 8, 16, 32 o 64 — siempre mucho menor que &lt;code>d_in&lt;/code> y &lt;code>d_out&lt;/code>).&lt;/p>
&lt;p>En un forward pass, en lugar de calcular &lt;code>y = W' x&lt;/code>, se calcula:&lt;/p>
&lt;p>$$y = W x + B(Ax)$$&lt;/p>
&lt;p>Es decir: el cómputo del base (&lt;code>Wx&lt;/code>) ocurre exactamente igual; el adapter añade dos matmuls baratos (&lt;code>Ax&lt;/code> y luego &lt;code>B(·)&lt;/code>) que añaden la corrección. La matriz &lt;code>BA&lt;/code> nunca se materializa explícitamente.&lt;/p>
&lt;p>&lt;strong>Lo que esto habilita en serving&lt;/strong>: si tienes el base cargado y N adapters distintos, el &lt;code>Wx&lt;/code> se calcula &lt;strong>una sola vez&lt;/strong> para todos los tokens del batch (el base es el mismo). Lo que cambia entre tokens es solo el delta &lt;code>B_i(A_i x)&lt;/code>. Si los tokens del batch usan adapters distintos, hay que aplicar deltas distintos por token — y eso es lo que el kernel SGMV hace en una pasada.&lt;/p>
&lt;p>Sin un kernel especializado, esto se degenera: hace falta lanzar N matmuls separados (uno por adapter), pagar overhead de kernel launch N veces y perder el batching. Con un kernel especializado (SGMV), todos los deltas se computan en una pasada agrupada por adapter.&lt;/p>
&lt;h2 id="sgmv-el-kernel-que-sostiene-todo">SGMV: el kernel que sostiene todo&lt;/h2>
&lt;p>&lt;strong>SGMV&lt;/strong> (Segmented Gather Matrix-Vector multiplication) es el kernel CUDA que Punica introdujo y que vLLM, LoRAX, SGLang y TGI han adoptado como motor multi-LoRA.&lt;/p>
&lt;p>Su trabajo es computar, dado un batch de tokens con adapters mixtos:&lt;/p>
&lt;p>$$y_t = W x_t + B_{a(t)} A_{a(t)} x_t \quad \forall t \in \text{batch}$$&lt;/p>
&lt;p>donde &lt;code>a(t)&lt;/code> es el adapter asignado al token &lt;code>t&lt;/code>. SGMV opera en dos fases:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>SGMV-shrink&lt;/strong>: proyección &lt;code>d_in → r&lt;/code> con la matriz &lt;code>A_{a(t)}&lt;/code> correspondiente.&lt;/li>
&lt;li>&lt;strong>SGMV-expand&lt;/strong>: proyección &lt;code>r → d_out&lt;/code> con la matriz &lt;code>B_{a(t)}&lt;/code> correspondiente.&lt;/li>
&lt;/ol>
&lt;p>Internamente, SGMV agrupa los tokens del batch por adapter (&lt;code>segmenta&lt;/code>), y para cada segment usa el kernel óptimo según el tamaño: para segmentos grandes (varios requests con el mismo adapter), pasa por tensor cores; para segmentos pequeños (un request por adapter), usa el path &lt;em>batched gather&lt;/em> que minimiza overhead de launch.&lt;/p>
&lt;p>El resultado, en una sola pasada de kernel, es el delta correcto para todos los tokens del batch, cualquier sea su rank o su adapter. Punica reportó hasta &lt;strong>12× throughput vs vLLM/FasterTransformer/HF Transformers/DeepSpeed&lt;/strong> en escenarios multi-tenant heterogéneos; cuando todos los requests usan el mismo adapter, SGMV es prácticamente equivalente al base sin LoRA porque se reduce al caso &amp;ldquo;un solo segment grande&amp;rdquo; óptimo para tensor cores.&lt;/p>
&lt;p>S-LoRA refinó SGMV con dos kernels específicos para distintas fases del serving: &lt;strong>MBGMM&lt;/strong> (Multi-size Batched Gather Matrix-Matrix) para prefill, &lt;strong>MBGMV&lt;/strong> para decode. Ambos soportan rank distinto entre requests del mismo batch, lo que en SGMV original era una limitación.&lt;/p>
&lt;div class="diagram" style="max-width:780px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 780 380" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Multi-LoRA serving con SGMV kernel y cache jerárquico">
&lt;style>
.base{fill:#d4ecff;stroke:#1f5fa8;stroke-width:1.4;rx:8}
.gpu{fill:#fff4d6;stroke:#a48000;stroke-width:1.2;rx:4}
.cpu{fill:#cdebd0;stroke:#2a7a40;stroke-width:1.2;rx:4}
.s3{fill:#e6d0ff;stroke:#5a2db0;stroke-width:1.2;rx:4}
.req{fill:#f6e0c8;stroke:#a76b1f;stroke-width:1.2;rx:4}
.kern{fill:#f6caca;stroke:#a52a2a;stroke-width:1.4;rx:6}
.lbl{font:600 12px sans-serif;fill:#222}
.sub{font:400 10px sans-serif;fill:#555}
.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#mlk)}
.evict{stroke:#999;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#mlk)}
&lt;/style>
&lt;defs>&lt;marker id="mlk" 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;/defs>
&lt;p>&lt;text x="20" y="22" class="lbl">1. Batch heterogéneo: cuatro requests con tres adapters distintos&lt;/text>
&lt;rect x="20" y="35" width="110" height="35" class="req"/>&lt;text x="75" y="55" text-anchor="middle" class="lbl">req_1 → A12&lt;/text>&lt;text x="75" y="68" text-anchor="middle" class="sub">tenant_1&lt;/text>
&lt;rect x="140" y="35" width="110" height="35" class="req"/>&lt;text x="195" y="55" text-anchor="middle" class="lbl">req_2 → A12&lt;/text>&lt;text x="195" y="68" text-anchor="middle" class="sub">tenant_1 (otro chat)&lt;/text>
&lt;rect x="260" y="35" width="110" height="35" class="req"/>&lt;text x="315" y="55" text-anchor="middle" class="lbl">req_3 → A47&lt;/text>&lt;text x="315" y="68" text-anchor="middle" class="sub">tenant_2&lt;/text>
&lt;rect x="380" y="35" width="110" height="35" class="req"/>&lt;text x="435" y="55" text-anchor="middle" class="lbl">req_4 → A89&lt;/text>&lt;text x="435" y="68" text-anchor="middle" class="sub">tenant_3&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="105" class="lbl">2. SGMV kernel: agrupa por adapter, computa en una sola pasada&lt;/text>
&lt;rect x="20" y="115" width="470" height="60" class="kern"/>
&lt;text x="255" y="138" text-anchor="middle" class="lbl">SGMV: Y = Wx (base) + Σ_a B_a · A_a · x_{tokens(a)}&lt;/text>
&lt;text x="255" y="156" text-anchor="middle" class="sub">segmento A12 (2 reqs, rank=16) | segmento A47 (1 req, rank=8) | segmento A89 (1 req, rank=32)&lt;/text>
&lt;text x="255" y="169" text-anchor="middle" class="sub">tensor cores para segmento grande, batched gather para los pequeños&lt;/text>&lt;/p>
&lt;p>&lt;text x="20" y="200" class="lbl">3. Memoria GPU: base compartido + pool unificado de adapters + KV cache&lt;/text>
&lt;rect x="20" y="210" width="470" height="50" class="base"/>
&lt;text x="255" y="230" text-anchor="middle" class="lbl">BASE: Llama-3-70B FP8 (~70 GB) — cargado una vez, compartido por todos&lt;/text>
&lt;text x="255" y="248" text-anchor="middle" class="sub">recibe Wx para todos los tokens del batch sin importar adapter&lt;/text>&lt;/p>
&lt;rect x="20" y="270" width="220" height="55" class="gpu"/>
&lt;text x="130" y="290" text-anchor="middle" class="lbl">POOL HBM: ~10 GB libres&lt;/text>
&lt;text x="130" y="305" text-anchor="middle" class="sub">~25 adapters r=16 activos (hot)&lt;/text>
&lt;text x="130" y="318" text-anchor="middle" class="sub">A12, A47, A89, A03, A18, A23, ...&lt;/text>
&lt;rect x="270" y="270" width="220" height="55" class="cpu"/>
&lt;text x="380" y="290" text-anchor="middle" class="lbl">CACHE RAM: ~512 GB&lt;/text>
&lt;text x="380" y="305" text-anchor="middle" class="sub">~1300 adapters warm&lt;/text>
&lt;text x="380" y="318" text-anchor="middle" class="sub">LRU eviction; H2D async al usarse&lt;/text>
&lt;p>&lt;text x="520" y="232" class="lbl">4. STORAGE&lt;/text>
&lt;rect x="520" y="240" width="240" height="85" class="s3"/>
&lt;text x="640" y="260" text-anchor="middle" class="lbl">MinIO / S3 / HF Hub&lt;/text>
&lt;text x="640" y="278" text-anchor="middle" class="sub">cold storage: miles de adapters&lt;/text>
&lt;text x="640" y="293" text-anchor="middle" class="sub">A0001 &amp;hellip; A9999&lt;/text>
&lt;text x="640" y="308" text-anchor="middle" class="sub">cold start: ~0.5-5s por adapter&lt;/text>&lt;/p>
&lt;path class="arr" d="M270,297 L240,297"/>
&lt;path class="evict" d="M240,310 L270,310"/>
&lt;path class="arr" d="M520,295 L490,295"/>
&lt;p>&lt;text x="20" y="355" class="sub">Flecha sólida = path on-demand (cache miss). Discontinua = eviction LRU al evictar.&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 números mueven cualquier decisión operacional con multi-LoRA.&lt;/p>
&lt;p>&lt;strong>Memoria por adapter.&lt;/strong> Para una matriz &lt;code>d_in × d_out&lt;/code> con rank &lt;code>r&lt;/code> y &lt;code>b&lt;/code> bytes por parámetro (BF16/FP16 = 2):&lt;/p>
&lt;p>$$\text{bytes_por_matriz} = (d_{\text{in}} \cdot r + r \cdot d_{\text{out}}) \cdot b$$&lt;/p>
&lt;p>Sumando sobre todas las matrices target de cada layer y multiplicando por el número de layers, se obtiene el tamaño del adapter. Cálculo concreto para &lt;strong>Llama-3-70B&lt;/strong>, rank 16, BF16, &lt;strong>todas&lt;/strong> las matrices (Q, K, V, O, gate, up, down):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Matriz&lt;/th>
&lt;th>Dimensión&lt;/th>
&lt;th>Bytes&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Q (8192→8192)&lt;/td>
&lt;td>8192·16 + 16·8192&lt;/td>
&lt;td>524 288&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>O (8192→8192)&lt;/td>
&lt;td>8192·16 + 16·8192&lt;/td>
&lt;td>524 288&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>K (8192→1024)&lt;/td>
&lt;td>8192·16 + 16·1024&lt;/td>
&lt;td>294 912&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>V (8192→1024)&lt;/td>
&lt;td>8192·16 + 16·1024&lt;/td>
&lt;td>294 912&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>gate (8192→28 672)&lt;/td>
&lt;td>8192·16 + 16·28 672&lt;/td>
&lt;td>1 179 648&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>up (8192→28 672)&lt;/td>
&lt;td>8192·16 + 16·28 672&lt;/td>
&lt;td>1 179 648&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>down (28 672→8192)&lt;/td>
&lt;td>28 672·16 + 16·8192&lt;/td>
&lt;td>1 179 648&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Suma por layer&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>5 177 344 ≈ 4.94 MB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>Total adapter (80 layers)&lt;/strong>&lt;/td>
&lt;td>&lt;/td>
&lt;td>&lt;strong>~395 MB&lt;/strong>&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Si se limita a attention-only (Q, O, V, K): ~125 MB por adapter. La elección de qué matrices recibe LoRA es del entrenador; en serving se hereda y determina el coste.&lt;/p>
&lt;p>&lt;strong>Memoria por rank.&lt;/strong> Lineal: rank 8 → ~200 MB; rank 16 → ~400 MB; rank 32 → ~800 MB; rank 64 → ~1.6 GB. La regla simple para el dimensionamiento es: &lt;strong>&lt;code>max_lora_rank&lt;/code> debe ser el rank máximo que vas a servir, no más&lt;/strong> — fijarlo más alto desperdicia memoria reservada en cada slot.&lt;/p>
&lt;p>&lt;strong>Cuántos adapters caben.&lt;/strong> Para H100 SXM 80 GB con base Llama-3-70B FP8 (~70 GB), quedan ~10 GB libres tras KV cache mínimo → ~&lt;strong>25 adapters r=16 fully target&lt;/strong> o ~&lt;strong>80 attention-only&lt;/strong>. Con base AWQ INT4 (~35 GB), quedan ~45 GB → cientos de adapters. La regla: &lt;strong>cuantizar el base no solo libera memoria, multiplica la economía de la plataforma&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>Overhead de latencia por adapter.&lt;/strong> En condiciones reales reportadas:&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Caso&lt;/th>
&lt;th>Overhead típico&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Todos los requests del batch mismo adapter&lt;/td>
&lt;td>~0 % (equivalente a merge estático)&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch heterogéneo, ranks similares (e.g., todos r=16)&lt;/td>
&lt;td>10-30 % por capa&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Batch heterogéneo, ranks dispares (r=8 con r=128)&lt;/td>
&lt;td>hasta +84 % P95 TTFT al de rank menor&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>LoRA naive PEFT (sin SGMV)&lt;/td>
&lt;td>250-950 % extra&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Escala lineal con rank: rank 8 ≈ baseline; rank 64 ≈ 3-4 × overhead.&lt;/p>
&lt;h2 id="las-implementaciones-reales-en-mayo-2026">Las implementaciones reales en mayo 2026&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Implementación&lt;/th>
&lt;th>Kernel base&lt;/th>
&lt;th>Hot-swap&lt;/th>
&lt;th>Quantized base + LoRA&lt;/th>
&lt;th>Notas operacionales&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;strong>vLLM&lt;/strong>&lt;/td>
&lt;td>SGMV + ext&lt;/td>
&lt;td>Sí (LoRAResolver, plugins S3/HF/FS)&lt;/td>
&lt;td>AWQ/GPTQ sí, bnb 4-bit solo offline&lt;/td>
&lt;td>Default de facto. &lt;code>--enable-lora --max-loras N --max-lora-rank R --max-cpu-loras M&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>LoRAX&lt;/strong> (Predibase)&lt;/td>
&lt;td>SGMV optimizado&lt;/td>
&lt;td>Sí (dynamic loading)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Diseñado específicamente para multi-LoRA. Soporta adapters Medusa por adapter (spec-dec por adapter).&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>SGLang&lt;/strong>&lt;/td>
&lt;td>SGMV / csgmv&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;code>--enable-lora-overlap-loading&lt;/code> reduce TTFT hasta 78 % en workloads LoRA-heavy&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>TensorRT-LLM&lt;/strong>&lt;/td>
&lt;td>LoRA Executor (C++)&lt;/td>
&lt;td>Pre-compile build-time&lt;/td>
&lt;td>INT4 + LoRA común&lt;/td>
&lt;td>Pico de throughput en H100/B200, menos flexible que vLLM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>HF TGI&lt;/strong>&lt;/td>
&lt;td>Punica fork&lt;/td>
&lt;td>Sí (&lt;code>LORA_ADAPTERS=...&lt;/code>)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>En &lt;em>maintenance mode&lt;/em> mayo 2026; HF recomienda vLLM o SGLang&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;strong>NVIDIA NIM&lt;/strong>&lt;/td>
&lt;td>TRT-LLM under hood&lt;/td>
&lt;td>Static o dynamic (&lt;code>NIM_PEFT_REFRESH_INTERVAL&lt;/code>)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>Adapter store por modelo; polling para hot-add/remove&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Tres observaciones operacionales:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>vLLM domina&lt;/strong> en open-source serving en 2026 por la combinación de SGMV maduro + LoRAResolver plugins + soporte de base cuantizada. El parámetro crítico es &lt;code>--max-lora-rank&lt;/code>: muchas instalaciones lo ponen al máximo &amp;ldquo;por si acaso&amp;rdquo; y desperdician memoria de forma silenciosa.&lt;/li>
&lt;li>&lt;strong>LoRAX gana&lt;/strong> en operaciones de producción con miles de adapters poco usados gracias a su dynamic loading que no bloquea requests concurrentes. Caso público: Convirza con 60+ adapters concurrentes y P95 sub-2s.&lt;/li>
&lt;li>&lt;strong>SGLang gana&lt;/strong> en latencia cuando los cold starts son frecuentes gracias a &lt;code>--enable-lora-overlap-loading&lt;/code> (H2D async durante el compute del request previo).&lt;/li>
&lt;/ol>
&lt;h2 id="patrón-combinado-con-quantization-y-disaggregated-serving">Patrón combinado con quantization y disaggregated serving&lt;/h2>
&lt;p>&lt;strong>Con quantization&lt;/strong> (&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization&lt;/a>). El stack canónico mayo 2026 es &lt;strong>base en FP8 (Hopper/Blackwell) o INT4 AWQ + adapters en BF16/FP16&lt;/strong>. Los adapters no se cuantizan: son pequeños, el ahorro de memoria es irrelevante, y el ruido de quantization se acumularía mal con el delta. Cuantizar el base &lt;strong>libera memoria masiva para más adapters&lt;/strong> sin pérdida significativa (&amp;lt;1 % en MMLU típico con AWQ INT4, algo más en math/code/reasoning). Un adapter entrenado con base BF16 funciona con base FP8/INT4 en inferencia con pérdida marginal — esto es lo que hace operacionalmente trivial el QLoRA: entrenar con base 4-bit y desplegar con base 4-bit consistente.&lt;/p>
&lt;p>&lt;strong>Con disaggregated serving&lt;/strong> (&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving&lt;/a>). Multi-LoRA + prefill/decode disaggregated añade una capa de gestión: cada pod necesita acceso al adapter activo del request. Estrategia 2026: &lt;strong>replicar adapters hot en todos los pods (prefill y decode), evictar fríos&lt;/strong>. Para adapters cold no presentes en el pod destino se transfieren bajo demanda, asumiendo coste extra en TTFT. Trabajos recientes (InfiniLoRA, FASTLIBRA, LoRAServe) automatizan este balanceo, pero la regla del pulgar simple funciona en la mayoría de despliegues.&lt;/p>
&lt;h2 id="pitfalls-operacionales">Pitfalls operacionales&lt;/h2>
&lt;p>&lt;strong>Cold start.&lt;/strong> El primer request a un adapter dormido implica fetch (de S3/MinIO/HF Hub) → load CPU → copy H2D. Para un adapter de ~400 MB: 0.5-5 s típicos, dependiendo de bandwidth. Bajo concurrencia alta puede ser &lt;strong>hasta 35 % del E2E latency&lt;/strong> (paper Predictive-LoRA, arXiv:2512.20210). Mitigaciones consolidadas: SGLang &lt;code>--enable-lora-overlap-loading&lt;/code> (reduce TTFT 35-78 %); vLLM pre-warming con dummy request al alta de adapter; prefetching predictivo basado en patrones (Predictive-LoRA reduce cold start un 68 %).&lt;/p>
&lt;p>&lt;strong>Worst case heterogéneo.&lt;/strong> Si cada request del batch tiene adapter distinto &lt;strong>y rank distinto&lt;/strong>, el SGMV pierde su ventaja porque cada segment es de tamaño 1. Throughput puede caer hasta 50 % vs base sin LoRA. Mitigación práctica: &lt;strong>agrupar adapters por rank en el routing previo&lt;/strong>, intentando que requests del mismo rank vayan al mismo batch step.&lt;/p>
&lt;p>&lt;strong>Rank dispar.&lt;/strong> Co-batching de rank 8 con rank 128 penaliza al pequeño: &lt;strong>+84 % P95 TTFT&lt;/strong> para los requests del rank-8 (paper Serving Heterogeneous LoRA Adapters, arXiv:2511.22880). Práctica: normalizar el rank dentro del fleet siempre que sea posible (entrenar todos los adapters al mismo rank, o al menos en un rango pequeño).&lt;/p>
&lt;p>&lt;strong>Eviction.&lt;/strong> LRU es el default. Si tienes más adapters que cabe en RAM CPU, los evictados se vuelven a fetchear del cold storage. Monitorear &lt;code>cold_starts_per_minute&lt;/code> y &lt;code>cache_hit_ratio&lt;/code> por endpoint es básico.&lt;/p>
&lt;p>&lt;strong>Versionado del base.&lt;/strong> Cada adapter está pegado a una versión exacta del base (Llama-3-70B-Instruct ≠ Llama-3.1-70B-Instruct). El routing debe validar la pareja base+adapter antes de servir, o el output será basura silenciosa.&lt;/p>
&lt;p>&lt;strong>Fragmentación de memoria.&lt;/strong> Sin paged management (caso pre-S-LoRA / pre-vLLM), evictar e insertar adapters fragmenta HBM hasta hacerla inservible. Unified Paging lo resuelve: los pesos LoRA y el KV cache viven en el mismo pool de bloques, intercambiables.&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> Caso clásico: base &lt;strong>Llama-3-8B FP8&lt;/strong> (~8 GB) o &lt;strong>Llama-3-8B AWQ-INT4&lt;/strong> (~5 GB) + decenas de adapters r=16 (~80 MB cada uno, son ~3 MB por adapter para Llama-3-8B con r=16 attention-only). En la 4090 entran fácilmente 50-100 adapters activos. Es el setup natural para &lt;strong>demos multi-tenant&lt;/strong>, &lt;strong>fine-tuning sobre 8B base&lt;/strong> y &lt;strong>prototipos de plataforma&lt;/strong>.&lt;/p>
&lt;p>&lt;strong>En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo).&lt;/strong> Aquí entra el setup serio:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Llama-3-70B FP8&lt;/strong> (~70 GB) cabe en 2 H100; las otras 2 GPUs disponibles para batch + adapters.&lt;/li>
&lt;li>&lt;strong>Llama-3-70B AWQ-INT4&lt;/strong> (~35 GB) cabe en 1 H100; el resto del cluster sirve más concurrencia o adapters.&lt;/li>
&lt;li>&lt;strong>~200 adapters r=16 fully target&lt;/strong> caben con presupuesto holgado en el cluster, suficientes para una plataforma SaaS con docenas de tenants y A/B simultáneo.&lt;/li>
&lt;li>&lt;strong>QLoRA training + serving consistente&lt;/strong>: el ciclo entrenar→deployar adapter es de horas, no días, gracias a que el adapter es ~400 MB en lugar de ~140 GB.&lt;/li>
&lt;/ul>
&lt;p>La regla de pulgar en cluster H100 mayo 2026: &lt;strong>base FP8 o INT4 + 100-500 adapters por cluster son operacionalmente triviales con vLLM o LoRAX; pasar de mil adapters concurrentes empieza a requerir tuning serio de eviction y prefetch&lt;/strong>.&lt;/p>
&lt;h2 id="stack-típico-en-producción">Stack típico en producción&lt;/h2>
&lt;pre tabindex="0">&lt;code>[API Gateway]
↓ (JWT con tenant_id / API key)
[Router]
↓ inyecta adapter_id en el request
[vLLM / LoRAX con --enable-lora]
--max-loras 16
--max-lora-rank 32
--max-cpu-loras 200
+ LoRAResolver → s3://adapters/{tenant_id}/{version}/
+ Base: Llama-3-70B-FP8 cargado una vez en 4×H100 TP=4
↓
[GPU]
Base FP8 (70 GB) + ~150 adapters BF16 hot en HBM + ~1000 warm en RAM
[MinIO / S3]
Storage cold de miles de adapters
[Pipeline CI]
→ entrena nuevo adapter QLoRA → push MinIO → notify server → warm-up
[Observability]
Prom: active_adapters, cache_hit_ratio, cold_starts_per_minute,
per_adapter_throughput, P99_with_lora vs P99_base
&lt;/code>&lt;/pre>&lt;p>Patrón de routing: &lt;code>Authorization: Bearer &amp;lt;key&amp;gt;&lt;/code> → middleware extrae &lt;code>tenant_id&lt;/code> → mapea a &lt;code>adapter_id&lt;/code> (Postgres o Redis) → &lt;code>POST /v1/completions&lt;/code> con &lt;code>model: &amp;quot;&amp;lt;adapter_id&amp;gt;&amp;quot;&lt;/code>.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto">Lo que no hemos cubierto&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>DoRA (Weight-Decomposed LoRA)&lt;/strong>: descompone la actualización en magnitud + dirección, cierra parte del gap de calidad con full fine-tuning. Soportado por TensorRT-LLM y otros, pero el patrón de serving es idéntico a LoRA.&lt;/li>
&lt;li>&lt;strong>MoE + LoRA&lt;/strong>: cómo se hace fine-tuning de adapters sobre un MoE, qué pasa con el routing — no trivial, área activa de investigación 2026.&lt;/li>
&lt;li>&lt;strong>Activated LoRA&lt;/strong> (arXiv:2512.17910): variante que reutiliza KV cache entre adapters compatibles, reduciendo el coste de cold start con prefijos compartidos.&lt;/li>
&lt;li>&lt;strong>LoRA para speculative decoding&lt;/strong>: cada adapter trae su propia Medusa head, soportado por LoRAX como &amp;ldquo;Turbo LoRA&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>Compress-then-Serve&lt;/strong> (arXiv:2407.00066): cuantizar los propios adapters para servir aún más concurrentes. Práctica todavía marginal en producción a mayo 2026.&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/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — el productor del fleet de adapters; multi-LoRA serving es el consumidor que cierra el ciclo.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle feedback → dataset → adapter&lt;/a> — cómo se entrena un adapter nuevo a partir de feedback de producción; el adapter resultante se sirve con este stack.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/alignment-moderno-dpo-kto-orpo-simpo/">Alignment moderno: DPO, KTO, ORPO, SimPO&lt;/a> — cada política de alignment puede vivir como un adapter distinto; multi-LoRA permite servirlas en paralelo y hacer A/B.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/quantization-fundamentos-inferencia/">Quantization para inferencia LLM&lt;/a> — cuantizar el base libera memoria para muchos más adapters; el stack canónico es base FP8/INT4 + adapter BF16.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/disaggregated-serving-prefill-decode/">Disaggregated serving: prefill y decode&lt;/a> — al separar pools, los adapters se gestionan por pod; estrategia 2026 es replicar hot en todos, evictar fríos.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/moe-inference-fundamentos/">MoE inference: el call center con 256 especialistas&lt;/a> — LoRA sobre MoE base es área activa; el routing del MoE complica el aplicar deltas correctamente.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/continuous-batching-fundamentos/">Continuous batching&lt;/a> — la tensión operacional crítica de multi-LoRA: cuando cada request del batch usa un adapter (y rank) distinto, el throughput puede caer hasta el 50 %; el scheduler debe agrupar requests por rank y adapter para mantener la economía.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/structured-output-fundamentos/">Structured output&lt;/a> — un adapter puede entrenarse específicamente para function calling o extracción; combinado con XGrammar da el contrato fuerte (adapter afín a la tarea + grammar que garantiza schema).&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>Hu, E. et al. &lt;em>LoRA: Low-Rank Adaptation of Large Language Models&lt;/em>. ICLR 2022. &lt;a href="https://arxiv.org/abs/2106.09685">https://arxiv.org/abs/2106.09685&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>Chen, L. et al. &lt;em>Punica: Multi-Tenant LoRA Serving&lt;/em>. MLSys 2024. &lt;a href="https://arxiv.org/abs/2310.18547">https://arxiv.org/abs/2310.18547&lt;/a>&lt;/li>
&lt;li>Dettmers, T. et al. &lt;em>QLoRA: Efficient Finetuning of Quantized LLMs&lt;/em>. NeurIPS 2023. &lt;a href="https://arxiv.org/abs/2305.14314">https://arxiv.org/abs/2305.14314&lt;/a>&lt;/li>
&lt;li>Brüel-Gabrielsson, R. et al. &lt;em>Compress then Serve: Serving Thousands of LoRA Adapters with Little Overhead&lt;/em>. 2024. &lt;a href="https://arxiv.org/abs/2407.00066">https://arxiv.org/abs/2407.00066&lt;/a>&lt;/li>
&lt;li>&lt;em>Serving Heterogeneous LoRA Adapters in Distributed LLM Inference&lt;/em>. 2025. &lt;a href="https://arxiv.org/abs/2511.22880">https://arxiv.org/abs/2511.22880&lt;/a>&lt;/li>
&lt;li>&lt;em>InfiniLoRA: Disaggregated Multi-LoRA Serving&lt;/em>. 2026. &lt;a href="https://arxiv.org/abs/2604.07173">https://arxiv.org/abs/2604.07173&lt;/a>&lt;/li>
&lt;li>&lt;em>Predictive-LoRA: Proactive Fragmentation-Aware Serverless LoRA Serving&lt;/em>. &lt;a href="https://arxiv.org/abs/2512.20210">https://arxiv.org/abs/2512.20210&lt;/a>&lt;/li>
&lt;li>Repo S-LoRA: &lt;a href="https://github.com/S-LoRA/S-LoRA">https://github.com/S-LoRA/S-LoRA&lt;/a>&lt;/li>
&lt;li>Repo Punica: &lt;a href="https://github.com/punica-ai/punica">https://github.com/punica-ai/punica&lt;/a>&lt;/li>
&lt;li>Repo LoRAX (Predibase): &lt;a href="https://github.com/predibase/lorax">https://github.com/predibase/lorax&lt;/a>&lt;/li>
&lt;li>vLLM LoRA docs: &lt;a href="https://docs.vllm.ai/en/stable/features/lora/">https://docs.vllm.ai/en/stable/features/lora/&lt;/a>&lt;/li>
&lt;li>SGLang LoRA docs: &lt;a href="https://docs.sglang.io/advanced_features/lora.html">https://docs.sglang.io/advanced_features/lora.html&lt;/a>&lt;/li>
&lt;li>TensorRT-LLM LoRA docs: &lt;a href="https://nvidia.github.io/TensorRT-LLM/advanced/lora.html">https://nvidia.github.io/TensorRT-LLM/advanced/lora.html&lt;/a>&lt;/li>
&lt;li>NVIDIA NIM PEFT: &lt;a href="https://docs.nvidia.com/nim/large-language-models/latest/peft.html">https://docs.nvidia.com/nim/large-language-models/latest/peft.html&lt;/a>&lt;/li>
&lt;li>HF TGI Multi-LoRA blog: &lt;a href="https://huggingface.co/blog/multi-lora-serving">https://huggingface.co/blog/multi-lora-serving&lt;/a>&lt;/li>
&lt;li>LMSYS S-LoRA recipe blog: &lt;a href="https://www.lmsys.org/blog/2023-11-15-slora/">https://www.lmsys.org/blog/2023-11-15-slora/&lt;/a>&lt;/li>
&lt;li>Predibase LoRAX blog: &lt;a href="https://predibase.com/blog/lorax-the-open-source-framework-for-serving-100s-of-fine-tuned-llms-in">https://predibase.com/blog/lorax-the-open-source-framework-for-serving-100s-of-fine-tuned-llms-in&lt;/a>&lt;/li>
&lt;/ul></description></item></channel></rss>