0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

高価なNVIDIA GPUなしでLLMをFine-tuningする

0
Posted at

高価なNVIDIA GPUなしでLLMをFine-tuningする:MLflow公式QLoRAチュートリアルをM5 Max / Apple Siliconで動かす

はじめに

LLMのFine-tuningを試してみたいと思ったとき、最初に立ちはだかるのがGPUです。

特にQLoRAやLoRAのチュートリアルを見ると、

  • NVIDIA GPU
  • CUDA
  • 20GB以上のVRAM
  • A10 / A100 / H100

といった環境を前提にしているものが多くあります。

今回ベースにした MLflow公式の「Fine-Tuning Open-Source LLM using QLoRA with MLflow and PEFT」 も、1枚のNVIDIA A10G 24GB GPUで検証されたチュートリアルです。公式では20GB以上のGPU VRAMが必要とされています。

しかし、個人の学習やモデル開発のためだけに高価なNVIDIA GPUを購入するのは、なかなかハードルが高いです。

そこで本記事では、

MLflow公式QLoRAチュートリアルをApple M5 Max上で動かす

ことを目標にします。

単にMistral 7Bを動かすだけではありません。

公式チュートリアルと同じように、

  1. データセットを準備する
  2. Mistral 7Bを4-bit量子化する
  3. LoRA Adapterを追加する
  4. QLoRAでFine-tuningする
  5. MLflowでLossやHyperparameterを管理する
  6. 学習済みLoRA AdapterをMLflowへ保存する
  7. MLflowからモデルを読み戻して推論する

ところまで行います。

Apple SiliconはCPUとGPUが同じUnified Memoryを共有する構造を持ち、PyTorch / Transformersからはmps backendを使ってGPUを利用できます。

また、2026年のbitsandbytes 0.50.0ではApple Silicon向けMPS backendが大きく改善され、4-bitおよびLLM.int8構成がMPS上で利用できるようになりました。

つまり、

NVIDIA GPUを購入しなくても、Apple Siliconを使ってLoRA / QLoRAを学び、モデル開発の一連のサイクルを経験できる

環境がかなり整ってきています。

ただし、本記事は「M5 MaxがNVIDIA GPUより高速」という話ではありません。

目的は、

手元のApple Siliconで、実際のLLM Fine-tuningとMLflowによるモデル開発サイクルを学べること

を示すことです。


この記事で分かること

この記事を最後まで実行すると、次の構成を自分のMac上に作れます。

最終的には、

Mistral-7B
    +
4-bit Quantization
    +
LoRA
    +
Apple M5 Max / MPS
    +
MLflow

という構成になります。


まず知っておきたい用語

いきなりコードに入る前に、今回登場する技術を簡単に整理します。

Fine-tuningとは

LLMは最初から大量の知識を持っています。

しかし、

一般的なLLM

に、

SQL生成が得意になってほしい

と要求しても、必ずしも期待通りにはなりません。

そこで特定用途のデータを追加学習させます。

これがFine-tuningです。


LoRAとは

通常のFine-tuningでは、モデルの大量のParameterを更新します。

7Bモデルなら約70億Parameterです。

これは非常に重い処理です。

LoRAではBase ModelのParameterを基本的に固定し、小さな追加行列だけを学習します。

つまり、

巨大なBase Model
      ↓
変更しない

小さなLoRA Adapter
      ↓
ここだけ学習

という考え方です。


Quantizationとは

Mistral 7BのParameterを、そのまま高精度の数値で保持すると大量のメモリを消費します。

そこでParameterを4-bitなどの小さな表現に変換します。

高精度Weight
   ↓
4-bit
   ↓
必要メモリ削減

これをQuantization(量子化)と呼びます。


QLoRAとは

QLoRAは非常に単純化すると、

Quantization
+
LoRA

です。

MLflow公式チュートリアルでも、4-bit量子化したMistral 7BにPEFTでLoRAを追加する構成になっています。


PEFTとは

PEFTはHugging Faceが提供する、

Parameter-Efficient Fine-Tuning

のためのライブラリです。

今回のLoRA Adapter生成を担当します。


MLflowとは

ここは誤解しやすいポイントです。

MLflow自身がLoRAを学習するわけではありません。

役割分担は次のようになります。

ライブラリ 主な役割
PyTorch Deep Learning基盤
Transformers Mistral、Tokenizer、Trainer
bitsandbytes 4-bit量子化
PEFT LoRA Adapter
Datasets 学習Dataset
MLflow 実験・Metric・Model管理

MLflowにはTransformers / PEFT向けのネイティブ統合があり、LoRA / QLoRAを含むPEFTモデルを記録・ロードできます。


NVIDIA版からM5 Max版へ何を変更するのか

今回の重要ポイントです。

MLflow公式TutorialをそのままMacへコピーしても動きません。

主な変更点は次のとおりです。

MLflow公式 M5 Max版
NVIDIA A10G 24GB Apple M5 Max
CUDA MPS / Metal
nvidia-smi torch.backends.mps
bitsandbytes CUDA bitsandbytes MPS
paged_adamw_8bit adamw_torch
batch size 2 batch size 1
gradient accumulation 4 8
effective batch 8 8
Python環境 uv + Python 3.13
MLflow 同じ
Mistral-7B 同じ
Dataset 同じ
LoRA設定 原則同じ

bitsandbytes 0.50.0ではMPS上で4-bit構成が動作しますが、Apple Siliconで8-bit optimizerを使う構成はNVIDIA/CUDA版と同じにはできません。そこでTraining時のOptimizerだけPyTorch標準のadamw_torchへ変更します。


全体のSTEP

MLflow公式チュートリアルの構成は次の7 STEPです。

1. Environment Set up
2. Dataset Preparation
3. Load the Base Model
4. Define a PEFT Model
5. Kick-off a Training Job
6. Save the PEFT Model to MLflow
7. Load the Saved PEFT Model

今回はその前に、

STEP 0
Python 3.13 + uv環境

を追加します。


STEP 0:uv + Python 3.13環境を作る

今回はPython管理にuvを使います。

venvでも構いませんが、uvなら、

Python本体
仮想環境
Package
Lock file

をまとめて管理できます。

