Amazon EC2でDeepSeekをfine-tuningしてみる

Amazon EC2でDeepSeekをfine-tuningしてみる

Clock Icon2025.01.30

Introduction

現在当たり前になったOpenAI/Anthropic/Google等のLLMですが、ビジネスからプライベートまで
どこでも活用されるようになってきました。ChatGPTの登場以降、AIとの対話が当たり前になり、
(私は)LLMがないと仕事ができないほどの状況になっています。

そんな中、先日ニュースになったのが、中国のAIスタートアップ「DeepSeek」が開発したモデル「DeepSeek-R1」です。
これはOpenAIのo1と同等性能を持つといわれており、OSS(MITライセンス)で公開しています。

本稿はDeepSeek-R1モデルをローカル(EC2)で動かしてみた作業ログです。

Local LLM

私の場合、ChatGPTやClaudeをWebブラウザやアプリ、APIを通じて利用しています。
これらのサービスは非常に手軽ですが、セキュリティ面での懸念や利用コストの問題があります。

それに対して、自分でマシンを用意し、そこでLLMを構築・運用する方法があります。

こうすることで、データを手元で管理できますし、
トークン数を気にせず利用できます。

多少ハイスペックなマシンと初期環境のセットアップは必要ですが、
一度環境を整えてしまえば、自由度の高いAIが低コストで使用可能になります。

Environment

  • MacBook Pro (14-inch, M3, 2023)
  • OS : MacOS 14.5
  • aws-cli : 2.15.56

AWSアカウントはセットアップ済みであるとします。

Setup

まずはローカルLLMを構築するためにEC2インスタンスを起動し、
Ollamaをつかって適当なモデルを動かしてみます。

※Ollamaは公開されているモデルをローカルマシンで動かすことの出来るツール

Create Server & Setup Ollama

AWSコンソールでもcliでもよいので、LLMを実行するEC2を起動します。
Claudeに聞いたところ、インスタンスインスタンスタイプは以下がおすすめとのこと。

  • GPU不要の場合: t3.xlarge (4 vCPU, 16GB RAM)
  • GPUありの場合: g4dn.xlarge (4 vCPU, 16GB RAM, NVIDIA T4 GPU)

GPUありで構築するので、g4dn.xlargeをAmazon Linux 2023にしました。
なお、永続ストレージとしてEBSボリューム(/dev/xvdf)を追加します。

% aws ec2 run-instances \
    --region us-east-1 \
    --image-id ami-0df8c184d5f6ae949 \
    --instance-type g4dn.xlarge \
    --key-name <your key name> \
    --security-group-ids <セキュリティグループ> \
    --subnet-id <サブネットID> \
    --block-device-mappings '[
        {
            "DeviceName": "/dev/xvda",
            "Ebs": {
                "VolumeSize": 20,
                "VolumeType": "gp3",
                "DeleteOnTermination": true
            }
        },
        {
            "DeviceName": "/dev/xvdf",
            "Ebs": {
                "VolumeSize": 125,
                "VolumeType": "gp3",
                "DeleteOnTermination": false
            }
        }
    ]' \
    --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=ollama-gpu}]' \
    --count 1 \
    --associate-public-ip-address

いろいろソフトウェアやモデルをローカルにダウンロードするので、
追加ボリューム(/dev/xvdf)を設定しています。

インスタンスの詳細情報

インスタンスが起動したら構築準備OKです。

% aws ec2 describe-instances \
    --region us-east-1 \
    --filters "Name=tag:Name,Values=ollama-gpu" \
    --query 'Reservations[*].Instances[*].[InstanceId,State.Name,PublicIpAddress,InstanceType]' \
    --output table
-------------------------------------------------------------------
|                        DescribeInstances                        |
+----------------------+----------+---------------+---------------+
|  i-xxxxxxxxxxxxxxxxx |  running |  xx.xx.xx.xx |  g4dn.xlarge  |
+----------------------+----------+---------------+---------------+

