2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

LoRA・QLoRA・Full Fine-tuningの選定と実装ガイド

2
Posted at

LoRA・QLoRA・Full Fine-tuningの選定と実装ガイド

この記事でわかること

  • Full Fine-tuning・LoRA・QLoRAの3手法の技術的な違いとGPUメモリ・精度・速度のトレードオフ
  • LoRAのハイパーパラメータ(rank, alpha, target modules)の選び方と実験に基づく指針
  • Hugging Face PEFT + TRLを使った実装コード(LoRA / QLoRA両対応)
  • DoRAなどLoRA派生手法の位置づけと使い分け
  • 本番環境でのアダプタデプロイ戦略(マージ vs 動的ロード)

対象読者

  • 想定読者: LLMのFine-tuningをこれから始める、または手法選定に迷っているMLエンジニア
  • 必要な前提知識:
    • PyTorchの基本的な使い方(torch.nn.ModuleTrainerクラスなど)
    • Transformerアーキテクチャの基礎(Attention、線形層の役割)
    • Hugging Face transformersライブラリの基本操作

結論・成果

GPU予算とタスク要件で手法を選べば、7Bモデルでも6〜120GBの範囲でVRAM使用量をコントロールできます。 Sebastian Raschkaの数百回に及ぶ実験(出典)によると、LoRA(r=256, alpha=512, 全線形層適用)はFull Fine-tuningと同等以上の精度を約17GBのVRAMで達成しています。QLoRAはさらにメモリを33%削減し約14GBで動作しますが、訓練速度は39%低下します。

以下の表が手法選定の出発点になります。

手法 7B VRAM目安 Full FT比の品質 訓練速度 主な用途
Full Fine-tuning 60〜120 GB 100%(基準) 基準 最高精度が必要な場合
LoRA 16〜21 GB 90〜95% 基準の約0.5倍 実験・プロダクション両方
QLoRA 6〜14 GB 80〜90% 基準の約0.36倍 GPU制約が厳しい環境

Full Fine-tuning・LoRA・QLoRAの仕組みを理解する

3つの手法は「モデルのどの部分を、どの精度で更新するか」が異なります。ここではそれぞれの原理を整理します。

Full Fine-tuningの仕組み

Full Fine-tuningは事前学習済みモデルの全パラメータを対象データで更新する手法です。7Bモデルの場合、約70億個のパラメータすべてに勾配を計算し、オプティマイザの状態(AdamWならパラメータ数×2のモーメンタム)も保持する必要があります。

# Full Fine-tuningの概念コード
from transformers import AutoModelForCausalLM, TrainingArguments, Trainer

model = AutoModelForCausalLM.from_pretrained("meta-llama/Llama-3.1-8B")

# 全パラメータが学習対象(requires_grad=True)
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
print(f"学習対象パラメータ: {trainable_params:,}")  # 約80億

training_args = TrainingArguments(
    output_dir="./full-ft-output",
    per_device_train_batch_size=1,  # VRAMが足りなければ1
    gradient_accumulation_steps=16,
    learning_rate=2e-5,
    num_train_epochs=1,  # 過学習防止のため1エポックが基本
    bf16=True,
    gradient_checkpointing=True,  # メモリ節約
)

trainer = Trainer(model=model, args=training_args, train_dataset=dataset)
trainer.train()

メモリの内訳(7Bモデル、bf16の場合):

  • モデルパラメータ: 約14 GB(7B × 2バイト)
  • 勾配: 約14 GB
  • オプティマイザ状態(AdamW): 約28 GB(パラメータ×4バイト×2)
  • 合計: 約56 GB(+活性化メモリでさらに増加)

なぜFull Fine-tuningを選ぶか:

  • タスク固有の精度が最優先で、GPU予算に制約がない場合
  • ドメイン特化モデルを作り込む場合(医療、法律など)

制約条件:

Full Fine-tuningは7Bモデルでも実質的にA100 80GB×2台以上が必要です。H100を使っても1回の学習で数百ドルのコストがかかるため、実験の反復には向きません。

LoRAの仕組み

LoRA(Low-Rank Adaptation)は、事前学習済みモデルの重みを凍結し、各線形層に小さな低ランク行列ペア(A, B)を挿入する手法です。元の重み行列W(d×d)に対して、ΔW = B×A(d×r と r×d、r << d)だけを学習します。

