Fine-Tuning Llama 3: Complete QLoRA Training Pipeline

スポンサーリンク

Fine-Tuning Llama 3: Complete QLoRA Training Pipeline

  • 4-bit Quantization
  • Assistant-Only Masking
  • Fine-Tuning LLMs
  • Hugging Face
  • LLaMA 3
  • Llama Fine-Tuning
  • LoRA
  • Model Training
  • Parameter-Efficient Tuning
  • PEFT
  • QLoRA
  • SAMSum
  • Supervised Fine-Tuning
  • Training Pipeline
  • Transformers
  • Wasif Mehmood
  • Ready Tensor
  • Mohamed Abdelhamid

🏠
Home – All Lessons

⬅️
Previous – Fine-Tuning GPT Models

➡️
Next – Fine-Tuning LLMs on RunPod

このレッスンでは、QLoRA — 4-bit量子化とLoRAアダプターの組み合わせを使って、SAMSum対話要約データセットでLlama 3.2 1B-Instructをトレーニングします。

以前のレッスンで構築したすべてを再利用します: データセットのロード、設定管理、評価関数。
次にトレーニングパイプラインを追加します — マスキングロジック、Trainerのセットアップ、モデルチェックポイント作成。

最後には、再現可能なトレーニングスクリプト、LoRAアダプターのセット、ベースラインに対する測定可能なパフォーマンス向上が得られます。

目標についての注記: このレッスンは、メカニクスを正しく理解することに焦点を当てています — エンドツーエンドで動作するパイプラインの構築。まだピークパフォーマンスの最適化は行いません。それは次で、実験追跡とハイパーパラメーター検索を追加するときです。

ベースラインからカスタムモデルのトレーニングへ

これまでに、SAMSumをロード・キャッシュし、ベースモデルで推論を実行し、ベースライン評価のROUGEメトリクスを計算しました。

今度はこれらのコンポーネントを再利用し、新しいことに焦点を当てます — モデルのファインチューニング。

このワークフローは、ノートブックではなくモジュール式のPythonスクリプトで実装されています。スケールし、実行間で再現可能であるように設計されています。

このレッスンでは:

  • アシスタントのみのマスキングを適用して要約にトレーニングを集中
  • QLoRA固有の設定でTrainer APIを設定
  • 最初の完全なトレーニングループを実行
  • LoRAアダプターを保存して評価

QLoRA Training Pipeline: 全体像

トレーニングの背後にあるツール

このワークフローは、以前のレッスンで使用してきた同じスタックに依存しています:

  • transformers — モデルとトークナイザーをロードし、Trainer APIを介してトレーニングループを処理
  • PEFT — パラメーター効率的なファインチューニングのためのLoRA/QLoRA機能を提供
  • bitsandbytes — 4-bit量子化と8-bitオプティマイザーを可能にする
  • datasets — SAMSumのロード、キャッシング、前処理を処理
  • accelerate — デバイス配置と分散トレーニングセットアップを管理

これらのツールはすでに評価に使用しています。今はトレーニングに使用します。

🎥 QLoRA Fine-Tuning Pipeline Overview

このビデオでは、トレーニングと評価ワークフローの両方を含む、QLoRAファインチューニングパイプラインの完全な高レベルの理解が得られます。この概要は、実装の詳細に迷わずにコードリポジトリをナビゲートするのに役立ちます。

🎥 QLoRA Preprocessing: Visual Walkthrough

このビデオでは、各変換ステップでの出力を調べることで、前処理中にデータに何が起こるかを正確に見ることができます。概念を明確で実用的にするために、具体的な例を使ってメカニクスを実演します。

🎥 QLoRA Training: From Setup to Execution

このビデオでは、量子化とLoRAアダプターを使ったモデルのセットアップ方法、効率的なトレーニングのための動的パディングの実装、HuggingFace Trainerを使ったファインチューニングプロセスの実行方法を学びます。

🎥 QLoRA Evaluation: Measuring Fine-Tuning Success

このビデオでは、アダプターをロードし、予測を生成し、ROUGEスコアを計算してベースラインモデルに対する改善を測定することで、ファインチューニング済みQLoRAモデルを評価する方法を学びます。