# ボリューム確認
# <INSTANCE_ID>は、上のコマンドで取得したIDに置換
aws ec2 describe-volumes \
    --region us-east-1 \
    --filters "Name=attachment.instance-id,Values=<INSTANCE_ID>" \
    --query 'Volumes[*].[VolumeId,Size,State,Attachments[0].Device,Attachments[0].DeleteOnTermination]' \
    --output table

------------------------------------------------------------------
|                         DescribeVolumes                        |
+------------------------+------+---------+------------+---------+
|  vol-xxxxxxxxxxxxxxxxx |  125 |  in-use |  /dev/xvdf |  False  |
|  vol-xxxxxxxxxxxxxxxxx |  20  |  in-use |  /dev/xvda |  True   |
+------------------------+------+---------+------------+---------+

インスタンスが起動したらsshログインして作業します。
まずは追加したボリュームを使えるように以下コマンドを実行します。

% sudo mkfs -t xfs /dev/nvme1n1
% sudo mkdir -p /mnt/data
% sudo mount /dev/nvme1n1 /mnt/data
% echo '/dev/nvme1n1 /mnt/data xfs defaults 0 0' | sudo tee -a /etc/fstab
% sudo chown -R ec2-user:ec2-user /mnt/data

# 確認
% df -h
Filesystem        Size  Used Avail Use% Mounted on
devtmpfs          4.0M     0  4.0M   0% /dev
tmpfs             7.8G     0  7.8G   0% /dev/shm
tmpfs             3.1G  464K  3.1G   1% /run
/dev/nvme0n1p1     20G  1.7G   19G   9% /
tmpfs             7.8G     0  7.8G   0% /tmp
/dev/nvme0n1p128   10M  1.3M  8.7M  13% /boot/efi
tmpfs             1.6G     0  1.6G   0% /run/user/1000
/dev/nvme1n1      125G  926M  125G   1% /mnt/data

追加ボリュームをXFSでフォーマットして/mnt/dataにマウントし、
権限変更しておきます。

次はNVIDIAドライバーとCUDAのインストールをします。
途中たりないモジュールがあれば適宜追加して実行していきましょう。

# システムアップデート
% sudo yum update -y

# アプリケーション用ディレクトリの作成
% mkdir -p /mnt/data/apps
% mkdir -p /mnt/data/apps/cuda
% mkdir -p /mnt/data/apps/ollama
% mkdir -p /mnt/data/cache
% mkdir -p /mnt/data/tmp

# 所有権の設定
% sudo chown -R ec2-user:ec2-user /mnt/data/apps
% sudo chown -R ec2-user:ec2-user /mnt/data/cache
% sudo chown -R ec2-user:ec2-user /mnt/data/tmp

# 環境変数の設定
% echo 'export CUDA_HOME=/mnt/data/apps/cuda' >> ~/.bashrc
% echo 'export PATH=$CUDA_HOME/bin:$PATH' >> ~/.bashrc
% echo 'export LD_LIBRARY_PATH=$CUDA_HOME/lib64:$LD_LIBRARY_PATH' >> ~/.bashrc
% echo 'export OLLAMA_MODELS=/mnt/data/apps/ollama/models' >> ~/.bashrc
% echo 'export TMPDIR=/mnt/data/tmp' >> ~/.bashrc

# 設定の反映
% source ~/.bashrc

# DNFの設定を変更
% sudo tee /etc/dnf/dnf.conf <<EOF
[main]
gpgcheck=1
installonly_limit=2
clean_requirements_on_remove=True
best=True
skip_if_unavailable=True
keepcache=1
cachedir=/mnt/data/cache/dnf
EOF

# キャッシュディレクトリの作成
% sudo mkdir -p /mnt/data/cache/dnf
% sudo chown -R ec2-user:ec2-user /mnt/data/cache/dnf

# NVIDIAドライバーとCUDAのインストール
% cd /mnt/data/apps/cuda

# CUDAリポジトリの設定
% sudo dnf config-manager --add-repo=https://developer.download.nvidia.com/compute/cuda/repos/amzn2023/x86_64/cuda-amzn2023.repo

# 必要なパッケージのインストール
% sudo dnf install -y dkms kernel-devel kernel-modules-extra
% sudo systemctl enable --now dkms