uvをインストール

curl -LsSf https://astral.sh/uv/install.sh | sh

shellを読み直します。

source ~/.zshrc

確認します。

uv --version

Python 3.13をインストール

uv python install 3.13

確認します。

uv python list

Project作成

mkdir -p ~/dev/mlflow-m5max-qlora
cd ~/dev/mlflow-m5max-qlora

Python 3.13を固定します。

uv python pin 3.13

仮想環境を作成します。

uv venv --python 3.13

有効化します。

source .venv/bin/activate

確認します。

python --version

想定:

Python 3.13.x

architectureも確認します。

python -c "import platform; print(platform.machine())"

Apple Siliconなら、

arm64

です。


uv projectとして初期化

uv init

最終的には次のような構成になります。

mlflow-m5max-qlora/
├── .python-version
├── .venv/
├── pyproject.toml
├── uv.lock
├── scripts/
└── README.md

STEP 1:PyTorchからM5 Max GPUを使えるか確認する

ここはMLflow公式Tutorialの、

1. Environment Set up
└─ Hardware Requirement

に相当します。

公式は、

nvidia-smi

でNVIDIA GPUを確認します。

M5 MaxではMPSを確認します。

PyTorchとNumPyを追加

uv add torch numpy

MPS確認

uv run python - <<'PY'
import torch

print("PyTorch      :", torch.__version__)
print("MPS built    :", torch.backends.mps.is_built())
print("MPS available:", torch.backends.mps.is_available())
PY

期待値:

MPS built    : True
MPS available: True

PyTorchのmps deviceはAppleのMetal Performance Shadersを利用してmacOS GPU上で計算を実行します。


実際にGPU計算してみる

uv run python - <<'PY'
import torch

device = torch.device("mps")

a = torch.randn(4096, 4096, device=device)
b = torch.randn(4096, 4096, device=device)

c = a @ b

print("device :", c.device)
print("shape  :", c.shape)
PY

成功すると、

device : mps

となります。

実際の出力は以下の通り
スクリーンショット 2026-08-11 23.59.24.png


BF16も確認する

MLflow公式QLoRA Tutorialは4-bit Weightを演算するとき、

torch.bfloat16

を使用します。

M5 Maxでも動くことを確認します。

uv run python - <<'PY'
import torch

device = torch.device("mps")

a = torch.randn(
    2048,
    2048,
    device=device,
    dtype=torch.bfloat16,
)

b = torch.randn(
    2048,
    2048,
    device=device,
    dtype=torch.bfloat16,
)

c = a @ b

print("device :", c.device)
print("dtype  :", c.dtype)
print("shape  :", c.shape)
PY

期待結果:

device : mps:0
dtype  : torch.bfloat16
shape  : torch.Size([2048, 2048])

実際に遭遇したWarning:NumPyがない

最初に次のWarningが出ました。

UserWarning: Failed to initialize NumPy:
No module named 'numpy'

これはBF16やMPSのエラーではありません。

結果が、

device : mps:0
dtype  : torch.bfloat16

まで出ていればGPU演算自体は成功しています。

NumPyを追加すればWarningは消えます。

uv add numpy

MPS fallback

MPSで未実装の演算があった場合、CPUへfallbackできるようにします。

export PYTORCH_ENABLE_MPS_FALLBACK=1

scripts/check_mps.py 全文
scripts/check_mps.py
import platform
import sys

import torch


print("=== Python ===")
print("version      :", sys.version.split()[0])
print("architecture :", platform.machine())

print()

print("=== PyTorch ===")
print("version       :", torch.__version__)
print("MPS built     :", torch.backends.mps.is_built())
print("MPS available :", torch.backends.mps.is_available())

if not torch.backends.mps.is_available():
    raise SystemExit("ERROR: MPS is not available")

device = torch.device("mps")

print()

print("=== MPS FP32 Test ===")

a = torch.randn(1024, 1024, device=device)
b = torch.randn(1024, 1024, device=device)
c = a @ b

print("device :", c.device)
print("dtype  :", c.dtype)
print("PASS")

print()

print("=== MPS BF16 Test ===")

a = torch.randn(
    1024,
    1024,
    device=device,
    dtype=torch.bfloat16,
)

b = torch.randn(
    1024,
    1024,
    device=device,
    dtype=torch.bfloat16,
)

c = a @ b

print("device :", c.device)
print("dtype  :", c.dtype)
print("PASS")

実行:

uv run python scripts/check_mps.py

STEP 1-2:MLflow / Transformers / PEFTをインストール

MLflow公式Tutorialの、

Environment Set up
└─ Install Python Libraries

に相当します。

必要なPackageを追加します。

uv add \
  mlflow \
  transformers \
  peft \
  accelerate \
  datasets \
  torchvision

bitsandbytesはMPS対応が改善された0.50系を使います。

uv add "bitsandbytes>=0.50.0"

macOS 26以降なら、bitsandbytesのMPS向け最適化Metal kernelを利用するためkernels packageを追加できます。

uv add kernels

なぜtorchvisionが必要なのか

今回の処理はText-to-SQLなので、画像処理をする予定はありません。

それでもtorchvisionを追加しています。

理由は、MLflowがTransformers Modelを保存するときに推論環境用Packageを自動検出し、PyTorch関連依存としてtorchvisionのVersionを調べるケースがあるためです。

実際、MLflowへPEFT Modelを保存したとき、

PackageNotFoundError:
No package metadata was found for torchvision

というエラーに遭遇しました。

解決方法は、

uv add torchvision

でした。

このエラーについては後でもう一度説明します。


scripts/check_mlflow_stack.py 全文
scripts/check_mlflow_stack.py
import torch
import mlflow
import transformers
import peft
import accelerate
import datasets
import bitsandbytes

from accelerate import Accelerator
from transformers import BitsAndBytesConfig


print("=== Versions ===")

print("torch        :", torch.__version__)
print("mlflow       :", mlflow.__version__)
print("transformers :", transformers.__version__)
print("peft         :", peft.__version__)
print("accelerate   :", accelerate.__version__)
print("datasets     :", datasets.__version__)
print("bitsandbytes :", bitsandbytes.__version__)