再利用するコンポーネント

以前のレッスンで構築したこれらのユーティリティが、今まとまります:

config.yaml

これらはデータセット、設定、モデルのセットアップを処理します。トレーニングロジックに純粋に焦点を当てます。

Configuration: トレーニングの青写真

トレーニングコードに入る前に、それを駆動するものを見てみましょう — config.yamlファイルです。

以前のレッスンでデータセットセクションをすでに見ました。今度は設定の残りの部分を使用します: 量子化設定、LoRAパラメーター、トレーニングハイパーパラメーター。

完全な設定は以下の通りです:

# ==============================================
# Model Configuration
# ==============================================
base_model: meta-llama/Llama-3.2-1B-Instruct
tokenizer_type: meta-llama/Llama-3.2-1B-Instruct

# ==============================================
# Dataset
# ==============================================
dataset:
  name: knkarthick/samsum
  cache_dir: ../data/datasets
  field_map:
    input: dialogue
    output: summary
  type: completion
  splits:
    train: all
    validation: 200
    test: 200
  seed: 42

task_instruction: >
  You are a helpful assistant who writes concise, factual summaries of conversations.
  Summarize the following conversation into a single sentence.

# ==============================================
# Quantization (for QLoRA)
# ==============================================
load_in_4bit: true
bnb_4bit_quant_type: nf4
bnb_4bit_use_double_quant: true
bnb_4bit_compute_dtype: bfloat16

# ==============================================
# LoRA Configuration
# ==============================================
lora_r: 8
lora_alpha: 16
lora_dropout: 0.1
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj"]

# ==============================================
# Training Configuration
# ==============================================
num_epochs: 1
max_steps: 400
learning_rate: 2e-5
batch_size: 4
gradient_accumulation_steps: 4
sequence_len: 512
lr_scheduler: cosine
warmup_steps: 50
bf16: true
logging_steps: 25
save_steps: 100
save_total_limit: 2
optim: paged_adamw_8bit

# ==============================================
# Output & Logging
# ==============================================
output_dir: ./outputs/lora_samsum
wandb_project: llama3_samsum
wandb_run_name: lora-finetuning-default-hps

このレッスンで使用する新しいセクションを分解してみましょう。

量子化設定

これらはモデルがメモリにロードされる方法を制御します:

load_in_4bit: true
bnb_4bit_quant_type: nf4
bnb_4bit_use_double_quant: true
bnb_4bit_compute_dtype: bfloat16

これが何をするか: NormalFloat4 (nf4)量子化を使用して、モデルを4-bit精度でロードします。モデルはトレーニング中に4-bit重みを使用しますが、安定性のためにbfloat16で計算します。二重量子化は、量子化定数自体を圧縮してメモリをさらに節約します。

なぜ重要か: フル精度の1Bモデルには約4GBのメモリが必要です。4-bit量子化により、約1GBに収まります — コンシューマーGPUでのファインチューニングが可能になります。

LoRA Configuration

これらはアダプターパラメーターを定義します:

lora_r: 8
lora_alpha: 16
lora_dropout: 0.1
target_modules: ["q_proj", "v_proj", "k_proj", "o_proj"]

これが何をするか: クエリ、キー、値、出力投影レイヤー(つまりLLaMAモデルのすべてのアテンションブロック)にランク8の低ランクアダプターを注入します。alpha/r比(16/8= 2)はLoRA更新のスケーリングを制御します。Dropoutは過学習の防止に役立ちます。

なぜ重要か: 10億個のパラメーターすべてを更新する代わりに、アダプター内の約100〜200万個のパラメーターのみをトレーニングします。これがファインチューニングを高速でメモリ効率的にするものです。

トレーニングハイパーパラメーター

これらは最適化プロセスを制御します:

num_epochs: 1
learning_rate: 2e-4
batch_size: 4
gradient_accumulation_steps: 4
sequence_len: 512
lr_scheduler: cosine
warmup_steps: 50

