Multi-Adapter Serving
S-LoRA unified paging, Punica batched GEMM, adapter hot-swapping — serving hundreds of LoRA adapters on a shared base model.
In production, you serve many fine-tuned variants (one per customer, task, or language) from a shared base model. Multi-adapter serving makes this efficient.
1. The Problem
Without multi-adapter serving, each LoRA variant needs its own model instance:
- 100 adapters × 14 GB base = 1,400 GB — 18 H100 GPUs just for base model copies
- Each adapter adds only ~100 MB, but the base model is duplicated
Multi-adapter serving: One base model in GPU memory, swap LoRA adapters per request.
2. S-LoRA: Unified Paging
S-LoRA stores all adapters in a unified memory pool and pages them in/out like virtual memory:
GPU Memory:
[Base model: 14 GB] [Adapter page pool: 4 GB]
Request arrives for adapter_42:
1. Check if adapter_42 is in page pool → if yes, use it
2. If not, evict LRU adapter, load adapter_42 from CPU/disk
3. Compute: y = W₀x + (α/r) · B₄₂ · A₄₂ · x
| Metric | Separate instances | S-LoRA |
|---|---|---|
| GPU memory for 100 adapters | 100 × 14 GB = 1,400 GB | 14 GB + 4 GB pool = 18 GB |
| Adapter switch time | Load full model (~seconds) | Page in adapter (~ms) |
| Max concurrent adapters | Limited by GPU count | Thousands |
3. Punica: Batched Multi-LoRA GEMM
When batching requests with different adapters, the naive approach is one GEMM per adapter:
Request 1 (adapter A): y₁ = W₀x₁ + B_A · A_A · x₁
Request 2 (adapter B): y₂ = W₀x₂ + B_B · A_B · x₂
Request 3 (adapter A): y₃ = W₀x₃ + B_A · A_A · x₃
Punica’s SGMV (Segmented Gather Matrix-Vector) kernel batches these into one operation:
Base: Y_base = W₀ · [x₁, x₂, x₃] (one batched GEMM)
LoRA: Y_lora = SegmentedGatherMV(adapters, inputs, segments) (one fused kernel)
Y = Y_base + Y_lora
Throughput improvement: 2-4× vs naive per-adapter GEMM for diverse adapter batches.
4. vLLM Multi-LoRA Support
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
enable_lora=True,
max_lora_rank=16,
max_loras=100, # max concurrent adapters in memory
max_cpu_loras=1000, # max adapters on CPU (paged in on demand)
)
# Different requests use different adapters
output_1 = llm.generate("Hello", lora_request=LoRARequest("customer_a", 1, "path/to/adapter_a"))
output_2 = llm.generate("Bonjour", lora_request=LoRARequest("french", 2, "path/to/adapter_french"))
For managing adapter lifecycle and A/B testing, see Adapter Registry & Versioning.
Self-Check
- Explain the memory waste problem with separate model instances per adapter
- Describe S-LoRA’s unified paging approach
- Explain Punica’s batched multi-LoRA GEMM optimization
- Configure vLLM for multi-LoRA serving with adapter hot-swapping