8.3 Multi-LoRA 服务:一份底座权重,多业务共享
LoRA 推理原理、vLLM 动态 Adapter 加载、max_loras/max_lora_rank 配置、调度与显存影响、单卡多租户的实践
一个底座模型(如 Llama-3.1-8B)+ 多个业务微调(客服、代码、翻译各一个 LoRA)——这是生产里最常见的省钱姿势:一份权重服务所有业务,每个业务只多带几 MB 到几百 MB 的 LoRA Adapter。8.3 讲 vLLM 怎么在同一引擎里动态加载/切换多个 LoRA,以及它背后的显存账和调度逻辑。
📑 目录
- 1. 为什么 Multi-LoRA 能省钱:Adapter 的尺寸账
- 2. LoRA 推理:base + 低秩增量
- 3. vLLM 的 Multi-LoRA:动态加载与切换
- 4. 关键参数:max_loras / max_lora_rank / max_cpu_loras
- 5. 调度与显存:多 Adapter 共存的开销
- 6. 单卡多租户实践
- 7. 局限与选型
- 📝 总结
- 🎯 自我检验清单
- 📚 参考资料
1. 为什么 Multi-LoRA 能省钱:Adapter 的尺寸账
全量微调(SFT):每个业务一份完整权重。Llama-3.1-70B 全量 ≈ 140GB——10 个业务 = 1.4TB 权重,且每份都要独立部署、独立显存。
LoRA 微调:冻结底座权重,只训练低秩增量(A×B 矩阵,秩 r 通常 8-128):
| 项 | 大小(70B 模型) |
|---|---|
| 底座权重(FP16/BF16) | ~140 GB |
| 一个 LoRA Adapter(r=64,全模块) | ~1-2 GB |
| 10 个业务总增量 | ~10-20 GB(共享同一份 140GB 底座) |
📌 关键点:Multi-LoRA 的本质是把”每个业务一份模型”变成”一份底座 + 每业务一个轻量补丁”。显存大头(底座)只付一次,业务间的差异只有 Adapter 的大小——这才是”单卡服务多租户”的经济学基础。
2. LoRA 推理:base + 低秩增量
LoRA 层的前向计算:
W:冻结的底座权重(所有业务共享)A, B:低秩矩阵(r × d与d × r,业务专属)α/r:缩放系数
推理时每步多算一次低秩矩阵乘(两个小矩阵的乘加),计算量占比极小——这就是为什么多 LoRA 对推理速度的影响主要是显存与调度,而不是算力。
💡 提示:LoRA 合并(merge)是另一个常用形态:把
W + (α/r)AB预先合并成普通权重,推理时零额外开销——但合并后就不能动态切换了。Multi-LoRA 服务的精髓恰恰是”不合并、动态换”。
3. vLLM 的 Multi-LoRA:动态加载与切换
3.1 启动
vllm serve meta-llama/Llama-3.2-3B-Instruct \
--enable-lora \
--lora-modules sql-lora=jeeejeee/llama32-3b-text2sql-spider \
--lora-modules chat-lora=my-org/chat-adapter # 可多个
--enable-lora:开启 LoRA 支持--lora-modules {name}={path}:预注册的 LoRA(可重复传多个;v0.26 也支持 JSON 格式带base_model_name)
3.2 动态加载
vLLM 不只支持启动时注册——运行时可以随时加载/卸载 Adapter(v0.26 支持动态加载新 LoRA):
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="dummy")
# 运行时加载
client.models.create( # 或对应管理 API
id="translation-lora",
path="my-org/translation-adapter",
base_model="meta-llama/Llama-3.2-3B-Instruct",
)
3.3 请求时选择
resp = client.chat.completions.create(
model="sql-lora", # ← 直接按 LoRA 名称选模型!
messages=[{"role": "user", "content": "SELECT 用户的订单总数"}],
)
📌 关键点:请求侧零改动——客户端把 LoRA 名当模型 ID 用(
model="sql-lora"),vLLM 内部完成”查 Adapter → 应用增量 → 生成”。这层抽象让业务方完全无感知:他们只是”用了另一个模型”。
4. 关键参数:max_loras / max_lora_rank / max_cpu_loras
| 参数 | 作用 | 默认/建议 |
|---|---|---|
--max-loras | GPU 上最多同时驻留的 Adapter 数 | 默认 1(建议按并发租户数调) |
--max-lora-rank | 允许的最大 LoRA 秩 | 默认 16(大模型微调常用 64-128,需调大) |
--max-cpu-loras | CPU 上暂存、按需换入 GPU 的 Adapter 数 | 默认 0(多租户场景建议 > max-loras) |
--lora-target-modules | 允许加载 LoRA 的目标模块白名单 | 默认全模块;限制可省显存 |
理解 max-loras 与 max-cpu-loras 的分工:
GPU(max-loras=4) CPU/磁盘(max-cpu-loras=16)
┌─────────────────┐ ┌──────────────────────────┐
│ 驻留的 4 个 Adapter │ ←──换入/出──│ 全部 20 个 Adapter 的缓存 │
│ (直接参与计算) │ │ (按需换入 GPU) │
└─────────────────┘ └──────────────────────────┘
- GPU 驻留的 Adapter 直接参与计算(换入/换出有开销)
- 超出 GPU 容量的 Adapter 放 CPU 缓存,请求命中时换入——换入换出是调度开销,命中率决定性能
💡 提示:
--max-lora-rank是隐蔽的坑——模型微调时用的秩(如 64)> vLLM 默认(16)时,加载会直接失败。启动前先确认微调配置的 rank,或统一微调规范(rank 固定、模块固定),服务端参数就好配了。
5. 调度与显存:多 Adapter 共存的开销
5.1 显存开销
总显存 = 底座权重 + KV Cache + Σ(GPU 驻留 Adapter) + 调度余量
- 每个驻留 Adapter:
2 × r × d大小的 A/B 矩阵(全模块)——70B 模型 r=64 约 1-2GB - KV Cache 不受影响(Adapter 不改注意力计算)
- max-loras 直接决定 Adapter 显存上限
5.2 计算调度
一个 batch 里的请求可能命中不同的 Adapter——vLLM 的 kernel 调度要处理”同 batch 混合 Adapter”:
- 按 Adapter 分组计算增量(同一 Adapter 的请求合并算 LoRA 分支)
- 或先算 base 分支(共享)、再逐 Adapter 算增量(分组)
5.3 换入换出(swap)的开销
CPU ↔ GPU 的 Adapter 换入不是免费的:
- 换入一个 2GB Adapter ≈ 数十到数百 ms(PCIe 带宽 32GB/s 量级)
- 请求排队 + 换入延迟:冷 Adapter 的首个请求明显变慢
- 优化:租户流量预测(峰谷错开)、热 Adapter 常驻(max-loras 给足)
📌 关键点:Multi-LoRA 的性能杀手不是计算,而是Adapter 的换入换出——本质是”缓存命中率”问题。经验法则:
max_loras≥ 活跃租户数(热数据全驻留 GPU),max_cpu_loras覆盖全部租户(冷数据能兜底)。这与 7.3 KV 池化的思路同构:把”数据就近”做到位,动态性才有价值。
6. 单卡多租户实践
6.1 典型架构
┌───────────────────────────────┐
租户 A ──→ │ 一个 vLLM 实例(1-8 卡) │
租户 B ──→ │ base: Llama-3.1-8B │ ← 共享
租户 C ──→ │ LoRA: A / B / C / D │ ← 按请求切换
租户 D ──→ │ 配额:每租户限流、限并发 │
└───────────────────────────────┘
- 租户间完全隔离的模型体验(各自微调、各自行为),共享底座与算力
- 配额控制(限流)防止单租户打爆共享池
- 观测:每租户(LoRA)的 token 用量、延迟分位点单独统计
6.2 配置模板
vllm serve meta-llama/Llama-3.1-8B-Instruct \
--enable-lora \
--max-loras 8 \
--max-cpu-loras 32 \
--max-lora-rank 64 \
--lora-modules tenant-a=... tenant-b=... tenant-c=... \
--max-num-seqs 256
6.3 容量规划
| 问题 | 答案 |
|---|---|
| 一台卡能服务几个租户? | 取决于 Adapter 大小与 max-loras;显存余量是硬约束 |
| 什么时候该加第二台? | 单实例利用率 > 80% 且仍有排队(DP 扩吞吐,见第 6 章) |
| 租户隔离能做到多强? | 逻辑隔离(配额/限流);物理隔离需独立实例 |
💡 提示:租户隔离有个现实边界——多租户共享 KV Cache 与显存,一个租户的突发可能影响他人尾部延迟。SLO 敏感的租户(第 7.4 章)建议物理隔离(独立实例),普通租户用逻辑隔离省钱。
7. 局限与选型
7.1 局限
- 仅 LoRA 族:QLoRA / DoRA 等变体支持有限,检查模型格式(vLLM 用 Safetensors 的 LoRA 格式)
- 动态性受限:换入换出开销、max-loras 上限——租户数远超硬件时退化
- 微调规范要统一:rank、target modules、格式不一致会增加服务端配置复杂度
- 底座更新是发布事件:底座权重升级 = 所有 Adapter 要重新验证(甚至重训)
7.2 选型决策
| 场景 | 方案 |
|---|---|
| 租户数少(<10)、权重装得下 | Multi-LoRA 单实例(省钱首选) |
| 租户多、Adapter 大、SLO 敏感 | 多实例 + 每实例少租户 |
| 业务差异大(不只是微调) | 独立模型部署(或分池) |
| 不需要动态切换 | 合并 LoRA 为普通权重(零开销、零灵活) |
📌 关键点:Multi-LoRA 的适用边界 = “共享底座 + 轻量差异 + 可容忍换入开销”。三个条件都满足,它是性价比之王;任何一个不满足(Adapter 巨大、切换极频繁、SLO 极严),老老实实多实例。
📝 总结
- 经济学:底座付一次、Adapter 按业务轻量叠加——单卡多租户的基础
- 推理:
Wx + (α/r)ABx,低秩增量计算量极小 - 动态:
--enable-lora+--lora-modules启动注册,运行时动态加载,请求按model=LoRA名选择 - 参数:max-loras(GPU 驻留)/ max-cpu-loras(CPU 缓存)/ max-lora-rank(与微调匹配)
- 性能关键:Adapter 换入换出的缓存命中率——热数据常驻、冷数据兜底
- 实践:逻辑隔离 + 配额;SLO 敏感租户物理隔离
- 边界:LoRA 族、统一微调规范、底座更新是发布事件
🎯 自我检验清单
- 算一笔 10 租户 × 70B 的 Multi-LoRA 显存账?
- LoRA 推理的前向公式?为什么计算开销小?
- 请求侧怎么选择 LoRA?(客户端视角)
- max_loras 与 max_cpu_loras 的分工?换入开销多大量级?
- 为什么说性能杀手是换入换出而不是计算?
- 单卡多租户的配置模板?SLO 敏感租户怎么办?
- 什么场景该放弃 Multi-LoRA?
📚 参考资料
- vLLM 官方文档:LoRA(https://docs.vllm.ai/en/latest/features/lora.html)
- Hu et al., LoRA: Low-Rank Adaptation of Large Language Models(https://arxiv.org/abs/2106.09685)
- vLLM 源码:vllm/lora/(v0.26.0)(https://github.com/vllm-project/vllm/tree/v0.26.0/vllm/lora)