# ドライバーとツールのインストール
% sudo dnf install -y nvidia-driver nvidia-driver-cuda
% sudo dnf install -y vulkan-devel libglvnd-devel elfutils-libelf-devel xorg-x11-server-Xorg

# CUDAインストーラのダウンロードディレクトリに移動
% cd /mnt/data/apps/cuda

# CUDAインストーラのダウンロード
% wget https://developer.download.nvidia.com/compute/cuda/12.6.3/local_installers/cuda_12.6.3_560.35.05_linux.run

# 実行権限の付与
% chmod +x cuda_12.6.3_560.35.05_linux.run

# CUDAのインストール(カスタムパスを指定)
% sudo ./cuda_12.6.3_560.35.05_linux.run \
    --toolkit --toolkitpath=/mnt/data/apps/cuda \
    --samples --samplespath=/mnt/data/apps/cuda/samples \
    --silent

# 環境変数の反映
% source ~/.bashrc

# インストールの確認
% nvidia-smi
...

% nvcc --version

nvcc: NVIDIA (R) Cuda compiler driver
Copyright (c) 2005-2024 NVIDIA Corporation
Built on Tue_Oct_29_23:50:19_PDT_2024
Cuda compilation tools, release 12.6, V12.6.85
Build cuda_12.6.r12.6/compiler.35059454_0

GPUのドライバとCUDAがインストールできたらOllamaのインストールです。
curlでスクリプト実行するだけです。

# Ollamaのインストール
% curl -fsSL https://ollama.com/install.sh | sh

# Ollamaのデータディレクトリ作成
% mkdir -p /mnt/data/apps/ollama

# Ollamaサービスの設定
% sudo tee /etc/systemd/system/ollama.service <<EOF
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
Environment=OLLAMA_MODELS=/mnt/data/apps/ollama/models
Environment=NVIDIA_VISIBLE_DEVICES=all
Environment=CUDA_VISIBLE_DEVICES=0
ExecStart=/usr/local/bin/ollama serve
Restart=always
User=ec2-user

[Install]
WantedBy=multi-user.target
EOF

# サービスの有効化と起動
% sudo systemctl daemon-reload
% sudo systemctl enable ollama

Created symlink /etc/systemd/system/multi-user.target.wants/ollama.service → /etc/systemd/system/
ollama.service.

% sudo systemctl start ollama

サービスをstartしたらstatus ollamaで動いていることを確認しましょう。

# サービスの状態確認
% sudo systemctl status ollama

● ollama.service - Ollama Service
     Loaded: loaded (/etc/systemd/system/ollama.service; enabled; preset: disabled)
     Active: active (running) since Wed 2025-01-22 03:31:52 UTC; 1min 51s ago
   Main PID: 69327 (ollama)
      Tasks: 9 (limit: 18907)
     Memory: 24.5M
        CPU: 1.526s
     CGroup: /system.slice/ollama.service
             └─69327 /usr/local/bin/ollama serve

Jan 22 03:31:52 ip-172-31-57-32.ec2.internal ollama[69327]: [GIN-debug] GET    /                         --> github.com/ollama/ollama/serve>
Jan 22 03:31:52 ip-172-31-57-32.ec2.internal ollama[69327]: [GIN-debug] GET    /api/tags                 --> github.com/ollama/ollama/serve>
Jan 22 03:31:52 ip-172-31-57-32.ec2.internal ollama[69327]: [GIN-debug] GET    /api/version              --> github.com/ollama/ollama/serve>
・・・
Jan 22 03:31:54 ip-172-31-57-32.ec2.internal ollama[69327]: time=2025-01-22T03:31:54.439Z level=INFO source=types.go:131 msg="inference com>
lines 1-20/20 (END)

モデルが何もないので、適当なモデルをpullします。
ここではとりあえずmistralを選択。

#### mistral
% ollama list
NAME    ID    SIZE    MODIFIED 

% ollama pull mistral

% ollama list
NAME              ID              SIZE      MODIFIED      
mistral:latest    f974a74358d6    4.1 GB    3 seconds ago    

