Llama 3.2-1B を Unsloth + vLLM でファインチューニング & 高速推論

## 📝 はじめに

本記事では、**Unsloth** を使用して **Llama 3.2-1B** をファインチューニングし、**vLLM** を活用して **高速推論** を実現する方法を紹介します。
ps:
実際はInstana＋vLLMでの監視を検証する目的ですが、ついでにFine Tuningまで動作確認しました。


使用する環境:

- **GPU**: NVIDIA® GeForce RTX™ 4090 Laptop GPU (16GB)
- **OS**: Windows 11 with Ubuntu 22.04.5 LTS
- **Python**: 3.10.12 in Ubuntu
- **CUDA**: 12.8
- **ドライバ**: NVIDIA-SMI 570.86.15 / Driver Version: 571.96

---

## 📌 1. 環境構築

### **1.1. Python のセットアップ**

まず、Python 3.10 以上をインストールしてください。

Ubuntu:

```bash
sudo apt update && sudo apt install python3 python3-venv python3-pip -y
```

### **1.2. 仮想環境の作成**

```bash
python3 -m venv llama_env
source llama_env/bin/activate  # Windowsなら `llama_env\Scripts\activate`
```

### **1.3. 必要ライブラリのインストール**

```bash
pip install --upgrade pip
pip install torch unsloth vllm datasets transformers accelerate huggingface_hub fastapi uvicorn wandb
```

### **1.4. Hugging Face の API トークンを設定**

```bash
export HUGGINGFACE_TOKEN="your_huggingface_token"
huggingface-cli login
```

---

## 📌 2. Llama 3.2-1B のファインチューニング

**ファイル名: `trainingtest/train_unsloth.py`**

```python
import os
from unsloth import FastLanguageModel, UnslothTrainer, UnslothTrainingArguments
from datasets import load_dataset

# Hugging Face APIトークンを設定
os.environ["HUGGINGFACE_TOKEN"] = "your_huggingface_token"

# モデル設定
model_name = "meta-llama/Llama-3.2-1B"
max_seq_length = 512

# モデルとトークナイザーのロード
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name=model_name,
    max_seq_length=max_seq_length,
    load_in_4bit=True
)

# LoRAの適用
model = FastLanguageModel.get_peft_model(
    model,
    r=32,
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
        "embed_tokens", "lm_head"
    ],
    lora_alpha=16,
    lora_dropout=0.05,
    bias="none",
    use_gradient_checkpointing=True,
    use_rslora=True
)

# データセットのロードと整形
dataset = load_dataset("kajuma/CC-news-2024-July-October-cleaned", split="train")
dataset = dataset.train_test_split(train_size=0.8)["train"]

EOS_TOKEN = tokenizer.eos_token
def format_text(examples):
    return {"text": [text + EOS_TOKEN for text in examples["text"]]}

dataset = dataset.map(format_text, batched=True)

# 学習設定
training_args = UnslothTrainingArguments(
    per_device_train_batch_size=8,
    gradient_accumulation_steps=1,
    num_train_epochs=1,
    learning_rate=5e-5,
    output_dir="./trainingtest/models/pretrain_output"
)

# 学習の実行
trainer = UnslothTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=training_args
)

trainer.train()

# 学習済みモデルの保存
model.save_pretrained("./trainingtest/models/fine_tuned_llama3_2_1b")
tokenizer.save_pretrained("./trainingtest/models/fine_tuned_llama3_2_1b")

print("学習完了！モデルは ./trainingtest/models/fine_tuned_llama3_2_1b に保存されました。")
```

