•22 min read

UnslothとLoRAによるDeepSeek R1のファインチューニング:メモリ効率の良い推論モデル

UnslothとLoRAによるDeepSeek R1のファインチューニング:メモリ効率の良い推論モデル

DeepSeek-R1-Distillモデルは、推論能力と効率性の魅力的なバランスを提供します。これらのモデルを特定のドメインやタスク向けにファインチューニングする場合、特に大規模なバリアントでは、VRAMとトレーニングスループットの課題に直面することがよくあります。このガイドでは、UnslothとLoRA/QLoRAを使用してDeepSeek-R1-Distillをファインチューニングするための、本番環境レベルの手法を詳述します。これにより、カスタムTritonカーネルを活用してVRAMを大幅に削減し、トレーニングを高速化します。データ準備、モデルトレーニング、およびGGUFとvLLMのエクスポートを含むデプロイメントの考慮事項について説明します。

Audio Briefing
0:00 / 0:00

DeepSeek-R1-Distill アーキテクチャの概要

DeepSeek-R1-Distillは、より大規模で高性能なDeepSeek-R1モデルから蒸留された、オープンソースの推論モデルシリーズです。これらは複雑な推論タスクで優れた性能を発揮するように設計されており、最終的な回答を生成する前に、明示的な思考連鎖(CoT: Chain-of-Thought)推論プロセスを区切るために<think>トークンを使用することがよくあります。この明示的なCoT構造は、解釈可能性とデバッグにとって非常に重要であり、ファインチューニング中にこれを維持することが最も重要です。これらのモデルは通常、マルチヘッドアテンションとフィードフォワードネットワークを利用したトランスフォーマーアーキテクチャに基づいています。

Advertisement

Unsloth: VRAM効率と速度

Unslothは、大規模言語モデルのLoRA/QLoRAファインチューニングを高速化および最適化するために設計されたライブラリです。いくつかの主要なイノベーションにより、大幅なVRAM削減と高速化を実現しています。

  1. カスタムTritonカーネル: Unslothは、標準のPyTorchアテンションカーネルとオプティマイザーカーネルを、高度に最適化されたTriton実装に置き換えます。これらのカーネルはLoRA/QLoRA向けに特別に設計されており、メモリオーバーヘッドを削減し、計算効率を向上させます。
  2. 勾配チェックポイントの最適化: PyTorchの勾配チェックポイントはメモリを節約しますが、Unslothは必要なアクティベーションのみをチェックポイントすることで、再計算のオーバーヘッドを削減し、さらに最適化します。
  3. 量子化対応トレーニング: 4ビットおよび8ビット量子化(QLoRA)とのシームレスな統合により、VRAM要件がさらに削減されます。

これらの最適化により、DeepSeek-R1-DistillのようなモデルをコンシューマーグレードのGPUで、またはプロフェッショナルなハードウェアでより大きなバッチサイズでファインチューニングすることが可能になります。

データ準備: <think> CoTの維持

DeepSeek-R1-Distillモデルは、推論を構造化するために特定の<think>トークンを使用するようにトレーニングされています。ファインチューニングデータを準備する際には、この形式を維持することが重要です。合成データ生成や慎重なアノテーションがしばしば必要となります。

CoTを含む典型的なDeepSeek-R1-Distillの会話ターンは次のようになります。

User: <prompt>
Assistant: <think>Thought process leading to the answer.</think><answer>Final answer.</answer>

ファインチューニングのために、データを会話ターンにフォーマットし、<think>と<answer>タグが正しく配置されていることを確認する必要があります。

データ形式の例

数学的推論問題のデータセットを考えてみましょう。各エントリは、会話を表す辞書のリストとして構造化する必要があります。

[
  {
    "messages": [
      {
        "role": "user",
        "content": "What is the sum of the first 10 prime numbers?"
      },
      {
        "role": "assistant",
        "content": "<think>The first 10 prime numbers are 2, 3, 5, 7, 11, 13, 17, 19, 23, 29. Summing them: 2+3+5+7+11+13+17+19+23+29 = 129.</think><answer>129</answer>"
      }
    ]
  },
  {
    "messages": [
      {
        "role": "user",
        "content": "If a car travels at 60 mph for 2 hours, how far does it travel?"
      },
      {
        "role": "assistant",
        "content": "<think>Distance = Speed × Time. Speed = 60 mph, Time = 2 hours. Distance = 60 * 2 = 120 miles.</think><answer>120 miles</answer>"
      }
    ]
  }
]

