UnslothやPEFTでGemma 4をファインチューニングして、training lossもきれいに下がり、vLLMもエラーひとつ出さずに起動した。ところが実際にリクエストを投げてみると、出力はbase modelとまったく同じ。学習させた内容がどこにも反映されていない――。
これがまさに典型的な vLLMでGemma 4のLoRA adapterが無視される 問題です。adapterファイル自体は正常で、vLLMも文句を言わずにロードしているのに、推論時にはadapterなど最初から存在しなかったかのように振る舞う。何ひとつ「失敗」しないからこそ厄介で、追いかけるべきstack traceもなく、ただ静かに間違った挙動を返してくるわけです。
この記事では、本当にadapterが無視されているのかを確認する方法、その背後にある4つの根本原因、そしてそれぞれの対処法を順に見ていきます。問題そのものを回避してしまう merge_and_unload のルートも紹介します。
症状:adapterはロードされるのに出力が変わらない
まずは、本当にこのバグを踏んでいるのか、それとも単にadapterの学習が弱いだけなのかを切り分けましょう。同じプロンプトを2回送り、一度は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 weightsを返しており、LoRA adapterは無視されています。正しく動いている環境なら、adapter名のモデルでは目に見えて異なる出力になります。
vLLMがLoRA adapterを無視する理由
よくある原因は4つあります。そのうち3つはエラーを一切出さないため、デバッグが非常に難しくなっています。
1. リクエストでadapterを指定していない
これが圧倒的に多い原因です。vLLMでは、LoRA adapterは それ自体が独立したモデル名 として公開されます。リクエストの model フィールドが google/gemma-4-26b のままだと、返ってくるのはbase modelそのもの。adapterはメモリ上にロードされていても、そのリクエストには適用されません。警告も出ません。vLLMはあなたが指定したとおりに動いているだけです。
2. LoRAサービングを有効にしていない
vLLMは、--enable-lora を付けて起動し、--lora-modules でadapterを登録したときにだけadapterをロードします。どちらかのフラグが抜けていると、サーバーはbaseのみで動きます。標準的なvLLMのデプロイからコピーしてきた起動スクリプトでは、これらのフラグごと抜け落ちていることがあります。
3. --max-lora-rank がadapterのrankより小さい
Unslothでは r=16、r=32、r=64 あたりで学習することが多いです。一方、vLLMの --max-lora-rank のデフォルトは 16。adapterのrankが32なのにサーバー側の上限が16だと、ロード時にエラーになるか、あるいはadapterが静かにクリップされて効果が大幅に薄まってしまいます。--max-lora-rank は必ず、学習時のrankと同じ値(か、それ以上)に設定してください。
4. adapterが、vLLMでは適用されないモジュールを対象にしている
これはUnsloth特有の、見落としやすい落とし穴です。Unslothのデフォルトでは target_modules に embed_tokens や lm_head が含まれることがあります。ところがvLLMのLoRAランタイムは、デフォルトでは標準的なprojection層(q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_proj)にしかadapterを適用しません。ファインチューニングの効きの大部分がembeddingやLM-head層に乗っていた場合、vLLMはadapterをロードしてprojection側のdeltaは適用するものの、embedding/head側のdeltaは 黙って捨てます。その結果、出力はほとんど変化しません。これはUnsloth adapterに関するvLLMのissueスレッド(例:#41754)で報告されている根本原因です。
対処1:adapterを正しくサーブする
LoRAを有効にし、明示的な名前でadapterを登録した状態でvLLMを起動します。
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 が含まれている必要があります。もしそこにmergedモデルのshardしか見当たらないなら、エクスポートするものを間違えています(対処3を参照)。
対処2:すべてのリクエストでadapterを指定する
登録が済むと、adapter名がモデル一覧に現れます。まず確認しましょう。
curl http://localhost:8000/v1/models
# You should see BOTH "google/gemma-4-26b" and "my-gemma-lora"そのうえで、modelにはbaseではなく adapter名 を指定します。
r = client.chat.completions.create(
model="my-gemma-lora", # NOT google/gemma-4-26b
messages=[{"role": "user", "content": "..."}],
)/v1/models の一覧に my-gemma-lora が出てこない場合、サーバーがそもそも登録できていません。対処1に戻って、--lora-modules のパスを確認してください。
対処3:merge_and_unloadという選択肢
adapterがembeddingやLM-head層を対象にしている場合、あるいはvLLMのランタイムLoRAの制約と格闘するのをそもそもやめたい場合は、サーブする前にadapterをbase weightsへマージしてしまいます。mergedモデルにはランタイム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で学習した場合は、1ステップでマージできます。
model.save_pretrained_merged(
"/path/to/gemma4-merged", tokenizer, save_method="merged_16bit"
)あとは、mergedディレクトリを通常のモデルとしてサーブするだけ。--enable-lora も、リクエストごとのadapter名指定も不要です。
vllm serve /path/to/gemma4-merged \
--host 0.0.0.0 --port 8000 --dtype bfloat16トレードオフとしては、mergedモデルはbase model分のVRAMをフルに消費し、adapterのホットスワップもできなくなります。ただ、本番でファインチューニングを1つだけ動かすなら、これがもっとも確実なやり方で、「adapterが無視される」という失敗モード自体が起こり得なくなります。フルのmergedコピーを置くだけのVRAMに余裕があるかは、ハードウェアガイドで確認しておきましょう。
adapterが実際に効いているか検証する
どの対処を適用したあとも、「エラーなく起動した」を鵜呑みにせず、ちゃんと反映されたかを確認してください。
- 起動ログを確認する。 登録に成功すると、vLLMは
Loaded LoRA adapter my-gemma-loraのような行をログに出します。この行が出ていなければ、adapterは効いていません。 - 出力を見比べる。 この記事の冒頭で紹介した2モデル比較をもう一度実行します。adapter名のほうで出力が変わっていれば成功です。
- カナリア(目印)を仕込む。 決まったフレーズや書式の癖など、ごく小さくて見分けやすい挙動を学習させておけば、
temperature=0で一目見ただけでbaseとadapterを判別できます。
サーバーをコンテナ化している場合は、これらのチェックをDocker healthcheckに組み込んでおきましょう。そうすれば、adapterの設定ミスがあったときに、base weightsを黙って本番投入してしまう代わりに、デプロイそのものが落ちてくれます。
FAQ
なぜvLLMはadapterをロードするのに、推論では無視するのですか?
ロードと適用が別々のステップだからです。vLLMはadapterを問題なく登録しますが、実際に適用されるのは model としてadapterを指定したリクエストに限られ、しかもサポートしている層だけです。base名に当たったリクエストや、未サポートの層に乗ったdeltaは、エラーも出さずにスキップされます。
rankは本当に一致させる必要がありますか?
--max-lora-rank は、adapterのrank以上でなければなりません。低すぎる値にした場合は、数少ないエラーを実際に投げるケースのひとつです。ただし、vLLMのバージョンによってはエラーではなくクランプ処理を行い、その結果として効果の薄い、部分的に無視されたadapterになってしまいます。
いつもマージしておけばいいのでは?
ファインチューニングを1つだけサーブしていて、VRAMにも余裕があるなら、merge_and_unload でのマージがもっともシンプルで確実な選択肢です。ランタイムLoRAは、1つのbase modelに多数のadapterをホットスワップしたいときのために取っておきましょう。
Transformersでは動くのにvLLMでは動きません。なぜですか?
TransformersはLoRAを embed_tokens や lm_head を含むすべてのtarget moduleに適用します。一方、vLLMのデフォルトのランタイムはそれをしないため、それらの層に依存したadapterは挙動がずれます。マージすれば、この差異はなくなります。
次のステップ
- サービングやスケーリングの設定については、vLLM本番デプロイガイドの全体像をご覧ください。
- 最初から
target_modulesをvLLMフレンドリーに保つコツは、ファインチューニングガイドで解説しています。 - ほかのデプロイの詰まりどころは? Gemma 4トラブルシューティングガイドで、OOM・起動の遅さ・quantization周りの問題を扱っています。
Stop reading. Start building.
~/gemma4 $ Get hands-on with the models discussed in this guide. No deployment, no friction, 100% free playground.
Launch Playground />


