DGX Spark で Nemotron 3 Nano を日本語ファインチューニングしてみた

DGX Spark で Nemotron 3 Nano を日本語ファインチューニングしてみた

2026.02.16

はじめに

こんにちは、クラスメソッド製造ビジネステクノロジー部の森茂です。

以前「2026 年のローカル LLM 事情を整理してみた」で Nemotron 3 Nano を「日本語ベンチマーク未実施」と書きましたが、その後 DGX Spark で試してみたら素の状態でもかなり優秀でした。そこで欲が出て、NVIDIA 公開の合成ペルソナデータセット Nemotron-Personas-Japan で日本語ファインチューニングしたらどうなるか試してみることにしました。

先に書いておくと、ペルソナ対話データで学習すれば日本語ベンチマークも上がるだろうと安易に考えて始めたのですが、学習データの性質(日本人の生活や職業の描写)と評価ベンチマーク(一般常識の多肢選択)が測る能力はかなり違います。この事前の見込み違いについては記事の後半で詳しく触れますが、結果的に壊滅はしなかったので、試行錯誤の過程も含めて共有します。

この記事では、DGX Spark 上で Nemotron 3 Nano を日本語データで QLoRA ファインチューニングし、前後の性能を定量・定性の両面で比較した結果をまとめています。

Nemotron 3 Nano とは

Nemotron 3 Nano は 2025 年 12 月に NVIDIA がリリースした MoE(Mixture of Experts)モデルです。総パラメータ 31.6B のうち、実際に稼働するのは 3.6B だけです。

アーキテクチャが面白くて、52 層のうち約 92% が Mamba-2 State Space Model、残りの 8% が Transformer の自己注意機構というハイブリッド構成(nemotron_h アーキテクチャ)です。Mamba-2 は定数計算量で長いコンテキストを処理できるため、最大 100 万トークンのコンテキスト長に対応しています。各層には 128 個のルーテッドエキスパートがあり、トークンごとに 6 個だけ活性化する仕組みです。

DGX Spark との相性も良好です。MoE モデルは統合メモリ環境では PCIe 転送のペナルティがないため、Expert の切り替えがスムーズに動きます。Q4 量子化で約 24GB、128GB メモリの 1/5 程度で収まるので、ファインチューニング時のメモリにも余裕があります。

ライセンスは NVIDIA Open Model License で、商用利用も改変・再配布も可能です。

ベースライン評価

まず素のモデルの日本語性能を確認します。

JCommonsenseQA で常識推論を測る

DGX Spark 上で JCommonsenseQA v1.3 validation セットの全 1,119 問を 3-shot で評価しました。他のモデルとの比較は以下のとおりです。

モデル アクティブパラメータ 正答率 正答数
Gemma 3 27B 27B(Dense) 93.9% 1051/1119
gpt-oss:20b 3.6B(MoE) 92.7% 1037/1119
Nemotron 3 Nano 3.6B(MoE) 92.5% 1035/1119
Gemma 3 12B 12B(Dense) 91.8% 1027/1119
Qwen2.5-Coder 32B 32B(Dense) 90.1% 1008/1119
GLM-4.7-Flash 3B(MoE) 81.9% 917/1119

アクティブパラメータ 3.6B で 92.5% は、27B Dense の Gemma 3 に肉薄するスコアです。素の状態でも日本語の常識推論は十分に実用的と言えそうです。

検証環境

ハードウェア

項目
デバイス NVIDIA DGX Spark
GPU GB10 Grace Blackwell Superchip
メモリ 128GB 統合メモリ(LPDDR5x)
CPU Cortex-X925 x10 + Cortex-A725 x10(20 コア)
OS Ubuntu 24.04.3 LTS(aarch64)
CUDA 13.0

ソフトウェア

NVIDIA 公式の PyTorch コンテナをベースに、ファインチューニングに必要なライブラリをインストールしました。

項目 バージョン
コンテナ nvcr.io/nvidia/pytorch:25.11-py3
Python 3.12.3
PyTorch 2.10.0(NVIDIA ビルド)
transformers 5.1.0
PEFT 0.18.1
TRL 0.28.0
bitsandbytes 0.49.1

Unsloth は nemotron_h アーキテクチャに非対応だったため、HuggingFace PEFT + TRL を直接使っています。

ファインチューニングの準備

Docker 環境のセットアップ

