vLLM 高性能推理部署指南 / 07 - LoRA 动态适配
07 - LoRA 动态适配
在不重启服务的情况下动态加载和切换 LoRA 适配器,实现一个基础模型服务多个垂直场景。
7.1 LoRA 基础概念
7.1.1 什么是 LoRA?
LoRA(Low-Rank Adaptation)是一种高效的模型微调方法。它冻结预训练模型的原始权重,仅训练少量的低秩分解矩阵,从而以极低的参数量实现模型适配。
原始权重矩阵 W (frozen): [d × d]
│
│ 原始推理: h = W · x
│
│ LoRA 推理: h = W · x + ΔW · x = W · x + B · A · x
│ ↑
│ LoRA 增量(可训练)
│
│ A: [d × r] (降维矩阵,r << d)
│ B: [r × d] (升维矩阵)
│
│ 参数量: 2 × d × r(远小于 d × d)
示例(d=4096, r=16):
原始参数: 4096 × 4096 = 16,777,216
LoRA 参数: 2 × 4096 × 16 = 131,072
压缩比: 0.78%(仅增加 0.78% 的参数)
7.1.2 LoRA 在 vLLM 中的优势
| 优势 | 说明 |
|---|---|
| 零停机切换 | 切换 LoRA 适配器无需重启服务 |
| 多 LoRA 并行 | 同一请求中不同 token 使用不同 LoRA |
| 显存高效 | 基础模型只加载一份,LoRA 适配器很小 |
| 快速适配 | 几分钟内即可训练新的 LoRA 适配器 |
| 多租户 | 不同租户使用不同的 LoRA |
7.2 使用 LoRA 适配器
7.2.1 基础用法
# lora_basic.py
"""LoRA 基础使用示例"""
from vllm import LLM, SamplingParams
# 启用 LoRA 支持
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
enable_lora=True, # 启用 LoRA
max_lora_rank=64, # 最大 LoRA rank
max_loras=4, # 最大同时加载的 LoRA 数量
max_model_len=4096,
gpu_memory_utilization=0.9,
)
# 加载并使用 LoRA 适配器
from vllm.lora.request import LoRARequest
lora_adapter = LoRARequest(
lora_name="medical-lora", # 适配器名称
lora_int_id=1, # 整数 ID
lora_path="/data/lora/medical-lora", # 适配器路径
)
sampling_params = SamplingParams(temperature=0.7, max_tokens=256)
# 使用 LoRA 生成
outputs = llm.generate(
["患者出现持续性头痛,可能是什么原因?"],
sampling_params,
lora_request=lora_adapter,
)
print(outputs[0].outputs[0].text)
7.2.2 在线服务中使用 LoRA
# 启动带 LoRA 支持的 API 服务
vllm serve Qwen/Qwen2.5-7B-Instruct \
--enable-lora \
--max-lora-rank 64 \
--max-loras 4 \
--lora-modules \
medical-lora=/data/lora/medical-lora \
legal-lora=/data/lora/legal-lora \
finance-lora=/data/lora/finance-lora
7.2.3 API 请求中指定 LoRA
# 使用指定 LoRA 发送请求
curl http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "qwen-7b",
"messages": [{"role": "user", "content": "诊断:慢性胃炎的治疗方案"}],
"max_tokens": 300,
"temperature": 0.7,
"model_extra": {
"lora_name": "medical-lora"
}
}'
# Python 客户端使用 LoRA
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")
# 指定 LoRA 适配器
response = client.chat.completions.create(
model="qwen-7b",
messages=[{"role": "user", "content": "分析这个合同条款的风险。"}],
max_tokens=300,
extra_body={"lora_name": "legal-lora"},
)
print(response.choices[0].message.content)
7.3 多 LoRA 并行
7.3.1 架构原理
多个请求同时使用不同 LoRA 适配器:
请求1(医疗)──┐
请求2(法律)──┤ ┌─────────────────────────┐
请求3(金融)──┼───→│ vLLM Engine │
请求4(无LoRA)┘ │ │
│ 基础模型权重(共享) │
│ │ │
│ ┌────▼────┐ │
│ │ LoRA A │← 请求1,请求4 │
│ │ LoRA B │← 请求2 │
│ │ LoRA C │← 请求3 │
│ └─────────┘ │
└─────────────────────────┘
一个前向推理批次中,不同 token 可能使用不同的 LoRA。
vLLM 通过 LoRA 权重矩阵的拼接和分段计算来高效处理。
7.3.2 多 LoRA 配置
# multi_lora.py
"""多 LoRA 同时加载"""
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct",
enable_lora=True,
max_lora_rank=64,
max_loras=8, # 最多同时加载 8 个 LoRA
max_model_len=4096,
)
# 定义多个 LoRA 适配器
loras = {
"medical": LoRARequest("medical", 1, "/data/lora/medical"),
"legal": LoRARequest("legal", 2, "/data/lora/legal"),
"finance": LoRARequest("finance", 3, "/data/lora/finance"),
"code": LoRARequest("code", 4, "/data/lora/code"),
}
# 不同请求使用不同的 LoRA
prompts_and_loras = [
("头痛的常见原因有哪些?", loras["medical"]),
("分析合同中的违约条款。", loras["legal"]),
("评估这个投资方案的风险。", loras["finance"]),
("写一个快速排序算法。", loras["code"]),
("你好,今天天气怎么样?", None), # 不使用 LoRA
]
sampling_params = SamplingParams(temperature=0.7, max_tokens=256)
for prompt, lora in prompts_and_loras:
output = llm.generate([prompt], sampling_params, lora_request=lora)
lora_name = lora.lora_name if lora else "base"
print(f"[{lora_name}] {output[0].outputs[0].text[:100]}...")
7.4 LoRA 热切换
7.4.1 动态加载新 LoRA
vLLM 支持在运行时动态加载新的 LoRA 适配器,无需重启服务:
# hot_swap.py
"""运行时动态加载 LoRA"""
import time
from vllm import AsyncLLMEngine, AsyncEngineArgs, SamplingParams
from vllm.lora.request import LoRARequest
async def demo_hot_swap():
# 创建引擎
engine_args = AsyncEngineArgs(
model="Qwen/Qwen2.5-7B-Instruct",
enable_lora=True,
max_lora_rank=64,
max_loras=4,
)
engine = AsyncLLMEngine.from_engine_args(engine_args)
sampling_params = SamplingParams(temperature=0.7, max_tokens=100)
# 初始 LoRA
lora_v1 = LoRARequest("model-v1", 1, "/data/lora/medical-v1")
result = await engine.generate(
"头痛的诊断流程是什么?",
sampling_params,
request_id="req-1",
lora_request=lora_v1,
)
print(f"V1: {result.outputs[0].text[:100]}")
# 热切换到新版本的 LoRA
lora_v2 = LoRARequest("model-v2", 2, "/data/lora/medical-v2")
result = await engine.generate(
"头痛的诊断流程是什么?",
sampling_params,
request_id="req-2",
lora_request=lora_v2,
)
print(f"V2: {result.outputs[0].text[:100]}")
7.4.2 LoRA 适配器管理
# lora_manager.py
"""LoRA 适配器管理"""
import os
from pathlib import Path
class LoRAManager:
"""管理 LoRA 适配器的加载和切换"""
def __init__(self, lora_base_dir: str):
self.base_dir = Path(lora_base_dir)
self.loaded_adapters: dict[str, LoRARequest] = {}
self._next_id = 1
def discover_adapters(self) -> list[str]:
"""发现可用的 LoRA 适配器"""
adapters = []
for d in self.base_dir.iterdir():
if d.is_dir() and (d / "adapter_config.json").exists():
adapters.append(d.name)
return adapters
def load_adapter(self, name: str) -> LoRARequest:
"""加载指定的适配器"""
if name in self.loaded_adapters:
return self.loaded_adapters[name]
adapter_path = self.base_dir / name
if not adapter_path.exists():
raise FileNotFoundError(f"适配器 {name} 不存在")
lora_request = LoRARequest(
lora_name=name,
lora_int_id=self._next_id,
lora_path=str(adapter_path),
)
self._next_id += 1
self.loaded_adapters[name] = lora_request
return lora_request
def get_adapter(self, name: str) -> LoRARequest:
"""获取已加载的适配器"""
if name not in self.loaded_adapters:
return self.load_adapter(name)
return self.loaded_adapters[name]
# 使用示例
manager = LoRAManager("/data/lora-adapters")
print("可用适配器:", manager.discover_adapters())
medical_lora = manager.load_adapter("medical-v3")
7.5 训练 LoRA 适配器
7.5.1 使用 PEFT 库训练
# train_lora.py
"""训练 LoRA 适配器示例"""
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments
from peft import LoraConfig, get_peft_model, TaskType
from datasets import load_dataset
from trl import SFTTrainer
# 1. 加载基础模型
model_name = "Qwen/Qwen2.5-7B-Instruct"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype="auto",
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
# 2. 配置 LoRA
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16, # LoRA rank
lora_alpha=32, # 缩放因子
lora_dropout=0.05, # Dropout
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
bias="none",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 输出: trainable params: 13,107,200 || all params: 7,614,939,136 || trainable%: 0.172
# 3. 准备数据
dataset = load_dataset("json", data_files="train_data.jsonl")
# 4. 训练
training_args = TrainingArguments(
output_dir="./lora-output",
num_train_epochs=3,
per_device_train_batch_size=4,
gradient_accumulation_steps=4,
learning_rate=2e-4,
warmup_ratio=0.1,
logging_steps=10,
save_strategy="epoch",
fp16=True,
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset["train"],
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=2048,
)
trainer.train()
# 5. 保存 LoRA 适配器
model.save_pretrained("./medical-lora-adapter")
tokenizer.save_pretrained("./medical-lora-adapter")
7.5.2 LoRA 训练数据格式
{"messages": [{"role": "system", "content": "你是一个医疗助手。"}, {"role": "user", "content": "什么是高血压?"}, {"role": "assistant", "content": "高血压是指动脉血压持续升高..."}]}
{"messages": [{"role": "system", "content": "你是一个医疗助手。"}, {"role": "user", "content": "糖尿病的早期症状有哪些?"}, {"role": "assistant", "content": "糖尿病的早期症状包括..."}]}
7.6 LoRA 与量化组合
7.6.1 AWQ + LoRA
# awq_lora.py
"""AWQ 量化 + LoRA 组合使用"""
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest
# 使用 AWQ 量化模型作为基础模型
llm = LLM(
model="Qwen/Qwen2.5-7B-Instruct-AWQ",
quantization="awq",
enable_lora=True,
max_lora_rank=64,
max_loras=4,
max_model_len=4096,
)
lora = LoRARequest("medical", 1, "/data/lora/medical")
sampling_params = SamplingParams(temperature=0.7, max_tokens=256)
# 注意:LoRA 适配器需要在原始 FP16 模型上训练
# vLLM 会自动处理精度转换
outputs = llm.generate(
["患者症状:头痛、发热、乏力。请给出诊断建议。"],
sampling_params,
lora_request=lora,
)
print(outputs[0].outputs[0].text)
7.7 性能考量
7.7.1 LoRA 对推理性能的影响
| 配置 | 吞吐量影响 | 显存影响 |
|---|---|---|
| 无 LoRA(基线) | 100% | 基线 |
| 1 个 LoRA(rank=16) | ~97% | +50MB |
| 4 个 LoRA(rank=16) | ~93% | +200MB |
| 1 个 LoRA(rank=64) | ~95% | +150MB |
| 4 个 LoRA(rank=64) | ~88% | +600MB |
7.7.2 优化建议
# 性能优化配置
llm = LLM(
model="model",
enable_lora=True,
max_lora_rank=32, # 不要设太大,16-32 通常足够
max_loras=4, # 根据实际需求设置
max_model_len=4096, # 减小可增加并发
gpu_memory_utilization=0.9,
# LoRA 相关的额外显存需要预留
)
7.8 业务场景
场景一:多租户 SaaS 服务
┌─── 租户 A: LoRA(金融领域)
基础模型 ─────┤
(LLaMA-70B) ├─── 租户 B: LoRA(医疗领域)
│
└─── 租户 C: LoRA(法律领域)
优势:
- 基础模型只加载一份(140GB)
- 每个 LoRA 仅 50-200MB
- 同一 GPU 服务所有租户
场景二:A/B 测试
# 同时运行两个版本的 LoRA,对比效果
lora_v1 = LoRARequest("model-v1", 1, "/lora/sentiment-v1")
lora_v2 = LoRARequest("model-v2", 2, "/lora/sentiment-v2")
# 50/50 分流
import random
for prompt in test_prompts:
lora = lora_v1 if random.random() < 0.5 else lora_v2
result = llm.generate([prompt], params, lora_request=lora)
场景三:多语言支持
基础模型: 英文大模型
├── LoRA: 中文增强
├── LoRA: 日文增强
└── LoRA: 韩文增强
7.9 注意事项
训练一致性:LoRA 适配器必须在与基础模型相同版本的模型上训练。Qwen2.5-7B 上训练的 LoRA 不能用在 Qwen2-7B 上。
Rank 选择:rank 越大,表达能力越强但显存占用越多。一般 8-64 范围内选择,大多数场景 rank=16 足够。
目标模块:对所有线性层应用 LoRA(包括 gate_proj, up_proj, down_proj)通常效果最好。
最大 LoRA 数:
max_loras参数决定同时加载的 LoRA 数量,超出的 LoRA 会使用 LRU 策略换出。
LoRA 权重合并:如果不需要动态切换,可以将 LoRA 权重合并到基础模型中,减少推理开销。
7.10 扩展阅读
上一章:06 - 模型量化 | 下一章:08 - 调度与批处理策略