このJSON構造は、datasetsライブラリとUnslothのデータローダーと互換性があります。

Unslothを使用したDeepSeek-R1-Distillのファインチューニング

このセクションでは、Unslothを使用してdeepseek-ai/deepseek-r1-3b-baseをファインチューニングするための、完全で実行可能な例を提供します。この原則は、より大規模なDeepSeek-R1-Distillモデルにも適用されます。

セットアップ

まず、Unslothとその他の必要なライブラリをインストールします。

pip install "unsloth[cu121] @ git+https://github.com/unslothai/unsloth.git"
pip install transformers peft accelerate bitsandbytes trl datasets torch

トレーニングスクリプト

import torch
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments, AutoTokenizer
from datasets import load_dataset
import os

# 1. Configuration
max_seq_length = 2048 # Max sequence length for DeepSeek-R1-Distill
model_name = "deepseek-ai/deepseek-r1-3b-base" # Or deepseek-ai/deepseek-r1-7b-base
dataset_path = "your_synthetic_reasoning_data.json" # Path to your JSON dataset

# 2. Load Model and Tokenizer with Unsloth
# Unsloth automatically handles 4-bit quantization (QLoRA)
# and loads the model with optimized kernels.
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_name,
    max_seq_length = max_seq_length,
    dtype = None, # None for auto detection (bfloat16 if supported, else float16)
    load_in_4bit = True, # Enable QLoRA
)

# 3. Configure LoRA Adapters
# Target all linear layers for optimal performance.
model = FastLanguageModel.get_peft_model(
    model,
    r = 16, # LoRA rank
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = 16,
    lora_dropout = 0.05,
    bias = "none",
    use_gradient_checkpointing = "unsloth", # Use Unsloth's optimized gradient checkpointing
    random_state = 3407,
    max_seq_length = max_seq_length,
)

# 4. Load and Format Dataset
# The dataset should be a JSON file with the structure described above.
# We use `apply_chat_template` to format messages into the model's expected input format.
# DeepSeek models typically use a specific chat template.
# Ensure the tokenizer has a chat template or define one.
# For DeepSeek, it's often similar to:
# {% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>
# ' + message['content'] + '<|EOT|>
# ' }}{% elif message['role'] == 'assistant' %}{{ '<|Bot|>
# ' + message['content'] + '<|EOT|>
# ' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Bot|>' }}{% endif %}

# If your tokenizer doesn't have a default, you might need to set it:
# tokenizer.chat_template = "{% for message in messages %}{% if message['role'] == 'user' %}{{ '<|User|>\n' + message['content'] + '<|EOT|>\n' }}{% elif message['role'] == 'assistant' %}{{ '<|Bot|>\n' + message['content'] + '<|EOT|>\n' }}{% endif %}{% endfor %}{% if add_generation_prompt %}{{ '<|Bot|>' }}{% endif %}"

def formatting_prompts_func(examples):
    """
    Formats the dataset examples into the model's expected chat template.
    Ensures the <think> and <answer> tokens are preserved within the assistant's response.
    """
    texts = []
    for i in range(len(examples["messages"])):
        # Apply chat template to each conversation
        # `add_generation_prompt=False` is crucial for fine-tuning to ensure
        # the assistant's response is fully included, not just the prompt for generation.
        formatted_text = tokenizer.apply_chat_template(
            examples["messages"][i],
            tokenize=False,
            add_generation_prompt=False
        )
        texts.append(formatted_text)
    return { "text" : texts }

dataset = load_dataset("json", data_files=dataset_path, split="train")
dataset = dataset.map(
    formatting_prompts_func,
    batched = True,
)