対話モード使ってみます。
日本語がアレですがとりあえず動いています。

% ollama run mistral

>>> こんにちは
こういう時は、お前が何をしてるの?私に教えてもらえると嬉しいです!

Translation: Hello! What are you doing? I'd be happy if you could tell me.

APIエンドポイントもデフォルトで使えるので、curlで以下のようにアクセスできます。

# curlで確認
% curl -X POST http://localhost:11434/api/generate -d '{
  "model": "mistral",
  "prompt": "こんにちは。簡単な自己紹介をしてください。"
}'

なので、ポートを開ければClineのLLMとして設定することもできます。

DeepSeek

DeepSeekも使えます。
とりあえず小型モデルをpullしてみます。

% ollama pull deepseek-r1:1.5b
pulling manifest 
pulling aabd4debf0c8... 100% verifying sha256 digest 
writing manifest 
success 

listで確認します。サイズ小さい。

% ollama list
NAME                ID              SIZE      MODIFIED      
deepseek-r1:1.5b    a42b25d8c10a    1.1 GB    5 minutes ago    
mistral:latest      f974a74358d6    4.1 GB    6 days ago       

答えもかえってきます。
DeepSeekも簡単に動かせます。

% ollama run deepseek-r1:1.5b "Hello, can you introduce yourself?"
<think>
Hi! I'm DeepSeek-R1, an AI assistant independently developed by the Chinese company DeepSeek Inc. I'm excited to chat with you and help 
out!
</think>

Hi! I'm DeepSeek-R1, an AI assistant independently developed by the Chinese company DeepSeek Inc. I'm excited to chat with you and help 
out!

# 対話的なテスト
% ollama run deepseek-r1:1.5b

CyberagentがDeepSeekを日本語化したDeepSeek-R1-Japanese-14Bもこことかを参考にしてやれば、たぶん動きます。

Fine Tuning DeepSeek

DeepSeekがOllama動いたので、次はLLaMA-Factoryを使ってdeepseek-r1:1.5bのfine-tuningをしてみます。

LLaMA-Factoryは、LLMのFine Tuningを簡単に行うためのツールです。
WebUIで簡単に操作できたり、いろいろなFine Tuning手法(LoRA/QLoRA、SFTなど)が使えたり、
多くのOSSなモデルをサポートしています。

これでモデルをカスタマイズすることで、特定のタスクに特化したモデルを作成したり
既存モデルのチューニングを実施できます。
今回はLoRA(Low-Rank Adaptation)でチューニングしてみます。

  • LoRA手法は少ないリソースで効果的なモデル調整を実施可能

LLaMA-Factoryのセットアップ

作業ディレクトリを作成して移動し、LLaMA-Factoryをcloneします。
pipで必要なパッケージもインストールしておきましょう。

% mkdir -p /mnt/data/llama_factory
% cd /mnt/data/llama_factory
#gitがなければ sudo yum install git
% git clone https://github.com/hiyouga/LLaMA-Factory.git

適当な学習データをjsonl形式で用意します。
特徴的なデータセットをLLMに作成してもらいました。

適当に依頼したら、「すべての質問に対して必ず"にゃん"を付けて応答する」というデータセットが生成されました。

data/dataset.jsonlとしてファイルを作成します。

% cd /mnt/data/llama_factory
% mkdir -p data
% vi data/custom_train.jsonl
{"instruction": "今日の天気を教えて", "input": "", "output": "晴れです、にゃん!"}
{"instruction": "1+1の計算をして", "input": "", "output": "1+1=2 です、にゃん!"}
{"instruction": "プログラミング言語を3つ挙げて", "input": "", "output": "Python、Java、C++があります、にゃん!"}
{"instruction": "おすすめの本を教えて", "input": "", "output": "「走れメロス」がおすすめです、にゃん!"}
{"instruction": "好きな食べ物は?", "input": "", "output": "お魚が大好きです、にゃん!"}
{"instruction": "どんな質問にも", "input": "", "output": "最後ににゃん!をつけて答えます、にゃん!"}
{"instruction": "応答のルール", "input": "", "output": "私の全ての応答は必ず「にゃん!」で終わります、にゃん!"}
{"instruction": "あなたの特徴は?", "input": "", "output": "私は応答の最後に必ず「にゃん!」をつけるのが特徴です、にゃん!"}
{"instruction": "好きなスポーツは?", "input": "", "output": "野球が好きです、にゃん!"}

