Serving adapters
An adapter is a deployment artefact: base digest, tokenizer, template and config. Merge it or swap it, route it from identity, and version every cache key.
The adapter is a deployment artefact
An adapter file is meaningless without the base model, the tokenizer and the chat template it was trained against. Serving it is therefore a four-part deployment, and the registry entry should name all four plus the training config and the evaluation that approved it. An adapter id alone is a filename, not a version.
Pin the base by digest, not by a tag that can move. A provider that updates the model behind the same name will change the behaviour of your adapter without a deploy, and the trace will still say the adapter version you tested. The base is part of the artefact even though you did not train it.
Keep the registry append-only. A version that was served stays in the record even after it is retired, because a trace from last month points at it and an incident review needs to load it. Deleting a version is deleting evidence.
Merge or swap
Merging adds the low-rank update into the base weights and produces an ordinary model file. It removes the adapter’s extra matrix multiplication and the per-request lookup, at the cost of one full model copy per variant. That is the right trade when a single behaviour serves all traffic.
Swapping keeps one base in memory and loads adapters beside it. Serving frameworks batch requests from different adapters against the same base, which is how one GPU serves many tenants. The cost moves to adapter memory, load time and a cache of recently used adapters.
The memory arithmetic decides the design. Each adapter is roughly rank times the sum of the adapted dimensions times two bytes, per module, and the KV cache needs its own headroom. A GPU that fits the base and four adapters may not fit the base, eight adapters and a long-context batch.
One base, many adapters
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct", enable_lora=True, max_loras=4)
request = LoRARequest("support-v3", 1, "adapters/support-v3")
llm.generate(["Where is my refund?"], SamplingParams(max_tokens=128), lora_request=request)What actually limits adapter count
Print the size of each adapter and the binding limit. Then add a rank-128 adapter and see whether memory or the framework slot count binds first.
Routing and isolation
A multi-tenant service chooses an adapter from the authenticated tenant, never from a request field. The tenant comes from the session or the API key, the registry maps it to a version, and the request is rejected if the mapping is missing. A client-supplied adapter name is a request to run someone else’s weights, and the registry is the authorisation table for weights.
Isolation is a correctness property, not a performance one. If adapter state can leak between batched requests, one tenant’s tone appears in another tenant’s answer. Test it with a canary prompt whose output differs by adapter, and assert the cross-tenant pair never matches. The test belongs in the serving layer, not in the fine-tuning notebook.
Always have a fallback. If an adapter fails to load, the service can answer from the base model with a recorded degradation, or refuse; either is better than a 500. The fallback is a product decision, so name it in the registry entry rather than discovering it during a deploy.
A mismatched chat template does not throw. It produces a plausible answer with the wrong special tokens, and the evaluation you ran offline no longer describes what is served. Version the template with the adapter and compare one rendered prompt at startup.
Rollout, rollback and cache
Roll out an adapter like any model change: a canary slice, a fixed window, the task suite and the general suite, and a gate that can say no. The canary should be large enough to see a rare failure and small enough to contain it. Ten percent for an hour is a start, not a law.
Put the adapter version in every trace, next to the base model digest and the template version. When the feedback turns, the join tells you which of the three changed. Without it, the review compares a prompt change with a weights change and learns nothing.
Invalidate the caches on deploy. A response cache keyed by prompt but not by adapter version will serve the old behaviour after the rollback, which looks like the rollback failed. The same applies to embeddings and to any evaluation cache used by the gate. The cache key is part of the version.
Route by identity, not by request
Run the requests and check that each tenant reaches its own adapter or a named fallback. Then raise the acme canary to half and confirm the split follows the roll value.
Check what you learned
One question at a time. A wrong answer explains itself — take another go.
Part 1 of 5
What must be stored with an adapter to serve it reproducibly?