# 5. Configure Training Arguments
trainer = SFTTrainer(
    model = model,
    tokenizer = tokenizer,
    train_dataset = dataset,
    dataset_text_field = "text", # Field containing the formatted text
    max_seq_length = max_seq_length,
    dataset_num_proc = os.cpu_count(), # Use all CPU cores for data processing
    packing = False, # Set to True for more efficient packing of short sequences
    args = TrainingArguments(
        per_device_train_batch_size = 2, # Adjust based on VRAM
        gradient_accumulation_steps = 4, # Accumulate gradients over 4 steps
        warmup_steps = 5,
        num_train_epochs = 3,
        learning_rate = 2e-4,
        fp16 = not torch.cuda.is_bf16_supported(), # Use fp16 if bfloat16 not supported
        bf16 = torch.cuda.is_bf16_supported(), # Use bf16 if supported
        logging_steps = 1,
        optim = "adamw_8bit", # Use 8-bit AdamW optimizer
        weight_decay = 0.01,
        lr_scheduler_type = "linear",
        seed = 3407,
        output_dir = "outputs",
        report_to = "none", # Disable reporting to W&B etc. for simplicity
    ),
)

# 6. Train the Model
trainer.train()

# 7. Save the LoRA Adapters
model.save_pretrained("deepseek_r1_lora_adapters")
tokenizer.save_pretrained("deepseek_r1_lora_adapters")

print("Fine-tuning complete. LoRA adapters saved to deepseek_r1_lora_adapters.")

主要なパラメータの説明:

  • max_seq_length: メモリにとって非常に重要です。DeepSeek-R1-Distillモデルは、より大きなコンテキストでトレーニングされることがよくあります。データとVRAMに基づいて調整してください。
  • load_in_4bit = True: QLoRAをアクティブにし、ベースモデルを4ビット精度でロードします。これが主要なVRAM節約メカニズムです。
  • target_modules: トランスフォーマーアーキテクチャのどの線形層にLoRAアダプターが適用されるかを指定します。q_proj、k_proj、v_proj、o_proj、gate_proj、up_proj、down_projをターゲットにすることは、包括的なファインチューニングの標準です。
  • use_gradient_checkpointing = "unsloth": Unslothの最適化された勾配チェックポイントを活用します。
  • formatting_prompts_func: この関数は非常に重要です。構造化されたデータを受け取り、<think>と<answer>タグを含む、モデルが期待する正確な文字列形式に変換します。tokenizer.apply_chat_templateメソッドがこれを行う推奨される方法です。
  • per_device_train_batch_sizeとgradient_accumulation_steps: これらをGPUのVRAMに合わせて調整します。より小さなper_device_train_batch_sizeとgradient_accumulation_stepsを組み合わせることで、ピークVRAMを増やすことなく、より大きな実効バッチサイズを可能にします。
  • optim = "adamw_8bit": 8ビットAdamWオプティマイザーを使用し、オプティマイザーの状態メモリをさらに削減します。
Advertisement

デプロイメント: GGUFとvLLMのエクスポート

ファインチューニング後、LoRAアダプターが作成されます。デプロイメントでは、通常、これらのアダプターをベースモデルの重みにマージし、適切な形式に変換します。

LoRAアダプターのマージ

import torch
from unsloth import FastLanguageModel
from transformers import AutoTokenizer

model_name = "deepseek-ai/deepseek-r1-3b-base"
lora_adapters_path = "deepseek_r1_lora_adapters"
output_merged_path = "deepseek_r1_merged_model"

# Load the base model and tokenizer
model, tokenizer = FastLanguageModel.from_pretrained(
    model_name = model_name,
    max_seq_length = 2048, # Must match training max_seq_length
    dtype = None, # Use the same dtype as during training or float16/bfloat16
    load_in_4bit = False, # Load in full precision for merging
)

# Load the LoRA adapters
model = FastLanguageModel.get_peft_model(
    model,
    r = 16, # Must match training r
    target_modules = ["q_proj", "k_proj", "v_proj", "o_proj",
                      "gate_proj", "up_proj", "down_proj"],
    lora_alpha = 16,
    lora_dropout = 0.05,
    bias = "none",
    use_gradient_checkpointing = False, # Not needed for inference/merging
    random_state = 3407,
    max_seq_length = 2048, # Must match training max_seq_length
)
model.load_adapter(lora_adapters_path)

