0% read

vLLMでGemma 4のLoRAが効かない原因と対処法

6月 2, 2026

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=16r=32r=64 あたりで学習することが多いです。一方、vLLMの --max-lora-rank のデフォルトは 16。adapterのrankが32なのにサーバー側の上限が16だと、ロード時にエラーになるか、あるいはadapterが静かにクリップされて効果が大幅に薄まってしまいます。--max-lora-rank は必ず、学習時のrankと同じ値(か、それ以上)に設定してください。

4. adapterが、vLLMでは適用されないモジュールを対象にしている

これはUnsloth特有の、見落としやすい落とし穴です。Unslothのデフォルトでは target_modulesembed_tokenslm_head が含まれることがあります。ところがvLLMのLoRAランタイムは、デフォルトでは標準的なprojection層(q_projk_projv_projo_projgate_projup_projdown_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.jsonadapter_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が実際に効いているか検証する

どの対処を適用したあとも、「エラーなく起動した」を鵜呑みにせず、ちゃんと反映されたかを確認してください。

  1. 起動ログを確認する。 登録に成功すると、vLLMは Loaded LoRA adapter my-gemma-lora のような行をログに出します。この行が出ていなければ、adapterは効いていません。
  2. 出力を見比べる。 この記事の冒頭で紹介した2モデル比較をもう一度実行します。adapter名のほうで出力が変わっていれば成功です。
  3. カナリア(目印)を仕込む。 決まったフレーズや書式の癖など、ごく小さくて見分けやすい挙動を学習させておけば、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_tokenslm_head を含むすべてのtarget moduleに適用します。一方、vLLMのデフォルトのランタイムはそれをしないため、それらの層に依存した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

Related Guides

vLLMでGemma 4のLoRAが効かない原因と対処法 | ブログ