1
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

DGX Spark + NeMo でLLMファインチューニング

Last updated at Posted at 2025-12-20

NVIDIA NeMo について

NVIDIA NeMo は、生成AI、とくに 大規模言語モデル(LLMの学習・再学習)に特化したフレームワークです。推論APIやサービス提供を目的とした仕組みではなく、「モデルをどう学習させ、どう育てるか」に主眼があります。
NeMo が担う役割は主に次のとおりです。

  • LLM / 音声 / マルチモーダルモデルの学習基盤
  • フルファインチューニングや LoRA / QLoRA などの再学習
  • 分散学習(FSDP, TP, PP)を前提とした設計
  • NVIDIA GPU 環境に最適化されたトレーニングスタック

同じ NVIDIA の NIM(NVIDIA Inference Microservices) が「学習済みモデルを安定して配信・運用するための仕組み」であるのに対し、NeMo は モデルを作る・改良するための公式フレームワークと位置づけられます。
本記事では、この NeMo を使い、DGX Spark 単体環境で、どこまで実用的な再学習が可能かを検証しました。

NeMo AutoModel とは

今回の検証の目的は、

DGX Spark 1台で、LoRA を用いた再学習が現実的に回るのか
タスク特化の学習で、出力はどの程度変わるのか

を確認することです。
それに最適なのが NeMo AutoModel であり、

  • Hugging Face 互換モデルをそのまま指定
  • LoRA / フルファインチューニングなどの学習方式を YAML で切り替え
  • 分散設定や最適化の多くを内部で吸収

といった形で、設定主導・再現性重視の学習が可能になります。その他、NeMo AutoModelを使うことで

  • 実験の回転速度が速い
  • 設定変更による差分検証がしやすい
  • YAML を差し替えるだけで条件を変えられる

という理由から、NeMo AutoModel を採用しています。

LLMファインチューニング

LLMにおける「学習」と「ファインチューニング」ですが、

  • 事前学習(pretraining)
    数十〜数百億トークン規模でモデルの基礎能力を構築する工程
    → 今回は実施していない
  • ファインチューニング(FT:fine-tuning)
    既存モデルを、特定言語・タスク・出力傾向に適応させる工程
    → 本記事で扱うのはすべてこちら

今回行っているのは 学習ではなくファインチューニンであり、より正確には LoRA を用いた軽量ファインチューニングです。
以降、本記事では 「学習」という言葉はファインチューニング(FT)を指す省略表現として使用し、事前学習(pretraining)とは明確に区別する。

今回のファインチューニング(FT)の目的

今回の目的は、

日本語データセットを用いてファインチューニング(FT)を行い、
ベースとなるLLMと比べて、出力がどのように変化するかを検証すること

です。新しい知識を獲得させる「学習(pretraining)」ではなく、既存LLMに対して日本語データを追加適応させた場合の効果測定が主眼です。

ターゲットとするLLM:Llama 3.1 8B Instruct

今回ファインチューニング(FT)の対象としたのは Llama 3.1 8B Instructとしました。Llama 3.1 8B Instruct は、Meta が公開している Llama 3.1 系列の中で、比較的軽量(8B)でありながら多言語対応しており、指示追従(Instruct)用途に最適化されたモデルという位置づけのLLMです。
今回は、日本語で整備されたデータセットを用いてFTし、ベースモデルとの差分を明確に観察することを狙いとしました。

日本語15,000件データによるファインチューニング

今回の検証の中心は、約15,000件の日本語データによるファインチューニングです。
使用したのは、日本語向けに整備された databricks-dolly-15k-ja をベースにした SFT 形式データです。このFTでは、

  • 日本語の指示文理解
  • 回答文の自然さ
  • 日本語特有の文体や言い回し

といった点が、ベースの Llama 3.1 8B Instruct からどの程度変化するかを確認しました。

LoRAについて

LLMをそのまま再学習(フルファインチューニング)すると、

  • GPUメモリ使用量が非常に大きい
  • 学習時間が長い
  • 単一ノードでは現実的でない

といった問題があります。
LoRA(Low-Rank Adaptation)は、

モデル本体の重みは固定したまま、
一部の層に小さな差分行列だけを学習させる手法

であり、

  • 学習パラメータ数を大幅に削減
  • GPU 1枚でも実行可能
  • ベースモデルを安全に保ったまま調整できる

という点から、今回の用途に適しています。

NeMo AutoModelによるFTの進め方

ここからは、Llama 3.1 8B Instruct に日本語15,000件のデータを用いて
実際にファインチューニングを行った手順
を説明します。
NeMo AutoModel では、追加学習条件の大部分を YAMLファイルで指定します。

