Multi-LoRA serving: el traductor único con mil glosarios — base compartido, miles de adapters concurrentes y el kernel SGMV

Este post complementa el de Fine-tuning continuo en producción. 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 Alignment moderno (DPO/KTO/ORPO/SimPO) (cada política de alignment puede vivir como un adapter distinto) y Quantization (el base cuantizado libera memoria para muchos más adapters).

Estás aquí: DEPLOY

Estás aquí: DEPLOY · base compartido, N adapters concurrentes en una sola GPU1 · Data2 · Tune3 · Eval4 · Deploy5 · Observe6 · Retrain

TL;DR

El patrón dominante en 2026 no es un modelo por cliente sino un único modelo base de propósito general más N adapters LoRA finos 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 cómo servirlos concurrentemente 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: S-LoRA (Sheng et al., Stanford + UC Berkeley, MLSys 2024) introdujo unified paging —los pesos de los adapters viven en el mismo pool de memoria que el KV cache, ambos paginables— y heterogeneous batching —un batch puede tener requests con adapters distintos y rank distintos sin padding—; Punica (Chen et al., UW + Duke, MLSys 2024) introdujo el kernel CUDA que se ha convertido en estándar de facto: SGMV (Segmented Gather Matrix-Vector multiplication), que computa en una sola pasada Y += Σ_i X_i · A_i · B_i agrupando requests por adapter. SGMV está hoy debajo de vLLM, LoRAX (Predibase), SGLang y TGI. El resultado operacional medible: hasta 2 000 adapters concurrentes en una sola GPU (S-LoRA paper), hasta 4× throughput vs vLLM naive y hasta 30× vs HuggingFace PEFT. El precio: overhead típico 10-30 % de latencia por capa con adapter activo en batch heterogéneo, prácticamente cero cuando todos los requests del batch usan el mismo adapter, 20-40 % 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.

La analogía: el traductor único con mil glosarios

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 caro de contratar y caro de entrenar: 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.

La agencia no contrata un traductor por cliente —sería ridículo, son el 90 % del trabajo común—. Lo que hace es mantener un glosario por cliente: 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.

Los glosarios viven en una estantería compartida, 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.

Y lo más importante: el traductor puede tener varios glosarios abiertos a la vez 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.

La analogía se sostiene en cinco mapeos:

  • El traductor único = el modelo base (Llama-3-70B, Qwen2.5-72B). Caro de entrenar, una sola copia en VRAM.
  • Cada glosario = un adapter LoRA. Pequeño (~150-400 MB), específico, fácil de actualizar.
  • La estantería con los glosarios a mano = el pool de adapters cacheados en VRAM (típicamente 50-200 a la vez con base FP8 en H100 80 GB).
  • El archivo = el storage de adapters en MinIO/S3/HF Hub. Cientos o miles, fetcheados on-demand.
  • Trabajar en paralelo con varios glosarios abiertos = batch heterogéneo con SGMV. El kernel que hace la consulta agrupada al glosario correcto por cada palabra del batch.

El mecanismo desnudo: qué hace un LoRA y por qué se puede servir multi-tenant

Un adapter LoRA modifica una matriz W del modelo base sumándole un producto de bajo rango:

$$W’ = W + B A$$

donde W ∈ R^{d_out × d_in} es la matriz original (los pesos del base), A ∈ R^{r × d_in} y B ∈ R^{d_out × r} son las matrices entrenables del adapter, y r es el rank (típicamente 8, 16, 32 o 64 — siempre mucho menor que d_in y d_out).

En un forward pass, en lugar de calcular y = W' x, se calcula:

$$y = W x + B(Ax)$$

Es decir: el cómputo del base (Wx) ocurre exactamente igual; el adapter añade dos matmuls baratos (Ax y luego B(·)) que añaden la corrección. La matriz BA nunca se materializa explícitamente.

Lo que esto habilita en serving: si tienes el base cargado y N adapters distintos, el Wx se calcula una sola vez para todos los tokens del batch (el base es el mismo). Lo que cambia entre tokens es solo el delta B_i(A_i x). 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.

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.

SGMV: el kernel que sostiene todo