核心的なアイデアは、Fine-tuning時の重み更新ΔWが低ランク構造を持つという仮説です。例えばrank=16であれば、7Bモデルの学習対象パラメータは全体の0.1〜0.3%程度(約400万〜2000万パラメータ)に抑えられます。

import torch
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    torch_dtype=torch.bfloat16,
    device_map="auto",
)

# LoRA設定
lora_config = LoraConfig(
    r=64,                          # ランク: 低ランク行列の次元
    lora_alpha=128,                # スケーリング係数(alpha/r で適用)
    target_modules="all-linear",   # 全線形層に適用
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# 例: trainable params: 83,886,080 || all params: 8,113,311,744 || trainable%: 1.034%

なぜ全線形層に適用するのか:

Raschkaの実験(出典)によると、従来のQuery・Value行列のみへの適用(学習パラメータ約420万)に対し、全線形層への適用(約2030万パラメータ)は性能を大幅に向上させます。メモリ増加は14.18 GB→16.62 GBと軽微です。

QLoRAの仕組み

QLoRA(Quantized LoRA)は、LoRAの技術にさらに3つのイノベーションを加えた手法です。

  1. 4-bit NormalFloat(NF4)量子化: 正規分布に最適化された4ビットデータ型で事前学習済みモデルを圧縮
  2. Double Quantization: 量子化の定数自体も量子化し、メモリをさらに削減(パラメータあたり約0.37ビット節約)
  3. Paged Optimizers: GPU↔CPUのメモリスワップでOOMを防止
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
import torch

# QLoRA用の4bit量子化設定
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NormalFloat4を使用
    bnb_4bit_compute_dtype=torch.bfloat16, # 計算はbf16で実行
    bnb_4bit_use_double_quant=True,       # Double Quantization有効化
)

model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto",
)

# LoRAアダプタの設定はLoRAと同じ
lora_config = LoraConfig(
    r=64,
    lora_alpha=128,
    target_modules="all-linear",
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

QLoRAのメモリ内訳(7Bモデルの場合):

  • ベースモデル(4-bit): 約3.5 GB(7B × 0.5バイト)
  • LoRAアダプタ(bf16): 約0.1〜0.5 GB
  • オプティマイザ + 活性化: 約2〜10 GB
  • 合計: 約6〜14 GB

QLoRAは量子化→逆量子化→計算→逆量子化のオーバーヘッドにより、LoRAと比べて約39%の訓練速度低下が報告されています(出典)。推論時の速度は影響を受けません(アダプタをマージして通常精度に戻すため)。

LoRAハイパーパラメータの選び方を実験データから学ぶ

LoRAの性能はハイパーパラメータに大きく依存します。ここでは数百回の実験結果に基づく知見を整理します。

rankの選び方

rankはLoRAの表現力を直接制御するパラメータです。大きいほど表現力が増しますが、パラメータ数・メモリ使用量も増加します。

rank 学習パラメータ(7B全線形層) VRAMの目安 推奨シナリオ
8 約800万 14〜15 GB 簡単なタスク、高速実験
16 約1600万 15〜16 GB 一般的な開始点
64 約6400万 16〜18 GB 中〜高複雑度タスク
128 約1.3億 17〜20 GB 高複雑度タスク
256 約2.6億 18〜22 GB 最高精度が必要な場合

Raschkaの実験(出典)では、r=256が多くのベンチマークで最高スコアを記録しました。しかし、rankを上げすぎると過学習のリスクがあるため、バリデーションセットでの評価が必須です。

実践的な手順:

  1. r=16から開始し、ベースラインを確立
  2. r=64に上げて精度向上を確認
  3. 精度が飽和するポイントを見つける(多くの場合r=64〜128)
  4. バリデーション損失が上昇し始めたらrankを下げる

alphaの設定

alphaはLoRAの更新をスケーリングする係数です。実際の更新は ΔW × (alpha / rank) としてスケーリングされます。

基本ルール: alpha = 2 × rankLightning AIの実験で推奨)

ただし、Raschkaの実験ではr=256, alpha=128(alpha = 0.5 × rank)が最高精度を示したケースもあり、タスク依存であることに注意してください。