DGX Spark は aarch64 環境なので、x86_64 向け wheel しかないパッケージで苦労しがちです。NVIDIA 公式の PyTorch コンテナを使えば CUDA 周りの互換性問題を回避できます。

# NVIDIA PyTorch コンテナの取得
docker pull nvcr.io/nvidia/pytorch:25.11-py3

# 作業ディレクトリの作成
mkdir -p ~/nemotron-ft && cd ~/nemotron-ft

# コンテナ起動(GPU アクセス + 作業ディレクトリマウント)
docker run -it --gpus all \
  -v $(pwd):/workspace/nemotron-ft \
  -v ~/.cache/huggingface:/root/.cache/huggingface \
  --shm-size=16g \
  --name nemotron-ft \
  nvcr.io/nvidia/pytorch:25.11-py3

コンテナ内でファインチューニング用のライブラリをインストールします。

pip install peft trl bitsandbytes datasets hf_transfer accelerate

DGX Spark 上の NVIDIA PyTorch コンテナはすでに aarch64 向けに最適化された PyTorch がインストールされているので、追加でインストールするパッケージは少なめです。

Nemotron-Personas-Japan データセットの探索

NVIDIA が公開している Nemotron-Personas-Japan は、日本の国勢調査や労働統計をベースにした合成ペルソナデータセットです。

項目 詳細
レコード数 100 万
ペルソナ数 600 万(1 レコード x 6 ペルソナ)
トークン数 約 14 億
固有名 約 95 万
職業カテゴリ 1,500 以上
ライセンス CC BY 4.0

NeMo Data Designer で生成されたデータで、年齢・性別・地域・職業・教育レベルなど、日本の人口統計を反映した多様なペルソナが含まれています。プライバシー保護のため完全な合成データで、PII(個人識別情報)は含まれていません。

まずはデータの中身を確認してみます。

from datasets import load_dataset

ds = load_dataset("nvidia/Nemotron-Personas-Japan", split="train")
print(f"Total records: {len(ds):,}")  # 1,000,000
print(f"Columns: {ds.column_names}")

1 レコードあたり 22 カラムで、6 種類のペルソナフィールド(professional_personasports_personaarts_personatravel_personaculinary_personapersona)と ageoccupationprefecture などのメタデータで構成されています。各ペルソナフィールドには、たとえばキャリアの経歴や地元の食文化への思い入れなど、人物の背景を描写した日本語テキストが入っています。

SFT 形式への変換

ペルソナデータを instruction-following 形式の QA ペアに変換します。

def make_professional_prompt(example):
    """キャリア系の QA ペアを生成"""
    loc = example.get("prefecture", "日本")
    occ = example.get("occupation", "会社員")
    user_msg = (
        f"{loc}{occ}として働いている方にお聞きします。"
        f"ご自身のキャリアや仕事の経験について教えてください。"
    )
    return user_msg, example["professional_persona"]

変換テンプレートは 4 種類用意して、1,000 件のサブセットに対してラウンドロビンで適用しました。

変換後のサンプルを 1 つ載せておきます。

[USER]
佐賀県で建設業 中堅として働いている方にお聞きします。
ご自身のキャリアや仕事の経験について教えてください。

[ASSISTANT]
大畠 颯介は、現場の安全と工程の透明性を重視する中堅管理者で、
CAD での簡易設計とモバイルログでのデータ可視化を組み合わせ、
チームに対し計画的かつ信頼性の高い実行を促す。...

ペルソナデータの特徴として、回答が三人称で書かれている点があります。SFT データとしては一人称のほうが自然ですが、今回は「1,000 件の少量データでどこまで効果があるか」を検証する目的なので、変換ロジックはシンプルに留めました。

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

モデルのロードと LoRA 設定

HuggingFace PEFT を使って Nemotron 3 Nano を 4-bit(NF4)量子化でロードし、LoRA アダプターを追加します。

from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig

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

model = AutoModelForCausalLM.from_pretrained(
    "nvidia/NVIDIA-Nemotron-3-Nano-30B-A3B-BF16",
    quantization_config=bnb_config,
    device_map={"": 0},
    torch_dtype=torch.bfloat16,
    trust_remote_code=True,
)

LoRA のターゲットモジュールが今回のポイントです。Nemotron 3 Nano は Transformer 層と Mamba-2 層のハイブリッドなので、両方の投射層を対象にしています。