これが何をするか: 学習率2e-4で1エポックトレーニングし、実効バッチサイズ16(4 × 4勾配蓄積)を使用します。シーケンスは512トークンに制限されます。cosineスケジューラーは50ウォームアップステップ後に学習率を徐々に減少させます。

なぜ重要か: これらはほとんどの指示ファインチューニングタスクでうまく機能する保守的な設定です。後で実験できますが、確実な出発点です。

メモリ最適化

2つの設定がメモリ使用量を低く保ちます:

bf16: true
optim: paged_adamw_8bit

BFloat16トレーニングは、活性化と勾配に16-bit精度を使用します — 品質損失を最小限に抑えながら32-bitよりもはるかに高速です。

Paged AdamW 8-bitは、8-bit精度を使用し、必要に応じてオプティマイザー状態をCPUにページングすることで、オプティマイザー状態メモリを削減します。

これらを組み合わせることで、16GB以下のGPUでトレーニングできます。

全体像: この設定により、最小限のメモリ要件と堅実なベースラインハイパーパラメーターを備えた本番環境対応のトレーニングセットアップが得られます。すべてがYAMLで公開されているため、コードに触れることなく後で実験できます。

では、トレーニングスクリプトがこれらの設定をどのように使用するかを見てみましょう。

Fine-Tuning Implementation – 主要コンポーネント

メインのファインチューニングロジックは、トレーニングスクリプトに存在します。新しいのは:

  • train_model() — Trainer APIを設定し、トレーニングを実行し、LoRAアダプターを保存
    Trainer
  • main() — 設定をロード、モデル、データセットを初期化し、トレーニングを開始

それぞれを見ていきましょう。

1️⃣ アシスタントのみのマスキング

指示データで効果的にトレーニングするには、アシスタントのトークンのみで損失を計算する必要があります — この場合は要約です。

なぜ? ユーザーの対話を予測することを学習するためにモデルの容量を無駄にしたくないからです。高品質な要約の生成に焦点を当ててほしいのです。

コードでの動作は以下の通りです:

def preprocess_samples(examples, tokenizer, task_instruction, max_length):
    """Tokenize dialogues and apply assistant-only masking for causal LM."""
    input_ids_list, labels_list, attn_masks = [], [], []

    for d, s in zip(examples["dialogue"], examples["summary"]):
        # Build message sequences
        msgs_full = build_messages_for_sample({"dialogue": d, "summary": s}, task_instruction, include_assistant=True)
        msgs_prompt = build_messages_for_sample({"dialogue": d, "summary": s}, task_instruction, include_assistant=False)

        text_full = tokenizer.apply_chat_template(msgs_full, tokenize=False, add_generation_prompt=False)
        text_prompt = tokenizer.apply_chat_template(msgs_prompt, tokenize=False, add_generation_prompt=True)
        prompt_len = len(text_prompt)

        tokens = tokenizer(
            text_full,
            max_length=max_length,
            truncation=True,
            padding="max_length",
            add_special_tokens=False,
            return_offsets_mapping=True,
        )

        # Find where the assistant response starts by matching character positions
        # offset_mapping tells us which character positions each token came from
        # We find the first token that starts at or after prompt_len
        start_idx = next((i for i, (start, _) in enumerate(tokens["offset_mapping"]) if start >= prompt_len), len(tokens["input_ids"]))

        # Mask everything before the assistant response with -100 (ignored in loss)
        labels = [-100] * start_idx + tokens["input_ids"][start_idx:]

        input_ids_list.append(tokens["input_ids"])
        labels_list.append(labels)
        attn_masks.append(tokens["attention_mask"])

    return {"input_ids": input_ids_list, "labels": labels_list, "attention_mask": attn_masks}

重要な洞察: offset_mappingは、各トークンがどの文字位置から来たかを示します。完全な会話をトークン化し、文字列内でアシスタントの応答がどこから始まるかを見つけ、その点より前のすべてを-100(損失関数が無視する)でマスクします。

offset_mapping
-100

これにより、モデルは要約を生成することを学習します — ユーザーの対話を予測するために容量を無駄にしません。

モデルは本番環境でユーザー入力を生成することはありません。アシスタントの応答のみを生成します。