# Merge LoRA adapters and save the full model
model.save_pretrained_merged(output_merged_path, tokenizer, save_method = "merged_16bit")
print(f"Merged model saved to {output_merged_path}")

CPU/エッジデプロイメントのためのGGUFエクスポート

GGUFは、llama.cppとそのバインディングを使用したCPU推論に最適化された形式です。さらなるメモリ削減のために量子化(例:Q4_K_M、Q5_K_M)をサポートしています。

import os
from transformers import AutoTokenizer, AutoModelForCausalLM
from huggingface_hub import HfApi, create_repo
from pathlib import Path

merged_model_path = "deepseek_r1_merged_model"
output_gguf_path = "deepseek_r1_merged_model_gguf"
quantization_type = "q4_k_m" # Example: q4_k_m, q5_k_m, q8_0

# Ensure llama.cpp is installed and converted.py is available
# You might need to clone llama.cpp and build it:
# git clone https://github.com/ggerganov/llama.cpp.git
# cd llama.cpp && make

# Load the merged model
model = AutoModelForCausalLM.from_pretrained(
    merged_model_path,
    torch_dtype=torch.float16, # Or bfloat16
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(merged_model_path)

# Save in a format compatible with llama.cpp's convert.py
# This typically means saving as a standard Hugging Face model
model.save_pretrained(output_gguf_path)
tokenizer.save_pretrained(output_gguf_path)

# Convert to GGUF using llama.cpp's convert.py
# Make sure you have llama.cpp cloned and built, and its `convert.py` is accessible.
# Adjust `llama_cpp_dir` to your llama.cpp installation path.
llama_cpp_dir = "/path/to/llama.cpp" # IMPORTANT: Set this path
convert_script = os.path.join(llama_cpp_dir, "convert.py")

if not os.path.exists(convert_script):
    raise FileNotFoundError(f"llama.cpp convert.py not found at {convert_script}. "
                            "Please clone and build llama.cpp.")

# Step 1: Convert PyTorch weights to ggml format (intermediate step for convert.py)
# This step is often implicitly handled by newer convert.py versions,
# but historically involved a separate script or specific arguments.
# For DeepSeek, ensure convert.py supports its architecture.
print(f"Converting {output_gguf_path} to GGUF...")
os.system(f"python {convert_script} {output_gguf_path} --outfile {output_gguf_path}/model.gguf")

# Step 2: Quantize the GGUF model
quantize_script = os.path.join(llama_cpp_dir, "quantize")
if not os.path.exists(quantize_script):
    # For older llama.cpp versions, quantize might be part of convert.py or a separate script
    # For newer versions, `quantize` is a compiled binary.
    # If not found, try `python {convert_script} --quantize {quantization_type} ...`
    raise FileNotFoundError(f"llama.cpp quantize binary not found at {quantize_script}. "
                            "Please build llama.cpp or check its documentation for quantization.")

os.system(f"{quantize_script} {output_gguf_path}/model.gguf {output_gguf_path}/model-{quantization_type}.gguf {quantization_type}")

print(f"GGUF model ({quantization_type}) saved to {output_gguf_path}/model-{quantization_type}.gguf")

# Optional: Upload to Hugging Face Hub
# api = HfApi()
# repo_id = "your_hf_username/deepseek-r1-3b-reasoning-gguf"
# create_repo(repo_id, repo_type="model", exist_ok=True)
# api.upload_file(
#     path_or_fileobj=f"{output_gguf_path}/model-{quantization_type}.gguf",
#     path_in_repo=f"deepseek-r1-3b-reasoning-{quantization_type}.gguf",
#     repo_id=repo_id,
# )
# print(f"GGUF model uploaded to Hugging Face Hub: {repo_id}")

高スループットGPU推論のためのvLLMエクスポート

vLLMは、LLM向けの最適化された推論エンジンであり、連続バッチ処理とPagedAttentionにより高スループットを提供します。Hugging Face transformersモデルを直接使用します。

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM

merged_model_path = "deepseek_r1_merged_model"
output_vllm_path = "deepseek_r1_vllm_model"

# Load the merged model and tokenizer
model = AutoModelForCausalLM.from_pretrained(
    merged_model_path,
    torch_dtype=torch.bfloat16, # Use bfloat16 for vLLM if supported, else float16
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(merged_model_path)

# Save the model in a format vLLM can directly load
# vLLM expects a standard Hugging Face model directory.
model.save_pretrained(output_vllm_path)
tokenizer.save_pretrained(output_vllm_path)

print(f"vLLM-compatible model saved to {output_vllm_path}")

# Example vLLM inference (requires vLLM to be installed)
# from vllm import LLM, SamplingParams
#
# llm = LLM(model=output_vllm_path, dtype=torch.bfloat16)
#
# prompts = [
#     "<|User|>\nWhat is the capital of France?<|EOT|>\n<|Bot|>",
#     "<|User|>\nIf x = 5 and y = 3, what is x + y?<|EOT|>\n<|Bot|>"
# ]
#
# sampling_params = SamplingParams(temperature=0.7, top_p=0.95, max_tokens=128)
#
# outputs = llm.generate(prompts, sampling_params)
#
# for output in outputs:
#     prompt = output.prompt
#     generated_text = output.outputs[0].text
#     print(f"Prompt: {prompt!r}, Generated text: {generated_text!r}")

アーキテクチャとトレードオフ

機能Unsloth LoRA/QLoRAフルファインチューニングGGUF (llama.cpp)vLLM
VRAM使用量 (トレーニング)非常に低い (4ビット)非常に高い (16/32ビット)N/AN/A
トレーニング速度速い (Tritonカーネル)標準N/AN/A
モデルサイズ (ディスク)アダプターのみ (MB)フルモデル (GB)量子化済み (GB)フルモデル (GB)
推論レイテンシ中程度 (アダプターマージ)低い低い (CPU)非常に低い (GPU)
推論スループット中程度高い中程度 (CPU)非常に高い (GPU)
ハードウェアコンシューマーGPUハイエンドGPUCPU、エッジデバイスハイエンドGPU
複雑さ低い (Unsloth抽象化)中程度中程度 (変換)低い (HF互換)
ユースケース迅速なイテレーション、リソース制約のあるトレーニング最大性能、大規模データセットローカル/オフライン、CPUのみ大規模API、GPUのみ

本番環境での注意点とトラブルシューティング

  1. トレーニング中のVRAM OOM:
    • 症状: CUDA out of memoryエラー。
    • 修正:
      • per_device_train_batch_sizeを減らす。
      • 小さいバッチサイズを補うためにgradient_accumulation_stepsを増やす。
      • max_seq_lengthを減らす。
      • load_in_4bit = Trueとoptim = "adamw_8bit"が設定されていることを確認する。
      • use_gradient_checkpointing = "unsloth"を確認する。
      • packing=Trueを使用している場合、データセットに十分に長いシーケンスが含まれていることを確認するか、シーケンスが非常に短い場合はパッキングを無効にする(パッキングはピークメモリを増加させることがあるため)。
  2. 不正確な<think>/<answer>フォーマット:
    • 症状: モデルが構造化されていないテキストを生成する、<think>タグを無視する、または不正な形式の応答を生成する。
    • 修正: formatting_prompts_funcと生のJSONデータを再確認する。<think>と<answer>タグがベースモデルが期待する通りであり、トレーニングデータのアシスタントの応答内に正しく囲まれていることを確認する。ここではtokenizer.apply_chat_templateが重要です。
  3. トレーニングが遅い:
    • 症状: トレーニングに予想よりも大幅に時間がかかる。
    • 修正:
      • UnslothがCUDAサポート(unsloth[cu121])で正しくインストールされていることを確認する。
      • torch.cuda.is_available()がTrueを返すことを確認する。
      • nvidia-smiでGPU使用率を確認する。使用率が低い場合は、データロードのボトルネックを示している可能性がある(dataset_num_procを増やす)。
      • シーケンスが短い場合は、GPU使用率を向上させるためにpacking=Trueを確認する。
  4. GGUF変換の問題:
    • 症状: convert.pyが失敗するか、使用できないGGUFファイルを生成する。
    • 修正:
      • llama.cppがクローンされ、正しくビルドされていることを確認する。
      • convert.pyスクリプトとquantizeバイナリのパスが正しいことを確認する。
      • 新しいアーキテクチャのサポートは進化しているため、DeepSeekモデルの最新の変換手順についてはllama.cppのGitHubを確認する。
      • 一部のモデルでは、特定のconvert.pyバージョンまたはフラグが必要です。
  5. vLLM推論エラー:
    • 症状: vLLMがモデルをロードできない、または誤った出力を生成する。
    • 修正:
      • マージされたモデルが、vLLMが認識できる標準のHugging Face形式で保存されていることを確認する。
      • vLLMでロードするために使用されるtorch_dtypeが、マージされたモデルのdtype(例:bfloat16)と一致することを確認する。
      • DeepSeekとの特定のモデル互換性または既知の問題については、vLLMのドキュメントを確認する。

よくある質問

  1. Unslothを使用して、単一の24GB GPU(例:RTX 3090/4090)でDeepSeek-R1-Distill-7Bをファインチューニングできますか? はい、Unslothの4ビットQLoRAと最適化された勾配チェックポイントを使用すれば、単一の24GB GPUでDeepSeek-R1-Distill-7Bをファインチューニングすることは可能です。おそらく、小さなper_device_train_batch_size(例:1または2)を使用し、gradient_accumulation_stepsで補う必要があるでしょう。
  2. ファインチューニング後もモデルが<think>トークンを使用し続けるようにするにはどうすればよいですか? 最も重要なステップは、ファインチューニングデータセットに、アシスタントの応答に<think>と<answer>トークンがベースモデルが期待する形式で明示的に含まれていることを確認することです。tokenizer.apply_chat_template関数をadd_generation_prompt=Falseと組み合わせて正しく使用することで、これを強制するのに役立ちます。推論時には、同じチャットテンプレートでモデルにプロンプトを出し、<think>ブロックを生成することを期待する必要があります。
  3. 4ビット量子化(QLoRA)がDeepSeek-R1-Distillに与える性能への影響はどれくらいですか? 4ビット量子化はわずかな精度損失をもたらしますが、多くの推論タスクでは、性能の劣化は最小限であり、大幅なVRAM削減とより大きなモデルをファインチューニングできる能力によって相殺されることがよくあります。DeepSeekモデルは一般的に量子化に対して堅牢です。ファインチューニングされた4ビットモデルを特定のタスクメトリクスに対して評価することをお勧めします。
  4. Unslothをフルファインチューニング(LoRAだけでなく)に使用できますか? Unslothは主にLoRA/QLoRAファインチューニング用に設計されています。最適化されたカーネルを提供しますが、VRAM削減と速度における主な利点は、LoRAのパラメータ効率的な性質に関連しています。フルファインチューニングの場合、通常は標準のHugging Face transformersトレーニングを使用しますが、これには大幅に多くのVRAMが必要になります。
  5. max_seq_lengthを超える非常に長い推論チェーンをどのように処理すればよいですか? 推論チェーンが頻繁にmax_seq_lengthを超える場合、いくつかの選択肢があります。
    • VRAMが許せばmax_seq_lengthを増やす。
    • データセット内の過度に長い推論チェーンを切り詰める(ただし、推論の品質に影響を与える可能性があります)。
    • モデルアーキテクチャがサポートしている場合は、「スライディングウィンドウアテンション」のような技術を検討するか、より大きなネイティブコンテキストウィンドウを持つモデルでファインチューニングする。DeepSeek-R1-Distillの場合、max_seq_lengthを増やすのが最も直接的なアプローチです。

このガイドは、DeepSeek-R1-Distillモデルを効率的にファインチューニングするための堅牢なフレームワークを提供します。Unslothの最適化を活用し、データフォーマットを慎重に管理することで、限られたハードウェアリソースでも強力で専門的な推論モデルをデプロイできます。

Share this article:

Stay Updated

Get the latest posts delivered straight to your inbox.

Free Developer Utilities

Free In-Browser Developer Tools

Clean AI CLI logs, build cron expressions, decode JWTs, and calculate chmod permissions offline.

Explore Tools
Advertisement