LLMを独自データで追加学習するとき、最初に悩むのが「QLoRAで十分なのか、それともフルファインチューニングが必要なのか」です。
今回、同じモデル・同じデータセット・同じ学習条件にそろえて、QLoRAとフルファインチューニングを比較しました。結論から言うと、一般的な社内文書への回答形式の調整や、特定の書式を覚えさせる用途では、まずQLoRAを選ぶのが現実的です。フルファインチューニングは、モデル全体の挙動を大きく変えたい場合や、十分なGPU資源と運用体制がある場合に検討するのがよいと感じました。
比較した条件
今回は7Bクラスの因果言語モデルを使い、学習データはinstruction形式に統一しました。QLoRAとフルファインチューニングで、できるだけ条件が変わらないようにしています。
{"instruction":"次の文章を要約してください。","input":"製品の利用規約に関する文章です。","output":"利用規約の要点をまとめると、ユーザーは規約に同意した上でサービスを利用します。"}
主な条件は次のとおりです。
- 学習エポック数:3
- 最大系列長:2048
- バッチサイズ:1
- gradient accumulation:8
- 学習率:2e-4(QLoRA)、2e-5(フルチューニング)
- データ型:bf16
- オプティマイザ:AdamW系
- 評価用データは学習データと分離
GPUの状態は、実行前に次のコマンドで確認しました。
nvidia-smi
python - <<'PY'
import torch
print("CUDA:", torch.cuda.is_available())
print("GPU:", torch.cuda.get_device_name(0))
print("bf16:", torch.cuda.is_bf16_supported())
PY
QLoRAの構成
QLoRAでは、ベースモデルを4bit量子化して読み込み、LoRAアダプターだけを学習します。ベースモデルのパラメータは固定されるため、勾配やオプティマイザ状態を持つパラメータ数を大幅に減らせます。
実験では、bitsandbytesとpeftを使いました。
pip install torch transformers datasets accelerate peft bitsandbytes
最小構成のコードは次のようになります。
import torch
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
)
from peft import LoraConfig, get_peft_model
model_name = "your-model-name"
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True,
)
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=quant_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
lora_config = LoraConfig(
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=[
"q_proj", "k_proj", "v_proj", "o_proj"
],
task_type="CAUSAL_LM",
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
私の環境では、学習対象パラメータが全体のごく一部になっていることを確認できました。QLoRAの学習後に保存されるのも、基本的にはLoRAアダプターです。そのため、チェックポイントの容量が小さく、複数の用途向けにアダプターを分けて管理しやすい点もメリットです。
一方で、4bit量子化による計算処理が入るため、1ステップあたりの処理速度が必ずしもフル精度モデルより速いとは限りません。QLoRAは「メモリを節約できる」手法であり、「常に学習時間も短い」手法ではない点には注意が必要です。
フルファインチューニングの構成
フルファインチューニングでは、モデル全体をbf16などで読み込み、すべてのパラメータを更新します。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
model_name = "your-model-name"
model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
for param in model.parameters():
param.requires_grad = True
trainable = sum(
p.numel() for p in model.parameters() if p.requires_grad
)
total = sum(p.numel() for p in model.parameters())
print(f"trainable: {trainable:,}")
print(f"total: {total:,}")
フルファインチューニングで特に重くなるのは、モデルの重みだけではありません。学習時には、勾配とオプティマイザ状態もGPUメモリを消費します。
7Bパラメータのモデルを例にすると、重みだけでもbf16で約14GBです。ここに勾配、Adam系オプティマイザのモーメント、アクティベーション、CUDAの作業領域が加わります。実際の必要メモリは、系列長やバッチサイズ、実装によって変わりますが、単一GPUではかなり厳しい構成になりやすいです。
モデルを複数GPUに分散する場合は、次のようにaccelerateを使えます。
accelerate config
accelerate launch --num_processes 4 train_full.py \
--model_name your-model-name \
--output_dir ./output-full \
--bf16 \
--per_device_train_batch_size 1 \
--gradient_accumulation_steps 8 \
--learning_rate 2e-5 \
--num_train_epochs 3 \
--gradient_checkpointing
フルファインチューニングでは、保存されるチェックポイントもモデル全体になります。実験を繰り返すほどストレージ使用量と保存時間が増え、チェックポイント管理の負担も大きくなりました。
学習コストを分解して比較する
QLoRAとフルファインチューニングの選択から評価までの流れを図にすると次の通りです。
今回の比較では、単純に「QLoRAのほうが安い」と考えるだけでは不十分だと感じました。コストは、少なくとも次の4つに分けて考える必要があります。
GPUメモリ
最も差が出るのがVRAMです。QLoRAは4bit量子化とLoRAによって、比較的小さいGPUでも学習を開始できます。フルファインチューニングは、モデルの重み以外にも勾配やオプティマイザ状態が必要です。
特に、系列長を2048から4096へ増やすとアクティベーションの負荷が大きくなります。メモリに余裕がない状態で無理にバッチサイズを上げるより、gradient accumulationとgradient checkpointingを使って調整するほうが安定しました。
学習時間
1ステップの速度は、量子化方式、GPU世代、カーネル実装によって変わります。QLoRAは更新対象が少ないため、勾配計算やオプティマイザ更新の負荷を抑えられますが、量子化モデルの計算がボトルネックになる場合もあります。
そのため、実際のコストは次のように測るのがよいです。
/usr/bin/time -v accelerate launch train.py \
--output_dir ./output \
--num_train_epochs 3
確認する項目は、総実行時間だけではありません。
- 1秒あたりの学習サンプル数
- 最大VRAM使用量
- 評価時の損失
- チェックポイント保存時間
- 学習後の推論品質
私の検証では、QLoRAはGPU台数を増やさずに実行しやすく、実験サイクルを回しやすい点が大きな利点でした。
ストレージ
QLoRAはアダプターだけを保存する構成にしやすいため、用途ごとにモデルを分ける場合に管理しやすいです。
model.save_pretrained("./adapter-support")
tokenizer.save_pretrained("./adapter-support")
フルファインチューニングではモデル全体の保存が必要になるため、保存先の容量だけでなく、バックアップや転送時間も考慮する必要があります。
運用と再現性
QLoRAではベースモデルを共通化し、用途ごとにアダプターを切り替えられます。たとえば「社内FAQ用」「要約用」「コードレビュー用」のように、目的別にアダプターを管理できます。
ただし、ベースモデルのバージョンと量子化設定が変わると、同じアダプターでも結果が変わる可能性があります。実験時には、モデル名、コミットハッシュ、量子化設定、LoRAのrやtarget_modulesを記録しておくことが重要です。
どちらを選ぶべきか
今回の結果から、私は次の基準で選ぶことにしました。
小規模なデータセットで、回答形式や文体を調整したい場合はQLoRAです。GPU資源が限られている場合や、複数の実験を短いサイクルで試したい場合にも向いています。
一方、モデル全体の知識表現や挙動を大きく変える必要があり、十分なGPUと保存領域を用意できる場合はフルファインチューニングを検討します。ただし、学習データが少ない状態で全パラメータを更新すると、過学習や元の能力の低下が起きやすくなります。
最初からフルファインチューニングに進むのではなく、まずQLoRAでベースラインを作り、評価データで改善幅を確認する手順が安全です。
まとめ
QLoRAとフルファインチューニングの違いは、単なる精度比較ではなく、GPUメモリ、学習時間、ストレージ、運用負荷を含めた総コストで考える必要があります。
QLoRAは、少ないVRAMで始めやすく、アダプター単位で管理できるのが強みです。フルファインチューニングは自由度が高い反面、必要な計算資源と運用コストが大きくなります。
まずは同じ評価データを用意し、QLoRAで実験する。その結果で限界が見えたときに、フルファインチューニングへ進む。この順番が、現実的な開発サイクルだと感じました。