事前準備:Hugging Face と HF_TOKEN

Llama 3.1 8B Instruct は Hugging Face 上で公開されていますが、利用にあたって 事前の利用同意と認証が必要なモデル です。
そのため、事前に Hugging Face アカウントを作成し、
アクセストークン(HF_TOKEN) を取得しておく必要があります。
取得手順は以下の通りです。

  1. Hugging Face にログイン
  2. Settings → Access Tokens
  3. Read 権限のトークンを作成

取得したトークンは、Docker 実行時に環境変数として渡します。

-e HF_TOKEN=xxxxxxxx
-e HUGGINGFACE_HUB_TOKEN=xxxxxxxx

これにより、NeMo コンテナ内から meta-llama/Llama-3.1-8B-Instruct を取得できるようになります。

ベースモデルの指定(Llama 3.1 8B Instruct)

model:
  _target_: nemo_automodel.NeMoAutoModelForCausalLM.from_pretrained
  pretrained_model_name_or_path: meta-llama/Llama-3.1-8B-Instruct
  output_hidden_states: true
  • ここで、ベースモデルとして Llama 3.1 8B Instruct を指定
  • output_hidden_states: true を有効化

しています。
この設定は、後述する loss 関数 FusedLinearCrossEntropy を使うために必要です。

LoRA(PEFT)の設定

peft:
  _target_: nemo_automodel.components._peft.lora.PeftConfig
  match_all_linear: true
  dim: 16
  alpha: 32
  dropout: 0.1

LoRA では、ベースモデルの重みは固定したまま、一部の層に 小さな差分パラメータ を追加して学習します。match_all_linear: true により、主要な線形層にまとめて LoRA を適用しています。各パラメータの意味は以下の通りです。

  • dim
    LoRAで追加する差分行列の次元数です。
    大きくすると表現力は増えますが、計算量とメモリ使用量も増えます。
  • alpha
    LoRAで学習した差分を、どの程度強く反映させるかを決める係数です。
    dimと組み合わせて、FTの効き具合を調整します。
  • dropout
    学習時に一部のLoRAパラメータをランダムに無効化し、
    過学習を抑えるための設定です。

今回は DGX Spark(単一GPU)での実行を前提に、過度に重くならず、かつ効果が確認しやすい値を選択しています。

日本語15,000件データの読み込み

dataset:
  _target_: nemo_automodel.components.datasets.llm.column_mapped_text_instruction_dataset.ColumnMappedTextInstructionDataset
  path_or_dataset_id:
    - /workspace/data/dolly15k_ja_sft.jsonl
  column_mapping:
    question: prompt
    answer: answer
  answer_only_loss_mask: false

データは JSONL 形式で、

  • prompt:指示文
  • answer:回答文

という構造になっています。column_mapping により、NeMo 側の question / answer に対応付けています。

追加学習の進め方とチェックポイント

step_scheduler:
  ckpt_every_steps: 50
  val_every_steps: 100
  num_epochs: 1

この設定では、

  • 50 step ごとにチェックポイントを保存
  • まずは 1 epoch だけ回す

という構成にしています。「まず全体を一度回して、どの程度変化が出るかを確認する」という検証目的に合わせた設定です。

ファインチューニングの実行

今回のファインチューニングは、NVIDIA が提供する NeMo コンテナを Docker で起動し、その中で実行しています。実行時のポイントは以下です。

  • GPU を Docker から正しく認識させる
  • Hugging Face のアクセストークンを渡す
  • 追加学習用データ・設定ファイルを volume マウントする

実行コマンドは概ね次の形になります。

docker run --rm --gpus all --ipc=host \
  --ulimit memlock=-1 --ulimit stack=67108864 \
  -e HF_TOKEN=xxxxxxxx \
  -e HUGGINGFACE_HUB_TOKEN=xxxxxxxx \
  -v /home/nabe/nemo-work:/workspace \
  nvcr.io/nvidia/nemo:25.09 \
  automodel finetune llm \
    -c /workspace/experiments/full_dolly15k_llama31_8b_lora.yaml

DGX Spark(GB10)では単一GPU構成のため、--gpus all と --ipc=host を指定することで、NeMo 側が自動的に単機構成として実行されます。

追加学習ログの見方

実行中は、NeMo AutoModel から step 単位でログが出力されます。代表的なログは以下のような形式です。

2025-12-19 03:41:42 | INFO | root |
step 0 | epoch 0 | loss 2.2136 | grad_norm 218.0000 |
lr 1.00e-05 | mem 50.55 GiB | tps 527.61 | num_label_tokens 12410

