大模型-解析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 数学原理)

  2. 整体架构

  3. 核心类详解

  4. 完整推理流程

  5. 关键代码逐段解析

  6. [多 LoRA 并发路由机制](#多 LoRA 并发路由机制)

  7. [LRU 缓存策略](#LRU 缓存策略)

  8. 配置参数说明

  9. 内存布局

  10. 日志与调试


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 模型的核心类,负责:

  1. 初始化时将目标模块替换为支持 LoRA 的版本

  2. 管理 CPU 缓存和 GPU 激活槽位

  3. 将 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_wrapperLoRAMapping

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.pyset_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 推理生效的检查清单

  1. 启动日志中出现 Activating LoRA. int id: X

  2. 请求返回结果与基础模型有差异

  3. /v1/models 接口返回列表中包含 LoRA 名称

  4. 服务端 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 微调效果)
相关推荐
alajl10 小时前
Hermes 源码阅读1
人工智能
碳基硅坊10 小时前
Mac Studio 部署 Qwen3.6-27B omlx & dflash 深度评测
人工智能·大模型部署·qwen3.6-27b
cci10 小时前
Moveit2 安装
人工智能
cci10 小时前
Moveit2 快速入门
人工智能
俊哥V10 小时前
每日 AI 研究简报 · 2026-05-28
人工智能·ai
wabs66610 小时前
本科毕业设计项目——基于RAG与大语言模型的408问答系统设计与实现【检索与生成功能的第三步答案生成是怎么实现的?】
人工智能·语言模型·自然语言处理
geneculture10 小时前
从“巴别塔”到“耶路撒冷”:融智学应对AI时代治理困境的系统方案
大数据·人工智能·融智学的重要应用·哲学与科学统一性·融智时代(杂志)·人际间性·人机间性
Engineer邓祥浩10 小时前
宏观认知(1):AI 是什么——吴恩达《AI for Everyone》Week1 学习笔记
人工智能·笔记·学习
小程故事多_8010 小时前
深入解析FlashAttention,大模型长序列训练的底层优化核心技术
人工智能·transformer