Chapter 07

多 LoRA 热插拔:一模型服多租户

给 100 个客户各自微调了 100 个 LoRA,难道要开 100 个服务?vLLM 让你一个 base 模型同时挂几十个 LoRA,按请求动态路由,显存只占 base 的一份。

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 原生集成。关键三件事:

  1. Unified Memory Pool:所有 adapter 统一放在一个显存池里,按需加载 / 换入换出
  2. Heterogeneous Batching:同一 batch 里不同请求走不同 adapter,kernel 一次 forward 分别计算旁路
  3. 动态路由:每个请求的 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 怎么配

--max-loras
同时驻 GPU 的 adapter 数。决定 batch 里并发可使用的 LoRA 种类数。默认 1,生产建议 4-16。
--max-cpu-loras
CPU RAM 缓存的 adapter 数(不在 GPU 但预加载)。请求到达时若目标 adapter 不在 GPU,从 CPU 换入比磁盘快几十倍。
--max-lora-rank
允许的最大 rank。统一到 64 最安全(大部分场景 r=8-32,极少超 64)。rank 越大显存占用越多。
换入换出策略
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/sTTFT p95TPOT p95
32 独立服务(各 1×A100)32 × 16GB = 512GB32 × 1800 = 57600100ms8ms
vLLM Multi-LoRA max-loras=816GB + 32×40MB ≈ 17.3GB3100180ms11ms
vLLM Multi-LoRA max-loras=1617.6GB3800140ms10ms
vLLM Multi-LoRA max-loras=3217.9GB4200120ms9ms

单机吞吐确实比 32 台的总和小,但成本只有 1/32。这对"长尾客户 QPS 不高但数量多"的 SaaS 是完美权衡——绝大多数客户 QPS < 1,32 台独立机器纯属浪费。

性能注意事项

什么时候不该用 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³同上

本章小结