大模型-解析vllm lora 模块
vLLM LoRA 实现深度解析
文档版本:2026-05-27 代码路径:
vllm/lora/覆盖文件:models.py,worker_manager.py,request.py,lora_weights.py,peft_helper.py
目录
1. LoRA 数学原理
LoRA(Low-Rank Adaptation)的核心思想是:在原始权重矩阵 W₀ ∈ ℝ^(d×k) 旁边注入一个低秩分解的增量,而不修改原始权重:
W = W₀ + ΔW = W₀ + B · A
其中:
-
A ∈ ℝ^(r×k):下投影矩阵(lora_a),随机高斯初始化 -
B ∈ ℝ^(d×r):上投影矩阵(lora_b),初始化为零 -
r ≪ min(d, k):秩,控制参数量
前向传播公式:
h = W₀·x + (α/r) · B·A·x
其中 α(lora_alpha)是缩放超参数,scaling = α/r。
rsLoRA 变体
使用 Rank-Stabilized LoRA(rsLoRA)时,缩放因子改为:
scaling = α / √r
在 peft_helper.py 中实现:
if self.use_rslora: self.vllm_lora_scaling_factor = self.lora_alpha / math.sqrt(self.r) else: self.vllm_lora_scaling_factor = self.lora_alpha / self.r
推理优化:预合并 scaling
为了减少推理时的乘法运算,vLLM 在激活 LoRA 时将 scaling 预乘进 lora_b:
# lora_weights.py def optimize(self) -> "LoRALayerWeights": if self.scaling == 1: return self self.lora_b *= self.scaling # 将 α/r 合并进 B self.scaling = 1 return self
2. 整体架构
模块依赖关系
vllm/lora/ ├── request.py # LoRARequest:请求描述符,携带名称/路径/整数ID ├── peft_helper.py # PEFTHelper:解析 adapter_config.json,校验 LoRA 配置 ├── lora_weights.py # LoRALayerWeights / PackedLoRALayerWeights:单层权重封装 ├── models.py # LoRAModel / LoRAModelManager / LRUCacheLoRAModelManager ├── worker_manager.py # WorkerLoRAManager:Worker 侧管理入口 ├── layers.py # BaseLayerWithLoRA 及各线性层的 LoRA 替换实现 ├── ops/ # Triton/CUDA 推理算子(lora_shrink, lora_expand) └── punica_wrapper/ # token 级别多 LoRA 路由元数据管理
类层次结构
WorkerLoRAManager ← Worker 侧入口,每个 GPU Worker 一个实例 └── LoRAModelManager ← 管理 LoRA 模型的加载/激活/卸载 └── LRUCacheLoRAModelManager ← LRU 版本,支持多适配器动态换入换出 ├── _registered_adapters: LoRALRUCache ← CPU 缓存 └── _active_adapters: LoRALRUCache ← GPU 激活槽 LoRAModel ← 一个完整的 LoRA 适配器 └── loras: dict[str, LoRALayerWeights] ← 模块名 → 该层的 A/B 矩阵 LoRARequest ← 客户端请求中携带的 LoRA 标识 ├── lora_name: str ← 用户可见名称(如 "asr-v1") ├── lora_int_id: int ← 内部全局唯一整数 ID └── lora_path: str ← 权重文件路径
3. 核心类详解
3.1 LoRARequest(request.py)
请求中携带的 LoRA 标识符,是连接前端请求与后端权重管理的桥梁。
class LoRARequest(msgspec.Struct): lora_name: str # API 层用户指定的名称,如 "asr-v1" lora_int_id: int # 全局唯一整数 ID,>0,用于 GPU 槽位索引 lora_path: str # 适配器权重磁盘/远程路径
设计要点:使用 msgspec.Struct 实现高效序列化,array_like=True 支持跨进程传递。
3.2 PEFTHelper(peft_helper.py)
解析 LoRA 适配器目录下的 adapter_config.json,提供标准化的配置接口。
关键字段:
-
r:LoRA 秩 -
lora_alpha:缩放因子 -
target_modules:目标模块名列表(如["q_proj", "v_proj"]) -
use_rslora:是否使用 rsLoRA -
use_dora:是否使用 DoRA(vLLM 暂不支持) -
vllm_lora_scaling_factor:最终缩放值(由__post_init__计算)
3.3 LoRALayerWeights(lora_weights.py)
封装单个模型层的 LoRA 权重(A 矩阵 + B 矩阵)。
class LoRALayerWeights: module_name: str # 所属模块名,如 "model.layers.0.self_attn.q_proj" rank: int # LoRA 秩 r lora_alpha: int # 缩放因子 α lora_a: torch.Tensor # shape: (r, k),输入投影 lora_b: torch.Tensor # shape: (d, r),输出投影 scaling: float # α/r,optimize() 后变为 1 def optimize(self): """将 scaling 预乘进 lora_b,推理时无需额外乘法""" self.lora_b *= self.scaling self.scaling = 1
PackedLoRALayerWeights 是其子类,处理 qkv_proj 等打包层(多个子模块合并到一个矩阵)。
3.4 LoRAModel(models.py)
代表一个完整加载的 LoRA 适配器,包含所有层的权重。
class LoRAModel: id: int # lora_int_id rank: int # 全局秩 loras: dict[str, LoRALayerWeights] # 模块名 → 该层 LoRA 权重
加载入口:LoRAModel.from_local_checkpoint(),支持以下格式:
| 文件名 | 格式 | 优先级 |
|---|---|---|
adapter_model.safetensors |
safetensors | 最高(推荐) |
adapter_model.bin |
PyTorch pickle | 次之 |
adapter_model.pt |
PyTorch pickle | 次之 |
3.5 LoRAModelManager(models.py)
管理 LoRA 模型的核心类,负责:
-
初始化时将目标模块替换为支持 LoRA 的版本
-
管理 CPU 缓存和 GPU 激活槽位
-
将 LoRA 权重写入 GPU buffer
关键属性:
_registered_adapters: dict[int, LoRAModel] # CPU 中已加载的 LoRA,以 lora_id 为 key _active_adapters: dict[int, None] # 当前激活在 GPU 上的 LoRA lora_index_to_id: list[Optional[int]] # GPU 槽位索引 → lora_id 映射,长度 = max_loras modules: dict[str, BaseLayerWithLoRA] # 模型中被替换的 LoRA 层 punica_wrapper # token 级别的 LoRA 路由管理器
3.6 WorkerLoRAManager(worker_manager.py)
Worker 进程的 LoRA 管理入口。每次推理 batch 前,Scheduler 会调用其 set_active_adapters() 来确保所需 LoRA 已加载并激活。
class WorkerLoRAManager: def set_active_adapters(self, requests, mapping): self._apply_adapters(requests) # 加载未缓存的 LoRA self._adapter_manager.set_adapter_mapping(mapping) # 更新路由
有两个子类:
-
WorkerLoRAManager:每次 batch 只保留当前需要的 LoRA,其余全部卸载 -
(对应
LRUCacheLoRAModelManager):LRU 策略,保留最近使用的 LoRA
4. 完整推理流程
客户端请求 │ POST /v1/chat model="asr-v1" ▼ API Server │ 解析 model 名称 → 查找 lora_modules 注册表 → 创建 LoRARequest │ LoRARequest(lora_name="asr-v1", lora_int_id=1, lora_path="/path/to/asr-v1") ▼ Scheduler(调度器) │ 将 LoRARequest 附加到 SequenceGroup │ batch 调度时收集所有 lora_requests 集合 ▼ Worker.execute_model() │ 传入 lora_requests: set[LoRARequest] │ 传入 lora_mapping: LoRAMapping(token → lora_id 映射) ▼ WorkerLoRAManager.set_active_adapters(lora_requests, lora_mapping) │ ├─ _apply_adapters(lora_requests) │ │ │ └─ for each lora_request: │ │ lora_id not in registered? │ │ YES → _load_adapter(lora_request) │ │ │ 读取 adapter_config.json → PEFTHelper │ │ │ 读取 adapter_model.safetensors → tensors │ │ │ LoRAModel.from_lora_tensors() → LoRAModel │ │ └─ LoRAModelManager.add_adapter(lora_model) │ │ │ └─ LoRAModelManager.activate_adapter(lora_int_id) │ │ 找空闲 GPU 槽位 index │ │ lora_index_to_id[index] = lora_id │ │ for each module: │ │ module.set_lora(index, A, B, ...) ← 写入 GPU buffer │ └─ _active_adapters[lora_id] = None │ └─ set_adapter_mapping(lora_mapping) │ punica_wrapper.update_metadata() └─ token_0 → slot_0, token_1 → slot_0, token_2 → slot_1, ... 模型 forward pass │ for each LoRA 层(已替换为 BaseLayerWithLoRA): │ output = W₀·x ← 基础权重 │ + lora_b[slot]·lora_a[slot]·x ← LoRA 增量(slot 由 punica 路由) ▼ 输出 token
5. 关键代码逐段解析
5.1 模型层替换 _create_lora_modules()
def _create_lora_modules(self): for module_name, module in self.model.named_modules(): if not self._match_target_modules(module_name): continue # 过滤多模态模型中的视觉塔(仅对语言模型部分应用 LoRA) if self._filter_unsupported_mm_module(module_name): continue # 将原始 nn.Linear 替换为 BaseLayerWithLoRA # 内部维护 lora_a_stacked[max_loras, r, k] 和 lora_b_stacked[max_loras, d, r] new_module = replace_submodule( self.model, module_name, from_layer(module, self.lora_slots, self.lora_config, ...) ) # 绑定 punica_wrapper,用于推理时 token→lora 路由 new_module.set_mapping(self.punica_wrapper) self.modules[module_name] = new_module
关键点:替换后的层内部预分配了 max_loras 个槽位的 GPU buffer,运行前把不同 LoRA 的 A/B 矩阵分别写入不同槽位。
5.2 LoRA 加载 from_local_checkpoint()
@classmethod
def from_local_checkpoint(cls, lora_dir, expected_lora_modules, peft_helper, ...):
# 1. 校验模块名合法性
def check_unexpected_modules(modules):
for lora_module in modules.keys():
module_name, _, _ = parse_fine_tuned_lora_name(lora_module)
if module_name.split(".")[-1] not in expected_lora_modules:
unexpected_modules.append(module_name)
if unexpected_modules:
raise ValueError(f"Unexpected modules: {unexpected_modules}")
# 2. 加载权重(优先 safetensors)
with safetensors.safe_open(lora_tensor_path, framework="pt") as f:
check_unexpected_modules(f)
tensors = {module: f.get_tensor(module) for module in f.keys()}
# 3. 构建 LoRAModel
return cls.from_lora_tensors(
tensors=tensors, peft_helper=peft_helper, device=device, ...
)
5.3 GPU 槽位激活 activate_adapter()
def activate_adapter(self, lora_id: int) -> bool:
if lora_id in self._active_adapters:
return False # 已激活,跳过
# 找第一个空闲槽位
first_free_slot = next(
((i, lid) for i, lid in enumerate(self.lora_index_to_id) if lid is None),
None
)
if first_free_slot is None:
raise ValueError("No free lora slots")
index, _ = first_free_slot
self._active_adapters[lora_id] = None
lora_model = self._registered_adapters[lora_id]
logger.info("Activating LoRA. int id: %d, slot index: %d", lora_model.id, index)
self.lora_index_to_id[index] = lora_model.id
# 将 A/B 矩阵写入对应槽位的 GPU buffer
for module_name, module in self.modules.items():
module_lora = self._get_lora_layer_weights(lora_model, module_name)
if module_lora:
module_lora.optimize() # 预乘 scaling 进 B
module.set_lora(
index, # 槽位编号
module_lora.lora_a, # A 矩阵
module_lora.lora_b, # B 矩阵(已含 scaling)
module_lora.embeddings_tensor,
module_lora.bias,
)
else:
module.reset_lora(index) # 该层无 LoRA,置零
return True
5.4 打包模块处理 _create_merged_loras_inplace()
对于 qkv_proj 这类将 Q/K/V 合并到同一矩阵的层,需要将三个独立的 LoRA 权重合并为一个 PackedLoRALayerWeights:
def _create_merged_loras_inplace(self, lora_model: LoRAModel): for module_name, new_module_names in self.packed_modules.items(): # module_name = "qkv_proj" # new_module_names = ["q_proj", "k_proj", "v_proj"] replacement_loras = [] for r in new_module_names: lora = lora_model.get_lora(r) # 获取各子模块的 LoRA replacement_loras.append(lora) # 合并为 PackedLoRALayerWeights lora_model.loras[module_name] = PackedLoRALayerWeights.pack(replacement_loras) # 删除原子模块的 LoRA 条目 for module in replaced_module: lora_model.loras.pop(module, None)
6. 多 LoRA 并发路由机制
vLLM 的一个核心能力是:同一个推理 batch 中,不同的 token 可以使用不同的 LoRA 适配器。这依赖 punica_wrapper 和 LoRAMapping。
LoRAMapping 结构
LoRAMapping: - token_lora_mapping: [lora_slot_0, lora_slot_0, lora_slot_1, ...] 每个 token 对应的 GPU 槽位编号(而非 lora_int_id)
路由更新流程
Scheduler 构建 LoRAMapping ↓ WorkerLoRAManager.set_active_adapters(requests, mapping) ↓ LoRAModelManager.set_adapter_mapping(mapping) ↓ punica_wrapper.update_metadata(mapping, lora_index_to_id, ...) 将 lora_int_id 翻译为 GPU 槽位 index ↓ 每层 BaseLayerWithLoRA 的 forward() 从 punica_wrapper 获取当前 token 对应的槽位 output += lora_b_stacked[slot] · lora_a_stacked[slot] · x
底层算子
位于 vllm/lora/ops/triton_ops/:
-
lora_shrink:计算A·x,将 hidden_size 降维到 lora_rank(shrink 阶段) -
lora_expand:计算B·(A·x),将 lora_rank 升维回 hidden_size(expand 阶段)
这两个 Triton 算子支持 batch 内混合多个 LoRA,通过 token_indices_sorted_by_lora_ids 按 LoRA ID 排列 token 以提升内存访问局部性。
7. LRU 缓存策略
当需要服务的 LoRA 适配器数量超过 GPU 能同时容纳的槽位数(max_loras)时,使用 LRUCacheLoRAModelManager。
两级缓存
┌─────────────────────────────────────────────┐ │ CPU 内存缓存(_registered_adapters) │ │ 容量 = max_cpu_loras(默认 = max_loras) │ │ 策略:LRU 淘汰 │ │ [asr-v1, asr-v2, asr-v3, ..., asr-v10] │ └─────────────────┬───────────────────────────┘ │ activate_adapter()(按需搬运到 GPU) ┌─────────────────▼───────────────────────────┐ │ GPU 激活槽(_active_adapters) │ │ 容量 = max_loras │ │ 策略:LRU 驱逐最久未使用的槽位 │ │ slot_0: asr-v1 │ slot_1: asr-v2 │ └─────────────────────────────────────────────┘
LRU 激活逻辑
# LRUCacheLoRAModelManager.activate_adapter() def activate_adapter(self, lora_id: int) -> bool: # GPU 槽满时,驱逐最旧的激活 LoRA if lora_id not in self._active_adapters and len(self._active_adapters) >= self.lora_slots: self._active_adapters.remove_oldest() # 从 GPU buffer 驱逐 result = super().activate_adapter(lora_id) # 写入 GPU buffer # 更新 LRU 顺序 self._active_adapters.touch(lora_id) return result
Pinning 机制
对于需要保证常驻 GPU 的高优先级 LoRA,可以调用 pin_adapter(lora_id):
def pin_adapter(self, lora_id: int) -> bool: self._pin_lora_in_cpu_cache(lora_id) # CPU 缓存中固定 self._pin_lora_in_gpu_cache(lora_id) # GPU 激活槽中固定(不被 LRU 驱逐)
8. 配置参数说明
| 启动参数 | 配置字段 | 默认值 | 说明 |
|---|---|---|---|
--enable-lora |
— | False | 开启 LoRA 支持 |
--max-loras |
lora_config.max_loras |
1 | GPU 同时激活的 LoRA 槽位数 |
--max-cpu-loras |
lora_config.max_cpu_loras |
= max_loras | CPU 内存缓存的 LoRA 总数 |
--max-lora-rank |
lora_config.max_lora_rank |
16 | 预分配 GPU buffer 时的最大秩 |
--lora-modules |
LoRARequest 注册表 | — | 格式:name=path [name2=path2 ...] |
--lora-dtype |
lora_config.lora_dtype |
同模型 | LoRA 权重的精度 |
--enable-lora-bias |
lora_config.bias_enabled |
False | 是否支持 LoRA bias |
典型启动命令
python -m vllm.entrypoints.openai.api_server \ --model /path/to/base/model \ --enable-lora \ --max-loras 4 \ --max-cpu-loras 16 \ --max-lora-rank 64 \ --lora-modules \ asr-v1=/path/to/lora/asr-v1 \ asr-v2=/path/to/lora/asr-v2
9. 内存布局
GPU Buffer 结构(每个 BaseLayerWithLoRA)
lora_a_stacked: Tensor[max_loras, max_lora_rank, in_features] slot 0 → asr-v1 的 A 矩阵 slot 1 → asr-v2 的 A 矩阵 slot 2 → None(空闲) slot 3 → None(空闲) lora_b_stacked: Tensor[max_loras, out_features, max_lora_rank] 同上结构
槽位映射表
lora_index_to_id = [1, 2, None, None] ↑ ↑ asr-v1 asr-v2(lora_int_id)
显存估算
单个 LoRA 适配器在 GPU 上占用的显存:
显存 = num_lora_layers × (in_features × rank + out_features × rank) × dtype_bytes × max_loras
例:Qwen2.5-7B,rank=64,bf16,32 个 LoRA 层,max_loras=4:
≈ 32 × (4096×64 + 4096×64) × 2 × 4 = 约 256MB
10. 日志与调试
关键日志点
| 文件 | 函数 | 日志内容 | 级别 |
|---|---|---|---|
models.py |
activate_adapter() |
Activating LoRA. int id: X, slot index: Y |
INFO |
models.py |
AdapterLRUCache._on_remove() |
Removing adapter int id: X |
DEBUG |
models.py |
add_adapter() |
Adding lora. Model id: X, int id: Y |
DEBUG |
peft_helper.py |
__post_init__() |
Loading LoRA weights trained with rsLoRA. |
INFO |
worker_manager.py |
_load_adapter() |
加载失败时的错误信息 | ERROR |
自定义日志(推荐添加位置)
在 worker_manager.py 的 set_active_adapters() 中添加 batch 级别日志:
def set_active_adapters(self, requests, mapping):
active_names = [r.lora_name for r in requests if r is not None]
if active_names:
logger.info("[LoRA] Batch 激活适配器: %s", active_names)
self._apply_adapters(requests)
if mapping is not None:
self._adapter_manager.set_adapter_mapping(mapping)
确认 LoRA 推理生效的检查清单
-
启动日志中出现
Activating LoRA. int id: X -
请求返回结果与基础模型有差异
-
/v1/models接口返回列表中包含 LoRA 名称 -
服务端 INFO 日志中出现
[LoRA] Batch 激活适配器
附录:关键数据流总结
客户端 model="asr-v1"
│
▼
LoRARequest(name="asr-v1", int_id=1, path="...")
│
▼
_load_adapter() → PEFTHelper.from_local_dir() → 读取 adapter_config.json
│ r=64, lora_alpha=16, target_modules=[...]
▼
LoRAModel.from_local_checkpoint()
│ → 读取 adapter_model.safetensors
│ → 按模块名组织 LoRALayerWeights
│ q_proj: {lora_a: (64,4096), lora_b: (4096,64), scaling: 0.25}
│ v_proj: {lora_a: (64,4096), lora_b: (4096,64), scaling: 0.25}
▼
LoRAModelManager.add_adapter(lora_model)
│ → _create_merged_loras_inplace() 合并 packed modules
│ → _registered_adapters[1] = lora_model
▼
LoRAModelManager.activate_adapter(lora_id=1)
│ → lora_index_to_id[0] = 1 (占用 slot 0)
│ → q_proj_layer.set_lora(0, A, B*scaling, ...)
│ → v_proj_layer.set_lora(0, A, B*scaling, ...)
▼
punica_wrapper.update_metadata(mapping)
│ → token_lora_mapping = [0, 0, 0, ...] (全部 token → slot 0)
▼
forward pass
│ h = W₀·x + lora_b_stacked[0] · lora_a_stacked[0] · x
▼
输出文本(含 LoRA 微调效果)
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)