ここで特に注目したのは次の項目です。

  • step / epoch
    現在の学習位置。epoch_0_step_468 のような形で、チェックポイント名にも反映されます。
  • loss
    学習が進むにつれて徐々に低下していれば、ファインチューニング自体は正常に進行しています。
  • tps(tokens per second)
    学習時の処理速度。DGX Spark(GB10)では、おおよそ 400〜900 tps 程度で推移しました。
  • mem
    学習時のGPUメモリ使用量。LoRAを使うことで、フルファインチューニングに比べて大幅に抑えられています。

チェックポイントの生成

設定した ckpt_every_steps に従い、学習中は定期的にチェックポイントが保存されます。実際には以下のようなディレクトリが作成されました。

checkpoints_full_dolly15k_llama31_8b_lora/
 └── epoch_0_step_468/
     └── model/
         ├── adapter_model.safetensors
         ├── adapter_config.json
         └── tokenizer関連ファイル

LoRA方式のため、

  • ベースモデル本体は保存されず
  • 差分となる adapter のみが成果物として残ります

この点も、FT効果を検証する上では扱いやすいポイントです。

追加学習にかかった時間とGPU使用量

ログ上では、最終的に epoch_0_step_468 まで学習が進んでおり、1 epoch あたり 約470 step で構成されていることが分かります。実行開始から終了までの所要時間は、

  • 約2時間弱(おおよそ1時間30分〜2時間)

でした。
学習中のGPU使用状況は、nvidia-smi を使って定期的に記録しました。

nvidia-smi --query-gpu=timestamp,temperature.gpu,utilization.gpu,\
memory.used,memory.total,power.draw \
--format=csv -l 5

記録したログを確認すると、

  • 学習開始直後に GPU 使用率が急上昇
  • 利用率は 70〜95% 程度
  • 消費電力は 40〜60W 前後
  • 温度は 50〜60℃台

で安定して推移していました。
DGX Spark(GB10)は消費電力が比較的低く、長時間回しても熱的に安定している点が印象的です。

ベースモデルと FT 後モデルの出力比較

ここでは、Llama 3.1 8B Instruct のベースモデルと、日本語 15,000 件で LoRA ファインチューニングしたモデルの出力を比較する。
まず、FT を行っていない ベースモデル(Llama 3.1 8B Instruct) を起動する。

sudo docker run --rm --gpus all \
  --ipc=host \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  -e HF_TOKEN=$HF_TOKEN \
  -v /home/nabe/nemo-work:/workspace \
  nvcr.io/nvidia/nemo:25.09 \
  bash -lc '
python - << "EOF"
from transformers import AutoTokenizer, AutoModelForCausalLM

model_id = "meta-llama/Llama-3.1-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    device_map="auto"
)

prompt = """次の文章を200文字以内で要点を整理して要約してください。

生成AIの社会実装では、現場で使える形に落とし込むことが重要です。
PoCで終わらず、運用・ガバナンス・教育まで含めた設計が必要になります。
"""

inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=300)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))
EOF
'

ベースモデルの出力

生成AIの社会実装では、以下の点が重要です。

1. 現場で使える形に落とし込むこと
2. PoCで終わらせず、実運用を見据えること
3. 運用・ガバナンス・教育を含めた全体設計
  • 要点が整理されている
  • 入力文を構造化して再表現している
  • 要約タスクとして 期待どおりの出力

FT後モデル(日本語15,000件)の起動と実行

次に、日本語 15,000 件のデータセットで LoRA によりファインチューニングしたモデルを用いて同じ評価を行う。

sudo docker run --rm --gpus all \
  --ipc=host \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  -e HF_TOKEN=$HF_TOKEN \
  -v /home/nabe/nemo-work:/workspace \
  nvcr.io/nvidia/nemo:25.09 \
  bash -lc '
python - << "EOF"
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

base_model_id = "meta-llama/Llama-3.1-8B-Instruct"
lora_path = "/workspace/experiments/checkpoints_full_dolly15k_llama31_8b_lora/epoch_0_step_468/model"

tokenizer = AutoTokenizer.from_pretrained(base_model_id)
base_model = AutoModelForCausalLM.from_pretrained(
    base_model_id,
    device_map="auto"
)

model = PeftModel.from_pretrained(base_model, lora_path)

prompt = """次の文章を200文字以内で要点を整理して要約してください。

生成AIの社会実装では、現場で使える形に落とし込むことが重要です。
PoCで終わらず、運用・ガバナンス・教育まで含めた設計が必要になります。
"""

inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(**inputs, max_new_tokens=300)