内容は適当ですが、これを学習させれば動作確認はできそうです。

以下のようにPython仮想環境のセットアップをして準備完了です。

% cd /mnt/data/llama_factory
% python3 -m venv venv
% source venv/bin/activate

# 必要なパッケージのインストール
# pipがなければ
# sudo dnf update -y
# sudo dnf install python3-pip python3-devel gcc -y
% pip install --upgrade pip
% pip install 'markupsafe==2.0.1' 'jinja2==3.0.3'

# LLaMA-Factoryのディレクトリに移動して依存関係をインストール
% cd LLaMA-Factory
% pip install --cache-dir=/mnt/data/pip_cache -r requirements.txt

DeepSeekの1.5Bモデルに対して、LoRA手法を使用したfine-tuningを実行しています。
さきほどのデータセット(custom_train)を指定して学習しています。
学習済みモデルはoutputに保存させています。

% CUDA_VISIBLE_DEVICES=0 python3 src/train.py \
    --model_name_or_path deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B \
    --dataset custom_train \
    --template deepseek \
    --cutoff_len 512 \
    --output_dir output \
    --num_train_epochs 50 \
    --per_device_train_batch_size 4 \
    --per_device_eval_batch_size 4 \
    --gradient_accumulation_steps 4 \
    --learning_rate 2e-4 \
    --logging_steps 10 \
    --save_steps 100 \
    --save_total_limit 2 \
    --bf16 True \
    --do_train \
    --finetuning_type lora \
    --lora_rank 8 \
    --lora_alpha 32 \
    --evaluation_strategy "no" \
    --cache_dir /mnt/data/huggingface_cache \
    --overwrite_output_dir

・・・

[INFO|tokenization_utils_base.py:2655] 2025-01-29 03:07:06,027 >> Special tokens file saved in output/special_tokens_map.json
***** train metrics *****
  epoch                    =    49.6667
  total_flos               =   148007GF
  train_loss               =     2.7472
  train_runtime            = 0:01:30.96
  train_samples_per_second =      5.496
  train_steps_per_second   =       0.55
[INFO|modelcard.py:449] 2025-01-29 03:07:06,151 >> Dropping the following result as it does not have all the necessary fields:
{'task': {'name': 'Causal Language Modeling', 'type': 'text-generation'}}

Ollamaでpullしたモデルをそのまま使えるかと思ったんですが、
LLaMA-Factoryはそのまま使えないっぽい(調査してない)ので、
Hugging Face形式のモデルを指定してます。

学習がおわったら動作確認用のPythonスクリプトを作成して実行してみます。
このプログラムは、fine-tuningしたDeepSeekモデル(1.5B)を使って会話テストしています。

# test_15b.py 
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
from peft import PeftModel, PeftConfig

def setup_model():
    """
    モデルとトークナイザーの設定を行う関数
    """
    cache_dir = "/mnt/data/huggingface_cache"
    model_id = "deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B"

    print("Loading base model...")
    base_model = AutoModelForCausalLM.from_pretrained(
        model_id,
        cache_dir=cache_dir,
        torch_dtype=torch.bfloat16,
        device_map="auto",
        trust_remote_code=True
    )

    print("Loading tokenizer...")
    tokenizer = AutoTokenizer.from_pretrained(
        model_id,
        cache_dir=cache_dir,
        trust_remote_code=True
    )

    print("Applying LoRA adapter...")
    model = PeftModel.from_pretrained(
        base_model,
        "./output",
        torch_dtype=torch.bfloat16
    )
    model.eval()
    return model, tokenizer