ユーザーテキストを予測するようにトレーニングすると、容量を無駄にし、実際にパフォーマンスを損なう可能性があります — モデルは決して使用しないパターンを学習し、簡潔な要約の代わりに対話構造を模倣した要約を生成し始める可能性があります。

マスキングは、モデルが実際に行うこと: 要約の生成、にすべての学習を集中させることでこれを修正します。

2️⃣ Training Configuration

次にTrainerを設定します。ここでバッチサイズ、学習率、オプティマイザー設定、ロギングを指定します。

def train_model(cfg, model, tokenizer, train_data, val_data):
    """Tokenize datasets, configure Trainer, and run LoRA fine-tuning."""
    task_instruction = cfg["task_instruction"]

    print("\n📚 Tokenizing datasets...")
    tokenized_train = train_data.map(
        lambda e: preprocess_samples(e, tokenizer, task_instruction, cfg["sequence_len"]),
        batched=True,
        remove_columns=train_data.column_names,
    )
    tokenized_val = val_data.map(
        lambda e: preprocess_samples(e, tokenizer, task_instruction, cfg["sequence_len"]),
        batched=True,
        remove_columns=val_data.column_names,
    )

    collator = DataCollatorForLanguageModeling(tokenizer=tokenizer, mlm=False)

    args = TrainingArguments(
        output_dir="./outputs/lora_samsum",
        num_train_epochs=cfg["num_epochs"],
        per_device_train_batch_size=cfg["batch_size"],
        gradient_accumulation_steps=cfg["gradient_accumulation_steps"],
        learning_rate=cfg["learning_rate"],
        lr_scheduler_type=cfg.get("lr_scheduler", "cosine"),
        warmup_steps=cfg.get("warmup_steps", 100),
        bf16=cfg.get("bf16", True),
        optim=cfg.get("optim", "paged_adamw_8bit"),
        logging_steps=cfg.get("logging_steps", 25),
        save_total_limit=cfg.get("save_total_limit", 2),
        report_to="wandb",
    )

    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=tokenized_train,
        eval_dataset=tokenized_val,
        data_collator=collator,
    )

    print("\n🎯 Starting LoRA fine-tuning...")
    trainer.train()
    print("\n✅ Training complete!")

    save_dir = "./outputs/lora_samsum/lora_adapters"
    model.save_pretrained(save_dir)
    tokenizer.save_pretrained(save_dir)
    print(f"💾 Saved LoRA adapters to {save_dir}")

注目すべきいくつかの設定:

  • bf16=True はBFloat16トレーニングを有効にし、精度をあまり失わずにメモリ効率を実現
  • paged_adamw_8bit はオプティマイザーメモリフットプリントを削減 — 限られたVRAMで作業するときに重要
  • gradient_accumulation_steps は、重みを更新する前に複数のフォワードパスで勾配を蓄積することで、より大きなバッチサイズをシミュレート

残りは標準的なHugging Face Trainer設定です。トレーニングループ、ロギング、チェックポイント作成を自動的に処理します。

3️⃣ トレーニングの実行

すべてがmain()エントリーポイントでまとまります:

def main():
    cfg = load_config()

    # Load dataset
    train_data, val_data, _ = load_and_prepare_dataset(cfg)

    # Initialize quantized model with LoRA adapters
    model, tokenizer = setup_model_and_tokenizer(cfg, use_4bit=True, use_lora=True)

    # Train
    train_model(cfg, model, tokenizer, train_data, val_data)

設定をロードし、データを準備し、4-bit量子化とLoRAでモデルを初期化し、トレーニングを開始します。

ファインチューニング済みモデルの評価

トレーニングが終了したら、LoRAアダプターを評価して改善を測定したいと思うでしょう。

評価プロセスは、ベースモデルで行ったこととほぼ同じです — ただし、今度はベースモデルの上にファインチューニング済みアダプターをロードしています。

動作は以下の通りです:

def evaluate_lora_model(cfg):
    """Load base model, attach LoRA adapters, and evaluate."""

    # Load base model (same as baseline evaluation)
    model, tokenizer = setup_model_and_tokenizer(cfg, use_4bit=True, use_lora=False)

    # Attach your fine-tuned LoRA adapters
    adapter_dir = "./outputs/lora_samsum/lora_adapters"
    model = PeftModel.from_pretrained(model, adapter_dir)
    model.eval()

    # Load validation data
    _, val_data, _ = load_and_prepare_dataset(cfg)

    # Generate predictions (same inference function as before)
    preds = generate_predictions(
        model=model,
        tokenizer=tokenizer,
        dataset=val_data,
        task_instruction=cfg["task_instruction"],
        batch_size=cfg.get("eval_batch_size", 4),
    )

    # Compute ROUGE scores (same as before)
    scores = compute_rouge(preds, val_data)

    return scores, preds

ベースライン評価との唯一の違いは、この行です:

model = PeftModel.from_pretrained(model, adapter_dir)

4-bit精度でベースモデルをロードし、次にPeftModel.from_pretrained()を使ってトレーニング済みアダプターをアタッチします。それ以外のすべて — データセットのロード、推論、ROUGE計算 — は、以前に構築したまったく同じユーティリティを再利用します。

PeftModel.from_pretrained()

この評価を実行すると、上記の表で示した結果が表示されます: ベースモデルに対してROUGE-Lが4.37%向上します。

Results: Llama 3.2 1B on SAMSum

QLoRA + LoRAアダプターでファインチューニングした後、モデルは検証分割(200サンプル)で以下のROUGEスコアを達成しました:

ROUGE-Lスコアは27.24%から39.13%に改善し、11.89%の改善です。

単一のGPUでパラメーター効率的な手法を使ってトレーニングされたファインチューニング済み1Bパラメーターモデルは、ベースバージョンを大きなマージンで上回りました。

これまでの結果の要約

すべてのモデルの比較は以下の通りです:

ファインチューニング済みLlama 3.2 1Bは、1桁以上小さいにもかかわらず、すべてのROUGEメトリクスでベースGPT-4o-miniを上回りました。

この改善は、単一のGPUでのパラメーター効率的なファインチューニングが、はるかに大きなベースラインのパフォーマンスに匹敵し、さらに上回る軽量モデルをどのように生み出すかを強調しています。

一息つきましょう

先に進む前に、少し立ち止まってください。
あなたが今行ったことは些細なことではありません — これは本物のマイルストーンです。

モデルを実行しただけではありません; 教えたのです。
ベースLlama 3.2 1Bを取り、会話の簡潔で正確な要約を生成するようにトレーニングしました — 以前はほとんどできなかったことです。

これは小さなステップのように感じるかもしれませんが、そうではありません。

自分のデータセットで10億パラメーターモデルをファインチューニングしたばかりです — そうすることで、言語モデル自体のより深い可能性を解き放ちました。

ここで、単にAIを使用することから、それを前進させることへと移行します — 自分のデータと設計を通じて、その能力、行動、結果を形作ります。

ここから先、実行するすべての実験はこの基盤の上に構築されます — 真の制御、創造性、エンジニアリングの洞察の基盤。

次のステップ

完全なファインチューニングパイプラインを構築しました — データのロードとマスキングの適用から、QLoRAでのモデルのトレーニングと評価まで。
今度はこのパイプラインを大規模に実行する時です。

これまでは、ローカルまたはColabでトレーニングしてきましたが、GPUメモリとランタイムの制限により速度が低下する可能性があります。
次のレッスンでは、ワークフローをRunPodに移します。RunPodは、環境を完全に制御できるクラウドGPUプラットフォームです。

学ぶことは:

  • RunPodインスタンスのセットアップと接続
  • ファインチューニングコードとデータの転送
  • 強力なGPUでのトレーニング実行の起動
  • 進捗の監視とコストの効率的な管理

トレーニングパイプラインがクラウドでスムーズに実行されるようになったら、さらに進む準備が整います — Weights & Biases (W&B)を使った実験追跡、再現性、ハイパーパラメーターチューニングの追加。

  • Building the Complete QLoRA Training Pipeline

コメント

タイトルとURLをコピーしました