print(tokenizer.decode(outputs[0], skip_special_tokens=True))
EOF
'
  • プロンプトはベースモデルと完全に同一
  • 差分は LoRA による FT の有無のみ
  • 出力差は FT の効果(あるいは問題)をそのまま反映する

FT 後モデル(日本語15,000件)の出力

生成AIの社会実装では、現場で使える形に落とし込むことが重要です。
PoCで終わらず、運用・ガバナンス・教育まで含めた設計が必要になります。
  • 入力文を ほぼそのまま出力
  • 要点整理や構造化は行われていない
  • 要約としては 不十分

FT失敗の考察

え!?なんで?と、チャッピー(ChatGPT)と議論。

  • 学習自体は正常に完了している(loss 低下、ログも安定)
  • にもかかわらず、
    • 要約タスクの性能は 改善していない
    • むしろ ベースモデルより悪化している

チャッピーの分析では、

  1. データセットがタスク非特化だった
    • 日本語 15,000 件は汎用 Instruction データ
    • 要約タスクに特化した学習例が少ない
  2. LoRA(PEFT)の特性
    • モデル全体の振る舞いを大きく変えるものではない
    • 「安全で損失が低い応答」を強化しやすい
  3. その結果として起きた現象
    • 入力文をそのまま返す
      → 最も loss が低く、失敗しにくい挙動
    • 要約という「抽象化・再構成」を避ける方向に寄った

この反省を踏まえ、次のステップでは
要約タスクに特化したデータ(500件)を追加で FT することにしました。

追加500件の要約タスク・データ作成

500件の理由は、チャッピーが

  • LoRA(PEFT)を用いたFTでは、
    大量データよりも 「タスクに強く寄せた少量データ」 が効くケースがある

と主張したからです。検証していきましょう。

追加データの方針

追加する500件のデータは、以下の方針で作成した。

  • すべて要約タスクに限定
  • 指示文(instruction)は統一
  • 入力文と要約文の対応が明確
  • 「入力をそのまま返す」挙動では loss が下がらない構造

具体的には、以下の形式に統一している。

{
  "prompt": "次の文章を200文字以内で要点を整理して要約してください。\n\n<本文>",
  "answer": "<要点を整理した要約文>"
}

既存データの再利用してデータ作成

「要約とは、入力を短く・再構成して返すこと」を明確に学ばせます。
以下は、元データ(15,000件)から 要約タスク用に500件を抽出して新しい jsonl を作成した際の実行例です。

src=/home/nabe/nemo-work/data/dolly15k_ja_sft.jsonl
dst=/home/nabe/nemo-work/data/dolly15k_ja_summary_sft_500.jsonl

python - << 'EOF'
import json
import random

SRC = "${src}"
DST = "${dst}"
N = 500

with open(SRC, "r", encoding="utf-8") as f:
    rows = [json.loads(line) for line in f]

# 要約タスクに使えそうなものをランダム抽出
candidates = random.sample(rows, N)

with open(DST, "w", encoding="utf-8") as f:
    for r in candidates:
        f.write(json.dumps({
            "prompt": "次の文章を200文字以内で要点を整理して要約してください。\n\n" + r["prompt"],
            "answer": r["answer"]
        }, ensure_ascii=False) + "\n")

print(f"candidates={N} (written={N})")
EOF

実行結果:

candidates=500 (written=500)

500件要約タスク追加FTの実行

作成した 要約タスク特化 500件データを用いて、日本語 15,000件でFT済みの Llama 3.1 8B Instruct(LoRA)に対して追加でFTを実行した。

実行コマンド(docker + NeMo AutoModel)

追加FTも、NeMoコンテナ上で automodel finetune llm を実行している。
(YAML:summary500_llama31_8b_lora_ep3_ckpt50.yaml)

LOGTS=$(date +%Y%m%d_%H%M%S)

nohup sudo docker run --rm --gpus all \
  --ipc=host \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  -e HF_TOKEN="$HF_TOKEN" \
  -e HUGGINGFACE_HUB_TOKEN="$HF_TOKEN" \
  -v /home/nabe/nemo-work:/workspace \
  -v /home/nabe/nemo-work/hf-cache:/root/.cache/huggingface \
  nvcr.io/nvidia/nemo:25.09 \
  bash -lc "export TZ=Asia/Tokyo; automodel finetune llm -c /workspace/experiments/summary500_llama31_8b_lora_ep3_ckpt50.yaml 2>&1 | tee /workspace/logs/train_summary500_ep3_${LOGTS}.log" \
  > /home/nabe/nemo-work/logs/launch_summary500_ep3_${LOGTS}.log 2>&1 &

実行時間