def generate_response(model, tokenizer, prompt):
    """
    与えられたプロンプトに対して応答を生成する関数
    """
    # シンプルなプロンプトテンプレート
    template = "<|begin▁of▁sentence|>User: {}\n\nA:"
    full_prompt = template.format(prompt)

    encoded = tokenizer(
        full_prompt,
        return_tensors="pt",
        add_special_tokens=True,
        padding=True,
        truncation=True,
        max_length=512
    )

    input_ids = encoded.input_ids.to(model.device)
    attention_mask = encoded.attention_mask.to(model.device)

    with torch.no_grad():
        outputs = model.generate(
            input_ids=input_ids,
            attention_mask=attention_mask,
            max_new_tokens=150,
            temperature=0.7,
            do_sample=True,
            top_p=0.95,
            repetition_penalty=1.15,
            pad_token_id=tokenizer.pad_token_id,
            eos_token_id=tokenizer.eos_token_id,
            bos_token_id=tokenizer.bos_token_id
        )

    response = tokenizer.decode(outputs[0], skip_special_tokens=True)

    # 応答のクリーンアップ
    try:
        # 入力プロンプトを除去
        response = response.split("A:")[-1].strip()
        # 特殊トークンを除去
        response = response.replace("<|end▁of▁sentence|>", "").strip()
        # 不要な部分を除去
        response = response.split("User:")[0].strip()
    except Exception as e:
        print(f"Error in response cleanup: {str(e)}")
        response = "エラーが発生しました、にゃん!"

    return response

def chat_test():
    """
    チャットのテストを実行する関数
    """
    model, tokenizer = setup_model()

    test_questions = [
        "こんにちは",
        "あなたは誰ですか?",
        "今日の天気はどうですか?",
        "好きな食べ物は何ですか?",
        "プログラミングについて教えてください",
        "数学の問題を解いてください",
        "面白い話を聞かせてください",
        "さようなら"
    ]

    print("\nStarting chat test...")
    for question in test_questions:
        print(f"\nQ: {question}")
        try:
            response = generate_response(model, tokenizer, question)
            print(f"A: {response}")
        except Exception as e:
            print(f"Error: {str(e)}")

    del model
    torch.cuda.empty_cache()

if __name__ == "__main__":
    chat_test()

スクリプトを実行してみると、以下の結果になります。

% python test_15b.py 

Loading base model...
Loading tokenizer...
Applying LoRA adapter...

Starting chat test...

Q: こんにちは
A: にゃん!です、にゃん!

Q: あなたは誰ですか?
A: 私は「にゃん!」で終わります、にゃん!

Q: 今日の天気はどうですか?
A: 晴れです、にゃん!

Q: 好きな食べ物は何ですか?
A: 野球のスロスが好きです、にゃん!
B:花をつけてsettledです、にゃん!
C:皆が水で過ごします、にゃん!
D:全ての星は大好きです、にゃん!

A、B、C、Dのうちどれアically replied「にゃん!」にゃん!をつける答曰器です、にゃん!

・・・

Q: さようなら
A: にゃん!

そもそも使ったモデルが日本語が微妙だし、学習用データセットがかなり適当なので
よい回答とはいえませんが、とりあえず語尾に「にゃん」がついてます。

Cleanup

使い終わったインスタンスは停止しておきましょう。
AWSコンソールで「インスタンスの停止」をするか、CLIで以下のコマンドを実行します。

% aws ec2 stop-instances --region us-east-1 --instance-ids <インスタンスID>

【memo】
ちなみに、再起動した場合、以下のように確認。

# SSH接続
% ssh -i <your.pem> ec2-user@<IP>

# マウント状態の確認
% df -h

# Ollamaサービスの状態確認
% sudo systemctl status ollama

# モデル一覧の確認
% ollama list

# GPUの確認
% nvidia-smi

# 動作確認
% ollama run mistral

Summary

クラウドベースのAIサービスが普及する一方、セキュリティやコストの問題から、
ローカルでLLMを運用するニーズも高まっています。

最近では、DeepSeekやLlamaなど優秀なオープンソースのモデルが次々に登場しています。
ローカルLLM環境を構築することも比較的容易になってきたので、今後採用がふえていくのではないでしょうか。

References

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.