跳到主要内容
推理优化

8.3 Multi-LoRA 服务:一份底座权重,多业务共享

LoRA 推理原理、vLLM 动态 Adapter 加载、max_loras/max_lora_rank 配置、调度与显存影响、单卡多租户的实践

Multi-LoRAAdapter多租户显存动态加载

一个底座模型(如 Llama-3.1-8B)+ 多个业务微调(客服、代码、翻译各一个 LoRA)——这是生产里最常见的省钱姿势:一份权重服务所有业务,每个业务只多带几 MB 到几百 MB 的 LoRA Adapter。8.3 讲 vLLM 怎么在同一引擎里动态加载/切换多个 LoRA,以及它背后的显存账和调度逻辑。

📑 目录


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 层的前向计算:

hout=Wx+αr(A×B)xLoRA 增量h_{\text{out}} = W x + \underbrace{\frac{\alpha}{r} (A \times B) x}_{\text{LoRA 增量}}
  • W:冻结的底座权重(所有业务共享)
  • A, B:低秩矩阵(r × dd × 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-lorasGPU 上最多同时驻留的 Adapter 数默认 1(建议按并发租户数调)
--max-lora-rank允许的最大 LoRA 秩默认 16(大模型微调常用 64-128,需调大)
--max-cpu-lorasCPU 上暂存、按需换入 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?

📚 参考资料