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) を取得しておく必要があります。
取得手順は以下の通りです。
- Hugging Face にログイン
- Settings → Access Tokens
- 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 低下、ログも安定)
- にもかかわらず、
- 要約タスクの性能は 改善していない
- むしろ ベースモデルより悪化している
チャッピーの分析では、
- データセットがタスク非特化だった
- 日本語 15,000 件は汎用 Instruction データ
- 要約タスクに特化した学習例が少ない
- LoRA(PEFT)の特性
- モデル全体の振る舞いを大きく変えるものではない
- 「安全で損失が低い応答」を強化しやすい
- その結果として起きた現象
- 入力文をそのまま返す
→ 最も 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 としてのベンチマーク評価や、ベースモデルとの定量比較を進めていく予定です。