跳到正文
原文
Google AI:DEV 作者专属(RSS)· The Homelab Postmortem·· 3 小时前精选AI 评分72

vLLM 0.30.0 忽略 LoRA rank_pattern 和 alpha_pattern,按错误缩放加载适配器

vLLM ignores LoRA rank_pattern and alpha_pattern and serves the adapter at the wrong scale

AI 导读

作者实测发现 vLLM 0.30.0 只读取 r 和 lora_alpha,静默丢弃 PEFT 适配器中的 rank_pattern 和 alpha_pattern,导致相关模块按错误缩放系数服务。

推荐理由

作者用小模型实测确认 vLLM 静默丢弃 LoRA pattern 导致缩放错误,并给出可复用的折叠修复脚本和检测工具。

正文 · AI 翻译

TL;DR:PEFT LoRA 适配器可以通过 adapter_config.json 中的 rank_pattern 和 alpha_pattern 为各个模块赋予各自的 rank 和 alpha,PEFT 会按每个模块自己的 alpha_m / r_m 对其进行缩放。vLLM 0.30.0 只读取 r 和 lora_alpha,并对每个模块都应用 lora_alpha / r。这些 pattern 会被静默丢弃,没有任何警告。在 WSL2 下的 RTX 2070 上,使用 PEFT 0.21.2 和一个小的随机 Qwen3,一个带有 rank_pattern = {"q_proj": 4} 的适配器给出的 prompt log-probabilities 与 PEFT 平均相差 0.0211,而同样设置但不带 pattern 时仅相差 0.0003。一个只设置了 alpha_pattern 的适配器偏差为 0.0168。将受影响模块的 lora_B 乘以缺失的因子并清除 pattern 后,偏差回到 0.0003。该工具包的 check-lora-patterns.py 会读取适配器的配置,并列出 vLLM 会错误缩放的模块,以及错误倍数。上游报告:vllm-project/vllm#59799。

症状

Windows 11 上的 RTX 2070(8 GB),WSL2 Ubuntu,驱动 591.86。vLLM 0.30.0 搭配 torch 2.13.0+cu130,PEFT 0.21.2。模型是一个随机初始化的两层 Qwen3(hidden size 256),因此无需下载任何内容。RTX 2070 不支持 bf16,所以一切都在 fp16 下运行(报告中用的是 bf16)。

在 q_proj 和 v_proj 上测试四个适配器,全部为 r=16、lora_alpha=32,并将 lora_B 调大,使适配器的效果明显高于 fp16 噪声。对每个适配器,以 PEFT 自身在四个 48-token prompt 上的 prompt log-probabilities 作为参考,然后让 vLLM 在同一基座模型上服务同一个适配器:

adapter                                   PEFT q_proj scale   mean |vLLM - PEFT|   max
none          no pattern                  2                   0.0003               0.0016
rank          rank_pattern {"q_proj": 4}  32/4   = 8          0.0211               0.1106
rank_folded   rank, q_proj lora_B x4,     2 (x4 in weights)   0.0003               0.0010
              rank_pattern {}
alpha         alpha_pattern {"q_proj":128} 128/16 = 8         0.0168               0.1046

第一行表明 vLLM 的 LoRA 路径没有问题:没有 pattern 时,它与 PEFT 的差异在 fp16 舍入误差范围内。当某个模块带有 pattern 时,误差大了 70 倍。折叠副本——将 PEFT 应用的因子移入权重本身——再次匹配。仅 alpha_pattern 也会产生同样的误差,所以问题出在缩放上,而不是模块具有不同的 rank。

vLLM 日志对此只字未提。运行过程中没有任何一行提到 rank_pattern 或 alpha_pattern。

为什么容易被忽视

vLLM 会接受该适配器。它已经会拒绝某些适配器:_validate_features 会拒绝 DoRA 和 modules_to_save,因此一个能顺利加载的适配器看起来就是受支持的。输出仍然是该适配器的输出,只是在带 pattern 的模块中比训练时更弱或更强。生成的文本没有任何迹象表明这一点。你必须与 PEFT 对比,或者衡量你训练时所追求的质量,才能发现它。