print()
print("=== MPS ===")

print("MPS built     :", torch.backends.mps.is_built())
print("MPS available :", torch.backends.mps.is_available())

accelerator = Accelerator()

print("Accelerate    :", accelerator.device)

print()
print("=== QLoRA Config ===")

config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

print("4bit          :", config.load_in_4bit)
print("quant type    :", config.bnb_4bit_quant_type)
print("double quant  :", config.bnb_4bit_use_double_quant)
print("compute dtype :", config.bnb_4bit_compute_dtype)

print()
print("PASS")

実行します。

uv run python scripts/check_mlflow_stack.py

期待値:

MPS built     : True
MPS available : True
Accelerate    : mps:0

4bit          : True
quant type    : nf4
double quant  : True
compute dtype : torch.bfloat16

PASS

STEP 2:Datasetを準備する

ここからMLflow公式Tutorialとほぼ同じ処理になります。

公式で使用しているDatasetは、

b-mc2/sql-create-context

です。

Text-to-SQL用のDatasetで、

question
context
answer

を持っています。

例えば、

context:
CREATE TABLE head (age INTEGER)

question:
How many heads of the departments are older than 56?

answer:
SELECT COUNT(*) FROM head WHERE age > 56

のようなデータです。


Train / Testに分ける

split_dataset = dataset.train_test_split(
    test_size=0.2,
    seed=42,
)

公式と同じ、

Train : 62,861
Test  : 15,716

になります。

seed=42を固定する理由は再現性です。

LoRA設定を変更して比較するとき、

Run A
r=16

Run B
r=32

でDatasetの分割まで変わってしまうと、公平な比較にならないためです。


Promptを作る

学習データを、

Table
Question
Answer

から、

### Table:

CREATE TABLE ...

### Question:

...

### Response:

SELECT ...

へ変換します。


Tokenizer

公式と同じ、

mistralai/Mistral-7B-v0.1

のTokenizerを使用します。

最大長も公式と同じ、

MAX_LENGTH = 256

です。


scripts/prepare_dataset.py 全文
scripts/prepare_dataset.py
from datasets import load_dataset
from transformers import AutoTokenizer


DATASET_ID = "b-mc2/sql-create-context"
BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"

MAX_LENGTH = 256
TEST_SIZE = 0.2
SEED = 42


PROMPT_TEMPLATE = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

### Table:

{context}

### Question:

{question}

### Response:

{output}"""


def apply_prompt_template(row):

    return {
        "prompt": PROMPT_TEMPLATE.format(
            question=row["question"],
            context=row["context"],
            output=row["answer"],
        )
    }


def main():

    print("Loading dataset...")

    dataset = load_dataset(
        DATASET_ID,
        split="train",
    )

    split_dataset = dataset.train_test_split(
        test_size=TEST_SIZE,
        seed=SEED,
    )

    train_dataset = split_dataset["train"]
    test_dataset = split_dataset["test"]

    print("Applying prompt template...")

    train_dataset = train_dataset.map(
        apply_prompt_template
    )

    print("Loading tokenizer...")

    tokenizer = AutoTokenizer.from_pretrained(
        BASE_MODEL_ID,
        model_max_length=MAX_LENGTH,
        padding_side="left",
        add_eos_token=True,
    )

    tokenizer.pad_token = tokenizer.eos_token

    def tokenize_and_pad_to_fixed_length(sample):

        result = tokenizer(
            sample["prompt"],
            truncation=True,
            max_length=MAX_LENGTH,
            padding="max_length",
        )

        result["labels"] = result["input_ids"].copy()

        return result

    print("Tokenizing...")

    tokenized_train_dataset = train_dataset.map(
        tokenize_and_pad_to_fixed_length
    )

    assert all(
        len(x["input_ids"]) == MAX_LENGTH
        for x in tokenized_train_dataset
    )

    print()
    print("=== Dataset Preparation ===")
    print("Dataset    :", DATASET_ID)
    print("Base model :", BASE_MODEL_ID)
    print("Train      :", len(train_dataset))
    print("Test       :", len(test_dataset))
    print("Max length :", MAX_LENGTH)
    print(
        "Tokenized  :",
        len(tokenized_train_dataset),
    )
    print()
    print("PASS")


if __name__ == "__main__":
    main()

実行:

uv run python scripts/prepare_dataset.py

STEP 3:Mistral 7Bを4-bitでM5 Maxへロードする

ここがNVIDIA版からM5 Max版へ移植するときの重要ポイントです。

MLflow公式では、

BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)

を使用します。

M5 Max版でもこのQuantization設定は変えません。

変更するのは、

device_map="mps"

です。


4-bitを確認する

ロード後、

model.is_loaded_in_4bit

が、

True

になることを確認します。

さらにbitsandbytesの、

bnb.nn.Linear4bit

が存在することも確認します。


scripts/load_base_model.py 全文
scripts/load_base_model.py
import torch
import bitsandbytes as bnb

from transformers import (
    AutoModelForCausalLM,
    BitsAndBytesConfig,
)


BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"


def main():

    if not torch.backends.mps.is_available():
        raise RuntimeError("MPS is not available")

    print("=== Environment ===")
    print("torch :", torch.__version__)
    print("MPS   :", torch.backends.mps.is_available())

    print()

    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
    )

    print("Loading model...")
    print(BASE_MODEL_ID)

    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_ID,
        quantization_config=quantization_config,
        device_map="mps",
    )

    linear4bit_count = sum(
        isinstance(module, bnb.nn.Linear4bit)
        for module in model.modules()
    )

    print()
    print("=== Model ===")

    print(
        "class            :",
        type(model).__name__,
    )

    print(
        "device           :",
        model.device,
    )

    print(
        "loaded in 4-bit  :",
        getattr(
            model,
            "is_loaded_in_4bit",
            False,
        ),
    )

    print(
        "Linear4bit layers:",
        linear4bit_count,
    )

    if not getattr(model, "is_loaded_in_4bit", False):
        raise RuntimeError(
            "Model was not loaded in 4-bit"
        )

    if linear4bit_count == 0:
        raise RuntimeError(
            "No Linear4bit layers detected"
        )

    print()
    print("PASS")


if __name__ == "__main__":
    main()

実行:

PYTORCH_ENABLE_MPS_FALLBACK=1 \
uv run python scripts/load_base_model.py

Fine-tuning前のMistralを確認する

Fine-tuningによって改善したか確認するには、

Fine-tuning前の結果

も見ておく必要があります。


scripts/test_base_model.py 全文
scripts/test_base_model.py
import torch

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)


BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"


PROMPT_TEMPLATE = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

### Table:

{context}

### Question:

{question}

### Response:

{output}"""


quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)


print("Loading model...")

model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL_ID,
    quantization_config=quantization_config,
    device_map="mps",
)


tokenizer = AutoTokenizer.from_pretrained(
    BASE_MODEL_ID
)

tokenizer.pad_token = tokenizer.eos_token


context = """
CREATE TABLE head (
    age INTEGER
)
"""

question = """
How many heads of the departments are older than 56?
"""


prompt = PROMPT_TEMPLATE.format(
    context=context,
    question=question,
    output="",
)


print()
print("=== Prompt ===")
print(prompt)


inputs = tokenizer(
    prompt,
    return_tensors="pt",
)

inputs = {
    k: v.to("mps")
    for k, v in inputs.items()
}


with torch.no_grad():

    output = model.generate(
        **inputs,
        max_new_tokens=128,
        repetition_penalty=1.15,
        do_sample=False,
    )


generated = tokenizer.decode(
    output[0][inputs["input_ids"].shape[1]:],
    skip_special_tokens=True,
)


print()
print("=== Base Model Response ===")
print(generated)

MLflow公式でも、Fine-tuning前のvanilla MistralはこのText-to-SQLタスクで適切なSQLを生成できないことをBaselineとして確認しています。


STEP 4:LoRA Adapterを追加する

4-bitにしただけではQLoRAではありません。

ここにLoRAを追加します。

4-bit Quantization
        +
       LoRA
        ↓
      QLoRA

今回のLoRA設定はMLflow公式と同じものを使います。

r=32
lora_alpha=64
lora_dropout=0.1

target moduleも公式に合わせます。

[
    "q_proj",
    "k_proj",
    "v_proj",
    "o_proj",
    "gate_proj",
    "up_proj",
    "down_proj",
    "lm_head",
]

Gradient Checkpointing

LLM Trainingでは大量の中間データをMemoryに保持します。

Gradient Checkpointingを使うと、一部を保存せず必要になったとき再計算することでMemory消費を削減します。

Memory使用量 ↓
計算量      ↑

というTrade-offです。


scripts/define_peft_model.py 全文
scripts/define_peft_model.py
import torch

from transformers import (
    AutoModelForCausalLM,
    BitsAndBytesConfig,
)

from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
)


BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"


def main():

    if not torch.backends.mps.is_available():
        raise RuntimeError("MPS is not available")

    print("=== Environment ===")
    print("PyTorch :", torch.__version__)
    print("Device  : mps")

    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
    )

    print()
    print("Loading base model...")

    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_ID,
        quantization_config=quantization_config,
        device_map="mps",
    )

    print()
    print("=== Base Model ===")
    print(
        "4-bit :",
        getattr(model, "is_loaded_in_4bit", False),
    )

    print()
    print("Enabling gradient checkpointing...")

    model.gradient_checkpointing_enable()

    print(
        "Preparing model for k-bit training..."
    )

    model = prepare_model_for_kbit_training(
        model
    )

    peft_config = LoraConfig(
        task_type="CAUSAL_LM",
        r=32,
        lora_alpha=64,
        lora_dropout=0.1,
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj",
            "o_proj",
            "gate_proj",
            "up_proj",
            "down_proj",
            "lm_head",
        ],
        bias="none",
    )

    print()
    print("Adding LoRA adapter...")

    peft_model = get_peft_model(
        model,
        peft_config,
    )

    print()
    print("=== Trainable Parameters ===")

    peft_model.print_trainable_parameters()

    trainable_params = 0
    all_params = 0

    for _, param in peft_model.named_parameters():

        all_params += param.numel()

        if param.requires_grad:
            trainable_params += param.numel()

    percentage = (
        100 * trainable_params / all_params
    )

    print()
    print("trainable :", f"{trainable_params:,}")
    print("all       :", f"{all_params:,}")
    print("percent   :", f"{percentage:.4f}%")

    trainable_names = [
        name
        for name, param
        in peft_model.named_parameters()
        if param.requires_grad
    ]

    print()
    print(
        "Trainable tensors :",
        len(trainable_names),
    )

    print()
    print("First trainable parameters:")

    for name in trainable_names[:10]:
        print(" ", name)

    print()
    print("=== PEFT Config ===")
    print(peft_config)

    print()
    print("PASS")


if __name__ == "__main__":
    main()

ここで、

lora_A
lora_B

というParameterだけがTrainableになっていれば成功です。


STEP 5:QLoRA TrainingをMLflowで管理する

いよいよ実際に学習します。

ここで、

Dataset
+
QLoRA
+
M5 Max
+
MLflow

がつながります。


MLflow Serverを起動

別Terminalで実行します。

cd ~/dev/mlflow-m5max-qlora
source .venv/bin/activate
uv run mlflow server \
  --host 127.0.0.1 \
  --port 5000

ブラウザで、

http://127.0.0.1:5000

を開きます。

スクリーンショット 2026-08-11 23.56.19.png


NVIDIA版からOptimizerを変更する

ここが今回の重要な差分です。

MLflow公式:

optim="paged_adamw_8bit"

M5 Max:

optim="adamw_torch"

bitsandbytesのApple Silicon backendでは4-bit QLoRAは利用できますが、NVIDIA版と同じ8-bit Optimizer構成にはできないためです。


Batch sizeも変更

公式:

batch = 2
gradient accumulation = 4

2 × 4 = 8

M5 Max:

batch = 1
gradient accumulation = 8

1 × 8 = 8

Effective Batch Sizeは同じ8にしています。


最初は10 STEPでSmoke Testする

いきなり500 STEP回す必要はありません。

まず、

MAX_STEPS = 10

で、