SGMV (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.

Su trabajo es computar, dado un batch de tokens con adapters mixtos:

$$y_t = W x_t + B_{a(t)} A_{a(t)} x_t \quad \forall t \in \text{batch}$$

donde a(t) es el adapter asignado al token t. SGMV opera en dos fases:

  1. SGMV-shrink: proyección d_in → r con la matriz A_{a(t)} correspondiente.
  2. SGMV-expand: proyección r → d_out con la matriz B_{a(t)} correspondiente.

Internamente, SGMV agrupa los tokens del batch por adapter (segmenta), 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 batched gather que minimiza overhead de launch.

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 12× throughput vs vLLM/FasterTransformer/HF Transformers/DeepSpeed 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 “un solo segment grande” óptimo para tensor cores.

S-LoRA refinó SGMV con dos kernels específicos para distintas fases del serving: MBGMM (Multi-size Batched Gather Matrix-Matrix) para prefill, MBGMV para decode. Ambos soportan rank distinto entre requests del mismo batch, lo que en SGMV original era una limitación.

1. Batch heterogéneo: cuatro requests con tres adapters distintosreq_1 → A12tenant_1req_2 → A12tenant_1 (otro chat)req_3 → A47tenant_2req_4 → A89tenant_3

2. SGMV kernel: agrupa por adapter, computa en una sola pasadaSGMV: Y = Wx (base) + Σ_a B_a · A_a · x_{tokens(a)}segmento A12 (2 reqs, rank=16) | segmento A47 (1 req, rank=8) | segmento A89 (1 req, rank=32)tensor cores para segmento grande, batched gather para los pequeños

3. Memoria GPU: base compartido + pool unificado de adapters + KV cacheBASE: Llama-3-70B FP8 (~70 GB) — cargado una vez, compartido por todosrecibe Wx para todos los tokens del batch sin importar adapter

POOL HBM: ~10 GB libres~25 adapters r=16 activos (hot)A12, A47, A89, A03, A18, A23, ...CACHE RAM: ~512 GB~1300 adapters warmLRU eviction; H2D async al usarse

4. STORAGEMinIO / S3 / HF Hubcold storage: miles de adaptersA0001 … A9999cold start: ~0.5-5s por adapter

Flecha sólida = path on-demand (cache miss). Discontinua = eviction LRU al evictar.

La matemática que importa

Tres números mueven cualquier decisión operacional con multi-LoRA.

Memoria por adapter. Para una matriz d_in × d_out con rank r y b bytes por parámetro (BF16/FP16 = 2):

$$\text{bytes_por_matriz} = (d_{\text{in}} \cdot r + r \cdot d_{\text{out}}) \cdot b$$

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 Llama-3-70B, rank 16, BF16, todas las matrices (Q, K, V, O, gate, up, down):

MatrizDimensiónBytes
Q (8192→8192)8192·16 + 16·8192524 288
O (8192→8192)8192·16 + 16·8192524 288
K (8192→1024)8192·16 + 16·1024294 912
V (8192→1024)8192·16 + 16·1024294 912
gate (8192→28 672)8192·16 + 16·28 6721 179 648
up (8192→28 672)8192·16 + 16·28 6721 179 648
down (28 672→8192)28 672·16 + 16·81921 179 648
Suma por layer5 177 344 ≈ 4.94 MB
Total adapter (80 layers)~395 MB

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.

Memoria por rank. 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: max_lora_rank debe ser el rank máximo que vas a servir, no más — fijarlo más alto desperdicia memoria reservada en cada slot.

Cuántos adapters caben. Para H100 SXM 80 GB con base Llama-3-70B FP8 (~70 GB), quedan ~10 GB libres tras KV cache mínimo → ~25 adapters r=16 fully target o ~80 attention-only. Con base AWQ INT4 (~35 GB), quedan ~45 GB → cientos de adapters. La regla: cuantizar el base no solo libera memoria, multiplica la economía de la plataforma.

Overhead de latencia por adapter. En condiciones reales reportadas:

CasoOverhead típico
Todos los requests del batch mismo adapter~0 % (equivalente a merge estático)
Batch heterogéneo, ranks similares (e.g., todos r=16)10-30 % por capa
Batch heterogéneo, ranks dispares (r=8 con r=128)hasta +84 % P95 TTFT al de rank menor
LoRA naive PEFT (sin SGMV)250-950 % extra

Escala lineal con rank: rank 8 ≈ baseline; rank 64 ≈ 3-4 × overhead.

Las implementaciones reales en mayo 2026

ImplementaciónKernel baseHot-swapQuantized base + LoRANotas operacionales
vLLMSGMV + extSí (LoRAResolver, plugins S3/HF/FS)AWQ/GPTQ sí, bnb 4-bit solo offlineDefault de facto. --enable-lora --max-loras N --max-lora-rank R --max-cpu-loras M
LoRAX (Predibase)SGMV optimizadoSí (dynamic loading)Diseñado específicamente para multi-LoRA. Soporta adapters Medusa por adapter (spec-dec por adapter).
SGLangSGMV / csgmv--enable-lora-overlap-loading reduce TTFT hasta 78 % en workloads LoRA-heavy
TensorRT-LLMLoRA Executor (C++)Pre-compile build-timeINT4 + LoRA comúnPico de throughput en H100/B200, menos flexible que vLLM
HF TGIPunica forkSí (LORA_ADAPTERS=...)En maintenance mode mayo 2026; HF recomienda vLLM o SGLang
NVIDIA NIMTRT-LLM under hoodStatic o dynamic (NIM_PEFT_REFRESH_INTERVAL)Adapter store por modelo; polling para hot-add/remove

Tres observaciones operacionales:

  1. vLLM domina 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 --max-lora-rank: muchas instalaciones lo ponen al máximo “por si acaso” y desperdician memoria de forma silenciosa.
  2. LoRAX gana 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.
  3. SGLang gana en latencia cuando los cold starts son frecuentes gracias a --enable-lora-overlap-loading (H2D async durante el compute del request previo).

Patrón combinado con quantization y disaggregated serving

Con quantization (Quantization). El stack canónico mayo 2026 es base en FP8 (Hopper/Blackwell) o INT4 AWQ + adapters en BF16/FP16. 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 libera memoria masiva para más adapters sin pérdida significativa (<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.

Con disaggregated serving (Disaggregated serving). Multi-LoRA + prefill/decode disaggregated añade una capa de gestión: cada pod necesita acceso al adapter activo del request. Estrategia 2026: replicar adapters hot en todos los pods (prefill y decode), evictar fríos. 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.

Pitfalls operacionales

Cold start. 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 hasta 35 % del E2E latency (paper Predictive-LoRA, arXiv:2512.20210). Mitigaciones consolidadas: SGLang --enable-lora-overlap-loading (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 %).

Worst case heterogéneo. Si cada request del batch tiene adapter distinto y rank distinto, 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: agrupar adapters por rank en el routing previo, intentando que requests del mismo rank vayan al mismo batch step.

Rank dispar. Co-batching de rank 8 con rank 128 penaliza al pequeño: +84 % P95 TTFT 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).

Eviction. LRU es el default. Si tienes más adapters que cabe en RAM CPU, los evictados se vuelven a fetchear del cold storage. Monitorear cold_starts_per_minute y cache_hit_ratio por endpoint es básico.

Versionado del base. 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.

Fragmentación de memoria. 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.

Implicaciones en hardware on-premise

En una RTX 4090 (24 GB). Caso clásico: base Llama-3-8B FP8 (~8 GB) o Llama-3-8B AWQ-INT4 (~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 demos multi-tenant, fine-tuning sobre 8B base y prototipos de plataforma.

En un cluster genérico 4×H100 SXM (320 GB, NVLink, FP8 nativo). Aquí entra el setup serio:

  • Llama-3-70B FP8 (~70 GB) cabe en 2 H100; las otras 2 GPUs disponibles para batch + adapters.
  • Llama-3-70B AWQ-INT4 (~35 GB) cabe en 1 H100; el resto del cluster sirve más concurrencia o adapters.
  • ~200 adapters r=16 fully target caben con presupuesto holgado en el cluster, suficientes para una plataforma SaaS con docenas de tenants y A/B simultáneo.
  • QLoRA training + serving consistente: el ciclo entrenar→deployar adapter es de horas, no días, gracias a que el adapter es ~400 MB en lugar de ~140 GB.

La regla de pulgar en cluster H100 mayo 2026: 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.

Stack típico en producción

[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

Patrón de routing: Authorization: Bearer <key> → middleware extrae tenant_id → mapea a adapter_id (Postgres o Redis) → POST /v1/completions con model: "<adapter_id>".

Lo que no hemos cubierto

  • DoRA (Weight-Decomposed LoRA): 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.
  • MoE + LoRA: cómo se hace fine-tuning de adapters sobre un MoE, qué pasa con el routing — no trivial, área activa de investigación 2026.
  • Activated LoRA (arXiv:2512.17910): variante que reutiliza KV cache entre adapters compatibles, reduciendo el coste de cold start con prefijos compartidos.
  • LoRA para speculative decoding: cada adapter trae su propia Medusa head, soportado por LoRAX como “Turbo LoRA”.
  • Compress-then-Serve (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.

Ver también

Referencias