実行コマンド:
```bash
python ./trainingtest/train_unsloth.py
```
```bash
python ./trainingtest/train_unsloth.py
🦥 Unsloth: Will patch your computer to enable 2x faster free finetuning.
🦥 Unsloth Zoo will now patch everything to make training faster!
==((====))==  Unsloth 2025.2.15: Fast Llama patching. Transformers: 4.49.0.
   \\   /|    GPU: NVIDIA GeForce RTX 4090 Laptop GPU. Max memory: 15.992 GB. Platform: Linux.
O^O/ \_/ \    Torch: 2.5.1+cu124. CUDA: 8.9. CUDA Toolkit: 12.4. Triton: 3.1.0
\        /    Bfloat16 = TRUE. FA [Xformers = 0.0.28.post3. FA2 = True]
 "-____-"     Free Apache license: http://github.com/unslothai/unsloth
Unsloth: Fast downloading is enabled - ignore downloading bars which are red colored!
model.safetensors: 100%|███████████████▉| 1.10G/1.10G [02:32<00:00, 7.21MB/s]
generation_config.json: 100%|███████████████| 230/230 [00:00<00:00, 1.31MB/s]
tokenizer_config.json: 100%|████████████| 50.6k/50.6k [00:00<00:00, 7.77MB/s]
tokenizer.json: 100%|███████████████████| 17.2M/17.2M [00:01<00:00, 9.38MB/s]
special_tokens_map.json: 100%|██████████████| 459/459 [00:00<00:00, 3.41MB/s]
Unsloth: Dropout = 0 is supported for fast patching. You are using dropout = 0.05.
Unsloth will patch all other layers, except LoRA matrices, causing a performance hit.
Unsloth 2025.2.15 patched 16 layers with 0 QKV layers, 0 O layers and 0 MLP layers.
Unsloth: Training embed_tokens in mixed precision to save VRAM
Unsloth: Training lm_head in mixed precision to save VRAM
README.md: 100%|████████████████████████| 1.47k/1.47k [00:00<00:00, 6.39MB/s]
train-00000-of-00005.parquet: 100%|██████▉| 255M/255M [00:34<00:00, 7.45MB/s]
train-00001-of-00005.parquet: 100%|██████▉| 255M/255M [00:36<00:00, 6.98MB/s]
train-00002-of-00005.parquet: 100%|██████▉| 256M/256M [00:33<00:00, 7.59MB/s]
train-00003-of-00005.parquet: 100%|██████▉| 255M/255M [00:36<00:00, 7.03MB/s]
train-00004-of-00005.parquet: 100%|██████▉| 256M/256M [00:36<00:00, 7.07MB/s]
test-00000-of-00001.parquet: 100%|██████| 12.7M/12.7M [00:01<00:00, 6.70MB/s]
Generating train split: 100%|█| 127006/127006 [00:02<00:00, 50016.18 examples
Generating test split: 100%|███| 1283/1283 [00:00<00:00, 37115.94 examples/s]
Map: 100%|█████████████████| 101604/101604 [00:05<00:00, 17252.32 examples/s]
Map (num_proc=32): 100%|████| 101604/101604 [00:46<00:00, 2200.01 examples/s]
==((====))==  Unsloth - 2x faster free finetuning | Num GPUs = 1
   \\   /|    Num examples = 101,604 | Num Epochs = 1
O^O/ \_/ \    Batch size per device = 8 | Gradient Accumulation steps = 1
\        /    Total batch size = 8 | Total steps = 12,701
 "-____-"     Number of trainable parameters = 547,880,960
wandb: WARNING The `run_name` is currently set to the same value as `TrainingArguments.output_dir`. If this was not intended, please specify a different run name by setting the `TrainingArguments.run_name` parameter.
wandb: Using wandb-core as the SDK backend.  Please refer to https://wandb.me/wandb-core for more information.
wandb: (1) Create a W&B account
wandb: (2) Use an existing W&B account
wandb: (3) Don't visualize my results
wandb: Enter your choice: 1
wandb: You chose 'Create a W&B account'
wandb: Create an account here: https://wandb.ai/authorize?signup=true
wandb: Paste an API key from your profile and hit enter, or press ctrl+c to quit:
wandb: No netrc file found, creating one.
wandb: Appending key for api.wandb.ai to your netrc file: /home/xxxxxxx/.netrc
wandb: Currently logged in as: xxxxxxx to https://api.wandb.ai. Use `wandb login --relogin` to force relogin
wandb: Tracking run with wandb version 0.19.7
wandb: Run data is saved locally in /home/xxxxxxx/vllmproject/wandb/run-20250223_195626-otn1xt95
wandb: Run `wandb offline` to turn off syncing.
wandb: Syncing run ./trainingtest/models/pretrain_output
wandb: ⭐️ View project at https://wandb.ai/xxxxxxx/huggingface
wandb: 🚀 View run at https://wandb.ai/xxxxxxx/huggingface/runs/otXXXXt95
  3%|██▋                                                                       3%|█                                 | 384/12701 [04:20<2:13:50,  1.53it/s]
{'loss': 2.2291, 'grad_norm': 3.7912888526916504, 'learning_rate': 4.803165105109834e-05, 'epoch': 0.04}
{'loss': 1.7289, 'grad_norm': 2.735246181488037, 'learning_rate': 4.606330210219668e-05, 'epoch': 0.08}
{'loss': 1.6247, 'grad_norm': 2.260746955871582, 'learning_rate': 4.4094953153295015e-05, 'epoch': 0.12}
{'loss': 1.5721, 'grad_norm': 2.0738134384155273, 'learning_rate': 4.2126604204393355e-05, 'epoch': 0.16}
{'loss': 1.5503, 'grad_norm': 2.5313053131103516, 'learning_rate': 4.0158255255491695e-05, 'epoch': 0.2}
{'loss': 1.4903, 'grad_norm': 2.4872641563415527, 'learning_rate': 3.8189906306590035e-05, 'epoch': 0.24}
{'loss': 1.4908, 'grad_norm': 2.8477089405059814, 'learning_rate': 3.6221557357688374e-05, 'epoch': 0.28}
{'loss': 1.4466, 'grad_norm': 2.0701661109924316, 'learning_rate': 3.4253208408786714e-05, 'epoch': 0.31}
{'loss': 1.4491, 'grad_norm': 1.9617587327957153, 'learning_rate': 3.228485945988505e-05, 'epoch': 0.35}
{'loss': 1.4069, 'grad_norm': 2.2118098735809326, 'learning_rate': 3.0316510510983387e-05, 'epoch': 0.39}
{'loss': 1.4276, 'grad_norm': 2.126932144165039, 'learning_rate': 2.8348161562081727e-05, 'epoch': 0.43}
{'loss': 1.4169, 'grad_norm': 2.4512362480163574, 'learning_rate': 2.6379812613180067e-05, 'epoch': 0.47}
{'loss': 1.3985, 'grad_norm': 2.1004855632781982, 'learning_rate': 2.4411463664278406e-05, 'epoch': 0.51}
{'loss': 1.3823, 'grad_norm': 1.8223148584365845, 'learning_rate': 2.2443114715376743e-05, 'epoch': 0.55}
{'loss': 1.3731, 'grad_norm': 2.264676094055176, 'learning_rate': 2.0474765766475083e-05, 'epoch': 0.59}
{'loss': 1.3653, 'grad_norm': 2.2205936908721924, 'learning_rate': 1.8506416817573423e-05, 'epoch': 0.63}
{'loss': 1.3628, 'grad_norm': 1.9629074335098267, 'learning_rate': 1.653806786867176e-05, 'epoch': 0.67}
{'loss': 1.3455, 'grad_norm': 2.228688955307007, 'learning_rate': 1.4569718919770097e-05, 'epoch': 0.71}
{'loss': 1.365, 'grad_norm': 2.4114575386047363, 'learning_rate': 1.2601369970868437e-05, 'epoch': 0.75}
{'loss': 1.3368, 'grad_norm': 2.053511619567871, 'learning_rate': 1.0633021021966773e-05, 'epoch': 0.79}
{'loss': 1.3395, 'grad_norm': 1.8470497131347656, 'learning_rate': 8.664672073065113e-06, 'epoch': 0.83}
{'loss': 1.3833, 'grad_norm': 1.9874024391174316, 'learning_rate': 6.696323124163452e-06, 'epoch': 0.87}
{'loss': 1.3541, 'grad_norm': 2.1954381465911865, 'learning_rate': 4.727974175261791e-06, 'epoch': 0.91}
{'loss': 1.338, 'grad_norm': 1.9556453227996826, 'learning_rate': 2.7596252263601292e-06, 'epoch': 0.94}
{'loss': 1.3329, 'grad_norm': 2.050063133239746, 'learning_rate': 7.912762774584679e-07, 'epoch': 0.98}
{'train_runtime': 8448.5151, 'train_samples_per_second': 12.026, 'train_steps_per_second': 1.503, 'train_loss': 1.4590118074218135, 'epoch': 1.0}
100%|████████████████████████████████| 12701/12701 [2:18:28<00:00,  1.53it/s]
学習完了！モデルは ./trainingtest/models/fine_tuned_llama3_2_1b に保存されま した。
wandb:
wandb: 🚀 View run ./trainingtest/models/pretrain_output at: https://wandb.ai/xxxxxxx/huggingface/runs/otXXXXt95
wandb: Find logs at: wandb/run-20250223_195626-otn1xt95/logs
```