# 推奨設定パターン
configs = [
    {"r": 16,  "lora_alpha": 32},   # alpha = 2r(基本)
    {"r": 64,  "lora_alpha": 128},  # alpha = 2r(基本)
    {"r": 256, "lora_alpha": 512},  # alpha = 2r(基本)
    {"r": 256, "lora_alpha": 128},  # alpha = 0.5r(高rankで有効な場合も)
]

よくある間違い:

最初はalpha=1に固定してrankだけを上げていましたが、高rankでは収束が不安定になりました。alphaを適切にスケーリングしないと、勾配が極端に小さくなり学習が進みません。alpha/rankの比率を意識することが重要です。

学習率とオプティマイザ

UnslothのハイパーパラメータガイドおよびRaschkaの実験に基づく推奨値です。

パラメータ 推奨値 備考
学習率 1e-4〜3e-4 QLoRAの場合は2e-4が多い
オプティマイザ AdamW(8-bit) SGDとの精度差は小さいがAdamWが安定
スケジューラ cosine SGD使用時は特に有効
エポック数 1〜3 過学習防止のため少なめ
warmup比率 0.03〜0.1 学習率の安定化

過学習への注意:

Raschkaの実験では、50Kサンプルで2エポック以上訓練すると性能が低下しました。特に算術タスクでは「基本的な算数を積極的にアンラーニングしている」兆候が見られたと報告されています。データセットの反復回数は慎重に設定してください。

target_modulesの選択

従来はQuery(q_proj)とValue(v_proj)のみにLoRAを適用するのが一般的でしたが、現在は全線形層への適用が推奨されています。

# 2024年以前の設定(非推奨)
old_config = LoraConfig(
    target_modules=["q_proj", "v_proj"],  # Attention Q/Vのみ
    # ...
)

# 2025年〜の推奨設定
new_config = LoraConfig(
    target_modules="all-linear",  # 全線形層
    # ...
)

Philipp Schmidの2025年ガイド(出典)でも lora_target_modules: "all-linear" が標準構成として採用されています。

SFTTrainerを使った実装の全体像を組み立てる

ここでは、Hugging Face TRL(公式ドキュメント)のSFTTrainerを使った実装例を示します。LoRAとQLoRAの切り替えが最小限のコード変更で可能な構成です。

完全な実装例

# train_lora.py
import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
)
from peft import LoraConfig
from trl import SFTTrainer

# --- 設定 ---
MODEL_NAME = "meta-llama/Llama-3.1-8B-Instruct"
USE_QLORA = True  # False にするとLoRA(16-bit)モードになる
LORA_R = 64
LORA_ALPHA = 128
MAX_SEQ_LENGTH = 2048
OUTPUT_DIR = "./output"

# --- トークナイザの準備 ---
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"

# --- モデルの準備 ---
model_kwargs = {
    "torch_dtype": torch.bfloat16,
    "device_map": "auto",
    "attn_implementation": "flash_attention_2",  # 高速化
}

if USE_QLORA:
    model_kwargs["quantization_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, **model_kwargs)

# --- データセットの準備 ---
dataset = load_dataset("json", data_files="train.jsonl", split="train")

def format_chat(example):
    """チャット形式にフォーマット"""
    messages = [
        {"role": "system", "content": "あなたは有用なアシスタントです。"},
        {"role": "user", "content": example["instruction"]},
        {"role": "assistant", "content": example["output"]},
    ]
    return {"text": tokenizer.apply_chat_template(messages, tokenize=False)}

dataset = dataset.map(format_chat)

# --- LoRA設定 ---
peft_config = LoraConfig(
    r=LORA_R,
    lora_alpha=LORA_ALPHA,
    target_modules="all-linear",
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
)

# --- トレーニング設定 ---
training_args = TrainingArguments(
    output_dir=OUTPUT_DIR,
    per_device_train_batch_size=4,
    gradient_accumulation_steps=4,  # 実効バッチサイズ = 16
    learning_rate=2e-4,
    num_train_epochs=1,
    lr_scheduler_type="cosine",
    warmup_ratio=0.05,
    bf16=True,
    gradient_checkpointing=True,
    gradient_checkpointing_kwargs={"use_reentrant": False},
    logging_steps=10,
    save_strategy="steps",
    save_steps=500,
    optim="adamw_8bit" if USE_QLORA else "adamw_torch",
)

# --- トレーニング実行 ---
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    peft_config=peft_config,
    max_seq_length=MAX_SEQ_LENGTH,
    dataset_text_field="text",
)

