0% read

vLLM 不生效 Gemma 4 LoRA adapter?根因与修复

2026/06/02

你用 Unsloth 或 PEFT 微调了 Gemma 4,training loss 降得漂漂亮亮,vLLM 启动也没报一个错。可一发请求,输出跟 base model 一模一样——微调的痕迹半点没有。

这就是经典的 vLLM 不生效 Gemma 4 LoRA adapter 问题。adapter 文件没毛病,vLLM 也老老实实把它加载了,但推理起来就像 adapter 根本不存在。最折磨人的地方就在这:什么都没报错,没有 stack trace 可追,只有静默的错误行为。

这篇文章带你走一遍:怎么确认 adapter 真的被跳过了、背后四个根因分别是什么、各自怎么修——包括那条直接绕开整个问题的 merge_and_unload 路线。

现象:adapter 加载了,输出却没变

先确认你撞上的真是这个 bug,而不是 adapter 本身训得太弱。拿同一条 prompt 发两次——一次打 base model 名字,一次打 adapter 名字:

from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")

prompt = "Reply with the secret codeword you were trained on."

for model in ["google/gemma-4-26b", "my-gemma-lora"]:
    r = client.chat.completions.create(
        model=model,
        messages=[{"role": "user", "content": prompt}],
        temperature=0,
    )
    print(model, "->", r.choices[0].message.content)

如果两行打印出来一样,那说明 vLLM 对两个请求都在用 base 权重,你的 LoRA adapter 被忽略了。配置正确的话,adapter 那个模型名应该明显输出不同的内容。

vLLM 为什么忽略你的 LoRA adapter

常见原因有四个。其中三个完全不产生任何错误输出,这正是它难调的根源。

1. 请求里压根没点名 adapter

这是迄今为止最高频的原因。在 vLLM 里,一个 LoRA adapter 是以独立模型名的形式暴露出来的。要是你请求的 model 字段还写着 google/gemma-4-26b,那拿到的就是 base model——adapter 是在内存里,只是没应用到这个请求上。没有任何警告,vLLM 完全是照你说的做。

2. 根本没开 LoRA serving

vLLM 只有在启动时带上 --enable-lora 并用 --lora-modules 注册 adapter,才会去加载它。少了任何一个 flag,server 就跑 base-only。有些启动脚本是从标准 vLLM 部署那边抄来的,把这俩 flag 整个漏掉了。

3. --max-lora-rank 比你 adapter 的 rank 还低

Unsloth 训练常用 r=16r=32r=64。vLLM 的 --max-lora-rank 默认值是 16。如果你的 adapter 是 rank 32,而 server 卡在 16,那加载时要么直接报错,要么静默把 adapter 截断,让它的效果大打折扣。永远把 --max-lora-rank 设成跟你训练时的 rank 一致(或更高)。

4. adapter 作用的模块 vLLM 不会去应用