lora_config = LoraConfig(
    r=16,
    lora_alpha=16,
    target_modules=[
        # Transformer self-attention
        "q_proj", "k_proj", "v_proj", "o_proj",
        # MoE feed-forward
        "gate_proj", "up_proj", "down_proj",
        # Mamba-2 projections
        "in_proj", "out_proj",
    ],
    lora_dropout=0,
    bias="none",
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)

LoRA アダプターの設定を確認してみます。

項目
ランク(r) 16
アルファ 16
ターゲットモジュール数 9(Transformer 7 + Mamba 2)
学習可能パラメータ 441,936,896(全体の 1.38%)
総パラメータ 32,019,874,240

全体の 1.38% だけを学習するわけですが、Transformer 層だけでなく Mamba-2 層の in_projout_proj も対象に含めたのが今回の工夫です。Mamba 層は全 52 層のうち 48 層を占めるので、ここを無視すると学習の影響範囲がかなり限定されてしまいます。

トレーニングの実行

TRL の SFTTrainer を使って 1 エポックの学習を回します。

from trl import SFTConfig, SFTTrainer

sft_config = SFTConfig(
    output_dir="./outputs/nemotron-ft-1k",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    warmup_steps=10,
    num_train_epochs=1,
    learning_rate=2e-4,
    bf16=True,
    logging_steps=10,
    save_steps=100,
    optim="adamw_8bit",
    max_length=2048,
    packing=False,
)

trainer = SFTTrainer(
    model=model,
    processing_class=tokenizer,
    train_dataset=dataset,
    args=sft_config,
)

trainer.train()

学習結果

項目
データ件数 1,000
エポック数 1
総ステップ数 120(実効バッチサイズ 8)
初期 Loss 15.78
最終 Loss 4.72
学習時間 約 81 分
LoRA アダプターサイズ 886MB

Loss の推移を見てみると、最初の 30 ステップで急激に下がり(15.78 → 6.65)、その後は緩やかに収束していく典型的なカーブでした。1,000 件という少量データでもモデルが日本語のパターンを吸収していることが分かります。

評価パイプライン

GGUF 変換と Ollama への読み込み

ここで気になるのが推論方法です。

HuggingFace の model.generate() を使うのが最もシンプルですが、Nemotron 3 Nano の HuggingFace 実装(modeling_nemotron_h.py)には forward pass にいくつかのバグがあり、正しい出力を得ることができませんでした。

そこで、ベースライン評価で使った Ollama(ggml バックエンド)を流用する方針に切り替えました。llama.cpp は NemotronHForCausalLM をネイティブサポートしており、こちらの実装は安定して動作します。

LoRA アダプターを Ollama で使うには、以下の手順で GGUF 形式に変換します。

# llama.cpp リポジトリをクローン
git clone --depth 1 https://github.com/ggml-org/llama.cpp /tmp/llama-cpp

# 変換用の Python 環境を準備
uv venv /tmp/gguf-env
source /tmp/gguf-env/bin/activate
uv pip install numpy sentencepiece gguf safetensors protobuf \
  transformers torch --index-url https://download.pytorch.org/whl/cpu

# LoRA アダプターを GGUF に変換
python /tmp/llama-cpp/convert_lora_to_gguf.py \
  ~/nemotron-ft/outputs/nemotron-ft-1k/lora/ \
  --outfile /tmp/nemotron-ft-1k-lora.gguf \
  --outtype f32

変換後の LoRA GGUF ファイルは 1.77GB でした。324 個のテンソル(各モジュールの LoRA A/B ペア)が含まれています。

次に Ollama でベースモデル + LoRA アダプター を組み合わせた推論用モデルを作ります。

Modelfile
FROM nemotron-3-nano:latest
ADAPTER /tmp/nemotron-ft-1k-lora.gguf
PARAMETER num_ctx 4096
PARAMETER temperature 0
ollama create nemotron-ft-1k -f Modelfile

ファインチューニング後の評価

JCommonsenseQA の前後比較

ベースラインと同じ条件(3-shot、全 1,119 問)で FT 後のモデルを評価しました。

モデル 正答率 正答数 差分
Nemotron 3 Nano(素) 92.5% 1035/1119 -
Nemotron 3 Nano(FT 後) 93.6% 1047/1119 +1.1%(+12 問)

1,000 件という少量のペルソナデータでの QLoRA でも、+1.1% の改善が見られました。

他モデルとの比較

ローカル LLM 記事で紹介した主要モデルと並べてみます。