Dataset
↓
Forward
↓
Backward
↓
Optimizer
↓
MPS
↓
MLflow

が一周するか確認します。

成功後、

MAX_STEPS = 500

へ変更します。


scripts/train_qlora_mlflow.py 全文
scripts/train_qlora_mlflow.py
from datetime import datetime

import mlflow
import torch
import transformers

from datasets import load_dataset

from peft import (
    LoraConfig,
    get_peft_model,
    prepare_model_for_kbit_training,
)

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
    TrainingArguments,
)


DATASET_ID = "b-mc2/sql-create-context"
BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"

MLFLOW_TRACKING_URI = "http://127.0.0.1:5000"
MLFLOW_EXPERIMENT = "M5 Max MLflow PEFT Tutorial"

MAX_LENGTH = 256
SEED = 42

# 最初は10。
# 動作確認後はMLflow公式Tutorialに合わせて500へ変更する。
MAX_STEPS = 10


PROMPT_TEMPLATE = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

### Table:

{context}

### Question:

{question}

### Response:

{output}"""


def prepare_dataset():

    print("Loading dataset...")

    dataset = load_dataset(
        DATASET_ID,
        split="train",
    )

    split_dataset = dataset.train_test_split(
        test_size=0.2,
        seed=SEED,
    )

    train_dataset = split_dataset["train"]

    def apply_prompt_template(row):

        return {
            "prompt": PROMPT_TEMPLATE.format(
                question=row["question"],
                context=row["context"],
                output=row["answer"],
            )
        }

    train_dataset = train_dataset.map(
        apply_prompt_template
    )

    tokenizer = AutoTokenizer.from_pretrained(
        BASE_MODEL_ID,
        model_max_length=MAX_LENGTH,
        padding_side="left",
        add_eos_token=True,
    )

    tokenizer.pad_token = tokenizer.eos_token

    def tokenize(sample):

        result = tokenizer(
            sample["prompt"],
            truncation=True,
            max_length=MAX_LENGTH,
            padding="max_length",
        )

        result["labels"] = result["input_ids"].copy()

        return result

    tokenized_train_dataset = train_dataset.map(
        tokenize
    )

    return (
        tokenizer,
        tokenized_train_dataset,
    )


def create_peft_model():

    if not torch.backends.mps.is_available():
        raise RuntimeError(
            "MPS is not available"
        )

    quantization_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16,
    )

    print("Loading Mistral 7B...")

    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL_ID,
        quantization_config=quantization_config,
        device_map="mps",
    )

    if not getattr(
        model,
        "is_loaded_in_4bit",
        False,
    ):
        raise RuntimeError(
            "Model is not loaded in 4-bit"
        )

    model.gradient_checkpointing_enable()

    model = prepare_model_for_kbit_training(
        model
    )

    peft_config = LoraConfig(
        task_type="CAUSAL_LM",
        r=32,
        lora_alpha=64,
        lora_dropout=0.1,
        target_modules=[
            "q_proj",
            "k_proj",
            "v_proj",
            "o_proj",
            "gate_proj",
            "up_proj",
            "down_proj",
            "lm_head",
        ],
        bias="none",
    )

    peft_model = get_peft_model(
        model,
        peft_config,
    )

    peft_model.config.use_cache = False

    return peft_model, peft_config


def main():

    print("=== Environment ===")
    print("PyTorch :", torch.__version__)
    print(
        "MPS     :",
        torch.backends.mps.is_available(),
    )

    mlflow.set_tracking_uri(
        MLFLOW_TRACKING_URI
    )

    mlflow.set_experiment(
        MLFLOW_EXPERIMENT
    )

    tokenizer, train_dataset = (
        prepare_dataset()
    )

    print()
    print(
        "Training samples:",
        len(train_dataset),
    )

    peft_model, peft_config = (
        create_peft_model()
    )

    print()
    peft_model.print_trainable_parameters()

    run_name = (
        "M5-Max-Mistral-7B-SQL-QLoRA-"
        + datetime.now().strftime(
            "%Y%m%d-%H%M%S"
        )
    )

    training_args = TrainingArguments(

        report_to="mlflow",

        run_name=run_name,

        output_dir="./outputs",

        # MLflow公式:
        # per_device_train_batch_size=2
        # gradient_accumulation_steps=4
        #
        # M5 Max:
        # 1 x 8 = effective batch 8
        per_device_train_batch_size=1,

        gradient_accumulation_steps=8,

        gradient_checkpointing=True,

        # MLflow公式:
        # optim="paged_adamw_8bit"
        #
        # MPS版:
        optim="adamw_torch",

        bf16=True,

        learning_rate=2e-5,

        lr_scheduler_type="constant",

        max_steps=MAX_STEPS,

        save_steps=10,

        logging_steps=1,

        warmup_steps=5,

        dataloader_pin_memory=False,
    )

    trainer = transformers.Trainer(

        model=peft_model,

        train_dataset=train_dataset,

        data_collator=(
            transformers
            .DataCollatorForLanguageModeling(
                tokenizer=tokenizer,
                mlm=False,
            )
        ),

        args=training_args,
    )

    with mlflow.start_run(
        run_name=run_name
    ) as run:

        mlflow.log_params({
            "base_model":
                BASE_MODEL_ID,

            "dataset":
                DATASET_ID,

            "device":
                "mps",

            "quantization":
                "4bit",

            "quant_type":
                "nf4",

            "double_quant":
                True,

            "compute_dtype":
                "bfloat16",

            "lora_r":
                peft_config.r,

            "lora_alpha":
                peft_config.lora_alpha,

            "lora_dropout":
                peft_config.lora_dropout,

            "optimizer":
                "adamw_torch",

            "max_length":
                MAX_LENGTH,
        })

        print()
        print("=== MLflow ===")
        print("Run ID   :", run.info.run_id)
        print("Run name :", run_name)

        print()
        print("=== Training ===")

        result = trainer.train()

        print()
        print("=== Result ===")
        print(result)

        print()
        print("PASS")


if __name__ == "__main__":
    main()

実行:

export PYTORCH_ENABLE_MPS_FALLBACK=1

uv run python scripts/train_qlora_mlflow.py

500 STEPへ変更する

Smoke Testが成功したら、

MAX_STEPS = 500

へ変更します。

合わせて、

save_steps=100
logging_steps=100

程度に変更します。

MLflow公式のTrainerもreport_to="mlflow"を利用し、Training parameterやmetricsをMLflowに記録します。

スクリーンショット 2026-08-12 0.02.34.png


STEP 6:LoRA AdapterをMLflowへ保存する

Trainingしただけでは、まだ学習モデルを再利用しづらい状態です。

そこでMLflowへModelとして保存します。

ポイントは、

Mistral 7B本体を毎回MLflowへコピーしない

ことです。

MLflowはPEFT Modelを保存するとき、Base ModelのWeightではなく、

LoRA Adapter
+
PEFT Config
+
Base ModelのHugging Face repository
+
commit hash

を記録します。

これはLoRAとの相性が非常に良い設計です。


既存checkpointを使う

例えば500 STEPまで学習した場合、

ls -d outputs/checkpoint-* | sort -V

結果:

outputs/checkpoint-100
outputs/checkpoint-200
outputs/checkpoint-300
outputs/checkpoint-400
outputs/checkpoint-500

なら、

checkpoint-500

を使います。

MLflowのRun IDを設定します。

export MLFLOW_RUN_ID="<STEP5で作成されたRun ID>"

scripts/log_checkpoint_to_mlflow.py 全文
scripts/log_checkpoint_to_mlflow.py
import os

import mlflow
import torch

from datasets import load_dataset

from mlflow.models import infer_signature

from peft import PeftModel

from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    BitsAndBytesConfig,
)


BASE_MODEL_ID = "mistralai/Mistral-7B-v0.1"

DATASET_ID = "b-mc2/sql-create-context"

CHECKPOINT = "./outputs/checkpoint-500"

MLFLOW_TRACKING_URI = "http://127.0.0.1:5000"

RUN_ID = os.environ["MLFLOW_RUN_ID"]


PROMPT_TEMPLATE = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

### Table:

{context}

### Question:

{question}

### Response:

{output}"""


quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16,
)


print("Loading base model...")

base_model = AutoModelForCausalLM.from_pretrained(
    BASE_MODEL_ID,
    quantization_config=quantization_config,
    device_map="mps",
)


print("Loading LoRA checkpoint...")

model = PeftModel.from_pretrained(
    base_model,
    CHECKPOINT,
)


tokenizer_no_pad = AutoTokenizer.from_pretrained(
    BASE_MODEL_ID,
    add_bos_token=True,
)


dataset = load_dataset(
    DATASET_ID,
    split="train",
)

split_dataset = dataset.train_test_split(
    test_size=0.2,
    seed=42,
)

train_dataset = split_dataset["train"]


def apply_prompt_template(row):

    return {
        "prompt": PROMPT_TEMPLATE.format(
            context=row["context"],
            question=row["question"],
            output=row["answer"],
        )
    }


train_dataset = train_dataset.map(
    apply_prompt_template
)


sample = train_dataset[1]


prompt_template = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

{prompt}

### Response:

"""


signature = infer_signature(
    model_input=sample["prompt"],
    model_output=sample["answer"],
    params={
        "max_new_tokens": 256,
        "repetition_penalty": 1.15,
        "return_full_text": False,
    },
)


mlflow.set_tracking_uri(
    MLFLOW_TRACKING_URI
)


with mlflow.start_run(
    run_id=RUN_ID
):

    model_info = mlflow.transformers.log_model(

        transformers_model={
            "model": model,
            "tokenizer": tokenizer_no_pad,
        },

        prompt_template=prompt_template,

        signature=signature,

        name="model",
    )


    print()
    print("=== MLflow Model ===")
    print("Run ID    :", RUN_ID)
    print("Model URI :", model_info.model_uri)


print()
print("PASS")

実行:

export MLFLOW_RUN_ID="<RUN_ID>"
export PYTORCH_ENABLE_MPS_FALLBACK=1

uv run python scripts/log_checkpoint_to_mlflow.py

実際に遭遇したエラー:torchvisionがない

このSTEPで実際に次のエラーが発生しました。

PackageNotFoundError:
No package metadata was found for torchvision

最終的には、

ModuleNotFoundError:
No module named 'torchvision'

で停止しました。

しかしログをよく見ると、

Loading base model...
Loading LoRA checkpoint...

までは成功しています。

さらに、

Overriding save_pretrained to False for PEFT models

も表示されていました。

つまり、

MPS       OK
4-bit     OK
Mistral   OK
LoRA      OK
MLflow    Model保存開始 OK

依存Package検出
       ↓
torchvisionなし
       ↓
ERROR

という状態です。

QLoRAの問題ではありません。

解決方法:

uv add torchvision

確認します。

uv run python - <<'PY'
import torch
import torchvision

print("torch       :", torch.__version__)
print("torchvision :", torchvision.__version__)
print("MPS         :", torch.backends.mps.is_available())
PY

その後、同じRun IDを使って再実行します。

export MLFLOW_RUN_ID="<RUN_ID>"
export PYTORCH_ENABLE_MPS_FALLBACK=1

uv run python scripts/log_checkpoint_to_mlflow.py

MLflowには何が保存されるのか

PEFT Modelでは概念的に、

model/
├── peft/
│   ├── adapter_config.json
│   └── adapter_model.safetensors
│
├── MLmodel
├── requirements.txt
├── python_env.yaml
└── ...

となります。

MLflow公式でもPEFT AdapterとConfig、Base Model repositoryとrevision、Prompt Template、SignatureなどをModel metadataに保持します。


STEP 7:MLflowからLoRA Modelをロードする

最後に保存したModelを読み戻します。

MLflow公式では、

mlflow.transformers.load_model()

または、

mlflow.pyfunc.load_model()

の2種類が紹介されています。


PyFuncでロードする

pyfuncのメリットは、

model.predict(...)

というMLflow共通Interfaceを使えることです。

さらにSTEP 6で保存したPrompt Templateや推論ParameterをModel側に持たせられます。


scripts/infer_mlflow_pyfunc.py 全文
scripts/infer_mlflow_pyfunc.py
import os

import mlflow


MLFLOW_TRACKING_URI = "http://127.0.0.1:5000"

RUN_ID = os.environ["MLFLOW_RUN_ID"]

MODEL_URI = f"runs:/{RUN_ID}/model"


mlflow.set_tracking_uri(
    MLFLOW_TRACKING_URI
)


print("=== MLflow Model ===")
print("Run ID    :", RUN_ID)
print("Model URI :", MODEL_URI)

print()
print("Loading MLflow PEFT model...")


mlflow_model = mlflow.pyfunc.load_model(
    MODEL_URI
)


print()
print("Model loaded.")


test_prompt = """
### Table:

CREATE TABLE table_name_50 (
    venue VARCHAR,
    away_team VARCHAR
)

### Question:

When Essendon played away; where did they play?
"""


print()
print("=== Input ===")
print(test_prompt)


result = mlflow_model.predict(
    test_prompt
)


print()
print("=== Raw Result ===")
print(result)


print()
print("=== Generated SQL ===")
print(result[0])


print()
print("PASS")

実行:

export MLFLOW_RUN_ID="<RUN_ID>"
export PYTORCH_ENABLE_MPS_FALLBACK=1

uv run python scripts/infer_mlflow_pyfunc.py

期待する方向のSQLは、

SELECT venue
FROM table_name_50
WHERE away_team = "essendon";

です。

生成AIなので完全な文字列一致を要求するものではありません。


MPSを明示してNative Transformersでロードする

M5 Maxで実行していることをより明確に確認したい場合は、

mlflow.transformers.load_model()

を使用します。


scripts/infer_mlflow_mps.py 全文
scripts/infer_mlflow_mps.py
import os

import mlflow
import torch


MLFLOW_TRACKING_URI = "http://127.0.0.1:5000"

RUN_ID = os.environ["MLFLOW_RUN_ID"]

MODEL_URI = f"runs:/{RUN_ID}/model"


if not torch.backends.mps.is_available():
    raise RuntimeError(
        "MPS is not available"
    )


print("=== Environment ===")
print("PyTorch :", torch.__version__)
print("MPS     :", torch.backends.mps.is_available())


mlflow.set_tracking_uri(
    MLFLOW_TRACKING_URI
)


print()
print("Loading MLflow Transformer model...")


pipeline = mlflow.transformers.load_model(
    MODEL_URI,
    return_type="pipeline",
    device="mps",
)


print()
print("=== Pipeline ===")
print("type   :", type(pipeline).__name__)
print("device :", pipeline.device)


prompt = """You are a powerful text-to-SQL model. Given the SQL tables and natural language question, your job is to write SQL query that answers the question.

### Table:

CREATE TABLE table_name_50 (
    venue VARCHAR,
    away_team VARCHAR
)

### Question:

When Essendon played away; where did they play?

### Response:

"""


print()
print("=== Inference ===")


result = pipeline(
    prompt,

    max_new_tokens=256,

    repetition_penalty=1.15,

    return_full_text=False,

    do_sample=False,
)


print()
print("=== Generated SQL ===")
print(result[0]["generated_text"])


print()
print("PASS")

実行結果:

export PYTORCH_ENABLE_MPS_FALLBACK=1

uv run python scripts/infer_mlflow_mps.py
=== Environment ===
PyTorch : 2.13.0
MPS     : True

Loading MLflow Transformer model...
Downloading artifacts:   0%|                                                                                                                                 | 0/1 [00:00<?, ?it/s]
Downloading artifacts: 100%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 10/10 [00:00<00:00, 10.48it/s]
2026/08/11 23:23:34 INFO mlflow.transformers: 'runs:/4ac3e7913d4241b49166a17b86f06958/model' resolved as 'mlflow-artifacts:/1/4ac3e7913d4241b49166a17b86f06958/artifacts/model'
Downloading artifacts:   0%|                                                                                                                                 | 0/1 [00:00<?, ?it/s]
Downloading artifacts:   0%|                                                                                                                                 | 0/1 [00:00<?, ?it/s]
Downloading artifacts: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 1/1 [00:00<00:00, 363.90it/s]
Warning: You are sending unauthenticated requests to the HF Hub. Please set a HF_TOKEN to enable higher rate limits and faster downloads.
Fetching 2 files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 2454.96it/s]
Fetching 2 files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 1496.10it/s]
Reconstruction complete: |                                                                                                                            |  0.00B /  0.00B            2026/08/11 23:23:36 WARNING mlflow.transformers.model_io: Could not specify device parameter for this pipeline type.Falling back to loading the model with the default device.      
Fetching 2 files: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 2/2 [00:00<00:00, 2481.84it/s]
Download complete: :                                                                                                                                           |  0.00B            
Reconstruction complete: |                                                                                                                            |  0.00B /  0.00B            
Download complete: :                                                                                                                                           |  0.00B            
Reconstruction complete: |                                                                                                                            |  0.00B /  0.00B            
Download complete: :                                                                                                                                           |  0.00B            
Reconstruction complete: |                                                                                                                            |  0.00B /  0.00B            
Loading weights: 100%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 291/291 [00:00<00:00, 18811.73it/s]
[transformers] `torch_dtype` is deprecated! Use `dtype` instead!

=== Pipeline ===
type   : TextGenerationPipeline
device : mps

=== Inference ===
[transformers] Passing `generation_config` together with generation-related arguments=({'max_new_tokens', 'do_sample', 'repetition_penalty'}) is deprecated and will be removed in future versions. Please pass either a `generation_config` object OR all generation parameters explicitly, but not both.
[transformers] Both `max_new_tokens` (=256) and `max_length`(=20) seem to have been set. `max_new_tokens` will take precedence. Please refer to the documentation for more information. (https://huggingface.co/docs/transformers/main/en/main_classes/text_generation)
[transformers] Ignoring clean_up_tokenization_spaces=True for BPE tokenizer TokenizersBackend. The clean_up_tokenization post-processing step is designed for WordPiece tokenizers and is destructive for BPE (it strips spaces before punctuation). Set clean_up_tokenization_spaces=False to suppress this warning, or set clean_up_tokenization_spaces_for_bpe_even_though_it_will_corrupt_output=True to force cleanup anyway.

=== Generated SQL ===
SELECT venue FROM table_name_50 WHERE away_team = "essendon"

### Table:

CREATE TABLE table_name_51 (
    date VARCHAR,
    home_team VARCHAR
)

### Question:

What day was the game when the home team was collingwood?

### Response:

SELECT date FROM table_name_51 WHERE home_team = "collingwood"

### Table:

CREATE TABLE table_name_52 (
    score VARCHAR,
    round VARCHAR
)

### Question:

What was the score of the game in round 3?

### Response:

SELECT score FROM table_name_52 WHERE round = 3

### Table:

CREATE TABLE table_name_53 (
    player VARCHAR,
    position VARCHAR
)

### Question:

Who plays as a forward?

### Response:

SELECT player FROM table_name_53 WHERE position = "forward"

### Table:

CREATE TABLE table_name_54 (
    player VARCHAR,
    position VAR

PASS


最終的に何ができたのか

最初は、

Apple M5 Max

しかありませんでした。

そこへ、

という一連のモデル開発環境を構築できました。


MLflowを使う意味

LoRAだけなら、PEFTだけでも実装できます。

MLflowを入れる意味は、

「どの設定で学習したModelなのか」を管理できること

です。

例えば、

Experiment
│
├─ Run A
│   ├─ r=16
│   ├─ alpha=32
│   └─ loss=...
│
├─ Run B
│   ├─ r=32
│   ├─ alpha=64
│   └─ loss=...
│
└─ Run C
    ├─ r=64
    ├─ alpha=128
    └─ loss=...

という比較ができます。

モデル開発では、

「動いた」

だけでは足りません。

重要なのは、

どのDatasetか

どのBase Modelか

どのLoRA設定か

どのLearning Rateか

どのLossになったか

どのModelを採用したか

を再現できることです。

これがMLflowの役目です。


NVIDIA GPUとApple Siliconの使い分け

今回の結果から、

Apple SiliconがあればNVIDIA GPUは不要

と結論付けるのは早計です。

大規模なTraining、Multi-GPU Training、CUDA向けに高度に最適化されたKernel、大規模Production用途ではNVIDIA GPUが依然重要です。

一方で、

LoRAを学びたい

QLoRAを試したい

PromptとFine-tuningを比較したい

MLflowを学びたい

7BクラスでPoCしたい

自社DatasetでModelを試作したい

という用途なら、

まず手元のApple Siliconで始める

という選択肢はかなり現実的になっています。

Transformers自身もApple SiliconのUnified MemoryとMPSを利用したローカルTrainingをサポートしています。


今回のポイントを整理

今回、MLflow公式TutorialをM5 Maxへ移植するために変更したポイントは、実はそれほど多くありません。

MLflow公式

NVIDIA A10G
     ↓
CUDA
     ↓
bitsandbytes
     ↓
QLoRA

を、

M5 Max版

Apple M5 Max
     ↓
Metal / MPS
     ↓
bitsandbytes MPS
     ↓
QLoRA

へ変更しました。

大きな変更点は、

CUDA
↓
MPS

と、

paged_adamw_8bit
↓
adamw_torch

です。

一方、

Mistral-7B

Text-to-SQL Dataset

4-bit NF4

LoRA r=32

LoRA alpha=64

MLflow

Prompt Template

Model Signature

は、できるだけMLflow公式Tutorialの考え方を維持しました。


今回作成したファイル

最終的なProjectは次のようになります。

mlflow-m5max-qlora/
│
├── .python-version
├── pyproject.toml
├── uv.lock
│
├── scripts/
│   │
│   ├── check_mps.py
│   │
│   ├── check_mlflow_stack.py
│   │
│   ├── prepare_dataset.py
│   │
│   ├── load_base_model.py
│   │
│   ├── test_base_model.py
│   │
│   ├── define_peft_model.py
│   │
│   ├── train_qlora_mlflow.py
│   │
│   ├── log_checkpoint_to_mlflow.py
│   │
│   ├── infer_mlflow_pyfunc.py
│   │
│   └── infer_mlflow_mps.py
│
├── outputs/
│   ├── checkpoint-100/
│   ├── checkpoint-200/
│   └── ...
│
└── README.md

まとめ

今回一番伝えたかったことは、

LLMのモデル開発を学ぶために、最初から高価なNVIDIA GPUを用意する必要はない

ということです。

Apple Siliconには、

Unified Memory
+
Metal
+
MPS

があります。

さらに現在では、

PyTorch
+
Transformers
+
bitsandbytes
+
PEFT
+
MLflow

の組み合わせで、M5 Max上でも、

Dataset準備
↓
Quantization
↓
LoRA
↓
QLoRA Training
↓
Experiment Tracking
↓
Model保存
↓
Model再ロード
↓
Inference

というモデル開発の一連の流れを経験できます。

Fine-tuningそのものより重要なのは、

学習 → 評価 → 比較 → 保存 → 再利用

というLoopを作ることです。

そして、このLoopを管理するのがMLflowです。

次は、

Base Model
      VS
LoRA Model

をMLflow Evaluateで定量比較したり、

r=16
r=32
r=64

を複数Runとして比較していくと、単なる「LoRAを動かしてみた」から、

実際のモデル開発

へ一段進めます。


参考にした公式資料

  • MLflow — Fine-Tuning Open-Source LLM using QLoRA with MLflow and PEFT
  • MLflow — Transformers Flavor
  • MLflow — PEFT Models / Transformers Integration
  • Hugging Face Transformers — Apple Silicon
  • Hugging Face Accelerate — MPS Training
  • PyTorch — MPS backend
  • bitsandbytes — Apple Silicon / MPS support
  • Qiita Markdown

MLflow公式チュートリアルではMistral 7B、4-bit NF4、PEFT/LoRA、Transformers Trainer、MLflow Model保存・再ロードという一連の流れが説明されています。

MLflowはPEFTモデルについて、Adapter WeightとConfigを保存し、Base ModelについてはHugging Face repositoryとrevisionをModel metadataへ記録する方式を採っています。

Apple SiliconではPyTorch MPSを利用でき、TransformersもMPS backendによるTrainingをサポートしています。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?