Serving Many LoRA Adapters at Once

AILLMMLOpsInfrastructure
Share on LinkedIn

Every enterprise customer wanted their own fine-tuned support bot, but launching a full Llama-8B copy per tenant would have consumed 40 GB per instance times two hundred tenants. LoRA adapters shrink the per-customer delta to tens of megabytes — the hard problem becomes serving them concurrently on shared GPUs without loading a separate base model for each. Multi-LoRA serving is how platform teams offer personalized models economically: one base weights tensor, many low-rank overlays, routed per request.

LoRA recap for serving context

LoRA adds trainable low-rank matrices to attention and MLP layers:

W' = W + BA    (B ∈ R^{d×r}, A ∈ R^{r×k}, r << d)

At inference, either merge W' offline or apply BA dynamically. Multi-adapter serving keeps W frozen and swaps (B, A) pairs per request.

Architecture overview

                    ┌─────────────────────────┐
Request ──adapter_id──→ Router ──→ Batch scheduler
                    └──────────┬──────────────┘
                               ↓
                    Base model (shared, GPU)
                    + Adapter pool (A₁,B₁)...(Aₙ,Bₙ)
                               ↓
                         Token stream out

Components:

vLLM multi-LoRA example

vLLM supports --enable-lora with multiple adapters:

python -m vllm.entrypoints.openai.api_server \
  --model meta-llama/Llama-3.1-8B-Instruct \
  --enable-lora \
  --max-loras 32 \
  --max-lora-rank 64 \
  --lora-modules support-v1=./adapters/support-v1 \
               legal-v2=./adapters/legal-v2

Request with adapter:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
resp = client.chat.completions.create(
    model="support-v1",  # LoRA module name
    messages=[{"role": "user", "content": "Reset my password"}],
)

vLLM loads adapters on demand up to max-loras; LRU evicts cold adapters from GPU.

LoRAX and S-LoRA patterns

LoRAX (Predibase) — production server designed for heterogeneous LoRA batches, dynamic adapter loading from HuggingFace Hub or S3.

S-LoRA — research system optimizing GPU memory for thousands of adapters via unified paging and custom CUDA kernels — influences commercial implementations.

Key optimization: batch by adapter — mixing 20 different adapters in one micro-batch without specialized kernels causes sequential adapter application and kills throughput.

# Scheduler pseudocode
def schedule_batch(pending: Queue) -> list[Request]:
    by_adapter = groupby(pending, key=lambda r: r.adapter_id)
    # Pick largest group that fits batch size budget
    return max(by_adapter, key=len)

Memory budgeting

Rough sizing:

GPU memory = base_model + kv_cache_batch + sum(loaded_adapters) + overhead

Llama-3-8B fp16 base ≈ 16 GB. Each LoRA adapter (rank 64, targeted modules) ≈ 50–200 MB depending on layers targeted.

With 24 GB GPU:

Monitor adapter_load_latency and adapter_cache_hit_rate.

Adapter registry and versioning

adapters:
  - id: tenant_442_support
    base_model: meta-llama/Llama-3.1-8B-Instruct@rev_abc123
    path: s3://models/lora/tenant_442/v3.safetensors
    rank: 32
    target_modules: [q_proj, v_proj]
    created: 2025-03-01

Reject requests where adapter base revision mismatches server — silent wrong answers beat loud errors, but both are bad; validate at load time.

Training-to-serving pipeline

  1. Fine-tune with PEFT, save adapter only
  2. Validate on holdout set in eval harness
  3. Upload to registry, sign artifact
  4. Rolling deploy — register adapter without restarting base if engine supports hot load
  5. Canary traffic 5% → 100%

Keep adapter training rank consistent with serving max-lora-rank config.

Multi-tenant isolation

Adapters provide logical separation, not cryptographic isolation — shared base model and shared process mean side-channel and timing attacks are out of scope for most threats, but prompt data from tenant A must never log into tenant B's analytics.

Separate API keys mapping to adapter IDs. Rate limit per tenant. Consider dedicated GPU pools for regulated tenants despite shared-base efficiency.

When merging beats multi-LoRA

Merge adapters offline when:

Serve merged model as standard deployment; keep multi-LoRA for long tail of tenants.

Benchmark adapter swap latency — loading adapters per request adds 100-500ms unless pre-warmed in GPU memory.

LoRA adapter hot-swapping

Pre-load top-N adapters in GPU memory by traffic forecast. Cold adapter load adds 200–500ms — unacceptable for interactive chat. Monitor adapter hit rate; demote cold adapters to CPU preload queue.

Common production mistakes

Teams get serving multi lora adapters wrong in predictable ways:

LLM features around serving multi lora adapters break in production when prompts assume deterministic output, context windows are sized for dev datasets, or token costs are never budgeted per user session. Always log prompt hash, model version, and latency—not raw prompts with PII.

Debugging and triage workflow

When serving multi lora adapters misbehaves in production, work top-down instead of guessing:

  1. Confirm scope — one tenant, region, or deployment stage? Narrow blast radius before deep diving.
  2. Check recent changes — deploys, flag flips, config pushes, and schema migrations in the last 24 hours.
  3. Compare golden signals — latency, error rate, saturation, and traffic for the affected surface vs. baseline.
  4. Reproduce minimally — smallest input or scenario that triggers the failure; capture traces/logs with correlation IDs.
  5. Fix forward or rollback — if rollback is faster than root-cause during incident, rollback first, postmortem second.
  6. Add a guard — alert, integration test, or circuit breaker so the same class of failure is caught earlier next time.

Document the timeline during triage. Future you (and on-call) will need timestamps, not just conclusions.

Resources

Frequently asked questions

Can I serve hundreds of LoRA adapters on one GPU?

Yes, with specialized serving systems like S-LoRA, LoRAX, and vLLM multi-LoRA support. LoRA adapters are small (often 10–100 MB) compared to base models (several GB), so many fit in GPU memory simultaneously. Throughput depends on batch composition — requests using different adapters in one batch require multi-adapter kernels or grouping by adapter.

How does the server know which LoRA adapter to use per request?

Pass an adapter ID in the request metadata — HTTP header, API field, or model name suffix (e.g., llama-3-8b:support-ticket-v2). The router loads or selects the adapter weights and applies them to the base model forward pass for that request. Register adapters in a catalog with version, tenant, and compatibility constraints matching the base model revision.

What is the difference between merging LoRA weights and runtime adapter switching?

Merging bakes adapter weights into the base model offline — faster inference, one model per adapter, no runtime switching. Runtime switching loads low-rank matrices separately and composes during forward pass — one base serves many adapters, essential for multi-tenant SaaS. Merge for single-tenant high-throughput; runtime switch for many tenants with sparse traffic each.

Hiring a senior Android / Flutter engineer?

I architect and ship production mobile software — Kotlin, Jetpack Compose, Flutter — for robotics, EV infrastructure, fintech, and real-time systems. Open to remote roles in Europe and the US.

Get in touch →