trainer.train()
trainer.save_model(OUTPUT_DIR)

ライブラリバージョンPhilipp Schmid 2025ガイドに基づく):

  • transformers>=4.46.3
  • peft>=0.13.2
  • trl>=0.12.1
  • bitsandbytes>=0.44.1
  • accelerate>=1.1.1

Unslothによる高速化

Unslothは、LoRA/QLoRAの訓練を最適化するフレームワークで、カスタムカーネルにより2.7倍の高速化と74%のメモリ削減を報告しています(NVIDIA Blog)。

# Unslothを使った場合の例
from unsloth import FastLanguageModel

model, tokenizer = FastLanguageModel.from_pretrained(
    model_name="unsloth/Meta-Llama-3.1-8B-Instruct",
    max_seq_length=2048,
    load_in_4bit=True,  # QLoRAモード
)

model = FastLanguageModel.get_peft_model(
    model,
    r=64,
    lora_alpha=128,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
    lora_dropout=0,
    bias="none",
)

なぜUnslothを検討すべきか:

  • Hugging Face TRLとの互換性が高く、SFTTrainerをそのまま使える
  • 同じGPUでバッチサイズを増やせるため、実効的なスループットが向上
  • RTX 4090などの消費者GPUでも7Bモデルの学習が現実的になる

注意点:

Unslothは全GPUアーキテクチャに対応しているわけではなく、特にAMD GPUや古いCUDAバージョンでは動作しない場合があります。また、分散学習のサポートはエンタープライズ版に限定されています(2026年3月時点)。

LoRA派生手法を比較し使い分けを判断する

LoRAの登場以降、多くの派生手法が提案されています。ここでは実用上重要なものを整理します。

DoRA(Weight-Decomposed Low-Rank Adaptation)

DoRAはNVIDIA Researchが提案し、ICML 2024でOral採択された手法です。重みの更新を**大きさ(magnitude)方向(direction)**に分解し、方向の更新にLoRAを使います。

DoRAの利点NVIDIA公式ブログ):

  • 低rankでもLoRAより高品質を維持(r=8でLoRAのr=32相当の性能)
  • 推論時のオーバーヘッドなし(LoRAと同様にマージ可能)
  • ドメイン適応(専門用語・文体の変更)で特に有効
# DoRAの設定(PEFT >= 0.13.0で対応)
from peft import LoraConfig

dora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    target_modules="all-linear",
    use_dora=True,  # DoRAを有効化
    task_type="CAUSAL_LM",
)

手法比較表

手法 提案年 精度(低rank) メモリ追加 推論オーバーヘッド 成熟度
LoRA 2021 基準 基準 なし(マージ後) 高(プロダクション実績多数)
QLoRA 2023 LoRA比やや低下 -33% なし(マージ後) 高(広く採用)
DoRA 2024 LoRA比向上 +数% なし(マージ後) 中(PEFT対応済み)
AdaLoRA 2023 LoRA比向上 +10〜20% なし(マージ後)
rsLoRA 2024 高rankで向上 同等 なし(マージ後)

手法選定の指針:

  • まずLoRAから始めて、ベースラインを確立する
  • GPU制約が厳しければQLoRAに切り替え
  • 低rankで精度が不足する場合はDoRAを試す
  • 高rankで性能が飽和する場合はrsLoRAを試す

本番デプロイ戦略を設計する

学習したLoRAアダプタを本番環境にデプロイする方法は、ユースケースに応じて大きく2つに分かれます。

アダプタマージ(単一タスク向け)

アダプタを元のモデルに統合し、通常のモデルとしてデプロイする方法です。

from peft import PeftModel
from transformers import AutoModelForCausalLM

# ベースモデルのロード
base_model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B-Instruct",
    torch_dtype=torch.bfloat16,
)

# アダプタの読み込みとマージ
model = PeftModel.from_pretrained(base_model, "./output")
merged_model = model.merge_and_unload()