モデル アクティブパラメータ JCommonsenseQA 備考
Gemma 3 27B 27B 93.9% Dense、140 言語
Nemotron 3 Nano(FT 後) 3.6B 93.6% MoE、日本語 FT
gpt-oss:20b 3.6B 92.7% MoE、OpenAI 初の OSS
Nemotron 3 Nano(素) 3.6B 92.5% MoE、Mamba-2 ハイブリッド
Gemma 3 12B 12B 91.8% Dense、日本語追加学習版あり
Qwen2.5-Coder 32B 32B 90.1% Dense、コード特化
GLM-4.7-Flash 3B 81.9% MoE、MIT ライセンス

FT 後の 93.6% は、27B Dense の Gemma 3 27B(93.9%)まであと 0.3% です。アクティブパラメータ 3.6B でここまで肉薄できているのは面白いですね。ただし後述のとおり、このベンチマークと学習データの相性は良くないので、スコア自体より「FT で壊れていない」ことの確認として見るほうが妥当です。

定性評価で日本語の変化を見る

数値だけでは分からない部分もあるので、FT 前後で同じプロンプトを投げて出力を比較してみました。

日本語の自然さ

プロンプト「東京から大阪への旅行プランを提案してください」

FT 前(BASE)は冒頭で「以下では、「新幹線」と「高速バス」の 2 つの代表的な移動手段をベースに」と始まり、文体がやや硬めです。FT 後は「旅行の目的や時間的余裕に合わせて、いくつかのルートと見どころをご紹介します。」と、より自然な導入になっています。交通手段の比較表も FT 後のほうが「メリット/デメリット」の対比がはっきりしていて読みやすい印象でした。

敬語とビジネス文書

プロンプト「会社の上司に来週の会議の日程変更を依頼するメールを書いてください」

ここで差が出ました。FT 前は本文中に scheduled(スケジュール) と英語がそのまま混入しています。FT 後はすべて日本語で統一されていて、署名ブロック(電話番号・メールアドレス・住所)まで含む完全なビジネスメール形式で出力されました。ペルソナデータに含まれる日本のビジネス慣習が反映されているようです。

日本固有の文化知識

プロンプト「日本のお正月の過ごし方を外国人の友達に説明してください」

FT 前も FT 後もお正月の主要な行事(大掃除、鏡餅、初詣など)を取り上げていますが、説明のスタイルが異なります。FT 前は巨大な表で「何をするか」「なぜ大切か」を一覧にまとめています。FT 後は「大掃除 → 鏡餅の準備 → おせち料理」と時系列で順を追って説明しており、外国人にも伝わりやすい流れです。ただし FT 後の出力ではお正月を「Seollal」(韓国の旧正月)と誤記しており、ペルソナデータで得た知識にも限界があると分かりました。

全体として、FT 後のほうが日本語として自然で、英語の混入が減り、日本のビジネス慣習に沿った出力が得られる傾向がありました。1,000 件のペルソナデータでここまで変化するのは個人的には予想以上です。

学習データとベンチマークの相性

冒頭でも触れたとおり、自分はペルソナ対話データで学習すれば日本語ベンチマーク全般が上がるだろうと思い込んで始めました。実際にはこの見込みは甘くて、JCommonsenseQA は今回の FT 効果を測るベンチマークとしてはあまり適切ではありません。

学習データは「佐賀県の建設業中堅管理者のキャリア描写」「静岡県の食文化に関するペルソナ」といった日本人の生活や職業のテキストです。一方 JCommonsenseQA は「占領地の最高指揮官を何と呼ぶか → 総督」「レアアースと呼ばれる元素は → 希土類元素」のような一般常識の多肢選択問題で、問われる知識の領域がかなり違います。本来なら学習データに合った評価指標を先に決めてから始めるべきでした。

+1.1% の改善は、ペルソナデータが常識推論を直接強化したというより、日本語テキストへの追加学習で読解精度や語彙の守備範囲が広がった間接効果でしょう。1,119 問中 +12 問という差は統計的にも有意か微妙なので、スコアを過度に重視するのは避けたほうが良さそうです。

定性評価で見えた「ビジネスメールの英語混入が消えた」「導入文が自然になった」といった変化のほうが、ペルソナデータによる FT 効果としてはストレートです。JCommonsenseQA はむしろ「FT で常識推論能力を壊していないことの確認」として捉えるのが妥当でしょう。