### 訓練結果確認
1.学習は ***138 分*** 掛かった。
```python
dataset = load_dataset("kajuma/CC-news-2024-July-October-cleaned", split="train")
dataset = dataset.train_test_split(train_size=0.8)["train"]
```
1️⃣元のデータセットの総数 ≈ 127,000 サンプル
2️⃣80% を train 用に使ったので: 127,000 × 0.8 = 101,600 サンプル
3️⃣WandB の train_samples_per_second も ≈ 101,604 とほぼ一致
4️⃣このデータセット全体を 1 エポックで訓練


2.WANDBでの訓練結果の確認：
![スクリーンショット 2025-02-24 121105.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3629252/b703f49a-e45e-4084-8c5c-82579b23a2d8.png)
 訓練結果の評価
1️⃣ train/loss (左上)
学習損失 (Loss) は最初 2.2 から 1.3 付近まで減少 しています。
この減少傾向は良好で、モデルが収束していることを示唆しています。
2️⃣ train/learning_rate (右上)
学習率が直線的に減少 しており、スケジュール通りに適用されている ことを確認できます。
問題なし。
3️⃣ train/grad_norm (中央右)
勾配ノルム (Gradient Norm) が最初 3.5 付近から 2.0 以下で安定 しています。
ただし、一部のポイントで ばらつき がありますが、大きなスパイクは見られません。
問題なし（ただし、スパイクを減らすには gradient_accumulation_steps を調整可能）。
4️⃣ train/global_step (左下)
グローバルステップが一直線で増加 しており、トレーニングがスムーズに進行していることを示します。
問題なし。
5️⃣ train/epoch (右下)
エポックの進行状況がリニアで1回完了 しているため、学習が正常に終わったことが分かります。
問題なし。
✅ 結論
全体的に良好な結果 です。
収束がスムーズで、学習が適切に進んでいる ことが分かります。
改善ポイント:
train/grad_norm にスパイクがあるため、gradient_accumulation_steps を増やすと安定化 する可能性があります。
num_train_epochs=1 なので、追加のエポックを試して最適な学習を探るのもアリかな。