# マージ済みモデルの保存
merged_model.save_pretrained("./merged-model")

マージのメリット:

  • 推論時にPEFTライブラリが不要
  • レイヤーごとの追加計算がなくなり、レイテンシが改善
  • 通常のTransformersモデルと同じように扱える

マージのデメリット:

  • モデル全体のサイズ(7Bなら約14GB)が必要
  • 複数のアダプタを切り替えられない

動的アダプタローディング(マルチテナント向け)

ベースモデルを共有し、リクエストに応じてアダプタを動的に切り替える方法です。vLLM、LoRAX、TGI(Text Generation Inference)などの推論フレームワークが対応しています。

# vLLMでのマルチLoRAサービングの概念例
from vllm import LLM, SamplingParams
from vllm.lora.request import LoRARequest

llm = LLM(
    model="meta-llama/Llama-3.1-8B-Instruct",
    enable_lora=True,
    max_lora_rank=64,
)

# リクエストごとに異なるアダプタを指定
output_customer_a = llm.generate(
    "こんにちは",
    sampling_params=SamplingParams(max_tokens=256),
    lora_request=LoRARequest("customer_a", 1, "./adapters/customer_a"),
)

output_customer_b = llm.generate(
    "こんにちは",
    sampling_params=SamplingParams(max_tokens=256),
    lora_request=LoRARequest("customer_b", 2, "./adapters/customer_b"),
)

動的ローディングのメリット:

  • アダプタは数MB〜数十MBで、ベースモデルを共有するためGPUメモリ効率が高い
  • 顧客ごとにパーソナライズされたモデルを提供できる

トレードオフ:

動的ローディングはContinuous Batchingと組み合わせることで効率的に運用できます。ただし、アダプタの切り替え頻度が高すぎると、キャッシュヒット率が下がりスループットが低下する可能性があります。Together AI(出典)は、数百のアダプタを同時にサービングするServerless Multi-LoRAを提供しています。

よくある問題と解決方法

問題 原因 解決方法
OOM(Out of Memory)が発生する バッチサイズが大きすぎる per_device_train_batch_size=1 + gradient_accumulation_stepsを増やす
損失が下がらない alpha/rank比が不適切 alpha = 2 × rank を基本に調整。学習率を1e-4〜3e-4の範囲で試す
検証損失が上昇する 過学習 エポック数を減らす(1〜2)。dropout=0.05〜0.1を追加
QLoRAでNaN損失が出る bf16と4bit量子化の相性問題 bnb_4bit_compute_dtype=torch.float16に変更。gradient_checkpointingを有効化
アダプタマージ後に品質が低下する マージ時の精度落ち model.merge_and_unload(safe_merge=True)を使用。マージ前後で推論結果を比較検証
LoRA適用後もベースモデルと変わらない 学習率が低すぎる、データが少なすぎる 学習率を5e-4まで上げて実験。最低1000サンプル以上のデータを用意

まとめと次のステップ

まとめ:

  • Full Fine-tuningは全パラメータを更新し最高精度だが、7Bモデルで60GB以上のVRAMが必要でコストが高い
  • LoRAは低ランク行列で効率的にFine-tuningし、90〜95%の品質を16〜21GBで達成する
  • QLoRAは4-bit量子化を組み合わせ、6〜14GBまでVRAMを削減するが訓練速度は39%低下する
  • ハイパーパラメータはrank=16〜256、alpha=2×rank、全線形層適用が基本方針
  • DoRAは低rankでの精度向上に有効で、PEFTライブラリでuse_dora=Trueのみで利用可能
  • 本番デプロイはアダプタマージ(単一タスク)と動的ローディング(マルチテナント)を使い分ける

次にやるべきこと:

  • 自身のデータセットでLoRA(r=16)を試し、ベースラインを確立する
  • 精度が不足すればrankを上げるか、DoRAに切り替えて比較する
  • GPU制約がある場合はQLoRAに切り替え、訓練速度と精度のトレードオフを確認する

参考


注意: この記事はAI(Claude Code)により自動生成されました。内容の正確性については複数の情報源で検証していますが、実際の利用時は公式ドキュメントもご確認ください。

2
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?