这些 pattern 也并不罕见。报告指出,PEFT 自己的 LoRA 文档推荐对混合专家模型使用 rank_pattern,给每个专家一个更小的 rank(r // num_experts),并指向 vLLM 进行服务。报告者在 OLMoE-1B-7B 上测量了后果:PEFT 中留出集困惑度为 10.06,vLLM 中为 11.28,而基座模型为 13.09。这意味着 vLLM 放弃了适配器所获得收益的约 40%。这些数字是报告者的,来自本实验室没有的硬件。该效应的方向和大小与此处的小模型相符。

真正发生了什么

0.30.0 中的 vllm/lora/peft_helper.py 定义了 vLLM 从 adapter_config.json 读取的字段:

r: int
lora_alpha: int
target_modules: list[str] | str
bias: ...
modules_to_save: list[str] | None = ...
use_rslora: bool = field(default=False)
use_dora: bool = field(default=False)
vllm_lora_scaling_factor: float = field(default=1.0)

并据此计算出一个缩放值:

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

没有针对 rank_pattern 或 alpha_pattern 的字段,对代码仓库的代码搜索也找不到其他引用。每个模块的 rank 仍然正确,因为它取自 safetensors 文件中的张量形状。只有缩放值是共享的,因此任何因 pattern 导致 alpha_m / r_m 与 lora_alpha / r 不同的模块,都会被恰好按该比例错误缩放。对于上面的 q_proj,即 8 对 2,因子为 4。

并非所有带模式的适配器都会受影响。根据报告,PEFT 的 save_as_lora 在动态秩下会为每个模块在两种模式中写入 r=1、lora_alpha=1 和相同的值。于是每个模块的 scale 都是 1,vLLM 只是碰巧算对了。

修复方案已在 #59801 中提出,它按模块应用 scale。该 PR 仍处于开放且未合并状态,当前发布版本是 0.30.0。

修复

在包含该修复的版本发布之前,把每个受影响模块的 scale 修正折叠进它的 lora_B,并移除这些模式。系数是该模块 PEFT 的 scale 除以 vLLM 的 scale,check-lora-patterns.py 会打印它。对于上面的适配器,q_proj 的系数是 4。这就是 rank_folded 那一行所用的折叠方式,写入一个新目录:

import json, os
from safetensors.torch import load_file, save_file

sd = load_file("rank/adapter_model.safetensors")
sd = {k: v * 4 if "q_proj.lora_B" in k else v for k, v in sd.items()}   # 4 = (32/4) / (32/16)
os.makedirs("rank_folded")
save_file(sd, "rank_folded/adapter_model.safetensors")
cfg = json.load(open("rank/adapter_config.json"))
cfg["rank_pattern"] = {}
json.dump(cfg, open("rank_folded/adapter_config.json", "w"))

两点注意事项:

  • 模式键是模块名模式。根据报告,PEFT 会将键与模块路径的末尾进行匹配,因此 q_proj 会作用于每一层的 q_proj,而像 layers.3.self_attn.q_proj 这样的键只作用于一个。要折叠该键实际覆盖的张量。
  • 不要只是删除 alpha_pattern。那也会改变 PEFT 的行为,而 vLLM 仍保持原样。

之后在折叠后的适配器上运行 check-lora-patterns.py。它应报告每个模块都保持 lora_alpha / r。

可推广的习惯

适配器配置是一组指令,而只识别其中一部分的加载器会悄悄地执行另一组指令。vLLM 会直接拒绝某些不支持的适配器特性,这就让人容易以为凡是它接受的,它就支持。事实并非如此。在你信任某个服务器能复现训练设置之前,先用同一批输入比较它的一批输出与训练框架的输出。这里只需要四个提示词和一个数字。

这是本站第二个 vLLM 接受适配器却提供其他东西的 LoRA 案例。第一个是 一个其目标模块永远不会被应用的适配器。它的权重缓存——可能提供另一个检查点的权重——是同一模式低一层级的体现:一项检查通过的范围小于它声称检查的范围。

工具包的 check-lora-patterns.py 会读取 adapter_config.json 文件,或某个目录下的每一个文件。对于模式会改变其 scale 的每个模块,它会打印 PEFT 的 scale、vLLM 的 scale,以及两者之间的系数。它不会标记那些模式使所有 scale 保持相等的 save_as_lora 式适配器。

工具包

本文的修复方案已在工具包中作为经过测试、可直接运行的脚本提供。

查看工具包 →

来源:Google AI:DEV 作者专属(RSS) · dev.to