---

## 📌 3. LoRA をベースモデルにマージ
上記学習済みモデルが LLaMA のベースモデル（フルモデル）ではなく、LoRA のアダプターのみ保存されています。vLLMを利用するために、学習した LoRA アダプターを、元の Llama-3.2-1B モデルにマージして、vLLM でロードできるようになる必要があります。

**ファイル名: `trainingtest/merge_lora.py`**
```python
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
import torch

base_model_path = "meta-llama/Llama-3.2-1B"
model = AutoModelForCausalLM.from_pretrained(base_model_path, torch_dtype=torch.float16)
tokenizer = AutoTokenizer.from_pretrained(base_model_path)

lora_model_path = "./trainingtest/models/fine_tuned_llama3_2_1b"
model = PeftModel.from_pretrained(model, lora_model_path)
model = model.merge_and_unload()

merged_model_path = "./trainingtest/models/merged_llama3_2_1b"
model.save_pretrained(merged_model_path)
tokenizer.save_pretrained(merged_model_path)

print("✅ LoRAをマージしたフルモデルを保存しました: ./trainingtest/models/merged_llama3_2_1b")
```

実行コマンド:
```bash
python trainingtest/merge_lora.py
```
```bash
✅ LoRAをマージしたフルモデルを保存しました: ./trainingtest/models/merged_llama3_2_1b
```
---

## 📌 4. vLLM API化（FastAPI）
**ファイル名: `trainingtest/api_vllm.py`**
```python
from fastapi import FastAPI
from pydantic import BaseModel
from vllm import LLM, SamplingParams

# FastAPIアプリの作成
app = FastAPI()

# ✅ マージ済みモデルのパス
model_path = "./trainingtest/models/merged_llama3_2_1b"

# ✅ vLLMで統合済みモデルをロード
llm = LLM(model=model_path, dtype="float16")

# ✅ サンプリング設定（繰り返し防止を強化）
class InputText(BaseModel):
    prompt: str
    max_tokens: int = 200
    temperature: float = 1.0  # 🔼 低すぎると繰り返しやすい
    top_p: float = 0.9  # 🔼 確率の高い候補を広く取る
    top_k: int = 50
    repetition_penalty: float = 1.2  # 🔥 追加！繰り返しを防ぐ

@app.post("/generate/")
async def generate_text(input_text: InputText):
    """
    統合済み Llama3 モデルを使用してテキストを生成する API
    """
    sampling_params = SamplingParams(
        max_tokens=input_text.max_tokens,
        temperature=input_text.temperature,
        top_p=input_text.top_p,
        top_k=input_text.top_k,
        repetition_penalty=input_text.repetition_penalty  # 🔥 追加！
    )

    outputs = llm.generate(input_text.prompt, sampling_params)
    generated_text = outputs[0].outputs[0].text  # 最初の応答のみ取得

    return {"generated_text": generated_text}

# ✅ サーバー起動（スクリプト実行時）
if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)
```

