你用 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=16、r=32 或 r=64。vLLM 的 --max-lora-rank 默认值是 16。如果你的 adapter 是 rank 32,而 server 卡在 16,那加载时要么直接报错,要么静默把 adapter 截断,让它的效果大打折扣。永远把 --max-lora-rank 设成跟你训练时的 rank 一致(或更高)。
4. adapter 作用的模块 vLLM 不会去应用
这是 Unsloth 特有的一个隐蔽坑。Unsloth 的默认配置有时会把 embed_tokens 和 lm_head 也放进 target_modules。而 vLLM 的 LoRA runtime 默认只对标准投影层(q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_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.json 和 adapter_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 真的在生效
任何修复做完后,都要确认它真的起作用了——别信"启动没报错"这句话:
- 看启动日志。 注册成功时 vLLM 会打一行类似
Loaded LoRA adapter my-gemma-lora的日志。没有这行,就说明没加载上 adapter。 - 对比输出。 把本文开头那段双模型对比重新跑一遍。adapter 名字给出不同的输出,就是成功了。
- 用一个 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_tokens 和 lm_head。vLLM 的默认 runtime 不会,所以那些重度依赖这些层的 adapter 就跑偏了。合并能消除这个差异。
下一步
- 想要 serving 和扩缩容的完整设置,看vLLM 生产部署指南。
- 想从一开始就让
target_modules对 vLLM 友好,看微调指南。 - 撞上别的部署坑?Gemma 4 排错指南讲了 OOM、启动慢和量化相关的问题。
Stop reading. Start building.
~/gemma4 $ Get hands-on with the models discussed in this guide. No deployment, no friction, 100% free playground.
Launch Playground />


