LoRA 服务:多适配器动态加载
谁该读这一篇? 业务有多个 fine-tuned adapter 要在线服务的 ML 工程师。 前置阅读:
04-optimizations/03-quantization.md§2-3。 耗时: 30 分钟 学完能: 1. 解释 LoRA 在 attention / MLP 层的注入位置; 2. 让 SGLang 加载多个 adapter 并按请求选; 3. 用 Punica / S-LoRA 思路把多 adapter 同 batch 跑; 4. 评估 LoRA 服务对 throughput 的影响; 5. 选 LoRA vs Full Fine-tune vs Prompt Engineering 的策略。
1. LoRA 复习(30 秒)
原始 Linear: y = W @ x
LoRA: y = W @ x + (B @ A) @ x # B [d, r] · A [r, d]
其中 r << d,通常 8 / 16 / 32
- 训练时:只更新 A, B。
- 推理时:两种选择
- Merge:加到 W 里,无额外算力。
- Adapter:保持 A, B 独立,每步加一次。
服务多 adapter 时 merge 不可行(要换 adapter 就换 W),用 adapter 路径。
2. Punica / S-LoRA 思路
朴素 adapter 路径:每请求各自跑一份 A·B·x:
batch 8 个请求各用不同 adapter:
请求 i: y_i = W @ x_i + B_i @ (A_i @ x_i)
每个 adapter 一个独立 matmul → 不能 batch 一起算。
Punica / S-LoRA 解法:
- 把所有 adapter 的 A, B 堆成大张量
[N_adapters, r, d]。 - 用 indirection(每请求一个 adapter id)gather 出本请求的 A, B。
- 写专门的 batched LoRA kernel:一次算 batch 内所有请求的 adapter 贡献。
效果:
- 单 adapter 时性能接近无 LoRA。
- 多 adapter 时只损失 5-15%。
源码:srt/lora/。
3. SGLang 启用 LoRA
python -m sglang.launch_server \
--model-path meta-llama/Llama-3.1-70B-Instruct \
--lora-paths adapter1=/path/a1 adapter2=/path/a2 \
--max-loras-per-batch 4 \
--tp 4
参数:
--lora-paths:加载的 adapter 列表(name=path)。--max-loras-per-batch:一个 batch 最多多少不同 adapter。--max-lora-rank:最大 rank(适配最大那个)。
客户端指定:
client.chat.completions.create(
model="...",
messages=[...],
extra_body={"lora_path": "adapter1"} # 或者用 model 字段
)
4. 动态加载
不想启动时就装所有 adapter?runtime 动态:
curl -X POST localhost:30000/load_lora_adapter \
-d '{"lora_name": "adapter3", "lora_path": "/path/a3"}'
curl -X POST localhost:30000/unload_lora_adapter \
-d '{"lora_name": "adapter3"}'
加载需要把 A, B 拷到 GPU;几秒钟。 卸载即时。
5. 性能影响
| 场景 | Throughput 相对基线 |
|---|---|
| 无 LoRA | 100% |
| 单 adapter | 95-98% |
| 4 adapter 同 batch | 90-95% |
| 16 adapter 同 batch | 80-90% |
| 100 adapter(adapter cache miss 多) | 50-70% |
max_loras_per_batch 是关键瓶颈。
6. CUDA Graph 与 LoRA
LoRA adapter id 在请求间不同 → batch 形状变化 → CUDA Graph fallback
实测 LoRA 服务 CUDA Graph 利用率显著下降,TPOT 涨。
对策:
- 限制 adapter 数(hot adapter < 8)。
- 业务侧把同 adapter 请求路由到同副本(router stick by adapter)。
- 接受 TPOT 略涨,换 adapter 灵活性。
7. 显存
每个 adapter 占:
2 × num_layers × r × hidden_dim × dtype_bytes
Llama-3-70B, r=16, fp16:
- 80 × 16 × 8192 × 2 = ~20 MB
- 100 adapter = ~2 GB
不大,主要是 GPU 上 adapter cache 容量。
8. LoRA 与受约束输出 / 多模态
兼容。 LoRA 只影响 LM weight,不影响:
- vocab mask(受约束)
- ViT encoder(多模态)
- KV cache(除非 LoRA 改了 attention 参数)
但 不同 adapter 不能共享 KV cache——RadixCache 要用 extra_key=lora_id 隔离。
9. 业务建议
| 场景 | 推荐 |
|---|---|
| 多业务领域(金融 / 医疗 / 法律) | 各自一个 adapter |
| 多语言(中 / 英 / 日) | 各一个 adapter,按用户语言路由 |
| 多客户(SaaS) | 每客户一个 adapter(隔离 + 个性化) |
| 模型轻度优化 | 单 adapter 全局应用 |
| 大幅风格改变 | Full fine-tune(LoRA 容量不够) |
| Prompt 能解决 | 不用 LoRA |
10. 失效模式
| 现象 | 排查 |
|---|---|
| adapter 加载失败 | path 错 / rank 超 max |
| adapter 切换慢 | adapter cache 满,evict 频繁 |
| 同 adapter 不同副本输出不同 | 副本加载的 adapter 版本不一致 |
| LoRA 输出比 base 还差 | adapter 训练问题;先 base 验证 |
| 显存涨 | adapter cache 容量调小 |
11. 监控
sglang:lora_active_count # 当前加载 adapter 数
sglang:lora_cache_hit_rate
sglang:lora_swap_count # adapter swap 频率
sglang:lora_per_request_overhead_ms # LoRA 给请求加的额外延迟
健康:
- swap_count < 1 / 分钟(adapter 稳定)。
- per_request_overhead < 2 ms。
12. 与 Full Fine-tune 的对比
| 维度 | LoRA | Full Fine-tune |
|---|---|---|
| 训练显存 | < 10% | 100% |
| 训练时间 | 1× | 5-10× |
| 推理性能 | 略低 | 与 base 一样 |
| 多 adapter 服务 | ✓ | × |
| 容量上限 | r 决定 | 全模型 |
| 部署灵活 | ✓ 动态加载 | 切模型要重启 |
业务变更频繁、多版本并存 → LoRA。 单一模型、追求极致性能 → Full fine-tune(merge 后即原速)。
13. 小结
- LoRA 用 A·B 低秩矩阵增量;推理时 adapter 路径。
- SGLang 用 Punica / S-LoRA 思路做 batched multi-LoRA。
max_loras_per_batch是关键参数。- LoRA 触发 CUDA Graph fallback,性能损 5-15%。
- 多业务 / 多语言 / 多租户场景值得开。
14. 自检
- Punica / S-LoRA 思路如何让多 adapter 同 batch 跑?
答案
朴素:每请求独立算 `B_i @ A_i @ x_i` matmul,N 个 adapter = N 次独立 matmul,不能合 batch。 Punica / S-LoRA: (1) 所有 adapter 的 A 矩阵堆成 `A_all: [N_adapters, r, d]`,B 同理。 (2) 给每请求一个 `adapter_id: int`,作为 indirection。 (3) 写**带 indirection 的 batched LoRA kernel**:单次 GPU kernel 内部 gather `A_all[adapter_id]` + `B_all[adapter_id]`,一次性算完 batch 内所有请求的 LoRA 贡献。 (4) Base 权重 W 的 matmul 仍是标准 batched GEMM。 效果:N 个不同 adapter 同 batch 算 LoRA 路径只比单 adapter 慢 5-15%(gather 开销 + GPU 计算非满载),远好于 N 次独立 matmul。 代价:实现复杂,需要专门 CUDA kernel。- LoRA 服务为什么让 CUDA Graph 利用率下降?
答案
CUDA Graph 录制时把"哪些 adapter 在 batch 里、它们的 indirection 模式"也烙进去。 每次 batch 内 adapter 组合变化都触发 graph fallback: (a) **请求间 adapter 切换**:batch=[req_A_with_adapter_1, req_B_with_adapter_2, ...],组合可能性爆炸(N adapter 选 K = C(N,K))。 (b) **adapter 加载 / 卸载**:每次 dynamic load 都打破 graph 元数据。 (c) **adapter rank 不同**:不同 adapter rank 不一致时 indirection shape 变。 对策: - **限制并发 adapter 数**:`--max-loras-per-batch 4` 让 graph 可枚举的组合可控。 - **Router stick by adapter**:同 adapter 请求路由同副本,让一个副本的 batch 主要是一种 adapter。 - 接受 TPOT 涨 10-20% 是用 LoRA 的固定成本。- 不同 adapter 能不能共享 RadixCache?为什么?
答案
**不能**。adapter 改变 LM 权重 → 同 token 序列的 attention output 不同 → KV 数值不同。 如果共享:adapter A 算的 KV 被 adapter B 复用 → adapter B 的输出错误,模型胡说。 实现:RadixCache 用 `RadixKey.extra_key = lora_id` 强制不同 adapter 走独立子树([`radix_cache.py:66`](../sglang/python/sglang/srt/mem_cache/radix_cache.py))。 代价:N 个 adapter 各自独立 cache,命中率从 60% × 1 → 60% × 1/N(每 adapter 流量摊薄)。 对策:(a) 业务用 few-shot prompt 而不是 LoRA 实现差异化;(b) LoRA 部署只对真需要差异化推理逻辑的业务;(c) router sticky 把同 adapter 流量集中,让单 adapter cache 沉淀。- 100 个 adapter 服务的瓶颈在哪?
答案
多瓶颈叠加: (1) **adapter cache 容量**:默认 GPU 上只装最热 N 个 adapter(如 8);100 个轮换 → `lora_swap_count` 高。 (2) **CUDA Graph fallback**:100 个 adapter 排列组合无法枚举,几乎所有 decode batch fallback。 (3) **RadixCache 命中率**:100 个独立 cache 子树,每个流量 = 总流量 / 100,sustainability 差。 (4) **HBM 带宽**:100 个 adapter 的 A/B 矩阵交替访问,L2 cache 抖动。 解决: - 业务侧合并:100 个 → 5-10 个核心 adapter + prompt 模板差异化处理长尾。 - Router 严格 sticky by adapter_id,让单副本只装少数 adapter。 - 接受 100 adapter 场景吞吐降 30-50%;预算上算账。- 多语言场景 LoRA vs prompt engineering 怎么选?
答案
**优先 prompt engineering**,迫不得已才 LoRA。 Prompt engineering:在 system 里写 "Answer in Chinese." 或 few-shot 中文例子,base 模型自己适应。优点:零部署成本、零运维负担、即时生效;缺点:弱模型语言切换效果差、长 prompt 占 token。 LoRA:针对每语言训一个 adapter。优点:base 模型小、单语言效果显著提升;缺点:需要训练数据 / 算力、运维 N adapter、CUDA Graph fallback。 规则: - **强模型(Qwen3 / Llama-3.3 / GPT-4 级)+ 主流语言**:纯 prompt engineering。 - **小模型 + 小语种 / 特定领域翻译**:LoRA 价值大。 - **业务要求严格风格 / 术语**:LoRA。 - **流量 < 10 QPS / 语言**:LoRA 性价比差,prompt engineering。 实战:90% 的多语言场景 prompt engineering 足够;LoRA 是兜底。15. 下一步
05-embedding.md— Embedding 服务。08-production-deployment/08-multi-tenant.md— 多租户 + LoRA。- 源码:
srt/lora/。