ハマったポイント

今回の検証ではいくつかの想定外のトラブルがありました。同じことを試す方の参考になればと思い、まとめておきます。

Unsloth が Nemotron 3 Nano に非対応だった

DGX Spark Playbooks では Unsloth を使った QLoRA の手順が紹介されていますが、Nemotron 3 Nano の nemotron_h アーキテクチャ(Mamba-2 + Transformer ハイブリッド MoE)は Unsloth の対応モデルリストに含まれていません。同じハイブリッド系の FalconH1 は Unsloth でサポートされているものの、内部のレイヤー構成が異なるため流用もできませんでした。

結果として HuggingFace PEFT + TRL を直接使う方針に切り替えましたが、コード量としては大きな差はありません。Unsloth の最適化(メモリ効率やトレーニング速度の向上)が使えないのは残念ですが、128GB 統合メモリの DGX Spark であれば素の HuggingFace でもメモリ不足にはなりませんでした。

HuggingFace の Nemotron 3 Nano 推論実装にバグがある

ここが今回一番苦労したポイントです。学習は transformersTrainer で正常に完了するのですが、推論時に model.generate() を呼ぶと出力が壊れます。

原因を追っていくと、HuggingFace の modeling_nemotron_h.py(Nemotron 3 Nano の forward pass を実装しているファイル)に複数のバグがありました。Mamba-2 層の状態管理や MoE のルーティングロジックに問題があり、単一の forward pass でも正しい logits が得られません。

学習自体は loss が下がっているので正常に動いていますが、推論が壊れているため「学習したモデルの出力を確認する」という当たり前のステップでつまずきます。自分は最初これに気づかず、LoRA の設定やデータセットを何度も見直して時間を溶かしました。

merge_and_unload() で重みが壊れる

HuggingFace での推論がダメなら GGUF に変換して Ollama で評価しよう、と考えたのですが、ここにも罠がありました。

一般的な LoRA の GGUF 変換フローは「merge_and_unload() でアダプターをベースモデルに統合 → GGUF に変換」ですが、4-bit QLoRA の場合は merge_and_unload() が NF4 → BF16 の逆量子化を行う過程で精度が大きく劣化し、重みが使い物にならなくなります。変換は正常に完了するのですが、出力はカンマや無関係な単語の羅列になります。

解決策は、llama.cpp の convert_lora_to_gguf.py を使って LoRA アダプターを個別に GGUF 変換し、Ollama の ADAPTER ディレクティブでベースモデルに適用する方法です。このアプローチならベースモデルの重みには一切手を加えないため、量子化による精度劣化が発生しません。

ここにたどり着くまでに丸一日かかりました、、、

まとめ

DGX Spark の 128GB 統合メモリを活かして、Nemotron 3 Nano(31.6B MoE)の日本語ファインチューニングを手元の環境だけで完結できました。

今回の検証結果をまとめます。

項目
ファインチューニング前 JCommonsenseQA 92.5%
ファインチューニング後 JCommonsenseQA 93.6%
改善幅 +1.1%(+12 問)
学習データ 1,000 件(ペルソナ QA)
学習時間 約 81 分

たった 1,000 件のペルソナデータによる QLoRA で、定性評価では英語混入の減少やビジネス文書の品質向上がはっきりと確認できました。JCommonsenseQA でも +1.1% の改善が見られましたが、これはペルソナデータの直接的な効果というよりは、日本語の処理能力が底上げされた間接効果だと考えています。

一方で、Nemotron 3 Nano のエコシステムはまだ発展途上です。Unsloth 非対応、HuggingFace の推論実装のバグ、merge_and_unload() の罠など、「学習は動くけど評価パイプラインを組むのに苦労する」という状況でした。llama.cpp(Ollama)側の実装は安定しているので、評価や推論は GGUF 経由で行うのが現時点での現実解かなと思っています。

1,000 件でこれだけ変化が出たので、100 万件のフルデータセットで学習したらどうなるかは気になります。また、今回の反省として、ペルソナデータで鍛えた対話品質や日本語の自然さを測るなら、JCommonsenseQA のような知識ベンチマークよりコストはかかりますが Japanese MT-Bench のような対話品質評価を最初から組み込んでおくべきでした。。。

同じことを DGX Spark で試してみたい方の参考になれば幸いです。

参考リンク

この記事をシェアする

FacebookHatena blogX

関連記事