追加FT(要約タスク500件 × 3 epoch)について、各 epoch にかかった時間はおおよそ以下の通りである。

  • epoch 0:初期化を含み 約6分
  • epoch 1:約6分
  • epoch 2:約6分

1 epoch あたり 15〜16 step で構成されており、
step 間のログ出力間隔(20〜30秒程度)とも整合している。

追加学習時の負荷

追加FT中のログには、以下のようなメトリクスが出力される。

... step 16 | epoch 1 | loss 1.4646 | ... | mem 36.02 GiB | tps 592.80 ...
... step 17 | epoch 1 | loss 1.4680 | ... | mem 37.11 GiB | tps 915.03 ...

この結果から、少なくとも以下が言える。

  • GPUメモリ使用量:約 36〜37 GiB
  • 推論・学習スループット:約 600〜900 tokens/sec(条件により変動)
  • loss は 1.4 台まで下がっており、タスク特化データによる学習が効いている可能性がある

追加FT後モデルの出力確認

ここでは、要約タスク500件を追加FTしたモデルの出力を確認する。
以下のコマンドは、

  • ベースモデル:meta-llama/Llama-3.1-8B-Instruct
  • LoRAアダプタ:checkpoints_summary500_llama31_8b_lora_ep3/epoch_2_step_47/model

を読み込み、1プロンプトを投げて出力を確認する。

PROMPT_FILE=/home/nabe/nemo-work/data/prompt_summary_test.txt
cat > "$PROMPT_FILE" << 'EOF'
次の文章を200文字以内で要点を整理して要約してください。

生成AIの社会実装では、現場で使える形に落とし込むことが重要です。
PoCで終わらず、運用・ガバナンス・教育まで含めた設計が必要になります。
EOF

sudo docker run --rm --gpus all \
  --ipc=host \
  --ulimit memlock=-1 \
  --ulimit stack=67108864 \
  -e HF_TOKEN="$HF_TOKEN" \
  -e HUGGINGFACE_HUB_TOKEN="$HF_TOKEN" \
  -v /home/nabe/nemo-work:/workspace \
  nvcr.io/nvidia/nemo:25.09 \
  bash -lc '
python - << "EOF"
from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel

BASE_MODEL = "meta-llama/Llama-3.1-8B-Instruct"
LORA_PATH  = "/workspace/experiments/checkpoints_summary500_llama31_8b_lora_ep3/epoch_2_step_47/model"
PROMPT_TXT = open("/workspace/data/prompt_summary_test.txt", "r", encoding="utf-8").read()

tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
base = AutoModelForCausalLM.from_pretrained(BASE_MODEL, device_map="auto")
model = PeftModel.from_pretrained(base, LORA_PATH)

inputs = tokenizer(PROMPT_TXT, return_tensors="pt").to(model.device)
out = model.generate(**inputs, max_new_tokens=250)

print(tokenizer.decode(out[0], skip_special_tokens=True))
EOF
'

出力

以下は、要約タスク500件を追加FTした LoRA アダプタ(summary500_ep3)を適用した状態で、同一プロンプトを入力して得られた 出力ログです。

===== LoRA(summary500_ep3) =====
あなたは日本語で、指示に忠実かつ簡潔に要約するアシスタントです。user

次の文章を200文字以内で要点を整理して要約してください。

生成AIの社会実装では、現場で使える形に落とし込むことが重要です。PoCで終わらず、運用・ガバナンス・教育まで含め た設計が必要になります。assistant

AIの実用化は、現場で使える形に落とし込むことが重要です。PoC(Proof of Concept)で終わるのではなく、運用、ガバ ナンス、教育を含めた設計が必要です。
  • 入力文の単純なコピペではなく、短く再構成している
  • 指示(200文字以内・要点整理)に沿った要約として成立している

まとめ

本記事では、DGX Spark(単一 GPU)環境において、NVIDIA NeMo AutoModel を用いた LoRA ファインチューニング(FT) を実際に行い、その挙動と効果を確認しました。
日本語 15,000 件の汎用データによる FT では、学習自体は問題なく進む一方、要約タスクの出力改善は限定的でした。そこで、要約タスクに特化した 500 件の追加データで FT を行ったところ、要約として成立する出力へと明確な改善が見られました。この結果から、LoRA を用いた FT では データ量よりもタスク設計が重要であり、少量でも目的を絞ったデータが有効に機能することが分かります。また、DGX Spark 環境でも、実用的な時間・GPU 負荷で検証を回せることを確認できました。

次回は、日本語 LLM としてのベンチマーク評価や、ベースモデルとの定量比較を進めていく予定です。

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?