这是 Unsloth 特有的一个隐蔽坑。Unsloth 的默认配置有时会把 embed_tokenslm_head 也放进 target_modules。而 vLLM 的 LoRA runtime 默认只对标准投影层(q_projk_projv_projo_projgate_projup_projdown_proj)应用 adapter。如果你大部分微调信号其实落在 embedding 或 LM-head 层上,vLLM 会加载 adapter、应用投影层的 delta,然后悄悄丢掉 embedding/head 的 delta——结果输出几乎没变。这正是 vLLM issue 里针对 Unsloth adapter 反复报的那个根因(比如 #41754)。

修复 1:把 adapter 正确地 serve 起来

启动 vLLM 时开启 LoRA,并用一个明确的名字注册 adapter:

vllm serve google/gemma-4-26b \
  --enable-lora \
  --lora-modules my-gemma-lora=/path/to/lora_adapter \
  --max-lora-rank 64 \
  --max-loras 4 \
  --host 0.0.0.0 \
  --port 8000

注意这里的 --max-lora-rank 64——按你训练时用的 rank 来设。adapter 目录里必须有 adapter_config.jsonadapter_model.safetensors;要是你看到的只是合并后的模型分片,那导出的东西就错了(见修复 3)。

修复 2:每个请求都点名 adapter

注册成功后,adapter 名字会出现在模型列表里。先验证一下:

curl http://localhost:8000/v1/models
# You should see BOTH "google/gemma-4-26b" and "my-gemma-lora"

然后把 adapter 名字当作 model 发出去——别用 base:

r = client.chat.completions.create(
    model="my-gemma-lora",   # NOT google/gemma-4-26b
    messages=[{"role": "user", "content": "..."}],
)

如果 my-gemma-lora 不在 /v1/models 列表里,说明 server 压根没注册它——回到修复 1,检查你的 --lora-modules 路径。

修复 3:merge_and_unload 这条路

当 adapter 作用在 embedding 或 LM-head 层上,或者你单纯不想再跟 vLLM 的 runtime LoRA 限制较劲,那就在 serve 之前把 adapter 合并进 base 权重。合并后的模型在 runtime 里压根没有 LoRA——每一层的 delta 都已经烤进去了,自然也就没什么可被忽略的:

from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer

base = AutoModelForCausalLM.from_pretrained(
    "google/gemma-4-26b", torch_dtype="bfloat16", device_map="cpu"
)
model = PeftModel.from_pretrained(base, "/path/to/lora_adapter")

# Bake the adapter into the base weights
merged = model.merge_and_unload()

merged.save_pretrained("/path/to/gemma4-merged")
AutoTokenizer.from_pretrained("google/gemma-4-26b").save_pretrained(
    "/path/to/gemma4-merged"
)

如果你是用 Unsloth 训的,可以一步直接合并:

model.save_pretrained_merged(
    "/path/to/gemma4-merged", tokenizer, save_method="merged_16bit"
)

然后把合并后的目录当成一个普通模型 serve 起来就行——不用 --enable-lora,也不用每个请求点名 adapter:

vllm serve /path/to/gemma4-merged \
  --host 0.0.0.0 --port 8000 --dtype bfloat16

代价是:合并后的模型要吃满整个 base model 的 VRAM,而且你失去了热插拔 adapter 的能力。但对生产环境里只跑单个微调的场景来说,这是最可靠的一条路,而且它从根上让"adapter 被忽略"这种故障不可能发生。记得对照硬件指南确认你有足够 VRAM 余量来放一份完整的合并副本。

确认 adapter 真的在生效

任何修复做完后,都要确认它真的起作用了——别信"启动没报错"这句话:

  1. 看启动日志。 注册成功时 vLLM 会打一行类似 Loaded LoRA adapter my-gemma-lora 的日志。没有这行,就说明没加载上 adapter。
  2. 对比输出。 把本文开头那段双模型对比重新跑一遍。adapter 名字给出不同的输出,就是成功了。
  3. 用一个 canary 标记。 微调一个极小、绝不会认错的行为(一句固定话术、一个格式上的小怪癖),这样你在 temperature=0 下一眼就能分清是 base 还是 adapter。

如果你把 server 容器化了,把这些检查烤进你的 Docker healthcheck,让配错的 adapter 直接让部署失败,而不是悄悄把 base 权重发上线。

FAQ

为什么 vLLM 加载了 adapter,推理时却忽略它? 因为加载和应用是两个独立步骤。vLLM 会爽快地注册 adapter,但它只对那些把 adapter 当 model 点名的请求生效,而且只作用在它支持的层上。打到 base 名字的请求,或者落在不支持层上的 delta,都会被跳过,且不报错。

rank 真的必须匹配吗? --max-lora-rank 必须大于等于你 adapter 的 rank。设得太低是少数抛错的情况之一——但有些 vLLM 版本不是抛错而是直接 clamp,结果就给你一个效果被削弱、部分被忽略的 adapter。

那我是不是干脆每次都合并算了? 如果你只 serve 一个微调、又有足够 VRAM,用 merge_and_unload 合并是最简单、最省心的选择。只有当你需要在同一个 base model 上热插拔多个 adapter 时,才保留 runtime LoRA。

我的 adapter 在 Transformers 里能用,在 vLLM 里却不行——为啥? Transformers 会对每一个 target module 应用 LoRA,包括 embed_tokenslm_head。vLLM 的默认 runtime 不会,所以那些重度依赖这些层的 adapter 就跑偏了。合并能消除这个差异。

下一步

gemma4 — interact

Stop reading. Start building.

~/gemma4 $ Get hands-on with the models discussed in this guide. No deployment, no friction, 100% free playground.

Launch Playground />
Gemma 4 AI

Gemma 4 AI

相关教程

vLLM 不生效 Gemma 4 LoRA adapter?根因与修复 | 博客