```bash
python trainingtest/api_vllm.py
```
```bash
......
INFO 02-24 11:50:59 llm_engine.py:436] init engine (profile, create kv cache, warmup model) took 8.24 seconds
INFO:     Started server process [2653497]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
```
---

## 📌 5. API の動作確認
Prompt: 2024年のTopニュースは？

```bash
curl -X POST "http://127.0.0.1:8000/generate/" \
     -H "Content-Type: application/json" \
     -d '{"prompt": "2024年のTopニュースは？", "max_tokens": 512, "temperature": 1.0, "top_p": 0.9, "top_k": 50, "repetition_penalty": 1.2}'
```
```bash
{"generated_text":" 日本の外国人移民が多すぎる「なぜ」とするのは？\n\n## Contents\n\n10年で１位に急上昇した日本国内からの出所者数（通算）、経済成長からみ た増加率と推定値にはかねて否定された理由を調べようとしているのが、米国のリブ ランド研究機関であるNational Center for Policy Analysisにより実施された調査。NAPAによる英語版公開記事では、日本人の人口のうち、その半分以上やりがいあると いうことを証明してきた。\n特権派・左-leaning議員からは批判的見解が出ているも のの、「どういうことだ」？\n著作：「マーケット予想法」\n日銀総裁への質疑応答 にもお披露目されているように、これまでアメリカでは政治団体の中央情報局との連 携がありながらも、このことが正当性を持って表現されないのか。\n\nArticle Index\n1．月間の少子化速い排斥されるフランスのイタリア系雇用者の割合は３％台となる中、マーグル大学職業分析部門教授のジャン＝レオン・アールサンス氏によれば、そ れほどではない事です；\n2．オーストラルのコロンビア語の話者が減らしており、大きな失礼だったところでしょうけど、今回取り上げられたデータ量によって新しい種 類の言語が生まれた可能性があるとも言えるため、しっかりとした説明しかできない ようなこともありますが、単に政府がこれらの変動を見ていない限り全く理解してい なかったといえなくなる訳でもありません;\n3．デモグラム社元最高責任官に就任し たトーマス・マッケンジー副主席がきょうより再びインターネットテラス紙に出稿し ている記録的な発言ぶりに政党関係者に対し非難を受けていたわけですが、こうした 問題意識があったのであれはいったい何だと判断します?;\n4．インフレ後期高収入族の人口構造について述べていただいたうえで、私たち自力的に知られる必要最低限の 要素であり続けた場合、税金の上場とする方法ができるだけ重要となります。またそ のために資本市場での利益は大きくなるとすればこの決断方"}(vllm_v0.7.1) 
```

## 追加情報：
1.LoRA の適用によって、モデルの繰り返し抑制が弱くなっているとともに、Sampling Parameters（温度・確率制限）が正しく設定しないと出力結果は繰り返しの結果が出る可能性があります。Sampling Parametersがチューニングと同時に、repetition_penalty が設定されると抑制効果がかなりあります。

2.APIでの推理ではなく、直接promptでの推理確認したい場合に、下記のサンプルをご利用いただけます。
```python
from vllm import LLM, SamplingParams

# マージ済みモデルのパス
model_path = "./trainingtest/models/merged_llama3_2_1b"

# vLLMでモデルをロード
llm = LLM(model=model_path, dtype="float16")

# 推論関数
def generate_text(prompt, max_tokens=200, temperature=0.8):
    sampling_params = SamplingParams(max_tokens=max_tokens, temperature=temperature)
    outputs = llm.generate(prompt, sampling_params)
    return outputs[0].outputs[0].text

# 実行
if __name__ == "__main__":
    prompt = input("プロンプトを入力してください: ")
    generated_text = generate_text(prompt)
    print("\n生成されたテキスト:\n", generated_text)
```

---
## 🎉 まとめ

✅ Llama 3.2-1B を **Unsloth でファインチューニング**
✅ **LoRA をベースモデルに統合**
✅ **vLLM で高速推論 API を構築** 🚀

---
## 参考記事：
https://qiita.com/engchina/items/1382ded7521dea24eaae
https://note.com/ganchan6636/n/n05ca529957c2