LoRA 速览:为什么能共享 base
LoRA(Low-Rank Adaptation)不改 base 权重,只在每个线性层旁边加一对低秩矩阵 A(d×r)、B(r×d),r 通常 8-64。推理时 W'x = Wx + BAx,base 不动,只加了一路极小的旁路。
Llama-3-8B base: 16 GB (bf16) 一个 LoRA adapter (r=16): ~40 MB 挂 50 个 LoRA: 16 GB + 50 × 40MB = 18 GB 独立部署 50 个模型: 16 × 50 = 800 GB
共享 base、各自路由——这就是 SaaS 场景最省钱的结构。每个客户一把专属 adapter,所有客户共用同一个 70B base。
vLLM 的 Multi-LoRA 怎么实现
论文出自 S-LoRA(Sheng et al., 2023),vLLM 原生集成。关键三件事:
- Unified Memory Pool:所有 adapter 统一放在一个显存池里,按需加载 / 换入换出
- Heterogeneous Batching:同一 batch 里不同请求走不同 adapter,kernel 一次 forward 分别计算旁路
- 动态路由:每个请求的
model字段决定走哪个 adapter,base 通路永远共享
启动 Multi-LoRA 服务
vllm serve meta-llama/Llama-3-8B-Instruct \ --enable-lora \ --max-loras 8 \ # 同时驻留在 GPU 的 adapter 数 --max-lora-rank 64 \ # 最大 rank,卡上限 --max-cpu-loras 32 \ # CPU 缓存更多,按需换入 --lora-modules \ legal-bot=/models/loras/legal-lora \ code-bot=/models/loras/code-lora \ medical-bot=/models/loras/med-lora
启动后 /v1/models 就能看到 4 个模型名:base 自身 + 3 个 LoRA 别名。
按请求切换 LoRA
from openai import OpenAI client = OpenAI(base_url="http://localhost:8000/v1", api_key="sk-xxx") # 请求走法律 LoRA r1 = client.chat.completions.create( model="legal-bot", messages=[{"role": "user", "content": "帮我审一份租赁合同"}], ) # 同一瞬间,另一个用户走代码 LoRA r2 = client.chat.completions.create( model="code-bot", messages=[{"role": "user", "content": "写个 BST 删除节点"}], ) # 不指定 LoRA → 纯 base r3 = client.chat.completions.create( model="meta-llama/Llama-3-8B-Instruct", messages=[{"role": "user", "content": "你好"}], )
同一个 batch 里三路并行处理,GPU 不空转,连续批处理照样生效。
动态上下线(不停服加 LoRA)
启动时没挂的 adapter,运行中也能加。vLLM 提供管理端点:
# 加载新 adapter curl -X POST http://localhost:8000/v1/load_lora_adapter \ -H "Content-Type: application/json" \ -d '{"lora_name": "new-customer-42", "lora_path": "/models/loras/cust42"}' # 卸载 curl -X POST http://localhost:8000/v1/unload_lora_adapter \ -H "Content-Type: application/json" \ -d '{"lora_name": "new-customer-42"}'
启动服务时需要加 --enable-lora-adapter-dynamic-loading(0.6+)。这对 SaaS 平台至关重要:新客户下单、付费、自动训完 LoRA → 调 /load_lora_adapter 立刻可用,不用重启服务。
max-loras vs max-cpu-loras 怎么配
vLLM 用 LRU:GPU 池满了、新请求要用的 adapter 不在 GPU,就把最久没用的那个换出到 CPU。换一次 40MB adapter 的代价约 5-10ms(PCIe 4.0)——对 TTFT 影响很小,但高并发 LoRA 切换密集时要加大
--max-loras。
训练出 vLLM 兼容的 LoRA
vLLM 直接吃 PEFT / HuggingFace 标准格式,训练工具链很多:
from peft import LoraConfig, get_peft_model from transformers import AutoModelForCausalLM, Trainer, TrainingArguments base = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3-8B-Instruct") cfg = LoraConfig( r=16, lora_alpha=32, target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], lora_dropout=0.05, task_type="CAUSAL_LM", ) model = get_peft_model(base, cfg) # ... 正常 Trainer 训练 ... # 保存 —— 产物就能给 vLLM 用 model.save_pretrained("/models/loras/legal-lora")
目录里会有 adapter_config.json + adapter_model.safetensors,这就是 vLLM --lora-modules 要的路径。unsloth、axolotl、llama-factory 训出来的 adapter 也都是这个格式。
SaaS 架构范式
┌──────────────────────────────────────────┐
│ 客户侧:每家公司上传自己的训练数据 │
└───────────────┬──────────────────────────┘
│
▼
┌─────────────────────┐
│ 训练调度(Ray/K8s) │ 每家训 1 个 LoRA
└──────────┬──────────┘ ~30分钟~2小时
│
▼
┌─────────────────────┐
│ S3 / OSS:adapter 仓库 │
└──────────┬──────────┘
│
▼ /load_lora_adapter
┌─────────────────────┐
│ vLLM Server │ 1× Llama-3-70B base
│ + N LoRA adapters │ + 动态加载
└──────────┬──────────┘
│
┌──────────┴───────────┐
▼ ▼ ▼
客户A 客户B 客户C
(legal) (code) (medical)
这种架构以前需要 N 台独立机器,现在 1 台 H100 服 50+ 客户,单客户边际成本接近零(只多了一个 40MB 的 adapter)。
实测:并发 32 个 LoRA
Llama-3-8B base + 32 个客户 LoRA(r=16),A100-80G 上 QPS=40 压测:
| 配置 | 显存占用 | 吞吐 tok/s | TTFT p95 | TPOT p95 |
|---|---|---|---|---|
| 32 独立服务(各 1×A100) | 32 × 16GB = 512GB | 32 × 1800 = 57600 | 100ms | 8ms |
| vLLM Multi-LoRA max-loras=8 | 16GB + 32×40MB ≈ 17.3GB | 3100 | 180ms | 11ms |
| vLLM Multi-LoRA max-loras=16 | 17.6GB | 3800 | 140ms | 10ms |
| vLLM Multi-LoRA max-loras=32 | 17.9GB | 4200 | 120ms | 9ms |
单机吞吐确实比 32 台的总和小,但成本只有 1/32。这对"长尾客户 QPS 不高但数量多"的 SaaS 是完美权衡——绝大多数客户 QPS < 1,32 台独立机器纯属浪费。
性能注意事项
- Punica kernel:vLLM 0.6+ 默认用 Punica(S-LoRA 作者开源),batched LoRA 计算开销从 ~25% 降到 ~5%
- target_modules 统一:batch 里不同 adapter 若 target 的层不一样,会导致部分序列走 skip path,调度复杂。团队内训 LoRA 时建议统一 target
- rank 差异大:batch 里有 r=8 也有 r=64,会按最大 rank 对齐算,小 rank 的 adapter 浪费一点计算。生产最好统一 rank
- base 量化后加 LoRA:base 是 AWQ-INT4,adapter 还是 bf16,vLLM 支持但要用
--fully-sharded-loras,否则某些层 fallback 到 FP16 会慢
什么时候不该用 Multi-LoRA
① 客户 QPS 极高(单客户 > 50 QPS):和其他 adapter 抢 batch 反而拖累
② 客户对 latency 要求极苛刻(p99 < 50ms):纯 base 服务更稳
③ 客户数据完全不能共用一台 GPU(合规要求):物理隔离只能多机
④ 用的是 full fine-tune 而不是 LoRA:没法共享 base,只能独立部署
LoRA 之外的选择:Adapter / Prompt Tuning
vLLM 目前原生支持的只有 LoRA。其他参数高效微调方案:
| 方案 | vLLM 支持 | 说明 |
|---|---|---|
| LoRA | ✅ | 事实标准,生态最全 |
| QLoRA | ✅(训练用 4bit,导出时合并回 LoRA) | 导出的 adapter 和普通 LoRA 一样 |
| DoRA | ✅ (0.6.3+) | 方向分解 LoRA,精度稍好 |
| Prefix Tuning / P-Tuning v2 | ❌ | 暂未支持,需改 kernel |
| IA³ | ❌ | 同上 |
本章小结
- Multi-LoRA 让一个 base 同时服 N 个客户,显存增量可忽略,单客户边际成本接近零
- 启动加
--enable-lora --max-loras --lora-modules,按请求model字段路由 - 运行时用
/v1/load_lora_adapter动态上下线,SaaS 新客户秒上线 - 训练产物符合 PEFT 标准即可,target_modules / rank 建议团队内统一
- Punica kernel 把 multi-LoRA 额外开销压到 ~5%,32 并发 adapter 吞吐只比纯 base 慢 10-20%
- 长尾 SaaS 场景省 90%+ 硬件成本;单客户高 QPS 时才考虑独立服务